Skip to main content

fcb_core/writer/
serializer.rs

1use crate::attribute::{encode_attributes_with_schema, AttributeSchema, AttributeSchemaMethods};
2use crate::fb::{
3    Appearance, AppearanceArgs, CityFeature, CityFeatureArgs, CityObject, CityObjectArgs,
4    CityObjectType, Geometry, GeometryArgs, GeometryType, Material, MaterialArgs, SemanticObject,
5    SemanticObjectArgs, SemanticSurfaceType, Texture, TextureArgs, TextureType, Vec2, Vertex,
6    WrapMode,
7};
8use crate::fb::{Column, ColumnArgs};
9use crate::fb::{
10    GeographicalExtent, Header, HeaderArgs, ReferenceSystem, ReferenceSystemArgs, Transform, Vector,
11};
12use crate::geom_encoder::encode;
13use crate::{
14    AttributeIndex, DoubleVertex, Extension, ExtensionArgs, GeometryInstance, GeometryInstanceArgs,
15    MaterialMapping, MaterialMappingArgs, TextureFormat, TextureMapping, TextureMappingArgs,
16    TransformationMatrix,
17};
18use cjseq::{
19    Appearance as CjAppearance, BorderColor as CjBorderColor, CityJSON, CityJSONFeature,
20    CityObject as CjCityObject, CityObjectType as CjCityObjectType, Geometry as CjGeometry,
21    GeometryType as CjGeometryType, PointOfContact as CjPointOfContact,
22    ReferenceSystem as CjReferenceSystem, SemanticSurfaceType as CjSemanticSurfaceType,
23    TextureFormat as CjTextureFormat, TextureType as CjTextureType, Transform as CjTransform,
24    WrapMode as CjWrapMode,
25};
26
27use crate::packed_rtree::NodeItem;
28use flatbuffers::FlatBufferBuilder;
29use serde_json::Value;
30
31use super::geom_encoder::{GMBoundaries, GMSemantics, MaterialMapping as GMMaterialMapping};
32use super::header_writer::HeaderWriterOptions;
33use crate::error::Result;
34
35#[derive(Debug, Clone)]
36pub(super) struct AttributeIndexInfo {
37    pub index: u16,
38    pub length: u32,
39    pub branching_factor: u16,
40    pub num_unique_items: u32,
41}
42/// -----------------------------------
43/// Serializer for Header
44/// -----------------------------------
45/// Converts a CityJSON header into FlatBuffers format
46///
47/// # Arguments
48///
49/// * `fbb` - FlatBuffers builder instance
50/// * `cj` - CityJSON data containing header information
51/// * `header_metadata` - Additional metadata for the header
52pub(super) fn to_fcb_header<'a>(
53    fbb: &mut flatbuffers::FlatBufferBuilder<'a>,
54    cj: &CityJSON,
55    header_options: HeaderWriterOptions,
56    attr_schema: &AttributeSchema,
57    semantic_attr_schema: Option<&AttributeSchema>,
58    attribute_indices_info: Option<&[AttributeIndexInfo]>,
59) -> Result<flatbuffers::WIPOffset<Header<'a>>> {
60    let version = Some(fbb.create_string(&cj.version));
61    let transform = to_transform(&cj.transform);
62    let features_count: u64 = header_options.feature_count;
63    let columns = Some(to_columns(fbb, attr_schema));
64    let semantic_columns = semantic_attr_schema.map(|schema| to_columns(fbb, schema));
65    let index_node_size = header_options.index_node_size;
66    let attribute_index = {
67        if let Some(attribute_indices_info) = attribute_indices_info {
68            let attribute_indices_info_vec = attribute_indices_info
69                .iter()
70                .map(|info| {
71                    AttributeIndex::new(
72                        info.index,
73                        info.length,
74                        info.branching_factor,
75                        info.num_unique_items,
76                    )
77                })
78                .collect::<Vec<_>>();
79            Some(fbb.create_vector(&attribute_indices_info_vec))
80        } else {
81            None
82        }
83    };
84
85    // Handle extensions, if present. `extensions` is the CityJSON member
86    // itself: a map of extension name to `{url, version}`. Only that reference
87    // is written; the schema document it points at is not fetched, so writing
88    // a file never makes a network request.
89    let extensions = cj
90        .extensions
91        .as_ref()
92        .and_then(|e| e.as_object())
93        .map(|extensions| {
94            let extensions = extensions
95                .iter()
96                .map(|(name, ext)| {
97                    let url = ext.get("url").and_then(|v| v.as_str()).unwrap_or_default();
98                    let version = ext
99                        .get("version")
100                        .and_then(|v| v.as_str())
101                        .unwrap_or_default();
102                    to_extension(fbb, name, url, version)
103                })
104                .collect::<Vec<_>>();
105            fbb.create_vector(&extensions)
106        });
107
108    // Use the geographical_extent from the HeaderWriterOptions if provided
109    let geographical_extent_from_options = header_options
110        .geographical_extent
111        .as_ref()
112        .map(to_geographical_extent);
113
114    let appearance = cj.appearance.as_ref().map(|app| to_appearance(fbb, app));
115
116    let (templates, templates_vertices) = match &cj.geometry_templates {
117        Some(gm) => {
118            let templates_vertices = to_templates_vertices(fbb, &gm.vertices_templates);
119
120            let gm_vec = gm
121                .templates
122                .iter()
123                .map(|g| to_geometry(fbb, g, semantic_attr_schema))
124                .collect::<Vec<_>>();
125            (Some(fbb.create_vector(&gm_vec)), Some(templates_vertices))
126        }
127        None => (None, None),
128    };
129
130    if let Some(meta) = cj.metadata.as_ref() {
131        let reference_system = meta
132            .reference_system
133            .as_ref()
134            .map(|ref_sys| to_reference_system(fbb, ref_sys));
135        // Use the geographical_extent from the HeaderWriterOptions if provided, otherwise use the one from the metadata
136        let geographical_extent = geographical_extent_from_options.or_else(|| {
137            meta.geographical_extent
138                .as_ref()
139                .map(to_geographical_extent)
140        });
141        let identifier = meta.identifier.as_ref().map(|i| fbb.create_string(i));
142        let reference_date = meta.reference_date.as_ref().map(|r| fbb.create_string(r));
143        let title = meta.title.as_ref().map(|t| fbb.create_string(t));
144        let poc_fields = meta
145            .point_of_contact
146            .as_ref()
147            .map(|poc| to_point_of_contact(fbb, poc));
148        let (
149            poc_contact_name,
150            poc_contact_type,
151            poc_role,
152            poc_phone,
153            poc_email,
154            poc_website,
155            poc_address_thoroughfare_number,
156            poc_address_thoroughfare_name,
157            poc_address_locality,
158            poc_address_postcode,
159            poc_address_country,
160        ) = poc_fields.map_or(
161            (
162                None, None, None, None, None, None, None, None, None, None, None,
163            ),
164            |poc| {
165                (
166                    poc.poc_contact_name,
167                    poc.poc_contact_type,
168                    poc.poc_role,
169                    poc.poc_phone,
170                    poc.poc_email,
171                    poc.poc_website,
172                    poc.poc_address_thoroughfare_number,
173                    poc.poc_address_thoroughfare_name,
174                    poc.poc_address_locality,
175                    poc.poc_address_postcode,
176                    poc.poc_address_country,
177                )
178            },
179        );
180
181        Ok(Header::create(
182            fbb,
183            &HeaderArgs {
184                transform: Some(transform).as_ref(),
185                columns,
186                semantic_columns,
187                features_count,
188                index_node_size,
189                geographical_extent: geographical_extent.as_ref(),
190                reference_system,
191                identifier,
192                attribute_index,
193                reference_date,
194                title,
195                poc_contact_name,
196                poc_contact_type,
197                poc_role,
198                poc_phone,
199                poc_email,
200                poc_website,
201                poc_address_thoroughfare_number,
202                poc_address_thoroughfare_name,
203                poc_address_locality,
204                poc_address_postcode,
205                poc_address_country,
206                attributes: None,
207                version,
208                appearance,
209                templates,
210                templates_vertices,
211                extensions,
212            },
213        ))
214    } else {
215        // No `metadata`, so every metadata-derived field above is absent --
216        // but `appearance` and the geometry templates are not among them. They
217        // come from `cj.appearance` and `cj.geometry_templates`, which are
218        // siblings of `metadata`, not children of it. Leaving them to
219        // `..Default::default()` here silently dropped the palette and every
220        // template whenever a header happened to carry no metadata
221        // (finding #32).
222        Ok(Header::create(
223            fbb,
224            &HeaderArgs {
225                transform: Some(transform).as_ref(),
226                columns,
227                semantic_columns,
228                features_count,
229                index_node_size,
230                geographical_extent: geographical_extent_from_options.as_ref(),
231                version,
232                attribute_index,
233                appearance,
234                templates,
235                templates_vertices,
236                extensions,
237                ..Default::default()
238            },
239        ))
240    }
241}
242
243/// Converts CityJSON geographical extent to FlatBuffers format
244///
245/// # Arguments
246///
247/// * `geographical_extent` - Array of 6 values [minx, miny, minz, maxx, maxy, maxz]
248pub(super) fn to_geographical_extent(geographical_extent: &[f64; 6]) -> GeographicalExtent {
249    let min = Vector::new(
250        geographical_extent[0],
251        geographical_extent[1],
252        geographical_extent[2],
253    );
254    let max = Vector::new(
255        geographical_extent[3],
256        geographical_extent[4],
257        geographical_extent[5],
258    );
259    GeographicalExtent::new(&min, &max)
260}
261
262/// Converts CityJSON transform to FlatBuffers format
263///
264/// # Arguments
265///
266/// * `transform` - CityJSON transform containing scale and translate values
267pub(super) fn to_transform(transform: &CjTransform) -> Transform {
268    let scale = Vector::new(transform.scale[0], transform.scale[1], transform.scale[2]);
269    let translate = Vector::new(
270        transform.translate[0],
271        transform.translate[1],
272        transform.translate[2],
273    );
274    Transform::new(&scale, &translate)
275}
276
277/// Converts CityJSON reference system to FlatBuffers format
278///
279/// # Arguments
280///
281/// * `fbb` - FlatBuffers builder instance
282/// * `metadata` - CityJSON metadata containing reference system information
283pub(super) fn to_reference_system<'a>(
284    fbb: &mut FlatBufferBuilder<'a>,
285    ref_system: &CjReferenceSystem,
286) -> flatbuffers::WIPOffset<ReferenceSystem<'a>> {
287    // A `referenceSystem` need not have the three-element OGC shape at all โ€”
288    // the schema constrains only its prefix โ€” so each accessor is fallible.
289    let authority = Some(fbb.create_string(ref_system.authority().unwrap_or_default()));
290
291    let version = ref_system
292        .version()
293        .and_then(|v| v.parse::<i32>().ok())
294        .unwrap_or(0);
295    let code = ref_system
296        .code()
297        .and_then(|c| c.parse::<i32>().ok())
298        .unwrap_or(0);
299
300    let code_string = None; // TODO: implement code_string
301
302    ReferenceSystem::create(
303        fbb,
304        &ReferenceSystemArgs {
305            authority,
306            version,
307            code,
308            code_string,
309        },
310    )
311}
312
313/// Internal struct used only as a return type for `to_point_of_contact`
314#[doc(hidden)]
315struct FcbPointOfContact<'a> {
316    poc_contact_name: Option<flatbuffers::WIPOffset<&'a str>>,
317    poc_contact_type: Option<flatbuffers::WIPOffset<&'a str>>,
318    poc_role: Option<flatbuffers::WIPOffset<&'a str>>,
319    poc_phone: Option<flatbuffers::WIPOffset<&'a str>>,
320    poc_email: Option<flatbuffers::WIPOffset<&'a str>>,
321    poc_website: Option<flatbuffers::WIPOffset<&'a str>>,
322    poc_address_thoroughfare_number: Option<flatbuffers::WIPOffset<&'a str>>,
323    poc_address_thoroughfare_name: Option<flatbuffers::WIPOffset<&'a str>>,
324    poc_address_locality: Option<flatbuffers::WIPOffset<&'a str>>,
325    poc_address_postcode: Option<flatbuffers::WIPOffset<&'a str>>,
326    poc_address_country: Option<flatbuffers::WIPOffset<&'a str>>,
327}
328
329fn to_point_of_contact<'a>(
330    fbb: &mut FlatBufferBuilder<'a>,
331    poc: &CjPointOfContact,
332) -> FcbPointOfContact<'a> {
333    let poc_contact_name = Some(fbb.create_string(&poc.contact_name));
334
335    let poc_contact_type = poc.contact_type.as_ref().map(|ct| fbb.create_string(ct));
336    let poc_role = poc.role.as_ref().map(|r| fbb.create_string(r));
337    let poc_phone = poc.phone.as_ref().map(|p| fbb.create_string(p));
338    let poc_email = Some(fbb.create_string(&poc.email_address));
339    let poc_website = poc.website.as_ref().map(|w| fbb.create_string(w));
340    // `metadata.schema.json` types `address` as a bare object with no named
341    // members ("any properties can be used, to accommodate the different ways
342    // addresses are structured in different countries", ยง 5.3), so each of the
343    // header's fixed address fields is looked up by name and may be absent.
344    // A number spelled as a JSON number rather than a string is kept verbatim.
345    let address_member = |key: &str| -> Option<String> {
346        let members = &poc.address.as_ref()?.members;
347        let value = members.get(key)?;
348        match value {
349            Value::String(s) => Some(s.clone()),
350            Value::Null => None,
351            other => Some(other.to_string()),
352        }
353    };
354    // The spec's own examples spell the postcode `postcode`; the schema's
355    // CityJSON 1.x examples used `postalCode`. Accept either.
356    let address_either = |a: &str, b: &str| address_member(a).or_else(|| address_member(b));
357
358    let poc_address_thoroughfare_number =
359        address_member("thoroughfareNumber").map(|v| fbb.create_string(&v));
360    let poc_address_thoroughfare_name =
361        address_member("thoroughfareName").map(|v| fbb.create_string(&v));
362    let poc_address_locality = address_member("locality").map(|v| fbb.create_string(&v));
363    let poc_address_postcode =
364        address_either("postcode", "postalCode").map(|v| fbb.create_string(&v));
365    let poc_address_country = address_member("country").map(|v| fbb.create_string(&v));
366    FcbPointOfContact {
367        poc_contact_name,
368        poc_contact_type,
369        poc_role,
370        poc_phone,
371        poc_email,
372        poc_website,
373        poc_address_thoroughfare_number,
374        poc_address_thoroughfare_name,
375        poc_address_locality,
376        poc_address_postcode,
377        poc_address_country,
378    }
379}
380
381/// Writes one entry of the CityJSON `extensions` member into the header.
382///
383/// Only the reference is written โ€” the name, the URL of the extension schema
384/// and the version, which is all `extensions` contains. The schema document
385/// itself is not fetched: doing so made writing a file depend on the network
386/// (and on the extension host still being up), and nothing in this crate reads
387/// the embedded schema back.
388pub fn to_extension<'a>(
389    fbb: &mut FlatBufferBuilder<'a>,
390    name: &str,
391    url: &str,
392    version: &str,
393) -> flatbuffers::WIPOffset<Extension<'a>> {
394    let name = fbb.create_string(name);
395    let url = fbb.create_string(url);
396    let version = fbb.create_string(version);
397
398    Extension::create(
399        fbb,
400        &ExtensionArgs {
401            name: Some(name),
402            url: Some(url),
403            version: Some(version),
404            ..Default::default()
405        },
406    )
407}
408
409/// -----------------------------------
410/// Serializer for CityJSONFeature
411/// -----------------------------------
412/// Creates a CityFeature in FlatBuffers format
413///
414/// # Arguments
415///
416/// * `fbb` - FlatBuffers builder instance
417/// * `id` - Feature identifier
418/// * `objects` - Vector of city objects
419/// * `vertices` - Vector of vertex coordinates
420pub(super) fn to_fcb_city_feature<'a>(
421    fbb: &mut flatbuffers::FlatBufferBuilder<'a>,
422    id: &str,
423    city_feature: &CityJSONFeature,
424    attr_schema: &AttributeSchema,
425    semantic_attr_schema: Option<&AttributeSchema>,
426) -> (flatbuffers::WIPOffset<CityFeature<'a>>, NodeItem) {
427    let id = Some(fbb.create_string(id));
428    // cjseq stores `city_objects` in a `HashMap`, whose iteration order is
429    // randomly seeded per process. Emitting the objects in that order made the
430    // same input serialise to different bytes on every run; sort by id so the
431    // feature payload is reproducible.
432    let mut object_ids: Vec<&String> = city_feature.city_objects.keys().collect();
433    object_ids.sort_unstable();
434    let city_objects: Vec<_> = object_ids
435        .into_iter()
436        .filter_map(|id| city_feature.city_objects.get(id).map(|co| (id, co)))
437        .map(|(id, co)| to_city_object(fbb, id, co, attr_schema, semantic_attr_schema))
438        .collect();
439    let objects = Some(fbb.create_vector(&city_objects));
440    let vertices = Some(
441        fbb.create_vector(
442            &city_feature
443                .vertices
444                .iter()
445                .map(|v| {
446                    Vertex::new(
447                        v[0].try_into().unwrap(),
448                        v[1].try_into().unwrap(),
449                        v[2].try_into().unwrap(),
450                    )
451                })
452                .collect::<Vec<_>>(),
453        ),
454    );
455
456    // Handle appearance if present
457    let appearance = city_feature
458        .appearance
459        .as_ref()
460        .map(|app| to_appearance(fbb, app));
461    let min_x = city_feature
462        .vertices
463        .iter()
464        .map(|v| v[0])
465        .min()
466        .unwrap_or(0) as f64;
467    let min_y = city_feature
468        .vertices
469        .iter()
470        .map(|v| v[1])
471        .min()
472        .unwrap_or(0) as f64;
473    let max_x = city_feature
474        .vertices
475        .iter()
476        .map(|v| v[0])
477        .max()
478        .unwrap_or(0) as f64;
479    let max_y = city_feature
480        .vertices
481        .iter()
482        .map(|v| v[1])
483        .max()
484        .unwrap_or(0) as f64;
485
486    let bbox = NodeItem::bounds(min_x, min_y, max_x, max_y);
487    (
488        CityFeature::create(
489            fbb,
490            &CityFeatureArgs {
491                id,
492                objects,
493                vertices,
494                appearance,
495            },
496        ),
497        bbox,
498    )
499}
500
501/// The FlatCityBuf tag for a CityJSON `wrapMode`.
502///
503/// Exhaustive by construction: there is no `_` arm, so a spelling added to
504/// `cjseq::WrapMode` -- which mirrors `appearance.schema.json`'s five --
505/// stops this crate from compiling until it is mapped here. This replaces a
506/// table lookup keyed on a *string*, whose fallback turned a mis-spelling into
507/// a valid-looking default: that is how a texture written
508/// `"wrapMode": "wrap"` came back as `"None"` and survived until the C++
509/// conformance corpus caught it.
510pub(crate) fn fb_wrap_mode(w: CjWrapMode) -> WrapMode {
511    match w {
512        CjWrapMode::None => WrapMode::None,
513        CjWrapMode::Wrap => WrapMode::Wrap,
514        CjWrapMode::Mirror => WrapMode::Mirror,
515        CjWrapMode::Clamp => WrapMode::Clamp,
516        CjWrapMode::Border => WrapMode::Border,
517    }
518}
519
520/// The FlatCityBuf tag for a CityJSON `textureType`. Exhaustive, as above.
521pub(crate) fn fb_texture_type(t: CjTextureType) -> TextureType {
522    match t {
523        CjTextureType::Unknown => TextureType::Unknown,
524        CjTextureType::Specific => TextureType::Specific,
525        CjTextureType::Typical => TextureType::Typical,
526    }
527}
528
529/// The FlatCityBuf tag for a CityJSON texture `type`. Exhaustive, as above.
530///
531/// `type` is optional in `appearance.schema.json` (the `Texture` object has no
532/// `required` keyword at all) but mandatory in `header.fbs`, whose default is
533/// `PNG`. An absent `type` therefore comes back as `"PNG"`; that lossiness
534/// predates this change and is fixed only by changing the schema, which would
535/// break the C++ reader.
536pub(crate) fn fb_texture_format(f: Option<CjTextureFormat>) -> TextureFormat {
537    match f {
538        Some(CjTextureFormat::PNG) | None => TextureFormat::PNG,
539        Some(CjTextureFormat::JPG) => TextureFormat::JPG,
540    }
541}
542
543pub(super) fn to_appearance<'a>(
544    fbb: &mut FlatBufferBuilder<'a>,
545    appearance: &CjAppearance,
546) -> flatbuffers::WIPOffset<Appearance<'a>> {
547    // Handle appearance if present
548
549    // `appearance.materials` and `appearance.textures` are typed by cjseq
550    // straight from `appearance.schema.json`, so every member here is read off
551    // a field rather than looked up by name in a `serde_json::Value`.
552    let materials = appearance.materials.as_ref().map(|materials| {
553        let material_offsets: Vec<_> = materials
554            .iter()
555            .map(|m| {
556                let name = fbb.create_string(&m.name);
557                let diffuse_color = m.diffuse_color.map(|c| fbb.create_vector(&c));
558                let emissive_color = m.emissive_color.map(|c| fbb.create_vector(&c));
559                let specular_color = m.specular_color.map(|c| fbb.create_vector(&c));
560                Material::create(
561                    fbb,
562                    &MaterialArgs {
563                        name: Some(name),
564                        ambient_intensity: m.ambient_intensity,
565                        diffuse_color,
566                        emissive_color,
567                        specular_color,
568                        shininess: m.shininess,
569                        transparency: m.transparency,
570                        is_smooth: m.is_smooth,
571                    },
572                )
573            })
574            .collect();
575        fbb.create_vector(&material_offsets)
576    });
577
578    let textures = appearance.textures.as_ref().map(|textures| {
579        let texture_offsets: Vec<_> = textures
580            .iter()
581            .map(|t| {
582                // `image` is optional in CityJSON but mandatory in the `.fbs`,
583                // so an absent one is written as the empty string.
584                let image = fbb.create_string(t.image.as_deref().unwrap_or_default());
585                let border_color = t.border_color.as_ref().map(|c| match c {
586                    CjBorderColor::Rgb(c) => fbb.create_vector(c),
587                    CjBorderColor::Rgba(c) => fbb.create_vector(c),
588                });
589                Texture::create(
590                    fbb,
591                    &TextureArgs {
592                        type_: fb_texture_format(t.thetype),
593                        image: Some(image),
594                        wrap_mode: t.wrap_mode.map(fb_wrap_mode),
595                        texture_type: t.texture_type.map(fb_texture_type),
596                        border_color,
597                    },
598                )
599            })
600            .collect();
601        fbb.create_vector(&texture_offsets)
602    });
603
604    let vertices_texture = appearance.vertices_texture.as_ref().map(|vertices| {
605        fbb.create_vector(
606            &vertices
607                .iter()
608                .map(|v| Vec2::new(v[0], v[1]))
609                .collect::<Vec<_>>(),
610        )
611    });
612
613    let default_theme_texture = appearance
614        .default_theme_texture
615        .as_ref()
616        .map(|t| fbb.create_string(t));
617    let default_theme_material = appearance
618        .default_theme_material
619        .as_ref()
620        .map(|m| fbb.create_string(m));
621
622    Appearance::create(
623        fbb,
624        &AppearanceArgs {
625            materials,
626            textures,
627            vertices_texture,
628            default_theme_texture,
629            default_theme_material,
630        },
631    )
632}
633
634/// Converts CityJSON object to FlatBuffers
635///
636/// # Arguments
637///
638/// * `fbb` - FlatBuffers builder instance
639/// * `id` - City object ID
640/// * `co` - CityJSON object
641/// * `attr_schema` - Attribute schema
642pub(super) fn to_city_object<'a>(
643    fbb: &mut flatbuffers::FlatBufferBuilder<'a>,
644    id: &str,
645    co: &CjCityObject,
646    attr_schema: &AttributeSchema,
647    semantic_attr_schema: Option<&AttributeSchema>,
648) -> flatbuffers::WIPOffset<CityObject<'a>> {
649    let id = Some(fbb.create_string(id));
650
651    let (type_, extension_type) = to_co_type(&co.thetype);
652    let extension_type = extension_type.as_ref().map(|et| fbb.create_string(et));
653    let geographical_extent = co.geographical_extent.as_ref().map(to_geographical_extent);
654    let geometry_without_instances = co.geometry.as_ref().map(|gs| {
655        gs.iter()
656            .filter(|g| g.geometry_type() != CjGeometryType::GeometryInstance)
657            .collect::<Vec<_>>()
658    });
659    let geometry_instances = co.geometry.as_ref().map(|gs| {
660        gs.iter()
661            .filter(|g| g.geometry_type() == CjGeometryType::GeometryInstance)
662            .collect::<Vec<_>>()
663    });
664    let geometries = {
665        let geometries = geometry_without_instances.map(|gs| {
666            gs.iter()
667                .map(|g| to_geometry(fbb, g, semantic_attr_schema))
668                .collect::<Vec<_>>()
669        });
670        geometries.map(|geometries| fbb.create_vector(&geometries))
671    };
672
673    let geometry_instances = {
674        let geometry_instances = geometry_instances.map(|gs| {
675            gs.iter()
676                .map(|g| to_geometry_instance(fbb, g))
677                .collect::<Vec<_>>()
678        });
679        geometry_instances.map(|geometry_instances| fbb.create_vector(&geometry_instances))
680    };
681
682    let attributes_and_columns = co
683        .attributes
684        .as_ref()
685        .map(|attr| {
686            if !attr.is_object() {
687                return (None, None);
688            }
689            let (attr_vec, own_schema) = to_fcb_attribute(fbb, attr, attr_schema);
690            let columns = own_schema.map(|schema| to_columns(fbb, &schema));
691            (Some(attr_vec), columns)
692        })
693        .unwrap_or((None, None));
694
695    let (attributes, columns) = attributes_and_columns;
696
697    let children = {
698        let children = co
699            .children
700            .as_ref()
701            .map(|c| c.iter().map(|s| fbb.create_string(s)).collect::<Vec<_>>());
702        children.map(|c| fbb.create_vector(&c))
703    };
704
705    let children_roles = {
706        // An unspecified role is `null` in CityJSON; the header has no way to
707        // spell that, so it is written as the empty string.
708        let children_roles_strings = co.children_roles.as_ref().map(|c| {
709            c.iter()
710                .map(|r| fbb.create_string(r.as_deref().unwrap_or_default()))
711                .collect::<Vec<_>>()
712        });
713        children_roles_strings.map(|c| fbb.create_vector(&c))
714    };
715
716    let parents = {
717        let parents = co
718            .parents
719            .as_ref()
720            .map(|p| p.iter().map(|s| fbb.create_string(s)).collect::<Vec<_>>());
721        parents.map(|p| fbb.create_vector(&p))
722    };
723
724    CityObject::create(
725        fbb,
726        &CityObjectArgs {
727            id,
728            type_,
729            extension_type,
730            geographical_extent: geographical_extent.as_ref(),
731            geometry: geometries,
732            geometry_instances,
733            attributes,
734            columns,
735            children,
736            children_roles,
737            parents,
738        },
739    )
740}
741
742/// Converts a CityJSON City Object type to the FlatBuffers enum.
743///
744/// An Extension type has no tag of its own: it becomes `ExtensionObject` plus
745/// the CityJSON name verbatim (leading `+` included) in `extension_type`.
746///
747/// The known names are associated `const`s on `CityObjectType`, not enum
748/// variants, so this matches on `*co_type` rather than on the reference โ€” a
749/// `const` pattern against a `&CityObjectType` is `E0308`, and rustc's fix-it
750/// suggestion silently turns the arm into a catch-all binding.
751///
752/// There is deliberately no `_` arm: listing every known const plus `Extension`
753/// *is* exhaustive, so a name added to `KnownCityObjectType` upstream fails to
754/// compile here rather than silently becoming a `GenericCityObject`.
755pub(super) fn to_co_type(co_type: &CjCityObjectType) -> (CityObjectType, Option<String>) {
756    match *co_type {
757        CjCityObjectType::Bridge => (CityObjectType::Bridge, None),
758        CjCityObjectType::BridgePart => (CityObjectType::BridgePart, None),
759        CjCityObjectType::BridgeInstallation => (CityObjectType::BridgeInstallation, None),
760        CjCityObjectType::BridgeConstructiveElement => {
761            (CityObjectType::BridgeConstructiveElement, None)
762        }
763        CjCityObjectType::BridgeRoom => (CityObjectType::BridgeRoom, None),
764        CjCityObjectType::BridgeFurniture => (CityObjectType::BridgeFurniture, None),
765        CjCityObjectType::Building => (CityObjectType::Building, None),
766        CjCityObjectType::BuildingPart => (CityObjectType::BuildingPart, None),
767        CjCityObjectType::BuildingInstallation => (CityObjectType::BuildingInstallation, None),
768        CjCityObjectType::BuildingConstructiveElement => {
769            (CityObjectType::BuildingConstructiveElement, None)
770        }
771        CjCityObjectType::BuildingFurniture => (CityObjectType::BuildingFurniture, None),
772        CjCityObjectType::BuildingStorey => (CityObjectType::BuildingStorey, None),
773        CjCityObjectType::BuildingRoom => (CityObjectType::BuildingRoom, None),
774        CjCityObjectType::BuildingUnit => (CityObjectType::BuildingUnit, None),
775        CjCityObjectType::CityFurniture => (CityObjectType::CityFurniture, None),
776        CjCityObjectType::CityObjectGroup => (CityObjectType::CityObjectGroup, None),
777        CjCityObjectType::GenericCityObject => (CityObjectType::GenericCityObject, None),
778        CjCityObjectType::LandUse => (CityObjectType::LandUse, None),
779        CjCityObjectType::OtherConstruction => (CityObjectType::OtherConstruction, None),
780        CjCityObjectType::PlantCover => (CityObjectType::PlantCover, None),
781        CjCityObjectType::SolitaryVegetationObject => {
782            (CityObjectType::SolitaryVegetationObject, None)
783        }
784        CjCityObjectType::TINRelief => (CityObjectType::TINRelief, None),
785        CjCityObjectType::Road => (CityObjectType::Road, None),
786        CjCityObjectType::Railway => (CityObjectType::Railway, None),
787        CjCityObjectType::Waterway => (CityObjectType::Waterway, None),
788        CjCityObjectType::TransportSquare => (CityObjectType::TransportSquare, None),
789        CjCityObjectType::Tunnel => (CityObjectType::Tunnel, None),
790        CjCityObjectType::TunnelPart => (CityObjectType::TunnelPart, None),
791        CjCityObjectType::TunnelInstallation => (CityObjectType::TunnelInstallation, None),
792        CjCityObjectType::TunnelConstructiveElement => {
793            (CityObjectType::TunnelConstructiveElement, None)
794        }
795        CjCityObjectType::TunnelHollowSpace => (CityObjectType::TunnelHollowSpace, None),
796        CjCityObjectType::TunnelFurniture => (CityObjectType::TunnelFurniture, None),
797        CjCityObjectType::WaterBody => (CityObjectType::WaterBody, None),
798        CjCityObjectType::Extension(ref name) => {
799            (CityObjectType::ExtensionObject, Some(name.clone()))
800        }
801    }
802}
803
804/// Converts CityJSON geometry type to FlatBuffers enum
805///
806/// # Arguments
807///
808/// * `geometry_type` - CityJSON geometry type
809pub(super) fn to_geom_type(geometry_type: &CjGeometryType) -> GeometryType {
810    match geometry_type {
811        CjGeometryType::MultiPoint => GeometryType::MultiPoint,
812        CjGeometryType::MultiLineString => GeometryType::MultiLineString,
813        CjGeometryType::MultiSurface => GeometryType::MultiSurface,
814        CjGeometryType::CompositeSurface => GeometryType::CompositeSurface,
815        CjGeometryType::Solid => GeometryType::Solid,
816        CjGeometryType::MultiSolid => GeometryType::MultiSolid,
817        CjGeometryType::CompositeSolid => GeometryType::CompositeSolid,
818        CjGeometryType::GeometryInstance => GeometryType::GeometryInstance,
819    }
820}
821
822/// A semantic surface type as FlatCityBuf stores it: the enum tag, plus the
823/// CityJSON name verbatim when the tag is `ExtraSemanticSurface`, which has no
824/// spelling of its own.
825pub(super) struct FcbSemanticSurfaceType {
826    pub(super) type_: SemanticSurfaceType,
827    pub(super) extension_type: Option<String>,
828}
829
830impl FcbSemanticSurfaceType {
831    fn known(type_: SemanticSurfaceType) -> Self {
832        FcbSemanticSurfaceType {
833            type_,
834            extension_type: None,
835        }
836    }
837}
838
839/// Converts a CityJSON semantic surface type to the FlatBuffers enum.
840///
841/// As with [`to_co_type`], the match is on the value rather than the reference:
842/// the known names are associated `const`s, and a `const` pattern against a
843/// reference is `E0308`. Listing every one of them is what makes this
844/// exhaustive โ€” adding a name to `KnownSemanticSurfaceType` upstream will fail
845/// to compile here rather than silently decay to `ExtraSemanticSurface`.
846impl From<&CjSemanticSurfaceType> for FcbSemanticSurfaceType {
847    fn from(ss_type: &CjSemanticSurfaceType) -> Self {
848        match *ss_type {
849            CjSemanticSurfaceType::RoofSurface => Self::known(SemanticSurfaceType::RoofSurface),
850            CjSemanticSurfaceType::GroundSurface => Self::known(SemanticSurfaceType::GroundSurface),
851            CjSemanticSurfaceType::WallSurface => Self::known(SemanticSurfaceType::WallSurface),
852            CjSemanticSurfaceType::ClosureSurface => {
853                Self::known(SemanticSurfaceType::ClosureSurface)
854            }
855            CjSemanticSurfaceType::OuterCeilingSurface => {
856                Self::known(SemanticSurfaceType::OuterCeilingSurface)
857            }
858            CjSemanticSurfaceType::OuterFloorSurface => {
859                Self::known(SemanticSurfaceType::OuterFloorSurface)
860            }
861            CjSemanticSurfaceType::Window => Self::known(SemanticSurfaceType::Window),
862            CjSemanticSurfaceType::Door => Self::known(SemanticSurfaceType::Door),
863            CjSemanticSurfaceType::InteriorWallSurface => {
864                Self::known(SemanticSurfaceType::InteriorWallSurface)
865            }
866            CjSemanticSurfaceType::CeilingSurface => {
867                Self::known(SemanticSurfaceType::CeilingSurface)
868            }
869            CjSemanticSurfaceType::FloorSurface => Self::known(SemanticSurfaceType::FloorSurface),
870            CjSemanticSurfaceType::WaterSurface => Self::known(SemanticSurfaceType::WaterSurface),
871            CjSemanticSurfaceType::WaterGroundSurface => {
872                Self::known(SemanticSurfaceType::WaterGroundSurface)
873            }
874            CjSemanticSurfaceType::WaterClosureSurface => {
875                Self::known(SemanticSurfaceType::WaterClosureSurface)
876            }
877            CjSemanticSurfaceType::TrafficArea => Self::known(SemanticSurfaceType::TrafficArea),
878            CjSemanticSurfaceType::AuxiliaryTrafficArea => {
879                Self::known(SemanticSurfaceType::AuxiliaryTrafficArea)
880            }
881            CjSemanticSurfaceType::TransportationMarking => {
882                Self::known(SemanticSurfaceType::TransportationMarking)
883            }
884            CjSemanticSurfaceType::TransportationHole => {
885                Self::known(SemanticSurfaceType::TransportationHole)
886            }
887            CjSemanticSurfaceType::Extension(ref name) => FcbSemanticSurfaceType {
888                type_: SemanticSurfaceType::ExtraSemanticSurface,
889                extension_type: Some(name.clone()),
890            },
891        }
892    }
893}
894
895/// Converts CityJSON geometry to FlatBuffers format
896///
897/// # Arguments
898///
899/// * `fbb` - FlatBuffers builder instance
900/// * `geometry` - CityJSON geometry object
901pub(crate) fn to_geometry<'a>(
902    fbb: &mut flatbuffers::FlatBufferBuilder<'a>,
903    geometry: &CjGeometry,
904    semantic_attr_schema: Option<&AttributeSchema>,
905) -> flatbuffers::WIPOffset<Geometry<'a>> {
906    let type_ = to_geom_type(&geometry.geometry_type());
907    let lod = geometry.lod().map(|lod| fbb.create_string(lod));
908
909    let encoded = encode(geometry);
910    let GMBoundaries {
911        solids,
912        shells,
913        surfaces,
914        strings,
915        indices,
916    } = encoded.boundaries;
917    let semantics = encoded
918        .semantics
919        .map(|GMSemantics { surfaces, values }| (surfaces, values));
920
921    let solids = Some(fbb.create_vector(&solids));
922    let shells = Some(fbb.create_vector(&shells));
923    let surfaces = Some(fbb.create_vector(&surfaces));
924    let strings = Some(fbb.create_vector(&strings));
925    let boundary_indices = Some(fbb.create_vector(&indices));
926
927    let (semantics_objects, semantics_values) =
928        semantics.map_or((None, None), |(surface, values)| {
929            let semantics_objects = surface
930                .iter()
931                .map(|s| {
932                    let children = s.children.as_ref().map(|c| {
933                        let c = c.iter().map(|&i| i as u32).collect::<Vec<_>>();
934                        fbb.create_vector(&c)
935                    });
936
937                    let FcbSemanticSurfaceType {
938                        type_,
939                        extension_type,
940                    } = FcbSemanticSurfaceType::from(&s.thetype);
941                    let extension_type = extension_type.map(|s| fbb.create_string(&s));
942                    let attributes = if s.other.is_empty() {
943                        None
944                    } else {
945                        let other = Value::Object(s.other.clone().into_iter().collect());
946                        semantic_attr_schema.as_ref().map(|schema| {
947                            fbb.create_vector(&encode_attributes_with_schema(&other, schema))
948                        })
949                    };
950                    SemanticObject::create(
951                        fbb,
952                        &SemanticObjectArgs {
953                            type_,
954                            extension_type,
955                            attributes,
956                            children,
957                            parent: s.parent.map(|p| p as u32),
958                        },
959                    )
960                })
961                .collect::<Vec<_>>();
962
963            (
964                Some(fbb.create_vector(&semantics_objects)),
965                // Absent, not empty, for `"values": null` -- which is valid
966                // CityJSON (required-but-nullable) and is not the same thing as
967                // an empty array.
968                values.map(|values| fbb.create_vector(&values)),
969            )
970        });
971
972    let material_mappings = encoded.materials.map(|m| {
973        let mappings = m
974            .iter()
975            .map(|m| match m {
976                GMMaterialMapping::Value(v) => {
977                    let theme = Some(fbb.create_string(&v.theme));
978                    let value = Some(v.value);
979                    MaterialMapping::create(
980                        fbb,
981                        &MaterialMappingArgs {
982                            theme,
983                            solids: None,
984                            shells: None,
985                            vertices: None,
986                            value,
987                        },
988                    )
989                }
990                GMMaterialMapping::Values(v) => {
991                    let theme = Some(fbb.create_string(&v.theme));
992                    let solids = Some(fbb.create_vector(&v.solids));
993                    let shells = Some(fbb.create_vector(&v.shells));
994                    // Present-but-empty, so that `"values": []` stays distinct
995                    // from the absent vector written for `"values": null`.
996                    let vertices = Some(fbb.create_vector(&v.vertices));
997                    let value = None;
998                    MaterialMapping::create(
999                        fbb,
1000                        &MaterialMappingArgs {
1001                            theme,
1002                            solids,
1003                            shells,
1004                            vertices,
1005                            value,
1006                        },
1007                    )
1008                }
1009                // `"values": null`: a theme, and no arrays at all.
1010                GMMaterialMapping::NullValues(theme) => {
1011                    let theme = Some(fbb.create_string(theme));
1012                    MaterialMapping::create(
1013                        fbb,
1014                        &MaterialMappingArgs {
1015                            theme,
1016                            ..Default::default()
1017                        },
1018                    )
1019                }
1020            })
1021            .collect::<Vec<_>>();
1022        fbb.create_vector(&mappings)
1023    });
1024
1025    let texture_mappings = encoded.textures.map(|t| {
1026        let mappings = t
1027            .iter()
1028            .map(|t| {
1029                let theme = Some(fbb.create_string(&t.theme));
1030                // A theme with no `values` member carries no arrays at all, so
1031                // that it stays distinct from one whose `values` is empty.
1032                let (solids, shells, surfaces, strings, vertices) = if t.has_values {
1033                    (
1034                        Some(fbb.create_vector(&t.solids)),
1035                        Some(fbb.create_vector(&t.shells)),
1036                        Some(fbb.create_vector(&t.surfaces)),
1037                        Some(fbb.create_vector(&t.strings)),
1038                        Some(fbb.create_vector(&t.vertices)),
1039                    )
1040                } else {
1041                    (None, None, None, None, None)
1042                };
1043                TextureMapping::create(
1044                    fbb,
1045                    &TextureMappingArgs {
1046                        theme,
1047                        solids,
1048                        shells,
1049                        surfaces,
1050                        strings,
1051                        vertices,
1052                    },
1053                )
1054            })
1055            .collect::<Vec<_>>();
1056        fbb.create_vector(&mappings)
1057    });
1058
1059    Geometry::create(
1060        fbb,
1061        &GeometryArgs {
1062            type_,
1063            lod,
1064            solids,
1065            shells,
1066            surfaces,
1067            strings,
1068            boundaries: boundary_indices,
1069            semantics: semantics_values,
1070            semantics_objects,
1071            material: material_mappings,
1072            texture: texture_mappings,
1073        },
1074    )
1075}
1076
1077pub(super) fn to_geometry_instance<'a>(
1078    fbb: &mut FlatBufferBuilder<'a>,
1079    geometry: &CjGeometry,
1080) -> flatbuffers::WIPOffset<GeometryInstance<'a>> {
1081    // `template`, `transformationMatrix` and single-index boundaries are part
1082    // of the `GeometryInstance` variant's type, so there is nothing to check
1083    // at runtime beyond having been handed the right variant.
1084    let CjGeometry::GeometryInstance {
1085        boundaries,
1086        template,
1087        transformation_matrix: m,
1088    } = geometry
1089    else {
1090        panic!(
1091            "to_geometry_instance was given a {:?}",
1092            geometry.geometry_type()
1093        );
1094        //TODO: don't use panic, instead, return Result type
1095    };
1096
1097    let template = *template as u32;
1098    let indices = boundaries.iter().map(|&i| i as u32).collect::<Vec<_>>();
1099    let boundaries = Some(fbb.create_vector(&indices));
1100    let transformation = Some(TransformationMatrix::new(
1101        m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9], m[10], m[11], m[12], m[13],
1102        m[14], m[15],
1103    ));
1104    GeometryInstance::create(
1105        fbb,
1106        &GeometryInstanceArgs {
1107            template,
1108            transformation: transformation.as_ref(),
1109            boundaries,
1110        },
1111    )
1112}
1113
1114/// `geometry-templates.vertices-templates` is an untyped `Value` in CityJSON;
1115/// anything that is not a 3-element array of numbers is not a vertex and is
1116/// skipped rather than guessed at.
1117pub(super) fn to_templates_vertices<'a>(
1118    fbb: &mut FlatBufferBuilder<'a>,
1119    vertices: &Value,
1120) -> flatbuffers::WIPOffset<flatbuffers::Vector<'a, DoubleVertex>> {
1121    let vertices_vec = vertices
1122        .as_array()
1123        .map(|vs| {
1124            vs.iter()
1125                .filter_map(|v| {
1126                    let v = v.as_array()?;
1127                    let coords: Vec<f64> = v.iter().filter_map(|c| c.as_f64()).collect();
1128                    let [x, y, z]: [f64; 3] = coords.try_into().ok()?;
1129                    Some(DoubleVertex::new(x, y, z))
1130                })
1131                .collect::<Vec<_>>()
1132        })
1133        .unwrap_or_default();
1134    fbb.create_vector(&vertices_vec)
1135}
1136
1137pub(crate) fn to_columns<'a>(
1138    fbb: &mut FlatBufferBuilder<'a>,
1139    attr_schema: &AttributeSchema,
1140) -> flatbuffers::WIPOffset<flatbuffers::Vector<'a, flatbuffers::ForwardsUOffset<Column<'a>>>> {
1141    let mut sorted_schema: Vec<_> = attr_schema.iter().collect();
1142    sorted_schema.sort_by_key(|(_, (index, _))| *index);
1143    let columns_vec = sorted_schema
1144        .iter()
1145        .map(|(name, (index, column_type))| {
1146            let name = fbb.create_string(name);
1147            Column::create(
1148                fbb,
1149                &ColumnArgs {
1150                    name: Some(name),
1151                    index: *index,
1152                    type_: *column_type,
1153                    ..Default::default()
1154                },
1155            )
1156        })
1157        .collect::<Vec<_>>();
1158    fbb.create_vector(&columns_vec)
1159}
1160
1161pub(super) fn to_fcb_attribute<'a>(
1162    fbb: &mut FlatBufferBuilder<'a>,
1163    attr: &Value,
1164    schema: &AttributeSchema,
1165) -> (
1166    flatbuffers::WIPOffset<flatbuffers::Vector<'a, u8>>,
1167    Option<AttributeSchema>,
1168) {
1169    let mut is_own_schema = false;
1170    for (key, _) in attr.as_object().unwrap().iter() {
1171        if !schema.contains_key(key) {
1172            is_own_schema = true;
1173        }
1174    }
1175    if is_own_schema {
1176        let mut own_schema = AttributeSchema::new();
1177        own_schema.add_attributes(attr);
1178        let encoded = encode_attributes_with_schema(attr, &own_schema);
1179        (fbb.create_vector(&encoded), Some(own_schema))
1180    } else {
1181        let encoded = encode_attributes_with_schema(attr, schema);
1182        (fbb.create_vector(&encoded), None)
1183    }
1184}
1185
1186#[cfg(test)]
1187mod tests {
1188    use super::*;
1189
1190    use crate::{deserializer::to_cj_co_type, feature_generated::root_as_city_feature};
1191
1192    use anyhow::Result;
1193    use cjseq::CityJSONFeature;
1194    use flatbuffers::FlatBufferBuilder;
1195    use pretty_assertions::assert_eq;
1196
1197    #[test]
1198    fn test_to_fcb_city_feature() -> Result<()> {
1199        let cj_city_feature: CityJSONFeature = CityJSONFeature::from_str(
1200            r#"{"type":"CityJSONFeature","id":"NL.IMBAG.Pand.0503100000005156","CityObjects":{"NL.IMBAG.Pand.0503100000005156-0":{"type":"BuildingPart","attributes":{},"geometry":[{"type":"Solid","lod":"1.2","boundaries":[[[[6,1,0,5,4,3,7,8]],[[9,5,0,10]],[[10,0,1,11]],[[12,3,4,13]],[[13,4,5,9]],[[14,7,3,12]],[[15,8,7,14]],[[16,6,8,15]],[[11,1,6,16]],[[11,16,15,14,12,13,9,10]]]],"semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"on_footprint_edge":true,"type":"WallSurface"},{"on_footprint_edge":false,"type":"WallSurface"}],"values":[[0,2,2,2,2,2,2,2,2,1]]}},{"type":"Solid","lod":"1.3","boundaries":[[[[3,7,8,6,1,17,0,5,4,18]],[[19,5,0,20]],[[21,22,17,1,23]],[[24,7,3,25]],[[26,8,7,24]],[[20,0,17,43]],[[44,45,43,46]],[[47,4,5,36]],[[48,18,4,47]],[[39,1,6,49]],[[41,3,18,48,50]],[[46,43,17,35,38]],[[49,6,8,42]],[[51,52,45,44]],[[53,54,55]],[[54,53,56]],[[50,48,52,51]],[[53,55,38,39,49,42]],[[54,56,44,46,38,55]],[[50,51,44,56,53,42,40,41]],[[52,48,47,36,37,43,45]]]],"semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"on_footprint_edge":true,"type":"WallSurface"},{"on_footprint_edge":false,"type":"WallSurface"}],"values":[[0,2,2,2,2,2,3,2,2,2,2,2,3,3,1,1]]}},{"type":"Solid","lod":"2.2","boundaries":[[[[1,35,17,0,5,4,18,3,7,8,6]],[[36,5,0,37]],[[38,35,1,39]],[[40,7,3,41]],[[42,8,7,40]],[[37,0,17,43]],[[44,45,43,46]],[[47,4,5,36]],[[48,18,4,47]],[[39,1,6,49]],[[41,3,18,48,50]],[[46,43,17,35,38]],[[49,6,8,42]],[[51,52,45,44]],[[53,54,55]],[[54,53,56]],[[50,48,52,51]],[[53,55,38,39,49,42]],[[54,56,44,46,38,55]],[[50,51,44,56,53,42,40,41]],[[52,48,47,36,37,43,45]]]],"semantics":{"surfaces":[{"type":"GroundSurface"},{"type":"RoofSurface"},{"on_footprint_edge":true,"type":"WallSurface"},{"on_footprint_edge":false,"type":"WallSurface"}],"values":[[0,2,2,2,2,2,3,2,2,2,2,2,2,3,3,3,3,1,1,1,1]]}}],"parents":["NL.IMBAG.Pand.0503100000005156"]},"NL.IMBAG.Pand.0503100000005156":{"type":"Building","geographicalExtent":[84734.8046875,446636.5625,0.6919999718666077,84746.9453125,446651.0625,11.119057655334473],"attributes":{"b3_bag_bag_overlap":0.0,"b3_bouwlagen":3,"b3_dak_type":"slanted","b3_h_dak_50p":8.609999656677246,"b3_h_dak_70p":9.239999771118164,"b3_h_dak_max":10.970000267028809,"b3_h_dak_min":3.890000104904175,"b3_h_maaiveld":0.6919999718666077,"b3_kas_warenhuis":false,"b3_mutatie_ahn3_ahn4":false,"b3_nodata_fractie_ahn3":0.002518891589716077,"b3_nodata_fractie_ahn4":0.0,"b3_nodata_radius_ahn3":0.359510600566864,"b3_nodata_radius_ahn4":0.34349295496940613,"b3_opp_buitenmuur":165.03,"b3_opp_dak_plat":51.38,"b3_opp_dak_schuin":63.5,"b3_opp_grond":99.21,"b3_opp_scheidingsmuur":129.53,"b3_puntdichtheid_ahn3":16.353534698486328,"b3_puntdichtheid_ahn4":46.19647216796875,"b3_pw_bron":"AHN4","b3_pw_datum":2020,"b3_pw_selectie_reden":"PREFERRED_AND_LATEST","b3_reconstructie_onvolledig":false,"b3_rmse_lod12":3.2317864894866943,"b3_rmse_lod13":0.642620861530304,"b3_rmse_lod22":0.09925124794244766,"b3_val3dity_lod12":"[]","b3_val3dity_lod13":"[]","b3_val3dity_lod22":"[]","b3_volume_lod12":845.0095825195312,"b3_volume_lod13":657.8263549804688,"b3_volume_lod22":636.9927368164062,"begingeldigheid":"1999-04-28","documentdatum":"1999-04-28","documentnummer":"408040.tif","eindgeldigheid":null,"eindregistratie":null,"geconstateerd":false,"identificatie":"NL.IMBAG.Pand.0503100000005156","oorspronkelijkbouwjaar":2000,"status":"Pand in gebruik","tijdstipeindregistratielv":null,"tijdstipinactief":null,"tijdstipinactieflv":null,"tijdstipnietbaglv":null,"tijdstipregistratie":"2010-10-13T12:29:24Z","tijdstipregistratielv":"2010-10-13T12:30:50Z","voorkomenidentificatie":1},"geometry":[{"type":"MultiSurface","lod":"0","boundaries":[[[0,1,2,3,4,5]]]}],"children":["NL.IMBAG.Pand.0503100000005156-0"]}},"vertices":[[-353581,253246,-44957],[-348730,242291,-44957],[-343550,244604,-44957],[-344288,246257,-44957],[-341437,247537,-44957],[-345635,256798,-44957],[-343558,244600,-44957],[-343662,244854,-44957],[-343926,244734,-44957],[-345635,256798,-36439],[-353581,253246,-36439],[-348730,242291,-36439],[-344288,246257,-36439],[-341437,247537,-36439],[-343662,244854,-36439],[-343926,244734,-36439],[-343558,244600,-36439],[-352596,251020,-44957],[-344083,246349,-44957],[-345635,256798,-41490],[-353581,253246,-41490],[-352596,251020,-35952],[-352596,251020,-41490],[-348730,242291,-35952],[-343662,244854,-35952],[-344288,246257,-35952],[-343926,244734,-35952],[-347233,253386,-35952],[-347233,253386,-41490],[-341437,247537,-41490],[-344083,246349,-41490],[-343558,244600,-35952],[-344083,246349,-35952],[-347089,253741,-35952],[-347089,253741,-41490],[-350613,246543,-44957],[-345635,256798,-41507],[-353581,253246,-41516],[-350613,246543,-34688],[-348730,242291,-36953],[-343662,244854,-37089],[-344288,246257,-37099],[-343926,244734,-36944],[-352596,251020,-41514],[-347233,253386,-37262],[-347233,253386,-41508],[-352596,251020,-37264],[-341437,247537,-41498],[-344083,246349,-41501],[-343558,244600,-37083],[-344083,246349,-37212],[-347089,253741,-37402],[-347089,253741,-41508],[-349425,246738,-34864],[-349425,246738,-34529],[-349862,246897,-34699],[-349238,248437,-35307]]}"#,
1201        )?;
1202
1203        let mut attr_schema = AttributeSchema::new();
1204        for (_, co) in cj_city_feature.city_objects.iter() {
1205            if let Some(attr) = &co.attributes {
1206                attr_schema.add_attributes(attr);
1207            }
1208        }
1209
1210        // Create FlatBuffer and encode
1211        let mut fbb = FlatBufferBuilder::new();
1212
1213        let (city_feature, _) =
1214            to_fcb_city_feature(&mut fbb, "test_id", &cj_city_feature, &attr_schema, None);
1215
1216        fbb.finish(city_feature, None);
1217        let buf = fbb.finished_data();
1218
1219        // Get encoded city object
1220        let fb_city_feature = root_as_city_feature(buf).unwrap();
1221        assert_eq!("test_id", fb_city_feature.id());
1222        assert_eq!(
1223            cj_city_feature.city_objects.len(),
1224            fb_city_feature.objects().unwrap().len()
1225        );
1226
1227        assert_eq!(
1228            cj_city_feature.vertices.len(),
1229            fb_city_feature.vertices().unwrap().len()
1230        );
1231        assert_eq!(
1232            cj_city_feature.vertices[0][0],
1233            fb_city_feature.vertices().unwrap().get(0).x() as i64,
1234        );
1235        assert_eq!(
1236            cj_city_feature.vertices[0][1],
1237            fb_city_feature.vertices().unwrap().get(0).y() as i64,
1238        );
1239        assert_eq!(
1240            cj_city_feature.vertices[0][2],
1241            fb_city_feature.vertices().unwrap().get(0).z() as i64,
1242        );
1243
1244        assert_eq!(
1245            cj_city_feature.vertices[1][0],
1246            fb_city_feature.vertices().unwrap().get(1).x() as i64,
1247        );
1248        assert_eq!(
1249            cj_city_feature.vertices[1][1],
1250            fb_city_feature.vertices().unwrap().get(1).y() as i64,
1251        );
1252        assert_eq!(
1253            cj_city_feature.vertices[1][2],
1254            fb_city_feature.vertices().unwrap().get(1).z() as i64,
1255        );
1256
1257        // iterate over city objects and check if the fields are correct
1258        for (id, cjco) in cj_city_feature.city_objects.iter() {
1259            let fb_city_object = fb_city_feature
1260                .objects()
1261                .unwrap()
1262                .iter()
1263                .find(|co| co.id() == id)
1264                .unwrap();
1265            assert_eq!(id, fb_city_object.id());
1266            assert_eq!(cjco.thetype, to_cj_co_type(fb_city_object.type_(), None));
1267
1268            //TODO: check attributes later
1269
1270            let fb_geometry = fb_city_object.geometry().unwrap();
1271            for fb_geometry in fb_geometry.iter() {
1272                let cj_geometry = cjco
1273                    .geometry
1274                    .as_ref()
1275                    .unwrap()
1276                    .iter()
1277                    .find(|g| g.lod() == fb_geometry.lod())
1278                    .unwrap();
1279                assert_eq!(
1280                    cj_geometry.geometry_type(),
1281                    fb_geometry
1282                        .type_()
1283                        .to_cj()
1284                        .expect("a written geometry has a known type")
1285                );
1286            }
1287
1288            if let Some(parents) = cjco.parents.as_ref() {
1289                for parent in fb_city_object.parents().unwrap().iter() {
1290                    assert!(parents.contains(&parent.to_string()));
1291                }
1292            }
1293
1294            if let Some(children) = cjco.children.as_ref() {
1295                for child in fb_city_object.children().unwrap().iter() {
1296                    assert!(children.contains(&child.to_string()));
1297                }
1298            }
1299
1300            if let Some(ge) = cjco.geographical_extent.as_ref() {
1301                // Check min x,y,z
1302                assert_eq!(
1303                    ge[0],
1304                    fb_city_object.geographical_extent().unwrap().min().x()
1305                );
1306                assert_eq!(
1307                    ge[1],
1308                    fb_city_object.geographical_extent().unwrap().min().y()
1309                );
1310                assert_eq!(
1311                    cjco.geographical_extent.as_ref().unwrap()[2],
1312                    fb_city_object.geographical_extent().unwrap().min().z()
1313                );
1314
1315                // Check max x,y,z
1316                assert_eq!(
1317                    cjco.geographical_extent.as_ref().unwrap()[3],
1318                    fb_city_object.geographical_extent().unwrap().max().x()
1319                );
1320                assert_eq!(
1321                    cjco.geographical_extent.as_ref().unwrap()[4],
1322                    fb_city_object.geographical_extent().unwrap().max().y()
1323                );
1324                assert_eq!(
1325                    cjco.geographical_extent.as_ref().unwrap()[5],
1326                    fb_city_object.geographical_extent().unwrap().max().z()
1327                );
1328            }
1329        }
1330
1331        Ok(())
1332    }
1333
1334    /// `metadata.schema.json` types `pointOfContact.address` as a bare object
1335    /// with no named members, so BOTH `postcode` (the spec's own examples) and
1336    /// `postalCode` (CityJSON 1.x's) are schema-valid spellings and the writer
1337    /// accepts either. The header has one fixed slot for them, so the reader
1338    /// has to pick one spelling to write back, and it picks `postcode`.
1339    ///
1340    /// That means a document spelling it `postalCode` round-trips RENAMED --
1341    /// a deliberate normalisation, not a loss, and the same shape of choice as
1342    /// the texture `image` empty-vs-absent one. Pinned here so that changing
1343    /// either half of it is a test failure rather than a silent behaviour
1344    /// change for anyone diffing input against output.
1345    #[test]
1346    fn either_postcode_spelling_is_accepted_and_both_come_back_as_postcode() -> Result<()> {
1347        let address_after_round_trip = |address: serde_json::Value| -> Result<cjseq::Address> {
1348            let cj: CityJSON = serde_json::from_value(serde_json::json!({
1349                "type": "CityJSON",
1350                "version": "2.0",
1351                "transform": {"scale": [1.0, 1.0, 1.0], "translate": [0.0, 0.0, 0.0]},
1352                "CityObjects": {},
1353                "vertices": [],
1354                "metadata": {
1355                    "pointOfContact": {
1356                        "contactName": "A Person",
1357                        "emailAddress": "a@example.org",
1358                        "address": address
1359                    }
1360                }
1361            }))?;
1362
1363            let mut fbb = FlatBufferBuilder::new();
1364            let header = to_fcb_header(
1365                &mut fbb,
1366                &cj,
1367                HeaderWriterOptions {
1368                    write_index: false,
1369                    feature_count: 0,
1370                    index_node_size: 16,
1371                    attribute_indices: None,
1372                    geographical_extent: None,
1373                },
1374                &AttributeSchema::new(),
1375                None,
1376                None,
1377            )?;
1378            fbb.finish(header, None);
1379            let buf = fbb.finished_data().to_vec();
1380            let header = flatbuffers::root::<crate::fb::Header>(&buf).unwrap();
1381            Ok(crate::reader::deserializer::to_cj_address(&header)
1382                .expect("the address has a postcode, so it is not empty"))
1383        };
1384
1385        for spelling in ["postcode", "postalCode"] {
1386            let address = address_after_round_trip(serde_json::json!({
1387                "locality": "Delft",
1388                spelling: "2628 CN"
1389            }))?;
1390            assert_eq!(
1391                address.members.get("postcode"),
1392                Some(&serde_json::json!("2628 CN")),
1393                "`{spelling}` must be accepted and written back as `postcode`"
1394            );
1395            assert_eq!(
1396                address.members.get("postalCode"),
1397                None,
1398                "the header has one postcode slot; `postalCode` is not also emitted"
1399            );
1400            //-- the neighbouring member is untouched by the normalisation
1401            assert_eq!(
1402                address.members.get("locality"),
1403                Some(&serde_json::json!("Delft"))
1404            );
1405        }
1406
1407        //-- and `postcode` wins when a document carries both, matching the
1408        //-- `.or_else()` order in `address_either`
1409        let address = address_after_round_trip(serde_json::json!({
1410            "postcode": "2628 CN",
1411            "postalCode": "1234 AB"
1412        }))?;
1413        assert_eq!(
1414            address.members.get("postcode"),
1415            Some(&serde_json::json!("2628 CN"))
1416        );
1417
1418        Ok(())
1419    }
1420}