ifc_lite_landxml/alignment/
numeric.rs1mod geometry;
8mod render;
9mod station;
10
11use super::{LandXmlAlignment, LandXmlAlignmentSegment, LandXmlStationEquation};
12use crate::LandXmlSourceId;
13use geometry::{evaluate_segment, segment_endpoints, validate_segment};
14pub use render::LandXmlAlignmentRenderSpan;
15
16pub(super) const EPSILON: f64 = 1e-9;
17pub const MAX_INTERACTIVE_STATION_PROBES: usize = 128;
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct LandXmlNumericDiagnostic {
22 pub source_id: LandXmlSourceId,
23 pub code: &'static str,
24 pub message: String,
25}
26impl std::fmt::Display for LandXmlNumericDiagnostic {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 write!(f, "{}: {}", self.code, self.message)
29 }
30}
31impl std::error::Error for LandXmlNumericDiagnostic {}
32pub(super) type Result<T> = std::result::Result<T, LandXmlNumericDiagnostic>;
33
34#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize)]
37pub struct LandXmlStationMapping {
38 pub geometric_distance: f64,
39 pub displayed_back: f64,
40 pub displayed_ahead: f64,
41 pub is_equation_boundary: bool,
42}
43#[derive(Clone, Debug, PartialEq, serde::Serialize)]
45pub struct LandXmlAlignmentProbe {
46 pub alignment_source_id: LandXmlSourceId,
47 pub segment_source_id: LandXmlSourceId,
48 pub geometric_distance: f64,
49 pub station: LandXmlStationMapping,
50 pub northing: f64,
51 pub easting: f64,
52 pub tangent_northing: f64,
53 pub tangent_easting: f64,
54}
55
56fn validate_numeric_alignment(alignment: &LandXmlAlignment) -> Result<()> {
57 valid_alignment_length(alignment)?;
58 let end = finite_add(
59 &alignment.source_id,
60 alignment.sta_start,
61 alignment.length,
62 "alignment station range overflows",
63 )?;
64 if alignment.segments.is_empty() {
65 return Err(diagnostic(
66 &alignment.source_id,
67 "LXMLA224",
68 "alignment has no numeric geometry",
69 ));
70 }
71 let mut total: f64 = 0.0;
72 let mut previous_end = None;
73 for segment in &alignment.segments {
74 let length = validate_segment(segment)?;
75 let (start, finish) = segment_endpoints(segment)?;
76 if let Some(previous) = previous_end {
77 if planar_distance(previous, start) > geometry_tolerance(total.max(length)) {
78 return Err(diagnostic(
79 &segment.source_id,
80 "LXMLA225",
81 "ordered alignment segments are discontinuous",
82 ));
83 }
84 }
85 total = finite_add(
86 &alignment.source_id,
87 total,
88 length,
89 "alignment segment lengths overflow",
90 )?;
91 previous_end = Some(finish);
92 }
93 if (alignment.length - total).abs() > geometry_tolerance(alignment.length.max(total)) {
94 return Err(diagnostic(
95 &alignment.source_id,
96 "LXMLA226",
97 "alignment length is inconsistent with ordered segment spans",
98 ));
99 }
100 validate_station_equations(alignment, end)
101}
102fn valid_alignment_length(alignment: &LandXmlAlignment) -> Result<()> {
103 if !alignment.length.is_finite() || alignment.length <= EPSILON {
104 return Err(diagnostic(
105 &alignment.source_id,
106 "LXMLA200",
107 "alignment length must be positive and finite",
108 ));
109 }
110 if !alignment.sta_start.is_finite() {
111 return Err(diagnostic(
112 &alignment.source_id,
113 "LXMLA200",
114 "alignment start station must be finite",
115 ));
116 }
117 Ok(())
118}
119fn station_mapping(
120 source_id: &LandXmlSourceId,
121 sta_start: f64,
122 internal: f64,
123 equations: &[LandXmlStationEquation],
124) -> Result<LandXmlStationMapping> {
125 let mut previous_internal = sta_start;
126 let mut displayed = sta_start;
127 let mut direction = 1.0;
128 for (index, equation) in equations.iter().enumerate() {
129 if !equation.sta_internal.is_finite()
130 || (index == 0 && equation.sta_internal < previous_internal - EPSILON)
131 || (index > 0 && equation.sta_internal <= previous_internal + EPSILON)
132 {
133 return Err(diagnostic(
134 source_id,
135 "LXMLA207",
136 "station equations must be strictly increasing",
137 ));
138 }
139 if internal < equation.sta_internal - EPSILON {
140 let value = displayed + direction * (internal - previous_internal);
141 return Ok(LandXmlStationMapping {
142 geometric_distance: internal - sta_start,
143 displayed_back: value,
144 displayed_ahead: value,
145 is_equation_boundary: false,
146 });
147 }
148 if (internal - equation.sta_internal).abs() <= EPSILON {
149 return Ok(LandXmlStationMapping {
150 geometric_distance: internal - sta_start,
151 displayed_back: equation
152 .sta_back
153 .unwrap_or(displayed + direction * (internal - previous_internal)),
154 displayed_ahead: equation.sta_ahead,
155 is_equation_boundary: true,
156 });
157 }
158 previous_internal = equation.sta_internal;
159 displayed = equation.sta_ahead;
160 direction = equation_direction(equation);
161 }
162 let value = displayed + direction * (internal - previous_internal);
163 Ok(LandXmlStationMapping {
164 geometric_distance: internal - sta_start,
165 displayed_back: value,
166 displayed_ahead: value,
167 is_equation_boundary: false,
168 })
169}
170fn validate_station_equations(alignment: &LandXmlAlignment, end: f64) -> Result<()> {
171 let mut previous = alignment.sta_start;
172 let mut displayed = alignment.sta_start;
173 let mut direction = 1.0;
174 for (index, equation) in alignment.station_equations.iter().enumerate() {
175 if !equation.sta_internal.is_finite()
176 || !equation.sta_ahead.is_finite()
177 || equation.sta_back.is_some_and(|value| !value.is_finite())
178 || equation.sta_internal > end + EPSILON
179 || (index == 0 && equation.sta_internal < previous - EPSILON)
180 || (index > 0 && equation.sta_internal <= previous + EPSILON)
181 {
182 return Err(diagnostic(
183 &alignment.source_id,
184 "LXMLA207",
185 "station equations must lie on the alignment and be strictly ordered",
186 ));
187 }
188 let expected_back = finite_add(
189 &alignment.source_id,
190 displayed,
191 direction * (equation.sta_internal - previous),
192 "station equation derived station overflows",
193 )?;
194 if equation.sta_back.is_some_and(|back| {
195 (back - expected_back).abs() > station_tolerance(back, expected_back)
196 }) {
197 return Err(diagnostic(
198 &equation.source_id,
199 "LXMLA227",
200 "StaEquation staBack is inconsistent with the preceding station axis",
201 ));
202 }
203 previous = equation.sta_internal;
204 displayed = equation.sta_ahead;
205 direction = equation_direction(equation);
206 }
207 let _ = finite_add(
208 &alignment.source_id,
209 displayed,
210 direction * (end - previous),
211 "station equation derived station overflows",
212 )?;
213 Ok(())
214}
215fn finite_add(
216 source_id: &LandXmlSourceId,
217 left: f64,
218 right: f64,
219 message: &'static str,
220) -> Result<f64> {
221 let value = left + right;
222 if value.is_finite() {
223 Ok(value)
224 } else {
225 Err(diagnostic(source_id, "LXMLA228", message))
226 }
227}
228fn planar_distance(
229 left: crate::alignment::LandXmlPlanPoint,
230 right: crate::alignment::LandXmlPlanPoint,
231) -> f64 {
232 (right.northing - left.northing).hypot(right.easting - left.easting)
233}
234fn geometry_tolerance(length: f64) -> f64 {
235 (length * 1e-6).max(1e-6)
236}
237fn station_tolerance(left: f64, right: f64) -> f64 {
238 left.abs().max(right.abs()).mul_add(1e-12, EPSILON)
239}
240fn equation_direction(equation: &LandXmlStationEquation) -> f64 {
241 if equation.sta_increment.as_deref() == Some("decreasing") {
242 -1.0
243 } else {
244 1.0
245 }
246}
247pub(super) fn diagnostic(
248 source_id: &LandXmlSourceId,
249 code: &'static str,
250 message: impl Into<String>,
251) -> LandXmlNumericDiagnostic {
252 LandXmlNumericDiagnostic {
253 source_id: source_id.clone(),
254 code,
255 message: message.into(),
256 }
257}