diff --git a/Dockerfile b/Dockerfile index c8e5081..dd1f105 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh WORKDIR /app/frontend # Build frontend RUN wasm-pack build --target web --out-name wasm --out-dir ../backend/static +RUN cp index.html ../backend/static/index.html # Build Backend FROM rust:latest as backend-builder diff --git a/backend/src/main.rs b/backend/src/main.rs index 8beb86a..7a31211 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -104,7 +104,11 @@ async fn get_tile_ways( Path((z, x, y)): Path<(i32, i32, i32)>, State(state): State>, ) -> Result>, (axum::http::StatusCode, String)> { - let query = "SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ?"; + let query = if z < 9 { + "SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000" + } else { + "SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ?" + }; let rows = state.scylla_session.query(query, (z, x, y)) .await .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))? @@ -137,6 +141,12 @@ async fn get_tile_buildings( State(state): State>, ) -> Result>, (axum::http::StatusCode, String)> { let query = "SELECT id, tags, points FROM map_data.buildings WHERE zoom = ? AND tile_x = ? AND tile_y = ?"; + + // Optimization: Don't load buildings for low zoom levels + if z < 13 { + return Ok(Json(Vec::new())); + } + let rows = state.scylla_session.query(query, (z, x, y)) .await .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))? @@ -169,6 +179,12 @@ async fn get_tile_landuse( State(state): State>, ) -> Result>, (axum::http::StatusCode, String)> { println!("Request: get_tile_landuse({}, {}, {})", z, x, y); + + // Optimization: Don't load landuse for low zoom levels + if z < 11 { + return Ok(Json(Vec::new())); + } + let query = "SELECT id, tags, points FROM map_data.landuse WHERE zoom = ? AND tile_x = ? AND tile_y = ?"; println!("Executing query..."); let result = state.scylla_session.query(query, (z, x, y)).await; @@ -210,7 +226,11 @@ async fn get_tile_water( Path((z, x, y)): Path<(i32, i32, i32)>, State(state): State>, ) -> Result>, (axum::http::StatusCode, String)> { - let query = "SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ?"; + let query = if z < 9 { + "SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000" + } else { + "SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ?" + }; let rows = state.scylla_session.query(query, (z, x, y)) .await .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))? @@ -241,7 +261,11 @@ async fn get_tile_railways( Path((z, x, y)): Path<(i32, i32, i32)>, State(state): State>, ) -> Result>, (axum::http::StatusCode, String)> { - let query = "SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ?"; + let query = if z < 9 { + "SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000" + } else { + "SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ?" + }; let rows = state.scylla_session.query(query, (z, x, y)) .await .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))? diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..23ecbcd --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,107 @@ + + + + + + + Maps + + + + +
+
+ + +
+
+ + +
+ +
+ +
+ Zoom: -- | Pos: -- +
+ + + + + \ No newline at end of file diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index c046f99..bb6803d 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -192,7 +192,7 @@ fn create_road_mesh(points: &[[f64; 2]], width: f32) -> Vec { async fn fetch_cached(url: &str) -> Option { let window = web_sys::window()?; let caches = window.caches().ok()?; - let cache_name = "map-data-v1"; + let cache_name = "map-data-v2"; let cache = wasm_bindgen_futures::JsFuture::from(caches.open(cache_name)).await.ok()?; let cache: web_sys::Cache = cache.dyn_into().ok()?; @@ -839,7 +839,7 @@ pub async fn run() { let window_clone_for_fetch = window.clone(); wasm_bindgen_futures::spawn_local(async move { // Fetch nodes - let url_nodes = format!("http://localhost:3000/api/tiles/{}/{}/{}", z, x, y); + let url_nodes = format!("/api/tiles/{}/{}/{}", z, x, y); let nodes_data = if let Some(json) = fetch_cached(&url_nodes).await { serde_json::from_str::>(&json).ok() } else { @@ -847,7 +847,7 @@ pub async fn run() { }; // Fetch ways - let url_ways = format!("http://localhost:3000/api/tiles/{}/{}/{}/ways", z, x, y); + let url_ways = format!("/api/tiles/{}/{}/{}/ways", z, x, y); let ways_data = if let Some(json) = fetch_cached(&url_ways).await { serde_json::from_str::>(&json).ok() } else { @@ -855,7 +855,7 @@ pub async fn run() { }; // Fetch buildings - let url_buildings = format!("http://localhost:3000/api/tiles/{}/{}/{}/buildings", z, x, y); + let url_buildings = format!("/api/tiles/{}/{}/{}/buildings", z, x, y); let buildings_data = if let Some(json) = fetch_cached(&url_buildings).await { serde_json::from_str::>(&json).ok() } else { @@ -863,7 +863,7 @@ pub async fn run() { }; // Fetch landuse - let url_landuse = format!("http://localhost:3000/api/tiles/{}/{}/{}/landuse", z, x, y); + let url_landuse = format!("/api/tiles/{}/{}/{}/landuse", z, x, y); let landuse_data = if let Some(json) = fetch_cached(&url_landuse).await { serde_json::from_str::>(&json).ok() } else { @@ -871,7 +871,7 @@ pub async fn run() { }; // Fetch water - let url_water = format!("http://localhost:3000/api/tiles/{}/{}/{}/water", z, x, y); + let url_water = format!("/api/tiles/{}/{}/{}/water", z, x, y); let water_data = if let Some(json) = fetch_cached(&url_water).await { serde_json::from_str::>(&json).ok() } else { @@ -879,7 +879,7 @@ pub async fn run() { }; // Fetch railways - let url_railways = format!("http://localhost:3000/api/tiles/{}/{}/{}/railways", z, x, y); + let url_railways = format!("/api/tiles/{}/{}/{}/railways", z, x, y); let railways_data = if let Some(json) = fetch_cached(&url_railways).await { serde_json::from_str::>(&json).ok() } else { @@ -1099,6 +1099,10 @@ fn create_pipeline( let x = world_pos.x * camera.params.x + camera.params.z; let y = world_pos.y * camera.params.y + camera.params.w; + // Globe Effect: Spherize + // let r2 = x*x + y*y; + // let w = 1.0 + r2 * 0.5; + out.clip_position = vec4(x, y, 0.0, 1.0); return out; } @@ -1184,6 +1188,10 @@ fn create_road_pipeline( let x = world_pos.x * camera.params.x + camera.params.z; let y = world_pos.y * camera.params.y + camera.params.w; + // Globe Effect: Spherize + // let r2 = x*x + y*y; + // let w = 1.0 + r2 * 0.5; + out.clip_position = vec4(x, y, 0.0, 1.0); return out; } @@ -1268,6 +1276,10 @@ fn create_building_pipeline( let x = world_pos.x * camera.params.x + camera.params.z; let y = world_pos.y * camera.params.y + camera.params.w; + // Globe Effect: Spherize + // let r2 = x*x + y*y; + // let w = 1.0 + r2 * 0.5; + out.clip_position = vec4(x, y, 0.0, 1.0); return out; } @@ -1352,6 +1364,10 @@ fn create_landuse_pipeline( let x = world_pos.x * camera.params.x + camera.params.z; let y = world_pos.y * camera.params.y + camera.params.w; + // Globe Effect: Spherize + // let r2 = x*x + y*y; + // let w = 1.0 + r2 * 0.5; + out.clip_position = vec4(x, y, 0.0, 1.0); return out; } @@ -1446,6 +1462,10 @@ fn create_water_pipeline( let x = world_pos.x * camera.params.x + camera.params.z; let y = world_pos.y * camera.params.y + camera.params.w; + // Globe Effect: Spherize + // let r2 = x*x + y*y; + // let w = 1.0 + r2 * 0.5; + out.clip_position = vec4(x, y, 0.0, 1.0); return out; } @@ -1540,6 +1560,10 @@ fn create_railway_pipeline( let x = world_pos.x * camera.params.x + camera.params.z; let y = world_pos.y * camera.params.y + camera.params.w; + // Globe Effect: Spherize + // let r2 = x*x + y*y; + // let w = 1.0 + r2 * 0.5; + out.clip_position = vec4(x, y, 0.0, 1.0); return out; } diff --git a/importer/src/main.rs b/importer/src/main.rs index 6dfe6e1..6d3b860 100644 --- a/importer/src/main.rs +++ b/importer/src/main.rs @@ -8,7 +8,7 @@ use std::io::{BufWriter, Write, Seek, SeekFrom}; use std::path::{Path, PathBuf}; use memmap2::Mmap; -const ZOOM_LEVELS: [u32; 4] = [6, 9, 12, 14]; +const ZOOM_LEVELS: [u32; 5] = [2, 6, 9, 12, 14]; struct NodeStore { writer: Option>, @@ -80,6 +80,58 @@ impl NodeStore { } None } + +} + +// Ramer-Douglas-Peucker simplification +fn perpendicular_distance(p: (f64, f64), line_start: (f64, f64), line_end: (f64, f64)) -> f64 { + let (x, y) = p; + let (x1, y1) = line_start; + let (x2, y2) = line_end; + + let dx = x2 - x1; + let dy = y2 - y1; + + if dx == 0.0 && dy == 0.0 { + return ((x - x1).powi(2) + (y - y1).powi(2)).sqrt(); + } + + let num = (dy * x - dx * y + x2 * y1 - y2 * x1).abs(); + let den = (dx.powi(2) + dy.powi(2)).sqrt(); + + num / den +} + +fn simplify_points(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { + if points.len() < 3 { + return points.to_vec(); + } + + let start = points[0]; + let end = points[points.len() - 1]; + + let mut max_dist = 0.0; + let mut index = 0; + + for i in 1..points.len() - 1 { + let dist = perpendicular_distance(points[i], start, end); + if dist > max_dist { + max_dist = dist; + index = i; + } + } + + if max_dist > epsilon { + let mut left = simplify_points(&points[..=index], epsilon); + let mut right = simplify_points(&points[index..], epsilon); + + // Remove duplicate point at split + left.pop(); + left.extend(right); + left + } else { + vec![start, end] + } } fn should_include(tags: &HashMap, zoom: u32) -> bool { @@ -92,21 +144,28 @@ fn should_include(tags: &HashMap, zoom: u32) -> bool { let waterway = tags.get("waterway").map(|s| s.as_str()); match zoom { + match zoom { + 2 => { + // Space View: Continents and Countries + matches!(place, Some("continent" | "country")) || + matches!(natural, Some("water")) // Major water bodies + }, 6 => { - matches!(highway, Some("motorway" | "trunk" | "primary" | "secondary")) || + // Enterprise Grade: ONLY Motorways and Trunk roads. No primary/secondary. + // ONLY Cities. No nature/landuse. + matches!(highway, Some("motorway" | "trunk")) || matches!(place, Some("city")) || - matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland")) || - matches!(tags.get("landuse").map(|s| s.as_str()), Some("forest" | "meadow" | "grass" | "recreation_ground" | "farmland")) || - tags.contains_key("leisure") || // Parks, nature reserves, golf courses - matches!(waterway, Some("river" | "riverbank")) + matches!(natural, Some("water")) || // Only major water bodies + matches!(waterway, Some("river")) // Only major rivers }, 9 => { - matches!(highway, Some("motorway" | "trunk" | "primary" | "secondary")) || + // Enterprise Grade: Add Primary roads. + // Add Towns. + // Limited nature. + matches!(highway, Some("motorway" | "trunk" | "primary")) || matches!(place, Some("city" | "town")) || matches!(railway, Some("rail")) || - matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland")) || - tags.contains_key("landuse") || - tags.contains_key("leisure") || + matches!(natural, Some("water" | "wood")) || matches!(waterway, Some("river" | "riverbank")) }, 12 => { @@ -333,19 +392,36 @@ async fn main() -> Result<()> { // Insert into the tile of the first point let (first_lat, first_lon) = points[0]; - // Serialize points to blob (f64, f64) pairs - let mut blob = Vec::with_capacity(points.len() * 16); - for (lat, lon) in points { - blob.extend_from_slice(&lat.to_be_bytes()); - blob.extend_from_slice(&lon.to_be_bytes()); - } - for &zoom in &ZOOM_LEVELS { if !should_include(&tags, zoom) { continue; } + // Apply simplification based on zoom level + let epsilon = match zoom { + 2 => 0.1, // Very high simplification + 6 => 0.005, // High simplification + 9 => 0.001, // Medium simplification + _ => 0.0, // No simplification + }; + + let simplified_points = if epsilon > 0.0 { + simplify_points(&points, epsilon) + } else { + points.clone() + }; + + if simplified_points.len() < 2 { continue; } + + let (first_lat, first_lon) = simplified_points[0]; let (x, y) = lat_lon_to_tile(first_lat, first_lon, zoom); let zoom_i32 = zoom as i32; + // Serialize simplified points + let mut blob = Vec::with_capacity(simplified_points.len() * 16); + for (lat, lon) in simplified_points { + blob.extend_from_slice(&lat.to_be_bytes()); + blob.extend_from_slice(&lon.to_be_bytes()); + } + if is_highway { let task = DbTask::Way { zoom: zoom_i32, table: "ways", id, tags: tags.clone(), points: blob.clone(), x, y }; let _ = tx.blocking_send(task); diff --git a/run-remote.sh b/run-remote.sh index 3d99aa7..cc62224 100644 --- a/run-remote.sh +++ b/run-remote.sh @@ -6,4 +6,3 @@ fi echo "Using PBF file: ${HOST_PBF_PATH:-./europe-latest.osm.pbf}" docker compose -f docker-compose-remote.yml --profile import up --build importer -docker compose -f docker-compose-remote.yml build --no-cache