ifc_lite_landxml/
pipes.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Durable LandXML pipe-network source records.
6//!
7//! These are source semantics only.  They deliberately do not imply an IFC
8//! mapping, hydraulic analysis, or a renderer representation.
9
10use std::collections::BTreeMap;
11
12use serde::{Deserialize, Serialize};
13
14use crate::{LandXmlDiagnosticCode, LandXmlSourceId};
15
16pub type LandXmlPipeProperties = BTreeMap<String, String>;
17
18/// A finite authored measurement and the source unit used to make it useful.
19#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
20pub struct LandXmlPipeMeasure {
21    pub value: f64,
22    pub unit: String,
23    pub meters: f64,
24}
25
26/// The length units effective for one pipe-network collection.
27#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
28pub struct LandXmlPipeUnits {
29    pub linear_unit: String,
30    pub elevation_unit: String,
31    pub diameter_unit: String,
32    pub width_unit: String,
33    pub height_unit: String,
34    /// Optional authored unit for `PipeFlow` and `StructFlow` quantities.
35    pub flow_unit: Option<String>,
36    pub linear_scale_to_meters: f64,
37    pub elevation_scale_to_meters: f64,
38    pub diameter_scale_to_meters: f64,
39    pub width_scale_to_meters: f64,
40    pub height_scale_to_meters: f64,
41}
42
43/// A coordinate authored directly in a pipe-network `Center` element.
44#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
45pub struct LandXmlPipePosition {
46    pub northing: f64,
47    pub easting: f64,
48    pub northing_meters: f64,
49    pub easting_meters: f64,
50    pub elevation: Option<LandXmlPipeMeasure>,
51}
52
53/// The declared cross-section part of a pipe.
54#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
55#[serde(tag = "kind", rename_all = "snake_case")]
56pub enum LandXmlPipePart {
57    Circular {
58        properties: LandXmlPipeProperties,
59        diameter: LandXmlPipeMeasure,
60        thickness: Option<LandXmlPipeMeasure>,
61        material: Option<String>,
62    },
63    Elliptical {
64        properties: LandXmlPipeProperties,
65        span: LandXmlPipeMeasure,
66        height: LandXmlPipeMeasure,
67        thickness: Option<LandXmlPipeMeasure>,
68        material: Option<String>,
69    },
70    Egg {
71        properties: LandXmlPipeProperties,
72        span: LandXmlPipeMeasure,
73        height: LandXmlPipeMeasure,
74        thickness: Option<LandXmlPipeMeasure>,
75        material: Option<String>,
76    },
77    Rectangular {
78        properties: LandXmlPipeProperties,
79        width: LandXmlPipeMeasure,
80        height: LandXmlPipeMeasure,
81        thickness: Option<LandXmlPipeMeasure>,
82        material: Option<String>,
83    },
84}
85
86/// The declared part of a structure.
87#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
88#[serde(tag = "kind", rename_all = "snake_case")]
89pub enum LandXmlStructurePart {
90    Circular {
91        properties: LandXmlPipeProperties,
92        diameter: LandXmlPipeMeasure,
93        thickness: Option<LandXmlPipeMeasure>,
94        material: Option<String>,
95    },
96    Rectangular {
97        properties: LandXmlPipeProperties,
98        length: LandXmlPipeMeasure,
99        width: LandXmlPipeMeasure,
100        thickness: Option<LandXmlPipeMeasure>,
101        material: Option<String>,
102    },
103    Inlet {
104        properties: LandXmlPipeProperties,
105    },
106    Outlet {
107        properties: LandXmlPipeProperties,
108    },
109    Connection {
110        properties: LandXmlPipeProperties,
111    },
112}
113
114/// The direction-specific invert belonging to a structure and pipe.
115#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
116pub struct LandXmlPipeInvert {
117    pub source_id: LandXmlSourceId,
118    pub source_path: String,
119    pub pipe_source_id: LandXmlSourceId,
120    pub flow_direction: String,
121    pub elevation: LandXmlPipeMeasure,
122    pub properties: LandXmlPipeProperties,
123}
124
125/// Preserved flow attributes; the parser does not derive hydraulic results.
126#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
127pub struct LandXmlPipeFlow {
128    pub source_id: LandXmlSourceId,
129    pub source_path: String,
130    pub unit: Option<String>,
131    pub flow_in: Option<f64>,
132    pub loss_in: Option<f64>,
133    pub loss_out: Option<f64>,
134    pub properties: LandXmlPipeProperties,
135}
136
137/// One structure in source order, with source-to-pipe invert provenance.
138#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
139pub struct LandXmlPipeStructure {
140    pub source_id: LandXmlSourceId,
141    pub source_path: String,
142    pub name: String,
143    pub properties: LandXmlPipeProperties,
144    /// Units effective at this record's exact `Structs` collection position.
145    pub units: LandXmlPipeUnits,
146    pub center: LandXmlPipePosition,
147    pub part: LandXmlStructurePart,
148    pub rim_elevation: Option<LandXmlPipeMeasure>,
149    pub sump_elevation: Option<LandXmlPipeMeasure>,
150    pub inverts: Vec<LandXmlPipeInvert>,
151    pub flow: Option<LandXmlPipeFlow>,
152}
153
154/// A pipe's graph edge after `refStart` and `refEnd` resolve to source nodes.
155#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
156pub struct LandXmlPipeConnectivity {
157    pub start_structure_source_id: LandXmlSourceId,
158    pub end_structure_source_id: LandXmlSourceId,
159}
160
161/// A direct point along the declared pipe route.
162///
163/// LandXML's `Pipe/Center` is a route pass-through point. It is not evidence
164/// of a circular arc, so this source model does not invent a radius or curve.
165#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
166#[serde(tag = "kind", rename_all = "snake_case")]
167pub enum LandXmlPipeGeometry {
168    Straight,
169    PassThrough { point: LandXmlPipePosition },
170}
171
172/// One validated graph edge in a pipe network.
173#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
174pub struct LandXmlPipe {
175    pub source_id: LandXmlSourceId,
176    pub source_path: String,
177    pub name: String,
178    pub properties: LandXmlPipeProperties,
179    /// Units effective at this record's exact `Pipes` collection position.
180    pub units: LandXmlPipeUnits,
181    pub connectivity: LandXmlPipeConnectivity,
182    pub part: LandXmlPipePart,
183    pub geometry: LandXmlPipeGeometry,
184    pub length: Option<LandXmlPipeMeasure>,
185    pub flow: Option<LandXmlPipeFlow>,
186}
187
188/// A semantic refusal for a single source element, not a fabricated fallback.
189#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
190pub struct LandXmlPipeRefusal {
191    pub source_id: LandXmlSourceId,
192    pub source_path: String,
193    pub code: LandXmlDiagnosticCode,
194    pub message: String,
195}
196
197/// A schema-valid `Feature` child retained without assigning IFC meaning.
198#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
199pub struct LandXmlPipeFeature {
200    pub source_id: LandXmlSourceId,
201    pub source_path: String,
202    pub owner_source_id: LandXmlSourceId,
203    pub properties: LandXmlPipeProperties,
204}
205
206/// One exact `PipeNetworks` collection and its authored metadata.
207#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
208pub struct LandXmlPipeNetworkCollection {
209    pub source_id: LandXmlSourceId,
210    pub source_path: String,
211    pub properties: LandXmlPipeProperties,
212}
213
214/// One exact LandXML `PipeNetwork` source hierarchy.
215#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
216pub struct LandXmlPipeNetwork {
217    pub source_id: LandXmlSourceId,
218    pub source_path: String,
219    pub name: String,
220    pub pipe_network_type: String,
221    pub properties: LandXmlPipeProperties,
222    pub features: Vec<LandXmlPipeFeature>,
223    pub structure_units: Option<LandXmlPipeUnits>,
224    pub pipe_units: Option<LandXmlPipeUnits>,
225    pub structures: Vec<LandXmlPipeStructure>,
226    pub pipes: Vec<LandXmlPipe>,
227}
228
229/// Pipe-network semantic result. It is intentionally independent from TIN.
230#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
231pub struct LandXmlPipeNetworkDocument {
232    /// Namespace-selected grammar used to parse this source, independent from
233    /// the producer's declared version string.
234    pub schema: String,
235    pub version: String,
236    /// Compatibility observations retained for direct pipe-parser consumers.
237    pub capability_diagnostics: Vec<crate::LandXmlCapabilityDiagnostic>,
238    pub root_units: Option<LandXmlPipeUnits>,
239    pub collections: Vec<LandXmlPipeNetworkCollection>,
240    /// Features authored at any schema-valid pipe-network owner.
241    pub features: Vec<LandXmlPipeFeature>,
242    pub networks: Vec<LandXmlPipeNetwork>,
243    pub refusals: Vec<LandXmlPipeRefusal>,
244}
245
246impl LandXmlPipeNetworkDocument {
247    /// Split finalized pipe metadata into move-owned stream records.
248    pub(crate) fn into_stream_parts(
249        self,
250        preflight_refusal_batches: Vec<Vec<usize>>,
251    ) -> crate::stream::metadata::PipeStreamParts {
252        crate::stream::metadata::PipeStreamParts::new(self, preflight_refusal_batches)
253    }
254}
255
256/// Stable source identities prepared for a future canonical LandXML adapter.
257///
258/// This deliberately batches semantics only: it creates neither renderer
259/// geometry nor a parallel model-loading path while the #5084 adapter remains
260/// the integration owner.
261#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
262pub struct LandXmlPipeSourceBatch {
263    pub source_ids: Vec<LandXmlSourceId>,
264}
265
266impl LandXmlPipeNetworkDocument {
267    /// Partition source identities without binding them to a renderer, WASM
268    /// surface, or invented IFC representation.
269    pub fn source_batches(&self, max_records: usize) -> Vec<LandXmlPipeSourceBatch> {
270        if max_records == 0 {
271            return Vec::new();
272        }
273        let source_ids = self
274            .collections
275            .iter()
276            .map(|collection| collection.source_id.clone())
277            .chain(
278                self.features
279                    .iter()
280                    .map(|feature| feature.source_id.clone()),
281            )
282            .chain(self.networks.iter().flat_map(|network| {
283                std::iter::once(network.source_id.clone())
284                    .chain(network.structures.iter().flat_map(|structure| {
285                        std::iter::once(structure.source_id.clone())
286                            .chain(
287                                structure
288                                    .inverts
289                                    .iter()
290                                    .map(|invert| invert.source_id.clone()),
291                            )
292                            .chain(structure.flow.iter().map(|flow| flow.source_id.clone()))
293                    }))
294                    .chain(network.pipes.iter().flat_map(|pipe| {
295                        std::iter::once(pipe.source_id.clone())
296                            .chain(pipe.flow.iter().map(|flow| flow.source_id.clone()))
297                    }))
298            }));
299        let mut batches = Vec::new();
300        let mut current = Vec::new();
301        for source_id in source_ids {
302            current.push(source_id);
303            if current.len() == max_records {
304                batches.push(LandXmlPipeSourceBatch {
305                    source_ids: current,
306                });
307                current = Vec::new();
308            }
309        }
310        if !current.is_empty() {
311            batches.push(LandXmlPipeSourceBatch {
312                source_ids: current,
313            });
314        }
315        batches
316    }
317}