stt_optimize/
recommend.rs1use crate::analysis::AnalysisResult;
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Recommendations {
16 pub min_zoom: u8,
18 pub max_zoom: u8,
20 pub temporal_bucket_ms: u64,
22 pub temporal_bucket_human: String,
24 pub confidence: u8,
26 pub explanations: Vec<String>,
28}
29
30pub fn generate_recommendations(result: &AnalysisResult) -> Recommendations {
32 let mut explanations = Vec::new();
33
34 let min_zoom = result.spatial.recommended_min_zoom;
36 let max_zoom = result.spatial.recommended_max_zoom;
37 explanations.push(format!(
38 "Zoom range {}-{} based on spatial coverage ({})",
39 min_zoom, max_zoom, result.spatial.distribution
40 ));
41
42 let temporal_bucket_ms = result.temporal.recommended_bucket_ms;
44 let temporal_bucket_human = result.temporal.recommended_bucket_human.clone();
45 if temporal_bucket_ms > 0 {
46 explanations.push(format!(
47 "Temporal bucket {} for {} distribution",
48 temporal_bucket_human, result.temporal.distribution
49 ));
50 }
51
52 let confidence = calculate_confidence(result);
54
55 Recommendations {
56 min_zoom,
57 max_zoom,
58 temporal_bucket_ms,
59 temporal_bucket_human,
60 confidence,
61 explanations,
62 }
63}
64
65fn calculate_confidence(result: &AnalysisResult) -> u8 {
67 let mut score = 100u8;
68
69 if result.feature_count < 1000 {
71 score = score.saturating_sub(20);
72 } else if result.feature_count < 10000 {
73 score = score.saturating_sub(10);
74 }
75
76 if result.density.issues.len() > 3 {
78 score = score.saturating_sub(15);
79 } else if !result.density.issues.is_empty() {
80 score = score.saturating_sub(5);
81 }
82
83 if matches!(result.spatial.distribution, crate::analysis::spatial::SpatialDistribution::Sparse) {
85 score = score.saturating_sub(10);
86 }
87
88 if matches!(result.geometry.complexity, crate::analysis::geometry::GeometryComplexity::VeryComplex) {
90 score = score.saturating_sub(10);
91 }
92
93 score
94}
95
96pub fn to_build_config(
98 recommendations: &Recommendations,
99 input: &Path,
100 time_field: &str,
101) -> serde_json::Value {
102 serde_json::json!({
103 "input": input.to_string_lossy(),
104 "time_field": time_field,
105 "min_zoom": recommendations.min_zoom,
106 "max_zoom": recommendations.max_zoom,
107 "temporal_bucket_ms": recommendations.temporal_bucket_ms,
108 "confidence": recommendations.confidence,
109 "explanations": recommendations.explanations,
110 })
111}
112
113pub fn to_command(
115 recommendations: &Recommendations,
116 input: &Path,
117 time_field: &str,
118) -> String {
119 let output = input.with_extension("stt");
120 let output_str = output.file_name()
121 .map(|n| n.to_string_lossy().to_string())
122 .unwrap_or_else(|| "output.stt".to_string());
123
124 let input_str = input.file_name()
125 .map(|n| n.to_string_lossy().to_string())
126 .unwrap_or_else(|| "input.parquet".to_string());
127
128 format!(
129 "stt-build --input {} --output {} \\\n --time-field {} --min-zoom {} --max-zoom {}",
130 input_str,
131 output_str,
132 time_field,
133 recommendations.min_zoom,
134 recommendations.max_zoom,
135 )
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_to_command() {
144 let rec = Recommendations {
145 min_zoom: 0,
146 max_zoom: 10,
147 temporal_bucket_ms: 3_600_000,
148 temporal_bucket_human: "1 hour".to_string(),
149 confidence: 85,
150 explanations: vec![],
151 };
152
153 let cmd = to_command(&rec, Path::new("data.parquet"), "timestamp");
154 assert!(cmd.contains("--min-zoom 0"));
155 assert!(cmd.contains("--max-zoom 10"));
156 }
157}
158
159