Skip to main content

oxigeo_core/vector/
feature.rs

1//! Feature types for vector data
2//!
3//! A feature is a geometry with associated properties (attributes).
4
5use crate::vector::geometry::Geometry;
6use core::fmt;
7use serde::{Deserialize, Serialize};
8#[cfg(feature = "std")]
9use serde_json::Value as JsonValue;
10
11#[cfg(feature = "std")]
12use std::collections::HashMap;
13#[cfg(feature = "std")]
14use std::string::String;
15
16#[cfg(all(not(feature = "std"), feature = "alloc"))]
17use alloc::{
18    collections::BTreeMap as HashMap,
19    string::{String, ToString},
20    vec::Vec,
21};
22
23/// A feature with geometry and properties
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct Feature {
26    /// Optional feature ID
27    pub id: Option<FeatureId>,
28    /// Geometry (may be None for attribute-only features)
29    pub geometry: Option<Geometry>,
30    /// Feature properties as key-value pairs
31    pub properties: HashMap<String, FieldValue>,
32}
33
34impl Feature {
35    /// Creates a new feature with geometry and no properties
36    #[must_use]
37    pub fn new(geometry: Geometry) -> Self {
38        Self {
39            id: None,
40            geometry: Some(geometry),
41            properties: HashMap::new(),
42        }
43    }
44
45    /// Creates a new feature with geometry and ID
46    #[must_use]
47    pub fn with_id(id: FeatureId, geometry: Geometry) -> Self {
48        Self {
49            id: Some(id),
50            geometry: Some(geometry),
51            properties: HashMap::new(),
52        }
53    }
54
55    /// Creates a new feature without geometry (attribute-only)
56    #[must_use]
57    pub fn new_attribute_only() -> Self {
58        Self {
59            id: None,
60            geometry: None,
61            properties: HashMap::new(),
62        }
63    }
64
65    /// Sets a property value
66    pub fn set_property<S: Into<String>>(&mut self, key: S, value: FieldValue) {
67        self.properties.insert(key.into(), value);
68    }
69
70    /// Gets a property value
71    #[must_use]
72    pub fn get_property(&self, key: &str) -> Option<&FieldValue> {
73        self.properties.get(key)
74    }
75
76    /// Removes a property
77    pub fn remove_property(&mut self, key: &str) -> Option<FieldValue> {
78        self.properties.remove(key)
79    }
80
81    /// Returns true if the feature has a geometry
82    #[must_use]
83    pub const fn has_geometry(&self) -> bool {
84        self.geometry.is_some()
85    }
86
87    /// Returns the bounding box of the geometry
88    #[must_use]
89    pub fn bounds(&self) -> Option<(f64, f64, f64, f64)> {
90        self.geometry
91            .as_ref()
92            .and_then(super::geometry::Geometry::bounds)
93    }
94}
95
96/// Feature ID - can be either integer or string
97#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
98#[serde(untagged)]
99pub enum FeatureId {
100    /// Integer ID
101    Integer(i64),
102    /// String ID
103    String(String),
104}
105
106impl From<i64> for FeatureId {
107    fn from(id: i64) -> Self {
108        Self::Integer(id)
109    }
110}
111
112impl From<String> for FeatureId {
113    fn from(id: String) -> Self {
114        Self::String(id)
115    }
116}
117
118impl From<&str> for FeatureId {
119    fn from(id: &str) -> Self {
120        Self::String(id.to_string())
121    }
122}
123
124/// A typed field value for feature attributes.
125///
126/// This enum covers all primitive and composite types that can appear as
127/// vector feature properties.  It replaces the former `PropertyValue` type
128/// and extends it with [`FieldValue::Date`] and [`FieldValue::Blob`] variants.
129///
130/// # Variant ordering and serde
131///
132/// The enum is tagged with `#[serde(untagged)]`, so deserialization probes
133/// variants in declaration order.  `Array` is declared before `Blob` so that
134/// JSON arrays whose elements happen to fit in `0..=255` are still decoded as
135/// `Array`, not accidentally as `Blob`.
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137#[serde(untagged)]
138pub enum FieldValue {
139    /// Null value
140    Null,
141    /// Boolean value
142    Bool(bool),
143    /// Signed integer value (i64)
144    Integer(i64),
145    /// Unsigned integer value (u64)
146    UInteger(u64),
147    /// Floating-point value (f64)
148    Float(f64),
149    /// UTF-8 string value
150    String(String),
151    /// Array of field values
152    Array(Vec<FieldValue>),
153    /// JSON object (nested key-value mapping)
154    Object(HashMap<String, FieldValue>),
155    /// Calendar date
156    ///
157    /// Only available when the `std` feature is enabled.
158    #[cfg(feature = "std")]
159    Date(time::Date),
160    /// Raw binary blob
161    Blob(Vec<u8>),
162}
163
164impl FieldValue {
165    /// Returns `true` if the value is [`FieldValue::Null`].
166    #[must_use]
167    pub const fn is_null(&self) -> bool {
168        matches!(self, Self::Null)
169    }
170
171    /// Converts to a [`serde_json::Value`].
172    ///
173    /// This is an alias for [`to_json`](Self::to_json) and produces identical
174    /// output. Prefer `to_json_value` in new code for clarity.
175    #[cfg(feature = "std")]
176    #[must_use]
177    pub fn to_json_value(&self) -> JsonValue {
178        match self {
179            Self::Null => JsonValue::Null,
180            Self::Bool(b) => JsonValue::Bool(*b),
181            Self::Integer(i) => JsonValue::Number((*i).into()),
182            Self::UInteger(u) => JsonValue::Number((*u).into()),
183            Self::Float(f) => {
184                JsonValue::Number(serde_json::Number::from_f64(*f).unwrap_or_else(|| 0.into()))
185            }
186            Self::String(s) => JsonValue::String(s.clone()),
187            Self::Array(arr) => JsonValue::Array(arr.iter().map(Self::to_json_value).collect()),
188            Self::Object(obj) => JsonValue::Object(
189                obj.iter()
190                    .map(|(k, v)| (k.clone(), v.to_json_value()))
191                    .collect(),
192            ),
193            #[cfg(feature = "std")]
194            Self::Date(d) => JsonValue::String(d.to_string()),
195            Self::Blob(bytes) => JsonValue::Array(
196                bytes
197                    .iter()
198                    .map(|b| JsonValue::Number(u64::from(*b).into()))
199                    .collect(),
200            ),
201        }
202    }
203
204    /// Converts to a [`serde_json::Value`].
205    ///
206    /// Kept for backward compatibility; delegates to [`to_json_value`](Self::to_json_value).
207    #[cfg(feature = "std")]
208    #[must_use]
209    pub fn to_json(&self) -> JsonValue {
210        self.to_json_value()
211    }
212
213    /// Creates a [`FieldValue`] from a [`serde_json::Value`].
214    ///
215    /// JSON arrays are decoded as [`FieldValue::Array`]; raw blobs must be
216    /// constructed directly.  JSON strings are decoded as
217    /// [`FieldValue::String`] without attempting date parsing.
218    #[cfg(feature = "std")]
219    #[must_use]
220    pub fn from_json(value: &JsonValue) -> Self {
221        match value {
222            JsonValue::Null => Self::Null,
223            JsonValue::Bool(b) => Self::Bool(*b),
224            JsonValue::Number(n) => {
225                if let Some(i) = n.as_i64() {
226                    Self::Integer(i)
227                } else if let Some(u) = n.as_u64() {
228                    Self::UInteger(u)
229                } else if let Some(f) = n.as_f64() {
230                    Self::Float(f)
231                } else {
232                    Self::Null
233                }
234            }
235            JsonValue::String(s) => Self::String(s.clone()),
236            JsonValue::Array(arr) => Self::Array(arr.iter().map(Self::from_json).collect()),
237            JsonValue::Object(obj) => Self::Object(
238                obj.iter()
239                    .map(|(k, v)| (k.clone(), Self::from_json(v)))
240                    .collect(),
241            ),
242        }
243    }
244
245    /// Returns the string contents if this is a [`FieldValue::String`] variant.
246    #[must_use]
247    pub fn as_string(&self) -> Option<&str> {
248        match self {
249            Self::String(s) => Some(s),
250            _ => None,
251        }
252    }
253
254    /// Returns the integer value if this is a [`FieldValue::Integer`] variant.
255    #[must_use]
256    pub const fn as_i64(&self) -> Option<i64> {
257        match self {
258            Self::Integer(i) => Some(*i),
259            _ => None,
260        }
261    }
262
263    /// Returns the unsigned integer value if this is a [`FieldValue::UInteger`] variant.
264    #[must_use]
265    pub const fn as_u64(&self) -> Option<u64> {
266        match self {
267            Self::UInteger(u) => Some(*u),
268            _ => None,
269        }
270    }
271
272    /// Returns the float value if this is a numeric variant.
273    ///
274    /// Coerces `Integer` and `UInteger` to `f64` automatically.
275    #[must_use]
276    pub fn as_f64(&self) -> Option<f64> {
277        match self {
278            Self::Float(f) => Some(*f),
279            Self::Integer(i) => Some(*i as f64),
280            Self::UInteger(u) => Some(*u as f64),
281            _ => None,
282        }
283    }
284
285    /// Returns the boolean value if this is a [`FieldValue::Bool`] variant.
286    #[must_use]
287    pub const fn as_bool(&self) -> Option<bool> {
288        match self {
289            Self::Bool(b) => Some(*b),
290            _ => None,
291        }
292    }
293
294    /// Returns the byte slice if this is a [`FieldValue::Blob`] variant.
295    #[must_use]
296    pub fn as_blob(&self) -> Option<&[u8]> {
297        match self {
298            Self::Blob(b) => Some(b),
299            _ => None,
300        }
301    }
302
303    /// Returns the [`time::Date`] if this is a [`FieldValue::Date`] variant.
304    #[cfg(feature = "std")]
305    #[must_use]
306    pub const fn as_date(&self) -> Option<time::Date> {
307        match self {
308            Self::Date(d) => Some(*d),
309            _ => None,
310        }
311    }
312}
313
314impl fmt::Display for FieldValue {
315    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316        match self {
317            Self::Null => f.write_str("null"),
318            Self::Bool(b) => write!(f, "{b}"),
319            Self::Integer(i) => write!(f, "{i}"),
320            Self::UInteger(u) => write!(f, "{u}"),
321            Self::Float(v) => write!(f, "{v}"),
322            Self::String(s) => write!(f, "\"{s}\""),
323            Self::Array(arr) => {
324                write!(f, "[")?;
325                for (i, v) in arr.iter().enumerate() {
326                    if i > 0 {
327                        write!(f, ", ")?;
328                    }
329                    write!(f, "{v}")?;
330                }
331                write!(f, "]")
332            }
333            Self::Object(obj) => {
334                write!(f, "{{")?;
335                let mut first = true;
336                for (k, v) in obj.iter() {
337                    if !first {
338                        write!(f, ", ")?;
339                    }
340                    first = false;
341                    write!(f, "\"{k}\": {v}")?;
342                }
343                write!(f, "}}")
344            }
345            #[cfg(feature = "std")]
346            Self::Date(d) => write!(f, "{d}"),
347            Self::Blob(b) => write!(f, "Blob({} bytes)", b.len()),
348        }
349    }
350}
351
352#[cfg(feature = "std")]
353impl From<serde_json::Value> for FieldValue {
354    fn from(v: serde_json::Value) -> Self {
355        Self::from_json(&v)
356    }
357}
358
359impl From<bool> for FieldValue {
360    fn from(b: bool) -> Self {
361        Self::Bool(b)
362    }
363}
364
365impl From<i64> for FieldValue {
366    fn from(i: i64) -> Self {
367        Self::Integer(i)
368    }
369}
370
371impl From<i32> for FieldValue {
372    fn from(i: i32) -> Self {
373        Self::Integer(i64::from(i))
374    }
375}
376
377impl From<u64> for FieldValue {
378    fn from(u: u64) -> Self {
379        Self::UInteger(u)
380    }
381}
382
383impl From<u32> for FieldValue {
384    fn from(u: u32) -> Self {
385        Self::UInteger(u64::from(u))
386    }
387}
388
389impl From<f64> for FieldValue {
390    fn from(f: f64) -> Self {
391        Self::Float(f)
392    }
393}
394
395impl From<f32> for FieldValue {
396    fn from(f: f32) -> Self {
397        Self::Float(f64::from(f))
398    }
399}
400
401impl From<String> for FieldValue {
402    fn from(s: String) -> Self {
403        Self::String(s)
404    }
405}
406
407impl From<&str> for FieldValue {
408    fn from(s: &str) -> Self {
409        Self::String(s.to_string())
410    }
411}
412
413/// A collection of features
414#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
415pub struct FeatureCollection {
416    /// Features in the collection
417    pub features: Vec<Feature>,
418    /// Optional metadata
419    #[serde(skip_serializing_if = "Option::is_none")]
420    pub metadata: Option<HashMap<String, FieldValue>>,
421}
422
423impl FeatureCollection {
424    /// Creates a new feature collection
425    #[must_use]
426    pub const fn new(features: Vec<Feature>) -> Self {
427        Self {
428            features,
429            metadata: None,
430        }
431    }
432
433    /// Creates an empty feature collection
434    #[must_use]
435    pub const fn empty() -> Self {
436        Self {
437            features: Vec::new(),
438            metadata: None,
439        }
440    }
441
442    /// Adds a feature to the collection
443    pub fn push(&mut self, feature: Feature) {
444        self.features.push(feature);
445    }
446
447    /// Returns the number of features
448    #[must_use]
449    pub fn len(&self) -> usize {
450        self.features.len()
451    }
452
453    /// Returns true if the collection is empty
454    #[must_use]
455    pub fn is_empty(&self) -> bool {
456        self.features.is_empty()
457    }
458
459    /// Computes the bounding box of all features
460    #[must_use]
461    pub fn bounds(&self) -> Option<(f64, f64, f64, f64)> {
462        if self.features.is_empty() {
463            return None;
464        }
465
466        let mut min_x = f64::INFINITY;
467        let mut min_y = f64::INFINITY;
468        let mut max_x = f64::NEG_INFINITY;
469        let mut max_y = f64::NEG_INFINITY;
470
471        for feature in &self.features {
472            if let Some((x_min, y_min, x_max, y_max)) = feature.bounds() {
473                min_x = min_x.min(x_min);
474                min_y = min_y.min(y_min);
475                max_x = max_x.max(x_max);
476                max_y = max_y.max(y_max);
477            }
478        }
479
480        if min_x.is_infinite() {
481            None
482        } else {
483            Some((min_x, min_y, max_x, max_y))
484        }
485    }
486}
487
488impl Default for FeatureCollection {
489    fn default() -> Self {
490        Self::empty()
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    #![allow(clippy::expect_used)]
497
498    use super::*;
499    use crate::vector::geometry::Point;
500
501    #[test]
502    fn test_feature_creation() {
503        let point = Point::new(1.0, 2.0);
504        let mut feature = Feature::new(Geometry::Point(point));
505
506        feature.set_property("name", FieldValue::String("Test Point".to_string()));
507        feature.set_property("value", FieldValue::Integer(42));
508
509        assert!(feature.has_geometry());
510        assert_eq!(feature.properties.len(), 2);
511
512        let name = feature.get_property("name");
513        assert!(name.is_some());
514        assert_eq!(name.and_then(|v| v.as_string()), Some("Test Point"));
515
516        let value = feature.get_property("value");
517        assert!(value.is_some());
518        assert_eq!(value.and_then(|v| v.as_i64()), Some(42));
519    }
520
521    #[test]
522    fn test_feature_id() {
523        let point = Point::new(1.0, 2.0);
524        let feature = Feature::with_id(FeatureId::Integer(123), Geometry::Point(point));
525
526        assert_eq!(feature.id, Some(FeatureId::Integer(123)));
527    }
528
529    #[test]
530    fn test_field_value_conversions() {
531        let pv_int = FieldValue::from(42_i64);
532        assert_eq!(pv_int.as_i64(), Some(42));
533
534        let pv_float = FieldValue::from(2.78_f64);
535        assert_eq!(pv_float.as_f64(), Some(2.78));
536
537        let pv_bool = FieldValue::from(true);
538        assert_eq!(pv_bool.as_bool(), Some(true));
539
540        let pv_str = FieldValue::from("hello");
541        assert_eq!(pv_str.as_string(), Some("hello"));
542    }
543
544    #[test]
545    fn test_feature_collection() {
546        let mut collection = FeatureCollection::empty();
547        assert!(collection.is_empty());
548
549        let point1 = Point::new(1.0, 2.0);
550        let feature1 = Feature::new(Geometry::Point(point1));
551        collection.push(feature1);
552
553        let point2 = Point::new(3.0, 4.0);
554        let feature2 = Feature::new(Geometry::Point(point2));
555        collection.push(feature2);
556
557        assert_eq!(collection.len(), 2);
558        assert!(!collection.is_empty());
559
560        let bounds = collection.bounds();
561        assert!(bounds.is_some());
562        let (min_x, min_y, max_x, max_y) = bounds.expect("bounds calculation failed");
563        assert_eq!(min_x, 1.0);
564        assert_eq!(min_y, 2.0);
565        assert_eq!(max_x, 3.0);
566        assert_eq!(max_y, 4.0);
567    }
568
569    #[test]
570    fn test_fieldvalue_variants_exhaustive() {
571        let _ = FieldValue::Null;
572        let _ = FieldValue::Bool(true);
573        let _ = FieldValue::Integer(-1);
574        let _ = FieldValue::UInteger(1u64);
575        let _ = FieldValue::Float(1.5);
576        #[cfg(feature = "std")]
577        let _ = FieldValue::Date(
578            time::Date::from_calendar_date(2024, time::Month::January, 1).expect("valid date"),
579        );
580        let _ = FieldValue::Blob(vec![0u8]);
581        let _ = FieldValue::String("x".into());
582        let _ = FieldValue::Array(vec![]);
583        let _ = FieldValue::Object(Default::default());
584    }
585
586    #[test]
587    #[cfg(feature = "std")]
588    fn test_fieldvalue_to_json_value_all_variants() {
589        assert_eq!(FieldValue::Null.to_json_value(), serde_json::Value::Null);
590        assert_eq!(
591            FieldValue::Bool(true).to_json_value(),
592            serde_json::Value::Bool(true)
593        );
594        let blob_json = FieldValue::Blob(vec![1, 2]).to_json_value();
595        assert!(matches!(blob_json, serde_json::Value::Array(_)));
596        if let serde_json::Value::Array(a) = blob_json {
597            assert_eq!(a.len(), 2);
598        }
599        // Date variant
600        let date =
601            time::Date::from_calendar_date(2024, time::Month::March, 15).expect("valid date");
602        let json = FieldValue::Date(date).to_json_value();
603        assert!(matches!(json, serde_json::Value::String(_)));
604        if let serde_json::Value::String(s) = json {
605            assert!(s.contains("2024"), "date string should contain year");
606        }
607    }
608
609    #[test]
610    #[cfg(feature = "std")]
611    fn test_from_json_value() {
612        let json = serde_json::json!({
613            "name": "test",
614            "count": 42,
615            "flag": true,
616            "score": 3.5,
617            "items": [1, 2, 3]
618        });
619        let fv = FieldValue::from(json);
620        assert!(matches!(fv, FieldValue::Object(_)));
621        if let FieldValue::Object(map) = fv {
622            assert!(map.contains_key("name"));
623            assert!(map.contains_key("count"));
624        }
625    }
626
627    #[test]
628    #[cfg(feature = "std")]
629    fn test_blob_accessor() {
630        let fv = FieldValue::Blob(vec![0xDE, 0xAD, 0xBE, 0xEF]);
631        assert_eq!(fv.as_blob(), Some([0xDE, 0xAD, 0xBE, 0xEFu8].as_ref()));
632    }
633
634    #[test]
635    fn test_display_fieldvalue_each_variant() {
636        assert_eq!(FieldValue::Null.to_string(), "null");
637        assert_eq!(FieldValue::Bool(true).to_string(), "true");
638        assert_eq!(FieldValue::Bool(false).to_string(), "false");
639        assert_eq!(FieldValue::Integer(-42).to_string(), "-42");
640        assert_eq!(FieldValue::UInteger(99).to_string(), "99");
641        assert_eq!(FieldValue::String("hello".into()).to_string(), "\"hello\"");
642        assert_eq!(FieldValue::Blob(vec![1, 2, 3]).to_string(), "Blob(3 bytes)");
643        assert_eq!(
644            FieldValue::Array(vec![FieldValue::Integer(1)]).to_string(),
645            "[1]"
646        );
647    }
648}