Skip to main content

document_svg/document/
gbxml.rs

1//! Bounded Green Building XML (gbXML) building/energy-model previews.
2//!
3//! gbXML transfers BIM building geometry and analysis metadata. This adapter
4//! reports the model's structural inventory without evaluating schedules,
5//! geometry, formulas, URLs or external weather/material resources.
6
7use std::path::Path;
8
9use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
10use crate::document::html::{HtmlBlock, render_blocks_to_pages};
11use crate::error::{Error, Result};
12use crate::geospatial::xml_tree::{XmlElement, XmlLimits, parse_xml_tree};
13use crate::table::{TableAlign, TableData};
14
15const MAX_GBXML_BYTES: u64 = 128 * 1024 * 1024;
16const MAX_GBXML_EVENTS: usize = 1_000_000;
17const MAX_GBXML_NODES: usize = 500_000;
18const MAX_GBXML_DEPTH: usize = 128;
19const MAX_GBXML_TEXT_BYTES: usize = 32 * 1024 * 1024;
20const MAX_GBXML_ROWS: usize = 200_000;
21const MAX_GBXML_DISPLAY_BYTES: usize = 512;
22
23#[derive(Default)]
24struct Summary {
25    version: String,
26    campuses: usize,
27    buildings: usize,
28    spaces: usize,
29    surfaces: usize,
30    openings: usize,
31    zones: usize,
32    constructions: usize,
33    materials: usize,
34    schedules: usize,
35    systems: usize,
36    locations: usize,
37    shell_geometries: usize,
38    occupants: usize,
39    rows: Vec<Vec<String>>,
40}
41
42struct GbxmlPageSink<'a> {
43    inner: &'a mut dyn PageConsumer,
44    warnings: &'a [String],
45}
46
47impl PageConsumer for GbxmlPageSink<'_> {
48    fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
49        page.source_format = "gbxml".into();
50        if page.title.is_empty() {
51            page.title = "gbXML building model".into();
52        }
53        page.description =
54            "gbXML BIM and building-energy structure is rendered as bounded inert metadata; geometry, schedules and external resources are not evaluated".into();
55        for warning in self.warnings {
56            page.warn(warning.clone());
57        }
58        self.inner.consume(page)
59    }
60}
61
62pub(crate) fn looks_like_prefix(prefix: &[u8]) -> bool {
63    crate::geospatial::xml_tree::looks_like_root(prefix, b"gbXML", None)
64        && String::from_utf8_lossy(prefix)
65            .to_ascii_lowercase()
66            .contains("gbxml.org/schema")
67}
68
69pub(crate) fn convert(
70    path: &Path,
71    options: &ConvertOptions,
72    sink: &mut dyn PageConsumer,
73) -> Result<Vec<String>> {
74    let bytes = read_limited_file(
75        path,
76        options.max_input_bytes.min(MAX_GBXML_BYTES),
77        "gbXML input",
78    )?;
79    let root = parse_xml_tree(
80        &bytes,
81        &XmlLimits {
82            max_events: options.max_xml_events.min(MAX_GBXML_EVENTS),
83            max_nodes: MAX_GBXML_NODES,
84            max_depth: MAX_GBXML_DEPTH,
85            max_text_bytes: MAX_GBXML_TEXT_BYTES,
86        },
87        "gbXML",
88    )?;
89    if !root.name.eq_ignore_ascii_case("gbxml") {
90        return Err(Error::InvalidInput("gbXML root must be <gbXML>".into()));
91    }
92    if root
93        .namespace
94        .as_deref()
95        .is_none_or(|namespace| !namespace.to_ascii_lowercase().contains("gbxml.org/schema"))
96    {
97        return Err(Error::InvalidInput(
98            "gbXML root uses an unsupported namespace".into(),
99        ));
100    }
101    let mut summary = Summary {
102        version: attr_local(&root, "version")
103            .map(truncate)
104            .unwrap_or_default(),
105        campuses: count_named(&root, "Campus"),
106        buildings: count_named(&root, "Building"),
107        spaces: count_named(&root, "Space"),
108        surfaces: count_named(&root, "Surface"),
109        openings: count_named(&root, "Opening"),
110        zones: count_named(&root, "Zone"),
111        constructions: count_named(&root, "Construction"),
112        materials: count_named(&root, "Material"),
113        schedules: count_named(&root, "Schedule") + count_named(&root, "DaySchedule"),
114        systems: count_named(&root, "AirLoop")
115            + count_named(&root, "ZoneEquipments")
116            + count_named(&root, "BuildingStorey")
117            + count_named(&root, "System"),
118        locations: count_named(&root, "Location") + count_named(&root, "WeatherStation"),
119        shell_geometries: count_named(&root, "ShellGeometry")
120            + count_named(&root, "RectangularGeometry"),
121        occupants: count_named(&root, "People")
122            + count_named(&root, "Occupant")
123            + count_named(&root, "Occupants"),
124        ..Summary::default()
125    };
126    push_row(
127        &mut summary.rows,
128        "Model",
129        "gbXML",
130        &format!("version={}", display_or_dash(&summary.version)),
131    )?;
132    push_row(
133        &mut summary.rows,
134        "Building",
135        &summary.buildings.to_string(),
136        &format!(
137            "campuses={} spaces={} zones={}",
138            summary.campuses, summary.spaces, summary.zones
139        ),
140    )?;
141    push_row(
142        &mut summary.rows,
143        "Envelope",
144        &summary.surfaces.to_string(),
145        &format!(
146            "openings={} shellGeometry={}",
147            summary.openings, summary.shell_geometries
148        ),
149    )?;
150    push_row(
151        &mut summary.rows,
152        "Materials",
153        &summary.materials.to_string(),
154        &format!(
155            "constructions={} schedules={}",
156            summary.constructions, summary.schedules
157        ),
158    )?;
159    push_row(
160        &mut summary.rows,
161        "Systems",
162        &summary.systems.to_string(),
163        &format!(
164            "locations={} occupants={}",
165            summary.locations, summary.occupants
166        ),
167    )?;
168    let blocks = vec![
169        HtmlBlock::Heading {
170            level: 1,
171            text: "gbXML building model".into(),
172        },
173        HtmlBlock::Paragraph {
174            text: "Green Building XML structure is summarized for BIM and energy-analysis review; geometry, schedules, formulas and external resources remain inert.".into(),
175        },
176        HtmlBlock::Table(TableData {
177            headers: vec!["Kind".into(), "Value".into(), "Detail".into()],
178            rows: summary.rows,
179            alignments: vec![TableAlign::Left; 3],
180            raw_source: String::new(),
181        }),
182    ];
183    let warnings = vec![
184        "gbXML IDs, coordinates, material properties, schedules, formulas, URLs and weather/resource payloads are omitted or redacted; only bounded structural counts are shown".into(),
185        "gbXML schema locations, external references, geometry calculations, energy simulation and CRS/unit conversion never run".into(),
186    ];
187    let mut page_sink = GbxmlPageSink {
188        inner: sink,
189        warnings: &warnings,
190    };
191    render_blocks_to_pages(&blocks, &mut page_sink, options)?;
192    Ok(warnings)
193}
194
195fn count_named(element: &XmlElement, name: &str) -> usize {
196    element
197        .children
198        .iter()
199        .map(|child| usize::from(child.name.eq_ignore_ascii_case(name)) + count_named(child, name))
200        .sum()
201}
202
203fn attr_local<'a>(element: &'a XmlElement, name: &str) -> Option<&'a str> {
204    element.attributes.iter().find_map(|(key, value)| {
205        key.rsplit(':')
206            .next()
207            .filter(|local| local.eq_ignore_ascii_case(name))
208            .map(|_| value.as_str())
209    })
210}
211
212fn display_or_dash(value: &str) -> &str {
213    if value.is_empty() { "—" } else { value }
214}
215
216fn push_row(rows: &mut Vec<Vec<String>>, kind: &str, value: &str, detail: &str) -> Result<()> {
217    if rows.len() >= MAX_GBXML_ROWS {
218        return Err(Error::LimitExceeded(format!(
219            "gbXML rows exceed {MAX_GBXML_ROWS}"
220        )));
221    }
222    rows.push(vec![truncate(kind), truncate(value), truncate(detail)]);
223    Ok(())
224}
225
226fn truncate(value: &str) -> String {
227    if value.len() <= MAX_GBXML_DISPLAY_BYTES {
228        value.to_owned()
229    } else {
230        let mut end = MAX_GBXML_DISPLAY_BYTES;
231        while end > 0 && !value.is_char_boundary(end) {
232            end -= 1;
233        }
234        format!("{}…", &value[..end])
235    }
236}