1use crate::models::Sighting;
2use anyhow::Result;
3use geo::{algorithm::convex_hull::ConvexHull, point, Coord};
4use geojson::{Geometry, Value};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct DbscanParams {
11 pub epsilon: f64,
13 pub min_points: usize,
15}
16
17impl Default for DbscanParams {
18 fn default() -> Self {
19 Self {
20 epsilon: 5.0, min_points: 5,
22 }
23 }
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct PackTerritory {
29 pub id: usize,
31 pub sighting_ids: Vec<i64>,
33 pub boundary: Option<String>,
35 pub area_km2: Option<f64>,
37 pub center: (f64, f64),
39 pub sighting_count: usize,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ClusteringResult {
46 pub territories: Vec<PackTerritory>,
48 pub noise_count: usize,
50 pub total_sightings: usize,
52}
53
54pub fn dbscan_cluster(sightings: &[Sighting], params: &DbscanParams) -> Result<ClusteringResult> {
56 if sightings.is_empty() {
57 return Ok(ClusteringResult {
58 territories: vec![],
59 noise_count: 0,
60 total_sightings: 0,
61 });
62 }
63
64 let mut visited = vec![false; sightings.len()];
65 let mut cluster_labels: Vec<Option<usize>> = vec![None; sightings.len()];
66 let mut cluster_id = 0;
67
68 for i in 0..sightings.len() {
69 if visited[i] {
70 continue;
71 }
72 visited[i] = true;
73
74 let neighbors = region_query(sightings, i, params);
75
76 if neighbors.len() < params.min_points {
77 cluster_labels[i] = None;
79 } else {
80 cluster_labels[i] = Some(cluster_id);
82 expand_cluster(
83 sightings,
84 &mut visited,
85 &mut cluster_labels,
86 &mut neighbors.clone(),
87 cluster_id,
88 params,
89 );
90 cluster_id += 1;
91 }
92 }
93
94 let mut cluster_map: HashMap<usize, Vec<i64>> = HashMap::new();
96 for (i, label) in cluster_labels.iter().enumerate() {
97 if let Some(cid) = label {
98 if let Some(id) = sightings[i].id {
99 cluster_map.entry(*cid).or_default().push(id);
100 }
101 }
102 }
103
104 let mut territories = Vec::new();
105 for (cid, sighting_ids) in cluster_map {
106 let cluster_sightings: Vec<&Sighting> = sighting_ids
107 .iter()
108 .filter_map(|&id| sightings.iter().find(|s| s.id == Some(id)))
109 .collect();
110
111 let center = calculate_center(&cluster_sightings);
112 let boundary = calculate_convex_hull(&cluster_sightings);
113 let area_km2 = calculate_area(&boundary);
114
115 territories.push(PackTerritory {
116 id: cid,
117 sighting_ids,
118 boundary,
119 area_km2,
120 center,
121 sighting_count: cluster_sightings.len(),
122 });
123 }
124
125 let noise_count = cluster_labels.iter().filter(|l| l.is_none()).count();
126
127 Ok(ClusteringResult {
128 territories,
129 noise_count,
130 total_sightings: sightings.len(),
131 })
132}
133
134fn region_query(sightings: &[Sighting], index: usize, params: &DbscanParams) -> Vec<usize> {
136 let mut neighbors = Vec::new();
137 let _p1 = point!(x: sightings[index].longitude, y: sightings[index].latitude);
138
139 for (i, sighting) in sightings.iter().enumerate() {
140 let _p2 = point!(x: sighting.longitude, y: sighting.latitude);
141 let distance_km = haversine_distance(
142 sightings[index].latitude,
143 sightings[index].longitude,
144 sighting.latitude,
145 sighting.longitude,
146 );
147
148 if distance_km <= params.epsilon {
149 neighbors.push(i);
150 }
151 }
152
153 neighbors
154}
155
156fn expand_cluster(
158 sightings: &[Sighting],
159 visited: &mut [bool],
160 cluster_labels: &mut [Option<usize>],
161 neighbors: &mut Vec<usize>,
162 cluster_id: usize,
163 params: &DbscanParams,
164) {
165 let mut i = 0;
166 while i < neighbors.len() {
167 let neighbor_idx = neighbors[i];
168
169 if !visited[neighbor_idx] {
170 visited[neighbor_idx] = true;
171 let new_neighbors = region_query(sightings, neighbor_idx, params);
172
173 if new_neighbors.len() >= params.min_points {
174 for &nn in &new_neighbors {
175 if !neighbors.contains(&nn) {
176 neighbors.push(nn);
177 }
178 }
179 }
180 }
181
182 if cluster_labels[neighbor_idx].is_none() {
183 cluster_labels[neighbor_idx] = Some(cluster_id);
184 }
185
186 i += 1;
187 }
188}
189
190fn calculate_center(sightings: &[&Sighting]) -> (f64, f64) {
192 if sightings.is_empty() {
193 return (0.0, 0.0);
194 }
195
196 let sum_lat: f64 = sightings.iter().map(|s| s.latitude).sum();
197 let sum_lon: f64 = sightings.iter().map(|s| s.longitude).sum();
198 let count = sightings.len() as f64;
199
200 (sum_lat / count, sum_lon / count)
201}
202
203fn calculate_convex_hull(sightings: &[&Sighting]) -> Option<String> {
205 if sightings.len() < 3 {
206 return None;
207 }
208
209 let points: Vec<Coord<f64>> = sightings
210 .iter()
211 .map(|s| Coord {
212 x: s.longitude,
213 y: s.latitude,
214 })
215 .collect();
216
217 let polygon = geo::Polygon::<f64>::new(points.into(), vec![]);
218 let hull = polygon.convex_hull();
219
220 let exterior = hull.exterior();
221 let coords: Vec<Vec<f64>> = exterior.points().map(|p| vec![p.x(), p.y()]).collect();
222
223 let geometry = Geometry::new(Value::Polygon(vec![coords]));
224 Some(serde_json::to_string(&geometry).unwrap_or_default())
225}
226
227fn calculate_area(boundary: &Option<String>) -> Option<f64> {
229 let boundary_str = boundary.as_ref()?;
230 let geometry: Geometry = serde_json::from_str(boundary_str).ok()?;
231
232 match geometry.value {
233 Value::Polygon(ref rings) => {
234 if let Some(exterior) = rings.first() {
235 if exterior.len() < 4 {
236 return None;
237 }
238
239 let mut area = 0.0;
241 for i in 0..exterior.len() - 1 {
242 let (x1, y1) = (exterior[i][0], exterior[i][1]);
243 let (x2, y2) = (exterior[i + 1][0], exterior[i + 1][1]);
244 area += (x1 * y2) - (x2 * y1);
245 }
246 area = area.abs() / 2.0;
247
248 Some(area * 111.32 * 111.32)
251 } else {
252 None
253 }
254 }
255 _ => None,
256 }
257}
258
259fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
261 const EARTH_RADIUS_KM: f64 = 6371.0;
262
263 let dlat = (lat2 - lat1).to_radians();
264 let dlon = (lon2 - lon1).to_radians();
265
266 let lat1_rad = lat1.to_radians();
267 let lat2_rad = lat2.to_radians();
268
269 let a = (dlat / 2.0).sin() * (dlat / 2.0).sin()
270 + (dlon / 2.0).sin() * (dlon / 2.0).sin() * lat1_rad.cos() * lat2_rad.cos();
271 let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
272
273 EARTH_RADIUS_KM * c
274}
275
276pub fn detect_overlaps(
278 territories: &[PackTerritory],
279 threshold_km: f64,
280) -> Vec<(usize, usize, f64)> {
281 let mut overlaps = Vec::new();
282
283 for i in 0..territories.len() {
284 for j in (i + 1)..territories.len() {
285 let distance = haversine_distance(
286 territories[i].center.0,
287 territories[i].center.1,
288 territories[j].center.0,
289 territories[j].center.1,
290 );
291
292 if distance < threshold_km {
293 overlaps.push((i, j, distance));
294 }
295 }
296 }
297
298 overlaps
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use chrono::Utc;
305
306 fn create_test_sighting(lat: f64, lon: f64, id: i64) -> Sighting {
307 Sighting {
308 id: Some(id),
309 species: "Canis lupus".to_string(),
310 scientific_name: Some("Canis lupus".to_string()),
311 latitude: lat,
312 longitude: lon,
313 observed_on: Utc::now(),
314 source: crate::models::Source::GBIF,
315 source_id: format!("test_{}", id),
316 details: None,
317 }
318 }
319
320 #[test]
321 fn test_haversine_distance() {
322 let distance = haversine_distance(0.0, 0.0, 0.0, 1.0);
323 assert!(distance > 100.0 && distance < 120.0); }
325
326 #[test]
327 fn test_dbscan_empty() {
328 let sightings = vec![];
329 let params = DbscanParams::default();
330 let result = dbscan_cluster(&sightings, ¶ms).unwrap();
331 assert_eq!(result.total_sightings, 0);
332 assert!(result.territories.is_empty());
333 }
334
335 #[test]
336 fn test_dbscan_single_cluster() {
337 let sightings = vec![
338 create_test_sighting(45.0, -122.0, 1),
339 create_test_sighting(45.01, -122.01, 2),
340 create_test_sighting(45.02, -122.02, 3),
341 create_test_sighting(45.03, -122.03, 4),
342 create_test_sighting(45.04, -122.04, 5),
343 ];
344
345 let params = DbscanParams {
346 epsilon: 10.0,
347 min_points: 3,
348 };
349
350 let result = dbscan_cluster(&sightings, ¶ms).unwrap();
351 assert_eq!(result.territories.len(), 1);
352 assert_eq!(result.territories[0].sighting_count, 5);
353 }
354
355 #[test]
356 fn test_dbscan_noise() {
357 let sightings = vec![
358 create_test_sighting(45.0, -122.0, 1),
359 create_test_sighting(45.01, -122.01, 2),
360 create_test_sighting(50.0, -120.0, 3), ];
362
363 let params = DbscanParams {
364 epsilon: 5.0,
365 min_points: 2,
366 };
367
368 let result = dbscan_cluster(&sightings, ¶ms).unwrap();
369 assert_eq!(result.territories.len(), 1);
370 assert_eq!(result.noise_count, 1);
371 }
372
373 #[test]
374 fn test_calculate_center() {
375 let sightings = [
376 create_test_sighting(45.0, -122.0, 1),
377 create_test_sighting(47.0, -124.0, 2),
378 ];
379
380 let center = calculate_center(&sightings.iter().collect::<Vec<_>>());
381 assert!((center.0 - 46.0).abs() < 0.01);
382 assert!((center.1 - (-123.0)).abs() < 0.01);
383 }
384}