Skip to main content

epsg_utils/projjson/
writer.rs

1//! Serialization of parsed WKT2 types to PROJJSON (JSON encoding of WKT2:2019).
2//!
3//! See <https://proj.org/en/stable/specifications/projjson.html> for the specification
4//! and <https://proj.org/en/latest/schemas/v0.7/projjson.schema.json> for the schema.
5
6use serde_json::{Map, Value, json};
7
8use crate::crs::*;
9
10// ---------------------------------------------------------------------------
11// Public API
12// ---------------------------------------------------------------------------
13
14impl Crs {
15    /// Serialize this CRS to a PROJJSON `serde_json::Value`.
16    pub fn to_projjson(&self) -> Value {
17        match self {
18            Crs::ProjectedCrs(crs) => crs.to_projjson(),
19            Crs::GeogCrs(crs) => crs.to_projjson(),
20            Crs::GeodCrs(crs) => crs.to_projjson(),
21            Crs::VertCrs(crs) => crs.to_projjson(),
22            Crs::CompoundCrs(crs) => crs.to_projjson(),
23        }
24    }
25}
26
27impl ProjectedCrs {
28    /// Serialize this projected CRS to a PROJJSON `serde_json::Value`.
29    pub fn to_projjson(&self) -> Value {
30        let mut obj = Map::new();
31        insert_schema(&mut obj);
32        obj.insert("type".into(), json!("ProjectedCRS"));
33        obj.insert("name".into(), json!(self.name));
34        obj.insert("base_crs".into(), base_crs_to_json(&self.base_geodetic_crs));
35        obj.insert(
36            "conversion".into(),
37            conversion_to_json(&self.map_projection),
38        );
39        obj.insert(
40            "coordinate_system".into(),
41            cs_to_json(&self.coordinate_system),
42        );
43
44        insert_usages(&mut obj, &self.usages);
45        if let Some(ref remark) = self.remark {
46            obj.insert("remarks".into(), json!(remark));
47        }
48        insert_ids(&mut obj, &self.identifiers);
49
50        Value::Object(obj)
51    }
52}
53
54impl GeogCrs {
55    /// Serialize this geographic CRS to a PROJJSON `serde_json::Value`.
56    pub fn to_projjson(&self) -> Value {
57        let mut obj = Map::new();
58        insert_schema(&mut obj);
59        obj.insert("type".into(), json!("GeographicCRS"));
60        obj.insert("name".into(), json!(self.name));
61
62        match &self.datum {
63            Datum::ReferenceFrame(rf) => {
64                obj.insert("datum".into(), datum_to_json(rf, self.dynamic.as_ref()));
65            }
66            Datum::Ensemble(ens) => {
67                obj.insert("datum_ensemble".into(), ensemble_to_json(ens));
68            }
69        }
70
71        if let Some(ref dynamic) = self.dynamic
72            && let Some(ref model) = dynamic.deformation_model
73        {
74            let mut m = Map::new();
75            m.insert("name".into(), json!(model.name));
76            insert_ids(&mut m, &model.identifiers);
77            obj.insert("deformation_models".into(), json!([Value::Object(m)]));
78        }
79
80        obj.insert(
81            "coordinate_system".into(),
82            cs_to_json(&self.coordinate_system),
83        );
84
85        insert_usages(&mut obj, &self.usages);
86        if let Some(ref remark) = self.remark {
87            obj.insert("remarks".into(), json!(remark));
88        }
89        insert_ids(&mut obj, &self.identifiers);
90
91        Value::Object(obj)
92    }
93}
94
95impl GeodCrs {
96    /// Serialize this geodetic CRS to a PROJJSON `serde_json::Value`.
97    pub fn to_projjson(&self) -> Value {
98        let mut obj = Map::new();
99        insert_schema(&mut obj);
100        obj.insert("type".into(), json!("GeodeticCRS"));
101        obj.insert("name".into(), json!(self.name));
102
103        match &self.datum {
104            Datum::ReferenceFrame(rf) => {
105                obj.insert("datum".into(), datum_to_json(rf, self.dynamic.as_ref()));
106            }
107            Datum::Ensemble(ens) => {
108                obj.insert("datum_ensemble".into(), ensemble_to_json(ens));
109            }
110        }
111
112        if let Some(ref dynamic) = self.dynamic
113            && let Some(ref model) = dynamic.deformation_model
114        {
115            let mut m = Map::new();
116            m.insert("name".into(), json!(model.name));
117            insert_ids(&mut m, &model.identifiers);
118            obj.insert("deformation_models".into(), json!([Value::Object(m)]));
119        }
120
121        obj.insert(
122            "coordinate_system".into(),
123            cs_to_json(&self.coordinate_system),
124        );
125
126        insert_usages(&mut obj, &self.usages);
127        if let Some(ref remark) = self.remark {
128            obj.insert("remarks".into(), json!(remark));
129        }
130        insert_ids(&mut obj, &self.identifiers);
131
132        Value::Object(obj)
133    }
134}
135
136impl VertCrs {
137    /// Serialize this vertical CRS to a PROJJSON `serde_json::Value`.
138    pub fn to_projjson(&self) -> Value {
139        let mut obj = Map::new();
140        insert_schema(&mut obj);
141
142        match &self.source {
143            VertCrsSource::Datum { dynamic, datum } => {
144                obj.insert("type".into(), json!("VerticalCRS"));
145                obj.insert("name".into(), json!(self.name));
146
147                match datum {
148                    VerticalDatum::ReferenceFrame(rf) => {
149                        obj.insert("datum".into(), vertical_datum_to_json(rf, dynamic.as_ref()));
150                    }
151                    VerticalDatum::Ensemble(ens) => {
152                        obj.insert("datum_ensemble".into(), ensemble_to_json(ens));
153                    }
154                }
155
156                if let Some(dynamic) = dynamic
157                    && let Some(ref model) = dynamic.deformation_model
158                {
159                    let mut m = Map::new();
160                    m.insert("name".into(), json!(model.name));
161                    insert_ids(&mut m, &model.identifiers);
162                    obj.insert("deformation_models".into(), json!([Value::Object(m)]));
163                }
164            }
165            VertCrsSource::Derived {
166                base_vert_crs,
167                deriving_conversion,
168            } => {
169                obj.insert("type".into(), json!("DerivedVerticalCRS"));
170                obj.insert("name".into(), json!(self.name));
171                obj.insert("base_crs".into(), base_vert_crs_to_json(base_vert_crs));
172                obj.insert("conversion".into(), conversion_to_json(deriving_conversion));
173            }
174        }
175
176        obj.insert(
177            "coordinate_system".into(),
178            cs_to_json(&self.coordinate_system),
179        );
180
181        if !self.geoid_models.is_empty() {
182            let models: Vec<Value> = self
183                .geoid_models
184                .iter()
185                .map(|gm| {
186                    let mut m = Map::new();
187                    m.insert("name".into(), json!(gm.name));
188                    insert_ids(&mut m, &gm.identifiers);
189                    Value::Object(m)
190                })
191                .collect();
192            obj.insert("geoid_models".into(), Value::Array(models));
193        }
194
195        insert_usages(&mut obj, &self.usages);
196        if let Some(ref remark) = self.remark {
197            obj.insert("remarks".into(), json!(remark));
198        }
199        insert_ids(&mut obj, &self.identifiers);
200
201        Value::Object(obj)
202    }
203}
204
205impl CompoundCrs {
206    /// Serialize this compound CRS to a PROJJSON `serde_json::Value`.
207    pub fn to_projjson(&self) -> Value {
208        let mut obj = Map::new();
209        insert_schema(&mut obj);
210        obj.insert("type".into(), json!("CompoundCRS"));
211        obj.insert("name".into(), json!(self.name));
212
213        let components: Vec<Value> = self
214            .components
215            .iter()
216            .map(|c| match c {
217                SingleCrs::ProjectedCrs(crs) => crs.to_projjson(),
218                SingleCrs::GeogCrs(crs) => crs.to_projjson(),
219                SingleCrs::GeodCrs(crs) => crs.to_projjson(),
220                SingleCrs::VertCrs(crs) => crs.to_projjson(),
221                SingleCrs::Other(raw) => json!({ "type": "Unknown", "wkt": raw }),
222            })
223            .collect();
224        obj.insert("components".into(), Value::Array(components));
225
226        insert_usages(&mut obj, &self.usages);
227        if let Some(ref remark) = self.remark {
228            obj.insert("remarks".into(), json!(remark));
229        }
230        insert_ids(&mut obj, &self.identifiers);
231
232        Value::Object(obj)
233    }
234}
235
236fn insert_schema(obj: &mut Map<String, Value>) {
237    obj.insert(
238        "$schema".into(),
239        json!("https://proj.org/schemas/v0.7/projjson.schema.json"),
240    );
241}
242
243// ---------------------------------------------------------------------------
244// Base CRS
245// ---------------------------------------------------------------------------
246
247fn base_crs_to_json(base: &BaseGeodeticCrs) -> Value {
248    let mut obj = Map::new();
249
250    let crs_type = match base.keyword {
251        BaseGeodeticCrsKeyword::BaseGeogCrs => "GeographicCRS",
252        BaseGeodeticCrsKeyword::BaseGeodCrs => "GeodeticCRS",
253    };
254    obj.insert("type".into(), json!(crs_type));
255    obj.insert("name".into(), json!(base.name));
256
257    match &base.datum {
258        Datum::ReferenceFrame(rf) => {
259            obj.insert("datum".into(), datum_to_json(rf, base.dynamic.as_ref()));
260        }
261        Datum::Ensemble(ens) => {
262            obj.insert("datum_ensemble".into(), ensemble_to_json(ens));
263        }
264    }
265
266    // deformation_models live on the CRS in PROJJSON, not inside the datum
267    if let Some(ref dynamic) = base.dynamic
268        && let Some(ref model) = dynamic.deformation_model
269    {
270        let mut m = Map::new();
271        m.insert("name".into(), json!(model.name));
272        insert_ids(&mut m, &model.identifiers);
273        obj.insert("deformation_models".into(), json!([Value::Object(m)]));
274    }
275
276    // base_crs in a ProjectedCRS typically doesn't have its own coordinate_system
277    // in PROJJSON output (it's implied). But if we have ellipsoidal_cs_unit, we could
278    // emit one. For now, omit it as PROJ itself does for projected CRS base_crs.
279
280    insert_ids(&mut obj, &base.identifiers);
281
282    Value::Object(obj)
283}
284
285fn base_vert_crs_to_json(base: &BaseVertCrs) -> Value {
286    let mut obj = Map::new();
287    obj.insert("type".into(), json!("VerticalCRS"));
288    obj.insert("name".into(), json!(base.name));
289
290    match &base.datum {
291        VerticalDatum::ReferenceFrame(rf) => {
292            obj.insert(
293                "datum".into(),
294                vertical_datum_to_json(rf, base.dynamic.as_ref()),
295            );
296        }
297        VerticalDatum::Ensemble(ens) => {
298            obj.insert("datum_ensemble".into(), ensemble_to_json(ens));
299        }
300    }
301
302    if let Some(ref dynamic) = base.dynamic
303        && let Some(ref model) = dynamic.deformation_model
304    {
305        let mut m = Map::new();
306        m.insert("name".into(), json!(model.name));
307        insert_ids(&mut m, &model.identifiers);
308        obj.insert("deformation_models".into(), json!([Value::Object(m)]));
309    }
310
311    insert_ids(&mut obj, &base.identifiers);
312
313    Value::Object(obj)
314}
315
316// ---------------------------------------------------------------------------
317// Datum
318// ---------------------------------------------------------------------------
319
320fn datum_to_json(rf: &GeodeticReferenceFrame, dynamic: Option<&DynamicCrs>) -> Value {
321    let mut obj = Map::new();
322
323    if let Some(dyn_crs) = dynamic {
324        obj.insert("type".into(), json!("DynamicGeodeticReferenceFrame"));
325        obj.insert(
326            "frame_reference_epoch".into(),
327            json!(dyn_crs.frame_reference_epoch),
328        );
329    } else {
330        obj.insert("type".into(), json!("GeodeticReferenceFrame"));
331    }
332
333    obj.insert("name".into(), json!(rf.name));
334
335    if let Some(ref anchor) = rf.anchor {
336        obj.insert("anchor".into(), json!(anchor));
337    }
338    if let Some(epoch) = rf.anchor_epoch {
339        obj.insert("anchor_epoch".into(), json!(epoch));
340    }
341
342    obj.insert("ellipsoid".into(), ellipsoid_to_json(&rf.ellipsoid));
343
344    if let Some(ref pm) = rf.prime_meridian {
345        obj.insert("prime_meridian".into(), prime_meridian_to_json(pm));
346    }
347
348    insert_ids(&mut obj, &rf.identifiers);
349
350    Value::Object(obj)
351}
352
353fn vertical_datum_to_json(rf: &VerticalReferenceFrame, dynamic: Option<&DynamicCrs>) -> Value {
354    let mut obj = Map::new();
355
356    if let Some(dyn_crs) = dynamic {
357        obj.insert("type".into(), json!("DynamicVerticalReferenceFrame"));
358        obj.insert(
359            "frame_reference_epoch".into(),
360            json!(dyn_crs.frame_reference_epoch),
361        );
362    } else {
363        obj.insert("type".into(), json!("VerticalReferenceFrame"));
364    }
365
366    obj.insert("name".into(), json!(rf.name));
367
368    if let Some(ref anchor) = rf.anchor {
369        obj.insert("anchor".into(), json!(anchor));
370    }
371    if let Some(epoch) = rf.anchor_epoch {
372        obj.insert("anchor_epoch".into(), json!(epoch));
373    }
374
375    insert_ids(&mut obj, &rf.identifiers);
376
377    Value::Object(obj)
378}
379
380fn ensemble_to_json(ens: &DatumEnsemble) -> Value {
381    let mut obj = Map::new();
382    obj.insert("type".into(), json!("DatumEnsemble"));
383    obj.insert("name".into(), json!(ens.name));
384
385    let members: Vec<Value> = ens
386        .members
387        .iter()
388        .map(|m| {
389            let mut mobj = Map::new();
390            mobj.insert("name".into(), json!(m.name));
391            insert_ids(&mut mobj, &m.identifiers);
392            Value::Object(mobj)
393        })
394        .collect();
395    obj.insert("members".into(), Value::Array(members));
396
397    if let Some(ref ellipsoid) = ens.ellipsoid {
398        obj.insert("ellipsoid".into(), ellipsoid_to_json(ellipsoid));
399    }
400
401    // PROJJSON schema says accuracy is a string
402    obj.insert("accuracy".into(), json!(ens.accuracy.to_string()));
403
404    insert_ids(&mut obj, &ens.identifiers);
405
406    Value::Object(obj)
407}
408
409// ---------------------------------------------------------------------------
410// Ellipsoid & Prime Meridian
411// ---------------------------------------------------------------------------
412
413fn ellipsoid_to_json(e: &Ellipsoid) -> Value {
414    let mut obj = Map::new();
415    obj.insert("name".into(), json!(e.name));
416    obj.insert(
417        "semi_major_axis".into(),
418        value_with_optional_unit(e.semi_major_axis, e.unit.as_ref()),
419    );
420    obj.insert("inverse_flattening".into(), json!(e.inverse_flattening));
421    insert_ids(&mut obj, &e.identifiers);
422    Value::Object(obj)
423}
424
425fn prime_meridian_to_json(pm: &PrimeMeridian) -> Value {
426    let mut obj = Map::new();
427    obj.insert("name".into(), json!(pm.name));
428    obj.insert(
429        "longitude".into(),
430        value_with_optional_unit(pm.irm_longitude, pm.unit.as_ref()),
431    );
432    insert_ids(&mut obj, &pm.identifiers);
433    Value::Object(obj)
434}
435
436/// If the unit is present and not the default for the context, emit `{"value": n, "unit": ...}`.
437/// Otherwise emit just the number.
438fn value_with_optional_unit(value: f64, unit: Option<&Unit>) -> Value {
439    match unit {
440        Some(u) => {
441            json!({
442                "value": value,
443                "unit": unit_to_json(u),
444            })
445        }
446        None => json!(value),
447    }
448}
449
450// ---------------------------------------------------------------------------
451// Map Projection (Conversion)
452// ---------------------------------------------------------------------------
453
454fn conversion_to_json(mp: &MapProjection) -> Value {
455    let mut obj = Map::new();
456    obj.insert("name".into(), json!(mp.name));
457    obj.insert("method".into(), method_to_json(&mp.method));
458
459    if !mp.parameters.is_empty() {
460        let params: Vec<Value> = mp.parameters.iter().map(parameter_to_json).collect();
461        obj.insert("parameters".into(), Value::Array(params));
462    }
463
464    insert_ids(&mut obj, &mp.identifiers);
465    Value::Object(obj)
466}
467
468fn method_to_json(m: &MapProjectionMethod) -> Value {
469    let mut obj = Map::new();
470    obj.insert("name".into(), json!(m.name));
471    insert_ids(&mut obj, &m.identifiers);
472    Value::Object(obj)
473}
474
475fn parameter_to_json(p: &MapProjectionParameter) -> Value {
476    let mut obj = Map::new();
477    obj.insert("name".into(), json!(p.name));
478    obj.insert("value".into(), json!(p.value));
479    if let Some(ref unit) = p.unit {
480        obj.insert("unit".into(), unit_to_json(unit));
481    }
482    insert_ids(&mut obj, &p.identifiers);
483    Value::Object(obj)
484}
485
486// ---------------------------------------------------------------------------
487// Coordinate System
488// ---------------------------------------------------------------------------
489
490fn cs_to_json(cs: &CoordinateSystem) -> Value {
491    let mut obj = Map::new();
492    obj.insert("subtype".into(), json!(cs.cs_type.to_string()));
493
494    let axes: Vec<Value> = cs
495        .axes
496        .iter()
497        .map(|a| axis_to_json(a, cs.cs_unit.as_ref()))
498        .collect();
499    obj.insert("axis".into(), Value::Array(axes));
500
501    insert_ids(&mut obj, &cs.identifiers);
502    Value::Object(obj)
503}
504
505fn axis_to_json(axis: &Axis, cs_unit: Option<&Unit>) -> Value {
506    let mut obj = Map::new();
507
508    let (name, abbreviation) = split_axis_name_abbrev(&axis.name_abbrev);
509    obj.insert("name".into(), json!(name));
510    obj.insert("abbreviation".into(), json!(abbreviation));
511    obj.insert("direction".into(), json!(axis.direction));
512
513    // Unit: prefer axis-level unit, fall back to CS-level unit
514    let effective_unit = axis.unit.as_ref().or(cs_unit);
515    if let Some(unit) = effective_unit {
516        obj.insert("unit".into(), unit_to_json(unit));
517    }
518
519    if let Some(ref meridian) = axis.meridian {
520        obj.insert("meridian".into(), meridian_to_json(meridian));
521    }
522
523    if let Some(min) = axis.axis_min_value {
524        obj.insert("minimum_value".into(), json!(min));
525    }
526    if let Some(max) = axis.axis_max_value {
527        obj.insert("maximum_value".into(), json!(max));
528    }
529    if let Some(rm) = axis.range_meaning {
530        obj.insert(
531            "range_meaning".into(),
532            json!(match rm {
533                RangeMeaning::Exact => "exact",
534                RangeMeaning::Wraparound => "wraparound",
535            }),
536        );
537    }
538
539    insert_ids(&mut obj, &axis.identifiers);
540    Value::Object(obj)
541}
542
543fn meridian_to_json(m: &Meridian) -> Value {
544    json!({
545        "longitude": m.value,
546        "unit": unit_to_json(&m.unit),
547    })
548}
549
550/// Split "easting (E)" into ("easting", "E").
551/// If no parenthesized abbreviation, use the full string as name and empty abbreviation.
552fn split_axis_name_abbrev(name_abbrev: &str) -> (&str, &str) {
553    if let Some(paren_start) = name_abbrev.rfind('(')
554        && let Some(paren_end) = name_abbrev[paren_start..].find(')')
555    {
556        let name = name_abbrev[..paren_start].trim();
557        let abbrev = &name_abbrev[paren_start + 1..paren_start + paren_end];
558        return (name, abbrev);
559    }
560    (name_abbrev, "")
561}
562
563// ---------------------------------------------------------------------------
564// Unit
565// ---------------------------------------------------------------------------
566
567fn unit_to_json(unit: &Unit) -> Value {
568    // Use shorthand for well-known units
569    match (unit.name.as_str(), unit.conversion_factor) {
570        ("metre", Some(1.0)) => return json!("metre"),
571        ("degree", Some(f)) if (f - 0.0174532925199433).abs() < 1e-15 => {
572            return json!("degree");
573        }
574        ("unity", Some(1.0)) => return json!("unity"),
575        _ => {}
576    }
577
578    let mut obj = Map::new();
579    let type_str = match unit.keyword {
580        UnitKeyword::AngleUnit => "AngularUnit",
581        UnitKeyword::LengthUnit => "LinearUnit",
582        UnitKeyword::ScaleUnit => "ScaleUnit",
583        UnitKeyword::TimeUnit => "TimeUnit",
584        UnitKeyword::ParametricUnit => "ParametricUnit",
585        UnitKeyword::Unit => "Unit",
586    };
587    obj.insert("type".into(), json!(type_str));
588    obj.insert("name".into(), json!(unit.name));
589    if let Some(factor) = unit.conversion_factor {
590        obj.insert("conversion_factor".into(), json!(factor));
591    }
592    insert_ids(&mut obj, &unit.identifiers);
593    Value::Object(obj)
594}
595
596// ---------------------------------------------------------------------------
597// Identifier
598// ---------------------------------------------------------------------------
599
600fn id_to_json(id: &Identifier) -> Value {
601    let mut obj = Map::new();
602    obj.insert("authority".into(), json!(id.authority_name));
603    match &id.authority_unique_id {
604        AuthorityId::Number(n) => {
605            // Emit as integer if it's a whole number
606            if *n == (*n as i64) as f64 {
607                obj.insert("code".into(), json!(*n as i64));
608            } else {
609                obj.insert("code".into(), json!(n));
610            }
611        }
612        AuthorityId::Text(s) => {
613            obj.insert("code".into(), json!(s));
614        }
615    }
616    if let Some(ref version) = id.version {
617        match version {
618            AuthorityId::Number(n) => obj.insert("version".into(), json!(n)),
619            AuthorityId::Text(s) => obj.insert("version".into(), json!(s)),
620        };
621    }
622    if let Some(ref citation) = id.citation {
623        obj.insert("authority_citation".into(), json!(citation));
624    }
625    if let Some(ref uri) = id.uri {
626        obj.insert("uri".into(), json!(uri));
627    }
628    Value::Object(obj)
629}
630
631/// Insert `id` (single) or `ids` (multiple) into a JSON object map.
632fn insert_ids(obj: &mut Map<String, Value>, identifiers: &[Identifier]) {
633    match identifiers.len() {
634        0 => {}
635        1 => {
636            obj.insert("id".into(), id_to_json(&identifiers[0]));
637        }
638        _ => {
639            let ids: Vec<Value> = identifiers.iter().map(id_to_json).collect();
640            obj.insert("ids".into(), Value::Array(ids));
641        }
642    }
643}
644
645// ---------------------------------------------------------------------------
646// Usage & Extent
647// ---------------------------------------------------------------------------
648
649fn insert_usages(obj: &mut Map<String, Value>, usages: &[Usage]) {
650    if usages.is_empty() {
651        return;
652    }
653
654    // Use the structured `usages` array form
655    let arr: Vec<Value> = usages.iter().map(usage_to_json).collect();
656    obj.insert("usages".into(), Value::Array(arr));
657}
658
659fn usage_to_json(u: &Usage) -> Value {
660    let mut obj = Map::new();
661    obj.insert("scope".into(), json!(u.scope));
662    if let Some(ref area) = u.area {
663        obj.insert("area".into(), json!(area));
664    }
665    if let Some(ref bbox) = u.bbox {
666        obj.insert("bbox".into(), bbox_to_json(bbox));
667    }
668    if let Some(ref ve) = u.vertical_extent {
669        obj.insert("vertical_extent".into(), vertical_extent_to_json(ve));
670    }
671    if let Some(ref te) = u.temporal_extent {
672        obj.insert("temporal_extent".into(), temporal_extent_to_json(te));
673    }
674    Value::Object(obj)
675}
676
677fn bbox_to_json(bbox: &BBox) -> Value {
678    json!({
679        "south_latitude": bbox.lower_left_latitude,
680        "west_longitude": bbox.lower_left_longitude,
681        "north_latitude": bbox.upper_right_latitude,
682        "east_longitude": bbox.upper_right_longitude,
683    })
684}
685
686fn vertical_extent_to_json(ve: &VerticalExtent) -> Value {
687    let mut obj = Map::new();
688    obj.insert("minimum".into(), json!(ve.minimum_height));
689    obj.insert("maximum".into(), json!(ve.maximum_height));
690    if let Some(ref unit) = ve.unit {
691        obj.insert("unit".into(), unit_to_json(unit));
692    }
693    Value::Object(obj)
694}
695
696fn temporal_extent_to_json(te: &TemporalExtent) -> Value {
697    json!({
698        "start": te.start,
699        "end": te.end,
700    })
701}