Skip to main content

fcb_core/reader/
deserializer.rs

1use std::{collections::HashMap, mem::size_of};
2
3use crate::{
4    error::Error,
5    fb::*,
6    geom_decoder::{
7        decode_materials, decode_points, decode_rings, decode_semantics, decode_shells,
8        decode_solids, decode_surfaces, decode_textures,
9    },
10};
11use byteorder::{ByteOrder, LittleEndian};
12use cjseq::{
13    Address as CjAddress, Appearance as CjAppearance, BorderColor as CjBorderColor, CityJSON,
14    CityJSONFeature, CityObject as CjCityObject, CityObjectType as CjCityObjectType,
15    Color as CjColor, Geometry as CjGeometry, GeometryCommon as CjGeometryCommon,
16    GeometryTemplates as CjGeometryTemplates, MaterialObject as CjMaterialObject,
17    Metadata as CjMetadata, PointOfContact as CjPointOfContact,
18    ReferenceSystem as CjReferenceSystem, Semantics as CjSemantics,
19    TextureFormat as CjTextureFormat, TextureObject as CjTextureObject,
20    TextureType as CjTextureType, Transform as CjTransform, WrapMode as CjWrapMode,
21};
22use serde_json::{json, Value};
23
24use super::meta::{Column as MetaColumn, ColumnType as MetaColumnType, Meta};
25
26pub fn to_cj_metadata(header: &Header) -> Result<CityJSON, Error> {
27    let mut cj = CityJSON::new();
28    let semantic_attr_schema = header.semantic_columns();
29    if let Some(transform) = header.transform() {
30        let (scale, translate) = (transform.scale(), transform.translate());
31        cj.transform = CjTransform {
32            scale: vec![scale.x(), scale.y(), scale.z()],
33            translate: vec![translate.x(), translate.y(), translate.z()],
34        };
35    }
36
37    // Extract extensions if present
38    if let Some(extensions_vec) = header.extensions() {
39        let mut extensions_map = serde_json::Map::new();
40        for extension in extensions_vec.iter() {
41            if let Some(name) = extension.name() {
42                extensions_map.insert(
43                    name.to_string(),
44                    json!({
45                        "url": extension.url().unwrap_or_default(),
46                        "version": extension.version().unwrap_or_default(),
47                    }),
48                );
49            }
50        }
51
52        if !extensions_map.is_empty() {
53            cj.extensions = Some(Value::Object(extensions_map));
54        }
55    }
56
57    let reference_system = header.reference_system().map(|rs| {
58        CjReferenceSystem::new(
59            None,
60            rs.authority().unwrap_or_default().to_string(),
61            rs.version().to_string(),
62            rs.code().to_string(),
63        )
64    });
65    cj.version = header.version().to_string();
66
67    let geographical_extent = header
68        .geographical_extent()
69        .map(|extent| {
70            [
71                extent.min().x(),
72                extent.min().y(),
73                extent.min().z(),
74                extent.max().x(),
75                extent.max().y(),
76                extent.max().z(),
77            ]
78        })
79        .unwrap_or_default();
80
81    let point_of_contact = match header.poc_contact_name() {
82        Some(_) => Some(to_cj_point_of_contact(header)?),
83        None => None,
84    };
85
86    cj.metadata = Some(CjMetadata {
87        geographical_extent: Some(geographical_extent),
88        identifier: header.identifier().map(|i| i.to_string()),
89        point_of_contact,
90        reference_date: header.reference_date().map(|r| r.to_string()),
91        reference_system,
92        title: header.title().map(|t| t.to_string()),
93        other: HashMap::new(),
94    });
95
96    // The header's own appearance palette. The geometry templates decoded
97    // below index straight into it -- a template belongs to no feature, so its
98    // `material`/`texture` mapping can refer to nothing else. Emitting the
99    // templates while dropping this left those mappings dangling (finding #31).
100    cj.appearance = header.appearance().map(to_cj_appearance).transpose()?;
101
102    // Decode Geometry Templates if present
103    if let (Some(fb_templates), Some(fb_vertices)) =
104        (header.templates(), header.templates_vertices())
105    {
106        let templates = fb_templates
107            .iter()
108            .map(|g| decode_geometry(g, semantic_attr_schema)) // Use local decode_geometry
109            .collect::<Result<Vec<_>, _>>()?;
110
111        let vertices_templates = Value::Array(
112            fb_vertices
113                .iter()
114                .map(|v| json!([v.x(), v.y(), v.z()]))
115                .collect(),
116        );
117
118        cj.geometry_templates = Some(CjGeometryTemplates {
119            templates,
120            vertices_templates,
121        });
122    }
123
124    Ok(cj)
125}
126
127pub(crate) fn to_meta(header: Header) -> Result<Meta, Error> {
128    let columns = header.columns().map(|c| {
129        c.iter()
130            .map(|c| {
131                let i = c.index();
132                MetaColumn {
133                    index: i,
134                    name: c.name().to_string(),
135                    _type: match c.type_() {
136                        ColumnType::Int => MetaColumnType::Int,
137                        ColumnType::UInt => MetaColumnType::UInt,
138                        ColumnType::Bool => MetaColumnType::Bool,
139                        ColumnType::Float => MetaColumnType::Float,
140                        ColumnType::Double => MetaColumnType::Double,
141                        ColumnType::String => MetaColumnType::String,
142                        ColumnType::DateTime => MetaColumnType::DateTime,
143                        ColumnType::Json => MetaColumnType::Json,
144                        ColumnType::Binary => MetaColumnType::Binary,
145                        ColumnType::Short => MetaColumnType::Short,
146                        ColumnType::UShort => MetaColumnType::UShort,
147                        ColumnType::Long => MetaColumnType::Long,
148                        ColumnType::ULong => MetaColumnType::ULong,
149                        _ => unreachable!(),
150                    },
151                    title: c.title().map(|t| t.to_string()),
152                    description: c.description().map(|d| d.to_string()),
153                    precision: Some(c.precision()),
154                    scale: Some(c.scale()),
155                    nullable: Some(c.nullable()),
156                    unique: Some(c.unique()),
157                    primary_key: Some(c.primary_key()),
158                    metadata: c.metadata().map(|m| m.to_string()),
159                    attr_index: Some(
160                        header
161                            .attribute_index()
162                            .map(|attr_indices| attr_indices.iter().any(|i| i.index() == c.index()))
163                            .unwrap_or(false),
164                    ),
165                }
166            })
167            .collect::<Vec<_>>()
168    });
169    if columns.is_none() {
170        return Err(Error::MissingRequiredField("columns".to_string()));
171    }
172    Ok(Meta {
173        columns: columns.unwrap(),
174        feature_count: header.features_count(),
175    })
176}
177
178pub(crate) fn to_cj_point_of_contact(header: &Header) -> Result<CjPointOfContact, Error> {
179    Ok(CjPointOfContact {
180        contact_name: header
181            .poc_contact_name()
182            .ok_or(Error::MissingRequiredField("contact_name".to_string()))?
183            .to_string(),
184        contact_type: header.poc_contact_type().map(|ct| ct.to_string()),
185        role: header.poc_role().map(|r| r.to_string()),
186        phone: header.poc_phone().map(|p| p.to_string()),
187        email_address: header
188            .poc_email()
189            .ok_or(Error::MissingRequiredField("email_address".to_string()))?
190            .to_string(),
191        website: header.poc_website().map(|w| w.to_string()),
192        organization: None,
193        address: to_cj_address(header),
194        other: HashMap::new(),
195    })
196}
197
198/// Rebuilds the free-form `address` object from the header's fixed fields.
199/// Members the writer had nothing to write are simply absent, which is what
200/// the schema wants — it names no required member at all.
201pub(crate) fn to_cj_address(header: &Header) -> Option<CjAddress> {
202    let mut members: HashMap<String, Value> = HashMap::new();
203    let mut insert = |key: &str, value: Option<&str>| {
204        if let Some(value) = value.filter(|v| !v.is_empty()) {
205            members.insert(key.to_string(), Value::String(value.to_string()));
206        }
207    };
208    insert(
209        "thoroughfareNumber",
210        header.poc_address_thoroughfare_number(),
211    );
212    insert("thoroughfareName", header.poc_address_thoroughfare_name());
213    insert("locality", header.poc_address_locality());
214    insert("postcode", header.poc_address_postcode());
215    insert("country", header.poc_address_country());
216
217    if members.is_empty() {
218        None
219    } else {
220        Some(CjAddress { members })
221    }
222}
223
224/// The CityJSON spelling of a FlatBuffers City Object type.
225///
226/// `ExtensionObject` carries its CityJSON name in `extension_type`, which the
227/// spec requires to start with `+`.
228///
229/// UNKNOWN-TAG POLICY, third of three sites (see
230/// [`crate::reader::geom_decoder::GeometryType::to_cj`]). Like a semantic
231/// surface and unlike a geometry, a City Object type has an extension escape
232/// hatch (§ 8 Extensions), so an `ExtensionObject` whose `extension_type`
233/// string is missing still has a schema-valid spelling available and gets one
234/// rather than an error. `"+UnknownCityObject"` and not `"ExtensionObject"`:
235/// the latter is the FlatBuffers enumerator name, is not a CityJSON City
236/// Object type, and carries no `+`, so a document containing it fails
237/// validation. The C++ reader emits the same string.
238pub(crate) fn to_cj_co_type(
239    co_type: CityObjectType,
240    extension_type: Option<&str>,
241) -> CjCityObjectType {
242    match co_type {
243        CityObjectType::Bridge => CjCityObjectType::Bridge,
244        CityObjectType::BridgePart => CjCityObjectType::BridgePart,
245        CityObjectType::BridgeInstallation => CjCityObjectType::BridgeInstallation,
246        CityObjectType::BridgeConstructiveElement => CjCityObjectType::BridgeConstructiveElement,
247        CityObjectType::BridgeRoom => CjCityObjectType::BridgeRoom,
248        CityObjectType::BridgeFurniture => CjCityObjectType::BridgeFurniture,
249        CityObjectType::Building => CjCityObjectType::Building,
250        CityObjectType::BuildingPart => CjCityObjectType::BuildingPart,
251        CityObjectType::BuildingInstallation => CjCityObjectType::BuildingInstallation,
252        CityObjectType::BuildingConstructiveElement => {
253            CjCityObjectType::BuildingConstructiveElement
254        }
255        CityObjectType::BuildingFurniture => CjCityObjectType::BuildingFurniture,
256        CityObjectType::BuildingStorey => CjCityObjectType::BuildingStorey,
257        CityObjectType::BuildingRoom => CjCityObjectType::BuildingRoom,
258        CityObjectType::BuildingUnit => CjCityObjectType::BuildingUnit,
259        CityObjectType::CityFurniture => CjCityObjectType::CityFurniture,
260        CityObjectType::CityObjectGroup => CjCityObjectType::CityObjectGroup,
261        CityObjectType::GenericCityObject => CjCityObjectType::GenericCityObject,
262        CityObjectType::LandUse => CjCityObjectType::LandUse,
263        CityObjectType::OtherConstruction => CjCityObjectType::OtherConstruction,
264        CityObjectType::PlantCover => CjCityObjectType::PlantCover,
265        CityObjectType::SolitaryVegetationObject => CjCityObjectType::SolitaryVegetationObject,
266        CityObjectType::TINRelief => CjCityObjectType::TINRelief,
267        CityObjectType::Road => CjCityObjectType::Road,
268        CityObjectType::Railway => CjCityObjectType::Railway,
269        CityObjectType::Waterway => CjCityObjectType::Waterway,
270        CityObjectType::TransportSquare => CjCityObjectType::TransportSquare,
271        CityObjectType::Tunnel => CjCityObjectType::Tunnel,
272        CityObjectType::TunnelPart => CjCityObjectType::TunnelPart,
273        CityObjectType::TunnelInstallation => CjCityObjectType::TunnelInstallation,
274        CityObjectType::TunnelConstructiveElement => CjCityObjectType::TunnelConstructiveElement,
275        CityObjectType::TunnelHollowSpace => CjCityObjectType::TunnelHollowSpace,
276        CityObjectType::TunnelFurniture => CjCityObjectType::TunnelFurniture,
277        CityObjectType::WaterBody => CjCityObjectType::WaterBody,
278        // ExtensionObject, and any tag a newer writer may add.
279        _ => {
280            CjCityObjectType::Extension(extension_type.unwrap_or("+UnknownCityObject").to_string())
281        }
282    }
283}
284
285pub fn decode_attributes(
286    columns: &flatbuffers::Vector<'_, flatbuffers::ForwardsUOffset<Column<'_>>>,
287    attributes: flatbuffers::Vector<'_, u8>,
288) -> serde_json::Value {
289    if attributes.is_empty() {
290        return serde_json::Value::Object(serde_json::Map::new());
291    }
292
293    let mut map = serde_json::Map::new();
294    let bytes = attributes.bytes();
295    let mut offset = 0;
296    while offset < bytes.len() {
297        let col_index = LittleEndian::read_u16(&bytes[offset..offset + size_of::<u16>()]) as u16;
298        offset += size_of::<u16>();
299        if col_index >= columns.len() as u16 {
300            panic!("column index out of range"); //TODO: handle this as an error
301        }
302        let column = columns.iter().find(|c| c.index() == col_index);
303        if column.is_none() {
304            panic!("column not found"); //TODO: handle this as an error
305        }
306        let column = column.unwrap();
307        match column.type_() {
308            ColumnType::Int => {
309                map.insert(
310                    column.name().to_string(),
311                    serde_json::Value::Number(serde_json::Number::from(LittleEndian::read_i32(
312                        &bytes[offset..offset + size_of::<i32>()],
313                    ))),
314                );
315                offset += size_of::<i32>();
316            }
317            ColumnType::UInt => {
318                map.insert(
319                    column.name().to_string(),
320                    serde_json::Value::Number(serde_json::Number::from(LittleEndian::read_u32(
321                        &bytes[offset..offset + size_of::<u32>()],
322                    ))),
323                );
324                offset += size_of::<u32>();
325            }
326            ColumnType::Bool => {
327                map.insert(
328                    column.name().to_string(),
329                    serde_json::Value::Bool(bytes[offset] != 0),
330                );
331                offset += size_of::<u8>();
332            }
333            ColumnType::Short => {
334                map.insert(
335                    column.name().to_string(),
336                    serde_json::Value::Number(serde_json::Number::from(LittleEndian::read_i16(
337                        &bytes[offset..offset + size_of::<i16>()],
338                    ))),
339                );
340                offset += size_of::<i16>();
341            }
342            ColumnType::UShort => {
343                map.insert(
344                    column.name().to_string(),
345                    serde_json::Value::Number(serde_json::Number::from(LittleEndian::read_u16(
346                        &bytes[offset..offset + size_of::<u16>()],
347                    ))),
348                );
349                offset += size_of::<u16>();
350            }
351            ColumnType::Long => {
352                map.insert(
353                    column.name().to_string(),
354                    serde_json::Value::Number(serde_json::Number::from(LittleEndian::read_i64(
355                        &bytes[offset..offset + size_of::<i64>()],
356                    ))),
357                );
358                offset += size_of::<i64>();
359            }
360            ColumnType::ULong => {
361                map.insert(
362                    column.name().to_string(),
363                    serde_json::Value::Number(serde_json::Number::from(LittleEndian::read_u64(
364                        &bytes[offset..offset + size_of::<u64>()],
365                    ))),
366                );
367                offset += size_of::<u64>();
368            }
369            ColumnType::Float => {
370                let f = LittleEndian::read_f32(&bytes[offset..offset + size_of::<f32>()]);
371                if let Some(num) = serde_json::Number::from_f64(f as f64) {
372                    map.insert(column.name().to_string(), serde_json::Value::Number(num));
373                }
374                offset += size_of::<f32>();
375            }
376            ColumnType::Double => {
377                let f = LittleEndian::read_f64(&bytes[offset..offset + size_of::<f64>()]);
378                if let Some(num) = serde_json::Number::from_f64(f) {
379                    map.insert(column.name().to_string(), serde_json::Value::Number(num));
380                }
381                offset += size_of::<f64>();
382            }
383            ColumnType::String => {
384                let len = LittleEndian::read_u32(&bytes[offset..offset + size_of::<u32>()]);
385                offset += size_of::<u32>();
386                let s = String::from_utf8(bytes[offset..offset + len as usize].to_vec())
387                    .unwrap_or_default();
388                map.insert(column.name().to_string(), serde_json::Value::String(s));
389                offset += len as usize;
390            }
391            ColumnType::DateTime => {
392                let len = LittleEndian::read_u32(&bytes[offset..offset + size_of::<u32>()]);
393                offset += size_of::<u32>();
394                let s = String::from_utf8(bytes[offset..offset + len as usize].to_vec())
395                    .unwrap_or_default();
396                map.insert(column.name().to_string(), serde_json::Value::String(s));
397                offset += len as usize;
398            }
399            ColumnType::Json => {
400                let len = LittleEndian::read_u32(&bytes[offset..offset + size_of::<u32>()]);
401                offset += size_of::<u32>();
402                let s = String::from_utf8(bytes[offset..offset + len as usize].to_vec())
403                    .unwrap_or_default();
404                map.insert(column.name().to_string(), serde_json::from_str(&s).unwrap());
405                offset += len as usize;
406            }
407
408            // These are emitted by the writer, so the reader must handle
409            // them: panicking here made any file containing such an attribute
410            // unreadable by the implementation that wrote it.
411            // `Byte` is stored as `u8` by the writer (`writer/attribute.rs`
412            // writes `out[offset] = b as u8`) and the index path
413            // (`reader/attr_query.rs`) already reads it back as `u8`, so this
414            // arm must too: decoding it as `i8` turned a stored 200 into -56.
415            ColumnType::Byte => {
416                map.insert(
417                    column.name().to_string(),
418                    serde_json::Value::Number(serde_json::Number::from(bytes[offset])),
419                );
420                offset += size_of::<u8>();
421            }
422            ColumnType::UByte => {
423                map.insert(
424                    column.name().to_string(),
425                    serde_json::Value::Number(serde_json::Number::from(bytes[offset])),
426                );
427                offset += size_of::<u8>();
428            }
429            ColumnType::Binary => {
430                let len = LittleEndian::read_u32(&bytes[offset..offset + size_of::<u32>()]);
431                offset += size_of::<u32>();
432                let raw = &bytes[offset..offset + len as usize];
433                map.insert(
434                    column.name().to_string(),
435                    serde_json::Value::Array(
436                        raw.iter()
437                            .map(|b| serde_json::Value::Number(serde_json::Number::from(*b)))
438                            .collect(),
439                    ),
440                );
441                offset += len as usize;
442            }
443            // An unknown ColumnType has no known width, so the remainder of
444            // the blob cannot be parsed. Stop rather than guess.
445            _ => {
446                return serde_json::Value::Object(map);
447            }
448        }
449    }
450
451    serde_json::Value::Object(map)
452}
453
454/// A material colour: `appearance.schema.json` fixes `diffuseColor`,
455/// `emissiveColor` and `specularColor` at exactly three numbers, which
456/// `cjseq::Color` spells as `[f64; 3]`. A stored vector of any other length is
457/// not a colour and is dropped rather than padded or truncated.
458fn to_color(color: Option<flatbuffers::Vector<'_, f64>>) -> Option<CjColor> {
459    let values: Vec<f64> = color?.iter().collect();
460    values.try_into().ok()
461}
462
463/// A texture's `borderColor`, which the schema allows to be three *or* four
464/// numbers (`"minItems": 3, "maxItems": 4`) -- the one CityJSON colour that is
465/// not fixed at three.
466fn to_border_color(color: Option<flatbuffers::Vector<'_, f64>>) -> Option<CjBorderColor> {
467    let values: Vec<f64> = color?.iter().collect();
468    match values.len() {
469        3 => values.try_into().ok().map(CjBorderColor::Rgb),
470        4 => values.try_into().ok().map(CjBorderColor::Rgba),
471        _ => None,
472    }
473}
474
475/// The CityJSON `wrapMode` for a FlatCityBuf tag.
476///
477/// The inverse of `serializer::fb_wrap_mode`. A FlatBuffers enumeration is
478/// generated as a newtype over `u8`, not as a Rust `enum`, so this direction
479/// cannot be made exhaustive by the compiler the way its mirror is. The `_`
480/// arm therefore *errors*: it does not fall back to a default, which is how a
481/// texture written `"wrapMode": "wrap"` used to come back as `"None"`. An
482/// unrecognised tag means the file was written by a newer writer or is
483/// corrupt, and the caller is told so.
484fn cj_wrap_mode(tag: WrapMode) -> Result<CjWrapMode, Error> {
485    Ok(match tag {
486        WrapMode::None => CjWrapMode::None,
487        WrapMode::Wrap => CjWrapMode::Wrap,
488        WrapMode::Mirror => CjWrapMode::Mirror,
489        WrapMode::Clamp => CjWrapMode::Clamp,
490        WrapMode::Border => CjWrapMode::Border,
491        _ => return Err(Error::UnknownEnumTag("wrapMode", format!("{tag:?}"))),
492    })
493}
494
495/// The CityJSON `textureType` for a FlatCityBuf tag. Errors on an unknown tag,
496/// as above.
497fn cj_texture_type(tag: TextureType) -> Result<CjTextureType, Error> {
498    Ok(match tag {
499        TextureType::Unknown => CjTextureType::Unknown,
500        TextureType::Specific => CjTextureType::Specific,
501        TextureType::Typical => CjTextureType::Typical,
502        _ => return Err(Error::UnknownEnumTag("textureType", format!("{tag:?}"))),
503    })
504}
505
506/// The CityJSON texture `type` for a FlatCityBuf tag. Errors on an unknown
507/// tag, as above.
508fn cj_texture_format(tag: TextureFormat) -> Result<CjTextureFormat, Error> {
509    Ok(match tag {
510        TextureFormat::PNG => CjTextureFormat::PNG,
511        TextureFormat::JPG => CjTextureFormat::JPG,
512        _ => return Err(Error::UnknownEnumTag("type", format!("{tag:?}"))),
513    })
514}
515
516pub fn to_cj_feature(
517    feature: CityFeature,
518    root_attr_schema: Option<flatbuffers::Vector<'_, flatbuffers::ForwardsUOffset<Column<'_>>>>,
519    semantic_attr_schema: Option<flatbuffers::Vector<'_, flatbuffers::ForwardsUOffset<Column<'_>>>>,
520) -> Result<CityJSONFeature, Error> {
521    // Ensure function returns Result
522    let mut cj = CityJSONFeature::new();
523    cj.id = feature.id().to_string();
524
525    if let Some(objects) = feature.objects() {
526        let city_objects_result: Result<HashMap<String, CjCityObject>, Error> = objects
527            .iter()
528            .map(|co| {
529                let geographical_extent = co.geographical_extent().map(|extent| {
530                    [
531                        extent.min().x(),
532                        extent.min().y(),
533                        extent.min().z(),
534                        extent.max().x(),
535                        extent.max().y(),
536                        extent.max().z(),
537                    ]
538                });
539
540                let mut all_geometries: Vec<cjseq::Geometry> = Vec::new();
541
542                // Process standard geometries
543                if let Some(standard_geometries) = co.geometry() {
544                    let decoded_standard = standard_geometries
545                        .iter()
546                        .map(|g| decode_geometry(g, semantic_attr_schema)) // Returns Result<CjGeometry, Error>
547                        .collect::<Result<Vec<_>, _>>()?; // Collect Results, propagate error
548                    all_geometries.extend(decoded_standard);
549                }
550
551                // Process geometry instances
552                if let Some(instances) = co.geometry_instances() {
553                    let decoded_instances = instances
554                        .iter()
555                        .map(|inst| decode_geometry_instance(&inst)) // Use reference, returns Result<CjGeometry, Error>
556                        .collect::<Result<Vec<_>, _>>()?; // Collect Results, propagate error
557                    all_geometries.extend(decoded_instances);
558                }
559
560                let final_geometries = if all_geometries.is_empty() {
561                    None
562                } else {
563                    Some(all_geometries)
564                };
565
566                let attributes = if root_attr_schema.is_none() && co.columns().is_none() {
567                    None
568                } else {
569                    co.attributes().map(|a| {
570                        decode_attributes(&co.columns().unwrap_or(root_attr_schema.unwrap()), a)
571                    })
572                };
573
574                // An unspecified role is `null` in CityJSON, and the writer
575                // spells that as the empty string.
576                let children_roles = co.children_roles().map(|c| {
577                    c.iter()
578                        .map(|s| (!s.is_empty()).then(|| s.to_string()))
579                        .collect()
580                });
581
582                let mut cjco = CjCityObject::new(to_cj_co_type(co.type_(), co.extension_type()));
583                cjco.geographical_extent = geographical_extent;
584                cjco.attributes = attributes;
585                cjco.geometry = final_geometries;
586                cjco.children = co
587                    .children()
588                    .map(|c| c.iter().map(|s| s.to_string()).collect());
589                cjco.children_roles = children_roles;
590                cjco.parents = co
591                    .parents()
592                    .map(|p| p.iter().map(|s| s.to_string()).collect());
593                Ok((co.id().to_string(), cjco)) // Return Result for map operation
594            })
595            .collect(); // Collect Results from map
596
597        let city_objects = city_objects_result?;
598        cj.city_objects = city_objects;
599    }
600
601    cj.vertices = feature
602        .vertices()
603        .map_or(Vec::new(), |v| to_cj_vertices(v.iter().collect()));
604
605    // Decode appearance if present
606    cj.appearance = feature.appearance().map(to_cj_appearance).transpose()?;
607
608    Ok(cj) // Return Result
609}
610
611/// Decode one FlatBuffers `Appearance` table into its CityJSON form.
612///
613/// Shared by both callers on purpose: a `Header` and a `CityFeature` each carry
614/// an `Appearance`, and the two must decode identically -- notably the
615/// `"image": "" -> absent` choice documented below, which the C++ reader
616/// mirrors. Duplicating this let the header path drift silently once already
617/// (finding #31: the header's palette was written but never read back).
618pub(crate) fn to_cj_appearance(appearance: Appearance) -> Result<CjAppearance, Error> {
619    let mut cj_appearance = CjAppearance {
620        materials: None,
621        textures: None,
622        vertices_texture: None,
623        default_theme_texture: None,
624        default_theme_material: None,
625    };
626
627    {
628        // Decode materials. `name` is the schema's one required member; every
629        // other one is optional, and an absent one stays absent rather than
630        // reappearing as `null`.
631        if let Some(materials) = appearance.materials() {
632            let cj_materials = materials
633                .iter()
634                .map(|m| CjMaterialObject {
635                    name: m.name().to_string(),
636                    ambient_intensity: m.ambient_intensity(),
637                    diffuse_color: to_color(m.diffuse_color()),
638                    emissive_color: to_color(m.emissive_color()),
639                    specular_color: to_color(m.specular_color()),
640                    shininess: m.shininess(),
641                    transparency: m.transparency(),
642                    is_smooth: m.is_smooth(),
643                })
644                .collect();
645
646            cj_appearance.materials = Some(cj_materials);
647        }
648
649        // Decode textures
650        if let Some(textures) = appearance.textures() {
651            let cj_textures = textures
652                .iter()
653                .map(|t| {
654                    Ok(CjTextureObject {
655                        thetype: Some(cj_texture_format(t.type_())?),
656                        // `image` is mandatory in the `.fbs` but optional in
657                        // CityJSON, so the writer stores `""` both for an
658                        // absent `image` and for a schema-valid `"image": ""`.
659                        // The two are indistinguishable on the wire and one of
660                        // them must be lost; decoding `""` back to *absent* is
661                        // a deliberate choice, not a derivation. It inverts
662                        // which side loses: `absent -> ""` before, `"" ->
663                        // absent` now. Both outputs are schema-valid, and not
664                        // inventing a member the input lacked is the better
665                        // default. The C++ reader must mirror this choice.
666                        image: Some(t.image())
667                            .filter(|i| !i.is_empty())
668                            .map(str::to_string),
669                        wrap_mode: t.wrap_mode().map(cj_wrap_mode).transpose()?,
670                        texture_type: t.texture_type().map(cj_texture_type).transpose()?,
671                        border_color: to_border_color(t.border_color()),
672                    })
673                })
674                .collect::<Result<Vec<_>, Error>>()?;
675
676            cj_appearance.textures = Some(cj_textures);
677        }
678
679        // Decode vertices_texture
680        if let Some(vertices_texture) = appearance.vertices_texture() {
681            cj_appearance.vertices_texture = Some(
682                vertices_texture
683                    .iter()
684                    .map(|v| [v.u(), v.v()])
685                    .collect::<Vec<_>>(),
686            );
687        }
688
689        // Decode default themes
690        if let Some(default_theme_texture) = appearance.default_theme_texture() {
691            cj_appearance.default_theme_texture = Some(default_theme_texture.to_string());
692        }
693
694        if let Some(default_theme_material) = appearance.default_theme_material() {
695            cj_appearance.default_theme_material = Some(default_theme_material.to_string());
696        }
697    }
698
699    Ok(cj_appearance)
700}
701
702pub(crate) fn decode_geometry(
703    g: Geometry,
704    semantic_attr_schema: Option<flatbuffers::Vector<'_, flatbuffers::ForwardsUOffset<Column<'_>>>>,
705) -> Result<CjGeometry, Error> {
706    let solids = g
707        .solids()
708        .map(|v| v.iter().collect::<Vec<_>>())
709        .unwrap_or_default();
710    let shells = g
711        .shells()
712        .map(|v| v.iter().collect::<Vec<_>>())
713        .unwrap_or_default();
714    let surfaces = g
715        .surfaces()
716        .map(|v| v.iter().collect::<Vec<_>>())
717        .unwrap_or_default();
718    let strings = g
719        .strings()
720        .map(|v| v.iter().collect::<Vec<_>>())
721        .unwrap_or_default();
722    let indices = g
723        .boundaries()
724        .map(|v| v.iter().collect::<Vec<_>>())
725        .unwrap_or_default();
726    let geometry_type = g.type_();
727
728    // The surfaces decide whether there is a `semantics` member at all; the
729    // values vector being absent is `"values": null`, which is a member with a
730    // null value rather than no member.
731    let semantics: Option<CjSemantics> = g.semantics_objects().map(|semantics_objects| {
732        let semantics_objects = semantics_objects.iter().collect::<Vec<_>>();
733        let semantics_values = g.semantics().map(|v| v.iter().collect::<Vec<_>>());
734        decode_semantics(
735            &solids,
736            &shells,
737            geometry_type,
738            semantics_objects,
739            semantics_values,
740            semantic_attr_schema,
741        )
742    });
743
744    // Decode material mappings if present
745    let material = if let Some(material_mappings) = g.material() {
746        decode_materials(geometry_type, &material_mappings.iter().collect::<Vec<_>>())
747    } else {
748        None
749    };
750
751    // Decode texture mappings if present
752    let texture = if let Some(texture_mappings) = g.texture() {
753        decode_textures(geometry_type, &texture_mappings.iter().collect::<Vec<_>>())
754    } else {
755        None
756    };
757
758    let common = CjGeometryCommon {
759        semantics,
760        material,
761        texture,
762    };
763    let lod = g.lod().map(|v| v.to_string());
764
765    // The geometry type selects the depth of `boundaries`; nothing looks at
766    // which of the count arrays happen to be populated.
767    Ok(match geometry_type {
768        GeometryType::MultiPoint => CjGeometry::MultiPoint {
769            lod,
770            boundaries: decode_points(&indices),
771            common,
772        },
773        GeometryType::MultiLineString => CjGeometry::MultiLineString {
774            lod,
775            boundaries: decode_rings(&strings, &indices),
776            common,
777        },
778        GeometryType::MultiSurface => CjGeometry::MultiSurface {
779            lod,
780            boundaries: decode_surfaces(&surfaces, &strings, &indices),
781            common,
782        },
783        GeometryType::CompositeSurface => CjGeometry::CompositeSurface {
784            lod,
785            boundaries: decode_surfaces(&surfaces, &strings, &indices),
786            common,
787        },
788        GeometryType::MultiSolid => CjGeometry::MultiSolid {
789            lod,
790            boundaries: decode_solids(&solids, &shells, &surfaces, &strings, &indices),
791            common,
792        },
793        GeometryType::CompositeSolid => CjGeometry::CompositeSolid {
794            lod,
795            boundaries: decode_solids(&solids, &shells, &surfaces, &strings, &indices),
796            common,
797        },
798        GeometryType::Solid => CjGeometry::Solid {
799            lod,
800            boundaries: decode_shells(&shells, &surfaces, &strings, &indices),
801            common,
802        },
803        // UNKNOWN-TAG POLICY. A `GeometryInstance` never reaches here -- it is
804        // dispatched to `decode_geometry_instance` -- so anything left is a
805        // tag outside the eight CityJSON names, and there is no `+`-prefixed
806        // spelling the schema would accept for it (§ 3). Decoding it as a
807        // Solid, which this used to do, silently reads the boundaries at the
808        // wrong depth. See `GeometryType::to_cj` for the full argument.
809        other => return Err(Error::UnknownEnumTag("GeometryType", format!("{other:?}"))),
810    })
811}
812
813/// Decodes a FlatBuffers GeometryInstance into a CityJSON Geometry struct.
814///
815/// # Arguments
816///
817/// * `instance` - A reference to the FlatBuffers GeometryInstance object.
818///
819/// # Returns
820///
821/// A Result containing the CityJSON Geometry struct representing the instance,
822/// or an Error if decoding fails (e.g., missing required fields).
823pub(crate) fn decode_geometry_instance(instance: &GeometryInstance) -> Result<CjGeometry, Error> {
824    let template_index = instance.template();
825
826    let boundaries = match instance.boundaries() {
827        Some(fb_boundaries) => {
828            if fb_boundaries.len() != 1 {
829                return Err(Error::InvalidAttributeValue {
830                    msg: format!("geometryinstance boundaries should contain exactly one vertex index, found {}", fb_boundaries.len())
831                });
832            }
833            vec![fb_boundaries.get(0) as usize]
834        }
835        None => {
836            return Err(Error::MissingRequiredField(
837                "geometryinstance boundaries".to_string(),
838            ));
839        }
840    };
841
842    let fb_matrix = instance.transformation().ok_or_else(|| {
843        Error::MissingRequiredField("geometryinstance transformation field".to_string())
844    })?;
845
846    // Convert FlatBuffers TransformationMatrix struct to a [f64; 16] array
847    let transformation_matrix_array = [
848        fb_matrix.m00(),
849        fb_matrix.m01(),
850        fb_matrix.m02(),
851        fb_matrix.m03(),
852        fb_matrix.m10(),
853        fb_matrix.m11(),
854        fb_matrix.m12(),
855        fb_matrix.m13(),
856        fb_matrix.m20(),
857        fb_matrix.m21(),
858        fb_matrix.m22(),
859        fb_matrix.m23(),
860        fb_matrix.m30(),
861        fb_matrix.m31(),
862        fb_matrix.m32(),
863        fb_matrix.m33(),
864    ];
865
866    // A GeometryInstance has no lod, semantics, material or texture: they live
867    // on the template it refers to, which is why the variant does not have
868    // fields for them.
869    Ok(CjGeometry::GeometryInstance {
870        boundaries,
871        template: template_index as usize,
872        transformation_matrix: transformation_matrix_array,
873    })
874}
875
876pub(crate) fn to_cj_vertices(vertices: Vec<&Vertex>) -> Vec<Vec<i64>> {
877    vertices
878        .iter()
879        .map(|v| vec![v.x() as i64, v.y() as i64, v.z() as i64])
880        .collect()
881}
882
883#[cfg(test)]
884mod tests {
885    use super::*;
886    use anyhow::Result;
887
888    use flatbuffers::FlatBufferBuilder;
889    #[test]
890    fn test_decode_geometry_instance() -> Result<()> {
891        let mut fbb = FlatBufferBuilder::new();
892
893        // Create test transformation matrix
894        let transformation = TransformationMatrix::new(
895            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 10.0, 10.0, 10.0,
896            1.0, // Translation part (10,10,10)
897        );
898
899        // Create boundary with a single vertex index
900        let boundaries_vec = vec![42u32]; // Reference vertex index 42
901        let boundaries = fbb.create_vector(&boundaries_vec);
902
903        // Create a GeometryInstance
904        let geometry_instance = GeometryInstance::create(
905            &mut fbb,
906            &crate::fb::GeometryInstanceArgs {
907                template: 5, // Template index
908                transformation: Some(&transformation),
909                boundaries: Some(boundaries),
910            },
911        );
912
913        fbb.finish(geometry_instance, None);
914        let buf = fbb.finished_data();
915
916        // Get a reference to the created GeometryInstance
917        let geometry_instance = flatbuffers::root::<GeometryInstance>(buf).unwrap();
918
919        // Decode the instance
920        let cj_geometry = decode_geometry_instance(&geometry_instance)?;
921
922        // Verify the decoded geometry. The variant carries `template`,
923        // `transformationMatrix` and single-index boundaries by construction,
924        // so an irrefutable-let would be a weaker assertion than this match.
925        let CjGeometry::GeometryInstance {
926            boundaries,
927            template,
928            transformation_matrix: matrix,
929        } = cj_geometry
930        else {
931            panic!("expected a GeometryInstance");
932        };
933        assert_eq!(template, 5);
934        assert_eq!(boundaries, vec![42]);
935        assert_eq!(matrix[0], 1.0); // First element
936        assert_eq!(matrix[12], 10.0); // Translation X
937        assert_eq!(matrix[13], 10.0); // Translation Y
938        assert_eq!(matrix[14], 10.0); // Translation Z
939
940        Ok(())
941    }
942
943    #[test]
944    fn test_decode_geometry_instance_missing_boundaries() -> Result<()> {
945        let mut fbb = FlatBufferBuilder::new();
946
947        // Create test transformation matrix
948        let transformation = TransformationMatrix::new(
949            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
950        );
951
952        // Create a GeometryInstance WITHOUT boundaries
953        let geometry_instance = GeometryInstance::create(
954            &mut fbb,
955            &crate::fb::GeometryInstanceArgs {
956                template: 5,
957                transformation: Some(&transformation),
958                boundaries: None, // Missing boundaries
959            },
960        );
961
962        fbb.finish(geometry_instance, None);
963        let buf = fbb.finished_data();
964        let geometry_instance = flatbuffers::root::<GeometryInstance>(buf).unwrap();
965
966        // Decode and assert error
967        let result = decode_geometry_instance(&geometry_instance);
968        assert!(result.is_err());
969        match result.err().unwrap() {
970            Error::MissingRequiredField(field) => {
971                assert!(field.contains("geometryinstance boundaries"));
972            }
973            _ => panic!("Expected MissingRequiredField error"),
974        }
975
976        Ok(())
977    }
978
979    #[test]
980    fn test_decode_geometry_instance_missing_transformation() -> Result<()> {
981        let mut fbb = FlatBufferBuilder::new();
982
983        // Create boundary with a single vertex index
984        let boundaries_vec = vec![42u32];
985        let boundaries = fbb.create_vector(&boundaries_vec);
986
987        // Create a GeometryInstance WITHOUT transformation
988        let geometry_instance = GeometryInstance::create(
989            &mut fbb,
990            &crate::fb::GeometryInstanceArgs {
991                template: 5,
992                transformation: None, // Missing transformation
993                boundaries: Some(boundaries),
994            },
995        );
996
997        fbb.finish(geometry_instance, None);
998        let buf = fbb.finished_data();
999        let geometry_instance = flatbuffers::root::<GeometryInstance>(buf).unwrap();
1000
1001        // Decode and assert error
1002        let result = decode_geometry_instance(&geometry_instance);
1003        assert!(result.is_err());
1004        match result.err().unwrap() {
1005            Error::MissingRequiredField(field) => {
1006                assert!(field.contains("geometryinstance transformation field"));
1007            }
1008            _ => panic!("Expected MissingRequiredField error"),
1009        }
1010
1011        Ok(())
1012    }
1013
1014    #[test]
1015    fn test_decode_geometry_instance_invalid_boundaries() -> Result<()> {
1016        let mut fbb = FlatBufferBuilder::new();
1017
1018        // Create test transformation matrix
1019        let transformation = TransformationMatrix::new(
1020            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
1021        );
1022
1023        // --- Test Case 1: Zero boundaries ---
1024        let boundaries_vec_zero: Vec<u32> = vec![];
1025        let boundaries_zero = fbb.create_vector(&boundaries_vec_zero);
1026        let geometry_instance_zero = GeometryInstance::create(
1027            &mut fbb,
1028            &crate::fb::GeometryInstanceArgs {
1029                template: 5,
1030                transformation: Some(&transformation),
1031                boundaries: Some(boundaries_zero),
1032            },
1033        );
1034        fbb.finish(geometry_instance_zero, None);
1035        let buf_zero = fbb.finished_data();
1036        let instance_zero = flatbuffers::root::<GeometryInstance>(buf_zero).unwrap();
1037
1038        let result_zero = decode_geometry_instance(&instance_zero);
1039        assert!(result_zero.is_err());
1040        match result_zero.err().unwrap() {
1041            Error::InvalidAttributeValue { msg } => {
1042                assert!(msg.contains("should contain exactly one vertex index, found 0"));
1043            }
1044            _ => panic!("Expected InvalidAttributeValue error for zero boundaries"),
1045        }
1046
1047        // --- Test Case 2: Multiple boundaries ---
1048        fbb.reset(); // Reset builder for the next case
1049        let boundaries_vec_multi = vec![42u32, 43u32]; // Two indices
1050        let boundaries_multi = fbb.create_vector(&boundaries_vec_multi);
1051        // Recreate transformation as it's part of the buffer
1052        let transformation_multi = TransformationMatrix::new(
1053            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
1054        );
1055        let geometry_instance_multi = GeometryInstance::create(
1056            &mut fbb,
1057            &crate::fb::GeometryInstanceArgs {
1058                template: 5,
1059                transformation: Some(&transformation_multi),
1060                boundaries: Some(boundaries_multi),
1061            },
1062        );
1063        fbb.finish(geometry_instance_multi, None);
1064        let buf_multi = fbb.finished_data();
1065        let instance_multi = flatbuffers::root::<GeometryInstance>(buf_multi).unwrap();
1066
1067        let result_multi = decode_geometry_instance(&instance_multi);
1068        assert!(result_multi.is_err());
1069        match result_multi.err().unwrap() {
1070            Error::InvalidAttributeValue { msg } => {
1071                assert!(msg.contains("should contain exactly one vertex index, found 2"));
1072            }
1073            _ => panic!("Expected InvalidAttributeValue error for multiple boundaries"),
1074        }
1075
1076        Ok(())
1077    }
1078
1079    /// `Byte`, `UByte` and `Binary` attributes must decode exactly as the
1080    /// writer stored them. `writer/attribute.rs` emits one entry as
1081    /// `[u16 LE column index][payload]`, where the payload is a single raw
1082    /// byte for `Byte`/`UByte` (`out[offset] = b as u8`) and a `u32` LE
1083    /// length followed by that many raw bytes for `Binary`. `Byte` in
1084    /// particular is stored *unsigned*, so 200 must come back as 200 and not
1085    /// as -56.
1086    #[test]
1087    fn test_decode_attributes_byte_ubyte_binary() -> Result<()> {
1088        let mut fbb = FlatBufferBuilder::new();
1089
1090        let byte_name = fbb.create_string("b");
1091        let ubyte_name = fbb.create_string("ub");
1092        let binary_name = fbb.create_string("bin");
1093        let byte_col = Column::create(
1094            &mut fbb,
1095            &ColumnArgs {
1096                index: 0,
1097                name: Some(byte_name),
1098                type_: ColumnType::Byte,
1099                ..Default::default()
1100            },
1101        );
1102        let ubyte_col = Column::create(
1103            &mut fbb,
1104            &ColumnArgs {
1105                index: 1,
1106                name: Some(ubyte_name),
1107                type_: ColumnType::UByte,
1108                ..Default::default()
1109            },
1110        );
1111        let binary_col = Column::create(
1112            &mut fbb,
1113            &ColumnArgs {
1114                index: 2,
1115                name: Some(binary_name),
1116                type_: ColumnType::Binary,
1117                ..Default::default()
1118            },
1119        );
1120        let columns = fbb.create_vector(&[byte_col, ubyte_col, binary_col]);
1121
1122        // The attribute blob, laid out by hand in the writer's wire format.
1123        let mut blob: Vec<u8> = Vec::new();
1124        blob.extend_from_slice(&0u16.to_le_bytes()); // column 0: b
1125        blob.push(200);
1126        blob.extend_from_slice(&1u16.to_le_bytes()); // column 1: ub
1127        blob.push(200);
1128        blob.extend_from_slice(&2u16.to_le_bytes()); // column 2: bin
1129        blob.extend_from_slice(&2u32.to_le_bytes());
1130        blob.extend_from_slice(&[1, 255]);
1131        let attributes = fbb.create_vector(&blob);
1132
1133        let id = fbb.create_string("obj-1");
1134        let city_object = CityObject::create(
1135            &mut fbb,
1136            &CityObjectArgs {
1137                id: Some(id),
1138                attributes: Some(attributes),
1139                columns: Some(columns),
1140                ..Default::default()
1141            },
1142        );
1143        fbb.finish(city_object, None);
1144
1145        let city_object = flatbuffers::root::<CityObject>(fbb.finished_data()).unwrap();
1146        let columns = city_object.columns().unwrap();
1147        let decoded = decode_attributes(&columns, city_object.attributes().unwrap());
1148
1149        assert_eq!(decoded, json!({"b": 200, "ub": 200, "bin": [1, 255]}));
1150
1151        Ok(())
1152    }
1153}