1use crate::loader::LoadedData;
7use anyhow::Result;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct GeometryAnalysis {
14 pub type_distribution: HashMap<String, usize>,
16 pub dominant_type: String,
18 pub vertex_stats: VertexStats,
20 pub size_stats: SizeStats,
22 pub property_stats: PropertyStats,
24 pub complexity: GeometryComplexity,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct VertexStats {
31 pub min: usize,
32 pub max: usize,
33 pub avg: f64,
34 pub median: usize,
35 pub p95: usize,
36 pub p99: usize,
37 pub total: usize,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct SizeStats {
43 pub min: usize,
44 pub max: usize,
45 pub avg: f64,
46 pub median: usize,
47 pub p95: usize,
48 pub p99: usize,
49 pub total: usize,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct PropertyStats {
55 pub min: usize,
56 pub max: usize,
57 pub avg: f64,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub enum GeometryComplexity {
63 Simple,
65 Moderate,
67 Complex,
69 VeryComplex,
71}
72
73impl std::fmt::Display for GeometryComplexity {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 GeometryComplexity::Simple => write!(f, "Simple (points)"),
77 GeometryComplexity::Moderate => write!(f, "Moderate"),
78 GeometryComplexity::Complex => write!(f, "Complex"),
79 GeometryComplexity::VeryComplex => write!(f, "Very Complex"),
80 }
81 }
82}
83
84pub fn analyze(data: &LoadedData) -> Result<GeometryAnalysis> {
86 if data.features.is_empty() {
87 return Ok(empty_analysis());
88 }
89
90 let mut type_counts: HashMap<String, usize> = HashMap::new();
92 for feature in &data.features {
93 *type_counts.entry(feature.geometry_type.to_string()).or_insert(0) += 1;
94 }
95
96 let dominant_type = type_counts
98 .iter()
99 .max_by_key(|(_, count)| *count)
100 .map(|(t, _)| t.clone())
101 .unwrap_or_else(|| "Unknown".to_string());
102
103 let mut vertex_counts: Vec<usize> = data.features.iter().map(|f| f.vertex_count).collect();
105 vertex_counts.sort();
106 let vertex_stats = calculate_stats(&vertex_counts);
107
108 let mut sizes: Vec<usize> = data.features.iter().map(|f| f.estimated_size).collect();
110 sizes.sort();
111 let size_stats = calculate_size_stats(&sizes);
112
113 let property_counts: Vec<usize> = data.features.iter().map(|f| f.property_count).collect();
115 let property_stats = PropertyStats {
116 min: property_counts.iter().copied().min().unwrap_or(0),
117 max: property_counts.iter().copied().max().unwrap_or(0),
118 avg: if property_counts.is_empty() {
119 0.0
120 } else {
121 property_counts.iter().sum::<usize>() as f64 / property_counts.len() as f64
122 },
123 };
124
125 let complexity = classify_complexity(&vertex_stats, &dominant_type, &type_counts);
127
128 Ok(GeometryAnalysis {
129 type_distribution: type_counts,
130 dominant_type,
131 vertex_stats,
132 size_stats,
133 property_stats,
134 complexity,
135 })
136}
137
138fn empty_analysis() -> GeometryAnalysis {
139 GeometryAnalysis {
140 type_distribution: HashMap::new(),
141 dominant_type: "None".to_string(),
142 vertex_stats: VertexStats {
143 min: 0,
144 max: 0,
145 avg: 0.0,
146 median: 0,
147 p95: 0,
148 p99: 0,
149 total: 0,
150 },
151 size_stats: SizeStats {
152 min: 0,
153 max: 0,
154 avg: 0.0,
155 median: 0,
156 p95: 0,
157 p99: 0,
158 total: 0,
159 },
160 property_stats: PropertyStats {
161 min: 0,
162 max: 0,
163 avg: 0.0,
164 },
165 complexity: GeometryComplexity::Simple,
166 }
167}
168
169fn calculate_stats(sorted: &[usize]) -> VertexStats {
171 if sorted.is_empty() {
172 return VertexStats {
173 min: 0,
174 max: 0,
175 avg: 0.0,
176 median: 0,
177 p95: 0,
178 p99: 0,
179 total: 0,
180 };
181 }
182
183 let n = sorted.len();
184 let total: usize = sorted.iter().sum();
185
186 VertexStats {
187 min: sorted[0],
188 max: sorted[n - 1],
189 avg: total as f64 / n as f64,
190 median: sorted[n / 2],
191 p95: sorted[((n as f64 * 0.95) as usize).min(n - 1)],
192 p99: sorted[((n as f64 * 0.99) as usize).min(n - 1)],
193 total,
194 }
195}
196
197fn calculate_size_stats(sorted: &[usize]) -> SizeStats {
199 if sorted.is_empty() {
200 return SizeStats {
201 min: 0,
202 max: 0,
203 avg: 0.0,
204 median: 0,
205 p95: 0,
206 p99: 0,
207 total: 0,
208 };
209 }
210
211 let n = sorted.len();
212 let total: usize = sorted.iter().sum();
213
214 SizeStats {
215 min: sorted[0],
216 max: sorted[n - 1],
217 avg: total as f64 / n as f64,
218 median: sorted[n / 2],
219 p95: sorted[((n as f64 * 0.95) as usize).min(n - 1)],
220 p99: sorted[((n as f64 * 0.99) as usize).min(n - 1)],
221 total,
222 }
223}
224
225fn classify_complexity(
227 vertex_stats: &VertexStats,
228 dominant_type: &str,
229 type_counts: &HashMap<String, usize>,
230) -> GeometryComplexity {
231 let has_multi = type_counts.keys().any(|t| t.starts_with("Multi"));
233
234 if dominant_type == "Point" && vertex_stats.avg < 1.5 && !has_multi {
236 return GeometryComplexity::Simple;
237 }
238
239 if vertex_stats.avg > 1000.0 || vertex_stats.p95 > 5000 {
241 return GeometryComplexity::VeryComplex;
242 }
243
244 if vertex_stats.avg > 100.0 || vertex_stats.p95 > 500 {
246 return GeometryComplexity::Complex;
247 }
248
249 if dominant_type == "Polygon" || dominant_type == "MultiPolygon" {
251 if vertex_stats.avg > 20.0 {
252 return GeometryComplexity::Complex;
253 }
254 return GeometryComplexity::Moderate;
255 }
256
257 if dominant_type == "LineString" || dominant_type == "MultiLineString" {
259 if vertex_stats.avg > 50.0 {
260 return GeometryComplexity::Complex;
261 }
262 return GeometryComplexity::Moderate;
263 }
264
265 GeometryComplexity::Moderate
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn test_calculate_stats() {
274 let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
275 let stats = calculate_stats(&data);
276 assert_eq!(stats.min, 1);
277 assert_eq!(stats.max, 10);
278 assert_eq!(stats.median, 6);
280 assert!((stats.avg - 5.5).abs() < 0.01);
281 }
282
283 #[test]
284 fn test_classify_complexity_simple() {
285 let stats = VertexStats {
286 min: 1,
287 max: 1,
288 avg: 1.0,
289 median: 1,
290 p95: 1,
291 p99: 1,
292 total: 100,
293 };
294 let types: HashMap<String, usize> = [("Point".to_string(), 100)].into_iter().collect();
295 let complexity = classify_complexity(&stats, "Point", &types);
296 assert!(matches!(complexity, GeometryComplexity::Simple));
297 }
298}
299