This commit is contained in:
Dongho Kim
2025-12-03 04:01:36 +09:00
parent afdcf23222
commit 003aae2b6b
10 changed files with 915 additions and 348 deletions

View File

@@ -9,6 +9,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
"Document",
"Window",
@@ -33,6 +34,7 @@ web-sys = { version = "0.3", features = [
"RequestMode",
"Response",
"HtmlInputElement",
"PositionOptions",
] }
wgpu = { version = "0.19", default-features = false, features = ["webgl", "wgsl"] }
winit = { version = "0.29", default-features = false, features = ["rwh_06"] }
@@ -43,4 +45,4 @@ console_log = "1.0"
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
earcutr = "0.4"
bincode = "1.3"

10
frontend/Trunk.toml Normal file
View File

@@ -0,0 +1,10 @@
[build]
target = "index.html"
[serve]
address = "127.0.0.1"
port = 8080
[[proxy]]
rewrite = "/api/"
backend = "http://localhost:3000/api/"

View File

@@ -68,10 +68,122 @@
padding: 5px 10px;
border-radius: 4px;
}
#compass {
position: absolute;
top: 20px;
left: 20px;
width: 60px;
height: 60px;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(5px);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
pointer-events: none;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
z-index: 100;
}
.direction {
position: absolute;
font-size: 14px;
font-family: monospace;
}
.n {
top: 4px;
color: #ff5555;
}
.s {
bottom: 4px;
color: #ddd;
}
.e {
right: 6px;
color: #ddd;
}
.w {
left: 6px;
color: #ddd;
}
.compass-center {
width: 4px;
height: 4px;
background: white;
border-radius: 50%;
}
.compass-arrow {
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 20px solid #ff5555;
transform: translate(-50%, -100%);
}
</style>
</head>
<body>
<div id="compass">
<div class="direction n">N</div>
<div class="direction e">E</div>
<div class="direction s">S</div>
<div class="direction w">W</div>
<div class="compass-arrow"></div>
<div class="compass-center"></div>
</div>
<div id="labels"></div>
<style>
#labels {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
overflow: hidden;
}
.label {
position: absolute;
transform: translate(-50%, -50%);
color: white;
text-shadow: 0 0 2px black, 0 0 4px black;
font-family: sans-serif;
white-space: nowrap;
pointer-events: none;
}
.label-country {
font-size: 16px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
color: #ffdddd;
}
.label-city {
font-size: 12px;
font-weight: normal;
color: #ffffff;
}
</style>
<div id="ui-container">
<div class="control-group">
<button id="btn-zoom-in">+</button>

View File

