Skip to main content

stt_optimize/analysis/
geometry.rs

1//! Geometry complexity and size analysis
2//!
3//! Analyzes geometry types, vertex counts, and estimated sizes
4//! to inform complexity classification and chunk sizing decisions.
5
6use crate::loader::LoadedData;
7use anyhow::Result;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Geometry analysis results
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct GeometryAnalysis {
14    /// Distribution of geometry types
15    pub type_distribution: HashMap<String, usize>,
16    /// Dominant geometry type (most common)
17    pub dominant_type: String,
18    /// Vertex count statistics
19    pub vertex_stats: VertexStats,
20    /// Estimated size statistics (bytes per feature)
21    pub size_stats: SizeStats,
22    /// Property count statistics
23    pub property_stats: PropertyStats,
24    /// Geometry complexity classification
25    pub complexity: GeometryComplexity,
26}
27
28/// Vertex count statistics
29#[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/// Size statistics (estimated bytes)
41#[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/// Property count statistics
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct PropertyStats {
55    pub min: usize,
56    pub max: usize,
57    pub avg: f64,
58}
59
60/// Classification of geometry complexity
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub enum GeometryComplexity {
63    /// Simple point data
64    Simple,
65    /// Moderate complexity (short linestrings, simple polygons)
66    Moderate,
67    /// High complexity (long linestrings, complex polygons)
68    Complex,
69    /// Very high complexity (multigeometries, many vertices)
70    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
84/// Analyze geometry characteristics of the dataset
85pub fn analyze(data: &LoadedData) -> Result<GeometryAnalysis> {
86    if data.features.is_empty() {
87        return Ok(empty_analysis());
88    }
89
90    // Count geometry types
91    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    // Find dominant type
97    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    // Calculate vertex statistics
104    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    // Calculate size statistics
109    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    // Calculate property statistics
114    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    // Classify complexity
126    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
169/// Calculate statistics from a sorted list of counts
170fn 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
197/// Calculate size statistics from a sorted list of sizes
198fn 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
225/// Classify geometry complexity
226fn classify_complexity(
227    vertex_stats: &VertexStats,
228    dominant_type: &str,
229    type_counts: &HashMap<String, usize>,
230) -> GeometryComplexity {
231    // Check for multi-geometries
232    let has_multi = type_counts.keys().any(|t| t.starts_with("Multi"));
233
234    // Simple points
235    if dominant_type == "Point" && vertex_stats.avg < 1.5 && !has_multi {
236        return GeometryComplexity::Simple;
237    }
238
239    // Very complex: high vertex counts or multi-geometries
240    if vertex_stats.avg > 1000.0 || vertex_stats.p95 > 5000 {
241        return GeometryComplexity::VeryComplex;
242    }
243
244    // Complex: moderate vertex counts
245    if vertex_stats.avg > 100.0 || vertex_stats.p95 > 500 {
246        return GeometryComplexity::Complex;
247    }
248
249    // Polygons tend to be more complex
250    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    // LineStrings
258    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        // Median at index 5 (0-indexed) is 6
279        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