use crate::analysis::AnalysisResult;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Recommendations {
pub min_zoom: u8,
pub max_zoom: u8,
pub temporal_bucket_ms: u64,
pub temporal_bucket_human: String,
pub confidence: u8,
pub explanations: Vec<String>,
}
pub fn generate_recommendations(result: &AnalysisResult) -> Recommendations {
let mut explanations = Vec::new();
let min_zoom = result.spatial.recommended_min_zoom;
let max_zoom = result.spatial.recommended_max_zoom;
explanations.push(format!(
"Zoom range {}-{} based on spatial coverage ({})",
min_zoom, max_zoom, result.spatial.distribution
));
let temporal_bucket_ms = result.temporal.recommended_bucket_ms;
let temporal_bucket_human = result.temporal.recommended_bucket_human.clone();
if temporal_bucket_ms > 0 {
explanations.push(format!(
"Temporal bucket {} for {} distribution",
temporal_bucket_human, result.temporal.distribution
));
}
let confidence = calculate_confidence(result);
Recommendations {
min_zoom,
max_zoom,
temporal_bucket_ms,
temporal_bucket_human,
confidence,
explanations,
}
}
fn calculate_confidence(result: &AnalysisResult) -> u8 {
let mut score = 100u8;
if result.feature_count < 1000 {
score = score.saturating_sub(20);
} else if result.feature_count < 10000 {
score = score.saturating_sub(10);
}
if result.density.issues.len() > 3 {
score = score.saturating_sub(15);
} else if !result.density.issues.is_empty() {
score = score.saturating_sub(5);
}
if matches!(result.spatial.distribution, crate::analysis::spatial::SpatialDistribution::Sparse) {
score = score.saturating_sub(10);
}
if matches!(result.geometry.complexity, crate::analysis::geometry::GeometryComplexity::VeryComplex) {
score = score.saturating_sub(10);
}
score
}
pub fn to_build_config(
recommendations: &Recommendations,
input: &Path,
time_field: &str,
) -> serde_json::Value {
serde_json::json!({
"input": input.to_string_lossy(),
"time_field": time_field,
"min_zoom": recommendations.min_zoom,
"max_zoom": recommendations.max_zoom,
"temporal_bucket_ms": recommendations.temporal_bucket_ms,
"confidence": recommendations.confidence,
"explanations": recommendations.explanations,
})
}
pub fn to_command(
recommendations: &Recommendations,
input: &Path,
time_field: &str,
) -> String {
let output = input.with_extension("stt");
let output_str = output.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "output.stt".to_string());
let input_str = input.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "input.parquet".to_string());
format!(
"stt-build --input {} --output {} \\\n --time-field {} --min-zoom {} --max-zoom {}",
input_str,
output_str,
time_field,
recommendations.min_zoom,
recommendations.max_zoom,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_command() {
let rec = Recommendations {
min_zoom: 0,
max_zoom: 10,
temporal_bucket_ms: 3_600_000,
temporal_bucket_human: "1 hour".to_string(),
confidence: 85,
explanations: vec![],
};
let cmd = to_command(&rec, Path::new("data.parquet"), "timestamp");
assert!(cmd.contains("--min-zoom 0"));
assert!(cmd.contains("--max-zoom 10"));
}
}