Skip to main content

egml_io/codec/geometry/primitives/
linear_ring.rs

1use crate::codec::geometry::GmlDirectPosition;
2use crate::codec::geometry::direct_position_list::GmlDirectPositionList;
3use crate::codec::geometry::primitives::abstract_ring::{
4    deserialize_abstract_ring, serialize_abstract_ring,
5};
6use crate::error::Error;
7use crate::util::{
8    Formatting, GmlElement, XmlNode, XmlNodeContent, dedup_adjacent_positions,
9    extract_xml_element_spans, serialize_inner,
10};
11use egml_core::model::geometry::DirectPosition;
12use egml_core::model::geometry::primitives::{AsAbstractRing, LinearRing};
13use quick_xml::de;
14use serde::{Deserialize, Serialize};
15
16pub fn deserialize_linear_ring(xml_document: &[u8]) -> Result<LinearRing, Error> {
17    let spans = extract_xml_element_spans(xml_document)?;
18    let abstract_ring = deserialize_abstract_ring(xml_document, &spans)?;
19
20    let parsed: GmlLinearRing = de::from_reader(xml_document)?;
21    let mut points: Vec<DirectPosition> = parsed.content.unwrap().try_into()?;
22    dedup_adjacent_positions(&mut points, "LinearRing");
23    if points.first().unwrap() == points.last().unwrap() {
24        points.pop();
25    }
26
27    let linear_ring = LinearRing::from_abstract_ring(abstract_ring, points)?;
28    Ok(linear_ring)
29}
30
31pub fn serialize_linear_ring(
32    linear_ring: &LinearRing,
33    formatting: Formatting,
34) -> Result<XmlNode, Error> {
35    let mut xml_node_parts = serialize_abstract_ring(linear_ring.abstract_ring(), formatting)?;
36
37    if let Some(raw) = serialize_inner(GmlLinearRing::from(linear_ring), formatting)? {
38        xml_node_parts.content.push(XmlNodeContent::Raw(raw));
39    }
40
41    Ok(XmlNode::new(GmlElement::LinearRing.into(), xml_node_parts))
42}
43
44#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
45pub struct GmlLinearRing {
46    #[serde(rename = "$value", skip_serializing_if = "Option::is_none")]
47    pub content: Option<GmlLinearRingContent>,
48}
49
50#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
51pub enum GmlLinearRingContent {
52    #[serde(rename(serialize = "gml:posList", deserialize = "posList"))]
53    PosList(GmlDirectPositionList),
54
55    #[serde(rename = "pos")]
56    Pos(Vec<GmlDirectPosition>),
57}
58
59impl TryFrom<GmlLinearRingContent> for Vec<DirectPosition> {
60    type Error = Error;
61
62    fn try_from(value: GmlLinearRingContent) -> Result<Self, Self::Error> {
63        match value {
64            GmlLinearRingContent::PosList(x) => x.try_into(),
65            GmlLinearRingContent::Pos(x) => {
66                let points: Vec<DirectPosition> = x
67                    .into_iter()
68                    .map(|p| p.try_into())
69                    .collect::<Result<Vec<_>, _>>()?;
70
71                Ok(points)
72            }
73        }
74    }
75}
76
77impl TryFrom<GmlLinearRing> for LinearRing {
78    type Error = Error;
79
80    fn try_from(value: GmlLinearRing) -> Result<Self, Self::Error> {
81        let points: Vec<DirectPosition> = value
82            .content
83            .ok_or(Error::ElementNotFound("No element found".to_string()))?
84            .try_into()?;
85
86        let linear_ring = LinearRing::new(points)?;
87        Ok(linear_ring)
88    }
89}
90
91impl From<&LinearRing> for GmlLinearRing {
92    fn from(ring: &LinearRing) -> Self {
93        // GML requires the ring to be closed: the closing vertex (= first point) must
94        // be written explicitly, but LinearRing stores points in open form (no repeat).
95        let mut points: Vec<DirectPosition> = ring.points().to_vec();
96        if let Some(&first) = ring.points().first() {
97            points.push(first);
98        }
99
100        Self {
101            content: Some(GmlLinearRingContent::PosList(GmlDirectPositionList::from(
102                points.as_slice(),
103            ))),
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::GmlLinearRingContent;
111    use crate::codec::geometry::primitives::{
112        GmlLinearRing, deserialize_linear_ring, serialize_linear_ring,
113    };
114    use crate::util::{Formatting, extract_xml_element_spans};
115    use egml_core::model::base::Id;
116    use egml_core::model::geometry::DirectPosition;
117    use egml_core::model::geometry::primitives::LinearRing;
118    use quick_xml::de;
119
120    fn make_triangle() -> LinearRing {
121        let points = vec![
122            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
123            DirectPosition::new(1.0, 0.0, 0.0).unwrap(),
124            DirectPosition::new(0.0, 1.0, 0.0).unwrap(),
125        ];
126        LinearRing::new(points).unwrap()
127    }
128
129    #[test]
130    fn deserialize_linear_ring_with_pos_list_and_id() {
131        let xml_document = b"<gml:LinearRing gml:id=\"4018115_LR.d2yyBHNssydL8g3B8MvW\">
132<gml:posList srsDimension=\"3\">
133678058.447 5403817.567 424.209
134678058.275 5403817.484 424.209
135678058.689 5403816.628 424.209
136678058.871 5403816.718 424.209
137678058.447 5403817.567 424.209
138</gml:posList>
139</gml:LinearRing>";
140
141        let linear_ring: LinearRing =
142            deserialize_linear_ring(xml_document).expect("should deserialize");
143        use egml_core::model::base::AsAbstractGml;
144        assert!(linear_ring.id().is_some());
145    }
146
147    #[test]
148    fn deserialize_linear_ring_with_pos_list() {
149        let xml_document = b"<gml:LinearRing>
150                            <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>
151                        </gml:LinearRing>";
152
153        let linear_ring_result = deserialize_linear_ring(xml_document);
154
155        assert!(linear_ring_result.is_ok());
156        let linear_ring = linear_ring_result.unwrap();
157        assert_eq!(linear_ring.points().len(), 3);
158    }
159
160    #[test]
161    fn deserialize_linear_ring_repairs_adjacent_duplicate_points() {
162        let xml_document = b"<gml:LinearRing>
163                      <gml:posList>0 0 0 1 0 0 1 0 0 1 1 0 0 0 0</gml:posList>
164                    </gml:LinearRing>";
165
166        let linear_ring = deserialize_linear_ring(xml_document).expect("should deserialize");
167
168        assert_eq!(linear_ring.points().len(), 3);
169    }
170
171    #[test]
172    fn deserialize_linear_ring_with_duplicate_points_returns_error() {
173        let xml_document = b"<gml:LinearRing gml:id=\"DEBY_LOD2_4959457_LR.EEAbfUPItTlOGZGH7VDv\">
174                      <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>
175                    </gml:LinearRing>";
176
177        let linear_ring_result = deserialize_linear_ring(xml_document);
178
179        assert!(linear_ring_result.is_err());
180    }
181
182    #[test]
183    fn serialize_linear_ring_writes_gml_tags() {
184        let linear_ring = make_triangle();
185        let xml_node = serialize_linear_ring(&linear_ring, Formatting::Compact).unwrap();
186        let xml = xml_node
187            .to_string(Formatting::Compact)
188            .expect("should serialize");
189
190        assert!(xml.contains("<gml:LinearRing"));
191        assert!(xml.contains("<gml:posList"));
192        assert!(!xml.contains("id="));
193    }
194
195    #[test]
196    fn serialize_linear_ring_appends_closing_vertex() {
197        let ring = make_triangle(); // 3 open points
198
199        // Verify via the intermediate GmlLinearRing that the closing vertex is present
200        let gml = GmlLinearRing::from(&ring);
201        let positions: Vec<DirectPosition> = match gml.content.unwrap() {
202            GmlLinearRingContent::PosList(pos_list) => pos_list.try_into().unwrap(),
203            GmlLinearRingContent::Pos(_) => panic!("expected PosList"),
204        };
205        assert_eq!(positions.len(), 4); // 3 open points + closing vertex
206        assert_eq!(positions.first(), positions.last()); // closing vertex equals first point
207    }
208
209    #[test]
210    fn serialize_linear_ring_with_id() {
211        use egml_core::model::base::AsAbstractGmlMut;
212        let points = vec![
213            DirectPosition::new(1.0, 0.0, 0.0).unwrap(),
214            DirectPosition::new(0.0, 1.0, 0.0).unwrap(),
215            DirectPosition::new(0.0, 0.0, 1.0).unwrap(),
216        ];
217        let mut linear_ring = LinearRing::new(points).unwrap();
218        linear_ring.set_id(Id::from_hashed_string("test-ring"));
219
220        let xml_node = serialize_linear_ring(&linear_ring, Formatting::Compact).unwrap();
221        let xml = xml_node
222            .to_string(Formatting::Compact)
223            .expect("should serialize");
224        assert!(xml.contains("id="));
225    }
226
227    #[test]
228    fn round_trip_linear_ring_preserves_points() {
229        let linear_ring = make_triangle();
230        let xml_node = serialize_linear_ring(&linear_ring, Formatting::Compact).unwrap();
231        let xml = xml_node
232            .to_string(Formatting::Compact)
233            .expect("should serialize");
234
235        let recovered_linear_ring: LinearRing =
236            deserialize_linear_ring(xml.as_ref()).expect("should deserialize");
237
238        assert_eq!(
239            recovered_linear_ring.points().len(),
240            linear_ring.points().len()
241        );
242        for (a, b) in recovered_linear_ring
243            .points()
244            .iter()
245            .zip(linear_ring.points().iter())
246        {
247            assert_eq!(a.x(), b.x());
248            assert_eq!(a.y(), b.y());
249            assert_eq!(a.z(), b.z());
250        }
251    }
252
253    #[test]
254    fn round_trip_linear_ring_preserves_float_precision() {
255        let points = vec![
256            DirectPosition::new(678058.447, 5403817.567, 424.209).unwrap(),
257            DirectPosition::new(678058.275, 5403817.484, 424.209).unwrap(),
258            DirectPosition::new(678058.689, 5403816.628, 424.209).unwrap(),
259        ];
260        let linear_ring = LinearRing::new(points.clone()).unwrap();
261
262        let xml_node = serialize_linear_ring(&linear_ring, Formatting::Compact).unwrap();
263        let xml = xml_node
264            .to_string(Formatting::Compact)
265            .expect("should serialize");
266
267        let recovered_linear_ring: LinearRing =
268            deserialize_linear_ring(xml.as_ref()).expect("should deserialize");
269
270        for (a, b) in recovered_linear_ring.points().iter().zip(points.iter()) {
271            assert_eq!(a.x(), b.x());
272            assert_eq!(a.y(), b.y());
273            assert_eq!(a.z(), b.z());
274        }
275    }
276
277    #[test]
278    fn round_trip_linear_ring_from_xml() {
279        let xml_document = "<gml:LinearRing gml:id=\"PolyID7350_878_759628_120742_0\">\
280            <gml:posList srsDimension=\"3\">0 0 0 1 0 0 0 1 0 0 0 0</gml:posList>\
281            </gml:LinearRing>";
282
283        let linear_ring =
284            deserialize_linear_ring(xml_document.as_ref()).expect("should deserialize");
285
286        let xml_node = serialize_linear_ring(&linear_ring, Formatting::Compact).unwrap();
287        let output_xml = xml_node
288            .to_string(Formatting::Compact)
289            .expect("should serialize");
290
291        assert_eq!(xml_document, output_xml);
292    }
293
294    #[test]
295    fn deserialize_linear_ring_with_pos_elements() {
296        let xml_document = b"<gml:LinearRing gml:id=\"PolyID7350_878_759628_120742_0\">
297                      <gml:pos>457842.0 5439088.0 118.317691453624</gml:pos>
298                      <gml:pos>457842.0 5439093.0 115.430940107676</gml:pos>
299                      <gml:pos>457842.0 5439093.0 111.8</gml:pos>
300                      <gml:pos>457842.0 5439083.0 111.8</gml:pos>
301                      <gml:pos>457842.0 5439083.0 115.430940107676</gml:pos>
302                      <gml:pos>457842.0 5439088.0 118.317691453624</gml:pos>
303                    </gml:LinearRing>";
304
305        let linear_ring_result = deserialize_linear_ring(xml_document);
306
307        assert!(linear_ring_result.is_ok());
308        let linear_ring = linear_ring_result.unwrap();
309        assert_eq!(linear_ring.points().len(), 5);
310    }
311}