update
This commit is contained in:
@@ -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
10
frontend/Trunk.toml
Normal 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/"
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user