Skip to main content

rustial_engine/
geometry.rs

1//! Internal geometry model for geographic vector features.
2//!
3//! This module defines the geometry primitives consumed by:
4//!
5//! - [`VectorLayer`](crate::layers::VectorLayer) -- tessellation + terrain draping
6//! - [`tessellator`](crate::tessellator) -- polygon triangulation + line stroking
7//! - [`simplify`](crate::simplify) -- Douglas-Peucker line simplification
8//! - [`geojson`](crate::geojson) parser (behind the `geojson` feature flag)
9//! - Both renderers (Bevy and pure WGPU) via [`VectorMeshData`](crate::layers::VectorMeshData)
10//!
11//! # Design
12//!
13//! - All coordinates are [`GeoCoord`] (lat/lon/alt in degrees + meters)
14//!   from the engine's math module.
15//! - Geometry types follow the OGC Simple Features / GeoJSON model:
16//!   `Point`, `LineString`, `Polygon`, plus their `Multi*` variants
17//!   and `GeometryCollection`.
18//! - [`Feature`] pairs a geometry with a string-keyed property map.
19//! - [`PropertyValue`] is a lightweight four-variant enum (Null, Bool,
20//!   Number, String) that covers the JSON property space without
21//!   pulling in a full `serde_json::Value` dependency.
22//! - Types are intentionally plain data: `Clone`, `Debug`, no
23//!   reference lifetimes.  They are meant to be owned by layers
24//!   and cheaply diffed / replaced each frame.
25
26use rustial_math::GeoCoord;
27use std::collections::HashMap;
28use std::fmt;
29
30// ---------------------------------------------------------------------------
31// Geometry primitives
32// ---------------------------------------------------------------------------
33
34/// A single geographic point.
35#[derive(Debug, Clone)]
36pub struct Point {
37    /// The geographic coordinate of the point.
38    pub coord: GeoCoord,
39}
40
41/// An ordered sequence of coordinates forming a line.
42///
43/// Must contain at least two coordinates to be meaningful; consumers
44/// should treat shorter sequences as degenerate.
45#[derive(Debug, Clone)]
46pub struct LineString {
47    /// The coordinates of the line vertices.
48    pub coords: Vec<GeoCoord>,
49}
50
51/// A polygon with an exterior ring and optional interior rings (holes).
52///
53/// # Winding order
54///
55/// By convention the exterior ring is counter-clockwise and interior
56/// rings (holes) are clockwise, matching the GeoJSON / OGC direction.
57/// The tessellator does not currently enforce winding, but future
58/// validation may rely on it.
59#[derive(Debug, Clone)]
60pub struct Polygon {
61    /// Exterior ring (counter-clockwise winding).
62    pub exterior: Vec<GeoCoord>,
63    /// Interior rings / holes (clockwise winding).
64    pub interiors: Vec<Vec<GeoCoord>>,
65}
66
67/// A collection of points.
68#[derive(Debug, Clone)]
69pub struct MultiPoint {
70    /// The points in the collection.
71    pub points: Vec<Point>,
72}
73
74/// A collection of line strings.
75#[derive(Debug, Clone)]
76pub struct MultiLineString {
77    /// The line strings in the collection.
78    pub lines: Vec<LineString>,
79}
80
81/// A collection of polygons.
82#[derive(Debug, Clone)]
83pub struct MultiPolygon {
84    /// The polygons in the collection.
85    pub polygons: Vec<Polygon>,
86}
87
88// ---------------------------------------------------------------------------
89// Geometry enum
90// ---------------------------------------------------------------------------
91
92/// Any supported geometry type (OGC Simple Features).
93///
94/// Matches the seven GeoJSON geometry types one-to-one.
95#[derive(Debug, Clone)]
96pub enum Geometry {
97    /// A single point.
98    Point(Point),
99    /// An ordered sequence of coordinates.
100    LineString(LineString),
101    /// A polygon with optional holes.
102    Polygon(Polygon),
103    /// Multiple points.
104    MultiPoint(MultiPoint),
105    /// Multiple line strings.
106    MultiLineString(MultiLineString),
107    /// Multiple polygons.
108    MultiPolygon(MultiPolygon),
109    /// A heterogeneous collection of geometries.
110    GeometryCollection(Vec<Geometry>),
111}
112
113impl Geometry {
114    /// Return the name of the geometry type (e.g. `"Point"`, `"Polygon"`).
115    ///
116    /// Useful for diagnostics and GeoJSON round-tripping.
117    pub fn type_name(&self) -> &'static str {
118        match self {
119            Geometry::Point(_) => "Point",
120            Geometry::LineString(_) => "LineString",
121            Geometry::Polygon(_) => "Polygon",
122            Geometry::MultiPoint(_) => "MultiPoint",
123            Geometry::MultiLineString(_) => "MultiLineString",
124            Geometry::MultiPolygon(_) => "MultiPolygon",
125            Geometry::GeometryCollection(_) => "GeometryCollection",
126        }
127    }
128
129    /// Return `true` if the geometry contains no coordinate data.
130    ///
131    /// A `GeometryCollection` is empty when it contains zero children
132    /// or all children are themselves empty.
133    pub fn is_empty(&self) -> bool {
134        match self {
135            Geometry::Point(_) => false,
136            Geometry::LineString(ls) => ls.coords.is_empty(),
137            Geometry::Polygon(p) => p.exterior.is_empty(),
138            Geometry::MultiPoint(mp) => mp.points.is_empty(),
139            Geometry::MultiLineString(mls) => mls.lines.is_empty(),
140            Geometry::MultiPolygon(mp) => mp.polygons.is_empty(),
141            Geometry::GeometryCollection(gc) => gc.iter().all(|g| g.is_empty()),
142        }
143    }
144
145    /// Count the total number of coordinate vertices in this geometry.
146    ///
147    /// For compound types the count is the sum of all children.
148    pub fn coord_count(&self) -> usize {
149        match self {
150            Geometry::Point(_) => 1,
151            Geometry::LineString(ls) => ls.coords.len(),
152            Geometry::Polygon(p) => {
153                p.exterior.len() + p.interiors.iter().map(|h| h.len()).sum::<usize>()
154            }
155            Geometry::MultiPoint(mp) => mp.points.len(),
156            Geometry::MultiLineString(mls) => mls.lines.iter().map(|l| l.coords.len()).sum(),
157            Geometry::MultiPolygon(mp) => mp
158                .polygons
159                .iter()
160                .map(|p| p.exterior.len() + p.interiors.iter().map(|h| h.len()).sum::<usize>())
161                .sum(),
162            Geometry::GeometryCollection(gc) => gc.iter().map(|g| g.coord_count()).sum(),
163        }
164    }
165}
166
167// ---------------------------------------------------------------------------
168// PropertyValue
169// ---------------------------------------------------------------------------
170
171/// A value in a feature's property map.
172///
173/// Covers the four JSON scalar types.  Nested objects / arrays are
174/// not supported; the GeoJSON parser serialises them to their JSON
175/// string representation via [`PropertyValue::String`].
176#[derive(Debug, Clone)]
177pub enum PropertyValue {
178    /// A null / missing value.
179    Null,
180    /// A boolean value.
181    Bool(bool),
182    /// A numeric value (f64 covers JSON Number).
183    Number(f64),
184    /// A string value.
185    String(String),
186}
187
188impl PropertyValue {
189    /// Return `true` if the value is [`Null`](Self::Null).
190    pub fn is_null(&self) -> bool {
191        matches!(self, PropertyValue::Null)
192    }
193
194    /// Try to extract a boolean.
195    pub fn as_bool(&self) -> Option<bool> {
196        match self {
197            PropertyValue::Bool(b) => Some(*b),
198            _ => None,
199        }
200    }
201
202    /// Try to extract a number.
203    pub fn as_f64(&self) -> Option<f64> {
204        match self {
205            PropertyValue::Number(n) => Some(*n),
206            _ => None,
207        }
208    }
209
210    /// Try to extract a string slice.
211    pub fn as_str(&self) -> Option<&str> {
212        match self {
213            PropertyValue::String(s) => Some(s),
214            _ => None,
215        }
216    }
217}
218
219impl fmt::Display for PropertyValue {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        match self {
222            PropertyValue::Null => write!(f, "null"),
223            PropertyValue::Bool(b) => write!(f, "{b}"),
224            PropertyValue::Number(n) => write!(f, "{n}"),
225            PropertyValue::String(s) => write!(f, "{s}"),
226        }
227    }
228}
229
230impl PartialEq for PropertyValue {
231    fn eq(&self, other: &Self) -> bool {
232        match (self, other) {
233            (PropertyValue::Null, PropertyValue::Null) => true,
234            (PropertyValue::Bool(a), PropertyValue::Bool(b)) => a == b,
235            (PropertyValue::Number(a), PropertyValue::Number(b)) => a == b,
236            (PropertyValue::String(a), PropertyValue::String(b)) => a == b,
237            _ => false,
238        }
239    }
240}
241
242// ---------------------------------------------------------------------------
243// Feature / FeatureCollection
244// ---------------------------------------------------------------------------
245
246/// A geographic feature: geometry + key-value properties.
247///
248/// Mirrors a GeoJSON Feature object.
249#[derive(Debug, Clone)]
250pub struct Feature {
251    /// The geometry of the feature.
252    pub geometry: Geometry,
253    /// Key-value properties associated with the feature.
254    pub properties: HashMap<String, PropertyValue>,
255}
256
257impl Feature {
258    /// Look up a property by key.
259    pub fn property(&self, key: &str) -> Option<&PropertyValue> {
260        self.properties.get(key)
261    }
262}
263
264/// A collection of [`Feature`]s.
265///
266/// The primary container returned by the GeoJSON parser and consumed
267/// by [`VectorLayer`](crate::layers::VectorLayer).
268#[derive(Debug, Clone, Default)]
269pub struct FeatureCollection {
270    /// The features in the collection.
271    pub features: Vec<Feature>,
272}
273
274impl FeatureCollection {
275    /// Number of features in the collection.
276    pub fn len(&self) -> usize {
277        self.features.len()
278    }
279
280    /// Whether the collection is empty.
281    pub fn is_empty(&self) -> bool {
282        self.features.is_empty()
283    }
284
285    /// Total number of coordinate vertices across all features.
286    pub fn total_coords(&self) -> usize {
287        self.features.iter().map(|f| f.geometry.coord_count()).sum()
288    }
289
290    /// Iterate over features.
291    pub fn iter(&self) -> std::slice::Iter<'_, Feature> {
292        self.features.iter()
293    }
294
295    /// Iterate mutably over features.
296    pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Feature> {
297        self.features.iter_mut()
298    }
299}
300
301impl<'a> IntoIterator for &'a FeatureCollection {
302    type Item = &'a Feature;
303    type IntoIter = std::slice::Iter<'a, Feature>;
304
305    fn into_iter(self) -> Self::IntoIter {
306        self.features.iter()
307    }
308}
309
310impl<'a> IntoIterator for &'a mut FeatureCollection {
311    type Item = &'a mut Feature;
312    type IntoIter = std::slice::IterMut<'a, Feature>;
313
314    fn into_iter(self) -> Self::IntoIter {
315        self.features.iter_mut()
316    }
317}
318
319// ---------------------------------------------------------------------------
320// Tests
321// ---------------------------------------------------------------------------
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    fn sample_point() -> Geometry {
328        Geometry::Point(Point {
329            coord: GeoCoord::from_lat_lon(51.1, 17.0),
330        })
331    }
332
333    fn sample_linestring() -> Geometry {
334        Geometry::LineString(LineString {
335            coords: vec![
336                GeoCoord::from_lat_lon(0.0, 0.0),
337                GeoCoord::from_lat_lon(1.0, 1.0),
338                GeoCoord::from_lat_lon(2.0, 2.0),
339            ],
340        })
341    }
342
343    fn sample_polygon() -> Geometry {
344        Geometry::Polygon(Polygon {
345            exterior: vec![
346                GeoCoord::from_lat_lon(0.0, 0.0),
347                GeoCoord::from_lat_lon(0.0, 1.0),
348                GeoCoord::from_lat_lon(1.0, 1.0),
349                GeoCoord::from_lat_lon(1.0, 0.0),
350                GeoCoord::from_lat_lon(0.0, 0.0),
351            ],
352            interiors: vec![vec![
353                GeoCoord::from_lat_lon(0.2, 0.2),
354                GeoCoord::from_lat_lon(0.2, 0.4),
355                GeoCoord::from_lat_lon(0.4, 0.4),
356                GeoCoord::from_lat_lon(0.4, 0.2),
357                GeoCoord::from_lat_lon(0.2, 0.2),
358            ]],
359        })
360    }
361
362    // -- type_name --------------------------------------------------------
363
364    #[test]
365    fn type_name_point() {
366        assert_eq!(sample_point().type_name(), "Point");
367    }
368
369    #[test]
370    fn type_name_linestring() {
371        assert_eq!(sample_linestring().type_name(), "LineString");
372    }
373
374    #[test]
375    fn type_name_polygon() {
376        assert_eq!(sample_polygon().type_name(), "Polygon");
377    }
378
379    #[test]
380    fn type_name_multi_and_collection() {
381        let mp = Geometry::MultiPoint(MultiPoint {
382            points: vec![Point {
383                coord: GeoCoord::from_lat_lon(0.0, 0.0),
384            }],
385        });
386        assert_eq!(mp.type_name(), "MultiPoint");
387
388        let mls = Geometry::MultiLineString(MultiLineString { lines: vec![] });
389        assert_eq!(mls.type_name(), "MultiLineString");
390
391        let mpoly = Geometry::MultiPolygon(MultiPolygon { polygons: vec![] });
392        assert_eq!(mpoly.type_name(), "MultiPolygon");
393
394        let gc = Geometry::GeometryCollection(vec![]);
395        assert_eq!(gc.type_name(), "GeometryCollection");
396    }
397
398    // -- is_empty ---------------------------------------------------------
399
400    #[test]
401    fn point_is_never_empty() {
402        assert!(!sample_point().is_empty());
403    }
404
405    #[test]
406    fn empty_linestring_is_empty() {
407        let ls = Geometry::LineString(LineString { coords: vec![] });
408        assert!(ls.is_empty());
409        assert!(!sample_linestring().is_empty());
410    }
411
412    #[test]
413    fn empty_polygon_is_empty() {
414        let p = Geometry::Polygon(Polygon {
415            exterior: vec![],
416            interiors: vec![],
417        });
418        assert!(p.is_empty());
419        assert!(!sample_polygon().is_empty());
420    }
421
422    #[test]
423    fn empty_geometry_collection() {
424        let gc = Geometry::GeometryCollection(vec![]);
425        assert!(gc.is_empty());
426
427        let gc_with_empty = Geometry::GeometryCollection(vec![Geometry::LineString(LineString {
428            coords: vec![],
429        })]);
430        assert!(gc_with_empty.is_empty());
431
432        let gc_with_content = Geometry::GeometryCollection(vec![sample_point()]);
433        assert!(!gc_with_content.is_empty());
434    }
435
436    // -- coord_count ------------------------------------------------------
437
438    #[test]
439    fn coord_count_point() {
440        assert_eq!(sample_point().coord_count(), 1);
441    }
442
443    #[test]
444    fn coord_count_linestring() {
445        assert_eq!(sample_linestring().coord_count(), 3);
446    }
447
448    #[test]
449    fn coord_count_polygon_with_hole() {
450        // 5 exterior + 5 hole = 10
451        assert_eq!(sample_polygon().coord_count(), 10);
452    }
453
454    #[test]
455    fn coord_count_multi_polygon() {
456        let mp = Geometry::MultiPolygon(MultiPolygon {
457            polygons: vec![
458                Polygon {
459                    exterior: vec![
460                        GeoCoord::from_lat_lon(0.0, 0.0),
461                        GeoCoord::from_lat_lon(0.0, 1.0),
462                        GeoCoord::from_lat_lon(1.0, 0.0),
463                    ],
464                    interiors: vec![],
465                },
466                Polygon {
467                    exterior: vec![
468                        GeoCoord::from_lat_lon(2.0, 2.0),
469                        GeoCoord::from_lat_lon(2.0, 3.0),
470                        GeoCoord::from_lat_lon(3.0, 2.0),
471                        GeoCoord::from_lat_lon(2.0, 2.0),
472                    ],
473                    interiors: vec![],
474                },
475            ],
476        });
477        assert_eq!(mp.coord_count(), 7);
478    }
479
480    #[test]
481    fn coord_count_geometry_collection() {
482        let gc = Geometry::GeometryCollection(vec![sample_point(), sample_linestring()]);
483        assert_eq!(gc.coord_count(), 4); // 1 + 3
484    }
485
486    // -- PropertyValue ----------------------------------------------------
487
488    #[test]
489    fn property_value_accessors() {
490        assert!(PropertyValue::Null.is_null());
491        assert!(!PropertyValue::Bool(true).is_null());
492
493        assert_eq!(PropertyValue::Bool(true).as_bool(), Some(true));
494        assert_eq!(PropertyValue::Number(42.0).as_bool(), None);
495
496        assert_eq!(PropertyValue::Number(3.125).as_f64(), Some(3.125));
497        assert_eq!(PropertyValue::String("hi".into()).as_f64(), None);
498
499        assert_eq!(
500            PropertyValue::String("hello".into()).as_str(),
501            Some("hello")
502        );
503        assert_eq!(PropertyValue::Null.as_str(), None);
504    }
505
506    #[test]
507    fn property_value_display() {
508        assert_eq!(format!("{}", PropertyValue::Null), "null");
509        assert_eq!(format!("{}", PropertyValue::Bool(false)), "false");
510        assert_eq!(format!("{}", PropertyValue::Number(1.5)), "1.5");
511        assert_eq!(
512            format!("{}", PropertyValue::String("abc".into())),
513            "abc"
514        );
515    }
516
517    #[test]
518    fn property_value_equality() {
519        assert_eq!(PropertyValue::Null, PropertyValue::Null);
520        assert_eq!(PropertyValue::Bool(true), PropertyValue::Bool(true));
521        assert_ne!(PropertyValue::Bool(true), PropertyValue::Bool(false));
522        assert_eq!(PropertyValue::Number(1.0), PropertyValue::Number(1.0));
523        assert_ne!(PropertyValue::Number(1.0), PropertyValue::Number(2.0));
524        assert_eq!(
525            PropertyValue::String("a".into()),
526            PropertyValue::String("a".into())
527        );
528        assert_ne!(PropertyValue::Null, PropertyValue::Bool(false));
529    }
530
531    // -- Feature ----------------------------------------------------------
532
533    #[test]
534    fn feature_property_lookup() {
535        let mut props = HashMap::new();
536        props.insert("name".into(), PropertyValue::String("test".into()));
537        let feature = Feature {
538            geometry: sample_point(),
539            properties: props,
540        };
541        assert_eq!(
542            feature.property("name").and_then(|v| v.as_str()),
543            Some("test")
544        );
545        assert!(feature.property("missing").is_none());
546    }
547
548    // -- FeatureCollection ------------------------------------------------
549
550    #[test]
551    fn feature_collection_len_and_empty() {
552        let fc = FeatureCollection::default();
553        assert!(fc.is_empty());
554        assert_eq!(fc.len(), 0);
555    }
556
557    #[test]
558    fn feature_collection_total_coords() {
559        let fc = FeatureCollection {
560            features: vec![
561                Feature {
562                    geometry: sample_point(),
563                    properties: HashMap::new(),
564                },
565                Feature {
566                    geometry: sample_linestring(),
567                    properties: HashMap::new(),
568                },
569            ],
570        };
571        assert_eq!(fc.total_coords(), 4); // 1 + 3
572        assert_eq!(fc.len(), 2);
573    }
574
575    #[test]
576    fn feature_collection_iteration() {
577        let fc = FeatureCollection {
578            features: vec![
579                Feature {
580                    geometry: sample_point(),
581                    properties: HashMap::new(),
582                },
583                Feature {
584                    geometry: sample_linestring(),
585                    properties: HashMap::new(),
586                },
587            ],
588        };
589        let names: Vec<_> = fc.iter().map(|f| f.geometry.type_name()).collect();
590        assert_eq!(names, vec!["Point", "LineString"]);
591
592        // IntoIterator via &fc
593        let count = (&fc).into_iter().count();
594        assert_eq!(count, 2);
595    }
596}