ecitygml-io 0.0.2-alpha.5

IO operations for processing CityGML data.
Documentation
use crate::Error;
use crate::gml::codec::core::{
    GmlOccupancyProperty, deserialize_abstract_occupied_space, serialize_abstract_occupied_space,
};
use crate::gml::util::CombinedCityGmlElement;
use chrono::NaiveDate;
use ecitygml_core::model::construction::{
    AbstractConstruction, AsAbstractConstruction, AsAbstractConstructionMut,
};
use ecitygml_core::model::core::AsAbstractOccupiedSpace;
use egml::io::util::{Formatting, XmlElementSpans, XmlNodeContent, XmlNodeParts, serialize_inner};
use quick_xml::de;
use serde::{Deserialize, Serialize};

pub fn deserialize_abstract_construction(
    xml_document: &[u8],
    spans: &XmlElementSpans<CombinedCityGmlElement>,
) -> Result<AbstractConstruction, Error> {
    let (abstract_occupied_space_result, parsed_result) = rayon::join(
        || deserialize_abstract_occupied_space(xml_document, spans),
        || de::from_reader::<_, GmlAbstractConstruction>(xml_document).map_err(Error::from),
    );
    let abstract_occupied_space = abstract_occupied_space_result?;
    let parsed = parsed_result?;
    let mut abstract_construction =
        AbstractConstruction::from_abstract_occupied_space(abstract_occupied_space);

    abstract_construction.set_date_of_construction(parsed.date_of_construction);
    abstract_construction.set_date_of_demolition(parsed.date_of_demolition);
    abstract_construction.set_occupancies(parsed.occupancies.into_iter().map(Into::into).collect());

    Ok(abstract_construction)
}

pub fn serialize_abstract_construction(
    abstract_construction: &AbstractConstruction,
    formatting: Formatting,
) -> Result<XmlNodeParts, Error> {
    let mut xml_node_parts = serialize_abstract_occupied_space(
        abstract_construction.abstract_occupied_space(),
        formatting,
    )?;

    if let Some(raw) = serialize_inner(
        GmlAbstractConstruction::from(abstract_construction),
        formatting,
    )? {
        xml_node_parts.content.push(XmlNodeContent::Raw(raw));
    }

    Ok(xml_node_parts)
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct GmlAbstractConstruction {
    #[serde(
        rename(
            serialize = "con:dateOfConstruction",
            deserialize = "dateOfConstruction"
        ),
        skip_serializing_if = "Option::is_none"
    )]
    pub date_of_construction: Option<NaiveDate>,

    #[serde(
        rename(serialize = "con:dateOfDemolition", deserialize = "dateOfDemolition"),
        skip_serializing_if = "Option::is_none"
    )]
    pub date_of_demolition: Option<NaiveDate>,

    #[serde(
        rename(serialize = "con:occupancy", deserialize = "occupancy"),
        default
    )]
    pub occupancies: Vec<GmlOccupancyProperty>,
}

