Skip to main content

geomap_engine/
geojson.rs

1use serde::Serialize;
2
3use crate::proto::{MapObject, SceneMap};
4
5/// A GeoJSON-like FeatureCollection: each MapObject becomes a Point
6/// feature, so the map can be written to a .geojson file and inspected
7/// with matplotlib/geopandas without any mobile app in the loop.
8#[derive(Serialize)]
9pub struct FeatureCollection {
10    #[serde(rename = "type")]
11    kind: &'static str,
12    updated_at: f64,
13    features: Vec<Feature>,
14}
15
16#[derive(Serialize)]
17struct Feature {
18    #[serde(rename = "type")]
19    kind: &'static str,
20    geometry: Geometry,
21    properties: Properties,
22}
23
24#[derive(Serialize)]
25struct Geometry {
26    #[serde(rename = "type")]
27    kind: &'static str,
28    coordinates: [f32; 2],
29}
30
31#[derive(Serialize)]
32struct Properties {
33    id: String,
34    label: String,
35    confidence: f32,
36    observation_count: i32,
37    first_seen: f64,
38    last_seen: f64,
39}
40
41impl SceneMap {
42    pub fn to_geojson(&self) -> FeatureCollection {
43        FeatureCollection {
44            kind: "FeatureCollection",
45            updated_at: self.updated_at,
46            features: self.objects.iter().map(Feature::from).collect(),
47        }
48    }
49
50    pub fn to_geojson_json(&self) -> serde_json::Result<String> {
51        serde_json::to_string_pretty(&self.to_geojson())
52    }
53}
54
55impl From<&MapObject> for Feature {
56    fn from(obj: &MapObject) -> Self {
57        Feature {
58            kind: "Feature",
59            geometry: Geometry { kind: "Point", coordinates: [obj.x, obj.y] },
60            properties: Properties {
61                id: obj.id.clone(),
62                label: obj.label.clone(),
63                confidence: obj.confidence,
64                observation_count: obj.observation_count,
65                first_seen: obj.first_seen,
66                last_seen: obj.last_seen,
67            },
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    fn sample_map() -> SceneMap {
77        SceneMap {
78            objects: vec![MapObject {
79                id: "abc-123".to_string(),
80                label: "chair".to_string(),
81                x: 1.5,
82                y: -2.5,
83                confidence: 0.87,
84                observation_count: 3,
85                first_seen: 10.0,
86                last_seen: 12.5,
87            }],
88            updated_at: 12.5,
89        }
90    }
91
92    #[test]
93    fn converts_map_objects_to_point_features() {
94        let fc = sample_map().to_geojson();
95        assert_eq!(fc.features.len(), 1);
96        assert_eq!(fc.features[0].geometry.coordinates, [1.5, -2.5]);
97        assert_eq!(fc.features[0].properties.label, "chair");
98    }
99
100    #[test]
101    fn serializes_to_valid_geojson_shaped_json() {
102        let json = sample_map().to_geojson_json().unwrap();
103        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
104
105        assert_eq!(value["type"], "FeatureCollection");
106        assert_eq!(value["features"][0]["type"], "Feature");
107        assert_eq!(value["features"][0]["geometry"]["type"], "Point");
108        assert_eq!(value["features"][0]["geometry"]["coordinates"][0], 1.5);
109        assert_eq!(value["features"][0]["properties"]["id"], "abc-123");
110    }
111}