Skip to main content

geonative_core/
feature.rs

1//! The "feature" — one row in a spatial dataset.
2//!
3//! ## Shape
4//!
5//! A `Feature` is the trio you'd expect from anything geospatial:
6//! - **`fid`** — optional row identifier (FileGDB OBJECTID, Shapefile record
7//!   number, GeoJSON's free-form `id`). `None` for formats that don't track
8//!   one (raw GeoJSON FeatureCollections, Arrow record batches).
9//! - **`geometry`** — optional `Geometry`. `None` for attribute-only tables
10//!   (GDB system tables, DBF-only Shapefiles, the `Layer` for a non-spatial
11//!   PostGIS view).
12//! - **`attributes`** — `Vec<Value>` in `Schema::fields` order. Length
13//!   **must** equal the schema's `fields.len()`; readers / writers enforce
14//!   this contract at row build time.
15//!
16//! ## Why a `Vec` and not a `HashMap<String, Value>`
17//!
18//! Format readers/writers care about field order: DBF stores columns in a
19//! fixed sequence, Arrow batches map column index to field, FileGDB rows
20//! emit nullable bitmap bits in declared order. A `HashMap` would lose this
21//! and double the per-row allocation cost. Name lookup is available via
22//! `Schema::field_index(name)`.
23
24use crate::{geometry::Geometry, value::Value};
25
26#[derive(Debug, Clone, PartialEq)]
27pub struct Feature {
28    /// Feature identifier. `None` for formats that don't expose stable FIDs.
29    pub fid: Option<i64>,
30    /// Geometry payload. `None` for attribute-only tables (GDB system tables,
31    /// DBF-only Shapefile sidecars).
32    pub geometry: Option<Geometry>,
33    /// Attribute values, in the same order as `Schema::fields`. Length
34    /// **must** equal `Schema::fields.len()`.
35    pub attributes: Vec<Value>,
36}
37
38impl Feature {
39    pub fn new(fid: Option<i64>, geometry: Option<Geometry>, attributes: Vec<Value>) -> Self {
40        Self {
41            fid,
42            geometry,
43            attributes,
44        }
45    }
46
47    pub fn attribute(&self, idx: usize) -> Option<&Value> {
48        self.attributes.get(idx)
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::{Coord, Geometry};
56
57    #[test]
58    fn construct_and_access() {
59        let f = Feature::new(
60            Some(42),
61            Some(Geometry::Point(Coord::xy(1.0, 2.0))),
62            vec![Value::Int32(7), Value::String("hi".into())],
63        );
64        assert_eq!(f.fid, Some(42));
65        assert!(matches!(f.geometry, Some(Geometry::Point(_))));
66        assert_eq!(f.attribute(0), Some(&Value::Int32(7)));
67        assert_eq!(f.attribute(2), None);
68    }
69}