diff --git a/Cargo.lock b/Cargo.lock index 5f03c8e..e938087 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,7 @@ dependencies = [ "moka", "serde", "serde_json", + "serde_qs", "tokio", "tower", "tower-http", @@ -361,6 +362,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -368,6 +384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -376,6 +393,23 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + [[package]] name = "futures-macro" version = "0.3.30" @@ -405,9 +439,13 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1078,6 +1116,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "axum", + "futures", + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 631d536..5a9822e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ gdal = { version = "0.16.0", features = ["bindgen"] } moka = { version = "0.12.5", features = ["future"] } serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.115" +serde_qs = { version = "0.13.0", features = ["axum"] } tokio = { version = "1.37.0", features = ["full"] } tower = "0.4.13" tower-http = { version = "0.5.2", features = ["fs", "trace"]} diff --git a/src/dem.rs b/src/dem.rs index c1daa61..a7f83c1 100644 --- a/src/dem.rs +++ b/src/dem.rs @@ -61,18 +61,18 @@ impl Clone for DatasetRepository { } } -pub async fn elevation_from_coordinates(dr: DatasetRepository, lat: f64, lon: f64) -> Option { +pub async fn elevation_from_coordinates(dr: &DatasetRepository, lat: f64, lon: f64) -> Option { let span = debug_span!("req", lat=%lat, lon=%lon); let _guard = span.enter(); let filename = get_filename_from_latlon(lat, lon); debug!(filename, "filename"); - let ds = &match dr.get(filename).await { Some(x) => x, None => return None, - }.ds; + } + .ds; let (px, py) = geo_to_pixel(ds, lat, lon).unwrap(); diff --git a/src/main.rs b/src/main.rs index aa44642..a9187a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,28 +1,68 @@ mod dem; use axum::{ - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, - routing::get, - Router, + extract::{Extension, Path, Query, State}, http::{response, StatusCode}, response::{IntoResponse, Response}, routing::get, Json, Router }; use axum_macros::debug_handler; -use std::env; +use std::{borrow::Borrow, fmt}; +use std::{env, str::FromStr}; +use serde::{Deserialize, Deserializer, Serialize}; +use tower_http::trace::TraceLayer; use tower_http::{ services::ServeDir, trace::{self, DefaultMakeSpan}, }; +use tracing::{info, Level, Span}; +use serde_qs::axum::{QsQuery, QsQueryConfig, QsQueryRejection}; + use dem::DatasetRepository; -use tower_http::trace::TraceLayer; -use tracing::{info, Level, Span}; - const DEFAULT_DATA_DIR: &str = "/data"; const DEFAULT_PORT: &str = "3000"; +#[derive(Deserialize, Debug)] +struct Opts { + #[serde(default, deserialize_with = "empty_string_as_none")] + json: bool, +} + +#[derive(Deserialize, Debug)] +struct JsParams{ + #[serde(default, deserialize_with = "deserialize_array")] + pts: Vec<(f64, f64)>, +} + +#[derive(Serialize, Debug)] +struct JsResult { + elevations: Vec>, +} + +fn deserialize_array<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + let result: Result, serde_json::Error> = serde_json::from_str(&s); + match result { + Ok(x) => Ok(x), + Err(e) => Err(serde::de::Error::custom("Invalid array: ".to_string() + &e.to_string())), + } +} + +fn empty_string_as_none<'de, D>(de: D) -> Result +where + D: Deserializer<'de>, +{ + let opt = Option::::deserialize(de)?; + match opt.as_deref() { + None => Ok(false), + Some("") => Ok(true), + Some(x) => Ok(x != "false"), + } +} + #[tokio::main] async fn main() { // initialize tracing @@ -33,15 +73,19 @@ async fn main() { let serve_dir = ServeDir::new("assets"); - let app = Router::new() + let app = Router::::new() .route("/elevation/:lat/:lon", get(get_elevation)) + .route("/ele", get(get_elevation_js)) .nest_service("/", serve_dir) .with_state(cache) .layer( TraceLayer::new_for_http() .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) .on_response(trace::DefaultOnResponse::new().level(Level::INFO)), - ); + ) + .layer(Extension(QsQueryConfig::new(5, false) + .error_handler(|err| { + QsQueryRejection::new(format!("Get fucked: {}", err), StatusCode::UNPROCESSABLE_ENTITY)}))); let host = format!("[::]:{}", config.port); info!("Will start server on {host}"); @@ -53,13 +97,48 @@ async fn main() { #[debug_handler] async fn get_elevation( State(dsr): State, + query_opts: Query, Path((lat, lon)): Path<(f64, f64)>, -) -> impl IntoResponse { - let ele = dem::elevation_from_coordinates(dsr, lat, lon); - match ele.await { - Some(ele) => (StatusCode::OK, format!("{ele}")), - None => (StatusCode::NOT_IMPLEMENTED, "".to_string()), +) -> Response { + println!("lat: {}, lon: {}", lat, lon); + println!("query_opts: {:?}", query_opts); + + let ele; + match dem::elevation_from_coordinates(&dsr, lat, lon).await { + //None => return (StatusCode::NOT_IMPLEMENTED, "".to_string()), + None => { + return "".to_string().into_response(); + } + Some(el) => ele = el, } + + #[derive(Serialize)] + struct Ele { + elevation: f64, + }; + + if query_opts.json { + let r = Ele { elevation: ele }; + return Json(r).into_response(); + } else { + return format!("{}", ele).into_response(); + } +} + + +#[debug_handler] +async fn get_elevation_js( + State(dsr): State, + Query(params): Query, +) -> Response { + let mut response = JsResult { + elevations: vec![], + }; + for pt in params.pts { + let ele = dem::elevation_from_coordinates(&dsr, pt.0, pt.1).await; + response.elevations.push(ele); + } + Json(response).into_response() } fn load_config() -> Result { @@ -68,7 +147,6 @@ fn load_config() -> Result { port: env::var("HTTP_PORT").unwrap_or_else(|_| DEFAULT_PORT.to_string()), }) } - struct Config { basedir: String, port: String,