Skip to main content

egml_io/geometry/primitives/
linear_ring.rs

1use crate::GmlDirectPosition;
2use crate::error::Error;
3use crate::geometry::direct_position_list::GmlDirectPositionList;
4use egml_core::model::base::{AsAbstractGml, AsAbstractGmlMut};
5use egml_core::model::geometry::DirectPosition;
6use egml_core::model::geometry::primitives::{AbstractRing, LinearRing};
7use quick_xml::se;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
11pub struct GmlLinearRing {
12    #[serde(
13        rename(serialize = "@gml:id", deserialize = "@id"),
14        skip_serializing_if = "Option::is_none"
15    )]
16    pub id: Option<String>,
17
18    #[serde(rename = "$value", skip_serializing_if = "Option::is_none")]
19    pub content: Option<GmlLinearRingContent>,
20}
21
22#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
23pub enum GmlLinearRingContent {
24    #[serde(rename(serialize = "gml:posList", deserialize = "posList"))]
25    PosList(GmlDirectPositionList),
26
27    #[serde(rename = "pos")]
28    Pos(Vec<GmlDirectPosition>),
29}
30
31impl TryFrom<GmlLinearRingContent> for Vec<DirectPosition> {
32    type Error = Error;
33
34    fn try_from(value: GmlLinearRingContent) -> Result<Self, Self::Error> {
35        match value {
36            GmlLinearRingContent::PosList(x) => x.try_into(),
37            GmlLinearRingContent::Pos(x) => {
38                let points: Vec<DirectPosition> = x
39                    .into_iter()
40                    .map(|p| p.try_into())
41                    .collect::<Result<Vec<_>, _>>()?;
42
43                Ok(points)
44            }
45        }
46    }
47}
48
49impl TryFrom<GmlLinearRing> for LinearRing {
50    type Error = Error;
51
52    fn try_from(value: GmlLinearRing) -> Result<Self, Self::Error> {
53        let id = value.id.map(|id| id.try_into()).transpose()?;
54        let mut abstract_ring = AbstractRing::default();
55        abstract_ring.set_id(id);
56
57        let mut points: Vec<DirectPosition> = value
58            .content
59            .ok_or(Error::ElementNotFound("No element found".to_string()))?
60            .try_into()?;
61
62        points.dedup();
63        if points.first().unwrap() == points.last().unwrap() {
64            points.pop();
65        }
66
67        let linear_ring = LinearRing::new(abstract_ring, points)?;
68        Ok(linear_ring)
69    }
70}
71
72impl From<&LinearRing> for GmlLinearRing {
73    fn from(ring: &LinearRing) -> Self {
74        // GML requires the ring to be closed: the closing vertex (= first point) must
75        // be written explicitly, but LinearRing stores points in open form (no repeat).
76        let mut points: Vec<DirectPosition> = ring.points().to_vec();
77        if let Some(&first) = ring.points().first() {
78            points.push(first);
79        }
80
81        Self {
82            id: ring.id().map(|id| id.to_string()),
83            content: Some(GmlLinearRingContent::PosList(GmlDirectPositionList::from(
84                points.as_slice(),
85            ))),
86        }
87    }
88}
89
90/// Serializes a [`LinearRing`] to a GML XML string.
91///
92/// # Errors
93///
94/// Returns [`Error::XmlSe`] if serialization fails.
95pub fn serialize_linear_ring(ring: &LinearRing) -> Result<String, Error> {
96    let gml_ring = GmlLinearRing::from(ring);
97    Ok(se::to_string_with_root("gml:LinearRing", &gml_ring)?)
98}
99
100#[cfg(test)]
101mod tests {
102    use super::GmlLinearRingContent;
103    use crate::Error;
104    use crate::primitives::{GmlLinearRing, serialize_linear_ring};
105    use egml_core::model::base::Id;
106    use egml_core::model::geometry::DirectPosition;
107    use egml_core::model::geometry::primitives::{AbstractRing, LinearRing};
108    use quick_xml::de;
109
110    fn make_triangle() -> LinearRing {
111        let points = vec![
112            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
113            DirectPosition::new(1.0, 0.0, 0.0).unwrap(),
114            DirectPosition::new(0.0, 1.0, 0.0).unwrap(),
115        ];
116        LinearRing::new(AbstractRing::default(), points).unwrap()
117    }
118
119    #[test]
120    fn deserialize_linear_ring_with_pos_list_and_id() {
121        let xml_document = b"<gml:LinearRing gml:id=\"4018115_LR.d2yyBHNssydL8g3B8MvW\">
122<gml:posList srsDimension=\"3\">
123678058.447 5403817.567 424.209
124678058.275 5403817.484 424.209
125678058.689 5403816.628 424.209
126678058.871 5403816.718 424.209
127678058.447 5403817.567 424.209
128</gml:posList>
129</gml:LinearRing>";
130
131        let parsed_geometry: GmlLinearRing = de::from_reader(xml_document.as_ref()).expect("");
132        let l: LinearRing = parsed_geometry.try_into().unwrap();
133        use egml_core::model::base::AsAbstractGml;
134        assert!(l.id().is_some());
135    }
136
137    #[test]
138    fn deserialize_linear_ring_with_pos_list() {
139        let xml_document = b"<gml:LinearRing>
140                            <gml:posList>350.54400634765625 972.9130249023438 0.11999999731779099 350.5414201635045 968.6025425887852 0.11999999731779099 350.54400634765625 968.6025096366793 0.11999999731779099 350.54400634765625 972.9130249023438 0.11999999731779099</gml:posList>
141                        </gml:LinearRing>";
142
143        let parsed_geometry: GmlLinearRing = de::from_reader(xml_document.as_ref()).expect("");
144        let result: Result<LinearRing, Error> = parsed_geometry.try_into();
145
146        assert!(result.is_ok());
147        let linear_ring = result.unwrap();
148        assert_eq!(linear_ring.points().len(), 3);
149    }
150
151    #[test]
152    fn deserialize_linear_ring_with_duplicate_points_returns_error() {
153        let xml_document = b"<gml:LinearRing gml:id=\"DEBY_LOD2_4959457_LR.EEAbfUPItTlOGZGH7VDv\">
154                      <gml:posList>691040.851 5336002.449 529.908 691040.741 5336002.172 529.908 691040.851 5336002.449 529.908 691040.851 5336002.449 529.908</gml:posList>
155                    </gml:LinearRing>";
156
157        let parsed_geometry: GmlLinearRing = de::from_reader(xml_document.as_ref()).expect("");
158        let result: Result<LinearRing, Error> = parsed_geometry.try_into();
159
160        assert!(result.is_err());
161    }
162
163    #[test]
164    fn serialize_linear_ring_writes_gml_tags() {
165        let ring = make_triangle();
166        let xml = serialize_linear_ring(&ring).unwrap();
167
168        assert!(xml.contains("<gml:LinearRing"));
169        assert!(xml.contains("<gml:posList"));
170        assert!(!xml.contains("id="));
171    }
172
173    #[test]
174    fn serialize_linear_ring_appends_closing_vertex() {
175        let ring = make_triangle(); // 3 open points
176
177        // Verify via the intermediate GmlLinearRing that the closing vertex is present
178        let gml = GmlLinearRing::from(&ring);
179        let positions: Vec<DirectPosition> = match gml.content.unwrap() {
180            GmlLinearRingContent::PosList(pos_list) => pos_list.try_into().unwrap(),
181            GmlLinearRingContent::Pos(_) => panic!("expected PosList"),
182        };
183        assert_eq!(positions.len(), 4); // 3 open points + closing vertex
184        assert_eq!(positions.first(), positions.last()); // closing vertex equals first point
185    }
186
187    #[test]
188    fn serialize_linear_ring_with_id() {
189        use egml_core::model::base::AsAbstractGmlMut;
190        let points = vec![
191            DirectPosition::new(1.0, 0.0, 0.0).unwrap(),
192            DirectPosition::new(0.0, 1.0, 0.0).unwrap(),
193            DirectPosition::new(0.0, 0.0, 1.0).unwrap(),
194        ];
195        let mut abstract_ring = AbstractRing::default();
196        abstract_ring.set_id(Some(Id::from_hashed_string("test-ring")));
197        let ring = LinearRing::new(abstract_ring, points).unwrap();
198
199        let xml = serialize_linear_ring(&ring).unwrap();
200        assert!(xml.contains("id="));
201    }
202
203    #[test]
204    fn round_trip_linear_ring_preserves_points() {
205        let ring = make_triangle();
206        let xml = serialize_linear_ring(&ring).unwrap();
207
208        let gml: GmlLinearRing = de::from_reader(xml.as_bytes()).unwrap();
209        let recovered: LinearRing = gml.try_into().unwrap();
210
211        assert_eq!(recovered.points().len(), ring.points().len());
212        for (a, b) in recovered.points().iter().zip(ring.points().iter()) {
213            assert_eq!(a.x(), b.x());
214            assert_eq!(a.y(), b.y());
215            assert_eq!(a.z(), b.z());
216        }
217    }
218
219    #[test]
220    fn round_trip_linear_ring_preserves_float_precision() {
221        let points = vec![
222            DirectPosition::new(678058.447, 5403817.567, 424.209).unwrap(),
223            DirectPosition::new(678058.275, 5403817.484, 424.209).unwrap(),
224            DirectPosition::new(678058.689, 5403816.628, 424.209).unwrap(),
225        ];
226        let ring = LinearRing::new(AbstractRing::default(), points.clone()).unwrap();
227
228        let xml = serialize_linear_ring(&ring).unwrap();
229        let gml: GmlLinearRing = de::from_reader(xml.as_bytes()).unwrap();
230        let recovered: LinearRing = gml.try_into().unwrap();
231
232        for (a, b) in recovered.points().iter().zip(points.iter()) {
233            assert_eq!(a.x(), b.x());
234            assert_eq!(a.y(), b.y());
235            assert_eq!(a.z(), b.z());
236        }
237    }
238
239    #[test]
240    fn round_trip_linear_ring_from_xml() {
241        let input_xml = "<gml:LinearRing gml:id=\"PolyID7350_878_759628_120742_0\">\
242            <gml:posList srsDimension=\"3\">0 0 0 1 0 0 0 1 0 0 0 0</gml:posList>\
243            </gml:LinearRing>";
244
245        let gml: GmlLinearRing = de::from_reader(input_xml.as_ref()).unwrap();
246        let ring: LinearRing = gml.try_into().unwrap();
247        let output_xml = serialize_linear_ring(&ring).unwrap();
248
249        assert_eq!(input_xml, output_xml);
250    }
251
252    #[test]
253    fn deserialize_linear_ring_with_pos_elements() {
254        let xml_document = b"<gml:LinearRing gml:id=\"PolyID7350_878_759628_120742_0\">
255                      <gml:pos>457842.0 5439088.0 118.317691453624</gml:pos>
256                      <gml:pos>457842.0 5439093.0 115.430940107676</gml:pos>
257                      <gml:pos>457842.0 5439093.0 111.8</gml:pos>
258                      <gml:pos>457842.0 5439083.0 111.8</gml:pos>
259                      <gml:pos>457842.0 5439083.0 115.430940107676</gml:pos>
260                      <gml:pos>457842.0 5439088.0 118.317691453624</gml:pos>
261                    </gml:LinearRing>";
262
263        let parsed_geometry: GmlLinearRing = de::from_reader(xml_document.as_ref()).expect("");
264        let result: Result<LinearRing, Error> = parsed_geometry.try_into();
265
266        assert!(result.is_ok());
267        let linear_ring = result.unwrap();
268        assert_eq!(linear_ring.points().len(), 5);
269    }
270}