From 169997eecd59f91d4d94e50b4723e1624bb42308 Mon Sep 17 00:00:00 2001 From: Dongho Kim Date: Sun, 14 Dec 2025 02:25:03 +0900 Subject: [PATCH] update --- .env | 2 +- Dockerfile | 10 +- backend/src/db.rs | 3 +- frontend/index.html | 26 + frontend/src/lib.rs | 1112 ++++++++++++++++++++++++++---------------- importer/src/main.rs | 64 ++- 6 files changed, 756 insertions(+), 461 deletions(-) diff --git a/.env b/.env index a710902..ed88a0e 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/oberbayern-latest.osm.pbf HOST_CACHE_DIR=./cache diff --git a/Dockerfile b/Dockerfile index dd1f105..dcfa34c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build Frontend -FROM rust:latest as frontend-builder +FROM rust:latest AS frontend-builder WORKDIR /app COPY frontend ./frontend COPY backend ./backend @@ -11,7 +11,7 @@ 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 +FROM rust:latest AS backend-builder WORKDIR /app COPY backend ./backend COPY --from=frontend-builder /app/backend/static ./backend/static @@ -19,14 +19,14 @@ WORKDIR /app/backend RUN cargo build --release # Build Importer -FROM rust:latest as importer-builder +FROM rust:latest AS importer-builder WORKDIR /app COPY importer ./importer WORKDIR /app/importer RUN cargo build --release # Backend Runtime -FROM debian:forky-slim as backend +FROM debian:forky-slim AS backend WORKDIR /app COPY --from=backend-builder /app/backend/target/release/backend ./backend COPY --from=frontend-builder /app/backend/static ./static @@ -37,7 +37,7 @@ EXPOSE 3000 CMD ["./backend"] # Importer Runtime -FROM debian:forky-slim as importer +FROM debian:forky-slim AS importer WORKDIR /app COPY --from=importer-builder /app/importer/target/release/importer ./importer # Install ca-certificates for HTTPS if needed diff --git a/backend/src/db.rs b/backend/src/db.rs index 9f0c4ae..464d9c4 100644 --- a/backend/src/db.rs +++ b/backend/src/db.rs @@ -1,5 +1,4 @@ -use scylla::{Session, SessionBuilder}; -use std::sync::Arc; +use scylla::Session; pub async fn initialize_schema(session: &Session) -> Result<(), Box> { // Create keyspace diff --git a/frontend/index.html b/frontend/index.html index 2108788..ee12835 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -183,6 +183,32 @@ font-weight: 600; color: #333; } + + /* Apple Maps-style street labels */ + .label-street { + font-size: 10px; + font-weight: 400; + color: #555; + letter-spacing: 0.3px; + text-shadow: + 0 0 2px rgba(255, 255, 255, 0.9), + 0 0 4px rgba(255, 255, 255, 0.8), + 1px 1px 1px rgba(255, 255, 255, 0.95); + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif; + } + + /* Apple Maps-style POI labels */ + .label-poi { + font-size: 11px; + font-weight: 500; + color: #c44; + letter-spacing: 0.2px; + text-shadow: + 0 0 3px rgba(255, 255, 255, 0.95), + 0 0 5px rgba(255, 255, 255, 0.9), + 1px 1px 2px rgba(255, 255, 255, 1); + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif; + }
diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index 43ad3c3..7f1edf4 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -11,6 +11,7 @@ use winit::platform::web::WindowExtWebSys; use serde::Deserialize; use wgpu::util::DeviceExt; +#[allow(dead_code)] #[derive(Deserialize, Debug, Clone)] struct MapNode { id: i64, @@ -19,6 +20,21 @@ struct MapNode { tags: std::collections::HashMap, } +struct AppState { + nodes: HashMap<(i32, i32, i32), Vec>, + ways: HashMap<(i32, i32, i32), Vec>, + buildings: HashMap<(i32, i32, i32), Vec>, + landuse: HashMap<(i32, i32, i32), Vec>, + water: HashMap<(i32, i32, i32), Vec>, + railways: HashMap<(i32, i32, i32), Vec>, + buffers: HashMap<(i32, i32, i32), std::sync::Arc>, + loaded_tiles: HashSet<(i32, i32, i32)>, + pending_tiles: HashSet<(i32, i32, i32)>, + user_location: Option<(f64, f64)>, + kalman_filter: Option, + watch_id: Option, +} + #[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] struct CameraUniform { @@ -111,7 +127,12 @@ fn project(lat: f64, lon: f64) -> (f32, f32) { let x = (lon + 180.0) / 360.0; let lat_rad = lat.to_radians(); let y = (1.0 - (lat_rad.tan() + (1.0 / lat_rad.cos())).ln() / std::f64::consts::PI) / 2.0; - (x as f32, y as f32) + + // Validate results - clamp to valid range and handle NaN/Infinity + let x = if x.is_finite() { (x as f32).clamp(0.0, 1.0) } else { 0.5 }; + let y = if y.is_finite() { (y as f32).clamp(0.0, 1.0) } else { 0.5 }; + + (x, y) } #[derive(Debug, Clone)] @@ -158,6 +179,7 @@ impl KalmanFilter { } } +#[allow(dead_code)] #[derive(Deserialize, Debug, Clone)] struct MapWay { id: i64, @@ -165,6 +187,7 @@ struct MapWay { points: Vec, } +#[allow(dead_code)] #[derive(Deserialize, Debug, Clone)] struct TileData { nodes: Vec, @@ -175,38 +198,37 @@ struct TileData { railways: Vec, } +#[allow(dead_code)] struct TileBuffers { point_instance_buffer: wgpu::Buffer, point_count: u32, - road_vertex_buffer: wgpu::Buffer, - road_vertex_count: u32, + + // Road Buffers + road_motorway_vertex_buffer: wgpu::Buffer, + road_motorway_vertex_count: u32, + road_primary_vertex_buffer: wgpu::Buffer, + road_primary_vertex_count: u32, + road_secondary_vertex_buffer: wgpu::Buffer, + road_secondary_vertex_count: u32, + road_residential_vertex_buffer: wgpu::Buffer, + road_residential_vertex_count: u32, + building_vertex_buffer: wgpu::Buffer, building_index_count: u32, - landuse_vertex_buffer: wgpu::Buffer, - landuse_index_count: u32, + + // Landuse Buffers + landuse_green_vertex_buffer: wgpu::Buffer, + landuse_green_index_count: u32, + landuse_residential_vertex_buffer: wgpu::Buffer, + landuse_residential_index_count: u32, + water_vertex_buffer: wgpu::Buffer, 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 { - nodes: HashMap<(i32, i32, i32), Vec>, - ways: HashMap<(i32, i32, i32), Vec>, - buildings: HashMap<(i32, i32, i32), Vec>, - landuse: HashMap<(i32, i32, i32), Vec>, - water: HashMap<(i32, i32, i32), Vec>, - railways: HashMap<(i32, i32, i32), Vec>, - buffers: HashMap<(i32, i32, i32), std::sync::Arc>, - 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, } +// Helper to create road mesh (Thick Lines) fn create_road_mesh(points: &[[f64; 2]], width: f32) -> Vec { let mut vertices = Vec::new(); if points.len() < 2 { return vertices; } @@ -215,6 +237,7 @@ fn create_road_mesh(points: &[[f64; 2]], width: f32) -> Vec { let p1 = points[i]; let p2 = points[i+1]; + // Convert to projected coordinates (0..1) let (x1, y1) = project(p1[0], p1[1]); let (x2, y2) = project(p2[0], p2[1]); @@ -222,12 +245,23 @@ fn create_road_mesh(points: &[[f64; 2]], width: f32) -> Vec { let dy = y2 - y1; let len = (dx * dx + dy * dy).sqrt(); - if len == 0.0 { continue; } + // Skip invalid segments: + // 1. Very short segments that would create degenerate geometry + // 2. Segments where width > length (creates giant rectangles instead of roads) + if len < 0.00001 || len < (width * 2.0) as f32 { continue; } + + // Normal vector scaled by width + // Screen aspect ratio might distort this if not handled, + // but typically Mercator is uniform scale locally. + // However, we need to account for aspect ratio if we want constant screen width? + // Actually, let's keep it simple: width in "world units" (0..1). + // Since 0..1 covers the whole world, width is tiny. + // e.g. 0.00001 let nx = -dy / len * width; let ny = dx / len * width; - // 4 corners of the quad + // 4 corners let v1 = Vertex { position: [x1 + nx, y1 + ny] }; let v2 = Vertex { position: [x1 - nx, y1 - ny] }; let v3 = Vertex { position: [x2 + nx, y2 + ny] }; @@ -249,7 +283,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-v3-combined"; + let cache_name = "map-data-v4-combined"; let cache = wasm_bindgen_futures::JsFuture::from(caches.open(cache_name)).await.ok()?; let cache: web_sys::Cache = cache.dyn_into().ok()?; @@ -392,7 +426,7 @@ pub async fn run() { last_cursor: None, }; - let mut camera_uniform = camera.lock().unwrap().to_uniform(); + let camera_uniform = camera.lock().unwrap().to_uniform(); let camera_buffer = device.create_buffer_init( &wgpu::util::BufferInitDescriptor { label: Some("Camera Buffer"), @@ -568,11 +602,11 @@ pub async fn run() { let mut state_guard = state_clone2.lock().unwrap(); - let (smooth_lat, smooth_lon) = if let Some(filter) = &mut state_guard.kalman_filter { + let (smooth_lat, smooth_lon): (f64, f64) = 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); + let 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); @@ -602,7 +636,7 @@ pub async fn run() { web_sys::console::log_1(&format!("Geolocation error: {:?}", error.message()).into()); }); - let mut options = web_sys::PositionOptions::new(); + let options = web_sys::PositionOptions::new(); options.set_enable_high_accuracy(true); options.set_timeout(5000); options.set_maximum_age(0); @@ -642,12 +676,21 @@ pub async fn run() { }); let pipeline = create_pipeline(&device, &config.format, &camera_bind_group_layout); - let road_pipeline = create_road_pipeline(&device, &config.format, &camera_bind_group_layout); + + // Road Pipelines + let motorway_pipeline = create_road_motorway_pipeline(&device, &config.format, &camera_bind_group_layout); + let primary_pipeline = create_road_primary_pipeline(&device, &config.format, &camera_bind_group_layout); + let secondary_pipeline = create_road_secondary_pipeline(&device, &config.format, &camera_bind_group_layout); + let residential_pipeline = create_road_residential_pipeline(&device, &config.format, &camera_bind_group_layout); + let building_pipeline = create_building_pipeline(&device, &config.format, &camera_bind_group_layout); - let landuse_pipeline = create_landuse_pipeline(&device, &config.format, &camera_bind_group_layout); + + // Landuse Pipelines + let landuse_green_pipeline = create_landuse_green_pipeline(&device, &config.format, &camera_bind_group_layout); + let landuse_residential_pipeline = create_landuse_residential_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(); @@ -704,91 +747,51 @@ pub async fn run() { cam.zoom /= zoom_factor; } - // Clamp zoom - cam.zoom = cam.zoom.max(20.0).min(50000.0); - + cam.zoom = cam.zoom.max(20.0).min(50000.0); // Clamp zoom window.request_redraw(); } WindowEvent::RedrawRequested => { - let mut cam = camera.lock().unwrap(); + let cam = camera.lock().unwrap(); - // Update debug info - let window_doc = web_sys::window().unwrap().document().unwrap(); - if let Some(el) = window_doc.get_element_by_id("debug-zoom") { - el.set_inner_html(&format!("{:.1}", cam.zoom)); - } - - // Sync slider - if let Some(slider) = window_doc.get_element_by_id("zoom-slider").and_then(|e| e.dyn_into::().ok()) { - let min_zoom: f32 = 20.0; - let max_zoom: f32 = 50000.0; - let log_min = min_zoom.ln(); - let log_max = max_zoom.ln(); - let log_zoom = cam.zoom.ln(); - let t = (log_zoom - log_min) / (log_max - log_min); - let val = (t * 100.0) as f64; - - // Only update if not dragging (optional, but good for UX? actually input event handles drag) - // But if we update while dragging it might fight. - // However, for scroll wheel we need this. - // Let's just update it. The browser handles the active state. - slider.set_value(&val.to_string()); - } - if let Some(el) = window_doc.get_element_by_id("debug-pos") { - el.set_inner_html(&format!("{:.4}, {:.4}", cam.y, cam.x)); // Lat, Lon approx + // Simple debug overlay update + if let Some(doc) = web_sys::window().and_then(|w| w.document()) { + if let Some(el) = doc.get_element_by_id("debug-zoom") { + el.set_inner_html(&format!("{:.1}", cam.zoom)); + } } // 1. Check visible tiles 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. + // Determine what to render let mut tiles_to_render_set = HashSet::new(); for tile in &visible_tiles { 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 + // Fallback to 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 + break; } (z, x, y) = parent; } } } - // 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. + // Cleanup let mut useful_tiles = tiles_to_render_set.clone(); for t in &visible_tiles { if state_guard.loaded_tiles.contains(t) { @@ -806,16 +809,22 @@ pub async fn run() { state_guard.buffers.retain(|t, _| useful_tiles.contains(t)); // 3. Create buffers for new tiles if needed - // 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(); - // Road mesh disabled + let mut point_instance_data: Vec = Vec::new(); + + // Road Data + let mut road_motorway_vertex_data: Vec = Vec::new(); + let mut road_primary_vertex_data: Vec = Vec::new(); + let mut road_secondary_vertex_data: Vec = Vec::new(); + let mut road_residential_vertex_data: Vec = Vec::new(); + let mut building_vertex_data: Vec = Vec::new(); - let mut landuse_vertex_data = Vec::new(); - let mut water_vertex_data = Vec::new(); + + let mut landuse_green_vertex_data: Vec = Vec::new(); + let mut landuse_residential_vertex_data: Vec = Vec::new(); + + let mut water_vertex_data: Vec = Vec::new(); // Process nodes if let Some(nodes) = state_guard.nodes.get(tile) { @@ -825,29 +834,66 @@ pub async fn run() { } } - // Process ways (roads) - simple line rendering OR mesh + // Process ways (roads) - Distribute to correct buffers if let Some(ways) = state_guard.ways.get(tile) { for way in ways { let points = &way.points; - if points.len() < 16 { continue; } // Need at least 2 points (16 bytes) + if points.len() < 16 { continue; } - // 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] }); - road_vertex_data.push(Vertex { position: [x2, y2] }); + 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]); + } + + let highway: Option<&str> = way.tags.get("highway").map(|s| s.as_str()); + match highway { + Some("motorway") | Some("trunk") => { + // Simple lines for motorway/trunk + for i in 0..parsed_points.len().saturating_sub(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_motorway_vertex_data.push(Vertex { position: [x1, y1] }); + road_motorway_vertex_data.push(Vertex { position: [x2, y2] }); + } + }, + Some("primary") => { + // Simple lines for primary + for i in 0..parsed_points.len().saturating_sub(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_primary_vertex_data.push(Vertex { position: [x1, y1] }); + road_primary_vertex_data.push(Vertex { position: [x2, y2] }); + } + }, + Some("secondary") | Some("tertiary") => { + // Simple lines for secondary + 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_secondary_vertex_data.push(Vertex { position: [x1, y1] }); + road_secondary_vertex_data.push(Vertex { position: [x2, y2] }); + } + }, + _ => { + // Residential / Others + 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_residential_vertex_data.push(Vertex { position: [x1, y1] }); + road_residential_vertex_data.push(Vertex { position: [x2, y2] }); + } + } } } } @@ -869,12 +915,25 @@ pub async fn run() { if let Some(landuse) = state_guard.landuse.get(tile) { for area in landuse { let points = &area.points; + let mut area_verts = 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; let (x, y) = project(lat, lon); - landuse_vertex_data.push(Vertex { position: [x, y] }); + area_verts.push(Vertex { position: [x, y] }); + } + + // Categorize Landuse + let is_green = area.tags.get("leisure").map(|s| s == "park" || s == "nature_reserve").unwrap_or(false) || + area.tags.get("landuse").map(|s| s == "forest" || s == "grass" || s == "meadow" || s == "farmland").unwrap_or(false) || + area.tags.get("natural").map(|s| s == "wood" || s == "scrub").unwrap_or(false); + + if is_green { + landuse_green_vertex_data.extend(area_verts); + } else { + // Assume residential/industrial default + landuse_residential_vertex_data.extend(area_verts); } } } @@ -922,23 +981,42 @@ pub async fn run() { } } - // Only create buffers if we have data - if !point_instance_data.is_empty() || !road_vertex_data.is_empty() || !building_vertex_data.is_empty() || !landuse_vertex_data.is_empty() || !water_vertex_data.is_empty() || !railway_vertex_data.is_empty() { + // Create buffers if any data exists + if !point_instance_data.is_empty() + || !road_motorway_vertex_data.is_empty() + || !road_primary_vertex_data.is_empty() + || !road_secondary_vertex_data.is_empty() + || !road_residential_vertex_data.is_empty() + || !building_vertex_data.is_empty() + || !landuse_green_vertex_data.is_empty() + || !landuse_residential_vertex_data.is_empty() + || !water_vertex_data.is_empty() + || !railway_vertex_data.is_empty() + { let point_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Tile Instance Buffer"), contents: bytemuck::cast_slice(&point_instance_data), usage: wgpu::BufferUsages::VERTEX, }); - let road_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Tile Road Buffer"), - contents: bytemuck::cast_slice(&road_vertex_data), + let road_motorway_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Road Motorway Buffer"), + contents: bytemuck::cast_slice(&road_motorway_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()), + let road_primary_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Road Primary Buffer"), + contents: bytemuck::cast_slice(&road_primary_vertex_data), + usage: wgpu::BufferUsages::VERTEX, + }); + let road_secondary_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Road Secondary Buffer"), + contents: bytemuck::cast_slice(&road_secondary_vertex_data), + usage: wgpu::BufferUsages::VERTEX, + }); + let road_residential_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Road Residential Buffer"), + contents: bytemuck::cast_slice(&road_residential_vertex_data), usage: wgpu::BufferUsages::VERTEX, }); @@ -948,9 +1026,14 @@ pub async fn run() { usage: wgpu::BufferUsages::VERTEX, }); - let landuse_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Tile Landuse Buffer"), - contents: bytemuck::cast_slice(&landuse_vertex_data), + let landuse_green_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Tile Landuse Green Buffer"), + contents: bytemuck::cast_slice(&landuse_green_vertex_data), + usage: wgpu::BufferUsages::VERTEX, + }); + let landuse_residential_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Tile Landuse Residential Buffer"), + contents: bytemuck::cast_slice(&landuse_residential_vertex_data), usage: wgpu::BufferUsages::VERTEX, }); @@ -970,26 +1053,35 @@ pub async fn run() { state_guard.buffers.insert(*tile, std::sync::Arc::new(TileBuffers { point_instance_buffer: point_buffer, point_count: point_instance_data.len() as u32, - road_vertex_buffer: road_buffer, - road_vertex_count: road_vertex_data.len() as u32, + + road_motorway_vertex_buffer: road_motorway_buffer, + road_motorway_vertex_count: road_motorway_vertex_data.len() as u32, + road_primary_vertex_buffer: road_primary_buffer, + road_primary_vertex_count: road_primary_vertex_data.len() as u32, + road_secondary_vertex_buffer: road_secondary_buffer, + road_secondary_vertex_count: road_secondary_vertex_data.len() as u32, + road_residential_vertex_buffer: road_residential_buffer, + road_residential_vertex_count: road_residential_vertex_data.len() as u32, + building_vertex_buffer: building_buffer, building_index_count: building_vertex_data.len() as u32, - landuse_vertex_buffer: landuse_buffer, - landuse_index_count: landuse_vertex_data.len() as u32, + + landuse_green_vertex_buffer: landuse_green_buffer, + landuse_green_index_count: landuse_green_vertex_data.len() as u32, + landuse_residential_vertex_buffer: landuse_residential_buffer, + landuse_residential_index_count: landuse_residential_vertex_data.len() as u32, + water_vertex_buffer: water_buffer, 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(); + let mut tiles_to_render_vec: Vec<(i32, i32, i32)> = tiles_to_render_set.into_iter().collect(); tiles_to_render_vec.sort_by_key(|(z, _, _)| *z); let mut tiles_to_render = Vec::new(); @@ -1005,7 +1097,7 @@ pub async fn run() { // 2. Spawn fetches for (z, x, y) in needs_fetch { let state_clone = state.clone(); - let window_clone_for_fetch = window.clone(); + let window_clone_for_fetch = window_clone.clone(); wasm_bindgen_futures::spawn_local(async move { // Fetch all data in one go let url = format!("/api/tiles/{}/{}/{}/all", z, x, y); @@ -1039,8 +1131,7 @@ pub async fn run() { } // Update camera uniform - camera_uniform = camera_uniform_data; - queue.write_buffer(&camera_buffer, 0, bytemuck::cast_slice(&[camera_uniform])); + queue.write_buffer(&camera_buffer, 0, bytemuck::cast_slice(&[camera_uniform_data])); let frame = match surface.get_current_texture() { Ok(frame) => frame, @@ -1086,7 +1177,7 @@ pub async fn run() { occlusion_query_set: None, }); - // Draw each tile - order: Roads -> Buildings -> Points (back to front) + // Draw each tile - order: Water -> Landuse -> Buildings -> Roads -> Railways -> Points for buffers in &tiles_to_render { // Draw Water (bottom layer) if buffers.water_index_count > 0 { @@ -1096,47 +1187,21 @@ pub async fn run() { rpass.draw(0..buffers.water_index_count, 0..1); } - // Draw Landuse (second layer) - if buffers.landuse_index_count > 0 { - rpass.set_pipeline(&landuse_pipeline); + // Draw Landuse + if buffers.landuse_residential_index_count > 0 { + rpass.set_pipeline(&landuse_residential_pipeline); rpass.set_bind_group(0, &camera_bind_group, &[]); - rpass.set_vertex_buffer(0, buffers.landuse_vertex_buffer.slice(..)); - rpass.draw(0..buffers.landuse_index_count, 0..1); + rpass.set_vertex_buffer(0, buffers.landuse_residential_vertex_buffer.slice(..)); + rpass.draw(0..buffers.landuse_residential_index_count, 0..1); + } + if buffers.landuse_green_index_count > 0 { + rpass.set_pipeline(&landuse_green_pipeline); + rpass.set_bind_group(0, &camera_bind_group, &[]); + rpass.set_vertex_buffer(0, buffers.landuse_green_vertex_buffer.slice(..)); + rpass.draw(0..buffers.landuse_green_index_count, 0..1); } - // Draw Roads (third layer) - if buffers.road_vertex_count > 0 { - rpass.set_pipeline(&road_pipeline); - rpass.set_bind_group(0, &camera_bind_group, &[]); - rpass.set_vertex_buffer(0, buffers.road_vertex_buffer.slice(..)); - 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); - rpass.set_bind_group(0, &camera_bind_group, &[]); - rpass.set_vertex_buffer(0, buffers.railway_vertex_buffer.slice(..)); - rpass.draw(0..buffers.railway_vertex_count, 0..1); - } - - // Draw Railways (on top of roads?) - if buffers.railway_vertex_count > 0 { - rpass.set_pipeline(&railway_pipeline); - rpass.set_bind_group(0, &camera_bind_group, &[]); - rpass.set_vertex_buffer(0, buffers.railway_vertex_buffer.slice(..)); - rpass.draw(0..buffers.railway_vertex_count, 0..1); - } - - // Draw Buildings (middle layer) + // Draw Buildings if buffers.building_index_count > 0 { rpass.set_pipeline(&building_pipeline); rpass.set_bind_group(0, &camera_bind_group, &[]); @@ -1144,17 +1209,41 @@ pub async fn run() { rpass.draw(0..buffers.building_index_count, 0..1); } - // Points disabled - they're debug markers only - // if buffers.point_count > 0 { - // rpass.set_pipeline(&pipeline); - // rpass.set_bind_group(0, &camera_bind_group, &[]); - // rpass.set_vertex_buffer(0, vertex_buffer.slice(..)); - // rpass.set_vertex_buffer(1, buffers.point_instance_buffer.slice(..)); - // rpass.draw(0..4, 0..buffers.point_count); - // } - } - - // Draw user location marker (top layer) + // Draw Roads (Layers: Residential -> Secondary -> Primary -> Motorway) + if buffers.road_residential_vertex_count > 0 { + rpass.set_pipeline(&residential_pipeline); + rpass.set_bind_group(0, &camera_bind_group, &[]); + rpass.set_vertex_buffer(0, buffers.road_residential_vertex_buffer.slice(..)); + rpass.draw(0..buffers.road_residential_vertex_count, 0..1); + } + if buffers.road_secondary_vertex_count > 0 { + rpass.set_pipeline(&secondary_pipeline); + rpass.set_bind_group(0, &camera_bind_group, &[]); + rpass.set_vertex_buffer(0, buffers.road_secondary_vertex_buffer.slice(..)); + rpass.draw(0..buffers.road_secondary_vertex_count, 0..1); + } + if buffers.road_primary_vertex_count > 0 { + rpass.set_pipeline(&primary_pipeline); + rpass.set_bind_group(0, &camera_bind_group, &[]); + rpass.set_vertex_buffer(0, buffers.road_primary_vertex_buffer.slice(..)); + rpass.draw(0..buffers.road_primary_vertex_count, 0..1); + } + if buffers.road_motorway_vertex_count > 0 { + rpass.set_pipeline(&motorway_pipeline); + rpass.set_bind_group(0, &camera_bind_group, &[]); + rpass.set_vertex_buffer(0, buffers.road_motorway_vertex_buffer.slice(..)); + rpass.draw(0..buffers.road_motorway_vertex_count, 0..1); + } + + + // Draw Railways + if buffers.railway_vertex_count > 0 { + rpass.set_pipeline(&railway_pipeline); + rpass.set_bind_group(0, &camera_bind_group, &[]); + rpass.set_vertex_buffer(0, buffers.railway_vertex_buffer.slice(..)); + rpass.draw(0..buffers.railway_vertex_count, 0..1); + } + } // Draw user location marker (top layer) if let Some(buffer) = &location_buffer { rpass.set_pipeline(&pipeline); @@ -1168,33 +1257,9 @@ 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: - // Debug scale factor once (or occasionally) - if state_guard.pending_tiles.is_empty() && state_guard.loaded_tiles.len() > 0 { - // web_sys::console::log_1(&format!("Scale Factor: {}", window.scale_factor()).into()); - } - update_labels(&web_sys::window().unwrap(), &cam, &state_guard, config.width as f64, config.height as f64, window.scale_factor()); + let can_lock = camera.lock().unwrap(); + let state_lock = state.lock().unwrap(); + update_labels(&web_sys::window().unwrap(), &can_lock, &state_lock, config.width as f64, config.height as f64, window.scale_factor()); } WindowEvent::CloseRequested => { #[cfg(not(target_arch = "wasm32"))] @@ -1303,94 +1368,6 @@ fn create_pipeline( }) } -fn create_road_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; - - // 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; - } - - @fragment - fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return vec4(0.4, 0.4, 0.4, 1.0); // Dark grey roads - } - "#)), - }); - - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Road 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::LineList, - 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 create_building_pipeline( device: &wgpu::Device, format: &wgpu::TextureFormat, @@ -1434,7 +1411,7 @@ fn create_building_pipeline( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return vec4(0.7, 0.7, 0.7, 1.0); // Darker grey buildings for visibility + return vec4(0.85, 0.85, 0.85, 1.0); // #d9d9d9 Medium-Light Grey for buildings } "#)), }); @@ -1479,104 +1456,6 @@ fn create_building_pipeline( }) } -fn create_landuse_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; - - // 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; - } - - @fragment - fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return vec4(0.77, 0.91, 0.77, 1.0); // Google Maps Park Green (#c5e8c5 approx) - } - "#)), - }); - - let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Landuse Pipeline Layout"), - bind_group_layouts: &[bind_group_layout], - push_constant_ranges: &[], - }); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Landuse Pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: "vs_main", - buffers: &[ - wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[ - wgpu::VertexAttribute { - offset: 0, - shader_location: 0, - format: wgpu::VertexFormat::Float32x2, - } - ], - } - ], - }, - 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 create_water_pipeline( device: &wgpu::Device, format: &wgpu::TextureFormat, @@ -1620,7 +1499,7 @@ fn create_water_pipeline( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return vec4(0.66, 0.85, 1.0, 1.0); // Google Maps Water Blue (#aadaff approx) + return vec4(0.62, 0.79, 1.0, 1.0); // #9ecaff Google Maps Water } "#)), }); @@ -1763,7 +1642,7 @@ fn create_railway_pipeline( }) } -fn create_road_mesh_pipeline( +fn create_road_motorway_pipeline( device: &wgpu::Device, format: &wgpu::TextureFormat, bind_group_layout: &wgpu::BindGroupLayout @@ -1776,55 +1655,239 @@ fn create_road_mesh_pipeline( }; @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 { + 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 + return vec4(0.91, 0.56, 0.63, 1.0); // #e990a0 Light Red/Orange } "#)), }); + create_simple_pipeline(device, format, bind_group_layout, &shader, "Motorway Pipeline", wgpu::PrimitiveTopology::LineList) +} +fn create_road_primary_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.99, 0.75, 0.44, 1.0); // #fdbf6f Orange-Yellow + } + "#)), + }); + create_simple_pipeline(device, format, bind_group_layout, &shader, "Primary Road Pipeline", wgpu::PrimitiveTopology::LineList) +} + +fn create_road_secondary_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(1.0, 1.0, 1.0, 1.0); // White + } + "#)), + }); + create_simple_pipeline(device, format, bind_group_layout, &shader, "Secondary Road Pipeline", wgpu::PrimitiveTopology::LineList) +} + +fn create_road_residential_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(1.0, 1.0, 1.0, 1.0); // White + } + "#)), + }); + create_simple_pipeline(device, format, bind_group_layout, &shader, "Residential Road Pipeline", wgpu::PrimitiveTopology::LineList) +} + +fn create_landuse_green_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.80, 0.92, 0.69, 1.0); // #cdebb0 Green + } + "#)), + }); + create_simple_pipeline(device, format, bind_group_layout, &shader, "Green Landuse Pipeline", wgpu::PrimitiveTopology::TriangleList) +} + +fn create_landuse_residential_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.88, 0.87, 0.87, 1.0); // #e0dfdf Light Grey + } + "#)), + }); + create_simple_pipeline(device, format, bind_group_layout, &shader, "Residential Landuse Pipeline", wgpu::PrimitiveTopology::TriangleList) +} + +fn create_simple_pipeline( + device: &wgpu::Device, + format: &wgpu::TextureFormat, + bind_group_layout: &wgpu::BindGroupLayout, + shader: &wgpu::ShaderModule, + label: &str, + topology: wgpu::PrimitiveTopology, +) -> wgpu::RenderPipeline { let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Road Mesh Pipeline Layout"), + label: Some(label), bind_group_layouts: &[bind_group_layout], push_constant_ranges: &[], }); device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: None, + label: Some(label), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { - module: &shader, + module: shader, entry_point: "vs_main", - buffers: &[ - Vertex::desc(), - ], + buffers: &[Vertex::desc()], }, fragment: Some(wgpu::FragmentState { - module: &shader, + module: shader, entry_point: "fs_main", targets: &[Some(wgpu::ColorTargetState { format: *format, @@ -1833,7 +1896,7 @@ fn create_road_mesh_pipeline( })], }), primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, + topology, strip_index_format: None, front_face: wgpu::FrontFace::Ccw, cull_mode: None, @@ -1853,6 +1916,16 @@ struct LabelCandidate { y: f64, priority: i32, is_country: bool, + rotation: f64, // Rotation angle in degrees + label_type: LabelType, +} + +#[derive(Clone, Copy, PartialEq)] +enum LabelType { + Country, + City, + Street, + Poi, // Points of Interest (hospitals, parks, etc.) } fn update_labels( @@ -1861,7 +1934,7 @@ fn update_labels( state: &AppState, width: f64, height: f64, - scale_factor: f64, + _scale_factor: f64, ) { let document = window.document().unwrap(); let container = document.get_element_by_id("labels").unwrap(); @@ -1875,11 +1948,11 @@ fn update_labels( let mut candidates: Vec = Vec::new(); let zoom = camera.zoom; - for tile in visible_tiles { + 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()); + let place: Option<&str> = node.tags.get("place").map(|s| s.as_str()); + let name: Option<&str> = node.tags.get("name").map(|s| s.as_str()); if let (Some(place), Some(name)) = (place, name) { // 1. Zoom Level Filtering @@ -1895,7 +1968,7 @@ fn update_labels( if !should_show { continue; } // 2. Priority Calculation - let mut priority = match place { + let mut priority: i32 = match place { "continent" => 1000, "country" => 100, "city" => 80, @@ -1935,13 +2008,181 @@ fn update_labels( let css_x = (cx as f64 + 1.0) * 0.5 * client_width; let css_y = (1.0 - cy as f64) * 0.5 * client_height; + + let name_string: String = name.to_string(); + let label_type = if place == "country" || place == "continent" { + LabelType::Country + } else { + LabelType::City + }; + candidates.push(LabelCandidate { + name: name_string, + x: css_x, + y: css_y, + priority, + is_country: place == "country" || place == "continent", + rotation: 0.0, + label_type, + }); + } + + // POI Labels (amenity, leisure, tourism) + let amenity: Option<&str> = node.tags.get("amenity").map(|s| s.as_str()); + let leisure: Option<&str> = node.tags.get("leisure").map(|s| s.as_str()); + let tourism: Option<&str> = node.tags.get("tourism").map(|s| s.as_str()); + let poi_name: Option<&str> = node.tags.get("name").map(|s| s.as_str()); + + if let Some(poi_name) = poi_name { + if poi_name.is_empty() { continue; } + + // Determine POI type and set zoom threshold + let (min_zoom, priority) = if let Some(amenity_type) = amenity { + match amenity_type { + "hospital" => (500.0, 45), + "university" | "college" => (800.0, 40), + "school" => (2000.0, 25), + "pharmacy" | "doctors" => (3000.0, 20), + "restaurant" | "cafe" => (5000.0, 15), + "fuel" | "parking" => (4000.0, 18), + "bank" | "atm" => (4000.0, 17), + _ => (6000.0, 10), + } + } else if let Some(leisure_type) = leisure { + match leisure_type { + "park" | "garden" => (800.0, 38), + "sports_centre" | "stadium" => (1500.0, 32), + "playground" => (4000.0, 15), + _ => (5000.0, 12), + } + } else if let Some(tourism_type) = tourism { + match tourism_type { + "attraction" | "museum" => (500.0, 42), + "hotel" => (2000.0, 28), + "viewpoint" => (1500.0, 30), + _ => (3000.0, 20), + } + } else { + continue; // Not a POI we're interested in + }; + + // Zoom filter + if zoom < min_zoom { continue; } + + // Project coordinates + let (x, y) = project(node.lat, node.lon); + let cx = x * uniforms.params[0] + uniforms.params[2]; + let cy = y * uniforms.params[1] + uniforms.params[3]; + + if cx < -1.2 || cx > 1.2 || cy < -1.2 || cy > 1.2 { continue; } + + let client_width = window.inner_width().ok().and_then(|v| v.as_f64()).unwrap_or(width); + let client_height = window.inner_height().ok().and_then(|v| v.as_f64()).unwrap_or(height); + let css_x = (cx as f64 + 1.0) * 0.5 * client_width; + let css_y = (1.0 - cy as f64) * 0.5 * client_height; + + candidates.push(LabelCandidate { + name: poi_name.to_string(), + x: css_x, + y: css_y, + priority, + is_country: false, + rotation: 0.0, + label_type: LabelType::Poi, + }); + } + } + } + } + // Process ways for street labels + let client_width = window.inner_width().ok().and_then(|v| v.as_f64()).unwrap_or(width); + let client_height = window.inner_height().ok().and_then(|v| v.as_f64()).unwrap_or(height); + + for tile in &visible_tiles { + if let Some(ways) = state.ways.get(tile) { + for way in ways { + // Check if road has a name + let name: Option<&str> = way.tags.get("name").map(|s| s.as_str()); + let highway: Option<&str> = way.tags.get("highway").map(|s| s.as_str()); + + if let (Some(name), Some(highway_type)) = (name, highway) { + // Skip unnamed or minor roads + if name.is_empty() { continue; } + + // Zoom filtering - Apple Maps style (show major roads first) + let min_zoom = match highway_type { + "motorway" | "trunk" => 200.0, + "primary" => 500.0, + "secondary" => 1500.0, + "tertiary" => 3000.0, + "residential" | "unclassified" => 6000.0, + _ => 10000.0, + }; + if zoom < min_zoom { continue; } + + // Priority based on road type + let priority: i32 = match highway_type { + "motorway" | "trunk" => 50, + "primary" => 40, + "secondary" => 30, + "tertiary" => 20, + _ => 10, + }; + + // Parse road points to find midpoint and angle + let points = &way.points; + if points.len() < 16 { continue; } // Need at least 2 points + + let mut parsed_points: Vec<[f64; 2]> = 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_or([0u8; 4])) as f64; + let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4])) as f64; + parsed_points.push([lat, lon]); + } + + if parsed_points.len() < 2 { continue; } + + // Calculate midpoint + let mid_idx = parsed_points.len() / 2; + let mid_point = parsed_points[mid_idx]; + + // Calculate angle from road direction + let p1 = if mid_idx > 0 { parsed_points[mid_idx - 1] } else { parsed_points[0] }; + let p2 = if mid_idx + 1 < parsed_points.len() { parsed_points[mid_idx + 1] } else { parsed_points[mid_idx] }; + + let (x1, y1) = project(p1[0], p1[1]); + let (x2, y2) = project(p2[0], p2[1]); + let dx = x2 - x1; + // Negate dy because screen Y is inverted (CSS Y increases downward) + let dy = -(y2 - y1); + + // Calculate angle in degrees (atan2 gives radians) + let mut angle_deg = (dy as f64).atan2(dx as f64).to_degrees(); + + // Keep text readable (don't flip upside down) + if angle_deg > 90.0 { angle_deg -= 180.0; } + if angle_deg < -90.0 { angle_deg += 180.0; } + + // Project midpoint to screen + let (mx, my) = project(mid_point[0], mid_point[1]); + let cx = mx * uniforms.params[0] + uniforms.params[2]; + let cy = my * uniforms.params[1] + uniforms.params[3]; + + // Clip check + if cx < -1.5 || cx > 1.5 || cy < -1.5 || cy > 1.5 { continue; } + + let css_x = (cx as f64 + 1.0) * 0.5 * client_width; + let css_y = (1.0 - cy as f64) * 0.5 * client_height; + candidates.push(LabelCandidate { name: name.to_string(), x: css_x, y: css_y, priority, - is_country: place == "country" || place == "continent", + is_country: false, + rotation: angle_deg, + label_type: LabelType::Street, }); } } @@ -1955,12 +2196,12 @@ fn update_labels( let mut placed_rects: Vec<(f64, f64, f64, f64)> = Vec::new(); // (x, y, w, h) for candidate in candidates { - // Estimate dimensions (approximate) - // Country labels are usually larger - let (est_w, est_h) = if candidate.is_country { - (candidate.name.len() as f64 * 12.0 + 20.0, 24.0) - } else { - (candidate.name.len() as f64 * 8.0 + 10.0, 16.0) + // Estimate dimensions based on label type + let (est_w, est_h) = match candidate.label_type { + LabelType::Country => (candidate.name.len() as f64 * 12.0 + 20.0, 24.0), + LabelType::City => (candidate.name.len() as f64 * 8.0 + 10.0, 16.0), + LabelType::Street => (candidate.name.len() as f64 * 6.0 + 8.0, 12.0), + LabelType::Poi => (candidate.name.len() as f64 * 6.5 + 10.0, 14.0), }; // Centered label @@ -1971,7 +2212,7 @@ fn update_labels( let mut collision = false; for (px, py, pw, ph) in &placed_rects { // Simple AABB intersection with padding - let padding = 5.0; // Reduced padding + let padding = if candidate.label_type == LabelType::Street { 3.0 } else { 5.0 }; if rect_x < px + pw + padding && rect_x + est_w + padding > *px && rect_y < py + ph + padding && @@ -1985,7 +2226,14 @@ fn update_labels( placed_rects.push((rect_x, rect_y, est_w, est_h)); let div = document.create_element("div").unwrap(); - let class_name = if candidate.is_country { "label label-country" } else { "label label-city" }; + + // Apple Maps-style class naming + let class_name = match candidate.label_type { + LabelType::Country => "label label-country", + LabelType::City => "label label-city", + LabelType::Street => "label label-street", + LabelType::Poi => "label label-poi", + }; div.set_class_name(class_name); div.set_text_content(Some(&candidate.name)); @@ -1994,6 +2242,12 @@ fn update_labels( style.set_property("left", &format!("{}px", candidate.x)).unwrap(); style.set_property("top", &format!("{}px", candidate.y)).unwrap(); + // Apply rotation for street labels + if candidate.label_type == LabelType::Street && candidate.rotation.abs() > 0.5 { + let transform = format!("translate(-50%, -50%) rotate({}deg)", candidate.rotation); + style.set_property("transform", &transform).unwrap(); + } + container.append_child(&div_html).unwrap(); } } diff --git a/importer/src/main.rs b/importer/src/main.rs index d9c6f8c..7fc723e 100644 --- a/importer/src/main.rs +++ b/importer/src/main.rs @@ -165,8 +165,8 @@ fn should_include(tags: &HashMap, zoom: u32) -> bool { 2 => { // Space View: Continents and Countries - matches!(place, Some("continent" | "country")) || - matches!(natural, Some("water")) || // Major water bodies + matches!(place, Some("continent" | "country" | "sea" | "ocean")) || + matches!(natural, Some("water" | "bay" | "strait")) || // 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 @@ -175,8 +175,8 @@ fn should_include(tags: &HashMap, zoom: u32) -> bool { 4 => { // Regional View (NEW) matches!(highway, Some("motorway" | "trunk")) || - matches!(place, Some("city" | "town")) || - matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland")) || + matches!(place, Some("city" | "town" | "sea" | "ocean")) || + matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland" | "bay" | "strait")) || 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")) @@ -185,8 +185,8 @@ fn should_include(tags: &HashMap, zoom: u32) -> bool { // Enterprise Grade: ONLY Motorways and Trunk roads. No primary/secondary. // ONLY Cities. No nature/landuse. matches!(highway, Some("motorway" | "trunk" | "primary")) || // Added primary - matches!(place, Some("city")) || - matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland")) || + matches!(place, Some("city" | "sea" | "ocean")) || + matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland" | "bay" | "strait")) || 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")) @@ -196,9 +196,9 @@ fn should_include(tags: &HashMap, zoom: u32) -> bool { // Add Towns. // Limited nature. matches!(highway, Some("motorway" | "trunk" | "primary")) || - matches!(place, Some("city" | "town")) || + matches!(place, Some("city" | "town" | "sea" | "ocean")) || matches!(railway, Some("rail")) || - matches!(natural, Some("water" | "wood")) || + matches!(natural, Some("water" | "wood" | "bay" | "strait")) || 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")) @@ -210,7 +210,7 @@ fn should_include(tags: &HashMap, zoom: u32) -> bool { tags.contains_key("building") || tags.contains_key("landuse") || tags.contains_key("leisure") || - matches!(natural, Some("water" | "wood" | "scrub" | "wetland" | "heath")) || + matches!(natural, Some("water" | "wood" | "scrub" | "wetland" | "heath" | "bay" | "strait")) || matches!(waterway, Some("river" | "riverbank" | "stream")) }, _ => false @@ -404,7 +404,8 @@ async fn main() -> Result<()> { // Filter for highways/roads OR buildings OR landuse OR water OR railways let is_highway = tags.contains_key("highway"); let is_building = tags.contains_key("building"); - let is_water = tags.get("natural").map(|v| v == "water" || v == "wetland").unwrap_or(false) || + let is_water = tags.get("natural").map(|v| v == "water" || v == "wetland" || v == "bay" || v == "strait").unwrap_or(false) || + tags.get("place").map(|v| v == "sea" || v == "ocean").unwrap_or(false) || tags.get("waterway").map(|v| v == "riverbank" || v == "stream" || v == "river").unwrap_or(false) || tags.get("landuse").map(|v| v == "basin" || v == "reservoir").unwrap_or(false); let is_landuse = tags.contains_key("leisure") || @@ -442,7 +443,11 @@ async fn main() -> Result<()> { }; let epsilon = if is_water || is_landuse || is_highway { - base_epsilon * 0.5 // Preserve more detail for natural features AND roads + if zoom <= 4 && is_landuse { + 0.0 // Disable simplification for landuse at low zoom to prevent disappearing polygons + } else { + base_epsilon * 0.5 // Preserve more detail for natural features AND roads + } } else { base_epsilon }; @@ -454,9 +459,17 @@ async fn main() -> Result<()> { }; // Serialize points - let mut final_points = simplified_points; + let mut final_points = simplified_points.clone(); - // Triangulate if it's a polygon type + // For highways and railways, we DON'T triangulate - they're line data + // Create the highway/railway blob BEFORE triangulation + let mut line_blob = Vec::with_capacity(simplified_points.len() * 8); + for (lat, lon) in &simplified_points { + line_blob.extend_from_slice(&(*lat as f32).to_le_bytes()); + line_blob.extend_from_slice(&(*lon as f32).to_le_bytes()); + } + + // Triangulate for polygon types (buildings, water, landuse) if is_building || is_water || is_landuse { // Close the loop if not closed if final_points.first() != final_points.last() { @@ -465,40 +478,43 @@ async fn main() -> Result<()> { final_points = triangulate_polygon(&final_points); } - if final_points.len() < 2 { continue; } + if final_points.len() < 3 && (is_building || is_water || is_landuse) { continue; } + if simplified_points.len() < 2 && (is_highway || is_railway) { continue; } - let (first_lat, first_lon) = final_points[0]; + 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; - 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()); + // Create polygon blob from triangulated points + let mut polygon_blob = Vec::with_capacity(final_points.len() * 8); + for (lat, lon) in &final_points { + polygon_blob.extend_from_slice(&(*lat as f32).to_le_bytes()); + polygon_blob.extend_from_slice(&(*lon as f32).to_le_bytes()); } + // Use line_blob for highways/railways, polygon_blob for others if is_highway { - let task = DbTask::Way { zoom: zoom_i32, table: "ways", id, tags: tags.clone(), points: blob.clone(), x, y }; + let task = DbTask::Way { zoom: zoom_i32, table: "ways", id, tags: tags.clone(), points: line_blob.clone(), x, y }; let _ = tx.blocking_send(task); } if is_building { - let task = DbTask::Way { zoom: zoom_i32, table: "buildings", id, tags: tags.clone(), points: blob.clone(), x, y }; + let task = DbTask::Way { zoom: zoom_i32, table: "buildings", id, tags: tags.clone(), points: polygon_blob.clone(), x, y }; let _ = tx.blocking_send(task); } if is_water { - let task = DbTask::Way { zoom: zoom_i32, table: "water", id, tags: tags.clone(), points: blob.clone(), x, y }; + let task = DbTask::Way { zoom: zoom_i32, table: "water", id, tags: tags.clone(), points: polygon_blob.clone(), x, y }; let _ = tx.blocking_send(task); } if is_landuse { - let task = DbTask::Way { zoom: zoom_i32, table: "landuse", id, tags: tags.clone(), points: blob.clone(), x, y }; + let task = DbTask::Way { zoom: zoom_i32, table: "landuse", id, tags: tags.clone(), points: polygon_blob.clone(), x, y }; let _ = tx.blocking_send(task); } if is_railway { - let task = DbTask::Way { zoom: zoom_i32, table: "railways", id, tags: tags.clone(), points: blob.clone(), x, y }; + let task = DbTask::Way { zoom: zoom_i32, table: "railways", id, tags: tags.clone(), points: line_blob.clone(), x, y }; let _ = tx.blocking_send(task); } }