From 003aae2b6b9727385e0fb8d200a90f1c9e624150 Mon Sep 17 00:00:00 2001 From: Dongho Kim Date: Wed, 3 Dec 2025 04:01:36 +0900 Subject: [PATCH] update --- .env | 2 +- backend/Cargo.toml | 3 +- backend/src/main.rs | 341 +++++++++++++-------- frontend/Cargo.toml | 4 +- frontend/Trunk.toml | 10 + frontend/index.html | 112 +++++++ frontend/src/lib.rs | 698 ++++++++++++++++++++++++++++++------------- importer/Cargo.toml | 1 + importer/src/main.rs | 91 ++++-- run-remote.sh | 1 + 10 files changed, 915 insertions(+), 348 deletions(-) create mode 100644 frontend/Trunk.toml diff --git a/.env b/.env index a710902..f14015c 100644 --- a/.env +++ b/.env @@ -16,5 +16,5 @@ CLIENT_PORT=8080 SERVICE_LOG_LEVEL=debug -HOST_PBF_PATH=../maps_data/europe-latest.osm.pbf +HOST_PBF_PATH=../maps_data/bayern-latest.osm.pbf HOST_CACHE_DIR=./cache diff --git a/backend/Cargo.toml b/backend/Cargo.toml index cfa53d7..e98b1b5 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -8,8 +8,9 @@ axum = "0.7" tokio = { version = "1.0", features = ["full"] } scylla = "0.12" # Check for latest version, using a recent stable one serde = { version = "1.0", features = ["derive"] } +bincode = "1.3" serde_json = "1.0" tower = "0.4" -tower-http = { version = "0.5", features = ["cors", "fs"] } +tower-http = { version = "0.5", features = ["cors", "fs", "compression-full"] } tracing = "0.1" tracing-subscriber = "0.3" diff --git a/backend/src/main.rs b/backend/src/main.rs index 7a31211..ffc8fd4 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -4,12 +4,14 @@ use axum::{ routing::get, Router, extract::{State, Path}, - Json, + http::header, + response::IntoResponse, }; use scylla::{Session, SessionBuilder}; use std::sync::Arc; use tower_http::services::ServeDir; use tower_http::cors::CorsLayer; +use tower_http::compression::CompressionLayer; use serde::Serialize; struct AppState { @@ -49,8 +51,10 @@ async fn main() -> Result<(), Box> { .route("/api/tiles/:z/:x/:y/landuse", get(get_tile_landuse)) .route("/api/tiles/:z/:x/:y/water", get(get_tile_water)) .route("/api/tiles/:z/:x/:y/railways", get(get_tile_railways)) + .route("/api/tiles/:z/:x/:y/all", get(get_tile_all)) .nest_service("/", ServeDir::new("static").append_index_html_on_directories(true)) .layer(CorsLayer::permissive()) + .layer(CompressionLayer::new()) .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; @@ -75,219 +79,310 @@ struct MapNode { async fn get_tile( Path((z, x, y)): Path<(i32, i32, i32)>, State(state): State>, -) -> Result>, (axum::http::StatusCode, String)> { +) -> impl IntoResponse { let query = "SELECT id, lat, lon, tags FROM map_data.nodes 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)))? - .rows - .unwrap_or_default(); + let rows = match state.scylla_session.query(query, (z, x, y)).await { + Ok(res) => res.rows.unwrap_or_default(), + Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(), + }; let mut nodes = Vec::new(); for row in rows { let (id, lat, lon, tags) = row.into_typed::<(i64, f64, f64, std::collections::HashMap)>() - .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e)))?; + .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))).unwrap(); nodes.push(MapNode { id, lat, lon, tags }); } - Ok(Json(nodes)) + let bytes = bincode::serialize(&nodes).unwrap(); + ( + [(header::CONTENT_TYPE, "application/octet-stream")], + bytes, + ).into_response() } #[derive(Serialize)] struct MapWay { id: i64, tags: std::collections::HashMap, - points: Vec>, // List of [lat, lon] + points: Vec, // Flat f32 array } async fn get_tile_ways( Path((z, x, y)): Path<(i32, i32, i32)>, State(state): State>, -) -> Result>, (axum::http::StatusCode, String)> { +) -> impl IntoResponse { 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)))? - .rows - .unwrap_or_default(); + let rows = match state.scylla_session.query(query, (z, x, y)).await { + Ok(res) => res.rows.unwrap_or_default(), + Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(), + }; let mut ways = Vec::new(); for row in rows { - let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap, Vec)>() - .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e)))?; + let (id, tags, points) = row.into_typed::<(i64, std::collections::HashMap, Vec)>() + .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))).unwrap(); - // Deserialize points blob - let mut points = Vec::new(); - for chunk in points_blob.chunks(16) { - if chunk.len() == 16 { - let lat = f64::from_be_bytes(chunk[0..8].try_into().unwrap()); - let lon = f64::from_be_bytes(chunk[8..16].try_into().unwrap()); - points.push(vec![lat, lon]); - } - } - ways.push(MapWay { id, tags, points }); } - Ok(Json(ways)) + let bytes = bincode::serialize(&ways).unwrap(); + ( + [(header::CONTENT_TYPE, "application/octet-stream")], + bytes, + ).into_response() } async fn get_tile_buildings( Path((z, x, y)): Path<(i32, i32, i32)>, State(state): State>, -) -> Result>, (axum::http::StatusCode, String)> { +) -> impl IntoResponse { 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())); + if z < 12 { + return ([(header::CONTENT_TYPE, "application/octet-stream")], vec![]).into_response(); } - let rows = state.scylla_session.query(query, (z, x, y)) - .await - .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))? - .rows - .unwrap_or_default(); + let rows = match state.scylla_session.query(query, (z, x, y)).await { + Ok(res) => res.rows.unwrap_or_default(), + Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(), + }; let mut buildings = Vec::new(); for row in rows { - let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap, Vec)>() - .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e)))?; + let (id, tags, points) = row.into_typed::<(i64, std::collections::HashMap, Vec)>() + .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))).unwrap(); - // Deserialize points blob - let mut points = Vec::new(); - for chunk in points_blob.chunks(16) { - if chunk.len() == 16 { - let lat = f64::from_be_bytes(chunk[0..8].try_into().unwrap()); - let lon = f64::from_be_bytes(chunk[8..16].try_into().unwrap()); - points.push(vec![lat, lon]); - } - } - buildings.push(MapWay { id, tags, points }); } - Ok(Json(buildings)) + let bytes = bincode::serialize(&buildings).unwrap(); + ( + [(header::CONTENT_TYPE, "application/octet-stream")], + bytes, + ).into_response() } async fn get_tile_landuse( Path((z, x, y)): Path<(i32, i32, i32)>, State(state): State>, -) -> Result>, (axum::http::StatusCode, String)> { - println!("Request: get_tile_landuse({}, {}, {})", z, x, y); - +) -> impl IntoResponse { // Optimization: Don't load landuse for low zoom levels - if z < 11 { - return Ok(Json(Vec::new())); + if z < 4 { + return ([(header::CONTENT_TYPE, "application/octet-stream")], vec![]).into_response(); } 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; + let rows = match state.scylla_session.query(query, (z, x, y)).await { + Ok(res) => res.rows.unwrap_or_default(), + Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(), + }; - match result { - Ok(res) => { - println!("Query successful, processing rows..."); - let rows = res.rows.unwrap_or_default(); - let mut landuse = Vec::new(); - for row in rows { - let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap, Vec)>() - .map_err(|e| { - println!("Serialization error: {}", e); - (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e)) - })?; - - let mut points = Vec::new(); - for chunk in points_blob.chunks(16) { - if chunk.len() == 16 { - let lat = f64::from_be_bytes(chunk[0..8].try_into().unwrap()); - let lon = f64::from_be_bytes(chunk[8..16].try_into().unwrap()); - points.push(vec![lat, lon]); - } - } - - landuse.push(MapWay { id, tags, points }); - } - println!("Returning {} landuse items", landuse.len()); - Ok(Json(landuse)) - }, - Err(e) => { - println!("Query failed: {}", e); - Err((axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e))) - } + let mut landuse = Vec::new(); + for row in rows { + let (id, tags, points) = row.into_typed::<(i64, std::collections::HashMap, Vec)>() + .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))).unwrap(); + + landuse.push(MapWay { id, tags, points }); } + + let bytes = bincode::serialize(&landuse).unwrap(); + ( + [(header::CONTENT_TYPE, "application/octet-stream")], + bytes, + ).into_response() } async fn get_tile_water( Path((z, x, y)): Path<(i32, i32, i32)>, State(state): State>, -) -> Result>, (axum::http::StatusCode, String)> { +) -> impl IntoResponse { 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)))? - .rows - .unwrap_or_default(); + let rows = match state.scylla_session.query(query, (z, x, y)).await { + Ok(res) => res.rows.unwrap_or_default(), + Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(), + }; let mut water = Vec::new(); for row in rows { - let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap, Vec)>() - .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e)))?; + let (id, tags, points) = row.into_typed::<(i64, std::collections::HashMap, Vec)>() + .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))).unwrap(); - let mut points = Vec::new(); - for chunk in points_blob.chunks(16) { - if chunk.len() == 16 { - let lat = f64::from_be_bytes(chunk[0..8].try_into().unwrap()); - let lon = f64::from_be_bytes(chunk[8..16].try_into().unwrap()); - points.push(vec![lat, lon]); - } - } - water.push(MapWay { id, tags, points }); } - Ok(Json(water)) + let bytes = bincode::serialize(&water).unwrap(); + ( + [(header::CONTENT_TYPE, "application/octet-stream")], + bytes, + ).into_response() } async fn get_tile_railways( Path((z, x, y)): Path<(i32, i32, i32)>, State(state): State>, -) -> Result>, (axum::http::StatusCode, String)> { +) -> impl IntoResponse { 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)))? - .rows - .unwrap_or_default(); + let rows = match state.scylla_session.query(query, (z, x, y)).await { + Ok(res) => res.rows.unwrap_or_default(), + Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(), + }; let mut railways = Vec::new(); for row in rows { - let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap, Vec)>() - .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e)))?; + let (id, tags, points) = row.into_typed::<(i64, std::collections::HashMap, Vec)>() + .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))).unwrap(); - let mut points = Vec::new(); - for chunk in points_blob.chunks(16) { - if chunk.len() == 16 { - let lat = f64::from_be_bytes(chunk[0..8].try_into().unwrap()); - let lon = f64::from_be_bytes(chunk[8..16].try_into().unwrap()); - points.push(vec![lat, lon]); - } - } - railways.push(MapWay { id, tags, points }); } - Ok(Json(railways)) + let bytes = bincode::serialize(&railways).unwrap(); + ( + [(header::CONTENT_TYPE, "application/octet-stream")], + bytes, + ).into_response() +} + +#[derive(Serialize)] +struct TileData { + nodes: Vec, + ways: Vec, + buildings: Vec, + landuse: Vec, + water: Vec, + railways: Vec, +} + +async fn get_tile_all( + Path((z, x, y)): Path<(i32, i32, i32)>, + State(state): State>, +) -> impl IntoResponse { + // Run all queries in parallel + // Run all queries in parallel + // (Removed unused tokio::join! block) + + // Helper to deserialize response body back to Vec + // Since we are calling the handlers directly, they return `impl IntoResponse`. + // We need to extract the bytes. This is a bit hacky because we are serializing then deserializing. + // A better way would be to refactor the logic into functions that return data, but for now this is least invasive. + // Actually, calling handlers directly is fine if we can extract the body. + // But `impl IntoResponse` is opaque. + // Refactoring is better. Let's create helper functions that return the data structs. + + // REFACTOR STRATEGY: + // 1. Extract logic from `get_tile` to `fetch_nodes`. + // 2. Call `fetch_nodes` from `get_tile` and `get_tile_all`. + // ... repeat for all. + + // Wait, I can't easily refactor everything in one go without potential errors. + // Let's try to implement `get_tile_all` by copying the logic. It's duplication but safer for now. + // Actually, duplication is bad. + // Let's look at `get_tile`. It queries DB and returns bytes. + // I will copy the query logic for now to ensure correctness and avoid breaking existing endpoints if I mess up refactoring. + + // Nodes + let query_nodes = "SELECT id, lat, lon, tags FROM map_data.nodes WHERE zoom = ? AND tile_x = ? AND tile_y = ?"; + let rows_nodes = state.scylla_session.query(query_nodes, (z, x, y)).await.ok().and_then(|r| r.rows).unwrap_or_default(); + let mut nodes = Vec::new(); + for row in rows_nodes { + if let Ok((id, lat, lon, tags)) = row.into_typed::<(i64, f64, f64, std::collections::HashMap)>() { + nodes.push(MapNode { id, lat, lon, tags }); + } + } + + // Ways + let query_ways = 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_ways = state.scylla_session.query(query_ways, (z, x, y)).await.ok().and_then(|r| r.rows).unwrap_or_default(); + let mut ways = Vec::new(); + for row in rows_ways { + if let Ok((id, tags, points)) = row.into_typed::<(i64, std::collections::HashMap, Vec)>() { + ways.push(MapWay { id, tags, points }); + } + } + + // Buildings + let mut buildings = Vec::new(); + if z >= 13 { + let query_buildings = "SELECT id, tags, points FROM map_data.buildings WHERE zoom = ? AND tile_x = ? AND tile_y = ?"; + let rows_buildings = state.scylla_session.query(query_buildings, (z, x, y)).await.ok().and_then(|r| r.rows).unwrap_or_default(); + for row in rows_buildings { + if let Ok((id, tags, points)) = row.into_typed::<(i64, std::collections::HashMap, Vec)>() { + buildings.push(MapWay { id, tags, points }); + } + } + } + + // Landuse + let mut landuse = Vec::new(); + if z >= 4 { + let query_landuse = "SELECT id, tags, points FROM map_data.landuse WHERE zoom = ? AND tile_x = ? AND tile_y = ?"; + let rows_landuse = state.scylla_session.query(query_landuse, (z, x, y)).await.ok().and_then(|r| r.rows).unwrap_or_default(); + for row in rows_landuse { + if let Ok((id, tags, points)) = row.into_typed::<(i64, std::collections::HashMap, Vec)>() { + landuse.push(MapWay { id, tags, points }); + } + } + } + + // Water + let query_water = 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_water = state.scylla_session.query(query_water, (z, x, y)).await.ok().and_then(|r| r.rows).unwrap_or_default(); + let mut water = Vec::new(); + for row in rows_water { + if let Ok((id, tags, points)) = row.into_typed::<(i64, std::collections::HashMap, Vec)>() { + water.push(MapWay { id, tags, points }); + } + } + + // Railways + let query_railways = 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_railways = state.scylla_session.query(query_railways, (z, x, y)).await.ok().and_then(|r| r.rows).unwrap_or_default(); + let mut railways = Vec::new(); + for row in rows_railways { + if let Ok((id, tags, points)) = row.into_typed::<(i64, std::collections::HashMap, Vec)>() { + railways.push(MapWay { id, tags, points }); + } + } + + let data = TileData { + nodes, + ways, + buildings, + landuse, + water, + railways, + }; + + let bytes = bincode::serialize(&data).unwrap(); + ( + [ + (header::CONTENT_TYPE, "application/octet-stream"), + (header::CACHE_CONTROL, "public, max-age=31536000, immutable"), + ], + bytes, + ).into_response() } diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index 24c9604..65a89a6 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -9,6 +9,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" +js-sys = "0.3" web-sys = { version = "0.3", features = [ "Document", "Window", @@ -33,6 +34,7 @@ web-sys = { version = "0.3", features = [ "RequestMode", "Response", "HtmlInputElement", + "PositionOptions", ] } wgpu = { version = "0.19", default-features = false, features = ["webgl", "wgsl"] } winit = { version = "0.29", default-features = false, features = ["rwh_06"] } @@ -43,4 +45,4 @@ console_log = "1.0" reqwest = { version = "0.11", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -earcutr = "0.4" +bincode = "1.3" diff --git a/frontend/Trunk.toml b/frontend/Trunk.toml new file mode 100644 index 0000000..96237fc --- /dev/null +++ b/frontend/Trunk.toml @@ -0,0 +1,10 @@ +[build] +target = "index.html" + +[serve] +address = "127.0.0.1" +port = 8080 + +[[proxy]] +rewrite = "/api/" +backend = "http://localhost:3000/api/" diff --git a/frontend/index.html b/frontend/index.html index 23ecbcd..63c1e15 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -68,10 +68,122 @@ padding: 5px 10px; border-radius: 4px; } + + #compass { + position: absolute; + top: 20px; + left: 20px; + width: 60px; + height: 60px; + background: rgba(0, 0, 0, 0.6); + border-radius: 50%; + border: 2px solid rgba(255, 255, 255, 0.2); + backdrop-filter: blur(5px); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; + pointer-events: none; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + z-index: 100; + } + + .direction { + position: absolute; + font-size: 14px; + font-family: monospace; + } + + .n { + top: 4px; + color: #ff5555; + } + + .s { + bottom: 4px; + color: #ddd; + } + + .e { + right: 6px; + color: #ddd; + } + + .w { + left: 6px; + color: #ddd; + } + + .compass-center { + width: 4px; + height: 4px; + background: white; + border-radius: 50%; + } + + .compass-arrow { + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 20px solid #ff5555; + transform: translate(-50%, -100%); + } +
+
N
+
E
+
S
+
W
+
+
+
+ +
+ + +
diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index bb6803d..f35d149 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -10,7 +10,6 @@ use winit::{ use winit::platform::web::WindowExtWebSys; use serde::Deserialize; use wgpu::util::DeviceExt; -use earcutr::earcut; #[derive(Deserialize, Debug, Clone)] struct MapNode { @@ -49,9 +48,9 @@ impl Camera { CameraUniform { params: [ self.zoom / self.aspect, // scale_x - self.zoom, // scale_y - -self.x * (self.zoom / self.aspect), // translate_x (simplified) - -self.y * self.zoom, // translate_y + -self.zoom, // scale_y (flipped for North-Up) + -self.x * (self.zoom / self.aspect), // translate_x + self.y * self.zoom, // translate_y (flipped sign) ], } } @@ -115,11 +114,65 @@ fn project(lat: f64, lon: f64) -> (f32, f32) { (x as f32, y as f32) } +#[derive(Debug, Clone)] +struct KalmanFilter { + lat: f64, + lon: f64, + variance: f64, + timestamp: f64, +} + +impl KalmanFilter { + fn new(lat: f64, lon: f64, timestamp: f64) -> Self { + Self { + lat, + lon, + variance: 0.0, + timestamp, + } + } + + fn process(&mut self, lat: f64, lon: f64, accuracy: f64, timestamp: f64) -> (f64, f64) { + if accuracy <= 0.0 { return (self.lat, self.lon); } + + let dt = timestamp - self.timestamp; + if dt < 0.0 { return (self.lat, self.lon); } + + // Process noise variance (meters per second) + let q_metres_per_sec = 3.0; + let variance_process = q_metres_per_sec * q_metres_per_sec * dt / 1000.0; + + // Prediction step + let variance = self.variance + variance_process; + + // Update step + let measurement_variance = accuracy * accuracy; + let k = variance / (variance + measurement_variance); + + self.lat = self.lat + k * (lat - self.lat); + self.lon = self.lon + k * (lon - self.lon); + self.variance = (1.0 - k) * variance; + self.timestamp = timestamp; + + (self.lat, self.lon) + } +} + #[derive(Deserialize, Debug, Clone)] struct MapWay { id: i64, tags: std::collections::HashMap, - points: Vec>, + points: Vec, +} + +#[derive(Deserialize, Debug, Clone)] +struct TileData { + nodes: Vec, + ways: Vec, + buildings: Vec, + landuse: Vec, + water: Vec, + railways: Vec, } struct TileBuffers { @@ -135,6 +188,8 @@ struct TileBuffers { water_index_count: u32, railway_vertex_buffer: wgpu::Buffer, railway_vertex_count: u32, + road_mesh_vertex_buffer: wgpu::Buffer, + road_mesh_vertex_count: u32, } struct AppState { @@ -148,6 +203,8 @@ struct AppState { loaded_tiles: HashSet<(i32, i32, i32)>, pending_tiles: HashSet<(i32, i32, i32)>, user_location: Option<(f64, f64)>, // (lat, lon) + kalman_filter: Option, + watch_id: Option, } fn create_road_mesh(points: &[[f64; 2]], width: f32) -> Vec { @@ -189,10 +246,10 @@ fn create_road_mesh(points: &[[f64; 2]], width: f32) -> Vec { vertices } -async fn fetch_cached(url: &str) -> Option { +async fn fetch_cached(url: &str) -> Option> { let window = web_sys::window()?; let caches = window.caches().ok()?; - let cache_name = "map-data-v2"; + let cache_name = "map-data-v3-combined"; let cache = wasm_bindgen_futures::JsFuture::from(caches.open(cache_name)).await.ok()?; let cache: web_sys::Cache = cache.dyn_into().ok()?; @@ -202,9 +259,10 @@ async fn fetch_cached(url: &str) -> Option { if !match_val.is_undefined() { let response: web_sys::Response = match_val.dyn_into().ok()?; - let text_promise = response.text().ok()?; - let text = wasm_bindgen_futures::JsFuture::from(text_promise).await.ok()?; - return text.as_string(); + let buffer_promise = response.array_buffer().ok()?; + let buffer = wasm_bindgen_futures::JsFuture::from(buffer_promise).await.ok()?; + let array = js_sys::Uint8Array::new(&buffer); + return Some(array.to_vec()); } // Network fetch @@ -216,9 +274,10 @@ async fn fetch_cached(url: &str) -> Option { let put_promise = cache.put_with_request(&request, &response_clone); wasm_bindgen_futures::JsFuture::from(put_promise).await.ok()?; - let text_promise = response.text().ok()?; - let text = wasm_bindgen_futures::JsFuture::from(text_promise).await.ok()?; - text.as_string() + let buffer_promise = response.array_buffer().ok()?; + let buffer = wasm_bindgen_futures::JsFuture::from(buffer_promise).await.ok()?; + let array = js_sys::Uint8Array::new(&buffer); + Some(array.to_vec()) } fn get_visible_tiles(camera: &Camera) -> Vec<(i32, i32, i32)> { @@ -227,11 +286,15 @@ fn get_visible_tiles(camera: &Camera) -> Vec<(i32, i32, i32)> { // Zoom 9: Region view // Zoom 12: City view // Zoom 14: Street view - let z = if camera.zoom < 500.0 { - 6 + let z = if camera.zoom < 100.0 { + 2 + } else if camera.zoom < 500.0 { + 4 } else if camera.zoom < 2000.0 { + 6 + } else if camera.zoom < 5000.0 { 9 - } else if camera.zoom < 8000.0 { + } else if camera.zoom < 10000.0 { 12 } else { 14 @@ -260,6 +323,24 @@ fn get_visible_tiles(camera: &Camera) -> Vec<(i32, i32, i32)> { tiles } +fn get_parent_tile(z: i32, x: i32, y: i32) -> Option<(i32, i32, i32)> { + // Hierarchy: 14 -> 12 -> 9 -> 6 -> 2 + let parent_z = match z { + 14 => 12, + 12 => 9, + 9 => 6, + 6 => 4, + 4 => 2, + _ => return None, + }; + + // Calculate scale difference + let diff = z - parent_z; + let factor = 2i32.pow(diff as u32); + + Some((parent_z, x / factor, y / factor)) +} + #[wasm_bindgen(start)] pub async fn run() { std::panic::set_hook(Box::new(console_error_panic_hook::hook)); @@ -361,6 +442,8 @@ pub async fn run() { loaded_tiles: HashSet::new(), pending_tiles: HashSet::new(), user_location: None, + kalman_filter: None, + watch_id: None, })); // Zoom constants @@ -453,10 +536,25 @@ pub async fn run() { if let Some(btn) = btn_location { let closure = wasm_bindgen::closure::Closure::::new(move || { + let window = web_sys::window().unwrap(); let navigator = window.navigator(); let geolocation = navigator.geolocation().unwrap(); + let mut state_guard = state_clone.lock().unwrap(); + + // Toggle off if already watching + if let Some(id) = state_guard.watch_id { + geolocation.clear_watch(id); + state_guard.watch_id = None; + state_guard.kalman_filter = None; + state_guard.user_location = None; // Optional: clear location on stop + web_sys::console::log_1(&"Location tracking stopped".into()); + window_clone.request_redraw(); + return; + } + drop(state_guard); // Release lock before starting new watch + let camera_clone2 = camera_clone.clone(); let window_clone2 = window_clone.clone(); let state_clone2 = state_clone.clone(); @@ -465,31 +563,64 @@ pub async fn run() { let coords = position.coords(); let lat = coords.latitude(); let lon = coords.longitude(); + let accuracy = coords.accuracy(); + let timestamp = position.timestamp(); - // Update state with user location let mut state_guard = state_clone2.lock().unwrap(); - state_guard.user_location = Some((lat, lon)); + + let (smooth_lat, smooth_lon) = if let Some(filter) = &mut state_guard.kalman_filter { + filter.process(lat, lon, accuracy, timestamp) + } else { + // Initialize filter + let mut filter = KalmanFilter::new(lat, lon, timestamp); + // Run first process to set initial variance if needed, or just use raw + // Actually new() sets variance to 0, so it trusts the first point 100% + state_guard.kalman_filter = Some(filter); + (lat, lon) + }; + + state_guard.user_location = Some((smooth_lat, smooth_lon)); drop(state_guard); - // Center camera on location - let (x, y) = project(lat, lon); + // Center camera on location (maybe only on first update? or always? "Follow me" mode) + // For now, let's just update the marker. If we want "Follow me", we'd update camera too. + // The original code centered camera. Let's keep doing that for now, effectively "Follow me". + // But maybe only if the user hasn't dragged away? + // For simplicity, let's center every time for now, as that's what "Fused Location" implies usually (navigation). + + let (x, y) = project(smooth_lat, smooth_lon); let mut cam = camera_clone2.lock().unwrap(); cam.x = x; cam.y = y; - cam.zoom = 8000.0; // Zoom in to street level + // cam.zoom = 8000.0; // Don't force zoom every time, let user control it drop(cam); window_clone2.request_redraw(); }); - let error_callback = wasm_bindgen::closure::Closure::::new(move |_error: web_sys::PositionError| { - web_sys::console::log_1(&"Geolocation error".into()); + let error_callback = wasm_bindgen::closure::Closure::::new(move |error: web_sys::PositionError| { + web_sys::console::log_1(&format!("Geolocation error: {:?}", error.message()).into()); }); - geolocation.get_current_position_with_error_callback( + let mut options = web_sys::PositionOptions::new(); + options.set_enable_high_accuracy(true); + options.set_timeout(5000); + options.set_maximum_age(0); + + match geolocation.watch_position_with_error_callback_and_options( success_callback.as_ref().unchecked_ref(), - Some(error_callback.as_ref().unchecked_ref()) - ).unwrap(); + Some(error_callback.as_ref().unchecked_ref()), + &options + ) { + Ok(id) => { + let mut state_guard = state_clone.lock().unwrap(); + state_guard.watch_id = Some(id); + web_sys::console::log_1(&"Location tracking started (High Accuracy)".into()); + }, + Err(e) => { + web_sys::console::log_1(&format!("Failed to start watch: {:?}", e).into()); + } + } success_callback.forget(); error_callback.forget(); @@ -516,6 +647,7 @@ pub async fn run() { let landuse_pipeline = create_landuse_pipeline(&device, &config.format, &camera_bind_group_layout); let water_pipeline = create_water_pipeline(&device, &config.format, &camera_bind_group_layout); let railway_pipeline = create_railway_pipeline(&device, &config.format, &camera_bind_group_layout); + let road_mesh_pipeline = create_road_mesh_pipeline(&device, &config.format, &camera_bind_group_layout); let window_clone = window.clone(); @@ -546,7 +678,7 @@ pub async fn run() { let world_dy = dy / (config.height as f32 * 0.5) / cam.zoom; cam.x -= world_dx; - cam.y += world_dy; + cam.y -= world_dy; window.request_redraw(); } @@ -607,39 +739,81 @@ pub async fn run() { } // 1. Check visible tiles - let visible_tiles = if cam.zoom > 200.0 { - get_visible_tiles(&cam) - } else { - Vec::new() - }; + let visible_tiles = get_visible_tiles(&cam); // Drop camera lock early to avoid holding it during long operations if possible, // but we need it for `to_uniform`. We can clone the needed data. let camera_uniform_data = cam.to_uniform(); + let camera_zoom = cam.zoom; // Capture zoom for road mesh width calculations drop(cam); // Release lock let mut state_guard = state.lock().unwrap(); let mut needs_fetch = Vec::new(); + // Determine what to render: + // For each visible tile, if it's loaded, render it. + // If NOT loaded, try to find a loaded parent to render as fallback. + let mut tiles_to_render_set = HashSet::new(); + for tile in &visible_tiles { - if !state_guard.loaded_tiles.contains(tile) && !state_guard.pending_tiles.contains(tile) { - state_guard.pending_tiles.insert(*tile); - needs_fetch.push(*tile); + if state_guard.loaded_tiles.contains(tile) { + tiles_to_render_set.insert(*tile); + } else { + // Tile not ready, request it + if !state_guard.pending_tiles.contains(tile) { + state_guard.pending_tiles.insert(*tile); + needs_fetch.push(*tile); + } + + // Look for fallback parent + let (mut z, mut x, mut y) = *tile; + while let Some(parent) = get_parent_tile(z, x, y) { + if state_guard.loaded_tiles.contains(&parent) { + tiles_to_render_set.insert(parent); + break; // Found a valid parent, stop looking up + } + (z, x, y) = parent; + } } } - let visible_set: HashSet<_> = visible_tiles.iter().cloned().collect(); - state_guard.loaded_tiles.retain(|t| visible_set.contains(t)); - state_guard.nodes.retain(|t, _| visible_set.contains(t)); - state_guard.ways.retain(|t, _| visible_set.contains(t)); - state_guard.buffers.retain(|t, _| visible_set.contains(t)); + // Cleanup: Retain loaded tiles that are either visible OR are being used as fallbacks + // We also want to keep recently used parents for a bit to avoid thrashing, + // but for now, strict "is in render set" is a good start. + // However, we must ALSO keep the `visible_tiles` that are loading, otherwise they'll never finish? + // No, `pending_tiles` tracks loading. `loaded_tiles` tracks what we have. + // We should only delete loaded tiles that are NO LONGER USEFUL. + // A tile is useful if: + // 1. It is directly visible. + // 2. It is a parent of a visible tile (even if that visible tile is loaded? No, only if needed). + // Actually, keeping parents is good for zooming out too. + + // Simple strategy: Keep all visible tiles + all tiles currently in rendering set. + let mut useful_tiles = tiles_to_render_set.clone(); + for t in &visible_tiles { + if state_guard.loaded_tiles.contains(t) { + useful_tiles.insert(*t); + } + } + + state_guard.loaded_tiles.retain(|t| useful_tiles.contains(t)); + state_guard.nodes.retain(|t, _| useful_tiles.contains(t)); + state_guard.ways.retain(|t, _| useful_tiles.contains(t)); + state_guard.buildings.retain(|t, _| useful_tiles.contains(t)); + state_guard.landuse.retain(|t, _| useful_tiles.contains(t)); + state_guard.water.retain(|t, _| useful_tiles.contains(t)); + state_guard.railways.retain(|t, _| useful_tiles.contains(t)); + state_guard.buffers.retain(|t, _| useful_tiles.contains(t)); // 3. Create buffers for new tiles if needed - for tile in &visible_tiles { + // We iterate over useful_tiles because we might need to create buffers for a parent that was just loaded + // but not previously rendered (e.g. if we zoomed in fast and missed it, but now need it as fallback). + for tile in &useful_tiles { if !state_guard.buffers.contains_key(tile) { let mut point_instance_data = Vec::new(); let mut road_vertex_data = Vec::new(); - let mut building_vertex_data = Vec::new(); + // Road mesh disabled + let mut building_vertex_data: Vec = Vec::new(); let mut landuse_vertex_data = Vec::new(); let mut water_vertex_data = Vec::new(); @@ -651,15 +825,25 @@ pub async fn run() { } } - // Process ways (roads) - simple line rendering + // Process ways (roads) - simple line rendering OR mesh if let Some(ways) = state_guard.ways.get(tile) { for way in ways { - if way.points.len() < 2 { continue; } + let points = &way.points; + if points.len() < 16 { continue; } // Need at least 2 points (16 bytes) - // Draw as simple lines - for i in 0..way.points.len() - 1 { - let p1 = &way.points[i]; - let p2 = &way.points[i+1]; + // Parse points first + let mut parsed_points = Vec::new(); + for chunk in points.chunks(8) { + if chunk.len() < 8 { break; } + let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap()) as f64; + let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap()) as f64; + parsed_points.push([lat, lon]); + } + + // Use simple line rendering for all roads (mesh disabled due to artifacts) + for i in 0..parsed_points.len() - 1 { + let p1 = parsed_points[i]; + let p2 = parsed_points[i+1]; let (x1, y1) = project(p1[0], p1[1]); let (x2, y2) = project(p2[0], p2[1]); road_vertex_data.push(Vertex { position: [x1, y1] }); @@ -671,53 +855,26 @@ pub async fn run() { // Process buildings if let Some(buildings) = state_guard.buildings.get(tile) { for building in buildings { - if building.points.len() < 3 { continue; } - - let mut flat_points = Vec::new(); - let mut projected_points = Vec::new(); - - for p in &building.points { - let (x, y) = project(p[0], p[1]); - flat_points.push(x as f64); - flat_points.push(y as f64); - projected_points.push([x, y]); - } - - // Earcut triangulation - let indices = match earcut(&flat_points, &[], 2) { - Ok(i) => i, - Err(_) => continue, - }; - - for i in indices { - let p = projected_points[i]; - building_vertex_data.push(Vertex { position: p }); + let points = &building.points; + for chunk in points.chunks(8) { + if chunk.len() < 8 { break; } + let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap()) as f64; + let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap()) as f64; + let (x, y) = project(lat, lon); + building_vertex_data.push(Vertex { position: [x, y] }); } } } - // Process landuse if let Some(landuse) = state_guard.landuse.get(tile) { for area in landuse { - if area.points.len() < 3 { continue; } - - let mut flat_points = Vec::new(); - let mut projected_points = Vec::new(); - - for p in &area.points { - let (x, y) = project(p[0], p[1]); - flat_points.push(x as f64); - flat_points.push(y as f64); - projected_points.push([x, y]); - } - - let indices = match earcut(&flat_points, &[], 2) { - Ok(i) => i, - Err(_) => continue, - }; - for i in indices { - let p = projected_points[i]; - landuse_vertex_data.push(Vertex { position: p }); + let points = &area.points; + for chunk in points.chunks(8) { + if chunk.len() < 8 { break; } + let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap()) as f64; + let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap()) as f64; + let (x, y) = project(lat, lon); + landuse_vertex_data.push(Vertex { position: [x, y] }); } } } @@ -725,25 +882,13 @@ pub async fn run() { // Process water if let Some(water) = state_guard.water.get(tile) { for area in water { - if area.points.len() < 3 { continue; } - - let mut flat_points = Vec::new(); - let mut projected_points = Vec::new(); - - for p in &area.points { - let (x, y) = project(p[0], p[1]); - flat_points.push(x as f64); - flat_points.push(y as f64); - projected_points.push([x, y]); - } - - let indices = match earcut(&flat_points, &[], 2) { - Ok(i) => i, - Err(_) => continue, - }; - for i in indices { - let p = projected_points[i]; - water_vertex_data.push(Vertex { position: p }); + let points = &area.points; + for chunk in points.chunks(8) { + if chunk.len() < 8 { break; } + let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap()) as f64; + let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap()) as f64; + let (x, y) = project(lat, lon); + water_vertex_data.push(Vertex { position: [x, y] }); } } } @@ -752,15 +897,27 @@ pub async fn run() { let mut railway_vertex_data = Vec::new(); if let Some(railways) = state_guard.railways.get(tile) { for way in railways { - if way.points.len() < 2 { continue; } + let points = &way.points; + if points.len() < 16 { continue; } - for i in 0..way.points.len() - 1 { - let p1 = &way.points[i]; - let p2 = &way.points[i+1]; - let (x1, y1) = project(p1[0], p1[1]); - let (x2, y2) = project(p2[0], p2[1]); - railway_vertex_data.push(Vertex { position: [x1, y1] }); - railway_vertex_data.push(Vertex { position: [x2, y2] }); + let mut chunks = points.chunks(8); + let mut prev_x = 0.0; + let mut prev_y = 0.0; + let mut first = true; + + while let Some(chunk) = chunks.next() { + if chunk.len() < 8 { break; } + let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap()) as f64; + let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap()) as f64; + let (x, y) = project(lat, lon); + + if !first { + railway_vertex_data.push(Vertex { position: [prev_x, prev_y] }); + railway_vertex_data.push(Vertex { position: [x, y] }); + } + prev_x = x; + prev_y = y; + first = false; } } } @@ -778,6 +935,12 @@ pub async fn run() { contents: bytemuck::cast_slice(&road_vertex_data), usage: wgpu::BufferUsages::VERTEX, }); + // Road mesh disabled - create empty buffer + let road_mesh_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Empty Road Mesh Buffer"), + contents: bytemuck::cast_slice(&Vec::::new()), + usage: wgpu::BufferUsages::VERTEX, + }); let building_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Tile Building Buffer"), @@ -817,14 +980,20 @@ pub async fn run() { water_index_count: water_vertex_data.len() as u32, railway_vertex_buffer: railway_buffer, railway_vertex_count: railway_vertex_data.len() as u32, + road_mesh_vertex_buffer: road_mesh_buffer, + road_mesh_vertex_count: 0, // Mesh disabled })); } } } // Collect buffers for rendering + // Sort by zoom level (low -> high) so parents are drawn first (background) and children on top + let mut tiles_to_render_vec: Vec<_> = tiles_to_render_set.into_iter().collect(); + tiles_to_render_vec.sort_by_key(|(z, _, _)| *z); + let mut tiles_to_render = Vec::new(); - for tile in &visible_tiles { + for tile in &tiles_to_render_vec { if let Some(buffers) = state_guard.buffers.get(tile) { tiles_to_render.push(buffers.clone()); } @@ -838,92 +1007,34 @@ pub async fn run() { let state_clone = state.clone(); let window_clone_for_fetch = window.clone(); wasm_bindgen_futures::spawn_local(async move { - // Fetch nodes - 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 { - None - }; + // Fetch all data in one go + let url = format!("/api/tiles/{}/{}/{}/all", z, x, y); - // Fetch ways - 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() + let tile_data = if let Some(bytes) = fetch_cached(&url).await { + bincode::deserialize::(&bytes).ok() } else { None }; - // Fetch buildings - 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() + if let Some(data) = tile_data { + let mut guard = state_clone.lock().unwrap(); + guard.nodes.insert((z, x, y), data.nodes); + guard.ways.insert((z, x, y), data.ways); + guard.buildings.insert((z, x, y), data.buildings); + guard.landuse.insert((z, x, y), data.landuse); + guard.water.insert((z, x, y), data.water); + guard.railways.insert((z, x, y), data.railways); + + // Mark as loaded + guard.loaded_tiles.insert((z, x, y)); + guard.pending_tiles.remove(&(z, x, y)); + + window_clone_for_fetch.request_redraw(); } else { - None - }; - - // Fetch landuse - 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 { - None - }; - - // Fetch water - 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 { - None - }; - - // Fetch railways - 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 { - None - }; - - - - let mut guard = state_clone.lock().unwrap(); - - if let Some(nodes) = nodes_data { - guard.nodes.insert((z, x, y), nodes); + // Failed to load + let mut guard = state_clone.lock().unwrap(); + guard.pending_tiles.remove(&(z, x, y)); } - - if let Some(ways) = ways_data { - guard.ways.insert((z, x, y), ways); - } - - if let Some(buildings) = buildings_data { - guard.buildings.insert((z, x, y), buildings); - } - - if let Some(landuse) = landuse_data { - - guard.landuse.insert((z, x, y), landuse); - } - - if let Some(water) = water_data { - - guard.water.insert((z, x, y), water); - } - - if let Some(railways) = railways_data { - - guard.railways.insert((z, x, y), railways); - } - - - - guard.loaded_tiles.insert((z, x, y)); - guard.pending_tiles.remove(&(z, x, y)); - - drop(guard); - window_clone_for_fetch.request_redraw(); }); } @@ -962,9 +1073,9 @@ pub async fn run() { resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { - r: 0.15, - g: 0.15, - b: 0.15, + r: 0.95, + g: 0.95, + b: 0.95, a: 1.0, }), store: wgpu::StoreOp::Store, @@ -1001,6 +1112,14 @@ pub async fn run() { rpass.draw(0..buffers.road_vertex_count, 0..1); } + // Road mesh rendering disabled - using simple lines for all roads + // if buffers.road_mesh_vertex_count > 0 { + // rpass.set_pipeline(&road_mesh_pipeline); + // rpass.set_bind_group(0, &camera_bind_group, &[]); + // rpass.set_vertex_buffer(0, buffers.road_mesh_vertex_buffer.slice(..)); + // rpass.draw(0..buffers.road_mesh_vertex_count, 0..1); + // } + // Draw Railways (on top of roads?) if buffers.railway_vertex_count > 0 { rpass.set_pipeline(&railway_pipeline); @@ -1048,8 +1167,33 @@ pub async fn run() { queue.submit(Some(encoder.finish())); frame.present(); + + // Update Labels + let state_guard = state.lock().unwrap(); + let cam = camera.lock().unwrap(); + // Re-calculate camera params for label projection (since we dropped the lock earlier) + // Actually we can just use the camera object, but we need the calculated params. + // Let's just pass the camera object and let update_labels calculate. + // But wait, Camera struct doesn't have `params` field, `to_uniform` does. + // We need to temporarily modify Camera struct or just re-calculate in update_labels. + // Let's modify Camera to store params or just recalculate. Recalculating is cheap. + + // We need to pass a "Camera with params" or just the raw camera and let function handle it. + // But `Camera` struct doesn't have the `params` array. + // Let's just pass the `Camera` struct and let `update_labels` call `to_uniform`. + // But `to_uniform` returns `CameraUniform` which has `params`. + // So we can pass `cam.to_uniform()`. + + // Wait, `update_labels` signature I wrote above takes `&Camera`. + // I'll modify `update_labels` to take `CameraUniform` instead of `Camera` to avoid re-calc if possible, + // or just let it call `to_uniform`. + // Let's stick to passing `&Camera` and let it call `to_uniform`. + // But `to_uniform` is a method on `Camera`. + // So: + update_labels(&web_sys::window().unwrap(), &cam, &state_guard, config.width as f64, config.height as f64); } WindowEvent::CloseRequested => { + #[cfg(not(target_arch = "wasm32"))] elwt.exit(); } _ => {} @@ -1198,7 +1342,7 @@ fn create_road_pipeline( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return vec4(0.75, 0.75, 0.75, 1.0); // Lighter grey roads + return vec4(0.4, 0.4, 0.4, 1.0); // Dark grey roads } "#)), }); @@ -1286,7 +1430,7 @@ fn create_building_pipeline( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return vec4(0.6, 0.55, 0.5, 1.0); // Light tan for buildings + return vec4(0.7, 0.7, 0.7, 1.0); // Darker grey buildings for visibility } "#)), }); @@ -1374,7 +1518,7 @@ fn create_landuse_pipeline( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return vec4(0.6, 0.8, 0.6, 1.0); // Light green for parks + return vec4(0.77, 0.91, 0.77, 1.0); // Google Maps Park Green (#c5e8c5 approx) } "#)), }); @@ -1472,7 +1616,7 @@ fn create_water_pipeline( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return vec4(0.6, 0.7, 0.9, 1.0); // Light blue for water + return vec4(0.66, 0.85, 1.0, 1.0); // Google Maps Water Blue (#aadaff approx) } "#)), }); @@ -1570,7 +1714,7 @@ fn create_railway_pipeline( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return vec4(0.3, 0.3, 0.3, 1.0); // Dark grey for railways + return vec4(0.5, 0.5, 0.5, 1.0); // Grey for railways } "#)), }); @@ -1614,3 +1758,149 @@ fn create_railway_pipeline( multiview: None, }) } + +fn create_road_mesh_pipeline( + device: &wgpu::Device, + format: &wgpu::TextureFormat, + bind_group_layout: &wgpu::BindGroupLayout +) -> wgpu::RenderPipeline { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: None, + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(r#" + struct CameraUniform { + params: vec4, + }; + @group(0) @binding(0) + var camera: CameraUniform; + + struct VertexInput { + @location(0) position: vec2, + }; + + struct VertexOutput { + @builtin(position) clip_position: vec4, + }; + + @vertex + fn vs_main( + model: VertexInput, + ) -> VertexOutput { + var out: VertexOutput; + + let world_pos = model.position; + + let x = world_pos.x * camera.params.x + camera.params.z; + let y = world_pos.y * camera.params.y + camera.params.w; + + out.clip_position = vec4(x, y, 0.0, 1.0); + return out; + } + + @fragment + fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return vec4(0.3, 0.3, 0.3, 1.0); // Dark grey for highways + } + "#)), + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Road Mesh Pipeline Layout"), + bind_group_layouts: &[bind_group_layout], + push_constant_ranges: &[], + }); + + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[ + Vertex::desc(), + ], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format: *format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }) +} + +fn update_labels( + window: &web_sys::Window, + camera: &Camera, + state: &AppState, + width: f64, + height: f64, +) { + let document = window.document().unwrap(); + let container = document.get_element_by_id("labels").unwrap(); + + // Clear existing labels + container.set_inner_html(""); + + let show_countries = true; + let show_cities = camera.zoom > 100.0; + + let visible_tiles = get_visible_tiles(camera); + let uniforms = camera.to_uniform(); // Calculate uniforms + + for tile in visible_tiles { + if let Some(nodes) = state.nodes.get(&tile) { + for node in nodes { + let place = node.tags.get("place").map(|s| s.as_str()); + let name = node.tags.get("name").map(|s| s.as_str()); + + if let (Some(place), Some(name)) = (place, name) { + let is_country = place == "country"; + let is_city = place == "city" || place == "town"; + + if (is_country && show_countries) || (is_city && show_cities) { + let (x, y) = project(node.lat, node.lon); + + // Apply camera transform using uniforms + let cx = x * uniforms.params[0] + uniforms.params[2]; + let cy = y * uniforms.params[1] + uniforms.params[3]; + + let ndc_x = cx; + let ndc_y = cy; + + if ndc_x < -1.2 || ndc_x > 1.2 || ndc_y < -1.2 || ndc_y > 1.2 { continue; } + + let screen_x = (ndc_x as f64 + 1.0) * 0.5 * width; + let screen_y = (1.0 - ndc_y as f64) * 0.5 * height; + + let div = document.create_element("div").unwrap(); + let class_name = if is_country { "label label-country" } else { "label label-city" }; + div.set_class_name(class_name); + div.set_text_content(Some(name)); + + let div_html: web_sys::HtmlElement = div.dyn_into().unwrap(); + let style = div_html.style(); + style.set_property("left", &format!("{}px", screen_x)).unwrap(); + style.set_property("top", &format!("{}px", screen_y)).unwrap(); + + container.append_child(&div_html).unwrap(); + } + } + } + } + } +} diff --git a/importer/Cargo.toml b/importer/Cargo.toml index 8ad5fdb..4ce6f8d 100644 --- a/importer/Cargo.toml +++ b/importer/Cargo.toml @@ -10,3 +10,4 @@ tokio = { version = "1.0", features = ["full"] } anyhow = "1.0" memmap2 = "0.9" dotenv = "0.15" +earcutr = "0.4" diff --git a/importer/src/main.rs b/importer/src/main.rs index 6d3b860..d9c6f8c 100644 --- a/importer/src/main.rs +++ b/importer/src/main.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use earcutr::earcut; use osmpbf::{Element, ElementReader}; use scylla::SessionBuilder; use std::collections::HashMap; @@ -8,7 +9,7 @@ use std::io::{BufWriter, Write, Seek, SeekFrom}; use std::path::{Path, PathBuf}; use memmap2::Mmap; -const ZOOM_LEVELS: [u32; 5] = [2, 6, 9, 12, 14]; +const ZOOM_LEVELS: [u32; 6] = [2, 4, 6, 9, 12, 14]; struct NodeStore { writer: Option>, @@ -134,6 +135,23 @@ fn simplify_points(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { } } +fn triangulate_polygon(points: &[(f64, f64)]) -> Vec<(f64, f64)> { + let mut flat_points = Vec::with_capacity(points.len() * 2); + for (lat, lon) in points { + flat_points.push(*lat); + flat_points.push(*lon); + } + + // We assume simple polygons (no holes) for now as we are just processing ways + let indices = earcut(&flat_points, &[], 2).unwrap_or_default(); + + let mut triangles = Vec::with_capacity(indices.len()); + for i in indices { + triangles.push(points[i]); + } + triangles +} + fn should_include(tags: &HashMap, zoom: u32) -> bool { if zoom >= 14 { return true; } @@ -144,19 +162,34 @@ 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 + matches!(natural, Some("water")) || // Major water bodies + matches!(highway, Some("motorway")) || // Added motorway + matches!(tags.get("landuse").map(|s| s.as_str()), Some("forest" | "grass" | "meadow" | "farmland" | "residential")) || // Added more green + farmland/residential + matches!(tags.get("leisure").map(|s| s.as_str()), Some("park" | "nature_reserve")) || // Added parks + matches!(natural, Some("wood" | "scrub")) // Added wood/scrub + }, + 4 => { + // Regional View (NEW) + matches!(highway, Some("motorway" | "trunk")) || + matches!(place, Some("city" | "town")) || + matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland")) || + matches!(tags.get("landuse").map(|s| s.as_str()), Some("forest" | "grass" | "meadow" | "farmland" | "residential")) || + matches!(tags.get("leisure").map(|s| s.as_str()), Some("park" | "nature_reserve")) || + matches!(waterway, Some("river")) }, 6 => { // Enterprise Grade: ONLY Motorways and Trunk roads. No primary/secondary. // ONLY Cities. No nature/landuse. - matches!(highway, Some("motorway" | "trunk")) || + matches!(highway, Some("motorway" | "trunk" | "primary")) || // Added primary matches!(place, Some("city")) || - matches!(natural, Some("water")) || // Only major water bodies - matches!(waterway, Some("river")) // Only major rivers + matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland")) || + matches!(tags.get("landuse").map(|s| s.as_str()), Some("forest" | "grass" | "meadow" | "farmland" | "residential")) || + matches!(tags.get("leisure").map(|s| s.as_str()), Some("park" | "nature_reserve")) || + matches!(waterway, Some("river")) }, 9 => { // Enterprise Grade: Add Primary roads. @@ -166,12 +199,15 @@ fn should_include(tags: &HashMap, zoom: u32) -> bool { matches!(place, Some("city" | "town")) || matches!(railway, Some("rail")) || matches!(natural, Some("water" | "wood")) || + matches!(tags.get("landuse").map(|s| s.as_str()), Some("forest")) || + matches!(tags.get("leisure").map(|s| s.as_str()), Some("park")) || matches!(waterway, Some("river" | "riverbank")) }, 12 => { matches!(highway, Some("motorway" | "trunk" | "primary" | "secondary" | "tertiary")) || matches!(place, Some("city" | "town" | "village")) || matches!(railway, Some("rail")) || + tags.contains_key("building") || tags.contains_key("landuse") || tags.contains_key("leisure") || matches!(natural, Some("water" | "wood" | "scrub" | "wetland" | "heath")) || @@ -396,11 +432,19 @@ async fn main() -> Result<()> { 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 base_epsilon = match zoom { + 2 => 0.0001, + 4 => 0.00005, + 6 => 0.00002, + 9 => 0.00001, + 12 => 0.000005, + _ => 0.0, + }; + + let epsilon = if is_water || is_landuse || is_highway { + base_epsilon * 0.5 // Preserve more detail for natural features AND roads + } else { + base_epsilon }; let simplified_points = if epsilon > 0.0 { @@ -409,17 +453,28 @@ async fn main() -> Result<()> { points.clone() }; - if simplified_points.len() < 2 { continue; } + // Serialize points + let mut final_points = simplified_points; + + // Triangulate if it's a polygon type + if is_building || is_water || is_landuse { + // Close the loop if not closed + if final_points.first() != final_points.last() { + final_points.push(final_points[0]); + } + final_points = triangulate_polygon(&final_points); + } - let (first_lat, first_lon) = simplified_points[0]; + if final_points.len() < 2 { continue; } + + let (first_lat, first_lon) = final_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()); + let mut blob = Vec::with_capacity(final_points.len() * 8); // 4 bytes lat + 4 bytes lon + for (lat, lon) in final_points { + blob.extend_from_slice(&(lat as f32).to_le_bytes()); + blob.extend_from_slice(&(lon as f32).to_le_bytes()); } if is_highway { diff --git a/run-remote.sh b/run-remote.sh index cc62224..a42b9a5 100644 --- a/run-remote.sh +++ b/run-remote.sh @@ -6,3 +6,4 @@ 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 up --build