pub mod middleware;
use crate::config::Config;
use crate::error::AppError;
use crate::handlers;
use axum::Router;
use duckdb::Connection;
use serde::Serialize;
use std::sync::{Arc, Mutex, RwLock};
use std::time::Instant;
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
#[derive(Clone)]
pub struct AppState {
#[allow(dead_code)]
pub config: Arc<Config>,
pub db: Arc<Mutex<Connection>>,
pub snapshot: Arc<RwLock<Arc<Snapshot>>>,
pub start_time: Instant,
}
impl AppState {
pub fn snapshot(&self) -> Arc<Snapshot> {
self.snapshot
.read()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
}
pub type BBox = (f64, f64, f64, f64);
pub struct Snapshot {
pub schema: TableSchema,
pub index_build_ms: u128,
pub spatial_bbox: Option<BBox>,
pub spatial_count: i64,
}
#[derive(Debug, Serialize)]
pub struct TableSchema {
pub source: String,
pub row_count: i64,
pub lat_col: String,
pub lon_col: String,
pub columns: Vec<ColumnMeta>,
}
#[derive(Debug, Serialize)]
pub struct ColumnMeta {
pub name: String,
pub dtype: String,
}
pub fn build_router(state: AppState) -> Router {
Router::new()
.route("/health", axum::routing::get(handlers::meta::health))
.route("/schema", axum::routing::get(handlers::meta::schema))
.route("/stats", axum::routing::get(handlers::meta::stats))
.route("/query", axum::routing::get(handlers::query::query))
.route(
"/query/within",
axum::routing::post(handlers::within::within),
)
.route(
"/geometry/convex-hull",
axum::routing::post(handlers::geometry::convex_hull),
)
.route(
"/geometry/area",
axum::routing::post(handlers::geometry::area),
)
.route(
"/geometry/centroid",
axum::routing::post(handlers::geometry::centroid),
)
.route(
"/geometry/buffer",
axum::routing::post(handlers::geometry::buffer),
)
.route(
"/geometry/dissolve",
axum::routing::post(handlers::geometry::dissolve),
)
.route(
"/geometry/simplify",
axum::routing::post(handlers::geometry::simplify),
)
.route(
"/geometry/distance",
axum::routing::post(handlers::geometry::distance),
)
.route(
"/geometry/bounds",
axum::routing::post(handlers::geometry::bounds),
)
.layer(CorsLayer::permissive())
.layer(TraceLayer::new_for_http())
.with_state(state)
}
pub fn build_snapshot(
conn: &Connection,
source: &str,
lat_col: &str,
lon_col: &str,
row_count: i64,
index_build_ms: u128,
) -> Result<Snapshot, AppError> {
let info = crate::db::get_table_info_relation(conn, "data")?;
let (spatial_bbox, spatial_count) = compute_extent(conn, lat_col, lon_col)?;
let schema = TableSchema {
source: source.to_string(),
row_count,
lat_col: lat_col.to_string(),
lon_col: lon_col.to_string(),
columns: info
.col_names
.iter()
.zip(info.col_types.iter())
.map(|(name, dtype)| ColumnMeta {
name: name.clone(),
dtype: dtype.clone(),
})
.collect(),
};
Ok(Snapshot {
schema,
index_build_ms,
spatial_bbox,
spatial_count,
})
}
fn compute_extent(
conn: &Connection,
lat_col: &str,
lon_col: &str,
) -> Result<(Option<BBox>, i64), AppError> {
let mut stmt = conn.prepare(&format!(
"SELECT MIN(\"{lat}\"), MIN(\"{lon}\"), MAX(\"{lat}\"), MAX(\"{lon}\"), COUNT(*) FROM raw_data WHERE \"{lat}\" IS NOT NULL AND \"{lon}\" IS NOT NULL",
lat = lat_col,
lon = lon_col,
))?;
let result: (Option<f64>, Option<f64>, Option<f64>, Option<f64>, i64) =
stmt.query_row([], |row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
))
})?;
let bbox = match result {
(Some(min_lat), Some(min_lon), Some(max_lat), Some(max_lon), _) => {
Some((min_lat, min_lon, max_lat, max_lon))
}
_ => None,
};
Ok((bbox, result.4))
}
pub async fn serve<A: tokio::net::ToSocketAddrs>(
state: AppState,
addr: A,
shutdown: impl std::future::Future<Output = ()> + Send + 'static,
) -> std::io::Result<()> {
let app = build_router(state);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app)
.with_graceful_shutdown(shutdown)
.await
}