@@ -10,7 +10,6 @@ use winit::{
use winit::platform::web::WindowExtWebSys;
use serde::Deserialize;
use wgpu::util::DeviceExt;
use earcutr::earcut;
#[derive(Deserialize, Debug, Clone)]
struct MapNode {
@@ -49,9 +48,9 @@ impl Camera {
CameraUniform {
params: [
self.zoom / self.aspect, // scale_x
self.zoom, // scale_y
-self.x * (self.zoom / self.aspect), // translate_x (simplified)
-self.y * self.zoom, // translate_y
-self.zoom, // scale_y (flipped for North-Up)
-self.x * (self.zoom / self.aspect), // translate_x
self.y * self.zoom, // translate_y (flipped sign)
],
}
}
@@ -115,11 +114,65 @@ fn project(lat: f64, lon: f64) -> (f32, f32) {
(x as f32, y as f32)
}
#[derive(Debug, Clone)]
struct KalmanFilter {
lat: f64,
lon: f64,
variance: f64,
timestamp: f64,
}
impl KalmanFilter {
fn new(lat: f64, lon: f64, timestamp: f64) -> Self {
Self {
lat,
lon,
variance: 0.0,
timestamp,
}
}
fn process(&mut self, lat: f64, lon: f64, accuracy: f64, timestamp: f64) -> (f64, f64) {
if accuracy <= 0.0 { return (self.lat, self.lon); }
let dt = timestamp - self.timestamp;
if dt < 0.0 { return (self.lat, self.lon); }
// Process noise variance (meters per second)
let q_metres_per_sec = 3.0;
let variance_process = q_metres_per_sec * q_metres_per_sec * dt / 1000.0;
// Prediction step
let variance = self.variance + variance_process;
// Update step
let measurement_variance = accuracy * accuracy;
let k = variance / (variance + measurement_variance);
self.lat = self.lat + k * (lat - self.lat);
self.lon = self.lon + k * (lon - self.lon);
self.variance = (1.0 - k) * variance;
self.timestamp = timestamp;
(self.lat, self.lon)
}
}
#[derive(Deserialize, Debug, Clone)]
struct MapWay {
id: i64,
tags: std::collections::HashMap<String, String>,
points: Vec<Vec<f64>>,
points: Vec<u8>,
}
#[derive(Deserialize, Debug, Clone)]
struct TileData {
nodes: Vec<MapNode>,
ways: Vec<MapWay>,
buildings: Vec<MapWay>,
landuse: Vec<MapWay>,
water: Vec<MapWay>,
railways: Vec<MapWay>,
}
struct TileBuffers {
@@ -135,6 +188,8 @@ struct TileBuffers {
water_index_count: u32,
railway_vertex_buffer: wgpu::Buffer,
railway_vertex_count: u32,
road_mesh_vertex_buffer: wgpu::Buffer,
road_mesh_vertex_count: u32,
}
struct AppState {
@@ -148,6 +203,8 @@ struct AppState {
loaded_tiles: HashSet<(i32, i32, i32)>,
pending_tiles: HashSet<(i32, i32, i32)>,
user_location: Option<(f64, f64)>, // (lat, lon)
kalman_filter: Option<KalmanFilter>,
watch_id: Option<i32>,
}
fn create_road_mesh(points: &[[f64; 2]], width: f32) -> Vec<Vertex> {
@@ -189,10 +246,10 @@ fn create_road_mesh(points: &[[f64; 2]], width: f32) -> Vec<Vertex> {
vertices
}
async fn fetch_cached(url: &str) -> Option<String> {
async fn fetch_cached(url: &str) -> Option<Vec<u8>> {
let window = web_sys::window()?;
let caches = window.caches().ok()?;
let cache_name = "map-data-v2";
let cache_name = "map-data-v3-combined";
let cache = wasm_bindgen_futures::JsFuture::from(caches.open(cache_name)).await.ok()?;
let cache: web_sys::Cache = cache.dyn_into().ok()?;
@@ -202,9 +259,10 @@ async fn fetch_cached(url: &str) -> Option<String> {
if !match_val.is_undefined() {
let response: web_sys::Response = match_val.dyn_into().ok()?;
let text_promise = response.text().ok()?;
let text = wasm_bindgen_futures::JsFuture::from(text_promise).await.ok()?;
return text.as_string();
let buffer_promise = response.array_buffer().ok()?;
let buffer = wasm_bindgen_futures::JsFuture::from(buffer_promise).await.ok()?;
let array = js_sys::Uint8Array::new(&buffer);
return Some(array.to_vec());
}
// Network fetch
@@ -216,9 +274,10 @@ async fn fetch_cached(url: &str) -> Option<String> {
let put_promise = cache.put_with_request(&request, &response_clone);
wasm_bindgen_futures::JsFuture::from(put_promise).await.ok()?;
let text_promise = response.text().ok()?;
let text = wasm_bindgen_futures::JsFuture::from(text_promise).await.ok()?;
text.as_string()
let buffer_promise = response.array_buffer().ok()?;
let buffer = wasm_bindgen_futures::JsFuture::from(buffer_promise).await.ok()?;
let array = js_sys::Uint8Array::new(&buffer);
Some(array.to_vec())
}
fn get_visible_tiles(camera: &Camera) -> Vec<(i32, i32, i32)> {
@@ -227,11 +286,15 @@ fn get_visible_tiles(camera: &Camera) -> Vec<(i32, i32, i32)> {
// Zoom 9: Region view
// Zoom 12: City view
// Zoom 14: Street view
let z = if camera.zoom < 500.0 {
6
let z = if camera.zoom < 100.0 {
2
} else if camera.zoom < 500.0 {
4
} else if camera.zoom < 2000.0 {
6
} else if camera.zoom < 5000.0 {
9
} else if camera.zoom < 8000.0 {
} else if camera.zoom < 10000.0 {
12
} else {
14
@@ -260,6 +323,24 @@ fn get_visible_tiles(camera: &Camera) -> Vec<(i32, i32, i32)> {
tiles
}
fn get_parent_tile(z: i32, x: i32, y: i32) -> Option<(i32, i32, i32)> {
// Hierarchy: 14 -> 12 -> 9 -> 6 -> 2
let parent_z = match z {
14 => 12,
12 => 9,
9 => 6,
6 => 4,
4 => 2,
_ => return None,
};
// Calculate scale difference
let diff = z - parent_z;
let factor = 2i32.pow(diff as u32);
Some((parent_z, x / factor, y / factor))
}
#[wasm_bindgen(start)]
pub async fn run() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
@@ -361,6 +442,8 @@ pub async fn run() {
loaded_tiles: HashSet::new(),
pending_tiles: HashSet::new(),
user_location: None,
kalman_filter: None,
watch_id: None,
}));
// Zoom constants
@@ -453,10 +536,25 @@ pub async fn run() {
if let Some(btn) = btn_location {
let closure = wasm_bindgen::closure::Closure::<dyn FnMut()>::new(move || {
let window = web_sys::window().unwrap();
let navigator = window.navigator();
let geolocation = navigator.geolocation().unwrap();
let mut state_guard = state_clone.lock().unwrap();
// Toggle off if already watching
if let Some(id) = state_guard.watch_id {
geolocation.clear_watch(id);
state_guard.watch_id = None;
state_guard.kalman_filter = None;
state_guard.user_location = None; // Optional: clear location on stop
web_sys::console::log_1(&"Location tracking stopped".into());
window_clone.request_redraw();
return;
}
drop(state_guard); // Release lock before starting new watch
let camera_clone2 = camera_clone.clone();
let window_clone2 = window_clone.clone();
let state_clone2 = state_clone.clone();
@@ -465,31 +563,64 @@ pub async fn run() {
let coords = position.coords();
let lat = coords.latitude();
let lon = coords.longitude();
let accuracy = coords.accuracy();
let timestamp = position.timestamp();
// Update state with user location
let mut state_guard = state_clone2.lock().unwrap();
state_guard.user_location = Some((lat, lon));
let (smooth_lat, smooth_lon) = if let Some(filter) = &mut state_guard.kalman_filter {
filter.process(lat, lon, accuracy, timestamp)
} else {
// Initialize filter
let mut filter = KalmanFilter::new(lat, lon, timestamp);
// Run first process to set initial variance if needed, or just use raw
// Actually new() sets variance to 0, so it trusts the first point 100%
state_guard.kalman_filter = Some(filter);
(lat, lon)
};
state_guard.user_location = Some((smooth_lat, smooth_lon));
drop(state_guard);
// Center camera on location
let (x, y) = project(lat, lon);
// Center camera on location (maybe only on first update? or always? "Follow me" mode)
// For now, let's just update the marker. If we want "Follow me", we'd update camera too.
// The original code centered camera. Let's keep doing that for now, effectively "Follow me".
// But maybe only if the user hasn't dragged away?
// For simplicity, let's center every time for now, as that's what "Fused Location" implies usually (navigation).
let (x, y) = project(smooth_lat, smooth_lon);
let mut cam = camera_clone2.lock().unwrap();
cam.x = x;
cam.y = y;
cam.zoom = 8000.0; // Zoom in to street level
// cam.zoom = 8000.0; // Don't force zoom every time, let user control it
drop(cam);
window_clone2.request_redraw();
});
let error_callback = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::PositionError)>::new(move |_error: web_sys::PositionError| {
web_sys::console::log_1(&"Geolocation error".into());
let error_callback = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::PositionError)>::new(move |error: web_sys::PositionError| {
web_sys::console::log_1(&format!("Geolocation error: {:?}", error.message()).into());
});
geolocation.get_current_position_with_error_callback(
let mut options = web_sys::PositionOptions::new();
options.set_enable_high_accuracy(true);
options.set_timeout(5000);
options.set_maximum_age(0);
match geolocation.watch_position_with_error_callback_and_options(
success_callback.as_ref().unchecked_ref(),
Some(error_callback.as_ref().unchecked_ref())
).unwrap();
Some(error_callback.as_ref().unchecked_ref()),
&options
) {
Ok(id) => {
let mut state_guard = state_clone.lock().unwrap();
state_guard.watch_id = Some(id);
web_sys::console::log_1(&"Location tracking started (High Accuracy)".into());
},
Err(e) => {
web_sys::console::log_1(&format!("Failed to start watch: {:?}", e).into());
}
}
success_callback.forget();
error_callback.forget();
@@ -516,6 +647,7 @@ pub async fn run() {
let landuse_pipeline = create_landuse_pipeline(&device, &config.format, &camera_bind_group_layout);
let water_pipeline = create_water_pipeline(&device, &config.format, &camera_bind_group_layout);
let railway_pipeline = create_railway_pipeline(&device, &config.format, &camera_bind_group_layout);
let road_mesh_pipeline = create_road_mesh_pipeline(&device, &config.format, &camera_bind_group_layout);
let window_clone = window.clone();
@@ -546,7 +678,7 @@ pub async fn run() {
let world_dy = dy / (config.height as f32 * 0.5) / cam.zoom;
cam.x -= world_dx;
cam.y += world_dy;
cam.y -= world_dy;
window.request_redraw();
}
@@ -607,39 +739,81 @@ pub async fn run() {
}
// 1. Check visible tiles
let visible_tiles = if cam.zoom > 200.0 {
get_visible_tiles(&cam)
} else {
Vec::new()
};
let visible_tiles = get_visible_tiles(&cam);
// Drop camera lock early to avoid holding it during long operations if possible,
// but we need it for `to_uniform`. We can clone the needed data.
let camera_uniform_data = cam.to_uniform();
let camera_zoom = cam.zoom; // Capture zoom for road mesh width calculations
drop(cam); // Release lock
let mut state_guard = state.lock().unwrap();
let mut needs_fetch = Vec::new();
// Determine what to render:
// For each visible tile, if it's loaded, render it.
// If NOT loaded, try to find a loaded parent to render as fallback.
let mut tiles_to_render_set = HashSet::new();
for tile in &visible_tiles {
if !state_guard.loaded_tiles.contains(tile) && !state_guard.pending_tiles.contains(tile) {
state_guard.pending_tiles.insert(*tile);
needs_fetch.push(*tile);
if state_guard.loaded_tiles.contains(tile) {
tiles_to_render_set.insert(*tile);
} else {
// Tile not ready, request it
if !state_guard.pending_tiles.contains(tile) {
state_guard.pending_tiles.insert(*tile);
needs_fetch.push(*tile);
}
// Look for fallback parent
let (mut z, mut x, mut y) = *tile;
while let Some(parent) = get_parent_tile(z, x, y) {
if state_guard.loaded_tiles.contains(&parent) {
tiles_to_render_set.insert(parent);
break; // Found a valid parent, stop looking up
}
(z, x, y) = parent;
}
}
}
let visible_set: HashSet<_> = visible_tiles.iter().cloned().collect();
state_guard.loaded_tiles.retain(|t| visible_set.contains(t));
state_guard.nodes.retain(|t, _| visible_set.contains(t));
state_guard.ways.retain(|t, _| visible_set.contains(t));
state_guard.buffers.retain(|t, _| visible_set.contains(t));
// Cleanup: Retain loaded tiles that are either visible OR are being used as fallbacks
// We also want to keep recently used parents for a bit to avoid thrashing,
// but for now, strict "is in render set" is a good start.
// However, we must ALSO keep the `visible_tiles` that are loading, otherwise they'll never finish?
// No, `pending_tiles` tracks loading. `loaded_tiles` tracks what we have.
// We should only delete loaded tiles that are NO LONGER USEFUL.
// A tile is useful if:
// 1. It is directly visible.
// 2. It is a parent of a visible tile (even if that visible tile is loaded? No, only if needed).
// Actually, keeping parents is good for zooming out too.
// Simple strategy: Keep all visible tiles + all tiles currently in rendering set.
let mut useful_tiles = tiles_to_render_set.clone();
for t in &visible_tiles {
if state_guard.loaded_tiles.contains(t) {
useful_tiles.insert(*t);
}
}
state_guard.loaded_tiles.retain(|t| useful_tiles.contains(t));
state_guard.nodes.retain(|t, _| useful_tiles.contains(t));
state_guard.ways.retain(|t, _| useful_tiles.contains(t));
state_guard.buildings.retain(|t, _| useful_tiles.contains(t));
state_guard.landuse.retain(|t, _| useful_tiles.contains(t));
state_guard.water.retain(|t, _| useful_tiles.contains(t));
state_guard.railways.retain(|t, _| useful_tiles.contains(t));
state_guard.buffers.retain(|t, _| useful_tiles.contains(t));
// 3. Create buffers for new tiles if needed
for tile in &visible_tiles {
// We iterate over useful_tiles because we might need to create buffers for a parent that was just loaded
// but not previously rendered (e.g. if we zoomed in fast and missed it, but now need it as fallback).
for tile in &useful_tiles {
if !state_guard.buffers.contains_key(tile) {
let mut point_instance_data = Vec::new();
let mut road_vertex_data = Vec::new();
let mut building_vertex_data = Vec::new();
// Road mesh disabled
let mut building_vertex_data: Vec<Vertex> = Vec::new();
let mut landuse_vertex_data = Vec::new();
let mut water_vertex_data = Vec::new();
@@ -651,15 +825,25 @@ pub async fn run() {
}
}
// Process ways (roads) - simple line rendering
// Process ways (roads) - simple line rendering OR mesh
if let Some(ways) = state_guard.ways.get(tile) {
for way in ways {
if way.points.len() < 2 { continue; }
let points = &way.points;
if points.len() < 16 { continue; } // Need at least 2 points (16 bytes)
// Draw as simple lines
for i in 0..way.points.len() - 1 {
let p1 = &way.points[i];
let p2 = &way.points[i+1];
// Parse points first
let mut parsed_points = Vec::new();
for chunk in points.chunks(8) {
if chunk.len() < 8 { break; }
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap()) as f64;
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap()) as f64;
parsed_points.push([lat, lon]);
}
// Use simple line rendering for all roads (mesh disabled due to artifacts)
for i in 0..parsed_points.len() - 1 {
let p1 = parsed_points[i];
let p2 = parsed_points[i+1];
let (x1, y1) = project(p1[0], p1[1]);
let (x2, y2) = project(p2[0], p2[1]);
road_vertex_data.push(Vertex { position: [x1, y1] });
@@ -671,53 +855,26 @@ pub async fn run() {
// Process buildings
if let Some(buildings) = state_guard.buildings.get(tile) {
for building in buildings {
if building.points.len() < 3 { continue; }
let mut flat_points = Vec::new();
let mut projected_points = Vec::new();
for p in &building.points {
let (x, y) = project(p[0], p[1]);
flat_points.push(x as f64);
flat_points.push(y as f64);
projected_points.push([x, y]);
}
// Earcut triangulation
let indices = match earcut(&flat_points, &[], 2) {
Ok(i) => i,
Err(_) => continue,
};
for i in indices {
let p = projected_points[i];
building_vertex_data.push(Vertex { position: p });
let points = &building.points;
for chunk in points.chunks(8) {
if chunk.len() < 8 { break; }
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap()) as f64;
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap()) as f64;
let (x, y) = project(lat, lon);
building_vertex_data.push(Vertex { position: [x, y] });
}
}
}
// Process landuse
if let Some(landuse) = state_guard.landuse.get(tile) {
for area in landuse {
if area.points.len() < 3 { continue; }
let mut flat_points = Vec::new();
let mut projected_points = Vec::new();
for p in &area.points {
let (x, y) = project(p[0], p[1]);
flat_points.push(x as f64);
flat_points.push(y as f64);
projected_points.push([x, y]);
}
let indices = match earcut(&flat_points, &[], 2) {
Ok(i) => i,
Err(_) => continue,
};
for i in indices {
let p = projected_points[i];
landuse_vertex_data.push(Vertex { position: p });
let points = &area.points;
for chunk in points.chunks(8) {
if chunk.len() < 8 { break; }
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap()) as f64;
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap()) as f64;
let (x, y) = project(lat, lon);
landuse_vertex_data.push(Vertex { position: [x, y] });
}
}
}
@@ -725,25 +882,13 @@ pub async fn run() {
// Process water
if let Some(water) = state_guard.water.get(tile) {
for area in water {
if area.points.len() < 3 { continue; }
let mut flat_points = Vec::new();
let mut projected_points = Vec::new();
for p in &area.points {
let (x, y) = project(p[0], p[1]);
flat_points.push(x as f64);
flat_points.push(y as f64);
projected_points.push([x, y]);
}
let indices = match earcut(&flat_points, &[], 2) {
Ok(i) => i,
Err(_) => continue,
};
for i in indices {
let p = projected_points[i];
water_vertex_data.push(Vertex { position: p });
let points = &area.points;
for chunk in points.chunks(8) {
if chunk.len() < 8 { break; }
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap()) as f64;
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap()) as f64;
let (x, y) = project(lat, lon);
water_vertex_data.push(Vertex { position: [x, y] });
}
}
}
@@ -752,15 +897,27 @@ pub async fn run() {
let mut railway_vertex_data = Vec::new();
if let Some(railways) = state_guard.railways.get(tile) {
for way in railways {
if way.points.len() < 2 { continue; }
let points = &way.points;
if points.len() < 16 { continue; }
for i in 0..way.points.len() - 1 {
let p1 = &way.points[i];
let p2 = &way.points[i+1];
let (x1, y1) = project(p1[0], p1[1]);
let (x2, y2) = project(p2[0], p2[1]);
railway_vertex_data.push(Vertex { position: [x1, y1] });
railway_vertex_data.push(Vertex { position: [x2, y2] });
let mut chunks = points.chunks(8);
let mut prev_x = 0.0;
let mut prev_y = 0.0;
let mut first = true;
while let Some(chunk) = chunks.next() {
if chunk.len() < 8 { break; }
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap()) as f64;
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap()) as f64;
let (x, y) = project(lat, lon);
if !first {
railway_vertex_data.push(Vertex { position: [prev_x, prev_y] });
railway_vertex_data.push(Vertex { position: [x, y] });
}
prev_x = x;
prev_y = y;
first = false;
}
}
}
@@ -778,6 +935,12 @@ pub async fn run() {
contents: bytemuck::cast_slice(&road_vertex_data),
usage: wgpu::BufferUsages::VERTEX,
});
// Road mesh disabled - create empty buffer
let road_mesh_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Empty Road Mesh Buffer"),
contents: bytemuck::cast_slice(&Vec::<Vertex>::new()),
usage: wgpu::BufferUsages::VERTEX,
});
let building_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Tile Building Buffer"),
@@ -817,14 +980,20 @@ pub async fn run() {
water_index_count: water_vertex_data.len() as u32,
railway_vertex_buffer: railway_buffer,
railway_vertex_count: railway_vertex_data.len() as u32,
road_mesh_vertex_buffer: road_mesh_buffer,
road_mesh_vertex_count: 0, // Mesh disabled
}));
}
}
}
// Collect buffers for rendering
// Sort by zoom level (low -> high) so parents are drawn first (background) and children on top
let mut tiles_to_render_vec: Vec<_> = tiles_to_render_set.into_iter().collect();
tiles_to_render_vec.sort_by_key(|(z, _, _)| *z);
let mut tiles_to_render = Vec::new();
for tile in &visible_tiles {
for tile in &tiles_to_render_vec {
if let Some(buffers) = state_guard.buffers.get(tile) {
tiles_to_render.push(buffers.clone());
}
@@ -838,92 +1007,34 @@ pub async fn run() {
let state_clone = state.clone();
let window_clone_for_fetch = window.clone();
wasm_bindgen_futures::spawn_local(async move {
// Fetch nodes
let url_nodes = format!("/api/tiles/{}/{}/{}", z, x, y);
let nodes_data = if let Some(json) = fetch_cached(&url_nodes).await {
serde_json::from_str::<Vec<MapNode>>(&json).ok()
} else {
None
};
// Fetch all data in one go
let url = format!("/api/tiles/{}/{}/{}/all", z, x, y);
// Fetch ways
let url_ways = format!("/api/tiles/{}/{}/{}/ways", z, x, y);
let ways_data = if let Some(json) = fetch_cached(&url_ways).await {
serde_json::from_str::<Vec<MapWay>>(&json).ok()
let tile_data = if let Some(bytes) = fetch_cached(&url).await {
bincode::deserialize::<TileData>(&bytes).ok()
} else {
None
};
// Fetch buildings
let url_buildings = format!("/api/tiles/{}/{}/{}/buildings", z, x, y);
let buildings_data = if let Some(json) = fetch_cached(&url_buildings).await {
serde_json::from_str::<Vec<MapWay>>(&json).ok()
if let Some(data) = tile_data {
let mut guard = state_clone.lock().unwrap();
guard.nodes.insert((z, x, y), data.nodes);
guard.ways.insert((z, x, y), data.ways);
guard.buildings.insert((z, x, y), data.buildings);
guard.landuse.insert((z, x, y), data.landuse);
guard.water.insert((z, x, y), data.water);
guard.railways.insert((z, x, y), data.railways);
// Mark as loaded
guard.loaded_tiles.insert((z, x, y));
guard.pending_tiles.remove(&(z, x, y));
window_clone_for_fetch.request_redraw();
} else {
None
};
// Fetch landuse
let url_landuse = format!("/api/tiles/{}/{}/{}/landuse", z, x, y);
let landuse_data = if let Some(json) = fetch_cached(&url_landuse).await {
serde_json::from_str::<Vec<MapWay>>(&json).ok()
} else {
None
};
// Fetch water
let url_water = format!("/api/tiles/{}/{}/{}/water", z, x, y);
let water_data = if let Some(json) = fetch_cached(&url_water).await {
serde_json::from_str::<Vec<MapWay>>(&json).ok()
} else {
None
};
// Fetch railways
let url_railways = format!("/api/tiles/{}/{}/{}/railways", z, x, y);
let railways_data = if let Some(json) = fetch_cached(&url_railways).await {
serde_json::from_str::<Vec<MapWay>>(&json).ok()
} else {
None
};
let mut guard = state_clone.lock().unwrap();
if let Some(nodes) = nodes_data {
guard.nodes.insert((z, x, y), nodes);
// Failed to load
let mut guard = state_clone.lock().unwrap();
guard.pending_tiles.remove(&(z, x, y));
}
if let Some(ways) = ways_data {
guard.ways.insert((z, x, y), ways);
}
if let Some(buildings) = buildings_data {
guard.buildings.insert((z, x, y), buildings);
}
if let Some(landuse) = landuse_data {
guard.landuse.insert((z, x, y), landuse);
}
if let Some(water) = water_data {
guard.water.insert((z, x, y), water);
}
if let Some(railways) = railways_data {
guard.railways.insert((z, x, y), railways);
}
guard.loaded_tiles.insert((z, x, y));
guard.pending_tiles.remove(&(z, x, y));
drop(guard);
window_clone_for_fetch.request_redraw();
});
}
@@ -962,9 +1073,9 @@ pub async fn run() {
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.15,
g: 0.15,
b: 0.15,
r: 0.95,
g: 0.95,
b: 0.95,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
@@ -1001,6 +1112,14 @@ pub async fn run() {
rpass.draw(0..buffers.road_vertex_count, 0..1);
}
// Road mesh rendering disabled - using simple lines for all roads
// if buffers.road_mesh_vertex_count > 0 {
// rpass.set_pipeline(&road_mesh_pipeline);
// rpass.set_bind_group(0, &camera_bind_group, &[]);
// rpass.set_vertex_buffer(0, buffers.road_mesh_vertex_buffer.slice(..));
// rpass.draw(0..buffers.road_mesh_vertex_count, 0..1);
// }
// Draw Railways (on top of roads?)
if buffers.railway_vertex_count > 0 {
rpass.set_pipeline(&railway_pipeline);
@@ -1048,8 +1167,33 @@ pub async fn run() {
queue.submit(Some(encoder.finish()));
frame.present();
// Update Labels
let state_guard = state.lock().unwrap();
let cam = camera.lock().unwrap();
// Re-calculate camera params for label projection (since we dropped the lock earlier)
// Actually we can just use the camera object, but we need the calculated params.
// Let's just pass the camera object and let update_labels calculate.
// But wait, Camera struct doesn't have `params` field, `to_uniform` does.
// We need to temporarily modify Camera struct or just re-calculate in update_labels.
// Let's modify Camera to store params or just recalculate. Recalculating is cheap.
// We need to pass a "Camera with params" or just the raw camera and let function handle it.
// But `Camera` struct doesn't have the `params` array.
// Let's just pass the `Camera` struct and let `update_labels` call `to_uniform`.
// But `to_uniform` returns `CameraUniform` which has `params`.
// So we can pass `cam.to_uniform()`.
// Wait, `update_labels` signature I wrote above takes `&Camera`.
// I'll modify `update_labels` to take `CameraUniform` instead of `Camera` to avoid re-calc if possible,
// or just let it call `to_uniform`.
// Let's stick to passing `&Camera` and let it call `to_uniform`.
// But `to_uniform` is a method on `Camera`.
// So:
update_labels(&web_sys::window().unwrap(), &cam, &state_guard, config.width as f64, config.height as f64);
}
WindowEvent::CloseRequested => {
#[cfg(not(target_arch = "wasm32"))]
elwt.exit();
}
_ => {}
@@ -1198,7 +1342,7 @@ fn create_road_pipeline(
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(0.75, 0.75, 0.75, 1.0); // Lighter grey roads
return vec4<f32>(0.4, 0.4, 0.4, 1.0); // Dark grey roads
}
"#)),
});
@@ -1286,7 +1430,7 @@ fn create_building_pipeline(
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(0.6, 0.55, 0.5, 1.0); // Light tan for buildings
return vec4<f32>(0.7, 0.7, 0.7, 1.0); // Darker grey buildings for visibility
}
"#)),
});
@@ -1374,7 +1518,7 @@ fn create_landuse_pipeline(
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(0.6, 0.8, 0.6, 1.0); // Light green for parks
return vec4<f32>(0.77, 0.91, 0.77, 1.0); // Google Maps Park Green (#c5e8c5 approx)
}
"#)),
});
@@ -1472,7 +1616,7 @@ fn create_water_pipeline(
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(0.6, 0.7, 0.9, 1.0); // Light blue for water
return vec4<f32>(0.66, 0.85, 1.0, 1.0); // Google Maps Water Blue (#aadaff approx)
}
"#)),
});
@@ -1570,7 +1714,7 @@ fn create_railway_pipeline(
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(0.3, 0.3, 0.3, 1.0); // Dark grey for railways
return vec4<f32>(0.5, 0.5, 0.5, 1.0); // Grey for railways
}
"#)),
});
@@ -1614,3 +1758,149 @@ fn create_railway_pipeline(
multiview: None,
})
}
fn create_road_mesh_pipeline(
device: &wgpu::Device,
format: &wgpu::TextureFormat,
bind_group_layout: &wgpu::BindGroupLayout
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(r#"
struct CameraUniform {
params: vec4<f32>,
};
@group(0) @binding(0)
var<uniform> camera: CameraUniform;
struct VertexInput {
@location(0) position: vec2<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
};
@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<f32>(x, y, 0.0, 1.0);
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(0.3, 0.3, 0.3, 1.0); // Dark grey for highways
}
"#)),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Road Mesh Pipeline Layout"),
bind_group_layouts: &[bind_group_layout],
push_constant_ranges: &[],
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[
Vertex::desc(),
],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: *format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
})
}
fn update_labels(
window: &web_sys::Window,
camera: &Camera,
state: &AppState,
width: f64,
height: f64,
) {
let document = window.document().unwrap();
let container = document.get_element_by_id("labels").unwrap();
// Clear existing labels
container.set_inner_html("");
let show_countries = true;
let show_cities = camera.zoom > 100.0;
let visible_tiles = get_visible_tiles(camera);
let uniforms = camera.to_uniform(); // Calculate uniforms
for tile in visible_tiles {
if let Some(nodes) = state.nodes.get(&tile) {
for node in nodes {
let place = node.tags.get("place").map(|s| s.as_str());
let name = node.tags.get("name").map(|s| s.as_str());
if let (Some(place), Some(name)) = (place, name) {
let is_country = place == "country";
let is_city = place == "city" || place == "town";
if (is_country && show_countries) || (is_city && show_cities) {
let (x, y) = project(node.lat, node.lon);
// Apply camera transform using uniforms
let cx = x * uniforms.params[0] + uniforms.params[2];
let cy = y * uniforms.params[1] + uniforms.params[3];
let ndc_x = cx;
let ndc_y = cy;
if ndc_x < -1.2 || ndc_x > 1.2 || ndc_y < -1.2 || ndc_y > 1.2 { continue; }
let screen_x = (ndc_x as f64 + 1.0) * 0.5 * width;
let screen_y = (1.0 - ndc_y as f64) * 0.5 * height;
let div = document.create_element("div").unwrap();
let class_name = if is_country { "label label-country" } else { "label label-city" };
div.set_class_name(class_name);
div.set_text_content(Some(name));
let div_html: web_sys::HtmlElement = div.dyn_into().unwrap();
let style = div_html.style();
style.set_property("left", &format!("{}px", screen_x)).unwrap();
style.set_property("top", &format!("{}px", screen_y)).unwrap();
container.append_child(&div_html).unwrap();
}
}
}
}
}
}