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 serde::{Deserialize, Serialize};
7use serde_json::Value as JsonValue;
8
9#[cfg(feature = "std")]
10use std::collections::HashMap;
11#[cfg(feature = "std")]
12use std::string::String;
13
14#[cfg(all(not(feature = "std"), feature = "alloc"))]
15use alloc::{
16    collections::BTreeMap as HashMap,
17    string::{String, ToString},
18};
19
20/// A feature with geometry and properties
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct Feature {
23    /// Optional feature ID
24    pub id: Option<FeatureId>,
25    /// Geometry (may be None for attribute-only features)
26    pub geometry: Option<Geometry>,
27    /// Feature properties as key-value pairs
28    pub properties: HashMap<String, PropertyValue>,
29}
30
31impl Feature {
32    /// Creates a new feature with geometry and no properties
33    #[must_use]
34    pub fn new(geometry: Geometry) -> Self {
35        Self {
36            id: None,
37            geometry: Some(geometry),
38            properties: HashMap::new(),
39        }
40    }
41
42    /// Creates a new feature with geometry and ID
43    #[must_use]
44    pub fn with_id(id: FeatureId, geometry: Geometry) -> Self {
45        Self {
46            id: Some(id),
47            geometry: Some(geometry),
48            properties: HashMap::new(),
49        }
50    }
51
52    /// Creates a new feature without geometry (attribute-only)
53    #[must_use]
54    pub fn new_attribute_only() -> Self {
55        Self {
56            id: None,
57            geometry: None,
58            properties: HashMap::new(),
59        }
60    }
61
62    /// Sets a property value
63    pub fn set_property<S: Into<String>>(&mut self, key: S, value: PropertyValue) {
64        self.properties.insert(key.into(), value);
65    }
66
67    /// Gets a property value
68    #[must_use]
69    pub fn get_property(&self, key: &str) -> Option<&PropertyValue> {
70        self.properties.get(key)
71    }
72
73    /// Removes a property
74    pub fn remove_property(&mut self, key: &str) -> Option<PropertyValue> {
75        self.properties.remove(key)
76    }
77
78    /// Returns true if the feature has a geometry
79    #[must_use]
80    pub const fn has_geometry(&self) -> bool {
81        self.geometry.is_some()
82    }
83
84    /// Returns the bounding box of the geometry
85    #[must_use]
86    pub fn bounds(&self) -> Option<(f64, f64, f64, f64)> {
87        self.geometry
88            .as_ref()
89            .and_then(super::geometry::Geometry::bounds)
90    }
91}
92
93/// Feature ID - can be either integer or string
94#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
95#[serde(untagged)]
96pub enum FeatureId {
97    /// Integer ID
98    Integer(i64),
99    /// String ID
100    String(String),
101}
102
103impl From<i64> for FeatureId {
104    fn from(id: i64) -> Self {
105        Self::Integer(id)
106    }
107}
108
109impl From<String> for FeatureId {
110    fn from(id: String) -> Self {
111        Self::String(id)
112    }
113}
114
115impl From<&str> for FeatureId {
116    fn from(id: &str) -> Self {
117        Self::String(id.to_string())
118    }
119}
120
121/// Property value type
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
123#[serde(untagged)]
124pub enum PropertyValue {
125    /// Null value
126    Null,
127    /// Boolean value
128    Bool(bool),
129    /// Integer value (i64)
130    Integer(i64),
131    /// Unsigned integer value
132    UInteger(u64),
133    /// Float value (f64)
134    Float(f64),
135    /// String value
136    String(String),
137    /// Array of values
138    Array(Vec<PropertyValue>),
139    /// JSON object
140    Object(HashMap<String, PropertyValue>),
141}
142
143impl PropertyValue {
144    /// Returns true if the value is null
145    #[must_use]
146    pub const fn is_null(&self) -> bool {
147        matches!(self, Self::Null)
148    }
149
150    /// Converts to JSON Value
151    #[cfg(feature = "std")]
152    #[must_use]
153    pub fn to_json(&self) -> JsonValue {
154        match self {
155            Self::Null => JsonValue::Null,
156            Self::Bool(b) => JsonValue::Bool(*b),
157            Self::Integer(i) => JsonValue::Number((*i).into()),
158            Self::UInteger(u) => JsonValue::Number((*u).into()),
159            Self::Float(f) => {
160                JsonValue::Number(serde_json::Number::from_f64(*f).unwrap_or_else(|| 0.into()))
161            }
162            Self::String(s) => JsonValue::String(s.clone()),
163            Self::Array(arr) => JsonValue::Array(arr.iter().map(PropertyValue::to_json).collect()),
164            Self::Object(obj) => {
165                JsonValue::Object(obj.iter().map(|(k, v)| (k.clone(), v.to_json())).collect())
166            }
167        }
168    }
169
170    /// Creates from JSON Value
171    #[cfg(feature = "std")]
172    #[must_use]
173    pub fn from_json(value: &JsonValue) -> Self {
174        match value {
175            JsonValue::Null => Self::Null,
176            JsonValue::Bool(b) => Self::Bool(*b),
177            JsonValue::Number(n) => {
178                if let Some(i) = n.as_i64() {
179                    Self::Integer(i)
180                } else if let Some(u) = n.as_u64() {
181                    Self::UInteger(u)
182                } else if let Some(f) = n.as_f64() {
183                    Self::Float(f)
184                } else {
185                    Self::Null
186                }
187            }
188            JsonValue::String(s) => Self::String(s.clone()),
189            JsonValue::Array(arr) => Self::Array(arr.iter().map(Self::from_json).collect()),
190            JsonValue::Object(obj) => Self::Object(
191                obj.iter()
192                    .map(|(k, v)| (k.clone(), Self::from_json(v)))
193                    .collect(),
194            ),
195        }
196    }
197
198    /// Tries to convert to string
199    #[must_use]
200    pub fn as_string(&self) -> Option<&str> {
201        match self {
202            Self::String(s) => Some(s),
203            _ => None,
204        }
205    }
206
207    /// Tries to convert to i64
208    #[must_use]
209    pub const fn as_i64(&self) -> Option<i64> {
210        match self {
211            Self::Integer(i) => Some(*i),
212            _ => None,
213        }
214    }
215
216    /// Tries to convert to u64
217    #[must_use]
218    pub const fn as_u64(&self) -> Option<u64> {
219        match self {
220            Self::UInteger(u) => Some(*u),
221            _ => None,
222        }
223    }
224
225    /// Tries to convert to f64
226    #[must_use]
227    pub fn as_f64(&self) -> Option<f64> {
228        match self {
229            Self::Float(f) => Some(*f),
230            Self::Integer(i) => Some(*i as f64),
231            Self::UInteger(u) => Some(*u as f64),
232            _ => None,
233        }
234    }
235
236    /// Tries to convert to bool
237    #[must_use]
238    pub const fn as_bool(&self) -> Option<bool> {
239        match self {
240            Self::Bool(b) => Some(*b),
241            _ => None,
242        }
243    }
244}
245
246impl From<bool> for PropertyValue {
247    fn from(b: bool) -> Self {
248        Self::Bool(b)
249    }
250}
251
252impl From<i64> for PropertyValue {
253    fn from(i: i64) -> Self {
254        Self::Integer(i)
255    }
256}
257
258impl From<i32> for PropertyValue {
259    fn from(i: i32) -> Self {
260        Self::Integer(i64::from(i))
261    }
262}
263
264impl From<u64> for PropertyValue {
265    fn from(u: u64) -> Self {
266        Self::UInteger(u)
267    }
268}
269
270impl From<u32> for PropertyValue {
271    fn from(u: u32) -> Self {
272        Self::UInteger(u64::from(u))
273    }
274}
275
276impl From<f64> for PropertyValue {
277    fn from(f: f64) -> Self {
278        Self::Float(f)
279    }
280}
281
282impl From<f32> for PropertyValue {
283    fn from(f: f32) -> Self {
284        Self::Float(f64::from(f))
285    }
286}
287
288impl From<String> for PropertyValue {
289    fn from(s: String) -> Self {
290        Self::String(s)
291    }
292}
293
294impl From<&str> for PropertyValue {
295    fn from(s: &str) -> Self {
296        Self::String(s.to_string())
297    }
298}
299
300/// A collection of features
301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
302pub struct FeatureCollection {
303    /// Features in the collection
304    pub features: Vec<Feature>,
305    /// Optional metadata
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub metadata: Option<HashMap<String, PropertyValue>>,
308}
309
310impl FeatureCollection {
311    /// Creates a new feature collection
312    #[must_use]
313    pub const fn new(features: Vec<Feature>) -> Self {
314        Self {
315            features,
316            metadata: None,
317        }
318    }
319
320    /// Creates an empty feature collection
321    #[must_use]
322    pub const fn empty() -> Self {
323        Self {
324            features: Vec::new(),
325            metadata: None,
326        }
327    }
328
329    /// Adds a feature to the collection
330    pub fn push(&mut self, feature: Feature) {
331        self.features.push(feature);
332    }
333
334    /// Returns the number of features
335    #[must_use]
336    pub fn len(&self) -> usize {
337        self.features.len()
338    }
339
340    /// Returns true if the collection is empty
341    #[must_use]
342    pub fn is_empty(&self) -> bool {
343        self.features.is_empty()
344    }
345
346    /// Computes the bounding box of all features
347    #[must_use]
348    pub fn bounds(&self) -> Option<(f64, f64, f64, f64)> {
349        if self.features.is_empty() {
350            return None;
351        }
352
353        let mut min_x = f64::INFINITY;
354        let mut min_y = f64::INFINITY;
355        let mut max_x = f64::NEG_INFINITY;
356        let mut max_y = f64::NEG_INFINITY;
357
358        for feature in &self.features {
359            if let Some((x_min, y_min, x_max, y_max)) = feature.bounds() {
360                min_x = min_x.min(x_min);
361                min_y = min_y.min(y_min);
362                max_x = max_x.max(x_max);
363                max_y = max_y.max(y_max);
364            }
365        }
366
367        if min_x.is_infinite() {
368            None
369        } else {
370            Some((min_x, min_y, max_x, max_y))
371        }
372    }
373}
374
375impl Default for FeatureCollection {
376    fn default() -> Self {
377        Self::empty()
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384    use crate::vector::geometry::Point;
385
386    #[test]
387    fn test_feature_creation() {
388        let point = Point::new(1.0, 2.0);
389        let mut feature = Feature::new(Geometry::Point(point));
390
391        feature.set_property("name", PropertyValue::String("Test Point".to_string()));
392        feature.set_property("value", PropertyValue::Integer(42));
393
394        assert!(feature.has_geometry());
395        assert_eq!(feature.properties.len(), 2);
396
397        let name = feature.get_property("name");
398        assert!(name.is_some());
399        assert_eq!(name.and_then(|v| v.as_string()), Some("Test Point"));
400
401        let value = feature.get_property("value");
402        assert!(value.is_some());
403        assert_eq!(value.and_then(|v| v.as_i64()), Some(42));
404    }
405
406    #[test]
407    fn test_feature_id() {
408        let point = Point::new(1.0, 2.0);
409        let feature = Feature::with_id(FeatureId::Integer(123), Geometry::Point(point));
410
411        assert_eq!(feature.id, Some(FeatureId::Integer(123)));
412    }
413
414    #[test]
415    fn test_property_value_conversions() {
416        let pv_int = PropertyValue::from(42_i64);
417        assert_eq!(pv_int.as_i64(), Some(42));
418
419        let pv_float = PropertyValue::from(2.78_f64);
420        assert_eq!(pv_float.as_f64(), Some(2.78));
421
422        let pv_bool = PropertyValue::from(true);
423        assert_eq!(pv_bool.as_bool(), Some(true));
424
425        let pv_str = PropertyValue::from("hello");
426        assert_eq!(pv_str.as_string(), Some("hello"));
427    }
428
429    #[test]
430    fn test_feature_collection() {
431        let mut collection = FeatureCollection::empty();
432        assert!(collection.is_empty());
433
434        let point1 = Point::new(1.0, 2.0);
435        let feature1 = Feature::new(Geometry::Point(point1));
436        collection.push(feature1);
437
438        let point2 = Point::new(3.0, 4.0);
439        let feature2 = Feature::new(Geometry::Point(point2));
440        collection.push(feature2);
441
442        assert_eq!(collection.len(), 2);
443        assert!(!collection.is_empty());
444
445        let bounds = collection.bounds();
446        assert!(bounds.is_some());
447        let (min_x, min_y, max_x, max_y) = bounds.expect("bounds calculation failed");
448        assert_eq!(min_x, 1.0);
449        assert_eq!(min_y, 2.0);
450        assert_eq!(max_x, 3.0);
451        assert_eq!(max_y, 4.0);
452    }
453}