update
This commit is contained in:
@@ -14,3 +14,8 @@ tower = "0.4"
|
||||
tower-http = { version = "0.5", features = ["cors", "fs", "compression-full"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
async-trait = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
mockall = "0.12"
|
||||
tokio-test = "0.4"
|
||||
|
||||
1
backend/src/api/handlers/mod.rs
Normal file
1
backend/src/api/handlers/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod tiles;
|
||||
167
backend/src/api/handlers/tiles.rs
Normal file
167
backend/src/api/handlers/tiles.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::header,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use crate::services::tile_service::TileService;
|
||||
|
||||
pub struct AppState {
|
||||
pub tile_service: Arc<TileService>,
|
||||
}
|
||||
|
||||
pub async fn get_tile(
|
||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
match state.tile_service.get_nodes(z, x, y).await {
|
||||
Ok(nodes) => {
|
||||
let bytes = bincode::serialize(&nodes).unwrap();
|
||||
([(header::CONTENT_TYPE, "application/octet-stream")], bytes).into_response()
|
||||
},
|
||||
Err(e) => (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Internal error: {}", e),
|
||||
).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_tile_ways(
|
||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
match state.tile_service.get_ways(z, x, y).await {
|
||||
Ok(ways) => {
|
||||
let bytes = bincode::serialize(&ways).unwrap();
|
||||
([(header::CONTENT_TYPE, "application/octet-stream")], bytes).into_response()
|
||||
},
|
||||
Err(e) => (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Internal error: {}", e),
|
||||
).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_tile_buildings(
|
||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
match state.tile_service.get_buildings(z, x, y).await {
|
||||
Ok(buildings) => {
|
||||
let bytes = bincode::serialize(&buildings).unwrap();
|
||||
([(header::CONTENT_TYPE, "application/octet-stream")], bytes).into_response()
|
||||
},
|
||||
Err(e) => (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Internal error: {}", e),
|
||||
).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_tile_landuse(
|
||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
match state.tile_service.get_landuse(z, x, y).await {
|
||||
Ok(landuse) => {
|
||||
let bytes = bincode::serialize(&landuse).unwrap();
|
||||
([(header::CONTENT_TYPE, "application/octet-stream")], bytes).into_response()
|
||||
},
|
||||
Err(e) => (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Internal error: {}", e),
|
||||
).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_tile_water(
|
||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
match state.tile_service.get_water(z, x, y).await {
|
||||
Ok(water) => {
|
||||
let bytes = bincode::serialize(&water).unwrap();
|
||||
([(header::CONTENT_TYPE, "application/octet-stream")], bytes).into_response()
|
||||
},
|
||||
Err(e) => (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Internal error: {}", e),
|
||||
).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_tile_railways(
|
||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
match state.tile_service.get_railways(z, x, y).await {
|
||||
Ok(railways) => {
|
||||
let bytes = bincode::serialize(&railways).unwrap();
|
||||
([(header::CONTENT_TYPE, "application/octet-stream")], bytes).into_response()
|
||||
},
|
||||
Err(e) => (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Internal error: {}", e),
|
||||
).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_tile_all(
|
||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
// Parallel fetching for performance
|
||||
let (nodes, ways, buildings, landuse, water, railways) = tokio::join!(
|
||||
state.tile_service.get_nodes(z, x, y),
|
||||
state.tile_service.get_ways(z, x, y),
|
||||
state.tile_service.get_buildings(z, x, y),
|
||||
state.tile_service.get_landuse(z, x, y),
|
||||
state.tile_service.get_water(z, x, y),
|
||||
state.tile_service.get_railways(z, x, y),
|
||||
);
|
||||
|
||||
// Initial capacity estimaton (removed unused var)
|
||||
|
||||
// Check errors and separate results?
|
||||
// For now, the endpoint likely expects a single binary blob of combined types or just simple sequential data.
|
||||
// The original logic didn't seem to implement get_tile_all in the viewed main.rs snippet.
|
||||
// Based on standard practices, I'll return a struct or just concatenation if that's what the frontend expects.
|
||||
// Wait, the original main.rs HAD `get_tile_all` registered but the implementation was truncated in view.
|
||||
// I will implementation it by combining all into a single structured response or just separate vectors if I define a TileData DTO.
|
||||
// Checking the plan... "models/tile_response.rs". I haven't created that yet.
|
||||
// For now, I'll stick to individual endpoints as primary, but `get_tile_all` is useful.
|
||||
// I'll return a tuple or struct serialized.
|
||||
|
||||
// Let's assume a structure similar to what the frontend expects.
|
||||
// If I don't know the exact format of `get_tile_all` from previous code, I should look at it or just stub it safely.
|
||||
// Actually, looking at `frontend/src/lib.rs` might reveal what it expects.
|
||||
|
||||
// For simplicity in this step, I will implement it returning a generic error if fails, or a tuple.
|
||||
if let (Ok(n), Ok(w), Ok(b), Ok(l), Ok(wt), Ok(r)) = (nodes, ways, buildings, landuse, water, railways) {
|
||||
#[derive(serde::Serialize)]
|
||||
struct TileData {
|
||||
nodes: Vec<crate::domain::MapNode>,
|
||||
ways: Vec<crate::domain::MapWay>,
|
||||
buildings: Vec<crate::domain::MapWay>,
|
||||
landuse: Vec<crate::domain::MapWay>,
|
||||
water: Vec<crate::domain::MapWay>,
|
||||
railways: Vec<crate::domain::MapWay>,
|
||||
}
|
||||
|
||||
let data = TileData {
|
||||
nodes: n,
|
||||
ways: w,
|
||||
buildings: b,
|
||||
landuse: l,
|
||||
water: wt,
|
||||
railways: r,
|
||||
};
|
||||
let bytes = bincode::serialize(&data).unwrap();
|
||||
([(header::CONTENT_TYPE, "application/octet-stream")], bytes).into_response()
|
||||
} else {
|
||||
(
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to fetch tile data".to_string(),
|
||||
).into_response()
|
||||
}
|
||||
}
|
||||
2
backend/src/api/mod.rs
Normal file
2
backend/src/api/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod handlers;
|
||||
pub mod models;
|
||||
1
backend/src/api/models/mod.rs
Normal file
1
backend/src/api/models/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
// Models will be added here
|
||||
5
backend/src/domain/mod.rs
Normal file
5
backend/src/domain/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod node;
|
||||
pub mod way;
|
||||
|
||||
pub use node::MapNode;
|
||||
pub use way::MapWay;
|
||||
10
backend/src/domain/node.rs
Normal file
10
backend/src/domain/node.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct MapNode {
|
||||
pub id: i64,
|
||||
pub lat: f64,
|
||||
pub lon: f64,
|
||||
pub tags: HashMap<String, String>,
|
||||
}
|
||||
9
backend/src/domain/way.rs
Normal file
9
backend/src/domain/way.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct MapWay {
|
||||
pub id: i64,
|
||||
pub tags: HashMap<String, String>,
|
||||
pub points: Vec<u8>, // Flat f32 array (lat, lon, lat, lon...)
|
||||
}
|
||||
@@ -1,22 +1,27 @@
|
||||
mod db;
|
||||
mod domain;
|
||||
mod repositories;
|
||||
mod services;
|
||||
mod api;
|
||||
|
||||
use axum::{
|
||||
routing::get,
|
||||
Router,
|
||||
extract::{State, Path},
|
||||
http::header,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use scylla::{Session, SessionBuilder};
|
||||
use scylla::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 {
|
||||
scylla_session: Arc<Session>,
|
||||
}
|
||||
use crate::repositories::way_repository::WayRepository;
|
||||
use crate::repositories::node_repository::NodeRepository;
|
||||
use crate::services::tile_service::TileService;
|
||||
use crate::api::handlers::tiles::{
|
||||
get_tile, get_tile_ways, get_tile_buildings,
|
||||
get_tile_landuse, get_tile_water, get_tile_railways, get_tile_all,
|
||||
AppState
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -24,7 +29,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
println!("Connecting to ScyllaDB...");
|
||||
println!("Starting backend with landuse support...");
|
||||
println!("Starting backend with layered architecture...");
|
||||
let uri = std::env::var("SCYLLA_URI").unwrap_or_else(|_| "127.0.0.1:9042".to_string());
|
||||
|
||||
let session = SessionBuilder::new()
|
||||
@@ -32,15 +37,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
// Initialize schema and seed data
|
||||
// Initialize schema and seed data (Keep existing db module for now)
|
||||
db::initialize_schema(&session).await?;
|
||||
db::seed_data(&session).await?;
|
||||
|
||||
let session = Arc::new(session);
|
||||
let session_arc = Arc::new(session);
|
||||
println!("Connected to ScyllaDB!");
|
||||
|
||||
// Dependency Injection
|
||||
let node_repo = Arc::new(NodeRepository::new(session_arc.clone()));
|
||||
let way_repo = Arc::new(WayRepository::new(session_arc.clone()));
|
||||
let tile_service = Arc::new(TileService::new(node_repo, way_repo));
|
||||
|
||||
let state = Arc::new(AppState {
|
||||
scylla_session: session,
|
||||
tile_service: tile_service,
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
@@ -67,298 +77,3 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn health_check() -> &'static str {
|
||||
"OK"
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MapNode {
|
||||
id: i64,
|
||||
lat: f64,
|
||||
lon: f64,
|
||||
tags: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
async fn get_tile(
|
||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
let query = "SELECT id, lat, lon, tags FROM map_data.nodes WHERE zoom = ? AND tile_x = ? AND tile_y = ?";
|
||||
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))).unwrap();
|
||||
nodes.push(MapNode { id, lat, lon, tags });
|
||||
}
|
||||
|
||||
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<u8>, // Flat f32 array
|
||||
}
|
||||
|
||||
async fn get_tile_ways(
|
||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
let query = "SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 50000";
|
||||
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) = 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();
|
||||
|
||||
ways.push(MapWay { id, tags, points });
|
||||
}
|
||||
|
||||
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>>,
|
||||
) -> impl IntoResponse {
|
||||
let query = "SELECT id, tags, points FROM map_data.buildings WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 50000";
|
||||
|
||||
// Optimization: Don't load buildings for low zoom levels
|
||||
if z < 12 {
|
||||
return ([(header::CONTENT_TYPE, "application/octet-stream")], vec![]).into_response();
|
||||
}
|
||||
|
||||
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) = 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();
|
||||
|
||||
buildings.push(MapWay { id, tags, points });
|
||||
}
|
||||
|
||||
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>>,
|
||||
) -> impl IntoResponse {
|
||||
// Optimization: Don't load landuse for low zoom levels
|
||||
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 = ? LIMIT 50000";
|
||||
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 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>>,
|
||||
) -> impl IntoResponse {
|
||||
let query = "SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 50000";
|
||||
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) = 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();
|
||||
|
||||
water.push(MapWay { id, tags, points });
|
||||
}
|
||||
|
||||
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>>,
|
||||
) -> impl IntoResponse {
|
||||
let query = "SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 50000";
|
||||
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) = 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();
|
||||
|
||||
railways.push(MapWay { id, tags, points });
|
||||
}
|
||||
|
||||
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 = "SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 50000";
|
||||
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 = ? LIMIT 50000";
|
||||
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 = ? LIMIT 50000";
|
||||
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 = "SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 50000";
|
||||
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 = "SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 50000";
|
||||
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()
|
||||
}
|
||||
|
||||
2
backend/src/repositories/mod.rs
Normal file
2
backend/src/repositories/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod way_repository;
|
||||
pub mod node_repository;
|
||||
37
backend/src/repositories/node_repository.rs
Normal file
37
backend/src/repositories/node_repository.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use scylla::Session;
|
||||
use std::sync::Arc;
|
||||
use crate::domain::node::MapNode;
|
||||
use std::error::Error;
|
||||
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
#[async_trait::async_trait]
|
||||
pub trait NodeRepositoryTrait: Send + Sync {
|
||||
async fn find_nodes_in_tile(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapNode>, Box<dyn Error + Send + Sync>>;
|
||||
}
|
||||
|
||||
pub struct NodeRepository {
|
||||
session: Arc<Session>,
|
||||
}
|
||||
|
||||
impl NodeRepository {
|
||||
pub fn new(session: Arc<Session>) -> Self {
|
||||
Self { session }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NodeRepositoryTrait for NodeRepository {
|
||||
async fn find_nodes_in_tile(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapNode>, Box<dyn Error + Send + Sync>> {
|
||||
let query = "SELECT id, lat, lon, tags FROM map_data.nodes WHERE zoom = ? AND tile_x = ? AND tile_y = ?";
|
||||
let rows = self.session.query(query, (z, x, y)).await?.rows.unwrap_or_default();
|
||||
|
||||
let mut nodes = Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
let (id, lat, lon, tags) = row.into_typed::<(i64, f64, f64, std::collections::HashMap<String, String>)>()
|
||||
.map_err(|e| Box::<dyn Error + Send + Sync>::from(format!("Serialization error: {}", e)))?;
|
||||
|
||||
nodes.push(MapNode { id, lat, lon, tags });
|
||||
}
|
||||
Ok(nodes)
|
||||
}
|
||||
}
|
||||
67
backend/src/repositories/way_repository.rs
Normal file
67
backend/src/repositories/way_repository.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use scylla::Session;
|
||||
use std::sync::Arc;
|
||||
use crate::domain::way::MapWay;
|
||||
use std::error::Error;
|
||||
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
#[async_trait::async_trait]
|
||||
pub trait WayRepositoryTrait: Send + Sync {
|
||||
async fn find_ways_in_tile(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>>;
|
||||
async fn find_buildings_in_tile(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>>;
|
||||
async fn find_landuse_in_tile(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>>;
|
||||
async fn find_water_in_tile(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>>;
|
||||
async fn find_railways_in_tile(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>>;
|
||||
}
|
||||
|
||||
pub struct WayRepository {
|
||||
session: Arc<Session>,
|
||||
}
|
||||
|
||||
impl WayRepository {
|
||||
pub fn new(session: Arc<Session>) -> Self {
|
||||
Self { session }
|
||||
}
|
||||
|
||||
async fn query_ways(&self, query: &str, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>> {
|
||||
let rows = self.session.query(query, (z, x, y)).await?.rows.unwrap_or_default();
|
||||
|
||||
let mut ways = Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
let (id, tags, points) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>()
|
||||
.map_err(|e| Box::<dyn Error + Send + Sync>::from(format!("Serialization error: {}", e)))?;
|
||||
|
||||
ways.push(MapWay { id, tags, points });
|
||||
}
|
||||
Ok(ways)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl WayRepositoryTrait for WayRepository {
|
||||
async fn find_ways_in_tile(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>> {
|
||||
let query = "SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 50000";
|
||||
self.query_ways(query, z, x, y).await
|
||||
}
|
||||
|
||||
async fn find_buildings_in_tile(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>> {
|
||||
if z < 12 { return Ok(Vec::new()); }
|
||||
let query = "SELECT id, tags, points FROM map_data.buildings WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 50000";
|
||||
self.query_ways(query, z, x, y).await
|
||||
}
|
||||
|
||||
async fn find_landuse_in_tile(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>> {
|
||||
if z < 4 { return Ok(Vec::new()); }
|
||||
let query = "SELECT id, tags, points FROM map_data.landuse WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 50000";
|
||||
self.query_ways(query, z, x, y).await
|
||||
}
|
||||
|
||||
async fn find_water_in_tile(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>> {
|
||||
let query = "SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 50000";
|
||||
self.query_ways(query, z, x, y).await
|
||||
}
|
||||
|
||||
async fn find_railways_in_tile(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>> {
|
||||
let query = "SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 50000";
|
||||
self.query_ways(query, z, x, y).await
|
||||
}
|
||||
}
|
||||
1
backend/src/services/mod.rs
Normal file
1
backend/src/services/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod tile_service;
|
||||
70
backend/src/services/tile_service.rs
Normal file
70
backend/src/services/tile_service.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use std::sync::Arc;
|
||||
use crate::repositories::way_repository::WayRepositoryTrait;
|
||||
use crate::repositories::node_repository::NodeRepositoryTrait;
|
||||
use crate::domain::node::MapNode;
|
||||
use crate::domain::way::MapWay;
|
||||
use std::error::Error;
|
||||
|
||||
pub struct TileService {
|
||||
node_repo: Arc<dyn NodeRepositoryTrait>,
|
||||
way_repo: Arc<dyn WayRepositoryTrait>,
|
||||
}
|
||||
|
||||
impl TileService {
|
||||
pub fn new(node_repo: Arc<dyn NodeRepositoryTrait>, way_repo: Arc<dyn WayRepositoryTrait>) -> Self {
|
||||
Self { node_repo, way_repo }
|
||||
}
|
||||
|
||||
pub async fn get_nodes(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapNode>, Box<dyn Error + Send + Sync>> {
|
||||
self.node_repo.find_nodes_in_tile(z, x, y).await
|
||||
}
|
||||
|
||||
pub async fn get_ways(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>> {
|
||||
self.way_repo.find_ways_in_tile(z, x, y).await
|
||||
}
|
||||
|
||||
pub async fn get_buildings(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>> {
|
||||
self.way_repo.find_buildings_in_tile(z, x, y).await
|
||||
}
|
||||
|
||||
pub async fn get_landuse(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>> {
|
||||
self.way_repo.find_landuse_in_tile(z, x, y).await
|
||||
}
|
||||
|
||||
pub async fn get_water(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>> {
|
||||
self.way_repo.find_water_in_tile(z, x, y).await
|
||||
}
|
||||
|
||||
pub async fn get_railways(&self, z: i32, x: i32, y: i32) -> Result<Vec<MapWay>, Box<dyn Error + Send + Sync>> {
|
||||
self.way_repo.find_railways_in_tile(z, x, y).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::domain::node::MapNode;
|
||||
use crate::repositories::node_repository::MockNodeRepositoryTrait;
|
||||
use crate::repositories::way_repository::MockWayRepositoryTrait;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_nodes() {
|
||||
let mut mock_node_repo = MockNodeRepositoryTrait::new();
|
||||
let mut mock_way_repo = MockWayRepositoryTrait::new();
|
||||
|
||||
mock_node_repo
|
||||
.expect_find_nodes_in_tile()
|
||||
.with(mockall::predicate::eq(1), mockall::predicate::eq(2), mockall::predicate::eq(3))
|
||||
.times(1)
|
||||
.returning(|_, _, _| Ok(vec![
|
||||
MapNode { id: 1, lat: 0.0, lon: 0.0, tags: HashMap::new() }
|
||||
]));
|
||||
|
||||
let service = TileService::new(Arc::new(mock_node_repo), Arc::new(mock_way_repo));
|
||||
|
||||
let result = service.get_nodes(1, 2, 3).await;
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap().len(), 1);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,8 @@ web-sys = { version = "0.3", features = [
|
||||
"Response",
|
||||
"HtmlInputElement",
|
||||
"PositionOptions",
|
||||
"DomTokenList",
|
||||
"CssStyleDeclaration",
|
||||
] }
|
||||
wgpu = { version = "0.19", default-features = false, features = ["webgl", "wgsl"] }
|
||||
winit = { version = "0.29", default-features = false, features = ["rwh_06"] }
|
||||
|
||||
@@ -781,6 +781,13 @@
|
||||
<span class="menu-label">Recents</span>
|
||||
</a>
|
||||
<div class="menu-divider"></div>
|
||||
<!-- Transportation Toggle -->
|
||||
<button type="button" class="menu-item" id="btn-transport" title="Toggle Public Transport"
|
||||
style="background:none; border:none; width:100%; font-family:inherit;">
|
||||
<span class="menu-icon">🚇</span>
|
||||
<span class="menu-label">Transit</span>
|
||||
</button>
|
||||
<div class="menu-divider"></div>
|
||||
<!-- Recent locations (placeholder tiles) -->
|
||||
<div class="recent-tiles">
|
||||
<div class="recent-tile" title="Recent Location">
|
||||
@@ -1168,6 +1175,15 @@
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.menu-item.active {
|
||||
background: rgba(0, 122, 255, 0.1);
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.menu-item.active .menu-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
@@ -1229,8 +1245,9 @@
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1324,18 +1341,17 @@
|
||||
</style>
|
||||
|
||||
<script type="module">
|
||||
import init from './wasm.js?v=fixed_labels_v20';
|
||||
import init, { run } from './wasm.js?v=transit_hover_v1';
|
||||
|
||||
async function run() {
|
||||
async function main() {
|
||||
try {
|
||||
await init();
|
||||
console.log("WASM initialized");
|
||||
// run() is auto-called via #[wasm_bindgen(start)] in lib.rs
|
||||
} catch (e) {
|
||||
console.error("Failed to initialize WASM:", e);
|
||||
console.error("Wasm failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
main();
|
||||
|
||||
// Hamburger menu toggle
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//! Camera and input state management
|
||||
|
||||
/// GPU-compatible camera uniform data
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
@@ -19,7 +17,16 @@ pub struct Camera {
|
||||
}
|
||||
|
||||
impl Camera {
|
||||
pub fn to_uniform(&self, is_dark: bool) -> CameraUniform {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
x: 0.5,
|
||||
y: 0.5,
|
||||
zoom: 1.0,
|
||||
aspect: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_uniform(&self, is_dark: bool, show_transit: bool) -> CameraUniform {
|
||||
// Simple 2D orthographic projection-like transform
|
||||
// We want to map world coordinates to clip space [-1, 1]
|
||||
// zoom controls how much of the world we see.
|
||||
@@ -39,14 +46,9 @@ impl Camera {
|
||||
],
|
||||
theme: [
|
||||
if is_dark { 1.0 } else { 0.0 },
|
||||
0.0, 0.0, 0.0
|
||||
if show_transit { 1.0 } else { 0.0 },
|
||||
0.0, 0.0
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mouse/touch input state for drag operations
|
||||
pub struct InputState {
|
||||
pub is_dragging: bool,
|
||||
pub last_cursor: Option<(f64, f64)>,
|
||||
}
|
||||
2
frontend/src/domain/mod.rs
Normal file
2
frontend/src/domain/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod camera;
|
||||
pub mod state;
|
||||
@@ -1,8 +1,5 @@
|
||||
//! Application state management
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::types::{MapNode, MapWay, TileBuffers};
|
||||
use crate::geo::KalmanFilter;
|
||||
|
||||
@@ -20,6 +17,8 @@ pub struct AppState {
|
||||
pub user_location: Option<(f64, f64)>,
|
||||
pub kalman_filter: Option<KalmanFilter>,
|
||||
pub watch_id: Option<i32>,
|
||||
pub show_transit: bool,
|
||||
pub cursor_position: Option<[f64; 2]>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -37,6 +36,8 @@ impl AppState {
|
||||
user_location: None,
|
||||
kalman_filter: None,
|
||||
watch_id: None,
|
||||
show_transit: false,
|
||||
cursor_position: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,3 +47,18 @@ impl Default for AppState {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Mouse/touch input state for drag operations
|
||||
pub struct InputState {
|
||||
pub is_dragging: bool,
|
||||
pub last_cursor: Option<(f64, f64)>,
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_dragging: false,
|
||||
last_cursor: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
//! Label rendering for map features
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
use crate::camera::Camera;
|
||||
use crate::state::AppState;
|
||||
use crate::domain::camera::Camera;
|
||||
use crate::domain::state::AppState;
|
||||
use crate::geo::project;
|
||||
use crate::tiles::get_visible_tiles;
|
||||
use crate::services::tile_service::TileService;
|
||||
|
||||
/// A candidate label for rendering
|
||||
pub struct LabelCandidate {
|
||||
@@ -43,9 +43,9 @@ pub fn update_labels(
|
||||
// Clear existing labels
|
||||
container.set_inner_html("");
|
||||
|
||||
let visible_tiles = get_visible_tiles(camera);
|
||||
let visible_tiles = TileService::get_visible_tiles(camera);
|
||||
let is_dark = document.document_element().map(|e| e.get_attribute("data-theme").unwrap_or_default() == "dark").unwrap_or(false);
|
||||
let uniforms = camera.to_uniform(is_dark);
|
||||
let uniforms = camera.to_uniform(is_dark, state.show_transit);
|
||||
|
||||
let mut candidates: Vec<LabelCandidate> = Vec::new();
|
||||
let zoom = camera.zoom;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,12 @@ pub fn create_building_pipeline(
|
||||
// Buildings: Light: #d9d9d9 (0.85), Dark: #333333 (0.2)
|
||||
let is_dark = camera.theme.x;
|
||||
let color = mix(vec3<f32>(0.92, 0.91, 0.90), vec3<f32>(0.2, 0.2, 0.2), is_dark);
|
||||
|
||||
// Transit Mode: Dim non-transit features
|
||||
if (camera.theme.y > 0.5) {
|
||||
return vec4<f32>(mix(color, vec3<f32>(0.5, 0.5, 0.5), 0.7), 0.3);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
"#)),
|
||||
@@ -136,8 +142,14 @@ pub fn create_colored_building_pipeline(
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let is_dark = camera.theme.x;
|
||||
// Dark theme: darken the light theme color
|
||||
let dark_color = in.v_color * 0.5;
|
||||
let dark_color = in.v_color * 0.4; // Darker buildings in dark mode
|
||||
let color = mix(in.v_color, dark_color, is_dark);
|
||||
|
||||
// Transit Mode: Dim non-transit features
|
||||
if (camera.theme.y > 0.5) {
|
||||
return vec4<f32>(mix(color, vec3<f32>(0.5, 0.5, 0.5), 0.7), 0.3);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
"#)),
|
||||
|
||||
@@ -63,6 +63,48 @@ pub struct RoadVertex {
|
||||
pub road_type: f32, // 0=motorway, 1=primary, 2=secondary, 3=residential
|
||||
}
|
||||
|
||||
/// GPU vertex for railways with center position, normal offset, and color
|
||||
/// The shader will apply: final_position = center + normal * width_scale
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct RailwayVertex {
|
||||
pub center: [f32; 2],
|
||||
pub normal: [f32; 2],
|
||||
pub color: [f32; 3],
|
||||
pub type_id: f32, // 0=standard, 1=u-bahn (dotted), 2=tram
|
||||
}
|
||||
|
||||
impl RailwayVertex {
|
||||
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<RailwayVertex>() as wgpu::BufferAddress,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &[
|
||||
wgpu::VertexAttribute {
|
||||
offset: 0,
|
||||
shader_location: 0,
|
||||
format: wgpu::VertexFormat::Float32x2, // center
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
|
||||
shader_location: 1,
|
||||
format: wgpu::VertexFormat::Float32x2, // normal
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: (std::mem::size_of::<[f32; 2]>() * 2) as wgpu::BufferAddress,
|
||||
shader_location: 2,
|
||||
format: wgpu::VertexFormat::Float32x3, // color
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: (std::mem::size_of::<[f32; 2]>() * 2 + std::mem::size_of::<[f32; 3]>()) as wgpu::BufferAddress,
|
||||
shader_location: 3,
|
||||
format: wgpu::VertexFormat::Float32, // type
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RoadVertex {
|
||||
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
|
||||
wgpu::VertexBufferLayout {
|
||||
|
||||
@@ -34,8 +34,14 @@ pub fn create_landuse_green_pipeline(
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let is_dark = camera.theme.x;
|
||||
// Green: Light #cdebb0, Dark #2d4a2d
|
||||
let color = mix(vec3<f32>(0.804, 0.922, 0.690), vec3<f32>(0.18, 0.29, 0.18), is_dark);
|
||||
// Green: Light #E8F5E3 (Apple Maps pale green), Dark #2d4a2d
|
||||
let color = mix(vec3<f32>(0.910, 0.961, 0.890), vec3<f32>(0.18, 0.29, 0.18), is_dark);
|
||||
|
||||
// Transit Mode: Dim non-transit features
|
||||
if (camera.theme.y > 0.5) {
|
||||
return vec4<f32>(mix(color, vec3<f32>(0.5, 0.5, 0.5), 0.7), 0.3);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
"#)),
|
||||
@@ -75,8 +81,14 @@ pub fn create_landuse_residential_pipeline(
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let is_dark = camera.theme.x;
|
||||
// Light: #e0dfdf, Dark: #1a1a1a (very dark grey for residential areas)
|
||||
let color = mix(vec3<f32>(0.88, 0.87, 0.87), vec3<f32>(0.1, 0.1, 0.1), is_dark);
|
||||
// Light: #EDE8E1 (Apple Maps beige), Dark: #1a1a1a
|
||||
let color = mix(vec3<f32>(0.929, 0.910, 0.882), vec3<f32>(0.1, 0.1, 0.1), is_dark);
|
||||
|
||||
// Transit Mode: Dim non-transit features
|
||||
if (camera.theme.y > 0.5) {
|
||||
return vec4<f32>(mix(color, vec3<f32>(0.5, 0.5, 0.5), 0.7), 0.3);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
"#)),
|
||||
@@ -118,6 +130,12 @@ pub fn create_sand_pipeline(
|
||||
let is_dark = camera.theme.x;
|
||||
// Sand: #e6d5ac (Light), Dark Sand: #5c5545 (Dark)
|
||||
let color = mix(vec3<f32>(0.90, 0.83, 0.67), vec3<f32>(0.36, 0.33, 0.27), is_dark);
|
||||
|
||||
// Transit Mode: Dim non-transit features
|
||||
if (camera.theme.y > 0.5) {
|
||||
return vec4<f32>(mix(color, vec3<f32>(0.5, 0.5, 0.5), 0.7), 0.3);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
"#)),
|
||||
|
||||
@@ -7,7 +7,7 @@ pub mod roads;
|
||||
pub mod landuse;
|
||||
pub mod railway;
|
||||
|
||||
pub use common::{Vertex, ColoredVertex, RoadVertex, create_simple_pipeline, create_road_pipeline};
|
||||
pub use common::{Vertex, ColoredVertex, RoadVertex, RailwayVertex, create_simple_pipeline, create_road_pipeline};
|
||||
pub use building::{create_building_pipeline, create_colored_building_pipeline};
|
||||
pub use water::{create_water_pipeline, create_water_line_pipeline};
|
||||
pub use roads::{
|
||||
@@ -22,4 +22,4 @@ pub use roads::{
|
||||
generate_road_geometry,
|
||||
};
|
||||
pub use landuse::{create_landuse_green_pipeline, create_landuse_residential_pipeline, create_sand_pipeline};
|
||||
pub use railway::create_railway_pipeline;
|
||||
pub use railway::{create_railway_pipeline, generate_railway_geometry};
|
||||
|
||||
@@ -1,6 +1,115 @@
|
||||
//! Railway render pipeline
|
||||
|
||||
use super::common::ColoredVertex;
|
||||
use super::common::RailwayVertex;
|
||||
|
||||
// Helper for vector math (copied from roads.rs to avoid pub clutter)
|
||||
fn normalize(v: [f32; 2]) -> [f32; 2] {
|
||||
let len = (v[0]*v[0] + v[1]*v[1]).sqrt();
|
||||
if len < 0.00001 { [0.0, 0.0] } else { [v[0]/len, v[1]/len] }
|
||||
}
|
||||
|
||||
fn dot(a: [f32; 2], b: [f32; 2]) -> f32 {
|
||||
a[0]*b[0] + a[1]*b[1]
|
||||
}
|
||||
|
||||
/// Generate thick railway geometry (quads)
|
||||
pub fn generate_railway_geometry(points: &[[f32; 2]], color: [f32; 3], railway_type: f32) -> Vec<RailwayVertex> {
|
||||
let vertices = Vec::new();
|
||||
if points.len() < 2 { return vertices; }
|
||||
|
||||
// Computes normals for each segment
|
||||
let mut segment_normals = Vec::with_capacity(points.len() - 1);
|
||||
for i in 0..points.len()-1 {
|
||||
let p1 = points[i];
|
||||
let p2 = points[i+1];
|
||||
let dx = p2[0] - p1[0];
|
||||
let dy = p2[1] - p1[1];
|
||||
let len = (dx*dx + dy*dy).sqrt();
|
||||
if len < 0.000001 {
|
||||
segment_normals.push([0.0, 0.0]); // Degenerate
|
||||
} else {
|
||||
segment_normals.push([-dy/len, dx/len]);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate vertices (left/right pairs) for each point
|
||||
let mut point_pairs = Vec::with_capacity(points.len() * 2);
|
||||
|
||||
for i in 0..points.len() {
|
||||
// Find normal for this vertex (miter)
|
||||
let normal: [f32; 2];
|
||||
let mut miter_len = 1.0f32;
|
||||
|
||||
if i == 0 {
|
||||
normal = segment_normals[0];
|
||||
} else if i == points.len() - 1 {
|
||||
normal = segment_normals[i-1];
|
||||
} else {
|
||||
let n1 = segment_normals[i-1];
|
||||
let n2 = segment_normals[i];
|
||||
|
||||
if dot(n1, n1) == 0.0 { normal = n2; }
|
||||
else if dot(n2, n2) == 0.0 { normal = n1; }
|
||||
else {
|
||||
let sum = [n1[0] + n2[0], n1[1] + n2[1]];
|
||||
let miter = normalize(sum);
|
||||
let d = dot(miter, n1);
|
||||
|
||||
if d.abs() < 0.1 { // Too sharp
|
||||
normal = n1;
|
||||
} else {
|
||||
miter_len = 1.0 / d;
|
||||
if miter_len > 4.0 { miter_len = 4.0; }
|
||||
normal = miter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let p = points[i];
|
||||
let nx = normal[0] * miter_len;
|
||||
let ny = normal[1] * miter_len;
|
||||
|
||||
point_pairs.push(RailwayVertex {
|
||||
center: p,
|
||||
normal: [nx, ny],
|
||||
color,
|
||||
type_id: railway_type,
|
||||
});
|
||||
point_pairs.push(RailwayVertex {
|
||||
center: p,
|
||||
normal: [-nx, -ny],
|
||||
color,
|
||||
type_id: railway_type,
|
||||
});
|
||||
}
|
||||
|
||||
// Triangulate
|
||||
let mut triangle_vertices = Vec::with_capacity((points.len() - 1) * 6);
|
||||
for i in 0..points.len()-1 {
|
||||
// Skip degenerate segment
|
||||
if dot(segment_normals[i], segment_normals[i]) == 0.0 { continue; }
|
||||
|
||||
let i_base = 2*i;
|
||||
let j_base = 2*(i+1);
|
||||
|
||||
let v1 = point_pairs[i_base]; // Left curr
|
||||
let v2 = point_pairs[i_base+1]; // Right curr
|
||||
let v3 = point_pairs[j_base]; // Left next
|
||||
let v4 = point_pairs[j_base+1]; // Right next
|
||||
|
||||
// Tri 1: v1, v2, v3
|
||||
triangle_vertices.push(v1);
|
||||
triangle_vertices.push(v2);
|
||||
triangle_vertices.push(v3);
|
||||
|
||||
// Tri 2: v2, v4, v3
|
||||
triangle_vertices.push(v2);
|
||||
triangle_vertices.push(v4);
|
||||
triangle_vertices.push(v3);
|
||||
}
|
||||
|
||||
triangle_vertices
|
||||
}
|
||||
|
||||
pub fn create_railway_pipeline(
|
||||
device: &wgpu::Device,
|
||||
@@ -18,13 +127,16 @@ pub fn create_railway_pipeline(
|
||||
var<uniform> camera: CameraUniform;
|
||||
|
||||
struct VertexInput {
|
||||
@location(0) position: vec2<f32>,
|
||||
@location(1) color: vec3<f32>,
|
||||
@location(0) center: vec2<f32>,
|
||||
@location(1) normal: vec2<f32>,
|
||||
@location(2) color: vec3<f32>,
|
||||
@location(3) type_id: f32,
|
||||
};
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) color: vec3<f32>,
|
||||
@location(1) type_id: f32,
|
||||
};
|
||||
|
||||
@vertex
|
||||
@@ -33,28 +145,72 @@ pub fn create_railway_pipeline(
|
||||
) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
|
||||
let world_pos = model.position;
|
||||
// Railway width logic (similar to roads)
|
||||
// Standard/S-Bahn: 3.0px
|
||||
// U-Bahn (type=1.0): 2.0px (thinner)
|
||||
var base_pixels = 3.0;
|
||||
if (model.type_id > 0.5 && model.type_id < 1.5) {
|
||||
base_pixels = 2.0;
|
||||
}
|
||||
|
||||
// Using 1000.0 constant to make lines thicker (visible on standard screens)
|
||||
let width = base_pixels / (camera.params.x * 1000.0);
|
||||
|
||||
let offset = model.normal * width;
|
||||
let world_pos = model.center + offset;
|
||||
|
||||
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);
|
||||
out.color = model.color;
|
||||
out.type_id = model.type_id;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let is_dark = camera.theme.x;
|
||||
let is_transit_mode = camera.theme.y > 0.5;
|
||||
|
||||
// Use vertex color if it has any value, otherwise use default grey
|
||||
let has_color = in.color.r > 0.01 || in.color.g > 0.01 || in.color.b > 0.01;
|
||||
|
||||
// Dashed line logic for U-Bahn (type_id ~ 1.0)
|
||||
if (in.type_id > 0.5 && in.type_id < 1.5) {
|
||||
// Simple screen-space stipple pattern
|
||||
// Use clip_position (pixel coordinates) to create gaps
|
||||
let p = in.clip_position.x + in.clip_position.y;
|
||||
// Pattern: Solid for 10px, Gap for 10px?
|
||||
// sin(p * freq) > 0.
|
||||
// freq = 0.3 approx 20px period
|
||||
if (sin(p * 0.3) < -0.2) { // biased to be slightly more solid than gap
|
||||
discard;
|
||||
}
|
||||
}
|
||||
|
||||
var final_color: vec3<f32>;
|
||||
|
||||
if (has_color) {
|
||||
return vec4<f32>(in.color, 1.0);
|
||||
final_color = in.color;
|
||||
} else {
|
||||
// Fallback: Light: #808080 (grey), Dark: #5a5a5a (darker grey)
|
||||
let is_dark = camera.theme.x;
|
||||
let color = mix(vec3<f32>(0.5, 0.5, 0.5), vec3<f32>(0.35, 0.35, 0.35), is_dark);
|
||||
return vec4<f32>(color, 1.0);
|
||||
final_color = mix(vec3<f32>(0.5, 0.5, 0.5), vec3<f32>(0.35, 0.35, 0.35), is_dark);
|
||||
}
|
||||
|
||||
// Transit Mode Logic:
|
||||
// When transit mode is ON: Show railways at full opacity
|
||||
// When transit mode is OFF: Dim/hide railways
|
||||
if (!is_transit_mode) {
|
||||
// Transit mode is OFF - hide all railways
|
||||
return vec4<f32>(final_color, 0.0);
|
||||
} else {
|
||||
// Transit mode is ON - show railways
|
||||
// Dim non-colored railways slightly to emphasize colored ones
|
||||
if (!has_color) {
|
||||
return vec4<f32>(mix(final_color, vec3<f32>(0.5, 0.5, 0.5), 0.3), 0.5);
|
||||
}
|
||||
return vec4<f32>(final_color, 1.0);
|
||||
}
|
||||
}
|
||||
"#)),
|
||||
@@ -73,7 +229,7 @@ pub fn create_railway_pipeline(
|
||||
module: &shader,
|
||||
entry_point: "vs_main",
|
||||
buffers: &[
|
||||
ColoredVertex::desc(),
|
||||
RailwayVertex::desc(),
|
||||
],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
@@ -81,12 +237,12 @@ pub fn create_railway_pipeline(
|
||||
entry_point: "fs_main",
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: *format,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::LineList,
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: None,
|
||||
|
||||
@@ -16,7 +16,7 @@ fn dot(a: [f32; 2], b: [f32; 2]) -> f32 {
|
||||
|
||||
/// Generate properly connected road geometry with miter joins
|
||||
pub fn generate_road_geometry(points: &[[f32; 2]], lanes: f32, road_type: f32) -> Vec<super::common::RoadVertex> {
|
||||
let mut vertices = Vec::new();
|
||||
let vertices = Vec::new();
|
||||
if points.len() < 2 { return vertices; }
|
||||
|
||||
// Computes normals for each segment
|
||||
@@ -166,6 +166,12 @@ pub fn create_road_motorway_outline_pipeline(
|
||||
let is_dark = camera.theme.x;
|
||||
// Outline Color: Subtle Grey (Google Maps style)
|
||||
let color = mix(vec3<f32>(0.8, 0.8, 0.8), vec3<f32>(0.3, 0.3, 0.3), is_dark);
|
||||
|
||||
// Transit Mode: Dim non-transit features
|
||||
if (camera.theme.y > 0.5) {
|
||||
return vec4<f32>(mix(color, vec3<f32>(0.5, 0.5, 0.5), 0.7), 0.3);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
"#)),
|
||||
@@ -217,6 +223,12 @@ pub fn create_road_motorway_pipeline(
|
||||
let is_dark = camera.theme.x;
|
||||
// Fill Color: Slight off-white/warm #fffcfa
|
||||
let color = mix(vec3<f32>(1.0, 0.99, 0.98), vec3<f32>(0.5, 0.5, 0.5), is_dark);
|
||||
|
||||
// Transit Mode: Dim non-transit features
|
||||
if (camera.theme.y > 0.5) {
|
||||
return vec4<f32>(mix(color, vec3<f32>(0.5, 0.5, 0.5), 0.7), 0.3);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
"#)),
|
||||
@@ -272,6 +284,12 @@ pub fn create_road_primary_outline_pipeline(
|
||||
let is_dark = camera.theme.x;
|
||||
// Outline: Subtle Grey
|
||||
let color = mix(vec3<f32>(0.8, 0.8, 0.8), vec3<f32>(0.3, 0.3, 0.3), is_dark);
|
||||
|
||||
// Transit Mode: Dim non-transit features
|
||||
if (camera.theme.y > 0.5) {
|
||||
return vec4<f32>(mix(color, vec3<f32>(0.5, 0.5, 0.5), 0.7), 0.3);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
"#)),
|
||||
@@ -324,6 +342,12 @@ pub fn create_road_primary_pipeline(
|
||||
// Fill
|
||||
// Fill
|
||||
let color = mix(vec3<f32>(0.99, 0.75, 0.44), vec3<f32>(0.88, 0.62, 0.25), is_dark);
|
||||
|
||||
// Transit Mode: Dim non-transit features
|
||||
if (camera.theme.y > 0.5) {
|
||||
return vec4<f32>(mix(color, vec3<f32>(0.5, 0.5, 0.5), 0.7), 0.3);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
"#)),
|
||||
@@ -380,6 +404,12 @@ pub fn create_road_secondary_outline_pipeline(
|
||||
// Outline: Gray
|
||||
// Outline: Gray
|
||||
let color = mix(vec3<f32>(0.5, 0.5, 0.5), vec3<f32>(0.2, 0.2, 0.2), is_dark);
|
||||
|
||||
// Transit Mode: Dim non-transit features
|
||||
if (camera.theme.y > 0.5) {
|
||||
return vec4<f32>(mix(color, vec3<f32>(0.5, 0.5, 0.5), 0.7), 0.3);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
"#)),
|
||||
@@ -432,6 +462,12 @@ pub fn create_road_secondary_pipeline(
|
||||
// Fill: White/Dark Gray
|
||||
// Fill: White/Dark Gray
|
||||
let color = mix(vec3<f32>(1.0, 1.0, 1.0), vec3<f32>(0.35, 0.35, 0.35), is_dark);
|
||||
|
||||
// Transit Mode: Dim non-transit features
|
||||
if (camera.theme.y > 0.5) {
|
||||
return vec4<f32>(mix(color, vec3<f32>(0.5, 0.5, 0.5), 0.7), 0.3);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
"#)),
|
||||
@@ -488,6 +524,12 @@ pub fn create_road_residential_outline_pipeline(
|
||||
// Outline: Gray
|
||||
// Outline: Gray
|
||||
let color = mix(vec3<f32>(0.5, 0.5, 0.5), vec3<f32>(0.2, 0.2, 0.2), is_dark);
|
||||
|
||||
// Transit Mode: Dim non-transit features
|
||||
if (camera.theme.y > 0.5) {
|
||||
return vec4<f32>(mix(color, vec3<f32>(0.5, 0.5, 0.5), 0.7), 0.3);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
"#)),
|
||||
@@ -540,6 +582,12 @@ pub fn create_road_residential_pipeline(
|
||||
// Fill: White/Dark Gray
|
||||
// Fill: White/Dark Gray
|
||||
let color = mix(vec3<f32>(1.0, 1.0, 1.0), vec3<f32>(0.35, 0.35, 0.35), is_dark);
|
||||
|
||||
// Transit Mode: Dim non-transit features
|
||||
if (camera.theme.y > 0.5) {
|
||||
return vec4<f32>(mix(color, vec3<f32>(0.5, 0.5, 0.5), 0.7), 0.3);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
"#)),
|
||||
|
||||
@@ -46,9 +46,15 @@ pub fn create_water_pipeline(
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
// Water: Light: #9ecaff, Dark: #1a2639
|
||||
// Water: Light: #C3E6FF (Apple Maps), Dark: #1a2639
|
||||
let is_dark = camera.theme.x;
|
||||
let color = mix(vec3<f32>(0.647, 0.749, 0.867), vec3<f32>(0.1, 0.15, 0.22), is_dark);
|
||||
let color = mix(vec3<f32>(0.765, 0.902, 1.0), vec3<f32>(0.1, 0.15, 0.22), is_dark);
|
||||
|
||||
// Transit Mode: Dim non-transit features
|
||||
if (camera.theme.y > 0.5) {
|
||||
return vec4<f32>(mix(color, vec3<f32>(0.5, 0.5, 0.5), 0.7), 0.3);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
"#)),
|
||||
@@ -140,8 +146,14 @@ pub fn create_water_line_pipeline(
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let is_dark = camera.theme.x;
|
||||
// Light: #a5bfdd (same/similar to water), Dark: #4a6fa5
|
||||
let color = mix(vec3<f32>(0.647, 0.749, 0.867), vec3<f32>(0.29, 0.44, 0.65), is_dark);
|
||||
// Light: #B8DAFF (lighter for streams), Dark: #4a6fa5
|
||||
let color = mix(vec3<f32>(0.722, 0.855, 1.0), vec3<f32>(0.29, 0.44, 0.65), is_dark);
|
||||
|
||||
// Transit Mode: Dim non-transit features
|
||||
if (camera.theme.y > 0.5) {
|
||||
return vec4<f32>(mix(color, vec3<f32>(0.5, 0.5, 0.5), 0.7), 0.3);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
"#)),
|
||||
|
||||
40
frontend/src/repositories/http_client.rs
Normal file
40
frontend/src/repositories/http_client.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
pub struct HttpClient;
|
||||
|
||||
impl HttpClient {
|
||||
/// Fetch tile data with caching
|
||||
pub async fn fetch_cached(url: &str) -> Option<Vec<u8>> {
|
||||
let window = web_sys::window()?;
|
||||
let caches = window.caches().ok()?;
|
||||
let cache_name = "map-data-v5-sand";
|
||||
let cache = wasm_bindgen_futures::JsFuture::from(caches.open(cache_name)).await.ok()?;
|
||||
let cache: web_sys::Cache = cache.dyn_into().ok()?;
|
||||
|
||||
let request = web_sys::Request::new_with_str(url).ok()?;
|
||||
let match_promise = cache.match_with_request(&request);
|
||||
let match_val = wasm_bindgen_futures::JsFuture::from(match_promise).await.ok()?;
|
||||
|
||||
if !match_val.is_undefined() {
|
||||
let response: web_sys::Response = match_val.dyn_into().ok()?;
|
||||
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
|
||||
let response_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request)).await.ok()?;
|
||||
let response: web_sys::Response = response_val.dyn_into().ok()?;
|
||||
|
||||
// Clone response for cache
|
||||
let response_clone = response.clone().ok()?;
|
||||
let put_promise = cache.put_with_request(&request, &response_clone);
|
||||
wasm_bindgen_futures::JsFuture::from(put_promise).await.ok()?;
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
1
frontend/src/repositories/mod.rs
Normal file
1
frontend/src/repositories/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod http_client;
|
||||
77
frontend/src/services/camera_service.rs
Normal file
77
frontend/src/services/camera_service.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use winit::event::{ElementState, MouseButton, MouseScrollDelta};
|
||||
use crate::domain::camera::Camera;
|
||||
use crate::domain::state::InputState;
|
||||
|
||||
pub struct CameraService;
|
||||
|
||||
impl CameraService {
|
||||
// Zoom constants
|
||||
const MIN_ZOOM: f32 = 20.0;
|
||||
const MAX_ZOOM: f32 = 50000.0;
|
||||
|
||||
pub fn handle_resize(camera: &Arc<Mutex<Camera>>, width: u32, height: u32) {
|
||||
let mut cam = camera.lock().unwrap();
|
||||
cam.aspect = width as f32 / height as f32;
|
||||
}
|
||||
|
||||
pub fn handle_mouse_input(input: &mut InputState, state: ElementState, button: MouseButton) {
|
||||
if button == MouseButton::Left {
|
||||
input.is_dragging = state == ElementState::Pressed;
|
||||
input.last_cursor = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_cursor_move(
|
||||
camera: &Arc<Mutex<Camera>>,
|
||||
input: &mut InputState,
|
||||
position_x: f64,
|
||||
position_y: f64,
|
||||
screen_height: f32
|
||||
) {
|
||||
if input.is_dragging {
|
||||
if let Some((lx, ly)) = input.last_cursor {
|
||||
let dx = position_x - lx;
|
||||
let dy = position_y - ly;
|
||||
|
||||
let mut cam = camera.lock().unwrap();
|
||||
|
||||
// Use IDENTICAL formula for both X and Y
|
||||
// Since Y works correctly with 2/(zoom*height), use the same for X
|
||||
let scale = 2.0 / (cam.zoom * screen_height);
|
||||
|
||||
cam.x -= (dx as f32) * scale;
|
||||
cam.y -= (dy as f32) * scale;
|
||||
}
|
||||
input.last_cursor = Some((position_x, position_y));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_wheel(camera: &Arc<Mutex<Camera>>, delta: MouseScrollDelta) {
|
||||
let scroll = match delta {
|
||||
MouseScrollDelta::LineDelta(_, y) => y as f64,
|
||||
MouseScrollDelta::PixelDelta(pos) => pos.y / 50.0,
|
||||
};
|
||||
let mut cam = camera.lock().unwrap();
|
||||
let factor = if scroll > 0.0 { 1.1f32 } else { 0.9f32 };
|
||||
cam.zoom = (cam.zoom * factor).clamp(Self::MIN_ZOOM, Self::MAX_ZOOM);
|
||||
}
|
||||
|
||||
// Helper to convert slider (0-100) to zoom (logarithmic)
|
||||
pub fn slider_to_zoom(val: f64) -> f32 {
|
||||
let t = val / 100.0;
|
||||
let log_min = Self::MIN_ZOOM.ln();
|
||||
let log_max = Self::MAX_ZOOM.ln();
|
||||
let log_zoom = log_min + (log_max - log_min) * t as f32;
|
||||
log_zoom.exp()
|
||||
}
|
||||
|
||||
// Helper to convert zoom to slider (0-100)
|
||||
pub fn zoom_to_slider(zoom: f32) -> f64 {
|
||||
let log_min = Self::MIN_ZOOM.ln();
|
||||
let log_max = Self::MAX_ZOOM.ln();
|
||||
let log_zoom = zoom.ln();
|
||||
let t = (log_zoom - log_min) / (log_max - log_min);
|
||||
(t * 100.0) as f64
|
||||
}
|
||||
}
|
||||
6
frontend/src/services/mod.rs
Normal file
6
frontend/src/services/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod tile_service;
|
||||
pub mod camera_service;
|
||||
pub mod transit_service;
|
||||
pub mod render_service;
|
||||
|
||||
|
||||
379
frontend/src/services/render_service.rs
Normal file
379
frontend/src/services/render_service.rs
Normal file
@@ -0,0 +1,379 @@
|
||||
use wgpu::util::DeviceExt;
|
||||
use crate::pipelines::{
|
||||
self, Vertex, ColoredVertex, RoadVertex, RailwayVertex, generate_railway_geometry,
|
||||
create_colored_building_pipeline, create_water_pipeline, create_water_line_pipeline,
|
||||
create_railway_pipeline, create_road_motorway_outline_pipeline, create_road_motorway_pipeline,
|
||||
create_road_primary_outline_pipeline, create_road_primary_pipeline,
|
||||
create_road_secondary_outline_pipeline, create_road_secondary_pipeline,
|
||||
create_road_residential_outline_pipeline, create_road_residential_pipeline,
|
||||
create_landuse_green_pipeline, create_landuse_residential_pipeline, create_sand_pipeline
|
||||
};
|
||||
use crate::types::TileBuffers;
|
||||
use crate::domain::state::AppState;
|
||||
use crate::geo::project;
|
||||
|
||||
pub struct RenderService {
|
||||
// Pipelines
|
||||
pub building_pipeline: wgpu::RenderPipeline,
|
||||
pub water_pipeline: wgpu::RenderPipeline,
|
||||
pub water_line_pipeline: wgpu::RenderPipeline,
|
||||
pub railway_pipeline: wgpu::RenderPipeline,
|
||||
pub motorway_outline: wgpu::RenderPipeline,
|
||||
pub motorway_fill: wgpu::RenderPipeline,
|
||||
pub primary_outline: wgpu::RenderPipeline,
|
||||
pub primary_fill: wgpu::RenderPipeline,
|
||||
pub secondary_outline: wgpu::RenderPipeline,
|
||||
pub secondary_fill: wgpu::RenderPipeline,
|
||||
pub residential_outline: wgpu::RenderPipeline,
|
||||
pub residential_fill: wgpu::RenderPipeline,
|
||||
pub landuse_green_pipeline: wgpu::RenderPipeline,
|
||||
pub landuse_residential_pipeline: wgpu::RenderPipeline,
|
||||
pub sand_pipeline: wgpu::RenderPipeline,
|
||||
}
|
||||
|
||||
impl RenderService {
|
||||
pub fn new(device: &wgpu::Device, format: &wgpu::TextureFormat, camera_layout: &wgpu::BindGroupLayout) -> Self {
|
||||
Self {
|
||||
building_pipeline: create_colored_building_pipeline(device, format, camera_layout),
|
||||
water_pipeline: create_water_pipeline(device, format, camera_layout),
|
||||
water_line_pipeline: create_water_line_pipeline(device, format, camera_layout),
|
||||
railway_pipeline: create_railway_pipeline(device, format, camera_layout),
|
||||
motorway_outline: create_road_motorway_outline_pipeline(device, format, camera_layout),
|
||||
motorway_fill: create_road_motorway_pipeline(device, format, camera_layout),
|
||||
primary_outline: create_road_primary_outline_pipeline(device, format, camera_layout),
|
||||
primary_fill: create_road_primary_pipeline(device, format, camera_layout),
|
||||
secondary_outline: create_road_secondary_outline_pipeline(device, format, camera_layout),
|
||||
secondary_fill: create_road_secondary_pipeline(device, format, camera_layout),
|
||||
residential_outline: create_road_residential_outline_pipeline(device, format, camera_layout),
|
||||
residential_fill: create_road_residential_pipeline(device, format, camera_layout),
|
||||
landuse_green_pipeline: create_landuse_green_pipeline(device, format, camera_layout),
|
||||
landuse_residential_pipeline: create_landuse_residential_pipeline(device, format, camera_layout),
|
||||
sand_pipeline: create_sand_pipeline(device, format, camera_layout),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_tile_buffers(device: &wgpu::Device, state: &mut AppState, tile: (i32, i32, i32)) {
|
||||
// Build vertex data for each feature type - roads use RoadVertex
|
||||
let mut road_motorway_vertex_data: Vec<RoadVertex> = Vec::new();
|
||||
let mut road_primary_vertex_data: Vec<RoadVertex> = Vec::new();
|
||||
let mut road_secondary_vertex_data: Vec<RoadVertex> = Vec::new();
|
||||
let mut road_residential_vertex_data: Vec<RoadVertex> = Vec::new();
|
||||
let mut building_vertex_data: Vec<ColoredVertex> = Vec::new();
|
||||
let mut landuse_green_vertex_data: Vec<Vertex> = Vec::new();
|
||||
let mut landuse_residential_vertex_data: Vec<Vertex> = Vec::new();
|
||||
let mut landuse_sand_vertex_data: Vec<Vertex> = Vec::new();
|
||||
let mut water_vertex_data: Vec<Vertex> = Vec::new();
|
||||
let mut railway_vertex_data: Vec<RailwayVertex> = Vec::new();
|
||||
let mut water_line_vertex_data: Vec<Vertex> = Vec::new();
|
||||
|
||||
// Process ways (roads) - generate quad geometry for zoom-aware width
|
||||
if let Some(ways) = state.ways.get(&tile) {
|
||||
for way in ways {
|
||||
let highway = way.tags.get("highway").map(|s| s.as_str());
|
||||
if highway.is_none() { continue; }
|
||||
|
||||
let highway_type = highway.unwrap();
|
||||
|
||||
// Road type: 0=motorway, 1=primary, 2=secondary, 3=residential
|
||||
let road_type: f32 = match highway_type {
|
||||
"motorway" | "motorway_link" | "trunk" | "trunk_link" => 0.0,
|
||||
"primary" | "primary_link" => 1.0,
|
||||
"secondary" | "secondary_link" => 2.0,
|
||||
_ => 3.0,
|
||||
};
|
||||
|
||||
let target: &mut Vec<RoadVertex> = match highway_type {
|
||||
"motorway" | "motorway_link" | "trunk" | "trunk_link" => &mut road_motorway_vertex_data,
|
||||
"primary" | "primary_link" => &mut road_primary_vertex_data,
|
||||
"secondary" | "secondary_link" => &mut road_secondary_vertex_data,
|
||||
_ => &mut road_residential_vertex_data,
|
||||
};
|
||||
|
||||
// Parse lanes (default based on road type)
|
||||
let default_lanes: f32 = match highway_type {
|
||||
"motorway" | "trunk" => 4.0,
|
||||
"motorway_link" | "trunk_link" | "primary" => 2.0,
|
||||
_ => 2.0,
|
||||
};
|
||||
let lanes: f32 = way.tags.get("lanes")
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(default_lanes);
|
||||
|
||||
// Parse all road centerline points
|
||||
let mut centers: Vec<[f32; 2]> = Vec::new();
|
||||
for chunk in way.points.chunks(8) {
|
||||
if chunk.len() < 8 { break; }
|
||||
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0u8; 4]));
|
||||
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4]));
|
||||
let (x, y) = project(lat as f64, lon as f64);
|
||||
centers.push([x, y]);
|
||||
}
|
||||
|
||||
// Generate connected geometry with miter joins
|
||||
let geom = pipelines::roads::generate_road_geometry(¢ers, lanes, road_type);
|
||||
target.extend(geom);
|
||||
}
|
||||
}
|
||||
|
||||
// Process buildings with type-based colors
|
||||
if let Some(buildings) = state.buildings.get(&tile) {
|
||||
for building in buildings {
|
||||
// Get building type from tags
|
||||
let building_type = building.tags.get("building").map(|s| s.as_str()).unwrap_or("yes");
|
||||
|
||||
// Also check amenity tag for public buildings (hospitals, schools, etc.)
|
||||
let amenity_type = building.tags.get("amenity").map(|s| s.as_str());
|
||||
let is_public_amenity = matches!(amenity_type,
|
||||
Some("hospital") | Some("school") | Some("university") | Some("college") |
|
||||
Some("kindergarten") | Some("library") | Some("place_of_worship") |
|
||||
Some("community_centre") | Some("townhall") | Some("police") |
|
||||
Some("fire_station") | Some("courthouse") | Some("embassy")
|
||||
);
|
||||
|
||||
// Assign color based on building type (light theme colors)
|
||||
let color: [f32; 3] = if is_public_amenity {
|
||||
// Public/Civic: light gray #e0ddd4
|
||||
[0.88, 0.87, 0.83]
|
||||
} else {
|
||||
match building_type {
|
||||
// Residential: cream #f2efe9
|
||||
"house" | "apartments" | "residential" | "detached" | "semidetached_house" | "terrace" | "dormitory" =>
|
||||
[0.95, 0.94, 0.91],
|
||||
// Commercial: tan #e8e4dc
|
||||
"commercial" | "retail" | "office" | "supermarket" | "kiosk" | "hotel" =>
|
||||
[0.91, 0.89, 0.86],
|
||||
// Industrial: gray-tan #d9d5cc
|
||||
"industrial" | "warehouse" | "factory" | "manufacture" =>
|
||||
[0.85, 0.84, 0.80],
|
||||
// Public/Civic: light gray #e0ddd4
|
||||
"school" | "hospital" | "university" | "government" | "public" | "church" | "cathedral" | "mosque" | "synagogue" | "temple" | "train_station" | "fire_station" | "police" | "library" | "kindergarten" =>
|
||||
[0.88, 0.87, 0.83],
|
||||
// Default: gray #d9d9d9
|
||||
_ => [0.85, 0.85, 0.85],
|
||||
}
|
||||
};
|
||||
|
||||
for chunk in building.points.chunks(8) {
|
||||
if chunk.len() < 8 { break; }
|
||||
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0u8; 4]));
|
||||
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4]));
|
||||
let (x, y) = project(lat as f64, lon as f64);
|
||||
building_vertex_data.push(ColoredVertex { position: [x, y], color });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process landuse
|
||||
if let Some(landuse_list) = state.landuse.get(&tile) {
|
||||
for landuse in landuse_list {
|
||||
let landuse_type = landuse.tags.get("landuse").or_else(|| landuse.tags.get("natural")).or_else(|| landuse.tags.get("leisure")).map(|s| s.as_str());
|
||||
|
||||
let target = match landuse_type {
|
||||
Some("forest") | Some("wood") | Some("grass") | Some("park") | Some("meadow") | Some("garden") | Some("nature_reserve") => &mut landuse_green_vertex_data,
|
||||
Some("beach") | Some("sand") | Some("island") => &mut landuse_sand_vertex_data,
|
||||
Some("residential") | Some("retail") | Some("commercial") | Some("industrial") => &mut landuse_residential_vertex_data,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
for chunk in landuse.points.chunks(8) {
|
||||
if chunk.len() < 8 { break; }
|
||||
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0u8; 4]));
|
||||
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4]));
|
||||
let (x, y) = project(lat as f64, lon as f64);
|
||||
target.push(Vertex { position: [x, y] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process water
|
||||
if let Some(water_list) = state.water.get(&tile) {
|
||||
for water in water_list {
|
||||
let is_line = water.tags.get("waterway").is_some();
|
||||
|
||||
// Parse all points first
|
||||
let mut water_points: Vec<Vertex> = Vec::new();
|
||||
for chunk in water.points.chunks(8) {
|
||||
if chunk.len() < 8 { break; }
|
||||
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0u8; 4]));
|
||||
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4]));
|
||||
let (x, y) = project(lat as f64, lon as f64);
|
||||
water_points.push(Vertex { position: [x, y] });
|
||||
}
|
||||
|
||||
if is_line {
|
||||
// For LineList: push pairs of vertices for each segment
|
||||
for i in 0..water_points.len().saturating_sub(1) {
|
||||
water_line_vertex_data.push(water_points[i]);
|
||||
water_line_vertex_data.push(water_points[i + 1]);
|
||||
}
|
||||
} else {
|
||||
// For TriangleList: just push all vertices
|
||||
water_vertex_data.extend(water_points);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process railways with colors and thick geometry
|
||||
if let Some(railway_list) = state.railways.get(&tile) {
|
||||
for railway in railway_list {
|
||||
// Parse color from tags (format: "#RRGGBB" or "#RGB")
|
||||
let color = railway.tags.get("colour")
|
||||
.or(railway.tags.get("color"))
|
||||
.map(|c| parse_hex_color(c))
|
||||
.unwrap_or([0.0, 0.0, 0.0]); // Default: no color (shader uses grey)
|
||||
|
||||
// Railway type: 0=standard, 1=u-bahn, 2=tram
|
||||
let rail_type_str = railway.tags.get("railway").map(|s| s.as_str()).unwrap_or("rail");
|
||||
let rail_type: f32 = match rail_type_str {
|
||||
"subway" => 1.0,
|
||||
"tram" => 2.0,
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
// Parse all points first for geometry generation
|
||||
let mut rail_points: Vec<[f32; 2]> = Vec::new();
|
||||
for chunk in railway.points.chunks(8) {
|
||||
if chunk.len() < 8 { break; }
|
||||
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0u8; 4]));
|
||||
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4]));
|
||||
let (x, y) = project(lat as f64, lon as f64);
|
||||
rail_points.push([x, y]);
|
||||
}
|
||||
|
||||
// Generate thick geometry (TriangleList of RailwayVertex)
|
||||
let geom = generate_railway_geometry(&rail_points, color, rail_type);
|
||||
railway_vertex_data.extend(geom);
|
||||
}
|
||||
}
|
||||
|
||||
// Create buffers if any data exists
|
||||
if !road_motorway_vertex_data.is_empty()
|
||||
|| !road_primary_vertex_data.is_empty()
|
||||
|| !road_secondary_vertex_data.is_empty()
|
||||
|| !road_residential_vertex_data.is_empty()
|
||||
|| !building_vertex_data.is_empty()
|
||||
|| !landuse_green_vertex_data.is_empty()
|
||||
|| !landuse_residential_vertex_data.is_empty()
|
||||
|| !water_vertex_data.is_empty()
|
||||
|| !railway_vertex_data.is_empty()
|
||||
|| !water_line_vertex_data.is_empty()
|
||||
{
|
||||
|
||||
let road_motorway_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Road Motorway Buffer"),
|
||||
contents: bytemuck::cast_slice(&road_motorway_vertex_data),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
let road_primary_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Road Primary Buffer"),
|
||||
contents: bytemuck::cast_slice(&road_primary_vertex_data),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
let road_secondary_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Road Secondary Buffer"),
|
||||
contents: bytemuck::cast_slice(&road_secondary_vertex_data),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
let road_residential_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Road Residential Buffer"),
|
||||
contents: bytemuck::cast_slice(&road_residential_vertex_data),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
let building_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Tile Building Buffer"),
|
||||
contents: bytemuck::cast_slice(&building_vertex_data),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
let landuse_green_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Tile Landuse Green Buffer"),
|
||||
contents: bytemuck::cast_slice(&landuse_green_vertex_data),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
let landuse_residential_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Tile Landuse Residential Buffer"),
|
||||
contents: bytemuck::cast_slice(&landuse_residential_vertex_data),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
let landuse_sand_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Tile Landuse Sand Buffer"),
|
||||
contents: bytemuck::cast_slice(&landuse_sand_vertex_data),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
|
||||
let water_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Tile Water Buffer"),
|
||||
contents: bytemuck::cast_slice(&water_vertex_data),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
let railway_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Tile Railway Buffer"),
|
||||
contents: bytemuck::cast_slice(&railway_vertex_data),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
let water_line_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Tile Water Line Buffer"),
|
||||
contents: bytemuck::cast_slice(&water_line_vertex_data),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
let buffers = TileBuffers {
|
||||
road_motorway_vertex_buffer: road_motorway_buffer,
|
||||
road_motorway_vertex_count: road_motorway_vertex_data.len() as u32,
|
||||
road_primary_vertex_buffer: road_primary_buffer,
|
||||
road_primary_vertex_count: road_primary_vertex_data.len() as u32,
|
||||
road_secondary_vertex_buffer: road_secondary_buffer,
|
||||
road_secondary_vertex_count: road_secondary_vertex_data.len() as u32,
|
||||
road_residential_vertex_buffer: road_residential_buffer,
|
||||
road_residential_vertex_count: road_residential_vertex_data.len() as u32,
|
||||
|
||||
building_vertex_buffer: building_buffer,
|
||||
building_index_count: building_vertex_data.len() as u32,
|
||||
|
||||
landuse_green_vertex_buffer: landuse_green_buffer,
|
||||
landuse_green_index_count: landuse_green_vertex_data.len() as u32,
|
||||
landuse_residential_vertex_buffer: landuse_residential_buffer,
|
||||
landuse_residential_index_count: landuse_residential_vertex_data.len() as u32,
|
||||
|
||||
landuse_sand_vertex_buffer: landuse_sand_buffer,
|
||||
landuse_sand_index_count: landuse_sand_vertex_data.len() as u32,
|
||||
|
||||
water_vertex_buffer: water_buffer,
|
||||
water_index_count: water_vertex_data.len() as u32,
|
||||
railway_vertex_buffer: railway_buffer,
|
||||
railway_vertex_count: railway_vertex_data.len() as u32,
|
||||
|
||||
water_line_vertex_buffer: water_line_buffer,
|
||||
water_line_vertex_count: water_line_vertex_data.len() as u32,
|
||||
};
|
||||
state.buffers.insert(tile, std::sync::Arc::new(buffers));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a hex color string (e.g., "#FF0000" or "#F00") into RGB floats [0.0-1.0]
|
||||
fn parse_hex_color(hex: &str) -> [f32; 3] {
|
||||
let hex = hex.trim_start_matches('#');
|
||||
|
||||
if hex.len() == 6 {
|
||||
// #RRGGBB format
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f32 / 255.0;
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f32 / 255.0;
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f32 / 255.0;
|
||||
[r, g, b]
|
||||
} else if hex.len() == 3 {
|
||||
// #RGB format (shorthand)
|
||||
let r = u8::from_str_radix(&hex[0..1], 16).unwrap_or(0) as f32 / 15.0;
|
||||
let g = u8::from_str_radix(&hex[1..2], 16).unwrap_or(0) as f32 / 15.0;
|
||||
let b = u8::from_str_radix(&hex[2..3], 16).unwrap_or(0) as f32 / 15.0;
|
||||
[r, g, b]
|
||||
} else {
|
||||
// Invalid format, return black (will trigger grey fallback in shader)
|
||||
[0.0, 0.0, 0.0]
|
||||
}
|
||||
}
|
||||
68
frontend/src/services/tile_service.rs
Normal file
68
frontend/src/services/tile_service.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use crate::domain::camera::Camera;
|
||||
|
||||
pub struct TileService;
|
||||
|
||||
impl TileService {
|
||||
/// Get visible tiles based on current camera position
|
||||
pub fn get_visible_tiles(camera: &Camera) -> Vec<(i32, i32, i32)> {
|
||||
// Select zoom level based on camera zoom
|
||||
// Zoom 6: World/Country view
|
||||
// Zoom 9: Region view
|
||||
// Zoom 12: City view
|
||||
// Zoom 14: Street view
|
||||
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 < 10000.0 {
|
||||
12
|
||||
} else {
|
||||
14
|
||||
};
|
||||
let n = 2.0f64.powi(z);
|
||||
|
||||
let half_width = 1.0 * camera.aspect / camera.zoom;
|
||||
let half_height = 1.0 / camera.zoom;
|
||||
|
||||
let min_x = (camera.x - half_width).max(0.0) as f64;
|
||||
let max_x = (camera.x + half_width).min(1.0) as f64;
|
||||
let min_y = (camera.y - half_height).max(0.0) as f64;
|
||||
let max_y = (camera.y + half_height).min(1.0) as f64;
|
||||
|
||||
let min_tile_x = (min_x * n).floor() as i32;
|
||||
let max_tile_x = (max_x * n).floor() as i32;
|
||||
let min_tile_y = (min_y * n).floor() as i32;
|
||||
let max_tile_y = (max_y * n).floor() as i32;
|
||||
|
||||
let mut tiles = Vec::new();
|
||||
for x in min_tile_x..=max_tile_x {
|
||||
for y in min_tile_y..=max_tile_y {
|
||||
tiles.push((z, x, y));
|
||||
}
|
||||
}
|
||||
tiles
|
||||
}
|
||||
|
||||
/// Get parent tile for the tile retention hierarchy
|
||||
pub 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))
|
||||
}
|
||||
}
|
||||
28
frontend/src/services/transit_service.rs
Normal file
28
frontend/src/services/transit_service.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use crate::domain::state::AppState;
|
||||
use std::cell::RefCell;
|
||||
|
||||
pub struct TransitService;
|
||||
|
||||
thread_local! {
|
||||
static GLOBAL_STATE: RefCell<Option<Arc<Mutex<AppState>>>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
impl TransitService {
|
||||
pub fn set_global_state(state: Arc<Mutex<AppState>>) {
|
||||
GLOBAL_STATE.with(|gs| {
|
||||
*gs.borrow_mut() = Some(state);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_global_transit() {
|
||||
GLOBAL_STATE.with(|gs| {
|
||||
if let Some(state_arc) = gs.borrow().as_ref() {
|
||||
if let Ok(mut state) = state_arc.try_lock() {
|
||||
state.show_transit = !state.show_transit;
|
||||
web_sys::console::log_1(&format!("Transit visibility toggled: {}", state.show_transit).into());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
//! Tile visibility and data fetching utilities
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
use crate::camera::Camera;
|
||||
|
||||
/// Fetch tile data with caching
|
||||
pub async fn fetch_cached(url: &str) -> Option<Vec<u8>> {
|
||||
let window = web_sys::window()?;
|
||||
let caches = window.caches().ok()?;
|
||||
let cache_name = "map-data-v5-sand";
|
||||
let cache = wasm_bindgen_futures::JsFuture::from(caches.open(cache_name)).await.ok()?;
|
||||
let cache: web_sys::Cache = cache.dyn_into().ok()?;
|
||||
|
||||
let request = web_sys::Request::new_with_str(url).ok()?;
|
||||
let match_promise = cache.match_with_request(&request);
|
||||
let match_val = wasm_bindgen_futures::JsFuture::from(match_promise).await.ok()?;
|
||||
|
||||
if !match_val.is_undefined() {
|
||||
let response: web_sys::Response = match_val.dyn_into().ok()?;
|
||||
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
|
||||
let response_val = wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request)).await.ok()?;
|
||||
let response: web_sys::Response = response_val.dyn_into().ok()?;
|
||||
|
||||
// Clone response for cache
|
||||
let response_clone = response.clone().ok()?;
|
||||
let put_promise = cache.put_with_request(&request, &response_clone);
|
||||
wasm_bindgen_futures::JsFuture::from(put_promise).await.ok()?;
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
/// Get visible tiles based on current camera position
|
||||
pub fn get_visible_tiles(camera: &Camera) -> Vec<(i32, i32, i32)> {
|
||||
// Select zoom level based on camera zoom
|
||||
// Zoom 6: World/Country view
|
||||
// Zoom 9: Region view
|
||||
// Zoom 12: City view
|
||||
// Zoom 14: Street view
|
||||
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 < 10000.0 {
|
||||
12
|
||||
} else {
|
||||
14
|
||||
};
|
||||
let n = 2.0f64.powi(z);
|
||||
|
||||
let half_width = 1.0 * camera.aspect / camera.zoom;
|
||||
let half_height = 1.0 / camera.zoom;
|
||||
|
||||
let min_x = (camera.x - half_width).max(0.0) as f64;
|
||||
let max_x = (camera.x + half_width).min(1.0) as f64;
|
||||
let min_y = (camera.y - half_height).max(0.0) as f64;
|
||||
let max_y = (camera.y + half_height).min(1.0) as f64;
|
||||
|
||||
let min_tile_x = (min_x * n).floor() as i32;
|
||||
let max_tile_x = (max_x * n).floor() as i32;
|
||||
let min_tile_y = (min_y * n).floor() as i32;
|
||||
let max_tile_y = (max_y * n).floor() as i32;
|
||||
|
||||
let mut tiles = Vec::new();
|
||||
for x in min_tile_x..=max_tile_x {
|
||||
for y in min_tile_y..=max_tile_y {
|
||||
tiles.push((z, x, y));
|
||||
}
|
||||
}
|
||||
tiles
|
||||
}
|
||||
|
||||
/// Get parent tile for the tile retention hierarchy
|
||||
pub 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))
|
||||
}
|
||||
1519
importer/Cargo.lock
generated
Normal file
1519
importer/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
importer/src/domain/mod.rs
Normal file
23
importer/src/domain/mod.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DbTask {
|
||||
Node {
|
||||
zoom: i32,
|
||||
id: i64,
|
||||
lat: f64,
|
||||
lon: f64,
|
||||
tags: HashMap<String, String>,
|
||||
x: i32,
|
||||
y: i32
|
||||
},
|
||||
Way {
|
||||
zoom: i32,
|
||||
table: &'static str,
|
||||
id: i64,
|
||||
tags: HashMap<String, String>,
|
||||
points: Vec<u8>,
|
||||
x: i32,
|
||||
y: i32
|
||||
},
|
||||
}
|
||||
@@ -1,376 +1,28 @@
|
||||
mod domain;
|
||||
mod repositories;
|
||||
mod services;
|
||||
mod parsers; // Empty for now, but kept for structure
|
||||
|
||||
use anyhow::Result;
|
||||
use earcutr::earcut;
|
||||
use osmpbf::{Element, ElementReader};
|
||||
use scylla::SessionBuilder;
|
||||
use std::collections::HashMap;
|
||||
use tokio::task::JoinSet;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{BufWriter, Write, Seek, SeekFrom};
|
||||
use std::path::{Path, PathBuf};
|
||||
use memmap2::Mmap;
|
||||
use std::sync::Arc;
|
||||
|
||||
const ZOOM_LEVELS: [u32; 6] = [2, 4, 6, 9, 12, 14];
|
||||
|
||||
// Store way geometries for multipolygon assembly
|
||||
struct WayStore {
|
||||
ways: HashMap<i64, Vec<i64>>, // way_id -> node_id list
|
||||
}
|
||||
|
||||
impl WayStore {
|
||||
fn new() -> Self {
|
||||
Self { ways: HashMap::new() }
|
||||
}
|
||||
|
||||
fn insert(&mut self, way_id: i64, node_refs: Vec<i64>) {
|
||||
self.ways.insert(way_id, node_refs);
|
||||
}
|
||||
|
||||
fn get(&self, way_id: i64) -> Option<&Vec<i64>> {
|
||||
self.ways.get(&way_id)
|
||||
}
|
||||
}
|
||||
|
||||
// Store railway ways for deferred insertion (after relation processing for colors)
|
||||
struct RailwayWay {
|
||||
id: i64,
|
||||
tags: HashMap<String, String>,
|
||||
points: Vec<u8>, // Serialized line blob
|
||||
first_lat: f64,
|
||||
first_lon: f64,
|
||||
}
|
||||
|
||||
struct RailwayStore {
|
||||
ways: HashMap<i64, RailwayWay>, // way_id -> railway data
|
||||
way_colors: HashMap<i64, String>, // way_id -> colour from route relation
|
||||
}
|
||||
|
||||
impl RailwayStore {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
ways: HashMap::new(),
|
||||
way_colors: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_way(&mut self, id: i64, tags: HashMap<String, String>, points: Vec<u8>, first_lat: f64, first_lon: f64) {
|
||||
self.ways.insert(id, RailwayWay { id, tags, points, first_lat, first_lon });
|
||||
}
|
||||
|
||||
fn set_color(&mut self, way_id: i64, color: String) {
|
||||
// Only set if not already set (first route relation wins)
|
||||
self.way_colors.entry(way_id).or_insert(color);
|
||||
}
|
||||
|
||||
fn get_color(&self, way_id: i64) -> Option<&String> {
|
||||
self.way_colors.get(&way_id)
|
||||
}
|
||||
}
|
||||
|
||||
// Assemble ways into MULTIPLE rings (connect end-to-end)
|
||||
// Rivers like the Isar have multiple separate channels/rings
|
||||
fn assemble_rings(way_ids: &[i64], way_store: &WayStore) -> Vec<Vec<i64>> {
|
||||
if way_ids.is_empty() { return Vec::new(); }
|
||||
|
||||
// Get all way geometries
|
||||
let mut segments: Vec<Vec<i64>> = Vec::new();
|
||||
for &way_id in way_ids {
|
||||
if let Some(nodes) = way_store.get(way_id) {
|
||||
if nodes.len() >= 2 {
|
||||
segments.push(nodes.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if segments.is_empty() { return Vec::new(); }
|
||||
|
||||
let mut completed_rings: Vec<Vec<i64>> = Vec::new();
|
||||
|
||||
// Keep assembling rings until we run out of segments
|
||||
while !segments.is_empty() {
|
||||
// Start a new ring with the first available segment
|
||||
let mut ring = segments.remove(0);
|
||||
|
||||
// Try to extend this ring
|
||||
let max_iterations = segments.len() * segments.len() + 100;
|
||||
let mut iterations = 0;
|
||||
|
||||
loop {
|
||||
iterations += 1;
|
||||
if iterations > max_iterations { break; }
|
||||
|
||||
let mut connected = false;
|
||||
|
||||
for i in 0..segments.len() {
|
||||
let seg = &segments[i];
|
||||
if seg.is_empty() { continue; }
|
||||
|
||||
let ring_start = *ring.first().unwrap();
|
||||
let ring_end = *ring.last().unwrap();
|
||||
let seg_start = *seg.first().unwrap();
|
||||
let seg_end = *seg.last().unwrap();
|
||||
|
||||
if ring_end == seg_start {
|
||||
// Connect: ring + seg (skip first node of seg)
|
||||
ring.extend(seg[1..].iter().cloned());
|
||||
segments.remove(i);
|
||||
connected = true;
|
||||
break;
|
||||
} else if ring_end == seg_end {
|
||||
// Connect: ring + reversed seg
|
||||
let reversed: Vec<i64> = seg.iter().rev().cloned().collect();
|
||||
ring.extend(reversed[1..].iter().cloned());
|
||||
segments.remove(i);
|
||||
connected = true;
|
||||
break;
|
||||
} else if ring_start == seg_end {
|
||||
// Connect: seg + ring
|
||||
let mut new_ring = seg.clone();
|
||||
new_ring.extend(ring[1..].iter().cloned());
|
||||
ring = new_ring;
|
||||
segments.remove(i);
|
||||
connected = true;
|
||||
break;
|
||||
} else if ring_start == seg_start {
|
||||
// Connect: reversed seg + ring
|
||||
let mut reversed: Vec<i64> = seg.iter().rev().cloned().collect();
|
||||
reversed.extend(ring[1..].iter().cloned());
|
||||
ring = reversed;
|
||||
segments.remove(i);
|
||||
connected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if ring is now closed
|
||||
if ring.len() >= 4 && ring.first() == ring.last() {
|
||||
completed_rings.push(ring);
|
||||
break; // Move to next ring
|
||||
}
|
||||
|
||||
// If no connection was made and ring isn't closed,
|
||||
// we can't extend this ring anymore
|
||||
if !connected {
|
||||
// Still save partial rings if they have enough points
|
||||
// This helps with incomplete data - at least show something
|
||||
if ring.len() >= 4 {
|
||||
// Force-close the ring
|
||||
let first = ring[0];
|
||||
ring.push(first);
|
||||
completed_rings.push(ring);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completed_rings
|
||||
}
|
||||
|
||||
struct NodeStore {
|
||||
writer: Option<BufWriter<File>>,
|
||||
mmap: Option<Mmap>,
|
||||
path: PathBuf,
|
||||
last_id: i64,
|
||||
}
|
||||
|
||||
impl NodeStore {
|
||||
fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&path)?;
|
||||
|
||||
let writer = BufWriter::with_capacity(10 * 1024 * 1024, file); // 10MB buffer
|
||||
|
||||
Ok(Self {
|
||||
writer: Some(writer),
|
||||
mmap: None,
|
||||
path,
|
||||
last_id: -1,
|
||||
})
|
||||
}
|
||||
|
||||
fn insert(&mut self, id: i64, lat: f64, lon: f64) -> Result<()> {
|
||||
if let Some(writer) = &mut self.writer {
|
||||
if id > self.last_id + 1 {
|
||||
let gap = id - self.last_id - 1;
|
||||
writer.seek(SeekFrom::Current(gap * 8))?;
|
||||
} else if id <= self.last_id {
|
||||
writer.seek(SeekFrom::Start(id as u64 * 8))?;
|
||||
}
|
||||
|
||||
let lat_i32 = (lat * 1e7) as i32;
|
||||
let lon_i32 = (lon * 1e7) as i32;
|
||||
writer.write_all(&lat_i32.to_le_bytes())?;
|
||||
writer.write_all(&lon_i32.to_le_bytes())?;
|
||||
|
||||
self.last_id = id;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare_for_reading(&mut self) -> Result<()> {
|
||||
self.writer = None; // Flush and close writer
|
||||
|
||||
let file = File::open(&self.path)?;
|
||||
let mmap = unsafe { Mmap::map(&file)? };
|
||||
self.mmap = Some(mmap);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get(&self, id: i64) -> Option<(f64, f64)> {
|
||||
if let Some(mmap) = &self.mmap {
|
||||
let offset = id as usize * 8;
|
||||
if offset + 8 <= mmap.len() {
|
||||
let chunk = &mmap[offset..offset+8];
|
||||
let lat_i32 = i32::from_le_bytes(chunk[0..4].try_into().unwrap());
|
||||
let lon_i32 = i32::from_le_bytes(chunk[4..8].try_into().unwrap());
|
||||
|
||||
if lat_i32 == 0 && lon_i32 == 0 { return None; }
|
||||
|
||||
return Some((lat_i32 as f64 / 1e7, lon_i32 as f64 / 1e7));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Ramer-Douglas-Peucker simplification
|
||||
fn perpendicular_distance(p: (f64, f64), line_start: (f64, f64), line_end: (f64, f64)) -> f64 {
|
||||
let (x, y) = p;
|
||||
let (x1, y1) = line_start;
|
||||
let (x2, y2) = line_end;
|
||||
|
||||
let dx = x2 - x1;
|
||||
let dy = y2 - y1;
|
||||
|
||||
if dx == 0.0 && dy == 0.0 {
|
||||
return ((x - x1).powi(2) + (y - y1).powi(2)).sqrt();
|
||||
}
|
||||
|
||||
let num = (dy * x - dx * y + x2 * y1 - y2 * x1).abs();
|
||||
let den = (dx.powi(2) + dy.powi(2)).sqrt();
|
||||
|
||||
num / den
|
||||
}
|
||||
|
||||
fn simplify_points(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> {
|
||||
if points.len() < 3 {
|
||||
return points.to_vec();
|
||||
}
|
||||
|
||||
let start = points[0];
|
||||
let end = points[points.len() - 1];
|
||||
|
||||
let mut max_dist = 0.0;
|
||||
let mut index = 0;
|
||||
|
||||
for i in 1..points.len() - 1 {
|
||||
let dist = perpendicular_distance(points[i], start, end);
|
||||
if dist > max_dist {
|
||||
max_dist = dist;
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
|
||||
if max_dist > epsilon {
|
||||
let mut left = simplify_points(&points[..=index], epsilon);
|
||||
let mut right = simplify_points(&points[index..], epsilon);
|
||||
|
||||
// Remove duplicate point at split
|
||||
left.pop();
|
||||
left.extend(right);
|
||||
left
|
||||
} else {
|
||||
vec![start, end]
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
let highway = tags.get("highway").map(|s| s.as_str());
|
||||
let place = tags.get("place").map(|s| s.as_str());
|
||||
let natural = tags.get("natural").map(|s| s.as_str());
|
||||
let railway = tags.get("railway").map(|s| s.as_str());
|
||||
let waterway = tags.get("waterway").map(|s| s.as_str());
|
||||
|
||||
match zoom {
|
||||
|
||||
2 => {
|
||||
// Space View: Continents and Countries
|
||||
matches!(place, Some("continent" | "country" | "sea" | "ocean")) ||
|
||||
matches!(natural, Some("water" | "bay" | "strait")) || // 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" | "sea" | "ocean")) ||
|
||||
matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland" | "bay" | "strait")) ||
|
||||
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" | "primary")) || // Added primary
|
||||
matches!(place, Some("city" | "sea" | "ocean")) ||
|
||||
matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland" | "bay" | "strait")) ||
|
||||
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.
|
||||
// Add Towns.
|
||||
// Limited nature.
|
||||
matches!(highway, Some("motorway" | "trunk" | "primary")) ||
|
||||
matches!(place, Some("city" | "town" | "sea" | "ocean" | "island" | "islet")) || // Islands!
|
||||
matches!(railway, Some("rail")) ||
|
||||
matches!(natural, Some("water" | "wood" | "scrub" | "bay" | "strait" | "wetland" | "heath" | "sand" | "beach" | "shingle" | "bare_rock")) || // Sand/Beaches!
|
||||
matches!(tags.get("landuse").map(|s| s.as_str()), Some("forest" | "grass" | "meadow" | "farmland" | "residential" | "basin" | "reservoir" | "allotments")) ||
|
||||
matches!(tags.get("leisure").map(|s| s.as_str()), Some("park" | "nature_reserve" | "garden")) || // Gardens
|
||||
matches!(waterway, Some("river" | "riverbank" | "canal")) // Added canal
|
||||
},
|
||||
12 => {
|
||||
matches!(highway, Some("motorway" | "trunk" | "primary" | "secondary" | "tertiary" | "residential" | "unclassified" | "pedestrian" | "service" | "track")) || // Added minor roads
|
||||
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" | "bay" | "strait" | "sand" | "beach" | "bare_rock")) ||
|
||||
matches!(waterway, Some("river" | "riverbank" | "stream" | "canal" | "drain" | "ditch")) // Added canal/drain/ditch
|
||||
},
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
use crate::domain::DbTask;
|
||||
use crate::repositories::{
|
||||
scylla_repository::ScyllaRepository,
|
||||
node_store::NodeStore,
|
||||
way_store::WayStore,
|
||||
railway_store::RailwayStore,
|
||||
};
|
||||
use crate::services::{
|
||||
filtering_service::FilteringService,
|
||||
tile_service::TileService,
|
||||
geometry_service::GeometryService,
|
||||
multipolygon_service::MultipolygonService,
|
||||
railway_service::RailwayService,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
@@ -381,70 +33,28 @@ async fn main() -> Result<()> {
|
||||
let uri = std::env::var("SCYLLA_URI").unwrap_or_else(|_| "127.0.0.1:9042".to_string());
|
||||
println!("Connecting to ScyllaDB at {}...", uri);
|
||||
|
||||
let session = loop {
|
||||
match SessionBuilder::new().known_node(&uri).build().await {
|
||||
Ok(session) => break session,
|
||||
Err(e) => {
|
||||
println!("Failed to connect to ScyllaDB: {}. Retrying in 5 seconds...", e);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
};
|
||||
let session = std::sync::Arc::new(session);
|
||||
let scylla_repo = Arc::new(ScyllaRepository::connect(&uri).await?);
|
||||
|
||||
// Ensure schema exists
|
||||
session.query("CREATE KEYSPACE IF NOT EXISTS map_data WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }", &[]).await?;
|
||||
|
||||
// Create tables
|
||||
session.query("CREATE TABLE IF NOT EXISTS map_data.nodes (zoom int, tile_x int, tile_y int, id bigint, lat double, lon double, tags map<text, text>, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
|
||||
session.query("CREATE TABLE IF NOT EXISTS map_data.ways (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
|
||||
session.query("CREATE TABLE IF NOT EXISTS map_data.buildings (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
|
||||
session.query("CREATE TABLE IF NOT EXISTS map_data.water (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
|
||||
session.query("CREATE TABLE IF NOT EXISTS map_data.landuse (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
|
||||
session.query("CREATE TABLE IF NOT EXISTS map_data.railways (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
|
||||
|
||||
// Prepare statements
|
||||
println!("Truncating tables...");
|
||||
session.query("TRUNCATE map_data.nodes", &[]).await?;
|
||||
session.query("TRUNCATE map_data.ways", &[]).await?;
|
||||
session.query("TRUNCATE map_data.buildings", &[]).await?;
|
||||
session.query("TRUNCATE map_data.water", &[]).await?;
|
||||
session.query("TRUNCATE map_data.landuse", &[]).await?;
|
||||
session.query("TRUNCATE map_data.railways", &[]).await?;
|
||||
println!("Tables truncated.");
|
||||
|
||||
println!("Preparing statements...");
|
||||
let insert_node = session.prepare("INSERT INTO map_data.nodes (zoom, tile_x, tile_y, id, lat, lon, tags) VALUES (?, ?, ?, ?, ?, ?, ?)").await?;
|
||||
let insert_ways = session.prepare("INSERT INTO map_data.ways (zoom, tile_x, tile_y, id, tags, points) VALUES (?, ?, ?, ?, ?, ?)").await?;
|
||||
let insert_buildings = session.prepare("INSERT INTO map_data.buildings (zoom, tile_x, tile_y, id, tags, points) VALUES (?, ?, ?, ?, ?, ?)").await?;
|
||||
let insert_water = session.prepare("INSERT INTO map_data.water (zoom, tile_x, tile_y, id, tags, points) VALUES (?, ?, ?, ?, ?, ?)").await?;
|
||||
let insert_landuse = session.prepare("INSERT INTO map_data.landuse (zoom, tile_x, tile_y, id, tags, points) VALUES (?, ?, ?, ?, ?, ?)").await?;
|
||||
let insert_railways = session.prepare("INSERT INTO map_data.railways (zoom, tile_x, tile_y, id, tags, points) VALUES (?, ?, ?, ?, ?, ?)").await?;
|
||||
println!("Statements prepared.");
|
||||
// Truncate tables
|
||||
scylla_repo.truncate_tables().await?;
|
||||
|
||||
let path = std::env::var("OSM_PBF_PATH")
|
||||
.or_else(|_| std::env::var("HOST_PBF_PATH"))
|
||||
.unwrap_or_else(|_| "europe-latest.osm.pbf".to_string());
|
||||
println!("Reading {}...", path);
|
||||
|
||||
let reader = ElementReader::from_path(path)?;
|
||||
|
||||
// Cache for node coordinates: ID -> (lat, lon)
|
||||
// Use flat file with mmap
|
||||
let cache_dir = std::env::var("CACHE_DIR").unwrap_or_else(|_| ".".to_string());
|
||||
let cache_path = std::path::Path::new(&cache_dir).join("node_cache.bin");
|
||||
println!("Using node cache at {:?}", cache_path);
|
||||
let mut node_store = NodeStore::new(cache_path.clone())?;
|
||||
|
||||
// Channel for backpressure
|
||||
// Producer (reader) -> Consumer (writer)
|
||||
enum DbTask {
|
||||
Node { zoom: i32, id: i64, lat: f64, lon: f64, tags: HashMap<String, String>, x: i32, y: i32 },
|
||||
Way { zoom: i32, table: &'static str, id: i64, tags: HashMap<String, String>, points: Vec<u8>, x: i32, y: i32 },
|
||||
}
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<DbTask>(10_000);
|
||||
|
||||
let session_clone = session.clone();
|
||||
let scylla_repo_clone = scylla_repo.clone();
|
||||
let consumer_handle = tokio::spawn(async move {
|
||||
let mut join_set = JoinSet::new();
|
||||
let mut inserted_count = 0;
|
||||
@@ -456,7 +66,7 @@ async fn main() -> Result<()> {
|
||||
println!("Starting consumer with max_concurrent={}", max_concurrent);
|
||||
|
||||
while let Some(task) = rx.recv().await {
|
||||
let session = session_clone.clone();
|
||||
let repo = scylla_repo_clone.clone();
|
||||
|
||||
// Backpressure: limit concurrent inserts
|
||||
while join_set.len() >= max_concurrent {
|
||||
@@ -465,29 +75,13 @@ async fn main() -> Result<()> {
|
||||
|
||||
match task {
|
||||
DbTask::Node { zoom, id, lat, lon, tags, x, y } => {
|
||||
let statement = insert_node.clone();
|
||||
join_set.spawn(async move {
|
||||
let _ = session.execute(
|
||||
&statement,
|
||||
(zoom, x, y, id, lat, lon, tags),
|
||||
).await;
|
||||
let _ = repo.insert_node(zoom, id, lat, lon, tags, x, y).await;
|
||||
});
|
||||
}
|
||||
DbTask::Way { zoom, table, id, tags, points, x, y } => {
|
||||
let statement = match table {
|
||||
"ways" => insert_ways.clone(),
|
||||
"buildings" => insert_buildings.clone(),
|
||||
"water" => insert_water.clone(),
|
||||
"landuse" => insert_landuse.clone(),
|
||||
"railways" => insert_railways.clone(),
|
||||
_ => panic!("Unknown table: {}", table),
|
||||
};
|
||||
|
||||
join_set.spawn(async move {
|
||||
let _ = session.execute(
|
||||
&statement,
|
||||
(zoom, x, y, id, tags, points),
|
||||
).await;
|
||||
let _ = repo.insert_way(table, zoom, id, tags, points, x, y).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -499,7 +93,7 @@ async fn main() -> Result<()> {
|
||||
println!("Consumer finished. Total inserted tasks: {}", inserted_count);
|
||||
});
|
||||
|
||||
// Run the PBF reader in a blocking task to allow blocking_send
|
||||
// Run the PBF reader in a blocking task
|
||||
let tx_clone = tx.clone();
|
||||
let reader_handle = tokio::task::spawn_blocking(move || -> Result<(usize, usize, usize)> {
|
||||
let tx = tx_clone;
|
||||
@@ -509,16 +103,9 @@ async fn main() -> Result<()> {
|
||||
let mut ways_pending = false;
|
||||
let mut relations_pending = false;
|
||||
|
||||
// Store way geometries for multipolygon assembly
|
||||
let mut way_store = WayStore::new();
|
||||
|
||||
// Store railway ways for deferred insertion (after relation processing for colors)
|
||||
let mut railway_store = RailwayStore::new();
|
||||
|
||||
// We process sequentially: Nodes first, then Ways, then Relations.
|
||||
// osmpbf yields nodes then ways then relations.
|
||||
// We need to detect when we switch from nodes to ways to prepare the store.
|
||||
|
||||
reader.for_each(|element| {
|
||||
match element {
|
||||
Element::Node(node) => {
|
||||
@@ -531,9 +118,9 @@ async fn main() -> Result<()> {
|
||||
let lon = node.lon();
|
||||
let tags: HashMap<String, String> = node.tags().map(|(k, v)| (k.to_string(), v.to_string())).collect();
|
||||
|
||||
for &zoom in &ZOOM_LEVELS {
|
||||
if should_include(&tags, zoom) {
|
||||
let (x, y) = lat_lon_to_tile(lat, lon, zoom);
|
||||
for &zoom in &FilteringService::ZOOM_LEVELS {
|
||||
if FilteringService::should_include(&tags, zoom) {
|
||||
let (x, y) = TileService::lat_lon_to_tile(lat, lon, zoom);
|
||||
let task = DbTask::Node { zoom: zoom as i32, id, lat, lon, tags: tags.clone(), x, y };
|
||||
let _ = tx.blocking_send(task);
|
||||
}
|
||||
@@ -550,9 +137,9 @@ async fn main() -> Result<()> {
|
||||
let lon = node.lon();
|
||||
let tags: HashMap<String, String> = node.tags().map(|(k, v)| (k.to_string(), v.to_string())).collect();
|
||||
|
||||
for &zoom in &ZOOM_LEVELS {
|
||||
if should_include(&tags, zoom) {
|
||||
let (x, y) = lat_lon_to_tile(lat, lon, zoom);
|
||||
for &zoom in &FilteringService::ZOOM_LEVELS {
|
||||
if FilteringService::should_include(&tags, zoom) {
|
||||
let (x, y) = TileService::lat_lon_to_tile(lat, lon, zoom);
|
||||
let task = DbTask::Node { zoom: zoom as i32, id, lat, lon, tags: tags.clone(), x, y };
|
||||
let _ = tx.blocking_send(task);
|
||||
}
|
||||
@@ -561,7 +148,6 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
Element::Way(way) => {
|
||||
if !ways_pending {
|
||||
// First way encountered. Prepare store for reading.
|
||||
println!("Switching to Way processing. Flushing node cache...");
|
||||
if let Err(e) = node_store.prepare_for_reading() {
|
||||
eprintln!("Failed to prepare node store: {}", e);
|
||||
@@ -572,18 +158,14 @@ async fn main() -> Result<()> {
|
||||
|
||||
way_count += 1;
|
||||
|
||||
// Store ALL way node refs for potential multipolygon use
|
||||
let node_refs: Vec<i64> = way.refs().collect();
|
||||
way_store.insert(way.id(), node_refs.clone());
|
||||
|
||||
let tags: HashMap<String, String> = way.tags().map(|(k, v)| (k.to_string(), v.to_string())).collect();
|
||||
|
||||
// Filter for highways/roads OR buildings OR landuse OR water OR railways
|
||||
// Filter for highways/roads OR buildings OR landuse OR water OR railways
|
||||
let is_highway = tags.contains_key("highway");
|
||||
let is_building = tags.contains_key("building");
|
||||
|
||||
// Split Water into Area (Polygon) and Line (Way)
|
||||
let is_water_area = tags.get("natural").map(|v| v == "water" || v == "wetland" || v == "bay" || v == "strait").unwrap_or(false) ||
|
||||
tags.get("place").map(|v| v == "sea" || v == "ocean").unwrap_or(false) ||
|
||||
tags.get("waterway").map(|v| v == "riverbank" || v == "dock").unwrap_or(false) ||
|
||||
@@ -599,7 +181,6 @@ async fn main() -> Result<()> {
|
||||
if is_highway || is_building || is_water_area || is_water_line || is_landuse || is_railway {
|
||||
let mut points = Vec::new();
|
||||
|
||||
// Resolve nodes from store
|
||||
for node_id in way.refs() {
|
||||
if let Some((lat, lon)) = node_store.get(node_id) {
|
||||
points.push((lat, lon));
|
||||
@@ -608,94 +189,75 @@ async fn main() -> Result<()> {
|
||||
|
||||
if points.len() >= 2 {
|
||||
let id = way.id();
|
||||
|
||||
|
||||
// Insert into the tile of the first point
|
||||
let (first_lat, first_lon) = points[0];
|
||||
let is_closed = points.first() == points.last();
|
||||
|
||||
// Detect if we should treat this as an area
|
||||
let mut treat_as_water_area = is_water_area && is_closed;
|
||||
let mut treat_as_landuse = is_landuse && is_closed;
|
||||
let mut treat_as_building = is_building && is_closed;
|
||||
|
||||
// Fallback: If water is open (e.g. riverbank segment), treat as line
|
||||
let mut treat_as_water_line = is_water_line || (is_water_area && !is_closed);
|
||||
|
||||
// If landuse/building is open, we skip it to avoid artifacts (giant triangles)
|
||||
if (is_landuse || is_building) && !is_closed {
|
||||
return;
|
||||
}
|
||||
|
||||
for &zoom in &ZOOM_LEVELS {
|
||||
if !should_include(&tags, zoom) { continue; }
|
||||
for &zoom in &FilteringService::ZOOM_LEVELS {
|
||||
if !FilteringService::should_include(&tags, zoom) { continue; }
|
||||
|
||||
// Apply simplification based on zoom level
|
||||
let base_epsilon = match zoom {
|
||||
2 => 0.01, // Was 0.0001 (~11m) -> Now ~1km
|
||||
4 => 0.002, // Was 0.00005 (~5m) -> Now ~200m
|
||||
6 => 0.0005, // Was 0.00002 (~2m) -> Now ~50m
|
||||
9 => 0.0001, // Was 0.00001 (~1m) -> Now ~10m
|
||||
2 => 0.01,
|
||||
4 => 0.002,
|
||||
6 => 0.0005,
|
||||
9 => 0.0001,
|
||||
12 => 0.000005,
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
let epsilon = if treat_as_water_area || treat_as_landuse || is_highway || treat_as_water_line {
|
||||
if zoom <= 4 && treat_as_landuse {
|
||||
0.0 // Disable simplification for landuse at low zoom
|
||||
0.0
|
||||
} else if treat_as_water_area || treat_as_landuse {
|
||||
// User requested "little more detail"
|
||||
// Almost disable simplification for organic shapes
|
||||
if zoom >= 9 {
|
||||
0.0 // No simplification at zoom 9+
|
||||
if zoom >= 9 { 0.0 } else { base_epsilon * 0.01 }
|
||||
} else {
|
||||
base_epsilon * 0.01 // 1% of standard simplification - high detail
|
||||
}
|
||||
} else {
|
||||
base_epsilon * 0.5 // Highways/Railways can handle some simplification
|
||||
base_epsilon * 0.5
|
||||
}
|
||||
} else {
|
||||
base_epsilon
|
||||
};
|
||||
|
||||
let simplified_points = if epsilon > 0.0 {
|
||||
simplify_points(&points, epsilon)
|
||||
GeometryService::simplify_points(&points, epsilon)
|
||||
} else {
|
||||
points.clone()
|
||||
};
|
||||
|
||||
// Serialize points
|
||||
let mut final_points = simplified_points.clone();
|
||||
|
||||
// For highways and railways, we DON'T triangulate - they're line data
|
||||
// Create the highway/railway blob BEFORE triangulation
|
||||
// Create blob for line features (highways/railways/water lines)
|
||||
let mut line_blob = Vec::with_capacity(simplified_points.len() * 8);
|
||||
for (lat, lon) in &simplified_points {
|
||||
line_blob.extend_from_slice(&(*lat as f32).to_le_bytes());
|
||||
line_blob.extend_from_slice(&(*lon as f32).to_le_bytes());
|
||||
}
|
||||
|
||||
// Triangulate for polygon types
|
||||
if treat_as_building || treat_as_water_area || treat_as_landuse {
|
||||
// Already checked closure above
|
||||
final_points = triangulate_polygon(&final_points);
|
||||
final_points = GeometryService::triangulate_polygon(&final_points);
|
||||
}
|
||||
|
||||
if final_points.len() < 3 && (treat_as_building || treat_as_water_area || treat_as_landuse) { continue; }
|
||||
if simplified_points.len() < 2 && (is_highway || is_railway || treat_as_water_line) { continue; }
|
||||
|
||||
let (first_lat, first_lon) = simplified_points[0];
|
||||
let (x, y) = lat_lon_to_tile(first_lat, first_lon, zoom);
|
||||
let (x, y) = TileService::lat_lon_to_tile(first_lat, first_lon, zoom);
|
||||
let zoom_i32 = zoom as i32;
|
||||
|
||||
// Create polygon blob from triangulated points
|
||||
let mut polygon_blob = Vec::with_capacity(final_points.len() * 8);
|
||||
for (lat, lon) in &final_points {
|
||||
polygon_blob.extend_from_slice(&(*lat as f32).to_le_bytes());
|
||||
polygon_blob.extend_from_slice(&(*lon as f32).to_le_bytes());
|
||||
}
|
||||
|
||||
// Use line_blob for highways/railways/water_lines, polygon_blob for others
|
||||
if is_highway || treat_as_water_line {
|
||||
let task = DbTask::Way { zoom: zoom_i32, table: "ways", id, tags: tags.clone(), points: line_blob.clone(), x, y };
|
||||
let _ = tx.blocking_send(task);
|
||||
@@ -717,7 +279,6 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
if is_railway {
|
||||
// Store for deferred insertion - colors will be applied from relations
|
||||
let (first_lat, first_lon) = simplified_points[0];
|
||||
railway_store.insert_way(id, tags.clone(), line_blob.clone(), first_lat, first_lon);
|
||||
}
|
||||
@@ -734,40 +295,18 @@ async fn main() -> Result<()> {
|
||||
relation_count += 1;
|
||||
let tags: HashMap<String, String> = rel.tags().map(|(k, v)| (k.to_string(), v.to_string())).collect();
|
||||
|
||||
// Process route relations for transit colors
|
||||
if tags.get("type").map(|t| t == "route").unwrap_or(false) {
|
||||
let route_type = tags.get("route").map(|s| s.as_str());
|
||||
let is_transit = match route_type {
|
||||
Some("subway") | Some("tram") | Some("light_rail") => true,
|
||||
Some("train") => {
|
||||
// Only include S-Bahn and suburban trains
|
||||
tags.get("network").map(|n| n.contains("S-Bahn")).unwrap_or(false) ||
|
||||
tags.get("service").map(|s| s == "suburban").unwrap_or(false) ||
|
||||
tags.get("ref").map(|r| r.starts_with("S")).unwrap_or(false)
|
||||
},
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_transit {
|
||||
// Extract colour tag
|
||||
if let Some(colour) = tags.get("colour").or(tags.get("color")) {
|
||||
// Map colour to all member ways
|
||||
if let Some(colour) = RailwayService::get_route_color(&tags) {
|
||||
for member in rel.members() {
|
||||
if let osmpbf::RelMemberType::Way = member.member_type {
|
||||
railway_store.set_color(member.member_id, colour.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process multipolygon relations (existing code)
|
||||
if tags.get("type").map(|t| t == "multipolygon").unwrap_or(false) {
|
||||
// Check if it's a water or landuse multipolygon
|
||||
// IMPORTANT: Rivers like the Isar are tagged waterway=river on the relation itself!
|
||||
let is_water = tags.get("natural").map(|v| v == "water" || v == "wetland" || v == "bay").unwrap_or(false) ||
|
||||
tags.get("waterway").map(|v| v == "riverbank" || v == "river" || v == "canal").unwrap_or(false) ||
|
||||
tags.get("water").is_some() || // Also check water=* tag
|
||||
tags.get("water").is_some() ||
|
||||
tags.get("landuse").map(|v| v == "basin" || v == "reservoir").unwrap_or(false);
|
||||
|
||||
let is_landuse = tags.get("landuse").is_some() ||
|
||||
@@ -775,7 +314,6 @@ async fn main() -> Result<()> {
|
||||
tags.get("natural").map(|v| v == "wood" || v == "scrub" || v == "heath").unwrap_or(false);
|
||||
|
||||
if is_water || is_landuse {
|
||||
// Collect outer way members
|
||||
let mut outer_ways: Vec<i64> = Vec::new();
|
||||
for member in rel.members() {
|
||||
if member.role().unwrap_or("") == "outer" {
|
||||
@@ -786,11 +324,9 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
if !outer_ways.is_empty() {
|
||||
// Assemble ALL rings from the outer ways (rivers have multiple rings!)
|
||||
let rings = assemble_rings(&outer_ways, &way_store);
|
||||
let rings = MultipolygonService::assemble_rings(&outer_ways, &way_store);
|
||||
|
||||
for ring_node_ids in rings {
|
||||
// Resolve node coordinates
|
||||
let mut points: Vec<(f64, f64)> = Vec::new();
|
||||
for node_id in &ring_node_ids {
|
||||
if let Some((lat, lon)) = node_store.get(*node_id) {
|
||||
@@ -802,17 +338,16 @@ async fn main() -> Result<()> {
|
||||
let id = rel.id();
|
||||
let (first_lat, first_lon) = points[0];
|
||||
|
||||
for &zoom in &ZOOM_LEVELS {
|
||||
if !should_include(&tags, zoom) { continue; }
|
||||
for &zoom in &FilteringService::ZOOM_LEVELS {
|
||||
if !FilteringService::should_include(&tags, zoom) { continue; }
|
||||
|
||||
// No simplification for multipolygons
|
||||
let final_points = triangulate_polygon(&points);
|
||||
let final_points = GeometryService::triangulate_polygon(&points);
|
||||
if final_points.len() < 3 { continue; }
|
||||
|
||||
let (x, y) = lat_lon_to_tile(first_lat, first_lon, zoom);
|
||||
let (x, y) = TileService::lat_lon_to_tile(first_lat, first_lon, zoom);
|
||||
let zoom_i32 = zoom as i32;
|
||||
|
||||
// Create polygon blob
|
||||
let mut polygon_blob = Vec::with_capacity(final_points.len() * 8);
|
||||
for (lat, lon) in &final_points {
|
||||
polygon_blob.extend_from_slice(&(*lat as f32).to_le_bytes());
|
||||
@@ -837,27 +372,26 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
})?;
|
||||
|
||||
// Deferred railway insertion - now with colors from route relations
|
||||
println!("Inserting {} railway ways with colors...", railway_store.ways.len());
|
||||
for (way_id, railway) in &railway_store.ways {
|
||||
let mut tags = railway.tags.clone();
|
||||
let (railways, colors) = railway_store.into_data();
|
||||
println!("Inserting {} railway ways with colors...", railways.len());
|
||||
for (id, railway) in railways {
|
||||
let mut tags = railway.tags;
|
||||
|
||||
// Apply color from route relation if available
|
||||
if let Some(colour) = railway_store.get_color(*way_id) {
|
||||
if let Some(colour) = colors.get(&id) {
|
||||
tags.insert("colour".to_string(), colour.clone());
|
||||
}
|
||||
|
||||
// Insert for all applicable zoom levels
|
||||
for &zoom in &ZOOM_LEVELS {
|
||||
if !should_include(&tags, zoom) { continue; }
|
||||
for &zoom in &FilteringService::ZOOM_LEVELS {
|
||||
if !FilteringService::should_include(&tags, zoom) { continue; }
|
||||
|
||||
let (x, y) = lat_lon_to_tile(railway.first_lat, railway.first_lon, zoom);
|
||||
let (x, y) = TileService::lat_lon_to_tile(railway.first_lat, railway.first_lon, zoom);
|
||||
let zoom_i32 = zoom as i32;
|
||||
|
||||
let task = DbTask::Way {
|
||||
zoom: zoom_i32,
|
||||
table: "railways",
|
||||
id: railway.id,
|
||||
id,
|
||||
tags: tags.clone(),
|
||||
points: railway.points.clone(),
|
||||
x,
|
||||
@@ -890,12 +424,9 @@ async fn main() -> Result<()> {
|
||||
for table in &tables {
|
||||
println!("Compacting map_data.{}...", table);
|
||||
let query = format!("ALTER TABLE map_data.{} WITH gc_grace_seconds = 0", table);
|
||||
let _ = session.query(query, &[]).await;
|
||||
let _ = scylla_repo.get_session().query(query, &[]).await;
|
||||
}
|
||||
|
||||
// Force a flush to ensure all data is on disk before compaction
|
||||
// Note: In ScyllaDB, compaction happens automatically, but we set gc_grace_seconds=0
|
||||
// to allow immediate tombstone cleanup. For manual compaction, use nodetool externally.
|
||||
println!("Compaction settings updated. Tombstones will be cleaned during next compaction cycle.");
|
||||
println!("For immediate compaction, run: docker exec scylla nodetool compact map_data");
|
||||
|
||||
@@ -903,11 +434,3 @@ async fn main() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn lat_lon_to_tile(lat: f64, lon: f64, zoom: u32) -> (i32, i32) {
|
||||
let n = 2.0f64.powi(zoom as i32);
|
||||
let x = (lon + 180.0) / 360.0 * n;
|
||||
let lat_rad = lat.to_radians();
|
||||
let y = (1.0 - (lat_rad.tan() + (1.0 / lat_rad.cos())).ln() / std::f64::consts::PI) / 2.0 * n;
|
||||
(x as i32, y as i32)
|
||||
}
|
||||
|
||||
1
importer/src/parsers/mod.rs
Normal file
1
importer/src/parsers/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
// Parsing logic is currently in main.rs loop
|
||||
5
importer/src/repositories/mod.rs
Normal file
5
importer/src/repositories/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod way_store;
|
||||
pub mod railway_store;
|
||||
pub mod node_store;
|
||||
pub mod scylla_repository;
|
||||
|
||||
77
importer/src/repositories/node_store.rs
Normal file
77
importer/src/repositories/node_store.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{BufWriter, Write, Seek, SeekFrom};
|
||||
use std::path::{Path, PathBuf};
|
||||
use memmap2::Mmap;
|
||||
use anyhow::Result;
|
||||
|
||||
pub struct NodeStore {
|
||||
writer: Option<BufWriter<File>>,
|
||||
mmap: Option<Mmap>,
|
||||
path: PathBuf,
|
||||
last_id: i64,
|
||||
}
|
||||
|
||||
impl NodeStore {
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&path)?;
|
||||
|
||||
let writer = BufWriter::with_capacity(10 * 1024 * 1024, file); // 10MB buffer
|
||||
|
||||
Ok(Self {
|
||||
writer: Some(writer),
|
||||
mmap: None,
|
||||
path,
|
||||
last_id: -1,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, id: i64, lat: f64, lon: f64) -> Result<()> {
|
||||
if let Some(writer) = &mut self.writer {
|
||||
if id > self.last_id + 1 {
|
||||
let gap = id - self.last_id - 1;
|
||||
writer.seek(SeekFrom::Current(gap * 8))?;
|
||||
} else if id <= self.last_id {
|
||||
writer.seek(SeekFrom::Start(id as u64 * 8))?;
|
||||
}
|
||||
|
||||
let lat_i32 = (lat * 1e7) as i32;
|
||||
let lon_i32 = (lon * 1e7) as i32;
|
||||
writer.write_all(&lat_i32.to_le_bytes())?;
|
||||
writer.write_all(&lon_i32.to_le_bytes())?;
|
||||
|
||||
self.last_id = id;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn prepare_for_reading(&mut self) -> Result<()> {
|
||||
self.writer = None; // Flush and close writer
|
||||
|
||||
let file = File::open(&self.path)?;
|
||||
let mmap = unsafe { Mmap::map(&file)? };
|
||||
self.mmap = Some(mmap);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get(&self, id: i64) -> Option<(f64, f64)> {
|
||||
if let Some(mmap) = &self.mmap {
|
||||
let offset = id as usize * 8;
|
||||
if offset + 8 <= mmap.len() {
|
||||
let chunk = &mmap[offset..offset+8];
|
||||
let lat_i32 = i32::from_le_bytes(chunk[0..4].try_into().unwrap());
|
||||
let lon_i32 = i32::from_le_bytes(chunk[4..8].try_into().unwrap());
|
||||
|
||||
if lat_i32 == 0 && lon_i32 == 0 { return None; }
|
||||
|
||||
return Some((lat_i32 as f64 / 1e7, lon_i32 as f64 / 1e7));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
41
importer/src/repositories/railway_store.rs
Normal file
41
importer/src/repositories/railway_store.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Store railway ways for deferred insertion (after relation processing for colors)
|
||||
pub struct RailwayWay {
|
||||
pub id: i64,
|
||||
pub tags: HashMap<String, String>,
|
||||
pub points: Vec<u8>, // Serialized line blob
|
||||
pub first_lat: f64,
|
||||
pub first_lon: f64,
|
||||
}
|
||||
|
||||
pub struct RailwayStore {
|
||||
ways: HashMap<i64, RailwayWay>, // way_id -> railway data
|
||||
way_colors: HashMap<i64, String>, // way_id -> colour from route relation
|
||||
}
|
||||
|
||||
impl RailwayStore {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ways: HashMap::new(),
|
||||
way_colors: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_way(&mut self, id: i64, tags: HashMap<String, String>, points: Vec<u8>, first_lat: f64, first_lon: f64) {
|
||||
self.ways.insert(id, RailwayWay { id, tags, points, first_lat, first_lon });
|
||||
}
|
||||
|
||||
pub fn set_color(&mut self, way_id: i64, color: String) {
|
||||
// Only set if not already set (first route relation wins)
|
||||
self.way_colors.entry(way_id).or_insert(color);
|
||||
}
|
||||
|
||||
pub fn get_color(&self, way_id: i64) -> Option<&String> {
|
||||
self.way_colors.get(&way_id)
|
||||
}
|
||||
|
||||
pub fn into_data(self) -> (HashMap<i64, RailwayWay>, HashMap<i64, String>) {
|
||||
(self.ways, self.way_colors)
|
||||
}
|
||||
}
|
||||
101
importer/src/repositories/scylla_repository.rs
Normal file
101
importer/src/repositories/scylla_repository.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use scylla::{Session, SessionBuilder};
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
use tokio::task::JoinSet;
|
||||
use anyhow::Result;
|
||||
|
||||
pub struct ScyllaRepository {
|
||||
session: Arc<Session>,
|
||||
insert_node: scylla::statement::prepared_statement::PreparedStatement,
|
||||
insert_ways: scylla::statement::prepared_statement::PreparedStatement,
|
||||
insert_buildings: scylla::statement::prepared_statement::PreparedStatement,
|
||||
insert_water: scylla::statement::prepared_statement::PreparedStatement,
|
||||
insert_landuse: scylla::statement::prepared_statement::PreparedStatement,
|
||||
insert_railways: scylla::statement::prepared_statement::PreparedStatement,
|
||||
}
|
||||
|
||||
impl ScyllaRepository {
|
||||
pub async fn connect(uri: &str) -> Result<Self> {
|
||||
let session = loop {
|
||||
match SessionBuilder::new().known_node(uri).build().await {
|
||||
Ok(session) => break session,
|
||||
Err(e) => {
|
||||
println!("Failed to connect to ScyllaDB: {}. Retrying in 5 seconds...", e);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let session = Arc::new(session);
|
||||
|
||||
// Ensure schema exists
|
||||
session.query("CREATE KEYSPACE IF NOT EXISTS map_data WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }", &[]).await?;
|
||||
|
||||
// Create tables
|
||||
session.query("CREATE TABLE IF NOT EXISTS map_data.nodes (zoom int, tile_x int, tile_y int, id bigint, lat double, lon double, tags map<text, text>, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
|
||||
session.query("CREATE TABLE IF NOT EXISTS map_data.ways (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
|
||||
session.query("CREATE TABLE IF NOT EXISTS map_data.buildings (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
|
||||
session.query("CREATE TABLE IF NOT EXISTS map_data.water (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
|
||||
session.query("CREATE TABLE IF NOT EXISTS map_data.landuse (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
|
||||
session.query("CREATE TABLE IF NOT EXISTS map_data.railways (zoom int, tile_x int, tile_y int, id bigint, tags map<text, text>, points blob, PRIMARY KEY ((zoom, tile_x, tile_y), id))", &[]).await?;
|
||||
|
||||
// Prepare statements
|
||||
let insert_node = session.prepare("INSERT INTO map_data.nodes (zoom, tile_x, tile_y, id, lat, lon, tags) VALUES (?, ?, ?, ?, ?, ?, ?)").await?;
|
||||
let insert_ways = session.prepare("INSERT INTO map_data.ways (zoom, tile_x, tile_y, id, tags, points) VALUES (?, ?, ?, ?, ?, ?)").await?;
|
||||
let insert_buildings = session.prepare("INSERT INTO map_data.buildings (zoom, tile_x, tile_y, id, tags, points) VALUES (?, ?, ?, ?, ?, ?)").await?;
|
||||
let insert_water = session.prepare("INSERT INTO map_data.water (zoom, tile_x, tile_y, id, tags, points) VALUES (?, ?, ?, ?, ?, ?)").await?;
|
||||
let insert_landuse = session.prepare("INSERT INTO map_data.landuse (zoom, tile_x, tile_y, id, tags, points) VALUES (?, ?, ?, ?, ?, ?)").await?;
|
||||
let insert_railways = session.prepare("INSERT INTO map_data.railways (zoom, tile_x, tile_y, id, tags, points) VALUES (?, ?, ?, ?, ?, ?)").await?;
|
||||
|
||||
Ok(Self {
|
||||
session,
|
||||
insert_node,
|
||||
insert_ways,
|
||||
insert_buildings,
|
||||
insert_water,
|
||||
insert_landuse,
|
||||
insert_railways,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn truncate_tables(&self) -> Result<()> {
|
||||
println!("Truncating tables...");
|
||||
self.session.query("TRUNCATE map_data.nodes", &[]).await?;
|
||||
self.session.query("TRUNCATE map_data.ways", &[]).await?;
|
||||
self.session.query("TRUNCATE map_data.buildings", &[]).await?;
|
||||
self.session.query("TRUNCATE map_data.water", &[]).await?;
|
||||
self.session.query("TRUNCATE map_data.landuse", &[]).await?;
|
||||
self.session.query("TRUNCATE map_data.railways", &[]).await?;
|
||||
println!("Tables truncated.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert_node(&self, zoom: i32, id: i64, lat: f64, lon: f64, tags: HashMap<String, String>, x: i32, y: i32) -> Result<()> {
|
||||
self.session.execute(
|
||||
&self.insert_node,
|
||||
(zoom, x, y, id, lat, lon, tags),
|
||||
).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert_way(&self, table: &str, zoom: i32, id: i64, tags: HashMap<String, String>, points: Vec<u8>, x: i32, y: i32) -> Result<()> {
|
||||
let statement = match table {
|
||||
"ways" => &self.insert_ways,
|
||||
"buildings" => &self.insert_buildings,
|
||||
"water" => &self.insert_water,
|
||||
"landuse" => &self.insert_landuse,
|
||||
"railways" => &self.insert_railways,
|
||||
_ => panic!("Unknown table: {}", table),
|
||||
};
|
||||
|
||||
self.session.execute(
|
||||
statement,
|
||||
(zoom, x, y, id, tags, points),
|
||||
).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_session(&self) -> Arc<Session> {
|
||||
self.session.clone()
|
||||
}
|
||||
}
|
||||
20
importer/src/repositories/way_store.rs
Normal file
20
importer/src/repositories/way_store.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Store way geometries for multipolygon assembly
|
||||
pub struct WayStore {
|
||||
ways: HashMap<i64, Vec<i64>>, // way_id -> node_id list
|
||||
}
|
||||
|
||||
impl WayStore {
|
||||
pub fn new() -> Self {
|
||||
Self { ways: HashMap::new() }
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, way_id: i64, node_refs: Vec<i64>) {
|
||||
self.ways.insert(way_id, node_refs);
|
||||
}
|
||||
|
||||
pub fn get(&self, way_id: i64) -> Option<&Vec<i64>> {
|
||||
self.ways.get(&way_id)
|
||||
}
|
||||
}
|
||||
98
importer/src/services/filtering_service.rs
Normal file
98
importer/src/services/filtering_service.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct FilteringService;
|
||||
|
||||
impl FilteringService {
|
||||
pub const ZOOM_LEVELS: [u32; 6] = [2, 4, 6, 9, 12, 14];
|
||||
|
||||
pub fn should_include(tags: &HashMap<String, String>, zoom: u32) -> bool {
|
||||
if zoom >= 14 { return true; }
|
||||
|
||||
let highway = tags.get("highway").map(|s| s.as_str());
|
||||
let place = tags.get("place").map(|s| s.as_str());
|
||||
let natural = tags.get("natural").map(|s| s.as_str());
|
||||
let railway = tags.get("railway").map(|s| s.as_str());
|
||||
let waterway = tags.get("waterway").map(|s| s.as_str());
|
||||
|
||||
match zoom {
|
||||
2 => {
|
||||
// Space View: Continents and Countries
|
||||
matches!(place, Some("continent" | "country" | "sea" | "ocean")) ||
|
||||
matches!(natural, Some("water" | "bay" | "strait")) || // 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" | "sea" | "ocean")) ||
|
||||
matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland" | "bay" | "strait")) ||
|
||||
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" | "primary")) || // Added primary
|
||||
matches!(place, Some("city" | "sea" | "ocean")) ||
|
||||
matches!(railway, Some("rail")) || // Major rail lines
|
||||
matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland" | "bay" | "strait")) ||
|
||||
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.
|
||||
// Add Towns.
|
||||
// Limited nature.
|
||||
matches!(highway, Some("motorway" | "trunk" | "primary")) ||
|
||||
matches!(place, Some("city" | "town" | "sea" | "ocean" | "island" | "islet")) || // Islands!
|
||||
matches!(railway, Some("rail" | "subway" | "light_rail" | "narrow_gauge")) || // Added urban transit
|
||||
matches!(natural, Some("water" | "wood" | "scrub" | "bay" | "strait" | "wetland" | "heath" | "sand" | "beach" | "shingle" | "bare_rock")) || // Sand/Beaches!
|
||||
matches!(tags.get("landuse").map(|s| s.as_str()), Some("forest" | "grass" | "meadow" | "farmland" | "residential" | "basin" | "reservoir" | "allotments")) ||
|
||||
matches!(tags.get("leisure").map(|s| s.as_str()), Some("park" | "nature_reserve" | "garden")) || // Gardens
|
||||
matches!(waterway, Some("river" | "riverbank" | "canal")) // Added canal
|
||||
},
|
||||
12 => {
|
||||
matches!(highway, Some("motorway" | "trunk" | "primary" | "secondary" | "tertiary" | "residential" | "unclassified" | "pedestrian" | "service" | "track")) || // Added minor roads
|
||||
matches!(place, Some("city" | "town" | "village")) ||
|
||||
matches!(railway, Some("rail" | "subway" | "light_rail" | "narrow_gauge" | "tram")) || // Added tram
|
||||
tags.contains_key("building") ||
|
||||
tags.contains_key("landuse") ||
|
||||
tags.contains_key("leisure") ||
|
||||
matches!(natural, Some("water" | "wood" | "scrub" | "wetland" | "heath" | "bay" | "strait" | "sand" | "beach" | "bare_rock")) ||
|
||||
matches!(waterway, Some("river" | "riverbank" | "stream" | "canal" | "drain" | "ditch")) // Added canal/drain/ditch
|
||||
},
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_should_include_motorway_zoom_2() {
|
||||
let mut tags = HashMap::new();
|
||||
tags.insert("highway".to_string(), "motorway".to_string());
|
||||
assert!(FilteringService::should_include(&tags, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_not_include_path_zoom_2() {
|
||||
let mut tags = HashMap::new();
|
||||
tags.insert("highway".to_string(), "path".to_string());
|
||||
assert!(!FilteringService::should_include(&tags, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_include_park_zoom_12() {
|
||||
let mut tags = HashMap::new();
|
||||
tags.insert("leisure".to_string(), "park".to_string());
|
||||
assert!(FilteringService::should_include(&tags, 12));
|
||||
}
|
||||
}
|
||||
91
importer/src/services/geometry_service.rs
Normal file
91
importer/src/services/geometry_service.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Point {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
impl Point {
|
||||
pub fn new(x: f64, y: f64) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GeometryService;
|
||||
|
||||
impl GeometryService {
|
||||
// Ramer-Douglas-Peucker simplification
|
||||
pub fn simplify_points(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> {
|
||||
if points.len() < 3 {
|
||||
return points.to_vec();
|
||||
}
|
||||
|
||||
let start = points[0];
|
||||
let end = points[points.len() - 1];
|
||||
|
||||
let mut max_dist = 0.0;
|
||||
let mut index = 0;
|
||||
|
||||
for i in 1..points.len() - 1 {
|
||||
let dist = Self::perpendicular_distance(points[i], start, end);
|
||||
if dist > max_dist {
|
||||
max_dist = dist;
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
|
||||
if max_dist > epsilon {
|
||||
let mut left = Self::simplify_points(&points[..=index], epsilon);
|
||||
let mut right = Self::simplify_points(&points[index..], epsilon);
|
||||
|
||||
// Remove duplicate point at split
|
||||
left.pop();
|
||||
left.extend(right);
|
||||
left
|
||||
} else {
|
||||
vec![start, end]
|
||||
}
|
||||
}
|
||||
|
||||
fn perpendicular_distance(p: (f64, f64), line_start: (f64, f64), line_end: (f64, f64)) -> f64 {
|
||||
let (x, y) = p;
|
||||
let (x1, y1) = line_start;
|
||||
let (x2, y2) = line_end;
|
||||
|
||||
let dx = x2 - x1;
|
||||
let dy = y2 - y1;
|
||||
|
||||
if dx == 0.0 && dy == 0.0 {
|
||||
return ((x - x1).powi(2) + (y - y1).powi(2)).sqrt();
|
||||
}
|
||||
|
||||
let num = (dy * x - dx * y + x2 * y1 - y2 * x1).abs();
|
||||
let den = (dx.powi(2) + dy.powi(2)).sqrt();
|
||||
|
||||
num / den
|
||||
}
|
||||
|
||||
pub 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
|
||||
// Complex multipolygons with holes are handled via relation processing which naturally closes rings
|
||||
// But for triangulation, earcutr handles holes if we provide hole indices.
|
||||
// For basic way polygons, we assume no holes.
|
||||
|
||||
let triangles = earcutr::earcut(&flat_points, &[], 2).unwrap_or_default();
|
||||
|
||||
let mut result = Vec::with_capacity(triangles.len());
|
||||
for i in triangles {
|
||||
let lat = flat_points[i * 2];
|
||||
let lon = flat_points[i * 2 + 1];
|
||||
result.push((lat, lon));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
6
importer/src/services/mod.rs
Normal file
6
importer/src/services/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod geometry_service;
|
||||
pub mod multipolygon_service;
|
||||
pub mod filtering_service;
|
||||
pub mod tile_service;
|
||||
pub mod railway_service;
|
||||
|
||||
105
importer/src/services/multipolygon_service.rs
Normal file
105
importer/src/services/multipolygon_service.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use crate::repositories::way_store::WayStore;
|
||||
|
||||
pub struct MultipolygonService;
|
||||
|
||||
impl MultipolygonService {
|
||||
// Assemble ways into MULTIPLE rings (connect end-to-end)
|
||||
// Rivers like the Isar have multiple separate channels/rings
|
||||
pub fn assemble_rings(way_ids: &[i64], way_store: &WayStore) -> Vec<Vec<i64>> {
|
||||
if way_ids.is_empty() { return Vec::new(); }
|
||||
|
||||
// Get all way geometries
|
||||
let mut segments: Vec<Vec<i64>> = Vec::new();
|
||||
for &way_id in way_ids {
|
||||
if let Some(nodes) = way_store.get(way_id) {
|
||||
if nodes.len() >= 2 {
|
||||
segments.push(nodes.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if segments.is_empty() { return Vec::new(); }
|
||||
|
||||
let mut completed_rings: Vec<Vec<i64>> = Vec::new();
|
||||
|
||||
// Keep assembling rings until we run out of segments
|
||||
while !segments.is_empty() {
|
||||
// Start a new ring with the first available segment
|
||||
let mut ring = segments.remove(0);
|
||||
|
||||
// Try to extend this ring
|
||||
let max_iterations = segments.len() * segments.len() + 100;
|
||||
let mut iterations = 0;
|
||||
|
||||
loop {
|
||||
iterations += 1;
|
||||
if iterations > max_iterations { break; }
|
||||
|
||||
let mut connected = false;
|
||||
|
||||
for i in 0..segments.len() {
|
||||
let seg = &segments[i];
|
||||
if seg.is_empty() { continue; }
|
||||
|
||||
let ring_start = *ring.first().unwrap();
|
||||
let ring_end = *ring.last().unwrap();
|
||||
let seg_start = *seg.first().unwrap();
|
||||
let seg_end = *seg.last().unwrap();
|
||||
|
||||
if ring_end == seg_start {
|
||||
// Connect: ring + seg (skip first node of seg)
|
||||
ring.extend(seg[1..].iter().cloned());
|
||||
segments.remove(i);
|
||||
connected = true;
|
||||
break;
|
||||
} else if ring_end == seg_end {
|
||||
// Connect: ring + reversed seg
|
||||
let reversed: Vec<i64> = seg.iter().rev().cloned().collect();
|
||||
ring.extend(reversed[1..].iter().cloned());
|
||||
segments.remove(i);
|
||||
connected = true;
|
||||
break;
|
||||
} else if ring_start == seg_end {
|
||||
// Connect: seg + ring
|
||||
let mut new_ring = seg.clone();
|
||||
new_ring.extend(ring[1..].iter().cloned());
|
||||
ring = new_ring;
|
||||
segments.remove(i);
|
||||
connected = true;
|
||||
break;
|
||||
} else if ring_start == seg_start {
|
||||
// Connect: reversed seg + ring
|
||||
let mut reversed: Vec<i64> = seg.iter().rev().cloned().collect();
|
||||
reversed.extend(ring[1..].iter().cloned());
|
||||
ring = reversed;
|
||||
segments.remove(i);
|
||||
connected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if ring is now closed
|
||||
if ring.len() >= 4 && ring.first() == ring.last() {
|
||||
completed_rings.push(ring);
|
||||
break; // Move to next ring
|
||||
}
|
||||
|
||||
// If no connection was made and ring isn't closed,
|
||||
// we can't extend this ring anymore
|
||||
if !connected {
|
||||
// Still save partial rings if they have enough points
|
||||
// This helps with incomplete data - at least show something
|
||||
if ring.len() >= 4 {
|
||||
// Force-close the ring
|
||||
let first = ring[0];
|
||||
ring.push(first);
|
||||
completed_rings.push(ring);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completed_rings
|
||||
}
|
||||
}
|
||||
28
importer/src/services/railway_service.rs
Normal file
28
importer/src/services/railway_service.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct RailwayService;
|
||||
|
||||
impl RailwayService {
|
||||
// Determine if a relation is a transit route and return its color
|
||||
pub fn get_route_color(tags: &HashMap<String, String>) -> Option<String> {
|
||||
if tags.get("type").map(|t| t == "route").unwrap_or(false) {
|
||||
let route_type = tags.get("route").map(|s| s.as_str());
|
||||
let is_transit = match route_type {
|
||||
Some("subway") | Some("tram") | Some("light_rail") => true,
|
||||
Some("train") => {
|
||||
// Only include S-Bahn and suburban trains
|
||||
tags.get("network").map(|n| n.contains("S-Bahn")).unwrap_or(false) ||
|
||||
tags.get("service").map(|s| s == "suburban").unwrap_or(false) ||
|
||||
tags.get("ref").map(|r| r.starts_with("S")).unwrap_or(false)
|
||||
},
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_transit {
|
||||
// Extract colour tag
|
||||
return tags.get("colour").or(tags.get("color")).cloned();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
13
importer/src/services/tile_service.rs
Normal file
13
importer/src/services/tile_service.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use std::f64::consts::PI;
|
||||
|
||||
pub struct TileService;
|
||||
|
||||
impl TileService {
|
||||
pub fn lat_lon_to_tile(lat: f64, lon: f64, zoom: u32) -> (i32, i32) {
|
||||
let n = 2.0_f64.powi(zoom as i32);
|
||||
let x = ((lon + 180.0) / 360.0 * n).floor() as i32;
|
||||
let lat_rad = lat.to_radians();
|
||||
let y = ((1.0 - (lat_rad.tan() + 1.0 / lat_rad.cos()).ln() / PI) / 2.0 * n).floor() as i32;
|
||||
(x, y)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user