use crate::check::discrepancy::TemporalPatterns;
use crate::check::preprocess::ProofCheckFile;
use chrono::NaiveDateTime;
use geo::{coord, Centroid, ConvexHull, Distance, Haversine, LineString, Point, Polygon};
use geojson::{
Feature, FeatureCollection, GeoJson, Geometry, JsonObject, JsonValue, Value as GeoJSONValue,
};
use serde::{Deserialize, Serialize};
use std::f64::consts::PI;
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TemporalAnalysis {
pub earliest: String,
pub latest: String,
pub elapsed_seconds: f64,
pub file_count: usize,
pub patterns: Option<TemporalPatterns>,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct PairwiseDistance {
pub file_a: String,
pub file_b: String,
pub distance_km: f64,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SpatialAnalysis {
pub max_distance_km: f64,
pub area_km2: f64,
pub centroid: [f64; 2],
pub point_count: usize,
pub pairwise_distances: Vec<PairwiseDistance>,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Synchrony {
pub verified: bool,
pub geojson: GeoJson,
pub temporal: Option<TemporalAnalysis>,
pub spatial: Option<SpatialAnalysis>,
}
pub fn check_synchrony(proof_files: &Vec<ProofCheckFile>) -> Synchrony {
let mut features: Vec<Feature> = vec![];
let mut coord_files: Vec<(String, f64, f64)> = vec![];
for file in proof_files {
if let Some(json) = &file.json {
if let Some(latitude) = json.location.latitude {
if let Some(longitude) = json.location.longitude {
let geometry = Geometry::new(GeoJSONValue::Point(vec![longitude, latitude]));
let image_name = file.name.clone();
let mut properties = JsonObject::new();
properties.insert(String::from("name"), JsonValue::from(image_name.clone()));
let feature = Feature {
bbox: None,
geometry: Some(geometry),
id: None,
properties: Some(properties),
foreign_members: None,
};
features.push(feature);
coord_files.push((image_name, latitude, longitude));
}
}
}
}
let mut coordinates = vec![];
for feature in features.clone() {
if let Some(geometry) = feature.geometry {
if let GeoJSONValue::Point(pt) = geometry.value {
let c = coord! {
x: pt[0],
y: pt[1],
};
coordinates.push(c);
}
}
}
let mut centroid_coords: [f64; 2] = [0.0, 0.0];
let coords = LineString(coordinates);
let polygon = Polygon::new(coords, vec![]);
let hull = polygon.convex_hull();
let exterior = hull.exterior();
let _exterior_string = format!("{:?}", exterior);
if let Some(centroid) = polygon.centroid() {
centroid_coords = [centroid.x(), centroid.y()];
let geojson_point = GeoJSONValue::Point(vec![centroid.x(), centroid.y()]);
let mut properties = JsonObject::new();
properties.insert(String::from("name"), JsonValue::from("Centroid"));
let feature = Feature {
bbox: None,
geometry: Some(Geometry::new(geojson_point)),
id: None,
properties: Some(properties),
foreign_members: None,
};
features.push(feature);
}
let geojson = FeatureCollection {
bbox: None,
features,
foreign_members: None,
};
let temporal = compute_temporal_analysis(proof_files);
let spatial = compute_spatial_analysis(&coord_files, centroid_coords);
Synchrony {
verified: true,
geojson: geojson.into(),
temporal,
spatial,
}
}
fn compute_temporal_analysis(proof_files: &[ProofCheckFile]) -> Option<TemporalAnalysis> {
let mut timestamps: Vec<(NaiveDateTime, &str)> = Vec::new();
for file in proof_files {
if let Some(json) = &file.json {
let time_str = json
.location
.time
.as_deref()
.or(json.file.created_at.as_deref());
if let Some(ts) = time_str {
if let Some(parsed) = parse_timestamp(ts) {
timestamps.push((parsed, &file.name));
}
}
}
}
if timestamps.is_empty() {
return None;
}
timestamps.sort_by_key(|(dt, _)| *dt);
let earliest = timestamps.first().unwrap().0;
let latest = timestamps.last().unwrap().0;
let elapsed = (latest - earliest).num_milliseconds() as f64 / 1000.0;
#[cfg(feature = "polars")]
let patterns = crate::check::analytics::analyze_temporal_patterns(proof_files);
#[cfg(not(feature = "polars"))]
let patterns = None;
Some(TemporalAnalysis {
earliest: earliest.format("%Y-%m-%dT%H:%M:%S").to_string(),
latest: latest.format("%Y-%m-%dT%H:%M:%S").to_string(),
elapsed_seconds: elapsed,
file_count: timestamps.len(),
patterns,
})
}
pub(crate) fn parse_timestamp(s: &str) -> Option<NaiveDateTime> {
let formats = [
"%Y-%m-%dT%H:%M:%S%.f",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%d %H:%M:%S",
"%Y:%m:%d %H:%M:%S",
"%Y-%m-%dT%H:%M:%S%.fZ",
"%Y-%m-%dT%H:%M:%SZ",
];
for fmt in &formats {
if let Ok(dt) = NaiveDateTime::parse_from_str(s, fmt) {
return Some(dt);
}
}
if let Ok(ms) = s.parse::<i64>() {
return chrono::DateTime::from_timestamp_millis(ms).map(|dt| dt.naive_utc());
}
None
}
fn compute_spatial_analysis(
coord_files: &[(String, f64, f64)],
centroid_coords: [f64; 2],
) -> Option<SpatialAnalysis> {
if coord_files.len() < 2 {
return None;
}
let mut pairwise_distances: Vec<PairwiseDistance> = Vec::new();
let mut max_distance_km: f64 = 0.0;
for i in 0..coord_files.len() {
for j in (i + 1)..coord_files.len() {
let (ref name_a, lat_a, lon_a) = coord_files[i];
let (ref name_b, lat_b, lon_b) = coord_files[j];
let point_a = Point::new(lon_a, lat_a);
let point_b = Point::new(lon_b, lat_b);
let distance_m = Haversine.distance(point_a, point_b);
let distance_km = distance_m / 1000.0;
if distance_km > max_distance_km {
max_distance_km = distance_km;
}
pairwise_distances.push(PairwiseDistance {
file_a: name_a.clone(),
file_b: name_b.clone(),
distance_km,
});
}
}
let radius_km = max_distance_km / 2.0;
let area_km2 = PI * radius_km * radius_km;
Some(SpatialAnalysis {
max_distance_km,
area_km2,
centroid: centroid_coords,
point_count: coord_files.len(),
pairwise_distances,
})
}