Skip to main content

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