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("");
122 let output_str = output.file_name()
123 .map(|n| n.to_string_lossy().to_string())
124 .unwrap_or_else(|| "output".to_string());
125
126 let input_str = input.file_name()
127 .map(|n| n.to_string_lossy().to_string())
128 .unwrap_or_else(|| "input.parquet".to_string());
129
130 format!(
131 "stt-build --input {} --output {} \\\n --time-field {} --min-zoom {} --max-zoom {}",
132 input_str,
133 output_str,
134 time_field,
135 recommendations.min_zoom,
136 recommendations.max_zoom,
137 )
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_to_command() {
146 let rec = Recommendations {
147 min_zoom: 0,
148 max_zoom: 10,
149 temporal_bucket_ms: 3_600_000,
150 temporal_bucket_human: "1 hour".to_string(),
151 confidence: 85,
152 explanations: vec![],
153 };
154
155 let cmd = to_command(&rec, Path::new("data.parquet"), "timestamp");
156 assert!(cmd.contains("--min-zoom 0"));
157 assert!(cmd.contains("--max-zoom 10"));
158 }
159}
160
161