ifc_lite_landxml/alignment/numeric/
station.rs1use super::{
6 diagnostic, equation_direction, evaluate_segment, station_mapping, validate_numeric_alignment,
7 validate_segment, LandXmlAlignment, LandXmlAlignmentProbe, LandXmlAlignmentSegment,
8 LandXmlStationMapping, Result, EPSILON,
9};
10
11impl LandXmlAlignment {
12 pub fn station_at_distance(&self, distance: f64) -> Result<LandXmlStationMapping> {
14 validate_numeric_alignment(self)?;
15 if !distance.is_finite() || distance < -EPSILON || distance > self.length + EPSILON {
16 return Err(diagnostic(
17 &self.source_id,
18 "LXMLA201",
19 "geometric distance is outside the alignment",
20 ));
21 }
22 station_mapping(
23 &self.source_id,
24 self.sta_start,
25 self.sta_start + distance.clamp(0.0, self.length),
26 &self.station_equations,
27 )
28 }
29
30 pub fn distances_for_station(&self, station: f64) -> Result<Vec<f64>> {
32 validate_numeric_alignment(self)?;
33 self.distances_for_station_validated(station, None)
34 }
35
36 fn distances_for_station_validated(
37 &self,
38 station: f64,
39 max_matches: Option<usize>,
40 ) -> Result<Vec<f64>> {
41 if !station.is_finite() {
42 return Err(diagnostic(
43 &self.source_id,
44 "LXMLA202",
45 "station must be finite",
46 ));
47 }
48 let mut boundaries = vec![self.sta_start];
49 boundaries.extend(
50 self.station_equations
51 .iter()
52 .map(|equation| equation.sta_internal),
53 );
54 boundaries.push(self.sta_start + self.length);
55 let mut out: Vec<f64> = Vec::new();
56 let mut direction = 1.0;
57 for (index, window) in boundaries.windows(2).enumerate() {
58 let display_start = if index == 0 {
59 self.sta_start
60 } else {
61 self.station_equations[index - 1].sta_ahead
62 };
63 let candidate = window[0] + (station - display_start) / direction;
64 if candidate >= window[0] - EPSILON && candidate <= window[1] + EPSILON {
65 let distance = (candidate - self.sta_start).clamp(0.0, self.length);
66 if out
67 .last()
68 .is_none_or(|previous| (distance - *previous).abs() > EPSILON)
69 {
70 out.push(distance);
71 if max_matches.is_some_and(|maximum| out.len() > maximum) {
72 return Err(diagnostic(
73 &self.source_id,
74 "LXMLA230",
75 "station label has too many physical matches for an interactive probe",
76 ));
77 }
78 }
79 }
80 if let Some(equation) = self.station_equations.get(index) {
81 direction = equation_direction(equation);
82 }
83 }
84 Ok(out)
85 }
86
87 pub fn probe_at_distance(
89 &self,
90 distance: f64,
91 offset_right: f64,
92 ) -> Result<LandXmlAlignmentProbe> {
93 validate_numeric_alignment(self)?;
94 self.probe_at_distance_validated(distance, offset_right)
95 }
96
97 fn probe_at_distance_validated(
98 &self,
99 distance: f64,
100 offset_right: f64,
101 ) -> Result<LandXmlAlignmentProbe> {
102 if !offset_right.is_finite() {
103 return Err(diagnostic(
104 &self.source_id,
105 "LXMLA203",
106 "offset must be finite",
107 ));
108 }
109 if !distance.is_finite() || distance < -EPSILON || distance > self.length + EPSILON {
110 return Err(diagnostic(
111 &self.source_id,
112 "LXMLA201",
113 "geometric distance is outside the alignment",
114 ));
115 }
116 let station = station_mapping(
117 &self.source_id,
118 self.sta_start,
119 self.sta_start + distance.clamp(0.0, self.length),
120 &self.station_equations,
121 )?;
122 let (segment, local) = self.segment_at_distance(distance)?;
123 let (point, tangent) = evaluate_segment(segment, local)?;
124 Ok(LandXmlAlignmentProbe {
125 alignment_source_id: self.source_id.clone(),
126 segment_source_id: segment.source_id.clone(),
127 geometric_distance: distance.clamp(0.0, self.length),
128 station,
129 northing: point.northing - tangent.1 * offset_right,
130 easting: point.easting + tangent.0 * offset_right,
131 tangent_northing: tangent.0,
132 tangent_easting: tangent.1,
133 })
134 }
135
136 pub fn probe_at_station(
138 &self,
139 station: f64,
140 offset_right: f64,
141 ) -> Result<LandXmlAlignmentProbe> {
142 validate_numeric_alignment(self)?;
143 match self
144 .distances_for_station_validated(station, None)?
145 .as_slice()
146 {
147 [distance] => self.probe_at_distance_validated(*distance, offset_right),
148 [] => Err(diagnostic(
149 &self.source_id,
150 "LXMLA204",
151 "station is in a station equation gap",
152 )),
153 _ => Err(diagnostic(
154 &self.source_id,
155 "LXMLA205",
156 "station label is duplicated by an equation",
157 )),
158 }
159 }
160
161 pub fn probes_at_station(
163 &self,
164 station: f64,
165 offset_right: f64,
166 max_matches: usize,
167 ) -> Result<Vec<LandXmlAlignmentProbe>> {
168 if max_matches == 0 {
169 return Err(diagnostic(
170 &self.source_id,
171 "LXMLA230",
172 "interactive station probe limit must be positive",
173 ));
174 }
175 validate_numeric_alignment(self)?;
176 self.distances_for_station_validated(station, Some(max_matches))?
177 .into_iter()
178 .map(|distance| self.probe_at_distance_validated(distance, offset_right))
179 .collect()
180 }
181
182 fn segment_at_distance(&self, distance: f64) -> Result<(&LandXmlAlignmentSegment, f64)> {
183 let mut start = 0.0;
184 for (index, segment) in self.segments.iter().enumerate() {
185 let length = validate_segment(segment)?;
186 let end = start + length;
187 if distance < end - EPSILON || index + 1 == self.segments.len() {
188 return Ok((segment, (distance - start).clamp(0.0, length)));
189 }
190 start = end;
191 }
192 Err(diagnostic(
193 &self.source_id,
194 "LXMLA206",
195 "no primitive covers geometric distance",
196 ))
197 }
198}