1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum WorldStratum {
12 WorldOfMan,
14 WorldOfGod,
16 Infosphere,
18 Sociosphere,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub enum CanvasLayer {
25 GeoSpatial {
26 endpoint: String,
27 stratum: WorldStratum,
28 },
29 Council {
30 endpoint: String,
31 },
32 Historical {
33 endpoint: String,
34 },
35 Celestial {
36 endpoint: String,
37 },
38 Infosphere {
39 endpoint: String,
40 },
41 MicroScale {
42 endpoint: String,
43 },
44 Biosphere {
45 endpoint: String,
46 },
47 Custom {
48 name: String,
49 endpoint: String,
50 stratum: WorldStratum,
51 },
52}
53
54impl CanvasLayer {
55 pub fn endpoint(&self) -> &str {
56 match self {
57 Self::GeoSpatial { endpoint, .. }
58 | Self::Council { endpoint }
59 | Self::Historical { endpoint }
60 | Self::Celestial { endpoint }
61 | Self::Infosphere { endpoint }
62 | Self::MicroScale { endpoint }
63 | Self::Biosphere { endpoint }
64 | Self::Custom { endpoint, .. } => endpoint,
65 }
66 }
67
68 pub fn stratum(&self) -> WorldStratum {
69 match self {
70 Self::GeoSpatial { stratum, .. } | Self::Custom { stratum, .. } => *stratum,
71 Self::Council { .. } | Self::Historical { .. } => WorldStratum::WorldOfMan,
72 Self::Celestial { .. } | Self::Biosphere { .. } | Self::MicroScale { .. } => {
73 WorldStratum::WorldOfGod
74 }
75 Self::Infosphere { .. } => WorldStratum::Infosphere,
76 }
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82pub struct CanvasNorm {
83 pub rule_uri: String,
84 #[serde(default)]
85 pub description: String,
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90pub struct CanvasAssetRef {
91 pub asset_id: String,
93 #[serde(default)]
95 pub lat: Option<f64>,
96 #[serde(default)]
97 pub lon: Option<f64>,
98 #[serde(default)]
99 pub alt_m: Option<f64>,
100 #[serde(default)]
102 pub valid_from: Option<u64>,
103 #[serde(default)]
104 pub valid_until: Option<u64>,
105 pub licence: String,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct CanvasWorldConfig {
112 pub id: String,
114 pub title: String,
116 pub temporal_range: Option<(u64, u64)>,
118 pub layer_stack: Vec<CanvasLayer>,
120 #[serde(default)]
122 pub assets: Vec<CanvasAssetRef>,
123 #[serde(default)]
125 pub permitted_nquins: Vec<u64>,
126 #[serde(default)]
128 pub norms: Vec<CanvasNorm>,
129 #[serde(default)]
131 pub origin_lat: f64,
132 #[serde(default)]
133 pub origin_lon: f64,
134 #[serde(default)]
135 pub origin_alt_m: f64,
136}
137
138impl Default for CanvasWorldConfig {
139 fn default() -> Self {
140 Self {
141 id: "q42:world:default".to_string(),
142 title: "Default World".to_string(),
143 temporal_range: None,
144 layer_stack: vec![],
145 assets: vec![],
146 permitted_nquins: vec![],
147 norms: vec![],
148 origin_lat: -33.8688,
149 origin_lon: 151.2093,
150 origin_alt_m: 0.0,
151 }
152 }
153}
154
155#[derive(Debug, PartialEq, Eq)]
156pub enum WorldConfigError {
157 EmptyId,
158 InvertedTemporalRange,
159 AssetMissingLicence { asset_id: String },
160 AssetInvertedValidity { asset_id: String },
161}
162
163impl std::fmt::Display for WorldConfigError {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 match self {
166 Self::EmptyId => write!(f, "world id must not be empty"),
167 Self::InvertedTemporalRange => write!(f, "temporal_range start must be <= end"),
168 Self::AssetMissingLicence { asset_id } => {
169 write!(f, "asset {asset_id} requires a licence")
170 }
171 Self::AssetInvertedValidity { asset_id } => {
172 write!(f, "asset {asset_id} valid_from must be <= valid_until")
173 }
174 }
175 }
176}
177impl std::error::Error for WorldConfigError {}
178
179impl CanvasWorldConfig {
180 pub fn validate(&self) -> Result<(), WorldConfigError> {
182 if self.id.trim().is_empty() {
183 return Err(WorldConfigError::EmptyId);
184 }
185 if let Some((t0, t1)) = self.temporal_range {
186 if t0 > t1 {
187 return Err(WorldConfigError::InvertedTemporalRange);
188 }
189 }
190 for asset in &self.assets {
191 if asset.licence.trim().is_empty() {
192 return Err(WorldConfigError::AssetMissingLicence {
193 asset_id: asset.asset_id.clone(),
194 });
195 }
196 if let (Some(vf), Some(vu)) = (asset.valid_from, asset.valid_until) {
197 if vf > vu {
198 return Err(WorldConfigError::AssetInvertedValidity {
199 asset_id: asset.asset_id.clone(),
200 });
201 }
202 }
203 }
204 Ok(())
205 }
206
207 pub fn seed_demo() -> Self {
209 Self {
210 id: "q42:world:demo-offline".to_string(),
211 title: "Chora Demo (offline)".to_string(),
212 temporal_range: Some((1_700_000_000, 1_900_000_000)),
213 layer_stack: vec![CanvasLayer::GeoSpatial {
214 endpoint: "local://terrain/demo".to_string(),
215 stratum: WorldStratum::WorldOfGod,
216 }],
217 assets: vec![CanvasAssetRef {
218 asset_id: "local://assets/demo-tile.10d".to_string(),
219 lat: Some(-33.8688),
220 lon: Some(151.2093),
221 alt_m: Some(0.0),
222 valid_from: Some(1_700_000_000),
223 valid_until: None,
224 licence: "CC0".to_string(),
225 }],
226 norms: vec![CanvasNorm {
227 rule_uri: "urn:qualia:canvas:public-commons".to_string(),
228 description: "Permissive-commons read; planting requires placement right"
229 .to_string(),
230 }],
231 origin_lat: -33.8688,
232 origin_lon: 151.2093,
233 origin_alt_m: 0.0,
234 ..Default::default()
235 }
236 }
237
238 pub fn layer_endpoints(&self) -> Vec<&str> {
240 let mut seen = std::collections::HashSet::new();
241 let mut out = Vec::new();
242 for layer in &self.layer_stack {
243 let ep = layer.endpoint();
244 if seen.insert(ep) {
245 out.push(ep);
246 }
247 }
248 out
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn demo_world_validates() {
258 let w = CanvasWorldConfig::seed_demo();
259 w.validate().unwrap();
260 assert_eq!(w.layer_stack.len(), 1);
261 assert_eq!(w.assets[0].licence, "CC0");
262 }
263
264 #[test]
265 fn rejects_inverted_temporal_range() {
266 let mut w = CanvasWorldConfig::default();
267 w.temporal_range = Some((2000, 1000));
268 assert_eq!(w.validate(), Err(WorldConfigError::InvertedTemporalRange));
269 }
270
271 #[test]
272 fn layer_endpoints_deduplicated() {
273 let w = CanvasWorldConfig {
274 layer_stack: vec![
275 CanvasLayer::GeoSpatial {
276 endpoint: "a".into(),
277 stratum: WorldStratum::WorldOfGod,
278 },
279 CanvasLayer::Council {
280 endpoint: "b".into(),
281 },
282 CanvasLayer::GeoSpatial {
283 endpoint: "a".into(),
284 stratum: WorldStratum::WorldOfGod,
285 },
286 ],
287 ..Default::default()
288 };
289 assert_eq!(w.layer_endpoints(), vec!["a", "b"]);
290 }
291}