From f3f1a568e250a6fe873f994eb63cef9666c78038 Mon Sep 17 00:00:00 2001 From: Dongho Kim Date: Mon, 29 Dec 2025 03:44:27 +0900 Subject: [PATCH] update --- .dockerignore | 7 ++ backend/src/main.rs | 18 +++- docker-compose.yml | 61 ++++++++++--- frontend/Cargo.toml | 6 ++ frontend/index.html | 62 +++++++++++++ frontend/src/geo.rs | 13 +++ frontend/src/labels.rs | 85 +++++++++-------- frontend/src/lib.rs | 18 +++- frontend/src/pipelines/railway.rs | 101 ++++++++++++++++++--- frontend/src/services/render_service.rs | 52 ++++++----- importer/src/main.rs | 38 ++++++-- importer/src/services/filtering_service.rs | 2 +- importer/src/services/geometry_service.rs | 13 +++ importer/src/services/mesh_service.rs | 4 +- 14 files changed, 380 insertions(+), 100 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..25707ae --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +target/ +backend/target/ +frontend/node_modules/ +importer/target/ +.git/ +*.swp +.env diff --git a/backend/src/main.rs b/backend/src/main.rs index c69a67c..371ba86 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -45,12 +45,26 @@ async fn main() -> Result<(), Box> { let session_arc = Arc::new(session); println!("Connected to ScyllaDB!"); + // Connect to Redis // Connect to Redis println!("Connecting to Redis..."); + println!("Verifying new build... REDIS_URI should be used."); let redis_uri = std::env::var("REDIS_URI") .unwrap_or_else(|_| "redis://redis:6379".to_string()); - let redis_repo = Arc::new(RedisRepository::new(&redis_uri).await?); - println!("Connected to Redis!"); + println!("Using Redis URI: {}", redis_uri); + + let redis_repo = loop { + match RedisRepository::new(&redis_uri).await { + Ok(repo) => { + println!("Connected to Redis!"); + break Arc::new(repo); + } + Err(e) => { + println!("Failed to connect to Redis: {}. Retrying in 5 seconds...", e); + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + } + } + }; // Dependency Injection let node_repo = Arc::new(NodeRepository::new(session_arc.clone())); diff --git a/docker-compose.yml b/docker-compose.yml index 3516d84..caa53a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,16 +2,17 @@ services: scylla: image: scylladb/scylla:latest container_name: scylla - command: --smp 1 --memory 2G --overprovisioned 1 --api-address 0.0.0.0 --max-memory-for-unlimited-query-soft-limit 1073741824 --tombstone-warn-threshold 10000000 + command: --smp 4 --memory 16G --overprovisioned 0 --api-address 0.0.0.0 --max-memory-for-unlimited-query-hard-limit 4294967296 + volumes: - - scylla_data:/var/lib/scylla + - scylla-data:/var/lib/scylla networks: - maps-net redis: image: redis:7-alpine container_name: map-redis - command: redis-server --maxmemory 2gb --maxmemory-policy allkeys-lru + command: redis-server --maxmemory 5gb --maxmemory-policy allkeys-lru volumes: - redis_data:/data networks: @@ -23,16 +24,28 @@ services: context: . target: backend container_name: map-app - ports: - - "3000:3000" + networks: + - proxy + - maps-net + environment: + - REDIS_URI=redis://map-redis:6379 depends_on: - scylla - - redis - environment: - - REDIS_URI=redis://redis:6379 - networks: - - maps-net restart: always + labels: + - traefik.enable=true + - traefik.docker.network=proxy + - traefik.http.routers.maps.entrypoints=http + - traefik.http.routers.maps.rule=Host(`maps.ekstrah.com`) + - traefik.http.middlewares.maps-redirect.redirectscheme.permanent=true + - traefik.http.middlewares.maps-redirect.redirectscheme.scheme=https + - traefik.http.routers.maps.middlewares=maps-redirect + - traefik.http.routers.maps-secure.entrypoints=https + - traefik.http.routers.maps-secure.rule=Host(`maps.ekstrah.com`) + - traefik.http.routers.maps-secure.tls=true + - traefik.http.routers.maps-secure.tls.certresolver=cloudflare + - traefik.http.routers.maps-secure.service=maps-secure-service + - traefik.http.services.maps-secure-service.loadbalancer.server.port=3000 importer: build: @@ -42,23 +55,41 @@ services: volumes: - ${HOST_PBF_PATH:-./europe-latest.osm.pbf}:/app/data.osm.pbf - ${HOST_CACHE_DIR:-./cache}:/cache + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: + - gpu + - compute + - utility environment: - SCYLLA_URI=scylla:9042 - OSM_PBF_PATH=/app/data.osm.pbf - CACHE_DIR=/cache - - DEBUG_WAY_ID=99 - - VERBOSE_DEBUG=1 + - VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/nvidia_icd.json + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=compute,utility,graphics + # - DEBUG_WAY_ID=99 # Logs detailed info for specific way + # - VERBOSE_DEBUG=1 # Logs transit line relations + # - DEBUG_MESH=1 # Logs mesh generation and vertex buffer info depends_on: - scylla - networks: - - maps-net profiles: - import + networks: + - maps-net networks: + proxy: + external: true maps-net: driver: bridge volumes: - scylla_data: + scylla-data: + driver: local redis_data: + diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index 002987d..51b7e3d 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -51,3 +51,9 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" bincode = "1.3" earcutr = "0.4" + +[profile.release] +lto = true +opt-level = 3 +codegen-units = 1 +panic = "abort" diff --git a/frontend/index.html b/frontend/index.html index 4c38ffa..fb507b7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -510,6 +510,68 @@ +
+
+
Loading Maps...
+
+
N
diff --git a/frontend/src/geo.rs b/frontend/src/geo.rs index 8015f49..9b2148e 100644 --- a/frontend/src/geo.rs +++ b/frontend/src/geo.rs @@ -14,6 +14,19 @@ pub fn project(lat: f64, lon: f64) -> (f32, f32) { (x, y) } +/// High precision Web Mercator Projection +/// Returns (x, y) in range [0.0, 1.0] for the whole world as f64 +pub fn project_high_precision(lat: f64, lon: f64) -> (f64, f64) { + 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; + + let x = if x.is_finite() { x.clamp(0.0, 1.0) } else { 0.5 }; + let y = if y.is_finite() { y.clamp(0.0, 1.0) } else { 0.5 }; + + (x, y) +} + /// Kalman filter for smoothing GPS location updates #[derive(Debug, Clone)] pub struct KalmanFilter { diff --git a/frontend/src/labels.rs b/frontend/src/labels.rs index a107821..3352837 100644 --- a/frontend/src/labels.rs +++ b/frontend/src/labels.rs @@ -222,7 +222,9 @@ pub fn extract_labels(tile_data: &TileData) -> Vec { } } - // 3. Process Railways (Transit line labels like S1, U3, etc.) + // 3. Process Railways (Transit line labels like S1, U3, etc.) - Grouped to reduce clutter + let mut transit_groups: HashMap> = HashMap::new(); + for railway in &tile_data.railways { // Get line reference (e.g., "S1", "U3", "S8") - stored as 'line_ref' by importer let line_ref = railway.tags.get("line_ref").map(|s| s.as_str()); @@ -232,42 +234,53 @@ pub fn extract_labels(tile_data: &TileData) -> Vec { if railway_type == Some("tram") { continue; } let Some(line_ref) = line_ref else { continue; }; if line_ref.is_empty() { continue; } - - // Parse points to find midpoint - let mut parsed_points: Vec<[f64; 2]> = Vec::new(); - for chunk in railway.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]); + + transit_groups.entry(line_ref.to_string()) + .or_insert_with(Vec::new) + .push(railway); + } + + // Process each transit line group and pick the best segment(s) for labeling + for (line_ref, segments) in transit_groups { + // Sort segments by length (number of points as proxy) descending + // We only label the top 1 longest segment per tile to avoid spam + let best_segment = segments.iter().max_by_key(|r| r.points.len()); + + if let Some(railway) = best_segment { + // Parse points to find midpoint + let mut parsed_points: Vec<[f64; 2]> = Vec::new(); + for chunk in railway.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; } + + // Use midpoint for label placement + let mid_idx = parsed_points.len() / 2; + let mid_point = parsed_points[mid_idx]; + + // Determine category based on line prefix + let category = if line_ref.starts_with('S') { + "sbahn" + } else if line_ref.starts_with('U') { + "ubahn" + } else { + "rail" + }; + + candidates.push(CachedLabel { + name: line_ref.to_string(), + lat: mid_point[0], + lon: mid_point[1], + label_type: LabelType::Transit, + rotation: 0.0, // Transit labels are always horizontal + priority: 150, // Very high priority - above cities + min_zoom: 100.0, // Show at most zoom levels + category: category.to_string(), + }); } - if parsed_points.len() < 2 { continue; } - - // Use midpoint for label placement - let mid_idx = parsed_points.len() / 2; - let mid_point = parsed_points[mid_idx]; - - // Determine category based on line prefix - let category = if line_ref.starts_with('S') { - "sbahn" - } else if line_ref.starts_with('U') { - "ubahn" - } else { - "rail" - }; - // Debug logging removed for production performance - // web_sys::console::log_1(&format!("Transit label found: {} at ({}, {})", line_ref, mid_point[0], mid_point[1]).into()); - - candidates.push(CachedLabel { - name: line_ref.to_string(), - lat: mid_point[0], - lon: mid_point[1], - label_type: LabelType::Transit, - rotation: 0.0, // Transit labels are always horizontal - priority: 150, // Very high priority - above cities - min_zoom: 100.0, // Show at most zoom levels - category: category.to_string(), - }); } candidates diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index 05b25bb..8b99232 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -25,6 +25,7 @@ use winit::{ window::WindowBuilder, platform::web::WindowExtWebSys, }; +use web_sys::Window; // Ensure web_sys Window is available if needed, though usually covered by wasm_bindgen use wgpu::util::DeviceExt; use crate::domain::camera::Camera; @@ -291,6 +292,20 @@ pub async fn run() { } } + // Hide loading screen + if let Some(loader) = window_doc.get_element_by_id("loading-screen") { + let _ = loader.class_list().add_1("fade-out"); + // Remove after transition + let closure = wasm_bindgen::closure::Closure::::new(move || { + let _ = loader.set_attribute("style", "display: none;"); + }); + window_doc.default_view().unwrap().set_timeout_with_callback_and_timeout_and_arguments_0( + closure.as_ref().unchecked_ref(), + 500, + ).unwrap(); + closure.forget(); + } + // Event Loop event_loop.run(move |event, elwt| { elwt.set_control_flow(winit::event_loop::ControlFlow::Wait); @@ -381,7 +396,7 @@ pub async fn run() { for tile in tiles_to_process { // Call RenderService static helper? Or just logic. // I put logic in RenderService::create_tile_buffers which takes state. - RenderService::create_tile_buffers(&device, &mut state_guard, tile); + RenderService::create_tile_buffers(&device, &mut state_guard, tile, &render_service.tile_bind_group_layout); } } @@ -567,6 +582,7 @@ pub async fn run() { rpass.set_bind_group(0, &camera_bind_group, &[]); for buffers in &tiles_to_render { if buffers.railway_vertex_count > 0 { + rpass.set_bind_group(1, &buffers.tile_bind_group, &[]); rpass.set_vertex_buffer(0, buffers.railway_vertex_buffer.slice(..)); rpass.draw(0..buffers.railway_vertex_count, 0..1); } diff --git a/frontend/src/pipelines/railway.rs b/frontend/src/pipelines/railway.rs index 873130b..4f8679e 100644 --- a/frontend/src/pipelines/railway.rs +++ b/frontend/src/pipelines/railway.rs @@ -14,8 +14,7 @@ fn dot(a: [f32; 2], b: [f32; 2]) -> f32 { /// Generate thick railway geometry (quads) pub fn generate_railway_geometry(points: &[[f32; 2]], color: [f32; 3], railway_type: f32) -> Vec { - let vertices = Vec::new(); - if points.len() < 2 { return vertices; } + if points.len() < 2 { return Vec::new(); } // Computes normals for each segment let mut segment_normals = Vec::with_capacity(points.len() - 1); @@ -25,7 +24,7 @@ pub fn generate_railway_geometry(points: &[[f32; 2]], color: [f32; 3], railway_t let dx = p2[0] - p1[0]; let dy = p2[1] - p1[1]; let len = (dx*dx + dy*dy).sqrt(); - if len < 0.000001 { + if len < 0.000000001 { segment_normals.push([0.0, 0.0]); // Degenerate } else { segment_normals.push([-dy/len, dx/len]); @@ -84,7 +83,7 @@ pub fn generate_railway_geometry(points: &[[f32; 2]], color: [f32; 3], railway_t } // Triangulate - let mut triangle_vertices = Vec::with_capacity((points.len() - 1) * 6); + let mut triangle_vertices: Vec = Vec::with_capacity((points.len() - 1) * 6); for i in 0..points.len()-1 { // Skip degenerate segment if dot(segment_normals[i], segment_normals[i]) == 0.0 { continue; } @@ -107,14 +106,78 @@ pub fn generate_railway_geometry(points: &[[f32; 2]], color: [f32; 3], railway_t triangle_vertices.push(v4); triangle_vertices.push(v3); } + + // Start Cap (at index 0) + if points.len() >= 2 { + let p0 = points[0]; + let p1 = points[1]; + let tangent = normalize([p1[0]-p0[0], p1[1]-p0[1]]); + let neg_tangent = [-tangent[0], -tangent[1]]; + + let v_left = point_pairs[0]; + let v_right = point_pairs[1]; + + let cap_tris: Vec = generate_railway_caps(p0, v_left, v_right, neg_tangent); + triangle_vertices.extend(cap_tris); + } + + // End Cap (at index len-1) + if points.len() >= 2 { + let last = points.len() - 1; + let p_last = points[last]; + let p_prev = points[last-1]; + let tangent = normalize([p_last[0]-p_prev[0], p_last[1]-p_prev[1]]); + + let v_left = point_pairs[last*2]; + let v_right = point_pairs[last*2 + 1]; + + let cap_tris: Vec = generate_railway_caps(p_last, v_left, v_right, tangent); + triangle_vertices.extend(cap_tris); + } triangle_vertices } +pub fn generate_railway_caps( + p: [f32; 2], + v_left: RailwayVertex, + v_right: RailwayVertex, + tangent_direction: [f32; 2] // Direction to extrude (outwards from line) +) -> Vec { + // Generate two new vertices that are extruded "outwards" along the tangent + // Vertex shader uses: pos = center + normal * width + // We want: pos = center + (normal_component + tangent_component) * width + // So we just add the tangent to the normal vector of the existing vertices. + + // Left Cap Vertex + let v_cap_left = RailwayVertex { + center: p, + normal: [v_left.normal[0] + tangent_direction[0], v_left.normal[1] + tangent_direction[1]], + ..v_left + }; + + // Right Cap Vertex + let v_cap_right = RailwayVertex { + center: p, + normal: [v_right.normal[0] + tangent_direction[0], v_right.normal[1] + tangent_direction[1]], + ..v_right + }; + + // Return as a quad strip sequence (v_left, v_cap_left, v_cap_right, etc for triangulation) + // Actually method above expects triangles. + // T1: v_left, v_cap_left, v_cap_right + // T2: v_left, v_cap_right, v_right + vec![ + v_left, v_cap_left, v_cap_right, + v_left, v_cap_right, v_right + ] +} + pub fn create_railway_pipeline( device: &wgpu::Device, format: &wgpu::TextureFormat, - bind_group_layout: &wgpu::BindGroupLayout + bind_group_layout: &wgpu::BindGroupLayout, + tile_bind_group_layout: &wgpu::BindGroupLayout ) -> wgpu::RenderPipeline { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: None, @@ -125,6 +188,13 @@ pub fn create_railway_pipeline( }; @group(0) @binding(0) var camera: CameraUniform; + + struct TileUniform { + origin: vec2, + scale: f32, + }; + @group(1) @binding(0) + var tile: TileUniform; struct VertexInput { @location(0) center: vec2, @@ -153,11 +223,20 @@ pub fn create_railway_pipeline( base_pixels = 2.0; // U-Bahn - thinner } - // Using 1000.0 constant to make lines thicker (visible on standard screens) + // Calculate line width in WORLD coordinates + // camera.params.x = zoom / aspect (World -> Clip X scale) + // We want: width_world * scale_x = width_clip + // width_clip = pixel_width / screen_width * 2.0 + // For now, continuing with previous scale factor logic approx: let width = base_pixels / (camera.params.x * 1000.0); let offset = model.normal * width; - let world_pos = model.center + offset; + + // Tile-Relative to World Coordinate Transformation + // model.center is 0.0-1.0 relative to tile + // tile.origin is top-left of tile in world coords + // tile.scale is size of tile in world coords + let world_pos = tile.origin + (model.center * tile.scale) + offset; let x = world_pos.x * camera.params.x + camera.params.z; let y = world_pos.y * camera.params.y + camera.params.w; @@ -193,12 +272,12 @@ pub fn create_railway_pipeline( // When transit mode is OFF: Dim/hide railways if (!is_transit_mode) { // Transit mode is OFF - hide all railways - return vec4(final_color, 0.0); + discard; } else { // Transit mode is ON - show railways // Dim non-colored railways slightly to emphasize colored ones if (!has_color) { - return vec4(mix(final_color, vec3(0.5, 0.5, 0.5), 0.3), 0.5); + return vec4(mix(final_color, vec3(0.5, 0.5, 0.5), 0.3), 1.0); } return vec4(final_color, 1.0); } @@ -208,7 +287,7 @@ pub fn create_railway_pipeline( let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Railway Pipeline Layout"), - bind_group_layouts: &[bind_group_layout], + bind_group_layouts: &[bind_group_layout, tile_bind_group_layout], push_constant_ranges: &[], }); @@ -227,7 +306,7 @@ pub fn create_railway_pipeline( entry_point: "fs_main", targets: &[Some(wgpu::ColorTargetState { format: *format, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), + blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], }), diff --git a/frontend/src/services/render_service.rs b/frontend/src/services/render_service.rs index 72d0487..919ef14 100644 --- a/frontend/src/services/render_service.rs +++ b/frontend/src/services/render_service.rs @@ -70,7 +70,7 @@ impl RenderService { building_pipeline: create_colored_building_pipeline(device, format, camera_layout), water_pipeline: create_water_pipeline(device, format, camera_layout, &tile_bind_group_layout), water_line_pipeline: create_water_line_pipeline(device, format, camera_layout), - railway_pipeline: create_railway_pipeline(device, format, camera_layout), + railway_pipeline: create_railway_pipeline(device, format, camera_layout, &tile_bind_group_layout), motorway_outline: create_road_motorway_outline_pipeline(device, format, camera_layout), motorway_fill: create_road_motorway_pipeline(device, format, camera_layout), primary_outline: create_road_primary_outline_pipeline(device, format, camera_layout), @@ -86,7 +86,7 @@ impl RenderService { } } - pub fn create_tile_buffers(device: &wgpu::Device, state: &mut AppState, tile: (i32, i32, i32)) { + pub fn create_tile_buffers(device: &wgpu::Device, state: &mut AppState, tile: (i32, i32, i32), tile_bind_group_layout: &wgpu::BindGroupLayout) { // Build vertex data for each feature type - roads use RoadVertex let mut road_motorway_vertex_data: Vec = Vec::new(); let mut road_primary_vertex_data: Vec = Vec::new(); @@ -164,7 +164,6 @@ impl RenderService { Some("fire_station") | Some("courthouse") | Some("embassy") ); - // Assign color based on building type (light theme colors) // Assign color based on building type (light theme colors) // Colors darkened for better visibility against #F5F4F0 background let color: [f32; 3] = if is_public_amenity { @@ -267,21 +266,43 @@ impl RenderService { // Process railways if let Some(railways) = state.railways.get(&tile) { + // Calculate tile origin and scale for relative coordinates (re-calculated here for loop) + let (z, x, y) = tile; + let tile_count = 2_f64.powi(z); + let tile_size = 1.0 / tile_count; + let tile_origin_x = x as f64 * tile_size; + let tile_origin_y = y as f64 * tile_size; + for railway in railways { let mut centers: Vec<[f32; 2]> = Vec::new(); + + // CRITICAL FIX: Match labels.rs logic - read as F32 (8 bytes per point) + // Previous logic used 16 bytes (f64), but if labels are working, data MUST be f32. for chunk in railway.points.chunks(8) { if chunk.len() < 8 { break; } - let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0u8; 4])); - let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4])); - let (x, y) = project(lat as f64, lon as f64); - centers.push([x as f32, y as f32]); + + // Read as f32 then cast to f64 for projection precision + 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; + + // Project to global + let (gx, gy) = crate::geo::project_high_precision(lat, lon); + + // Convert to tile-relative (0.0 - 1.0) + let rx = ((gx - tile_origin_x) / tile_size) as f32; + let ry = ((gy - tile_origin_y) / tile_size) as f32; + + centers.push([rx, ry]); } + // Determine color + // ... (rest of logic) + // Get color from tags (OSM uses 'colour', but also check 'color') let color = railway.tags.get("colour") .or_else(|| railway.tags.get("color")) .and_then(|c| parse_hex_color(c)) - .unwrap_or([0.5, 0.5, 0.5]); // Default gray if no color + .unwrap_or([0.5, 0.5, 0.5]); // Default to grey if no color specified // Determine railway type - only S-Bahn and U-Bahn, skip tram // 0.0 = S-Bahn (solid line with white outline, wider) @@ -392,21 +413,6 @@ impl RenderService { // Get RenderService to access tile_bind_group_layout // We need to pass it from lib.rs or store it somewhere accessible // For now, we'll recreate it (not ideal but functional) - let tile_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - } - ], - label: Some("tile_bind_group_layout"), - }); let tile_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &tile_bind_group_layout, diff --git a/importer/src/main.rs b/importer/src/main.rs index 0c3d165..da072ad 100644 --- a/importer/src/main.rs +++ b/importer/src/main.rs @@ -501,7 +501,13 @@ async fn main() -> Result<()> { if is_railway { let (first_lat, first_lon) = simplified_points[0]; - railway_store.insert_way(id, tags.clone(), line_blob.clone(), first_lat, first_lon); + // Serialize as f64 for high precision + let mut railway_blob = Vec::with_capacity(simplified_points.len() * 16); + for (lat, lon) in &simplified_points { + railway_blob.extend_from_slice(&lat.to_le_bytes()); + railway_blob.extend_from_slice(&lon.to_le_bytes()); + } + railway_store.insert_way(id, tags.clone(), railway_blob, first_lat, first_lon); } } } @@ -704,14 +710,28 @@ async fn main() -> Result<()> { let (x, y) = TileService::lat_lon_to_tile(railway.first_lat, railway.first_lon, zoom); let zoom_i32 = zoom as i32; - // Parse geometry from blob and generate railway mesh + // Parse geometry from blob (f64) and generate tile-relative railway mesh let mut points: Vec<[f32; 2]> = Vec::new(); - for chunk in railway.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; - let (x, y) = GeometryService::project(lat, lon); - points.push([x, y]); + + // Calculate tile origin and scale + let tile_count = 2_f64.powi(zoom as i32); + let tile_size = 1.0 / tile_count; + let tile_origin_x = x as f64 * tile_size; + let tile_origin_y = y as f64 * tile_size; + + for chunk in railway.points.chunks(16) { + if chunk.len() < 16 { break; } + let lat = f64::from_le_bytes(chunk[0..8].try_into().unwrap_or([0u8; 8])); + let lon = f64::from_le_bytes(chunk[8..16].try_into().unwrap_or([0u8; 8])); + + // Project to Global + let (gx, gy) = GeometryService::project_high_precision(lat, lon); + + // Convert to Tile-Relative (0.0 to 1.0) + let rx = ((gx as f64 - tile_origin_x) / tile_size) as f32; + let ry = ((gy as f64 - tile_origin_y) / tile_size) as f32; + + points.push([rx, ry]); } // Parse color and rail type @@ -733,7 +753,7 @@ async fn main() -> Result<()> { let rail_type_str = tags.get("railway").map(|s| s.as_str()).unwrap_or("rail"); let rail_type: f32 = match rail_type_str { "subway" => 1.0, - "tram" => 2.0, + // "tram" => 2.0, // Disabled _ => 0.0, }; diff --git a/importer/src/services/filtering_service.rs b/importer/src/services/filtering_service.rs index e902549..6f86c58 100644 --- a/importer/src/services/filtering_service.rs +++ b/importer/src/services/filtering_service.rs @@ -59,7 +59,7 @@ impl FilteringService { 12 => { matches!(highway, Some("motorway" | "trunk" | "primary" | "secondary" | "tertiary" | "residential" | "unclassified" | "pedestrian" | "service" | "track")) || // Added minor roads matches!(place, Some("city" | "town" | "village")) || - matches!(railway, Some("rail" | "subway" | "light_rail" | "narrow_gauge" | "tram")) || // Added tram + matches!(railway, Some("rail" | "subway" | "light_rail" | "narrow_gauge")) || // Removed tram tags.contains_key("building") || tags.contains_key("landuse") || tags.contains_key("leisure") || diff --git a/importer/src/services/geometry_service.rs b/importer/src/services/geometry_service.rs index 3a0a88f..fea2ae1 100644 --- a/importer/src/services/geometry_service.rs +++ b/importer/src/services/geometry_service.rs @@ -102,4 +102,17 @@ impl GeometryService { (x, y) } + + /// High precision Web Mercator Projection + /// Returns (x, y) in range [0.0, 1.0] for the whole world as f64 + pub fn project_high_precision(lat: f64, lon: f64) -> (f64, f64) { + 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; + + let x = if x.is_finite() { x.clamp(0.0, 1.0) } else { 0.5 }; + let y = if y.is_finite() { y.clamp(0.0, 1.0) } else { 0.5 }; + + (x, y) + } } diff --git a/importer/src/services/mesh_service.rs b/importer/src/services/mesh_service.rs index e163146..ee6c6a7 100644 --- a/importer/src/services/mesh_service.rs +++ b/importer/src/services/mesh_service.rs @@ -176,7 +176,7 @@ impl MeshGenerationService { let dx = p2[0] - p1[0]; let dy = p2[1] - p1[1]; let len = (dx * dx + dy * dy).sqrt(); - if len < 0.000001 { + if len < 0.000000001 { segment_normals.push([0.0, 0.0]); degenerate_count += 1; } else { @@ -293,7 +293,7 @@ impl MeshGenerationService { let dx = p2[0] - p1[0]; let dy = p2[1] - p1[1]; let len = (dx * dx + dy * dy).sqrt(); - if len < 0.000001 { + if len < 0.000000001 { segment_normals.push([0.0, 0.0]); } else { segment_normals.push([-dy / len, dx / len]);