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

2
.env
View File

@@ -16,5 +16,5 @@ CLIENT_PORT=8080
SERVICE_LOG_LEVEL=debug
HOST_PBF_PATH=../maps_data/europe-latest.osm.pbf
HOST_PBF_PATH=../maps_data/bayern-latest.osm.pbf
HOST_CACHE_DIR=./cache

View File

@@ -8,8 +8,9 @@ axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
scylla = "0.12" # Check for latest version, using a recent stable one
serde = { version = "1.0", features = ["derive"] }
bincode = "1.3"
serde_json = "1.0"
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "fs"] }
tower-http = { version = "0.5", features = ["cors", "fs", "compression-full"] }
tracing = "0.1"
tracing-subscriber = "0.3"

View File

@@ -4,12 +4,14 @@ use axum::{
routing::get,
Router,
extract::{State, Path},
Json,
http::header,
response::IntoResponse,
};
use scylla::{Session, SessionBuilder};
use std::sync::Arc;
use tower_http::services::ServeDir;
use tower_http::cors::CorsLayer;
use tower_http::compression::CompressionLayer;
use serde::Serialize;
struct AppState {
@@ -49,8 +51,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.route("/api/tiles/:z/:x/:y/landuse", get(get_tile_landuse))
.route("/api/tiles/:z/:x/:y/water", get(get_tile_water))
.route("/api/tiles/:z/:x/:y/railways", get(get_tile_railways))
.route("/api/tiles/:z/:x/:y/all", get(get_tile_all))
.nest_service("/", ServeDir::new("static").append_index_html_on_directories(true))
.layer(CorsLayer::permissive())
.layer(CompressionLayer::new())
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
@@ -75,219 +79,310 @@ struct MapNode {
async fn get_tile(
Path((z, x, y)): Path<(i32, i32, i32)>,
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<MapNode>>, (axum::http::StatusCode, String)> {
) -> impl IntoResponse {
let query = "SELECT id, lat, lon, tags FROM map_data.nodes WHERE zoom = ? AND tile_x = ? AND tile_y = ?";
let rows = state.scylla_session.query(query, (z, x, y))
.await
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
.rows
.unwrap_or_default();
let rows = match state.scylla_session.query(query, (z, x, y)).await {
Ok(res) => res.rows.unwrap_or_default(),
Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(),
};
let mut nodes = Vec::new();
for row in rows {
let (id, lat, lon, tags) = row.into_typed::<(i64, f64, f64, std::collections::HashMap<String, String>)>()
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e)))?;
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))).unwrap();
nodes.push(MapNode { id, lat, lon, tags });
}
Ok(Json(nodes))
let bytes = bincode::serialize(&nodes).unwrap();
(
[(header::CONTENT_TYPE, "application/octet-stream")],
bytes,
).into_response()
}
#[derive(Serialize)]
struct MapWay {
id: i64,
tags: std::collections::HashMap<String, String>,
points: Vec<Vec<f64>>, // List of [lat, lon]
points: Vec<u8>, // Flat f32 array
}
async fn get_tile_ways(
Path((z, x, y)): Path<(i32, i32, i32)>,
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
) -> impl IntoResponse {
let query = if z < 9 {
"SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000"
} else {
"SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ?"
};
let rows = state.scylla_session.query(query, (z, x, y))
.await
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
.rows
.unwrap_or_default();
let rows = match state.scylla_session.query(query, (z, x, y)).await {
Ok(res) => res.rows.unwrap_or_default(),
Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(),
};
let mut ways = Vec::new();
for row in rows {
let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e)))?;
let (id, tags, points) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))).unwrap();
// Deserialize points blob
let mut points = Vec::new();
for chunk in points_blob.chunks(16) {
if chunk.len() == 16 {
let lat = f64::from_be_bytes(chunk[0..8].try_into().unwrap());
let lon = f64::from_be_bytes(chunk[8..16].try_into().unwrap());
points.push(vec![lat, lon]);
}
}
ways.push(MapWay { id, tags, points });
}
Ok(Json(ways))
let bytes = bincode::serialize(&ways).unwrap();
(
[(header::CONTENT_TYPE, "application/octet-stream")],
bytes,
).into_response()
}
async fn get_tile_buildings(
Path((z, x, y)): Path<(i32, i32, i32)>,
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
) -> impl IntoResponse {
let query = "SELECT id, tags, points FROM map_data.buildings WHERE zoom = ? AND tile_x = ? AND tile_y = ?";
// Optimization: Don't load buildings for low zoom levels
if z < 13 {
return Ok(Json(Vec::new()));
if z < 12 {
return ([(header::CONTENT_TYPE, "application/octet-stream")], vec![]).into_response();
}
let rows = state.scylla_session.query(query, (z, x, y))
.await
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
.rows
.unwrap_or_default();
let rows = match state.scylla_session.query(query, (z, x, y)).await {
Ok(res) => res.rows.unwrap_or_default(),
Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(),
};
let mut buildings = Vec::new();
for row in rows {
let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e)))?;
let (id, tags, points) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))).unwrap();
// Deserialize points blob
let mut points = Vec::new();
for chunk in points_blob.chunks(16) {
if chunk.len() == 16 {
let lat = f64::from_be_bytes(chunk[0..8].try_into().unwrap());
let lon = f64::from_be_bytes(chunk[8..16].try_into().unwrap());
points.push(vec![lat, lon]);
}
}
buildings.push(MapWay { id, tags, points });
}
Ok(Json(buildings))
let bytes = bincode::serialize(&buildings).unwrap();
(
[(header::CONTENT_TYPE, "application/octet-stream")],
bytes,
).into_response()
}
async fn get_tile_landuse(
Path((z, x, y)): Path<(i32, i32, i32)>,
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
println!("Request: get_tile_landuse({}, {}, {})", z, x, y);
) -> impl IntoResponse {
// Optimization: Don't load landuse for low zoom levels
if z < 11 {
return Ok(Json(Vec::new()));
if z < 4 {
return ([(header::CONTENT_TYPE, "application/octet-stream")], vec![]).into_response();
}
let query = "SELECT id, tags, points FROM map_data.landuse WHERE zoom = ? AND tile_x = ? AND tile_y = ?";
println!("Executing query...");
let result = state.scylla_session.query(query, (z, x, y)).await;
let rows = match state.scylla_session.query(query, (z, x, y)).await {
Ok(res) => res.rows.unwrap_or_default(),
Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(),
};
match result {
Ok(res) => {
println!("Query successful, processing rows...");
let rows = res.rows.unwrap_or_default();
let mut landuse = Vec::new();
for row in rows {
let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
.map_err(|e| {
println!("Serialization error: {}", e);
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))
})?;
let mut points = Vec::new();
for chunk in points_blob.chunks(16) {
if chunk.len() == 16 {
let lat = f64::from_be_bytes(chunk[0..8].try_into().unwrap());
let lon = f64::from_be_bytes(chunk[8..16].try_into().unwrap());
points.push(vec![lat, lon]);
}
}
landuse.push(MapWay { id, tags, points });
}
println!("Returning {} landuse items", landuse.len());
Ok(Json(landuse))
},
Err(e) => {
println!("Query failed: {}", e);
Err((axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))
}
let mut landuse = Vec::new();
for row in rows {
let (id, tags, points) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))).unwrap();
landuse.push(MapWay { id, tags, points });
}
let bytes = bincode::serialize(&landuse).unwrap();
(
[(header::CONTENT_TYPE, "application/octet-stream")],
bytes,
).into_response()
}
async fn get_tile_water(
Path((z, x, y)): Path<(i32, i32, i32)>,
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
) -> impl IntoResponse {
let query = if z < 9 {
"SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000"
} else {
"SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ?"
};
let rows = state.scylla_session.query(query, (z, x, y))
.await
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
.rows
.unwrap_or_default();
let rows = match state.scylla_session.query(query, (z, x, y)).await {
Ok(res) => res.rows.unwrap_or_default(),
Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(),
};
let mut water = Vec::new();
for row in rows {
let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e)))?;
let (id, tags, points) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))).unwrap();
let mut points = Vec::new();
for chunk in points_blob.chunks(16) {
if chunk.len() == 16 {
let lat = f64::from_be_bytes(chunk[0..8].try_into().unwrap());
let lon = f64::from_be_bytes(chunk[8..16].try_into().unwrap());
points.push(vec![lat, lon]);
}
}
water.push(MapWay { id, tags, points });
}
Ok(Json(water))
let bytes = bincode::serialize(&water).unwrap();
(
[(header::CONTENT_TYPE, "application/octet-stream")],
bytes,
).into_response()
}
async fn get_tile_railways(
Path((z, x, y)): Path<(i32, i32, i32)>,
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
) -> impl IntoResponse {
let query = if z < 9 {
"SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000"
} else {
"SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ?"
};
let rows = state.scylla_session.query(query, (z, x, y))
.await
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
.rows
.unwrap_or_default();
let rows = match state.scylla_session.query(query, (z, x, y)).await {
Ok(res) => res.rows.unwrap_or_default(),
Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(),
};
let mut railways = Vec::new();
for row in rows {
let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e)))?;
let (id, tags, points) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))).unwrap();
let mut points = Vec::new();
for chunk in points_blob.chunks(16) {
if chunk.len() == 16 {
let lat = f64::from_be_bytes(chunk[0..8].try_into().unwrap());
let lon = f64::from_be_bytes(chunk[8..16].try_into().unwrap());
points.push(vec![lat, lon]);
}
}
railways.push(MapWay { id, tags, points });
}
Ok(Json(railways))
let bytes = bincode::serialize(&railways).unwrap();
(
[(header::CONTENT_TYPE, "application/octet-stream")],
bytes,
).into_response()
}
#[derive(Serialize)]
struct TileData {
nodes: Vec<MapNode>,
ways: Vec<MapWay>,
buildings: Vec<MapWay>,
landuse: Vec<MapWay>,
water: Vec<MapWay>,
railways: Vec<MapWay>,
}
async fn get_tile_all(
Path((z, x, y)): Path<(i32, i32, i32)>,
State(state): State<Arc<AppState>>,
) -> impl IntoResponse {
// Run all queries in parallel
// Run all queries in parallel
// (Removed unused tokio::join! block)
// Helper to deserialize response body back to Vec<T>
// Since we are calling the handlers directly, they return `impl IntoResponse`.
// We need to extract the bytes. This is a bit hacky because we are serializing then deserializing.
// A better way would be to refactor the logic into functions that return data, but for now this is least invasive.
// Actually, calling handlers directly is fine if we can extract the body.
// But `impl IntoResponse` is opaque.
// Refactoring is better. Let's create helper functions that return the data structs.
// REFACTOR STRATEGY:
// 1. Extract logic from `get_tile` to `fetch_nodes`.
// 2. Call `fetch_nodes` from `get_tile` and `get_tile_all`.
// ... repeat for all.
// Wait, I can't easily refactor everything in one go without potential errors.
// Let's try to implement `get_tile_all` by copying the logic. It's duplication but safer for now.
// Actually, duplication is bad.
// Let's look at `get_tile`. It queries DB and returns bytes.
// I will copy the query logic for now to ensure correctness and avoid breaking existing endpoints if I mess up refactoring.
// Nodes
let query_nodes = "SELECT id, lat, lon, tags FROM map_data.nodes WHERE zoom = ? AND tile_x = ? AND tile_y = ?";
let rows_nodes = state.scylla_session.query(query_nodes, (z, x, y)).await.ok().and_then(|r| r.rows).unwrap_or_default();
let mut nodes = Vec::new();
for row in rows_nodes {
if let Ok((id, lat, lon, tags)) = row.into_typed::<(i64, f64, f64, std::collections::HashMap<String, String>)>() {
nodes.push(MapNode { id, lat, lon, tags });
}
}
// Ways
let query_ways = if z < 9 {
"SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000"
} else {
"SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ?"
};
let rows_ways = state.scylla_session.query(query_ways, (z, x, y)).await.ok().and_then(|r| r.rows).unwrap_or_default();
let mut ways = Vec::new();
for row in rows_ways {
if let Ok((id, tags, points)) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>() {
ways.push(MapWay { id, tags, points });
}
}
// Buildings
let mut buildings = Vec::new();
if z >= 13 {
let query_buildings = "SELECT id, tags, points FROM map_data.buildings WHERE zoom = ? AND tile_x = ? AND tile_y = ?";
let rows_buildings = state.scylla_session.query(query_buildings, (z, x, y)).await.ok().and_then(|r| r.rows).unwrap_or_default();
for row in rows_buildings {
if let Ok((id, tags, points)) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>() {
buildings.push(MapWay { id, tags, points });
}
}
}
// Landuse
let mut landuse = Vec::new();
if z >= 4 {
let query_landuse = "SELECT id, tags, points FROM map_data.landuse WHERE zoom = ? AND tile_x = ? AND tile_y = ?";
let rows_landuse = state.scylla_session.query(query_landuse, (z, x, y)).await.ok().and_then(|r| r.rows).unwrap_or_default();
for row in rows_landuse {
if let Ok((id, tags, points)) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>() {
landuse.push(MapWay { id, tags, points });
}
}
}
// Water
let query_water = if z < 9 {
"SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000"
} else {
"SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ?"
};
let rows_water = state.scylla_session.query(query_water, (z, x, y)).await.ok().and_then(|r| r.rows).unwrap_or_default();
let mut water = Vec::new();
for row in rows_water {
if let Ok((id, tags, points)) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>() {
water.push(MapWay { id, tags, points });
}
}
// Railways
let query_railways = if z < 9 {
"SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000"
} else {
"SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ?"
};
let rows_railways = state.scylla_session.query(query_railways, (z, x, y)).await.ok().and_then(|r| r.rows).unwrap_or_default();
let mut railways = Vec::new();
for row in rows_railways {
if let Ok((id, tags, points)) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>() {
railways.push(MapWay { id, tags, points });
}
}
let data = TileData {
nodes,
ways,
buildings,
landuse,
water,
railways,
};
let bytes = bincode::serialize(&data).unwrap();
(
[
(header::CONTENT_TYPE, "application/octet-stream"),
(header::CACHE_CONTROL, "public, max-age=31536000, immutable"),
],
bytes,
).into_response()
}

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();
}
}
}
}
}
}

