1use chrono::{TimeZone, Utc};
6
7use crate::canvas_store::{CanvasWorldStore, UpsertError};
8use crate::canvas_world::{
9 CanvasAssetRef, CanvasLayer, CanvasNorm, CanvasWorldConfig, WorldStratum,
10};
11
12const SYDNEY_LAT: f64 = -33.8688;
14const SYDNEY_LON: f64 = 151.2093;
15
16fn year_mid(y: u32) -> u64 {
24 const FIRST_UNIX: u64 = 0; const YEAR_1970_MID: u64 = 15_778_800;
27 match Utc.with_ymd_and_hms(y as i32, 7, 1, 0, 0, 0).single() {
28 Some(dt) => {
29 let ts = dt.timestamp();
30 if ts >= 0 {
31 ts as u64
32 } else {
33 let y = y.min(1969) as u64;
35 FIRST_UNIX + (y * YEAR_1970_MID) / 1970
36 }
37 }
38 None => y as u64,
39 }
40}
41
42pub fn history_world() -> CanvasWorldConfig {
44 CanvasWorldConfig {
45 id: "q42:world:history-sydney".to_string(),
46 title: "Sydney History (OpenHistoricalMap)".to_string(),
47 temporal_range: Some((1800, 2026)),
48 layer_stack: vec![
49 CanvasLayer::Historical {
50 endpoint: "adapter://openhistoricalmap/v1".to_string(),
51 },
52 CanvasLayer::GeoSpatial {
53 endpoint: "adapter://osm/terrain/sydney".to_string(),
54 stratum: WorldStratum::WorldOfGod,
55 },
56 ],
57 assets: vec![
58 CanvasAssetRef {
59 asset_id: "urn:qualia:asset:history:customs-house".to_string(),
60 lat: Some(-33.8672),
61 lon: Some(151.2113),
62 alt_m: Some(0.0),
63 valid_from: Some(year_mid(1845)),
64 valid_until: None,
65 licence: "CC0".to_string(),
66 },
67 CanvasAssetRef {
68 asset_id: "urn:qualia:asset:history:general-post-office".to_string(),
69 lat: Some(-33.8720),
70 lon: Some(151.2075),
71 alt_m: Some(0.0),
72 valid_from: Some(year_mid(1874)),
73 valid_until: None,
74 licence: "CC0".to_string(),
75 },
76 CanvasAssetRef {
77 asset_id: "urn:qualia:asset:history:town-hall".to_string(),
78 lat: Some(-33.8732),
79 lon: Some(151.2060),
80 alt_m: Some(0.0),
81 valid_from: Some(year_mid(1889)),
82 valid_until: None,
83 licence: "CC0".to_string(),
84 },
85 CanvasAssetRef {
86 asset_id: "urn:qualia:asset:history:harbour-bridge".to_string(),
87 lat: Some(-33.8523),
88 lon: Some(151.2108),
89 alt_m: Some(0.0),
90 valid_from: Some(year_mid(1932)),
91 valid_until: None,
92 licence: "CC0".to_string(),
93 },
94 CanvasAssetRef {
95 asset_id: "urn:qualia:asset:history:opera-house".to_string(),
96 lat: Some(-33.8568),
97 lon: Some(151.2153),
98 alt_m: Some(0.0),
99 valid_from: Some(year_mid(1973)),
100 valid_until: None,
101 licence: "CC0".to_string(),
102 },
103 CanvasAssetRef {
104 asset_id: "urn:qualia:asset:history:kent-brewery".to_string(),
105 lat: Some(-33.8795),
106 lon: Some(151.1948),
107 alt_m: Some(0.0),
108 valid_from: Some(year_mid(1835)),
109 valid_until: Some(year_mid(2005)),
110 licence: "CC0".to_string(),
111 },
112 ],
113 norms: vec![
114 CanvasNorm {
115 rule_uri: "urn:qualia:canvas:public-commons".to_string(),
116 description: "Historical commons read; HGIS attribution required on export"
117 .to_string(),
118 },
119 CanvasNorm {
120 rule_uri: "urn:qualia:canvas:temporal-scrub".to_string(),
121 description: "Spawn/decay governed by valid-time intervals".to_string(),
122 },
123 ],
124 origin_lat: SYDNEY_LAT,
125 origin_lon: SYDNEY_LON,
126 origin_alt_m: 0.0,
127 ..Default::default()
128 }
129}
130
131pub fn biosphere_world() -> CanvasWorldConfig {
133 CanvasWorldConfig {
134 id: "q42:world:biosphere".to_string(),
135 title: "Biosphere (GBIF)".to_string(),
136 temporal_range: None,
137 layer_stack: vec![
138 CanvasLayer::Biosphere {
139 endpoint: "adapter://gbif/v1/occurrence".to_string(),
140 },
141 CanvasLayer::GeoSpatial {
142 endpoint: "adapter://terrain/global-dem".to_string(),
143 stratum: WorldStratum::WorldOfGod,
144 },
145 ],
146 assets: vec![],
147 norms: vec![CanvasNorm {
148 rule_uri: "urn:qualia:canvas:world-of-god".to_string(),
149 description: "Biosphere outputs are Hypothesis under F/A — never ground truth"
150 .to_string(),
151 }],
152 origin_lat: 0.0,
153 origin_lon: 0.0,
154 origin_alt_m: 0.0,
155 ..Default::default()
156 }
157}
158
159pub fn council_world() -> CanvasWorldConfig {
161 CanvasWorldConfig {
162 id: "q42:world:council-sydney".to_string(),
163 title: "Sydney Council Open Data".to_string(),
164 temporal_range: None,
165 layer_stack: vec![
166 CanvasLayer::Council {
167 endpoint: "adapter://council/sydney-open-data/v1".to_string(),
168 },
169 CanvasLayer::GeoSpatial {
170 endpoint: "adapter://osm/buildings/sydney".to_string(),
171 stratum: WorldStratum::WorldOfMan,
172 },
173 ],
174 assets: vec![],
175 norms: vec![CanvasNorm {
176 rule_uri: "urn:qualia:canvas:council-commons".to_string(),
177 description: "Municipal open data; placement requires council placement right"
178 .to_string(),
179 }],
180 origin_lat: SYDNEY_LAT,
181 origin_lon: SYDNEY_LON,
182 origin_alt_m: 0.0,
183 ..Default::default()
184 }
185}
186
187pub fn sdg_world() -> CanvasWorldConfig {
189 CanvasWorldConfig {
190 id: "q42:world:sdg-dashboard".to_string(),
191 title: "SDG Dashboard Walkthrough".to_string(),
192 temporal_range: Some((2015, 2030)),
193 layer_stack: vec![
194 CanvasLayer::Infosphere {
195 endpoint: "adapter://infosphere/sdg-indicators/v1".to_string(),
196 },
197 CanvasLayer::Council {
198 endpoint: "adapter://council/sdg-local-metrics/v1".to_string(),
199 },
200 ],
201 assets: vec![CanvasAssetRef {
202 asset_id: "urn:qualia:asset:sdg:walkthrough-anchor".to_string(),
203 lat: Some(SYDNEY_LAT),
204 lon: Some(SYDNEY_LON),
205 alt_m: Some(0.0),
206 valid_from: Some(year_mid(2015)),
207 valid_until: Some(year_mid(2030)),
208 licence: "CC-BY-4.0".to_string(),
209 }],
210 norms: vec![
211 CanvasNorm {
212 rule_uri: "urn:qualia:canvas:sdg-alignment".to_string(),
213 description: "Constructed-vs-natural interaction metrics for SDG walkthrough"
214 .to_string(),
215 },
216 CanvasNorm {
217 rule_uri: "urn:qualia:canvas:public-commons".to_string(),
218 description: "Indicator dashboards readable under permissive commons".to_string(),
219 },
220 ],
221 origin_lat: SYDNEY_LAT,
222 origin_lon: SYDNEY_LON,
223 origin_alt_m: 0.0,
224 ..Default::default()
225 }
226}
227
228pub fn glam_world() -> CanvasWorldConfig {
230 CanvasWorldConfig {
231 id: "q42:world:glam-commons".to_string(),
232 title: "GLAM Commons Publishing".to_string(),
233 temporal_range: None,
234 layer_stack: vec![
235 CanvasLayer::Infosphere {
236 endpoint: "adapter://glam/iiif-manifests/v1".to_string(),
237 },
238 CanvasLayer::Historical {
239 endpoint: "adapter://hgis/world-historical-gazetteer/v1".to_string(),
240 },
241 ],
242 assets: vec![CanvasAssetRef {
243 asset_id: "urn:qualia:asset:glam:sample-map-tile".to_string(),
244 lat: Some(SYDNEY_LAT),
245 lon: Some(SYDNEY_LON),
246 alt_m: Some(0.0),
247 valid_from: None,
248 valid_until: None,
249 licence: "CC0".to_string(),
250 }],
251 norms: vec![CanvasNorm {
252 rule_uri: "urn:qualia:canvas:glam-stewardship".to_string(),
253 description: "GLAM holdings explorable; export requires provenance sidecar".to_string(),
254 }],
255 origin_lat: SYDNEY_LAT,
256 origin_lon: SYDNEY_LON,
257 origin_alt_m: 0.0,
258 ..Default::default()
259 }
260}
261
262pub fn all_flagship_worlds() -> Vec<CanvasWorldConfig> {
264 vec![
265 history_world(),
266 biosphere_world(),
267 council_world(),
268 sdg_world(),
269 glam_world(),
270 ]
271}
272
273pub fn seed_all_flagships(store: &CanvasWorldStore, now_unix: u64) -> std::io::Result<usize> {
275 let existing: std::collections::HashSet<String> =
276 store.list()?.into_iter().map(|w| w.id).collect();
277 let mut seeded = 0usize;
278 for config in all_flagship_worlds() {
279 if existing.contains(&config.id) {
280 continue;
281 }
282 store.upsert(config, now_unix).map_err(|e| match e {
283 UpsertError::Io(e) => e,
284 UpsertError::Invalid(err) => std::io::Error::other(err.to_string()),
285 })?;
286 seeded += 1;
287 }
288 Ok(seeded)
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use std::collections::HashSet;
295 use std::fs;
296
297 #[test]
298 fn year_mid_preserves_historical_order() {
299 let a = year_mid(1835);
300 let b = year_mid(2005);
301 assert!(a <= b, "1835 mid ({a}) must be <= 2005 mid ({b})");
302 let c = year_mid(1973);
303 let d = year_mid(2015);
304 assert!(c <= d);
305 }
306
307 #[test]
308 fn flagship_worlds_validate_five_distinct_ids() {
309 let worlds = all_flagship_worlds();
310 assert_eq!(worlds.len(), 5, "expected five flagship worlds");
311 let ids: HashSet<_> = worlds.iter().map(|w| w.id.as_str()).collect();
312 assert_eq!(ids.len(), 5, "flagship world ids must be distinct");
313 for world in &worlds {
314 world.validate().expect("flagship world should validate");
315 }
316 }
317
318 #[test]
319 fn seed_all_flagships_upserts_only_missing() {
320 let dir = std::env::temp_dir().join(format!("chora-flagship-{}", std::process::id()));
321 let _ = fs::remove_dir_all(&dir);
322 let store = CanvasWorldStore::open(&dir).unwrap();
323
324 let first = seed_all_flagships(&store, 1_700_000_000).unwrap();
325 assert_eq!(first, 5);
326 assert_eq!(store.list().unwrap().len(), 5);
327
328 let second = seed_all_flagships(&store, 1_700_000_100).unwrap();
329 assert_eq!(second, 0);
330 assert_eq!(store.list().unwrap().len(), 5);
331
332 let _ = fs::remove_dir_all(&dir);
333 }
334}