Skip to main content

oxigdal_geojson/types/
feature.rs

1//! GeoJSON Feature and FeatureCollection types
2//!
3//! This module implements Feature and FeatureCollection types according to RFC 7946.
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::error::{GeoJsonError, Result};
9use crate::types::{BBox, Crs, ForeignMembers, Geometry};
10
11/// Feature ID (can be string or number)
12///
13/// Custom `Deserialize` implementation is used instead of relying on the derived
14/// `#[serde(untagged)]` deserializer to handle `serde_json/arbitrary_precision`
15/// correctly when activated by workspace dependencies (e.g. `bigdecimal`). With
16/// `arbitrary_precision`, numeric values are stored internally as `Content::Map`
17/// rather than `Content::I64`, which breaks the generated untagged deserialization
18/// code. Deserializing first into `serde_json::Value` sidesteps the issue because
19/// `serde_json` handles its own `arbitrary_precision` feature correctly.
20///
21/// `#[serde(untagged)]` is kept for the `Serialize` derive so that string IDs are
22/// written as `"feature-1"` rather than `{"String":"feature-1"}`.
23#[derive(Debug, Clone, PartialEq, Serialize)]
24#[serde(untagged)]
25pub enum FeatureId {
26    /// String ID
27    String(String),
28    /// Numeric ID
29    Number(i64),
30}
31
32impl<'de> Deserialize<'de> for FeatureId {
33    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
34    where
35        D: serde::Deserializer<'de>,
36    {
37        use serde::de::Error as _;
38        let value = serde_json::Value::deserialize(deserializer)?;
39        match &value {
40            serde_json::Value::String(s) => Ok(FeatureId::String(s.clone())),
41            serde_json::Value::Number(n) => n
42                .as_i64()
43                .map(FeatureId::Number)
44                .ok_or_else(|| D::Error::custom("FeatureId number must be representable as i64")),
45            _ => Err(D::Error::custom("FeatureId must be a string or number")),
46        }
47    }
48}
49
50impl FeatureId {
51    /// Creates a new string ID
52    pub fn string<S: Into<String>>(s: S) -> Self {
53        Self::String(s.into())
54    }
55
56    /// Creates a new numeric ID
57    pub const fn number(n: i64) -> Self {
58        Self::Number(n)
59    }
60
61    /// Returns the ID as a string
62    #[must_use]
63    pub fn as_string(&self) -> String {
64        match self {
65            Self::String(s) => s.clone(),
66            Self::Number(n) => n.to_string(),
67        }
68    }
69}
70
71impl From<String> for FeatureId {
72    fn from(s: String) -> Self {
73        Self::String(s)
74    }
75}
76
77impl From<&str> for FeatureId {
78    fn from(s: &str) -> Self {
79        Self::String(s.to_string())
80    }
81}
82
83impl From<i64> for FeatureId {
84    fn from(n: i64) -> Self {
85        Self::Number(n)
86    }
87}
88
89impl From<i32> for FeatureId {
90    fn from(n: i32) -> Self {
91        Self::Number(i64::from(n))
92    }
93}
94
95impl From<u32> for FeatureId {
96    fn from(n: u32) -> Self {
97        Self::Number(i64::from(n))
98    }
99}
100
101/// Feature properties (JSON object)
102pub type Properties = serde_json::Map<String, Value>;
103
104/// GeoJSON Feature
105///
106/// A Feature object represents a spatially bounded entity, associating
107/// a Geometry with properties.
108#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
109pub struct Feature {
110    /// Type discriminator (always "Feature")
111    #[serde(rename = "type")]
112    pub feature_type: String,
113
114    /// Optional feature ID
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub id: Option<FeatureId>,
117
118    /// Optional geometry (can be null)
119    pub geometry: Option<Geometry>,
120
121    /// Feature properties (can be null)
122    pub properties: Option<Properties>,
123
124    /// Optional bounding box
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub bbox: Option<BBox>,
127
128    /// Optional CRS (deprecated in RFC 7946 but still supported)
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub crs: Option<Crs>,
131
132    /// Foreign members (additional properties)
133    #[serde(flatten)]
134    pub foreign_members: Option<ForeignMembers>,
135}
136
137impl Feature {
138    /// Creates a new Feature with geometry and properties
139    pub fn new(geometry: Option<Geometry>, properties: Option<Properties>) -> Self {
140        Self {
141            feature_type: "Feature".to_string(),
142            id: None,
143            geometry,
144            properties,
145            bbox: None,
146            crs: None,
147            foreign_members: None,
148        }
149    }
150
151    /// Creates a new Feature with ID, geometry, and properties
152    pub fn with_id<I: Into<FeatureId>>(
153        id: I,
154        geometry: Option<Geometry>,
155        properties: Option<Properties>,
156    ) -> Self {
157        Self {
158            feature_type: "Feature".to_string(),
159            id: Some(id.into()),
160            geometry,
161            properties,
162            bbox: None,
163            crs: None,
164            foreign_members: None,
165        }
166    }
167
168    /// Sets the feature ID
169    pub fn set_id<I: Into<FeatureId>>(&mut self, id: I) {
170        self.id = Some(id.into());
171    }
172
173    /// Sets the geometry
174    pub fn set_geometry(&mut self, geometry: Geometry) {
175        self.geometry = Some(geometry);
176    }
177
178    /// Sets the properties
179    pub fn set_properties(&mut self, properties: Properties) {
180        self.properties = Some(properties);
181    }
182
183    /// Adds a property
184    pub fn add_property<K: Into<String>, V: Into<Value>>(&mut self, key: K, value: V) {
185        let props = self.properties.get_or_insert_with(Properties::new);
186        props.insert(key.into(), value.into());
187    }
188
189    /// Gets a property value
190    #[must_use]
191    pub fn get_property(&self, key: &str) -> Option<&Value> {
192        self.properties.as_ref().and_then(|p| p.get(key))
193    }
194
195    /// Sets the bounding box
196    pub fn set_bbox(&mut self, bbox: BBox) {
197        self.bbox = Some(bbox);
198    }
199
200    /// Computes and sets the bounding box from geometry
201    pub fn compute_bbox(&mut self) {
202        if let Some(ref geometry) = self.geometry {
203            self.bbox = geometry.compute_bbox();
204        }
205    }
206
207    /// Sets the CRS
208    pub fn set_crs(&mut self, crs: Crs) {
209        self.crs = Some(crs);
210    }
211
212    /// Validates the feature
213    pub fn validate(&self) -> Result<()> {
214        if self.feature_type != "Feature" {
215            return Err(GeoJsonError::InvalidFeature {
216                message: format!(
217                    "Invalid type: expected 'Feature', got '{}'",
218                    self.feature_type
219                ),
220                feature_id: self.id.as_ref().map(|id| id.as_string()),
221            });
222        }
223
224        if let Some(ref geometry) = self.geometry {
225            geometry
226                .validate()
227                .map_err(|e| GeoJsonError::InvalidFeature {
228                    message: format!("Invalid geometry: {e}"),
229                    feature_id: self.id.as_ref().map(|id| id.as_string()),
230                })?;
231        }
232
233        if let Some(ref bbox) = self.bbox {
234            crate::types::geometry::validate_bbox(bbox)?;
235        }
236
237        Ok(())
238    }
239
240    /// Returns true if the feature has a geometry
241    #[must_use]
242    pub const fn has_geometry(&self) -> bool {
243        self.geometry.is_some()
244    }
245
246    /// Returns true if the feature has properties
247    #[must_use]
248    pub const fn has_properties(&self) -> bool {
249        self.properties.is_some()
250    }
251
252    /// Returns the number of properties
253    #[must_use]
254    pub fn property_count(&self) -> usize {
255        self.properties.as_ref().map_or(0, |p| p.len())
256    }
257}
258
259impl Default for Feature {
260    fn default() -> Self {
261        Self::new(None, None)
262    }
263}
264
265/// GeoJSON FeatureCollection
266///
267/// A FeatureCollection is a collection of Feature objects.
268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
269pub struct FeatureCollection {
270    /// Type discriminator (always "FeatureCollection")
271    #[serde(rename = "type")]
272    pub collection_type: String,
273
274    /// The features in the collection
275    pub features: Vec<Feature>,
276
277    /// Optional bounding box
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub bbox: Option<BBox>,
280
281    /// Optional CRS (deprecated in RFC 7946 but still supported)
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub crs: Option<Crs>,
284
285    /// Foreign members (additional properties)
286    #[serde(flatten)]
287    pub foreign_members: Option<ForeignMembers>,
288}
289
290impl FeatureCollection {
291    /// Creates a new FeatureCollection
292    pub fn new(features: Vec<Feature>) -> Self {
293        Self {
294            collection_type: "FeatureCollection".to_string(),
295            features,
296            bbox: None,
297            crs: None,
298            foreign_members: None,
299        }
300    }
301
302    /// Creates an empty FeatureCollection
303    pub fn empty() -> Self {
304        Self::new(Vec::new())
305    }
306
307    /// Creates a FeatureCollection with capacity
308    pub fn with_capacity(capacity: usize) -> Self {
309        Self {
310            collection_type: "FeatureCollection".to_string(),
311            features: Vec::with_capacity(capacity),
312            bbox: None,
313            crs: None,
314            foreign_members: None,
315        }
316    }
317
318    /// Adds a feature to the collection
319    pub fn add_feature(&mut self, feature: Feature) {
320        self.features.push(feature);
321    }
322
323    /// Adds multiple features to the collection
324    pub fn add_features(&mut self, features: Vec<Feature>) {
325        self.features.extend(features);
326    }
327
328    /// Sets the bounding box
329    pub fn set_bbox(&mut self, bbox: BBox) {
330        self.bbox = Some(bbox);
331    }
332
333    /// Computes and sets the bounding box from all features
334    pub fn compute_bbox(&mut self) {
335        if self.features.is_empty() {
336            self.bbox = None;
337            return;
338        }
339
340        let mut min_x = f64::INFINITY;
341        let mut min_y = f64::INFINITY;
342        let mut max_x = f64::NEG_INFINITY;
343        let mut max_y = f64::NEG_INFINITY;
344
345        for feature in &self.features {
346            if let Some(ref geometry) = feature.geometry {
347                if let Some(bbox) = geometry.compute_bbox() {
348                    if bbox.len() >= 4 {
349                        min_x = min_x.min(bbox[0]);
350                        min_y = min_y.min(bbox[1]);
351                        max_x = max_x.max(bbox[2]);
352                        max_y = max_y.max(bbox[3]);
353                    }
354                }
355            }
356        }
357
358        if min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite() {
359            self.bbox = Some(vec![min_x, min_y, max_x, max_y]);
360        }
361    }
362
363    /// Sets the CRS for the entire collection
364    pub fn set_crs(&mut self, crs: Crs) {
365        self.crs = Some(crs);
366    }
367
368    /// Validates the feature collection
369    pub fn validate(&self) -> Result<()> {
370        if self.collection_type != "FeatureCollection" {
371            return Err(GeoJsonError::InvalidFeatureCollection {
372                message: format!(
373                    "Invalid type: expected 'FeatureCollection', got '{}'",
374                    self.collection_type
375                ),
376            });
377        }
378
379        for (i, feature) in self.features.iter().enumerate() {
380            feature
381                .validate()
382                .map_err(|e| GeoJsonError::validation_at(e.to_string(), format!("features/{i}")))?;
383        }
384
385        if let Some(ref bbox) = self.bbox {
386            crate::types::geometry::validate_bbox(bbox)?;
387        }
388
389        Ok(())
390    }
391
392    /// Returns the number of features
393    #[must_use]
394    pub fn len(&self) -> usize {
395        self.features.len()
396    }
397
398    /// Returns true if the collection is empty
399    #[must_use]
400    pub fn is_empty(&self) -> bool {
401        self.features.is_empty()
402    }
403
404    /// Returns an iterator over the features
405    pub fn iter(&self) -> impl Iterator<Item = &Feature> {
406        self.features.iter()
407    }
408
409    /// Returns a mutable iterator over the features
410    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Feature> {
411        self.features.iter_mut()
412    }
413
414    /// Filters features by a predicate
415    pub fn filter<F>(&self, predicate: F) -> Self
416    where
417        F: Fn(&Feature) -> bool,
418    {
419        Self::new(
420            self.features
421                .iter()
422                .filter(|f| predicate(f))
423                .cloned()
424                .collect(),
425        )
426    }
427
428    /// Returns features with a specific property value
429    pub fn with_property(&self, key: &str, value: &Value) -> Self {
430        self.filter(|f| f.properties.as_ref().and_then(|p| p.get(key)) == Some(value))
431    }
432
433    /// Removes all features
434    pub fn clear(&mut self) {
435        self.features.clear();
436        self.bbox = None;
437    }
438
439    /// Retains only the features that satisfy the predicate
440    pub fn retain<F>(&mut self, predicate: F)
441    where
442        F: FnMut(&Feature) -> bool,
443    {
444        self.features.retain(predicate);
445    }
446}
447
448impl Default for FeatureCollection {
449    fn default() -> Self {
450        Self::empty()
451    }
452}
453
454impl IntoIterator for FeatureCollection {
455    type Item = Feature;
456    type IntoIter = std::vec::IntoIter<Feature>;
457
458    fn into_iter(self) -> Self::IntoIter {
459        self.features.into_iter()
460    }
461}
462
463impl<'a> IntoIterator for &'a FeatureCollection {
464    type Item = &'a Feature;
465    type IntoIter = std::slice::Iter<'a, Feature>;
466
467    fn into_iter(self) -> Self::IntoIter {
468        self.features.iter()
469    }
470}
471
472impl<'a> IntoIterator for &'a mut FeatureCollection {
473    type Item = &'a mut Feature;
474    type IntoIter = std::slice::IterMut<'a, Feature>;
475
476    fn into_iter(self) -> Self::IntoIter {
477        self.features.iter_mut()
478    }
479}
480
481impl FromIterator<Feature> for FeatureCollection {
482    fn from_iter<T: IntoIterator<Item = Feature>>(iter: T) -> Self {
483        Self::new(iter.into_iter().collect())
484    }
485}
486
487#[cfg(test)]
488#[allow(clippy::panic)]
489mod tests {
490    use super::*;
491    use crate::types::geometry::Point;
492
493    #[test]
494    fn test_feature_id() {
495        let string_id = FeatureId::string("feature-1");
496        assert_eq!(string_id.as_string(), "feature-1");
497
498        let num_id = FeatureId::number(42);
499        assert_eq!(num_id.as_string(), "42");
500    }
501
502    #[test]
503    fn test_feature_creation() {
504        let point = Point::new_2d(-122.4, 37.8).expect("valid point");
505        let geometry = Geometry::Point(point);
506
507        let mut props = Properties::new();
508        props.insert(
509            "name".to_string(),
510            Value::String("San Francisco".to_string()),
511        );
512
513        let feature = Feature::new(Some(geometry), Some(props));
514        assert!(feature.has_geometry());
515        assert!(feature.has_properties());
516        assert_eq!(feature.property_count(), 1);
517    }
518
519    #[test]
520    fn test_feature_with_id() {
521        let point = Point::new_2d(0.0, 0.0).expect("valid point");
522        let geometry = Geometry::Point(point);
523
524        let feature = Feature::with_id("test-id", Some(geometry), None);
525        assert!(feature.id.is_some());
526        if let Some(FeatureId::String(id)) = &feature.id {
527            assert_eq!(id, "test-id");
528        } else {
529            panic!("Expected string ID");
530        }
531    }
532
533    #[test]
534    fn test_feature_properties() {
535        let mut feature = Feature::default();
536        feature.add_property("name", "Test");
537        feature.add_property("count", 42);
538
539        assert_eq!(feature.property_count(), 2);
540        assert!(feature.get_property("name").is_some());
541    }
542
543    #[test]
544    fn test_feature_validation() {
545        let point = Point::new_2d(-122.4, 37.8).expect("valid point");
546        let geometry = Geometry::Point(point);
547        let feature = Feature::new(Some(geometry), None);
548
549        assert!(feature.validate().is_ok());
550    }
551
552    #[test]
553    fn test_feature_collection_creation() {
554        let fc = FeatureCollection::empty();
555        assert!(fc.is_empty());
556        assert_eq!(fc.len(), 0);
557    }
558
559    #[test]
560    fn test_feature_collection_add() {
561        let mut fc = FeatureCollection::empty();
562
563        let point = Point::new_2d(0.0, 0.0).expect("valid point");
564        let geometry = Geometry::Point(point);
565        let feature = Feature::new(Some(geometry), None);
566
567        fc.add_feature(feature);
568        assert_eq!(fc.len(), 1);
569        assert!(!fc.is_empty());
570    }
571
572    #[test]
573    fn test_feature_collection_compute_bbox() {
574        let mut fc = FeatureCollection::empty();
575
576        let p1 = Point::new_2d(0.0, 0.0).expect("valid point");
577        let p2 = Point::new_2d(10.0, 10.0).expect("valid point");
578
579        fc.add_feature(Feature::new(Some(Geometry::Point(p1)), None));
580        fc.add_feature(Feature::new(Some(Geometry::Point(p2)), None));
581
582        fc.compute_bbox();
583        assert!(fc.bbox.is_some());
584
585        if let Some(bbox) = &fc.bbox {
586            assert_eq!(bbox[0], 0.0);
587            assert_eq!(bbox[1], 0.0);
588            assert_eq!(bbox[2], 10.0);
589            assert_eq!(bbox[3], 10.0);
590        }
591    }
592
593    #[test]
594    fn test_feature_collection_filter() {
595        let mut fc = FeatureCollection::empty();
596
597        for i in 0..5 {
598            let point = Point::new_2d(f64::from(i), f64::from(i)).expect("valid point");
599            let mut feature = Feature::new(Some(Geometry::Point(point)), None);
600            feature.add_property("id", i);
601            fc.add_feature(feature);
602        }
603
604        let filtered = fc.with_property("id", &Value::Number(serde_json::Number::from(2)));
605        assert_eq!(filtered.len(), 1);
606    }
607
608    #[test]
609    fn test_feature_collection_iterator() {
610        let mut fc = FeatureCollection::with_capacity(3);
611
612        for i in 0..3 {
613            let point = Point::new_2d(f64::from(i), f64::from(i)).expect("valid point");
614            fc.add_feature(Feature::new(Some(Geometry::Point(point)), None));
615        }
616
617        let count = fc.iter().count();
618        assert_eq!(count, 3);
619    }
620
621    #[test]
622    fn test_feature_collection_validation() {
623        let mut fc = FeatureCollection::empty();
624
625        let point = Point::new_2d(0.0, 0.0).expect("valid point");
626        let feature = Feature::new(Some(Geometry::Point(point)), None);
627        fc.add_feature(feature);
628
629        assert!(fc.validate().is_ok());
630    }
631}