View File

@@ -10,3 +10,4 @@ tokio = { version = "1.0", features = ["full"] }
anyhow = "1.0"
memmap2 = "0.9"
dotenv = "0.15"
earcutr = "0.4"

View File

@@ -1,4 +1,5 @@
use anyhow::Result;
use earcutr::earcut;
use osmpbf::{Element, ElementReader};
use scylla::SessionBuilder;
use std::collections::HashMap;
@@ -8,7 +9,7 @@ use std::io::{BufWriter, Write, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use memmap2::Mmap;
const ZOOM_LEVELS: [u32; 5] = [2, 6, 9, 12, 14];
const ZOOM_LEVELS: [u32; 6] = [2, 4, 6, 9, 12, 14];
struct NodeStore {
writer: Option<BufWriter<File>>,
@@ -134,6 +135,23 @@ fn simplify_points(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> {
}
}
fn triangulate_polygon(points: &[(f64, f64)]) -> Vec<(f64, f64)> {
let mut flat_points = Vec::with_capacity(points.len() * 2);
for (lat, lon) in points {
flat_points.push(*lat);
flat_points.push(*lon);
}
// We assume simple polygons (no holes) for now as we are just processing ways
let indices = earcut(&flat_points, &[], 2).unwrap_or_default();
let mut triangles = Vec::with_capacity(indices.len());
for i in indices {
triangles.push(points[i]);
}
triangles
}
fn should_include(tags: &HashMap<String, String>, zoom: u32) -> bool {
if zoom >= 14 { return true; }
@@ -144,19 +162,34 @@ fn should_include(tags: &HashMap<String, String>, zoom: u32) -> bool {
let waterway = tags.get("waterway").map(|s| s.as_str());
match zoom {
match zoom {
2 => {
// Space View: Continents and Countries
matches!(place, Some("continent" | "country")) ||
matches!(natural, Some("water")) // Major water bodies
matches!(natural, Some("water")) || // Major water bodies
matches!(highway, Some("motorway")) || // Added motorway
matches!(tags.get("landuse").map(|s| s.as_str()), Some("forest" | "grass" | "meadow" | "farmland" | "residential")) || // Added more green + farmland/residential
matches!(tags.get("leisure").map(|s| s.as_str()), Some("park" | "nature_reserve")) || // Added parks
matches!(natural, Some("wood" | "scrub")) // Added wood/scrub
},
4 => {
// Regional View (NEW)
matches!(highway, Some("motorway" | "trunk")) ||
matches!(place, Some("city" | "town")) ||
matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland")) ||
matches!(tags.get("landuse").map(|s| s.as_str()), Some("forest" | "grass" | "meadow" | "farmland" | "residential")) ||
matches!(tags.get("leisure").map(|s| s.as_str()), Some("park" | "nature_reserve")) ||
matches!(waterway, Some("river"))
},
6 => {
// Enterprise Grade: ONLY Motorways and Trunk roads. No primary/secondary.
// ONLY Cities. No nature/landuse.
matches!(highway, Some("motorway" | "trunk")) ||
matches!(highway, Some("motorway" | "trunk" | "primary")) || // Added primary
matches!(place, Some("city")) ||
matches!(natural, Some("water")) || // Only major water bodies
matches!(waterway, Some("river")) // Only major rivers
matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland")) ||
matches!(tags.get("landuse").map(|s| s.as_str()), Some("forest" | "grass" | "meadow" | "farmland" | "residential")) ||
matches!(tags.get("leisure").map(|s| s.as_str()), Some("park" | "nature_reserve")) ||
matches!(waterway, Some("river"))
},
9 => {
// Enterprise Grade: Add Primary roads.
@@ -166,12 +199,15 @@ fn should_include(tags: &HashMap<String, String>, zoom: u32) -> bool {
matches!(place, Some("city" | "town")) ||
matches!(railway, Some("rail")) ||
matches!(natural, Some("water" | "wood")) ||
matches!(tags.get("landuse").map(|s| s.as_str()), Some("forest")) ||
matches!(tags.get("leisure").map(|s| s.as_str()), Some("park")) ||
matches!(waterway, Some("river" | "riverbank"))
},
12 => {
matches!(highway, Some("motorway" | "trunk" | "primary" | "secondary" | "tertiary")) ||
matches!(place, Some("city" | "town" | "village")) ||
matches!(railway, Some("rail")) ||
tags.contains_key("building") ||
tags.contains_key("landuse") ||
tags.contains_key("leisure") ||
matches!(natural, Some("water" | "wood" | "scrub" | "wetland" | "heath")) ||
@@ -396,11 +432,19 @@ async fn main() -> Result<()> {
if !should_include(&tags, zoom) { continue; }
// Apply simplification based on zoom level
let epsilon = match zoom {
2 => 0.1, // Very high simplification
6 => 0.005, // High simplification
9 => 0.001, // Medium simplification
_ => 0.0, // No simplification
let base_epsilon = match zoom {
2 => 0.0001,
4 => 0.00005,
6 => 0.00002,
9 => 0.00001,
12 => 0.000005,
_ => 0.0,
};
let epsilon = if is_water || is_landuse || is_highway {
base_epsilon * 0.5 // Preserve more detail for natural features AND roads
} else {
base_epsilon
};
let simplified_points = if epsilon > 0.0 {
@@ -409,17 +453,28 @@ async fn main() -> Result<()> {
points.clone()
};
if simplified_points.len() < 2 { continue; }
// Serialize points
let mut final_points = simplified_points;
// Triangulate if it's a polygon type
if is_building || is_water || is_landuse {
// Close the loop if not closed
if final_points.first() != final_points.last() {
final_points.push(final_points[0]);
}
final_points = triangulate_polygon(&final_points);
}
let (first_lat, first_lon) = simplified_points[0];
if final_points.len() < 2 { continue; }
let (first_lat, first_lon) = final_points[0];
let (x, y) = lat_lon_to_tile(first_lat, first_lon, zoom);
let zoom_i32 = zoom as i32;
// Serialize simplified points
let mut blob = Vec::with_capacity(simplified_points.len() * 16);
for (lat, lon) in simplified_points {
blob.extend_from_slice(&lat.to_be_bytes());
blob.extend_from_slice(&lon.to_be_bytes());
let mut blob = Vec::with_capacity(final_points.len() * 8); // 4 bytes lat + 4 bytes lon
for (lat, lon) in final_points {
blob.extend_from_slice(&(lat as f32).to_le_bytes());
blob.extend_from_slice(&(lon as f32).to_le_bytes());
}
if is_highway {

View File

@@ -6,3 +6,4 @@ fi
echo "Using PBF file: ${HOST_PBF_PATH:-./europe-latest.osm.pbf}"
docker compose -f docker-compose-remote.yml --profile import up --build importer
docker compose -f docker-compose-remote.yml up --build