Skip to main content

everymap_core/domains/
positioning.rs

1use crate::error::EveryMapResult;
2use crate::types::Coordinate;
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5
6/// Options for network-based positioning.
7///
8/// Network positioning uses WiFi, cell tower, and Bluetooth observations
9/// to estimate a device's location. The observation data is provider-specific
10/// and passed via `provider_extra`.
11#[derive(Debug, Clone, Default, Serialize, Deserialize)]
12pub struct PositioningOptions {
13    /// Provider-specific observation data (HERE: wlan/cell/bluetooth arrays; Google: considerIp, wifiAccessPoints, cellTowers)
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub provider_extra: Option<serde_json::Value>,
16}
17
18/// A unified positioning response from the core trait.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct PositioningResponse {
21    /// The estimated position
22    pub coordinate: Coordinate,
23    /// Accuracy of the position estimate in meters
24    pub accuracy: Option<f64>,
25    /// Altitude in meters above sea level (if available)
26    pub altitude: Option<f64>,
27    /// Accuracy of the altitude estimate in meters
28    pub altitude_accuracy: Option<f64>,
29    /// Provider-specific raw data for advanced use cases
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub raw: Option<serde_json::Value>,
32}
33
34#[async_trait]
35pub trait NetworkPositioner: Send + Sync {
36    async fn get_position(
37        &self,
38        options: &PositioningOptions,
39    ) -> EveryMapResult<PositioningResponse>;
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn test_positioning_options_default() {
48        let options = PositioningOptions::default();
49        assert!(options.provider_extra.is_none());
50    }
51
52    #[test]
53    fn test_positioning_options_with_provider_extra() {
54        let options = PositioningOptions {
55            provider_extra: Some(serde_json::json!({"wlan": [{"mac": "aa:bb:cc:dd:ee:ff"}]})),
56        };
57        assert!(options.provider_extra.is_some());
58    }
59
60    #[test]
61    fn test_positioning_response_construction() {
62        let coordinate = Coordinate::new(52.5, 13.4).unwrap();
63        let response = PositioningResponse {
64            coordinate,
65            accuracy: Some(50.0),
66            altitude: Some(34.0),
67            altitude_accuracy: Some(10.0),
68            raw: None,
69        };
70        assert_eq!(response.coordinate.lat, 52.5);
71        assert_eq!(response.accuracy, Some(50.0));
72        assert_eq!(response.altitude, Some(34.0));
73    }
74
75    // --- PositioningResponse serde roundtrip ---
76
77    #[test]
78    fn test_positioning_response_serde_roundtrip() {
79        let coordinate = Coordinate::new(52.5, 13.4).unwrap();
80        let response = PositioningResponse {
81            coordinate,
82            accuracy: Some(50.0),
83            altitude: Some(34.0),
84            altitude_accuracy: Some(10.0),
85            raw: Some(serde_json::json!({"wlan_count": 5})),
86        };
87        let json = serde_json::to_string(&response).unwrap();
88        let back: PositioningResponse = serde_json::from_str(&json).unwrap();
89        assert_eq!(back.coordinate.lat, 52.5);
90        assert_eq!(back.accuracy, Some(50.0));
91        assert_eq!(back.altitude, Some(34.0));
92        assert!(back.raw.is_some());
93    }
94
95    #[test]
96    fn test_positioning_response_minimal_serde_roundtrip() {
97        let coordinate = Coordinate::ORIGIN;
98        let response = PositioningResponse {
99            coordinate,
100            accuracy: None,
101            altitude: None,
102            altitude_accuracy: None,
103            raw: None,
104        };
105        let json = serde_json::to_string(&response).unwrap();
106        let back: PositioningResponse = serde_json::from_str(&json).unwrap();
107        assert_eq!(back.coordinate, coordinate);
108        assert!(back.accuracy.is_none());
109    }
110
111    // --- PositioningOptions serde roundtrip ---
112
113    #[test]
114    fn test_positioning_options_serde_roundtrip() {
115        let options = PositioningOptions {
116            provider_extra: Some(serde_json::json!({
117                "wlan": [{"mac": "aa:bb:cc:dd:ee:ff", "signalStrength": -70}],
118                "cell": [{"mcc": 262, "mnc": 1, "cid": 12345}]
119            })),
120        };
121        let json = serde_json::to_string(&options).unwrap();
122        let back: PositioningOptions = serde_json::from_str(&json).unwrap();
123        assert!(back.provider_extra.is_some());
124    }
125
126    // --- Edge cases ---
127
128    #[test]
129    fn test_positioning_response_boundary_coordinates() {
130        let coordinate = Coordinate::new(90.0, 180.0).unwrap();
131        let response = PositioningResponse {
132            coordinate,
133            accuracy: Some(0.0),
134            altitude: Some(0.0),
135            altitude_accuracy: Some(0.0),
136            raw: None,
137        };
138        assert_eq!(response.coordinate.lat, 90.0);
139        assert_eq!(response.accuracy, Some(0.0));
140    }
141
142    #[test]
143    fn test_positioning_response_negative_altitude() {
144        let coordinate = Coordinate::new(-33.8688, 151.2093).unwrap();
145        let response = PositioningResponse {
146            coordinate,
147            accuracy: Some(15.0),
148            altitude: Some(-11.0),
149            altitude_accuracy: Some(5.0),
150            raw: None,
151        };
152        assert_eq!(response.altitude, Some(-11.0));
153    }
154}