ifc_lite_landxml/alignment/numeric/
station.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
5use 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    /// Maps physical distance to displayed station without conflating the two.
13    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    /// Returns every physical distance matching a displayed station. Empty is a station gap.
31    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    /// Evaluates an authored primitive at a physical distance, never a mesh request.
88    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    /// Resolves one displayed label, refusing duplicate labels rather than guessing.
137    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    /// Evaluates a bounded number of displayed-station matches after one validation pass.
162    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}