update
This commit is contained in:
2
.env
2
.env
@@ -16,5 +16,5 @@ CLIENT_PORT=8080
|
|||||||
|
|
||||||
SERVICE_LOG_LEVEL=debug
|
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
|
HOST_CACHE_DIR=./cache
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ axum = "0.7"
|
|||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
scylla = "0.12" # Check for latest version, using a recent stable one
|
scylla = "0.12" # Check for latest version, using a recent stable one
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
bincode = "1.3"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tower = "0.4"
|
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 = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ use axum::{
|
|||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
extract::{State, Path},
|
extract::{State, Path},
|
||||||
Json,
|
http::header,
|
||||||
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use scylla::{Session, SessionBuilder};
|
use scylla::{Session, SessionBuilder};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tower_http::cors::CorsLayer;
|
use tower_http::cors::CorsLayer;
|
||||||
|
use tower_http::compression::CompressionLayer;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
struct AppState {
|
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/landuse", get(get_tile_landuse))
|
||||||
.route("/api/tiles/:z/:x/:y/water", get(get_tile_water))
|
.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/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))
|
.nest_service("/", ServeDir::new("static").append_index_html_on_directories(true))
|
||||||
.layer(CorsLayer::permissive())
|
.layer(CorsLayer::permissive())
|
||||||
|
.layer(CompressionLayer::new())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
|
||||||
@@ -75,219 +79,310 @@ struct MapNode {
|
|||||||
async fn get_tile(
|
async fn get_tile(
|
||||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||||
State(state): State<Arc<AppState>>,
|
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 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))
|
let rows = match state.scylla_session.query(query, (z, x, y)).await {
|
||||||
.await
|
Ok(res) => res.rows.unwrap_or_default(),
|
||||||
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
|
Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(),
|
||||||
.rows
|
};
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let mut nodes = Vec::new();
|
let mut nodes = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let (id, lat, lon, tags) = row.into_typed::<(i64, f64, f64, std::collections::HashMap<String, String>)>()
|
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 });
|
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)]
|
#[derive(Serialize)]
|
||||||
struct MapWay {
|
struct MapWay {
|
||||||
id: i64,
|
id: i64,
|
||||||
tags: std::collections::HashMap<String, String>,
|
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(
|
async fn get_tile_ways(
|
||||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
|
) -> impl IntoResponse {
|
||||||
let query = if z < 9 {
|
let query = if z < 9 {
|
||||||
"SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000"
|
"SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000"
|
||||||
} else {
|
} else {
|
||||||
"SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ?"
|
"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))
|
let rows = match state.scylla_session.query(query, (z, x, y)).await {
|
||||||
.await
|
Ok(res) => res.rows.unwrap_or_default(),
|
||||||
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
|
Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(),
|
||||||
.rows
|
};
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let mut ways = Vec::new();
|
let mut ways = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
|
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)))?;
|
.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 });
|
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(
|
async fn get_tile_buildings(
|
||||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||||
State(state): State<Arc<AppState>>,
|
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 = ?";
|
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
|
// Optimization: Don't load buildings for low zoom levels
|
||||||
if z < 13 {
|
if z < 12 {
|
||||||
return Ok(Json(Vec::new()));
|
return ([(header::CONTENT_TYPE, "application/octet-stream")], vec![]).into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
let rows = state.scylla_session.query(query, (z, x, y))
|
let rows = match state.scylla_session.query(query, (z, x, y)).await {
|
||||||
.await
|
Ok(res) => res.rows.unwrap_or_default(),
|
||||||
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
|
Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(),
|
||||||
.rows
|
};
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let mut buildings = Vec::new();
|
let mut buildings = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
|
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)))?;
|
.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 });
|
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(
|
async fn get_tile_landuse(
|
||||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
|
) -> impl IntoResponse {
|
||||||
println!("Request: get_tile_landuse({}, {}, {})", z, x, y);
|
|
||||||
|
|
||||||
// Optimization: Don't load landuse for low zoom levels
|
// Optimization: Don't load landuse for low zoom levels
|
||||||
if z < 11 {
|
if z < 4 {
|
||||||
return Ok(Json(Vec::new()));
|
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 = ?";
|
let query = "SELECT id, tags, points FROM map_data.landuse WHERE zoom = ? AND tile_x = ? AND tile_y = ?";
|
||||||
println!("Executing query...");
|
let rows = match state.scylla_session.query(query, (z, x, y)).await {
|
||||||
let result = 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 {
|
let mut landuse = Vec::new();
|
||||||
Ok(res) => {
|
for row in rows {
|
||||||
println!("Query successful, processing rows...");
|
let (id, tags, points) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
|
||||||
let rows = res.rows.unwrap_or_default();
|
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))).unwrap();
|
||||||
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();
|
landuse.push(MapWay { id, tags, points });
|
||||||
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 bytes = bincode::serialize(&landuse).unwrap();
|
||||||
|
(
|
||||||
|
[(header::CONTENT_TYPE, "application/octet-stream")],
|
||||||
|
bytes,
|
||||||
|
).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_tile_water(
|
async fn get_tile_water(
|
||||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
|
) -> impl IntoResponse {
|
||||||
let query = if z < 9 {
|
let query = if z < 9 {
|
||||||
"SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000"
|
"SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000"
|
||||||
} else {
|
} else {
|
||||||
"SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ?"
|
"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))
|
let rows = match state.scylla_session.query(query, (z, x, y)).await {
|
||||||
.await
|
Ok(res) => res.rows.unwrap_or_default(),
|
||||||
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
|
Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(),
|
||||||
.rows
|
};
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let mut water = Vec::new();
|
let mut water = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
|
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)))?;
|
.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 });
|
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(
|
async fn get_tile_railways(
|
||||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
|
) -> impl IntoResponse {
|
||||||
let query = if z < 9 {
|
let query = if z < 9 {
|
||||||
"SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000"
|
"SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000"
|
||||||
} else {
|
} else {
|
||||||
"SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ?"
|
"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))
|
let rows = match state.scylla_session.query(query, (z, x, y)).await {
|
||||||
.await
|
Ok(res) => res.rows.unwrap_or_default(),
|
||||||
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
|
Err(e) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response(),
|
||||||
.rows
|
};
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let mut railways = Vec::new();
|
let mut railways = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
|
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)))?;
|
.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 });
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ crate-type = ["cdylib", "rlib"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
|
js-sys = "0.3"
|
||||||
web-sys = { version = "0.3", features = [
|
web-sys = { version = "0.3", features = [
|
||||||
"Document",
|
"Document",
|
||||||
"Window",
|
"Window",
|
||||||
@@ -33,6 +34,7 @@ web-sys = { version = "0.3", features = [
|
|||||||
"RequestMode",
|
"RequestMode",
|
||||||
"Response",
|
"Response",
|
||||||
"HtmlInputElement",
|
"HtmlInputElement",
|
||||||
|
"PositionOptions",
|
||||||
] }
|
] }
|
||||||
wgpu = { version = "0.19", default-features = false, features = ["webgl", "wgsl"] }
|
wgpu = { version = "0.19", default-features = false, features = ["webgl", "wgsl"] }
|
||||||
winit = { version = "0.29", default-features = false, features = ["rwh_06"] }
|
winit = { version = "0.29", default-features = false, features = ["rwh_06"] }
|
||||||
@@ -43,4 +45,4 @@ console_log = "1.0"
|
|||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
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;
|
padding: 5px 10px;
|
||||||
border-radius: 4px;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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 id="ui-container">
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<button id="btn-zoom-in">+</button>
|
<button id="btn-zoom-in">+</button>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use winit::{
|
|||||||
use winit::platform::web::WindowExtWebSys;
|
use winit::platform::web::WindowExtWebSys;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use wgpu::util::DeviceExt;
|
use wgpu::util::DeviceExt;
|
||||||
use earcutr::earcut;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
struct MapNode {
|
struct MapNode {
|
||||||
@@ -49,9 +48,9 @@ impl Camera {
|
|||||||
CameraUniform {
|
CameraUniform {
|
||||||
params: [
|
params: [
|
||||||
self.zoom / self.aspect, // scale_x
|
self.zoom / self.aspect, // scale_x
|
||||||
self.zoom, // scale_y
|
-self.zoom, // scale_y (flipped for North-Up)
|
||||||
-self.x * (self.zoom / self.aspect), // translate_x (simplified)
|
-self.x * (self.zoom / self.aspect), // translate_x
|
||||||
-self.y * self.zoom, // translate_y
|
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)
|
(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)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
struct MapWay {
|
struct MapWay {
|
||||||
id: i64,
|
id: i64,
|
||||||
tags: std::collections::HashMap<String, String>,
|
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 {
|
struct TileBuffers {
|
||||||
@@ -135,6 +188,8 @@ struct TileBuffers {
|
|||||||
water_index_count: u32,
|
water_index_count: u32,
|
||||||
railway_vertex_buffer: wgpu::Buffer,
|
railway_vertex_buffer: wgpu::Buffer,
|
||||||
railway_vertex_count: u32,
|
railway_vertex_count: u32,
|
||||||
|
road_mesh_vertex_buffer: wgpu::Buffer,
|
||||||
|
road_mesh_vertex_count: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
@@ -148,6 +203,8 @@ struct AppState {
|
|||||||
loaded_tiles: HashSet<(i32, i32, i32)>,
|
loaded_tiles: HashSet<(i32, i32, i32)>,
|
||||||
pending_tiles: HashSet<(i32, i32, i32)>,
|
pending_tiles: HashSet<(i32, i32, i32)>,
|
||||||
user_location: Option<(f64, f64)>, // (lat, lon)
|
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> {
|
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
|
vertices
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_cached(url: &str) -> Option<String> {
|
async fn fetch_cached(url: &str) -> Option<Vec<u8>> {
|
||||||
let window = web_sys::window()?;
|
let window = web_sys::window()?;
|
||||||
let caches = window.caches().ok()?;
|
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 = wasm_bindgen_futures::JsFuture::from(caches.open(cache_name)).await.ok()?;
|
||||||
let cache: web_sys::Cache = cache.dyn_into().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() {
|
if !match_val.is_undefined() {
|
||||||
let response: web_sys::Response = match_val.dyn_into().ok()?;
|
let response: web_sys::Response = match_val.dyn_into().ok()?;
|
||||||
let text_promise = response.text().ok()?;
|
let buffer_promise = response.array_buffer().ok()?;
|
||||||
let text = wasm_bindgen_futures::JsFuture::from(text_promise).await.ok()?;
|
let buffer = wasm_bindgen_futures::JsFuture::from(buffer_promise).await.ok()?;
|
||||||
return text.as_string();
|
let array = js_sys::Uint8Array::new(&buffer);
|
||||||
|
return Some(array.to_vec());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network fetch
|
// Network fetch
|
||||||
@@ -216,9 +274,10 @@ async fn fetch_cached(url: &str) -> Option<String> {
|
|||||||
let put_promise = cache.put_with_request(&request, &response_clone);
|
let put_promise = cache.put_with_request(&request, &response_clone);
|
||||||
wasm_bindgen_futures::JsFuture::from(put_promise).await.ok()?;
|
wasm_bindgen_futures::JsFuture::from(put_promise).await.ok()?;
|
||||||
|
|
||||||
let text_promise = response.text().ok()?;
|
let buffer_promise = response.array_buffer().ok()?;
|
||||||
let text = wasm_bindgen_futures::JsFuture::from(text_promise).await.ok()?;
|
let buffer = wasm_bindgen_futures::JsFuture::from(buffer_promise).await.ok()?;
|
||||||
text.as_string()
|
let array = js_sys::Uint8Array::new(&buffer);
|
||||||
|
Some(array.to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_visible_tiles(camera: &Camera) -> Vec<(i32, i32, i32)> {
|
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 9: Region view
|
||||||
// Zoom 12: City view
|
// Zoom 12: City view
|
||||||
// Zoom 14: Street view
|
// Zoom 14: Street view
|
||||||
let z = if camera.zoom < 500.0 {
|
let z = if camera.zoom < 100.0 {
|
||||||
6
|
2
|
||||||
|
} else if camera.zoom < 500.0 {
|
||||||
|
4
|
||||||
} else if camera.zoom < 2000.0 {
|
} else if camera.zoom < 2000.0 {
|
||||||
|
6
|
||||||
|
} else if camera.zoom < 5000.0 {
|
||||||
9
|
9
|
||||||
} else if camera.zoom < 8000.0 {
|
} else if camera.zoom < 10000.0 {
|
||||||
12
|
12
|
||||||
} else {
|
} else {
|
||||||
14
|
14
|
||||||
@@ -260,6 +323,24 @@ fn get_visible_tiles(camera: &Camera) -> Vec<(i32, i32, i32)> {
|
|||||||
tiles
|
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)]
|
#[wasm_bindgen(start)]
|
||||||
pub async fn run() {
|
pub async fn run() {
|
||||||
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||||
@@ -361,6 +442,8 @@ pub async fn run() {
|
|||||||
loaded_tiles: HashSet::new(),
|
loaded_tiles: HashSet::new(),
|
||||||
pending_tiles: HashSet::new(),
|
pending_tiles: HashSet::new(),
|
||||||
user_location: None,
|
user_location: None,
|
||||||
|
kalman_filter: None,
|
||||||
|
watch_id: None,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Zoom constants
|
// Zoom constants
|
||||||
@@ -453,10 +536,25 @@ pub async fn run() {
|
|||||||
|
|
||||||
if let Some(btn) = btn_location {
|
if let Some(btn) = btn_location {
|
||||||
let closure = wasm_bindgen::closure::Closure::<dyn FnMut()>::new(move || {
|
let closure = wasm_bindgen::closure::Closure::<dyn FnMut()>::new(move || {
|
||||||
|
|
||||||
let window = web_sys::window().unwrap();
|
let window = web_sys::window().unwrap();
|
||||||
let navigator = window.navigator();
|
let navigator = window.navigator();
|
||||||
let geolocation = navigator.geolocation().unwrap();
|
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 camera_clone2 = camera_clone.clone();
|
||||||
let window_clone2 = window_clone.clone();
|
let window_clone2 = window_clone.clone();
|
||||||
let state_clone2 = state_clone.clone();
|
let state_clone2 = state_clone.clone();
|
||||||
@@ -465,31 +563,64 @@ pub async fn run() {
|
|||||||
let coords = position.coords();
|
let coords = position.coords();
|
||||||
let lat = coords.latitude();
|
let lat = coords.latitude();
|
||||||
let lon = coords.longitude();
|
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();
|
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);
|
drop(state_guard);
|
||||||
|
|
||||||
// Center camera on location
|
// Center camera on location (maybe only on first update? or always? "Follow me" mode)
|
||||||
let (x, y) = project(lat, lon);
|
// 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();
|
let mut cam = camera_clone2.lock().unwrap();
|
||||||
cam.x = x;
|
cam.x = x;
|
||||||
cam.y = y;
|
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);
|
drop(cam);
|
||||||
|
|
||||||
window_clone2.request_redraw();
|
window_clone2.request_redraw();
|
||||||
});
|
});
|
||||||
|
|
||||||
let error_callback = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::PositionError)>::new(move |_error: web_sys::PositionError| {
|
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());
|
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(),
|
success_callback.as_ref().unchecked_ref(),
|
||||||
Some(error_callback.as_ref().unchecked_ref())
|
Some(error_callback.as_ref().unchecked_ref()),
|
||||||
).unwrap();
|
&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();
|
success_callback.forget();
|
||||||
error_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 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 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 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();
|
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;
|
let world_dy = dy / (config.height as f32 * 0.5) / cam.zoom;
|
||||||
|
|
||||||
cam.x -= world_dx;
|
cam.x -= world_dx;
|
||||||
cam.y += world_dy;
|
cam.y -= world_dy;
|
||||||
|
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -607,39 +739,81 @@ pub async fn run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. Check visible tiles
|
// 1. Check visible tiles
|
||||||
let visible_tiles = if cam.zoom > 200.0 {
|
let visible_tiles = get_visible_tiles(&cam);
|
||||||
get_visible_tiles(&cam)
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Drop camera lock early to avoid holding it during long operations if possible,
|
// 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.
|
// but we need it for `to_uniform`. We can clone the needed data.
|
||||||
let camera_uniform_data = cam.to_uniform();
|
let camera_uniform_data = cam.to_uniform();
|
||||||
|
let camera_zoom = cam.zoom; // Capture zoom for road mesh width calculations
|
||||||
drop(cam); // Release lock
|
drop(cam); // Release lock
|
||||||
|
|
||||||
let mut state_guard = state.lock().unwrap();
|
let mut state_guard = state.lock().unwrap();
|
||||||
let mut needs_fetch = Vec::new();
|
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 {
|
for tile in &visible_tiles {
|
||||||
if !state_guard.loaded_tiles.contains(tile) && !state_guard.pending_tiles.contains(tile) {
|
if state_guard.loaded_tiles.contains(tile) {
|
||||||
state_guard.pending_tiles.insert(*tile);
|
tiles_to_render_set.insert(*tile);
|
||||||
needs_fetch.push(*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();
|
// Cleanup: Retain loaded tiles that are either visible OR are being used as fallbacks
|
||||||
state_guard.loaded_tiles.retain(|t| visible_set.contains(t));
|
// We also want to keep recently used parents for a bit to avoid thrashing,
|
||||||
state_guard.nodes.retain(|t, _| visible_set.contains(t));
|
// but for now, strict "is in render set" is a good start.
|
||||||
state_guard.ways.retain(|t, _| visible_set.contains(t));
|
// However, we must ALSO keep the `visible_tiles` that are loading, otherwise they'll never finish?
|
||||||
state_guard.buffers.retain(|t, _| visible_set.contains(t));
|
// 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
|
// 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) {
|
if !state_guard.buffers.contains_key(tile) {
|
||||||
let mut point_instance_data = Vec::new();
|
let mut point_instance_data = Vec::new();
|
||||||
let mut road_vertex_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 landuse_vertex_data = Vec::new();
|
||||||
let mut water_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) {
|
if let Some(ways) = state_guard.ways.get(tile) {
|
||||||
for way in ways {
|
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
|
// Parse points first
|
||||||
for i in 0..way.points.len() - 1 {
|
let mut parsed_points = Vec::new();
|
||||||
let p1 = &way.points[i];
|
for chunk in points.chunks(8) {
|
||||||
let p2 = &way.points[i+1];
|
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 (x1, y1) = project(p1[0], p1[1]);
|
||||||
let (x2, y2) = project(p2[0], p2[1]);
|
let (x2, y2) = project(p2[0], p2[1]);
|
||||||
road_vertex_data.push(Vertex { position: [x1, y1] });
|
road_vertex_data.push(Vertex { position: [x1, y1] });
|
||||||
@@ -671,53 +855,26 @@ pub async fn run() {
|
|||||||
// Process buildings
|
// Process buildings
|
||||||
if let Some(buildings) = state_guard.buildings.get(tile) {
|
if let Some(buildings) = state_guard.buildings.get(tile) {
|
||||||
for building in buildings {
|
for building in buildings {
|
||||||
if building.points.len() < 3 { continue; }
|
let points = &building.points;
|
||||||
|
for chunk in points.chunks(8) {
|
||||||
let mut flat_points = Vec::new();
|
if chunk.len() < 8 { break; }
|
||||||
let mut projected_points = Vec::new();
|
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;
|
||||||
for p in &building.points {
|
let (x, y) = project(lat, lon);
|
||||||
let (x, y) = project(p[0], p[1]);
|
building_vertex_data.push(Vertex { position: [x, y] });
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process landuse
|
|
||||||
if let Some(landuse) = state_guard.landuse.get(tile) {
|
if let Some(landuse) = state_guard.landuse.get(tile) {
|
||||||
for area in landuse {
|
for area in landuse {
|
||||||
if area.points.len() < 3 { continue; }
|
let points = &area.points;
|
||||||
|
for chunk in points.chunks(8) {
|
||||||
let mut flat_points = Vec::new();
|
if chunk.len() < 8 { break; }
|
||||||
let mut projected_points = Vec::new();
|
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;
|
||||||
for p in &area.points {
|
let (x, y) = project(lat, lon);
|
||||||
let (x, y) = project(p[0], p[1]);
|
landuse_vertex_data.push(Vertex { position: [x, y] });
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -725,25 +882,13 @@ pub async fn run() {
|
|||||||
// Process water
|
// Process water
|
||||||
if let Some(water) = state_guard.water.get(tile) {
|
if let Some(water) = state_guard.water.get(tile) {
|
||||||
for area in water {
|
for area in water {
|
||||||
if area.points.len() < 3 { continue; }
|
let points = &area.points;
|
||||||
|
for chunk in points.chunks(8) {
|
||||||
let mut flat_points = Vec::new();
|
if chunk.len() < 8 { break; }
|
||||||
let mut projected_points = Vec::new();
|
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;
|
||||||
for p in &area.points {
|
let (x, y) = project(lat, lon);
|
||||||
let (x, y) = project(p[0], p[1]);
|
water_vertex_data.push(Vertex { position: [x, y] });
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -752,15 +897,27 @@ pub async fn run() {
|
|||||||
let mut railway_vertex_data = Vec::new();
|
let mut railway_vertex_data = Vec::new();
|
||||||
if let Some(railways) = state_guard.railways.get(tile) {
|
if let Some(railways) = state_guard.railways.get(tile) {
|
||||||
for way in railways {
|
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 mut chunks = points.chunks(8);
|
||||||
let p1 = &way.points[i];
|
let mut prev_x = 0.0;
|
||||||
let p2 = &way.points[i+1];
|
let mut prev_y = 0.0;
|
||||||
let (x1, y1) = project(p1[0], p1[1]);
|
let mut first = true;
|
||||||
let (x2, y2) = project(p2[0], p2[1]);
|
|
||||||
railway_vertex_data.push(Vertex { position: [x1, y1] });
|
while let Some(chunk) = chunks.next() {
|
||||||
railway_vertex_data.push(Vertex { position: [x2, y2] });
|
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),
|
contents: bytemuck::cast_slice(&road_vertex_data),
|
||||||
usage: wgpu::BufferUsages::VERTEX,
|
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 {
|
let building_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
label: Some("Tile Building Buffer"),
|
label: Some("Tile Building Buffer"),
|
||||||
@@ -817,14 +980,20 @@ pub async fn run() {
|
|||||||
water_index_count: water_vertex_data.len() as u32,
|
water_index_count: water_vertex_data.len() as u32,
|
||||||
railway_vertex_buffer: railway_buffer,
|
railway_vertex_buffer: railway_buffer,
|
||||||
railway_vertex_count: railway_vertex_data.len() as u32,
|
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
|
// 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();
|
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) {
|
if let Some(buffers) = state_guard.buffers.get(tile) {
|
||||||
tiles_to_render.push(buffers.clone());
|
tiles_to_render.push(buffers.clone());
|
||||||
}
|
}
|
||||||
@@ -838,92 +1007,34 @@ pub async fn run() {
|
|||||||
let state_clone = state.clone();
|
let state_clone = state.clone();
|
||||||
let window_clone_for_fetch = window.clone();
|
let window_clone_for_fetch = window.clone();
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
// Fetch nodes
|
// Fetch all data in one go
|
||||||
let url_nodes = format!("/api/tiles/{}/{}/{}", z, x, y);
|
let url = format!("/api/tiles/{}/{}/{}/all", z, x, y);
|
||||||
let nodes_data = if let Some(json) = fetch_cached(&url_nodes).await {
|
|
||||||
serde_json::from_str::<Vec<MapNode>>(&json).ok()
|
let tile_data = if let Some(bytes) = fetch_cached(&url).await {
|
||||||
|
bincode::deserialize::<TileData>(&bytes).ok()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch ways
|
if let Some(data) = tile_data {
|
||||||
let url_ways = format!("/api/tiles/{}/{}/{}/ways", z, x, y);
|
let mut guard = state_clone.lock().unwrap();
|
||||||
let ways_data = if let Some(json) = fetch_cached(&url_ways).await {
|
guard.nodes.insert((z, x, y), data.nodes);
|
||||||
serde_json::from_str::<Vec<MapWay>>(&json).ok()
|
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 {
|
} else {
|
||||||
None
|
// Failed to load
|
||||||
};
|
let mut guard = state_clone.lock().unwrap();
|
||||||
|
guard.pending_tiles.remove(&(z, x, y));
|
||||||
// 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()
|
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
resolve_target: None,
|
||||||
ops: wgpu::Operations {
|
ops: wgpu::Operations {
|
||||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||||
r: 0.15,
|
r: 0.95,
|
||||||
g: 0.15,
|
g: 0.95,
|
||||||
b: 0.15,
|
b: 0.95,
|
||||||
a: 1.0,
|
a: 1.0,
|
||||||
}),
|
}),
|
||||||
store: wgpu::StoreOp::Store,
|
store: wgpu::StoreOp::Store,
|
||||||
@@ -1001,6 +1112,14 @@ pub async fn run() {
|
|||||||
rpass.draw(0..buffers.road_vertex_count, 0..1);
|
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?)
|
// Draw Railways (on top of roads?)
|
||||||
if buffers.railway_vertex_count > 0 {
|
if buffers.railway_vertex_count > 0 {
|
||||||
rpass.set_pipeline(&railway_pipeline);
|
rpass.set_pipeline(&railway_pipeline);
|
||||||
@@ -1048,8 +1167,33 @@ pub async fn run() {
|
|||||||
|
|
||||||
queue.submit(Some(encoder.finish()));
|
queue.submit(Some(encoder.finish()));
|
||||||
frame.present();
|
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 => {
|
WindowEvent::CloseRequested => {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
elwt.exit();
|
elwt.exit();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -1198,7 +1342,7 @@ fn create_road_pipeline(
|
|||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
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
|
@fragment
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
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
|
@fragment
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
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
|
@fragment
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
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
|
@fragment
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
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,
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ tokio = { version = "1.0", features = ["full"] }
|
|||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
memmap2 = "0.9"
|
memmap2 = "0.9"
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
|
earcutr = "0.4"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use earcutr::earcut;
|
||||||
use osmpbf::{Element, ElementReader};
|
use osmpbf::{Element, ElementReader};
|
||||||
use scylla::SessionBuilder;
|
use scylla::SessionBuilder;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -8,7 +9,7 @@ use std::io::{BufWriter, Write, Seek, SeekFrom};
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use memmap2::Mmap;
|
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 {
|
struct NodeStore {
|
||||||
writer: Option<BufWriter<File>>,
|
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 {
|
fn should_include(tags: &HashMap<String, String>, zoom: u32) -> bool {
|
||||||
if zoom >= 14 { return true; }
|
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());
|
let waterway = tags.get("waterway").map(|s| s.as_str());
|
||||||
|
|
||||||
match zoom {
|
match zoom {
|
||||||
match zoom {
|
|
||||||
2 => {
|
2 => {
|
||||||
// Space View: Continents and Countries
|
// Space View: Continents and Countries
|
||||||
matches!(place, Some("continent" | "country")) ||
|
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 => {
|
6 => {
|
||||||
// Enterprise Grade: ONLY Motorways and Trunk roads. No primary/secondary.
|
// Enterprise Grade: ONLY Motorways and Trunk roads. No primary/secondary.
|
||||||
// ONLY Cities. No nature/landuse.
|
// ONLY Cities. No nature/landuse.
|
||||||
matches!(highway, Some("motorway" | "trunk")) ||
|
matches!(highway, Some("motorway" | "trunk" | "primary")) || // Added primary
|
||||||
matches!(place, Some("city")) ||
|
matches!(place, Some("city")) ||
|
||||||
matches!(natural, Some("water")) || // Only major water bodies
|
matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland")) ||
|
||||||
matches!(waterway, Some("river")) // Only major rivers
|
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 => {
|
9 => {
|
||||||
// Enterprise Grade: Add Primary roads.
|
// 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!(place, Some("city" | "town")) ||
|
||||||
matches!(railway, Some("rail")) ||
|
matches!(railway, Some("rail")) ||
|
||||||
matches!(natural, Some("water" | "wood")) ||
|
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"))
|
matches!(waterway, Some("river" | "riverbank"))
|
||||||
},
|
},
|
||||||
12 => {
|
12 => {
|
||||||
matches!(highway, Some("motorway" | "trunk" | "primary" | "secondary" | "tertiary")) ||
|
matches!(highway, Some("motorway" | "trunk" | "primary" | "secondary" | "tertiary")) ||
|
||||||
matches!(place, Some("city" | "town" | "village")) ||
|
matches!(place, Some("city" | "town" | "village")) ||
|
||||||
matches!(railway, Some("rail")) ||
|
matches!(railway, Some("rail")) ||
|
||||||
|
tags.contains_key("building") ||
|
||||||
tags.contains_key("landuse") ||
|
tags.contains_key("landuse") ||
|
||||||
tags.contains_key("leisure") ||
|
tags.contains_key("leisure") ||
|
||||||
matches!(natural, Some("water" | "wood" | "scrub" | "wetland" | "heath")) ||
|
matches!(natural, Some("water" | "wood" | "scrub" | "wetland" | "heath")) ||
|
||||||
@@ -396,11 +432,19 @@ async fn main() -> Result<()> {
|
|||||||
if !should_include(&tags, zoom) { continue; }
|
if !should_include(&tags, zoom) { continue; }
|
||||||
|
|
||||||
// Apply simplification based on zoom level
|
// Apply simplification based on zoom level
|
||||||
let epsilon = match zoom {
|
let base_epsilon = match zoom {
|
||||||
2 => 0.1, // Very high simplification
|
2 => 0.0001,
|
||||||
6 => 0.005, // High simplification
|
4 => 0.00005,
|
||||||
9 => 0.001, // Medium simplification
|
6 => 0.00002,
|
||||||
_ => 0.0, // No simplification
|
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 {
|
let simplified_points = if epsilon > 0.0 {
|
||||||
@@ -409,17 +453,28 @@ async fn main() -> Result<()> {
|
|||||||
points.clone()
|
points.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
if simplified_points.len() < 2 { continue; }
|
// Serialize points
|
||||||
|
let mut final_points = simplified_points;
|
||||||
|
|
||||||
let (first_lat, first_lon) = simplified_points[0];
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (x, y) = lat_lon_to_tile(first_lat, first_lon, zoom);
|
||||||
let zoom_i32 = zoom as i32;
|
let zoom_i32 = zoom as i32;
|
||||||
|
|
||||||
// Serialize simplified points
|
let mut blob = Vec::with_capacity(final_points.len() * 8); // 4 bytes lat + 4 bytes lon
|
||||||
let mut blob = Vec::with_capacity(simplified_points.len() * 16);
|
for (lat, lon) in final_points {
|
||||||
for (lat, lon) in simplified_points {
|
blob.extend_from_slice(&(lat as f32).to_le_bytes());
|
||||||
blob.extend_from_slice(&lat.to_be_bytes());
|
blob.extend_from_slice(&(lon as f32).to_le_bytes());
|
||||||
blob.extend_from_slice(&lon.to_be_bytes());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_highway {
|
if is_highway {
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ fi
|
|||||||
|
|
||||||
echo "Using PBF file: ${HOST_PBF_PATH:-./europe-latest.osm.pbf}"
|
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 --profile import up --build importer
|
||||||
|
docker compose -f docker-compose-remote.yml up --build
|
||||||
|
|||||||
Reference in New Issue
Block a user