This commit is contained in:
Dongho Kim
2025-12-29 03:44:27 +09:00
parent 3885ddd977
commit f3f1a568e2
14 changed files with 380 additions and 100 deletions

View File

@@ -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"

View File

@@ -510,6 +510,68 @@
</head>
<body>
<div id="loading-screen">
<div class="spinner"></div>
<div class="loading-text">Loading Maps...</div>
</div>
<style>
#loading-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #1a1a1a;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
transition: opacity 0.5s ease-out;
}
#loading-screen.fade-out {
opacity: 0;
pointer-events: none;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-radius: 50%;
border-top-color: #007AFF;
animation: spin 1s ease-in-out infinite;
margin-bottom: 16px;
}
.loading-text {
color: rgba(255, 255, 255, 0.8);
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.5px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
[data-theme="light"] #loading-screen {
background-color: #f5f5f7;
}
[data-theme="light"] .loading-text {
color: rgba(0, 0, 0, 0.6);
}
[data-theme="light"] .spinner {
border-color: rgba(0, 0, 0, 0.1);
border-top-color: #007AFF;
}
</style>
<div id="user-location"></div>
<div id="compass">
<div class="direction n">N</div>

View File

@@ -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 {

View File

@@ -222,7 +222,9 @@ pub fn extract_labels(tile_data: &TileData) -> Vec<CachedLabel> {
}
}
// 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<String, Vec<&crate::types::MapWay>> = 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<CachedLabel> {
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

View File

@@ -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::<dyn FnMut()>::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);
}

View File

@@ -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<RailwayVertex> {
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<RailwayVertex> = 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<RailwayVertex> = 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<RailwayVertex> = 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<RailwayVertex> {
// 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<uniform> camera: CameraUniform;
struct TileUniform {
origin: vec2<f32>,
scale: f32,
};
@group(1) @binding(0)
var<uniform> tile: TileUniform;
struct VertexInput {
@location(0) center: vec2<f32>,
@@ -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<f32>(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<f32>(mix(final_color, vec3<f32>(0.5, 0.5, 0.5), 0.3), 0.5);
return vec4<f32>(mix(final_color, vec3<f32>(0.5, 0.5, 0.5), 0.3), 1.0);
}
return vec4<f32>(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,
})],
}),

View File

@@ -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<RoadVertex> = Vec::new();
let mut road_primary_vertex_data: Vec<RoadVertex> = 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,