diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6320cd2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +data \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..229880d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM docker.io/lukemathwalker/cargo-chef:latest-rust-1 AS chef +WORKDIR /app + +FROM chef AS planner +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + +FROM chef AS builder +RUN apt update && apt install -y clang libgdal-dev +RUN apt install -y libclang-dev +COPY --from=planner /app/recipe.json recipe.json +# Build dependencies - this is the caching Docker layer! +RUN cargo chef cook --release --recipe-path recipe.json +# Build application +COPY . . +RUN cargo build --release --bin api-server + +# We do not need the Rust toolchain to run the binary! +FROM debian:bookworm-slim AS runtime +RUN apt update && apt install -y libgdal-dev +WORKDIR /app +COPY --from=builder /app/target/release/api-server /usr/local/bin +ENTRYPOINT ["/usr/local/bin/api-server"] \ No newline at end of file diff --git a/src/dem.rs b/src/dem.rs index f4584eb..8410187 100644 --- a/src/dem.rs +++ b/src/dem.rs @@ -1,55 +1,47 @@ - - -use std::borrow::{Borrow, BorrowMut}; -use std::collections::HashMap; -use std::sync::{Arc}; +use std::sync::Arc; +use std::env; use gdal::errors::GdalError; use gdal::Dataset; -use gdal; -use tracing_subscriber::registry::Data; -use crate::{DSC}; +use tracing::{info, debug_span, debug}; -struct Pos { - lat: f64, - lon: f64, -} +use moka::future::Cache; pub struct MyDataset{pub ds: Dataset} unsafe impl Send for MyDataset{} unsafe impl Sync for MyDataset{} -#[derive(Default)] -pub struct DatasetCache { - hm: HashMap +pub type DSC = Cache>; +pub fn new_cache(max_elems: u64) -> DSC { + Cache::builder() + // Up to 10,000 entries. + .max_capacity(max_elems) + // Create the cache. + .build() } -impl DatasetCache { - pub fn new() -> Self { - let hm = HashMap::::new(); - DatasetCache { - hm - } +pub async fn elevation_from_coordinates(dsc: DSC, lat: f64, lon: f64) -> f64 { + let span = debug_span!("req", lat=%lat, lon=%lon); + let _guard = span.enter(); + + let filename = get_filename_from_latlon(lat, lon); + debug!(filename, "filename"); + + + if !dsc.contains_key(&filename) { + info!("Will open {filename} because not in cache!"); + let ds = Arc::new(MyDataset{ds:Dataset::open(filename.clone()).unwrap()}); + dsc.insert(filename.clone(), ds).await; } - pub fn get_dataset_for_filename(&mut self, filename: String) -> &MyDataset { - let ret: &MyDataset; + let ds = &dsc.get(&filename).await.unwrap().ds; + + let (px, py) = geo_to_pixel(ds, lat, lon).unwrap(); - if !self.hm.contains_key(&filename){ - let ds = Dataset::open(filename.clone()).unwrap(); - self.hm.insert(filename.clone(), MyDataset{ds: ds}); - } - - - self.hm.get(&filename).unwrap().to_owned() -/* - self.hm.entry(filename.clone()).or_insert_with(|| { - println!("{filename} not in cache!"); - Box::new(Dataset::open(filename).unwrap()) - }) - */ - } + let raster_band = ds.rasterband(1).unwrap(); + let raster_value = raster_band.read_as::((px, py), (1, 1), (1, 1), None).unwrap(); + raster_value.data[0] } @@ -66,9 +58,11 @@ fn get_filename_from_latlon(lat: f64, lon: f64) -> String { let lat_prefix = if rounded_lat >= 0.0 { "N" } else { "S" }; let lon_prefix = if rounded_lon >= 0.0 { "E" } else { "W" }; + + let filename_prefix = env::var("DEM_LOCATION").unwrap_or("/data".to_string()); // Construct the filename let filename = format!( - "Copernicus_DSM_30_{}{}_00_{}{}_00_DEM.tif", + "{filename_prefix}/Copernicus_DSM_30_{}{}_00_{}{}_00_DEM.tif", lat_prefix, lat_deg, lon_prefix, lon_deg ); @@ -82,30 +76,3 @@ fn geo_to_pixel(dataset: &Dataset, lat: f64, lon: f64) -> Result<(isize, isize), let y_pixel = ((lat - transform[3]) / transform[5]).round() as isize; Ok((x_pixel, y_pixel)) } - -pub async fn elevation_from_coordinates(dsc: DSC, lat: f64, lon: f64) -> f64 { - let file = get_filename_from_latlon(lat, lon); - let full_filename = format!("data/{file}"); - - println!("file for {lat} {lon} is {full_filename}"); - - - if !dsc.contains_key(&full_filename) { - println!(">>> WILL GET {full_filename} because not in cache!"); - let ds = Arc::new(MyDataset{ds:Dataset::open(full_filename.clone()).unwrap()}); - dsc.insert(full_filename.clone(), ds).await; - } - - let ds = &dsc.get(&full_filename).await.unwrap().ds; - - - println!("This {} is in '{}' and has {} bands.", ds.driver().long_name(), ds.spatial_ref().unwrap().name().unwrap(), ds.raster_count()); - println!("PRojection: {}", ds.projection()); - - - let (px, py) = geo_to_pixel(&ds, lat, lon).unwrap(); - - let raster_band = ds.rasterband(1).unwrap(); - let raster_value = raster_band.read_as::((px, py), (1, 1), (1, 1), None).unwrap(); - raster_value.data[0] -} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index b9c47a6..f4e9e2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,23 +2,16 @@ mod dem; -use std::sync::Arc; - +use std::env; use axum_macros::debug_handler; use axum::{ - extract::{Path, State}, http::StatusCode, routing::{get}, Router + extract::{Path, State}, routing::{get}, Router }; -use dem::{MyDataset}; +use dem::{DSC, new_cache}; -use moka::future::Cache; +use tracing::info; -//#[derive(Default)] -//struct AppState { -// db: RwLock>, -//} - -type DSC = Cache>; #[tokio::main(flavor = "current_thread")] async fn main() { @@ -26,29 +19,21 @@ async fn main() { tracing_subscriber::fmt::init(); - // Evict based on the number of entries in the cache. - let cache = Cache::builder() - // Up to 10,000 entries. - .max_capacity(10_000) - // Create the cache. - .build(); - //cache.insert("hello".to_string(), Arc::new(dem::MyDataset{ds: Dataset::open("oueou").unwrap()})).await; + let cache = new_cache(1000); // build our application with a route let app = Router::new() - // `GET /` goes to `root` - .route("/", get(root)) .route("/elevation/:lat/:lon", get(get_elevation)) .with_state(cache); - // run our app with hyper, listening globally on port 3000 - let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); - axum::serve(listener, app).await.unwrap(); -} -// basic handler that responds with a static string -async fn root() -> &'static str { - "Hello, World!" + let port = env::var("HTTP_PORT").unwrap_or("3000".to_string()); + let host = format!("[::]:{port}"); + info!("Will start server on {host}"); + + + let listener = tokio::net::TcpListener::bind(host).await.unwrap(); + axum::serve(listener, app).await.unwrap(); } @@ -57,6 +42,6 @@ async fn get_elevation(State(shared): State, Path((lat, lon)): Path<(f64, f let ele = dem::elevation_from_coordinates(shared, lat, lon); let myele = ele.await; - format!("{lat} {lon} {myele}") + format!("{myele}") }