impl From<&AbstractConstruction> for GmlAbstractConstruction {
    fn from(item: &AbstractConstruction) -> Self {
        Self {
            date_of_construction: item.date_of_construction().copied(),
            date_of_demolition: item.date_of_demolition().copied(),
            occupancies: item
                .occupancies()
                .iter()
                .map(GmlOccupancyProperty::from)
                .collect(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::gml::codec::building::deserialize_building;
    use crate::gml::codec::core::deserialize_abstract_thematic_surface;
    use chrono::{Datelike, Timelike};
    use ecitygml_core::model::construction::AsAbstractConstruction;
    use ecitygml_core::model::core::{
        AsAbstractCityObject, AsAbstractFeature, AsAbstractFeatureWithLifespan,
        AsAbstractThematicSurface,
    };
    use egml::io::util::extract_xml_element_spans;
    use egml::model::base::Id;
    use quick_xml::{DeError, de};

    #[test]
    fn test_deserialize_basic_abstract_construction() {
        let xml_document = b"
<bldg:Building gml:id=\"DEBY_LOD2_4959457\">
      <creationDate>2025-03-02T15:16:17+01:00</creationDate>
      <externalReference>
        <ExternalReference>
          <targetResource>DEBYvAAAAABTMIyA</targetResource>
          <informationSystem>http://repository.gdi-de.org/schemas/adv/citygml/fdv/art.htm#_9100</informationSystem>
        </ExternalReference>
      </externalReference>
      <genericAttribute>
        <gen:StringAttribute>
          <gen:name>DatenquelleBodenhoehe</gen:name>
          <gen:value>1100</gen:value>
        </gen:StringAttribute>
      </genericAttribute>
      <con:height>
        <con:Height>
          <con:highReference>highestRoofEdge</con:highReference>
          <con:lowReference>lowestGroundPoint</con:lowReference>
          <con:status>measured</con:status>
          <con:value uom=\"urn:adv:uom:m\">19.408</con:value>
        </con:Height>
      </con:height>
    </bldg:Building>";

        let spans = extract_xml_element_spans(xml_document).expect("should work");
        let abstract_construction =
            deserialize_abstract_construction(xml_document, &spans).expect("should work");
        let creation_date = abstract_construction
            .creation_date()
            .expect("should have creation date");
        assert_eq!(creation_date.year(), 2025);
        assert_eq!(creation_date.month(), 3);
        assert_eq!(creation_date.day(), 2);
        assert_eq!(creation_date.hour(), 15);
        assert_eq!(creation_date.minute(), 16);
        assert_eq!(creation_date.second(), 17);
    }

    #[test]
    fn test_deserialize_abstract_construction_with_occupancy() {
        let xml_document = b"\
<bldg:Building gml:id=\"DEBY_LOD2_4959457\">
    <con:occupancy>
        <Occupancy>
            <numberOfOccupants>123</numberOfOccupants>
            <interval>myInterval</interval>
            <occupantType>myOccupantType</occupantType>
        </Occupancy>
    </con:occupancy>
</bldg:Building>";

        let spans = extract_xml_element_spans(xml_document).expect("should work");
        let abstract_construction =
            deserialize_abstract_construction(xml_document, &spans).expect("should work");

        assert_eq!(abstract_construction.occupancies().len(), 1);

        let occupancy = &abstract_construction.occupancies()[0];
        assert_eq!(occupancy.number_of_occupants(), 123);
        assert_eq!(
            occupancy.interval().expect("must exist").code().value(),
            "myInterval"
        );
        assert_eq!(
            occupancy
                .occupant_type()
                .expect("must exist")
                .code()
                .value(),
            "myOccupantType"
        );
    }

    #[test]
    fn test_serialize_abstract_construction_with_occupancy() {
        let xml_document = b"\
<bldg:Building gml:id=\"DEBY_LOD2_4959457\">
    <con:occupancy>
        <Occupancy>
            <numberOfOccupants>123</numberOfOccupants>
            <interval>myInterval</interval>
            <occupantType>myOccupantType</occupantType>
        </Occupancy>
    </con:occupancy>
</bldg:Building>";

        let spans = extract_xml_element_spans(xml_document).expect("should work");
        let abstract_construction =
            deserialize_abstract_construction(xml_document, &spans).expect("should work");

        let xml_node_parts =
            serialize_abstract_construction(&abstract_construction, Formatting::Compact)
                .expect("should serialize");
        let xml = egml::io::util::XmlNode::new("bldg:Building", xml_node_parts)
            .to_string(Formatting::Compact)
            .expect("should convert to string");

        assert!(xml.contains("con:occupancy"));
        assert!(xml.contains("Occupancy"));
        assert!(xml.contains("numberOfOccupants"));
        assert!(xml.contains("123"));
        assert!(xml.contains("myInterval"));
        assert!(xml.contains("myOccupantType"));
    }

    #[test]
    fn test_round_trip_abstract_construction_with_occupancy() {
        let xml_document = b"\
<bldg:Building gml:id=\"DEBY_LOD2_4959457\">
    <con:occupancy>
        <Occupancy>
            <numberOfOccupants>123</numberOfOccupants>
            <interval>myInterval</interval>
            <occupantType>myOccupantType</occupantType>
        </Occupancy>
    </con:occupancy>
</bldg:Building>";

        let spans = extract_xml_element_spans(xml_document).expect("should work");
        let abstract_construction =
            deserialize_abstract_construction(xml_document, &spans).expect("should work");

        let xml_node_parts =
            serialize_abstract_construction(&abstract_construction, Formatting::Compact)
                .expect("should serialize");
        let xml = egml::io::util::XmlNode::new("bldg:Building", xml_node_parts)
            .to_string(Formatting::Compact)
            .expect("should convert to string");

        let round_tripped_spans = extract_xml_element_spans(xml.as_bytes()).expect("should work");
        let round_tripped = deserialize_abstract_construction(xml.as_bytes(), &round_tripped_spans)
            .expect("should work");

        assert_eq!(
            abstract_construction.occupancies(),
            round_tripped.occupancies()
        );
    }
}