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}