use std::collections::HashMap;
use crate::component::types::ComponentBbox;
#[derive(Debug, Clone)]
pub enum FieldValue {
Null,
Bool(bool),
Int(i64),
Float(f64),
String(String),
Bytes(Vec<u8>),
}
impl FieldValue {
pub fn as_f64(&self) -> Option<f64> {
match self {
Self::Int(v) => Some(*v as f64),
Self::Float(v) => Some(*v),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
if let Self::String(s) = self {
Some(s.as_str())
} else {
None
}
}
pub fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
}
impl PartialEq for FieldValue {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Null, Self::Null) => true,
(Self::Bool(a), Self::Bool(b)) => a == b,
(Self::Int(a), Self::Int(b)) => a == b,
(Self::Float(a), Self::Float(b)) => a == b,
(Self::String(a), Self::String(b)) => a == b,
(Self::Bytes(a), Self::Bytes(b)) => a == b,
_ => false,
}
}
}
#[derive(Debug, Clone)]
pub struct ComponentFeature {
pub id: Option<String>,
pub geometry_wkb: Option<Vec<u8>>,
pub properties: HashMap<String, FieldValue>,
pub bbox: Option<ComponentBbox>,
}
impl ComponentFeature {
pub fn new() -> Self {
Self {
id: None,
geometry_wkb: None,
properties: HashMap::new(),
bbox: None,
}
}
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn with_geometry(mut self, wkb: Vec<u8>) -> Self {
self.geometry_wkb = Some(wkb);
self
}
pub fn with_bbox(mut self, bbox: ComponentBbox) -> Self {
self.bbox = Some(bbox);
self
}
pub fn set_property(&mut self, key: impl Into<String>, value: FieldValue) {
self.properties.insert(key.into(), value);
}
pub fn get_property(&self, key: &str) -> Option<&FieldValue> {
self.properties.get(key)
}
pub fn has_geometry(&self) -> bool {
self.geometry_wkb.is_some()
}
pub fn property_count(&self) -> usize {
self.properties.len()
}
}
impl Default for ComponentFeature {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ComponentFeatureCollection {
pub features: Vec<ComponentFeature>,
pub crs_wkt: Option<String>,
pub bbox: Option<ComponentBbox>,
}
impl ComponentFeatureCollection {
pub fn new() -> Self {
Self {
features: Vec::new(),
crs_wkt: None,
bbox: None,
}
}
pub fn add_feature(&mut self, feature: ComponentFeature) {
self.features.push(feature);
}
pub fn len(&self) -> usize {
self.features.len()
}
pub fn is_empty(&self) -> bool {
self.features.is_empty()
}
pub fn filter_by_bbox(&self, bbox: &ComponentBbox) -> Self {
let filtered = self
.features
.iter()
.filter(|f| {
f.bbox.as_ref().map(|b| b.intersects(bbox)).unwrap_or(true) })
.cloned()
.collect();
Self {
features: filtered,
crs_wkt: self.crs_wkt.clone(),
bbox: Some(bbox.clone()),
}
}
pub fn filter_by_bbox_strict(&self, bbox: &ComponentBbox) -> Self {
let filtered = self
.features
.iter()
.filter(|f| {
f.bbox
.as_ref()
.map(|b| {
b.min_x >= bbox.min_x
&& b.min_y >= bbox.min_y
&& b.max_x <= bbox.max_x
&& b.max_y <= bbox.max_y
})
.unwrap_or(false)
})
.cloned()
.collect();
Self {
features: filtered,
crs_wkt: self.crs_wkt.clone(),
bbox: Some(bbox.clone()),
}
}
pub fn compute_bbox(&self) -> Option<ComponentBbox> {
let mut union: Option<ComponentBbox> = None;
for f in &self.features {
if let Some(b) = &f.bbox {
union = Some(match union {
None => b.clone(),
Some(u) => u.union(b),
});
}
}
union
}
}
impl Default for ComponentFeatureCollection {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn feature_new_is_empty() {
let f = ComponentFeature::new();
assert!(f.id.is_none());
assert!(!f.has_geometry());
assert_eq!(f.property_count(), 0);
}
#[test]
fn feature_with_id() {
let f = ComponentFeature::new().with_id("feat-1");
assert_eq!(f.id.as_deref(), Some("feat-1"));
}
#[test]
fn feature_with_geometry() {
let wkb = vec![1u8, 2, 3, 4];
let f = ComponentFeature::new().with_geometry(wkb.clone());
assert!(f.has_geometry());
assert_eq!(f.geometry_wkb.as_deref(), Some(wkb.as_slice()));
}
#[test]
fn feature_set_get_property() {
let mut f = ComponentFeature::new();
f.set_property("name", FieldValue::String("hello".into()));
assert_eq!(
f.get_property("name").and_then(|v| v.as_str()),
Some("hello")
);
}
#[test]
fn property_value_as_f64() {
assert_eq!(FieldValue::Int(42).as_f64(), Some(42.0));
assert_eq!(FieldValue::Float(1.234).as_f64(), Some(1.234));
assert!(FieldValue::Null.as_f64().is_none());
}
#[test]
fn property_value_is_null() {
assert!(FieldValue::Null.is_null());
assert!(!FieldValue::Bool(true).is_null());
}
#[test]
fn collection_add_and_len() {
let mut col = ComponentFeatureCollection::new();
assert!(col.is_empty());
col.add_feature(ComponentFeature::new());
assert_eq!(col.len(), 1);
}
#[test]
fn collection_filter_by_bbox() {
let mut col = ComponentFeatureCollection::new();
col.add_feature(ComponentFeature::new().with_bbox(ComponentBbox::new(1.0, 1.0, 2.0, 2.0)));
col.add_feature(
ComponentFeature::new().with_bbox(ComponentBbox::new(100.0, 100.0, 200.0, 200.0)),
);
col.add_feature(ComponentFeature::new());
let filter = ComponentBbox::new(0.0, 0.0, 5.0, 5.0);
let result = col.filter_by_bbox(&filter);
assert_eq!(result.len(), 2); }
#[test]
fn collection_filter_no_match() {
let mut col = ComponentFeatureCollection::new();
col.add_feature(
ComponentFeature::new().with_bbox(ComponentBbox::new(100.0, 100.0, 200.0, 200.0)),
);
let filter = ComponentBbox::new(0.0, 0.0, 5.0, 5.0);
let result = col.filter_by_bbox_strict(&filter);
assert!(result.is_empty());
}
}