Skip to main content

c_its_parser/
geo_utils.rs

1//! Conversions from ETSI to geo-types
2//!
3//! - Positions are converted to [`geo_types::Point`]
4//! - Paths ([`PathHistory`](`crate::standards::cdd_1_3_1_1::its_container::PathHistory`)/ [`Path`](`crate::standards::cdd_2_2_1::etsi_its_cdd::Path`))
5//!   and MAPEM lanes [`NodeSetXY`](`crate::standards::dsrc_2_2_1::etsi_its_dsrc::NodeSetXY`) are converted to [`geo_types::LineString`]
6//!
7//! Take a look at the individual data types in [`crate::standards`] to discover available conversion methods and initialization functions.
8//!
9//! Note: These conversions are only available with the optional `geo` feature flag.
10//! And when used in a `no_std` environment, the `libm` flags is required for conversions from X/Y delta positions.
11
12#[cfg(all(feature = "libm", not(feature = "std")))]
13use num_traits::float::Float;
14
15const EARTH_CIRCUMFERENCE: f32 = 39_940_653.; // Earth's circumference in meters
16
17// generic conversion helpers
18
19/// Convert ETSI XY deltas to absolute lon/lat position (in degrees)
20///
21/// X points east (longitude), Y points north (latitude)!
22#[must_use]
23pub fn point_from_dxy(dx: f32, dy: f32, ref_pos: &geo_types::Point) -> geo_types::Point {
24    dxy_to_geo(dx, dy, ref_pos).into()
25}
26
27// used by DSRC 2.2.1
28#[cfg(feature = "_dsrc_2_2_1")]
29impl From<crate::standards::dsrc_2_2_1::etsi_its_dsrc::Position3D> for geo_types::Point {
30    fn from(other: crate::standards::dsrc_2_2_1::etsi_its_dsrc::Position3D) -> Self {
31        geo_types::Point::new(other.long.as_deg(), other.lat.as_deg())
32    }
33}
34#[cfg(feature = "_dsrc_2_2_1")]
35impl From<geo_types::Point> for crate::standards::dsrc_2_2_1::etsi_its_dsrc::Position3D {
36    fn from(other: geo_types::Point) -> Self {
37        use crate::standards::cdd_2_2_1::etsi_its_cdd::{Latitude, Longitude};
38
39        Self {
40            lat: Latitude::from_deg(other.y()),
41            long: Longitude::from_deg(other.x()),
42            elevation: None,
43            regional: None,
44        }
45    }
46}
47#[cfg(feature = "_dsrc_2_2_1")]
48impl From<crate::standards::dsrc_2_2_1::etsi_its_dsrc::NodeLLmD64b> for geo_types::Point {
49    fn from(other: crate::standards::dsrc_2_2_1::etsi_its_dsrc::NodeLLmD64b) -> Self {
50        geo_types::Point::new(other.lon.as_deg(), other.lat.as_deg())
51    }
52}
53#[cfg(feature = "_dsrc_2_2_1")]
54impl From<&crate::standards::dsrc_2_2_1::etsi_its_dsrc::NodeLLmD64b> for geo_types::Point {
55    fn from(other: &crate::standards::dsrc_2_2_1::etsi_its_dsrc::NodeLLmD64b) -> Self {
56        geo_types::Point::new(other.lon.as_deg(), other.lat.as_deg())
57    }
58}
59
60/// convert ETSI ReferencePosition to [`geo_types::Point`]
61#[cfg(any(feature = "_cdd_1_3_1_1", feature = "_cdd_2_2_1"))]
62macro_rules! refpos_to_point {
63    ($t:ty) => {
64        impl From<$t> for geo_types::Point {
65            fn from(other: $t) -> Self {
66                geo_types::Point::new(other.longitude.as_deg(), other.latitude.as_deg())
67            }
68        }
69    };
70}
71
72// used by CDD 1.3.1 (DENM 1.3.1, CAM 1.4.1, CPM 1)
73#[cfg(feature = "_cdd_1_3_1_1")]
74refpos_to_point!(crate::standards::cdd_1_3_1_1::its_container::ReferencePosition);
75
76// used by CDD 2.2.1 (DENM 2.2.1, CPM 2.1.1)
77#[cfg(feature = "_cdd_2_2_1")]
78refpos_to_point!(crate::standards::cdd_2_2_1::etsi_its_cdd::ReferencePosition);
79
80// ETSI PathHistory
81
82#[cfg(any(feature = "_cdd_1_3_1_1", feature = "denm_2_2_1"))]
83macro_rules! ph_to_line_string {
84    ($t:ty) => {
85        impl $t {
86            /// Resolve delta positions to absolute geo positions
87            ///
88            /// Output geo positions are in degrees lon/lat starting with the reference position.
89            ///
90            /// Input data is assumed to be in ETSI format. This means that the first point is
91            /// relative to the `ref_pos` and subsequent points are relative to the point before.
92            #[allow(
93                clippy::missing_panics_doc,
94                reason = "panic on last().unwrap() is impossible here"
95            )]
96            #[must_use]
97            pub fn to_line_string(&self, ref_pos: geo_types::Point) -> geo_types::LineString {
98                self.0
99                    .iter()
100                    .fold(alloc::vec![ref_pos], |mut acc, pt| {
101                        // vector is primed with one element, so should never return None
102                        let origin = acc.last().unwrap();
103                        let delta = geo_types::Point::new(
104                            pt.path_position.delta_longitude.as_deg(),
105                            pt.path_position.delta_latitude.as_deg(),
106                        );
107
108                        acc.push(*origin + delta);
109                        acc
110                    })
111                    .into()
112            }
113        }
114    };
115}
116
117// used by CDD 1.3.1 (CAM 1.4.1 and DENM 1.3.1)
118#[cfg(feature = "_cdd_1_3_1_1")]
119ph_to_line_string!(crate::standards::cdd_1_3_1_1::its_container::PathHistory);
120
121// used by CDD 2.2.1 (DENM 2.1.1)
122#[cfg(feature = "denm_2_2_1")]
123ph_to_line_string!(crate::standards::cdd_2_2_1::etsi_its_cdd::Path);
124
125// Used by DSRC 2.2.1 (MAPEM lanes)
126#[cfg(feature = "mapem_2_2_1")]
127impl crate::standards::dsrc_2_2_1::etsi_its_dsrc::NodeSetXY {
128    /// Resolve delta positions to absolute geo positions
129    ///
130    /// Output geo positions are in degrees lon/lat excluding the reference position.
131    ///
132    /// Input data is assumed to be in ETSI format. This means that the first point is
133    /// relative to the `ref_pos` and subsequent points are relative to the point before.
134    #[allow(
135        clippy::missing_panics_doc,
136        reason = "panic on last().unwrap() is impossible here"
137    )]
138    #[must_use]
139    pub fn to_line_string(&self, ref_pos: geo_types::Point) -> geo_types::LineString {
140        let mut path = self.0.iter().fold(alloc::vec![ref_pos], |mut acc, pt| {
141            // vector is primed with one element, so should never return None
142            let origin = acc.last().unwrap();
143            let pos = pt.delta.to_geo(origin);
144
145            acc.push(pos);
146            acc
147        });
148        path.remove(0); // remove ref_pos again
149
150        path.into()
151    }
152
153    /// Converts all nodes to X/Y deltas relative to the reference point
154    ///
155    /// Input data is assumed to be in ETSI format. This means that the first point is
156    /// relative to the `ref_pos` and subsequent points are relative to the point before.
157    ///
158    /// Output is in meters in the ETSI coordinate system (X pointing east, Y pointing north).
159    #[must_use]
160    pub fn to_line_string_xy(&self, ref_pos: geo_types::Point) -> geo_types::LineString {
161        let mut prev_delta = geo_types::Point::default();
162        let mut prev_ref_pos = ref_pos;
163        self.0
164            .iter()
165            .map(|pt| {
166                let node = match &pt.delta {
167                    crate::standards::dsrc_2_2_1::etsi_its_dsrc::NodeOffsetPointXY::node_LatLon(
168                        node_llm_d64b,
169                    ) => {
170                        let result = pt.delta.to_ddist(&prev_ref_pos);
171
172                        // reference position needs to be the last `node_LatLon`
173                        prev_ref_pos = node_llm_d64b.into();
174                        result
175                    }
176                    _ => prev_delta + pt.delta.to_ddist(&ref_pos),
177                };
178                prev_delta = node;
179
180                node
181            })
182            .collect()
183    }
184}
185
186#[cfg(feature = "mapem_2_2_1")]
187impl crate::standards::dsrc_2_2_1::etsi_its_dsrc::NodeOffsetPointXY {
188    /// Convert to absolute lon/lat (in degrees)
189    ///
190    /// [`NodeOffsetPointXY`](`Self`) may contain XY delta positions or absolute lat/lon positions.
191    /// XY positions will be converted to delta geo-coordinates near the `ref_pos`.
192    #[must_use]
193    pub fn to_geo(&self, ref_pos: &geo_types::Point) -> geo_types::Point {
194        use crate::standards::dsrc_2_2_1::etsi_its_dsrc::NodeOffsetPointXY;
195
196        let (lon, lat) = match self {
197            NodeOffsetPointXY::node_XY1(etsi) => {
198                dxy_to_geo((&etsi.x).into(), (&etsi.y).into(), ref_pos)
199            }
200            NodeOffsetPointXY::node_XY2(etsi) => {
201                dxy_to_geo((&etsi.x).into(), (&etsi.y).into(), ref_pos)
202            }
203            NodeOffsetPointXY::node_XY3(etsi) => {
204                dxy_to_geo((&etsi.x).into(), (&etsi.y).into(), ref_pos)
205            }
206            NodeOffsetPointXY::node_XY4(etsi) => {
207                dxy_to_geo((&etsi.x).into(), (&etsi.y).into(), ref_pos)
208            }
209            NodeOffsetPointXY::node_XY5(etsi) => {
210                dxy_to_geo((&etsi.x).into(), (&etsi.y).into(), ref_pos)
211            }
212            NodeOffsetPointXY::node_XY6(etsi) => {
213                dxy_to_geo((&etsi.x).into(), (&etsi.y).into(), ref_pos)
214            }
215            NodeOffsetPointXY::node_LatLon(etsi) => (etsi.lon.as_deg(), etsi.lat.as_deg()),
216            NodeOffsetPointXY::regional(_) => (0., 0.),
217        };
218
219        geo_types::Point::new(lon, lat)
220    }
221
222    /// Convert to delta distance (in meters)
223    ///
224    /// [`NodeOffsetPointXY`](`Self`) may contain XY delta positions or absolute lat/lon positions.
225    /// lat/lon delta coordinates will be converted to XY offsets near the `ref_pos`.
226    ///
227    /// Output will be according to ETSI coordinate system (X pointing east, X pointing north)!
228    #[must_use]
229    pub fn to_ddist(&self, ref_pos: &geo_types::Point) -> geo_types::Point {
230        use crate::standards::dsrc_2_2_1::etsi_its_dsrc::NodeOffsetPointXY;
231
232        let (x, y) = match self {
233            NodeOffsetPointXY::node_XY1(etsi) => ((&etsi.x).into(), (&etsi.y).into()),
234            NodeOffsetPointXY::node_XY2(etsi) => ((&etsi.x).into(), (&etsi.y).into()),
235            NodeOffsetPointXY::node_XY3(etsi) => ((&etsi.x).into(), (&etsi.y).into()),
236            NodeOffsetPointXY::node_XY4(etsi) => ((&etsi.x).into(), (&etsi.y).into()),
237            NodeOffsetPointXY::node_XY5(etsi) => ((&etsi.x).into(), (&etsi.y).into()),
238            NodeOffsetPointXY::node_XY6(etsi) => ((&etsi.x).into(), (&etsi.y).into()),
239            NodeOffsetPointXY::node_LatLon(etsi) => latlon_to_dcart(etsi, ref_pos),
240            NodeOffsetPointXY::regional(_) => (0., 0.),
241        };
242
243        geo_types::Point::new(x.into(), y.into())
244    }
245}
246
247#[cfg(any(feature = "std", feature = "libm"))]
248/// Convert ETSI XY deltas to absolute lon/lat position (in degrees)
249///
250/// X points east (longitude), Y points north (latitude)!
251fn dxy_to_geo(dx: f32, dy: f32, ref_pos: &geo_types::Point) -> (f64, f64) {
252    // latitude degree per meter is independent from longitude
253    let dlat = f64::from(dy) / f64::from(EARTH_CIRCUMFERENCE) * 360.;
254
255    // longitude degree per meter has a different value depending on the latitude
256    let ref_lat_rad = ref_pos.to_radians().y();
257    let dlon = f64::from(dx) / (f64::from(EARTH_CIRCUMFERENCE) * ref_lat_rad.cos()) * 360.;
258
259    (ref_pos.x() + dlon, ref_pos.y() + dlat)
260}
261
262/// Convert absolute lon/lat position to ETSI XY position (in meters)
263///
264/// X points east (longitude), Y points north (latitude)!
265#[cfg(all(feature = "mapem_2_2_1", any(feature = "std", feature = "libm")))]
266fn latlon_to_dcart(
267    dpos: &crate::standards::dsrc_2_2_1::etsi_its_dsrc::NodeLLmD64b,
268    ref_pos: &geo_types::Point,
269) -> (f32, f32) {
270    // latitude degree per meter is independent from longitude
271    #[allow(clippy::cast_possible_truncation)]
272    let dlat_deg = dpos.lat.as_deg() as f32 - ref_pos.y() as f32;
273    let dy = dlat_deg / 360. * EARTH_CIRCUMFERENCE;
274
275    // longitude degree per meter has a different value depending on the latitude
276    #[allow(clippy::cast_possible_truncation)]
277    let ref_lat_rad = ref_pos.to_radians().y() as f32;
278    #[allow(clippy::cast_possible_truncation)]
279    let dlon_deg = dpos.lon.as_deg() as f32 - ref_pos.x() as f32;
280    let dx = dlon_deg / 360. * (EARTH_CIRCUMFERENCE * ref_lat_rad.cos());
281
282    (dx, dy)
283}
284
285#[cfg(feature = "cpm_1")]
286impl crate::standards::cpm_1::cpm_pdu_descriptions::NodeOffsetPointZ {
287    #[must_use]
288    pub fn to_meters(&self) -> f32 {
289        use crate::standards::cpm_1::cpm_pdu_descriptions::NodeOffsetPointZ;
290
291        match self {
292            NodeOffsetPointZ::node_Z1(etsi) => etsi.into(),
293            NodeOffsetPointZ::node_Z2(etsi) => etsi.into(),
294            NodeOffsetPointZ::node_Z3(etsi) => etsi.into(),
295            NodeOffsetPointZ::node_Z4(etsi) => etsi.into(),
296            NodeOffsetPointZ::node_Z5(etsi) => etsi.into(),
297            NodeOffsetPointZ::node_Z6(etsi) => etsi.into(),
298        }
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    #[test]
305    fn dxy_to_geo_test() {
306        use crate::geo_utils::dxy_to_geo;
307
308        let ref_pos = geo_types::point! {x: 9.936_521, y: 53.550_728};
309
310        // trivial test
311        assert_eq!((ref_pos.x(), ref_pos.y()), dxy_to_geo(0., 0., &ref_pos));
312
313        // 1/10th nautical mile north/ south
314        let (dlon, dlat) = dxy_to_geo(0., 185., &ref_pos);
315        assert_float_eq::assert_float_absolute_eq!(ref_pos.x() + 0., dlon);
316        assert_float_eq::assert_float_absolute_eq!(ref_pos.y() + 0.001_667, dlat);
317
318        let (dlon, dlat) = dxy_to_geo(0., -185., &ref_pos);
319        assert_float_eq::assert_float_absolute_eq!(ref_pos.x() + 0., dlon);
320        assert_float_eq::assert_float_absolute_eq!(ref_pos.y() + -0.001_667, dlat);
321
322        // east/west from online calculation tool
323        let (dlon, dlat) = dxy_to_geo(66., 0., &ref_pos);
324        assert_float_eq::assert_float_absolute_eq!(ref_pos.x() + 0.001_001, dlon);
325        assert_float_eq::assert_float_absolute_eq!(ref_pos.y() + 0., dlat);
326
327        // combine both
328        let (dlon, dlat) = dxy_to_geo(66., 185., &ref_pos);
329        assert_float_eq::assert_float_absolute_eq!(ref_pos.x() + 0.001_001, dlon);
330        assert_float_eq::assert_float_absolute_eq!(ref_pos.y() + 0.001_667, dlat);
331    }
332
333    #[test]
334    #[cfg(feature = "mapem_2_2_1")]
335    fn nodeset_to_line_string() {
336        use crate::standards::cdd_2_2_1::etsi_its_cdd::{Latitude, Longitude};
337        use crate::standards::dsrc_2_2_1::etsi_its_dsrc::{
338            NodeLLmD64b,
339            NodeOffsetPointXY,
340            NodeSetXY,
341            NodeXY,
342            NodeXY32b,
343            OffsetB16,
344        };
345
346        let ref_pos = geo_types::point! {x: 9.936_521, y: 53.550_728};
347
348        // latlon test
349        {
350            let point1 = NodeLLmD64b::new(
351                Longitude::from_deg(ref_pos.x() + 0.001),
352                Latitude::from_deg(ref_pos.y() + 0.005),
353            );
354            let point2 = NodeLLmD64b::new(
355                Longitude::from_deg(ref_pos.x() + -0.042),
356                Latitude::from_deg(ref_pos.y() + 0.),
357            );
358            let nodes = alloc::vec![
359                NodeXY::new(NodeOffsetPointXY::node_LatLon(point1), None),
360                NodeXY::new(NodeOffsetPointXY::node_LatLon(point2), None),
361            ];
362
363            let geo_nodes = NodeSetXY(nodes).to_line_string(ref_pos).into_points();
364            let geo_point1 = geo_types::Point::new(9.936_521 + 0.001, 53.550_728 + 0.005);
365            assert_float_eq::assert_float_absolute_eq!(geo_point1.x(), geo_nodes[0].x());
366            assert_float_eq::assert_float_absolute_eq!(geo_point1.y(), geo_nodes[0].y());
367            let geo_point2 = geo_types::Point::new(9.936_521 - 0.042, 53.550_728 + 0.);
368            assert_float_eq::assert_float_absolute_eq!(geo_point2.x(), geo_nodes[1].x());
369            assert_float_eq::assert_float_absolute_eq!(geo_point2.y(), geo_nodes[1].y());
370        }
371
372        // delta X/Y test
373        {
374            let point1 = NodeXY32b::new(
375                OffsetB16::from_meters(0.).unwrap(),
376                OffsetB16::from_meters(185.).unwrap(),
377            );
378            let point2 = NodeXY32b::new(
379                OffsetB16::from_meters(66.).unwrap(),
380                OffsetB16::from_meters(0.).unwrap(),
381            );
382            let nodes = alloc::vec![
383                NodeXY::new(NodeOffsetPointXY::node_XY6(point1), None),
384                NodeXY::new(NodeOffsetPointXY::node_XY6(point2), None),
385            ];
386
387            let geo_nodes = NodeSetXY(nodes).to_line_string(ref_pos).into_points();
388            let geo_point1 = geo_types::Point::new(9.936_521, 53.550_728 + 0.001_667);
389            assert_float_eq::assert_float_absolute_eq!(geo_point1.x(), geo_nodes[0].x());
390            assert_float_eq::assert_float_absolute_eq!(geo_point1.y(), geo_nodes[0].y());
391
392            let geo_point2 = geo_types::Point::new(9.936_521 + 0.001_001, 53.550_728 + 0.001_667);
393            assert_float_eq::assert_float_absolute_eq!(geo_point2.x(), geo_nodes[1].x());
394            assert_float_eq::assert_float_absolute_eq!(geo_point2.y(), geo_nodes[1].y());
395        }
396
397        // delta X/Y with intermediate lat/lon
398        {
399            let point1 = NodeXY32b::new(
400                OffsetB16::from_meters(0.).unwrap(),
401                OffsetB16::from_meters(185.).unwrap(),
402            );
403            let point2 = NodeXY32b::new(
404                OffsetB16::from_meters(66.).unwrap(),
405                OffsetB16::from_meters(0.).unwrap(),
406            );
407            let point3 = NodeLLmD64b::new(
408                Longitude::from_deg(9.930_123),
409                Latitude::from_deg(53.550_420),
410            );
411            let point4 = NodeXY32b::new(
412                OffsetB16::from_meters(66.).unwrap(),
413                OffsetB16::from_meters(0.).unwrap(),
414            );
415            let nodes = alloc::vec![
416                NodeXY::new(NodeOffsetPointXY::node_XY6(point1), None),
417                NodeXY::new(NodeOffsetPointXY::node_XY6(point2), None),
418                NodeXY::new(NodeOffsetPointXY::node_LatLon(point3), None),
419                NodeXY::new(NodeOffsetPointXY::node_XY6(point4), None),
420            ];
421
422            let geo_nodes = NodeSetXY(nodes).to_line_string(ref_pos).into_points();
423            let geo_point1 = geo_types::Point::new(9.936_521, 53.550_728 + 0.001_667);
424            assert_float_eq::assert_float_absolute_eq!(geo_point1.x(), geo_nodes[0].x());
425            assert_float_eq::assert_float_absolute_eq!(geo_point1.y(), geo_nodes[0].y());
426
427            let geo_point2 = geo_types::Point::new(9.936_521 + 0.001_001, 53.550_728 + 0.001_667);
428            assert_float_eq::assert_float_absolute_eq!(geo_point2.x(), geo_nodes[1].x());
429            assert_float_eq::assert_float_absolute_eq!(geo_point2.y(), geo_nodes[1].y());
430
431            let geo_point3 = geo_types::Point::new(9.930_123, 53.550_420);
432            assert_float_eq::assert_float_absolute_eq!(geo_point3.x(), geo_nodes[2].x());
433            assert_float_eq::assert_float_absolute_eq!(geo_point3.y(), geo_nodes[2].y());
434
435            let geo_point4 = geo_types::Point::new(9.930_123 + 0.001_001, 53.550_420);
436            assert_float_eq::assert_float_absolute_eq!(geo_point4.x(), geo_nodes[3].x());
437            assert_float_eq::assert_float_absolute_eq!(geo_point4.y(), geo_nodes[3].y());
438        }
439    }
440
441    #[test]
442    #[cfg(feature = "mapem_2_2_1")]
443    fn nodeset_to_line_string_xy() {
444        use crate::standards::cdd_2_2_1::etsi_its_cdd::{Latitude, Longitude};
445        use crate::standards::dsrc_2_2_1::etsi_its_dsrc::{
446            NodeLLmD64b,
447            NodeOffsetPointXY,
448            NodeSetXY,
449            NodeXY,
450            NodeXY32b,
451            OffsetB16,
452        };
453
454        let ref_pos = geo_types::point! {x: 9.936_521, y: 53.550_728};
455
456        // latlon test
457        {
458            let point1 = NodeLLmD64b::new(
459                Longitude::from_deg(ref_pos.x()),
460                Latitude::from_deg(ref_pos.y() + 0.001_667),
461            );
462            let point2 = NodeLLmD64b::new(
463                Longitude::from_deg(ref_pos.x() + 0.001_001),
464                Latitude::from_deg(ref_pos.y() + 0.001_667),
465            );
466            let nodes = alloc::vec![
467                NodeXY::new(NodeOffsetPointXY::node_LatLon(point1), None),
468                NodeXY::new(NodeOffsetPointXY::node_LatLon(point2), None),
469            ];
470
471            let geo_nodes = NodeSetXY(nodes).to_line_string_xy(ref_pos).into_points();
472            let geo_point1 = geo_types::Point::new(0., 184.95);
473            assert_float_eq::assert_float_absolute_eq!(geo_point1.x(), geo_nodes[0].x(), 0.01);
474            assert_float_eq::assert_float_absolute_eq!(geo_point1.y(), geo_nodes[0].y(), 0.01);
475            let geo_point2 = geo_types::Point::new(66., 0.);
476            assert_float_eq::assert_float_absolute_eq!(geo_point2.x(), geo_nodes[1].x(), 0.01);
477            assert_float_eq::assert_float_absolute_eq!(geo_point2.y(), geo_nodes[1].y(), 0.01);
478        }
479
480        // delta X/Y test
481        {
482            let point1 = NodeXY32b::new(
483                OffsetB16::from_meters(0.).unwrap(),
484                OffsetB16::from_meters(185.).unwrap(),
485            );
486            let point2 = NodeXY32b::new(
487                OffsetB16::from_meters(66.).unwrap(),
488                OffsetB16::from_meters(0.).unwrap(),
489            );
490            let nodes = alloc::vec![
491                NodeXY::new(NodeOffsetPointXY::node_XY6(point1), None),
492                NodeXY::new(NodeOffsetPointXY::node_XY6(point2), None),
493            ];
494
495            let geo_nodes = NodeSetXY(nodes).to_line_string_xy(ref_pos).into_points();
496            let geo_point1 = geo_types::Point::new(0., 185.);
497            assert_float_eq::assert_float_absolute_eq!(geo_point1.x(), geo_nodes[0].x());
498            assert_float_eq::assert_float_absolute_eq!(geo_point1.y(), geo_nodes[0].y());
499
500            let geo_point2 = geo_types::Point::new(0. + 66., 185. + 0.);
501            assert_float_eq::assert_float_absolute_eq!(geo_point2.x(), geo_nodes[1].x());
502            assert_float_eq::assert_float_absolute_eq!(geo_point2.y(), geo_nodes[1].y());
503        }
504
505        // delta X/Y with intermediate lat/lon
506        {
507            let point1 = NodeXY32b::new(
508                OffsetB16::from_meters(0.).unwrap(),
509                OffsetB16::from_meters(185.).unwrap(),
510            );
511            let point2 = NodeXY32b::new(
512                OffsetB16::from_meters(66.).unwrap(),
513                OffsetB16::from_meters(0.).unwrap(),
514            );
515            let point3 = NodeLLmD64b::new(
516                Longitude::from_deg(ref_pos.x() + 0.001_001), // 66m
517                Latitude::from_deg(ref_pos.y() + 0.001_667),  // 184.95m
518            );
519            let point4 = NodeXY32b::new(
520                OffsetB16::from_meters(42.).unwrap(),
521                OffsetB16::from_meters(23.).unwrap(),
522            );
523            let nodes = alloc::vec![
524                NodeXY::new(NodeOffsetPointXY::node_XY6(point1), None),
525                NodeXY::new(NodeOffsetPointXY::node_XY6(point2), None),
526                NodeXY::new(NodeOffsetPointXY::node_LatLon(point3), None),
527                NodeXY::new(NodeOffsetPointXY::node_XY6(point4), None),
528            ];
529
530            let geo_nodes = NodeSetXY(nodes).to_line_string_xy(ref_pos).into_points();
531            let geo_point1 = geo_types::Point::new(0., 185.);
532            assert_float_eq::assert_float_absolute_eq!(geo_point1.x(), geo_nodes[0].x());
533            assert_float_eq::assert_float_absolute_eq!(geo_point1.y(), geo_nodes[0].y());
534
535            let geo_point2 = geo_types::Point::new(0. + 66., 185. + 0.);
536            assert_float_eq::assert_float_absolute_eq!(geo_point2.x(), geo_nodes[1].x());
537            assert_float_eq::assert_float_absolute_eq!(geo_point2.y(), geo_nodes[1].y());
538
539            let geo_point3 = geo_types::Point::new(66., 184.95);
540            assert_float_eq::assert_float_absolute_eq!(geo_point3.x(), geo_nodes[2].x(), 0.01);
541            assert_float_eq::assert_float_absolute_eq!(geo_point3.y(), geo_nodes[2].y(), 0.01);
542
543            let geo_point4 = geo_types::Point::new(66. + 42., 184.95 + 23.);
544            assert_float_eq::assert_float_absolute_eq!(geo_point4.x(), geo_nodes[3].x(), 0.01);
545            assert_float_eq::assert_float_absolute_eq!(geo_point4.y(), geo_nodes[3].y(), 0.01);
546        }
547    }
548}