This commit is contained in:
Dongho Kim
2025-11-28 23:25:17 +09:00
parent 8e889aa992
commit afdcf23222
6 changed files with 259 additions and 28 deletions

View File

@@ -8,6 +8,7 @@ RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
WORKDIR /app/frontend WORKDIR /app/frontend
# Build frontend # Build frontend
RUN wasm-pack build --target web --out-name wasm --out-dir ../backend/static RUN wasm-pack build --target web --out-name wasm --out-dir ../backend/static
RUN cp index.html ../backend/static/index.html
# Build Backend # Build Backend
FROM rust:latest as backend-builder FROM rust:latest as backend-builder

View File

@@ -104,7 +104,11 @@ async fn get_tile_ways(
Path((z, x, y)): Path<(i32, i32, i32)>, Path((z, x, y)): Path<(i32, i32, i32)>,
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> { ) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
let query = "SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ?"; let query = if z < 9 {
"SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000"
} else {
"SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ?"
};
let rows = state.scylla_session.query(query, (z, x, y)) let rows = state.scylla_session.query(query, (z, x, y))
.await .await
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))? .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
@@ -137,6 +141,12 @@ async fn get_tile_buildings(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> { ) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
let query = "SELECT id, tags, points FROM map_data.buildings WHERE zoom = ? AND tile_x = ? AND tile_y = ?"; let query = "SELECT id, tags, points FROM map_data.buildings WHERE zoom = ? AND tile_x = ? AND tile_y = ?";
// Optimization: Don't load buildings for low zoom levels
if z < 13 {
return Ok(Json(Vec::new()));
}
let rows = state.scylla_session.query(query, (z, x, y)) let rows = state.scylla_session.query(query, (z, x, y))
.await .await
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))? .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
@@ -169,6 +179,12 @@ async fn get_tile_landuse(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> { ) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
println!("Request: get_tile_landuse({}, {}, {})", z, x, y); println!("Request: get_tile_landuse({}, {}, {})", z, x, y);
// Optimization: Don't load landuse for low zoom levels
if z < 11 {
return Ok(Json(Vec::new()));
}
let query = "SELECT id, tags, points FROM map_data.landuse WHERE zoom = ? AND tile_x = ? AND tile_y = ?"; let query = "SELECT id, tags, points FROM map_data.landuse WHERE zoom = ? AND tile_x = ? AND tile_y = ?";
println!("Executing query..."); println!("Executing query...");
let result = state.scylla_session.query(query, (z, x, y)).await; let result = state.scylla_session.query(query, (z, x, y)).await;
@@ -210,7 +226,11 @@ async fn get_tile_water(
Path((z, x, y)): Path<(i32, i32, i32)>, Path((z, x, y)): Path<(i32, i32, i32)>,
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> { ) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
let query = "SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ?"; let query = if z < 9 {
"SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000"
} else {
"SELECT id, tags, points FROM map_data.water WHERE zoom = ? AND tile_x = ? AND tile_y = ?"
};
let rows = state.scylla_session.query(query, (z, x, y)) let rows = state.scylla_session.query(query, (z, x, y))
.await .await
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))? .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
@@ -241,7 +261,11 @@ async fn get_tile_railways(
Path((z, x, y)): Path<(i32, i32, i32)>, Path((z, x, y)): Path<(i32, i32, i32)>,
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> { ) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
let query = "SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ?"; let query = if z < 9 {
"SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 10000"
} else {
"SELECT id, tags, points FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ?"
};
let rows = state.scylla_session.query(query, (z, x, y)) let rows = state.scylla_session.query(query, (z, x, y))
.await .await
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))? .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?

107
frontend/index.html Normal file
View File

@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Maps</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #1a1a1a;
font-family: system-ui, -apple-system, sans-serif;
}
canvas {
width: 100vw;
height: 100vh;
display: block;
}
#ui-container {
position: absolute;
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
background: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 8px;
backdrop-filter: blur(5px);
color: white;
}
.control-group {
display: flex;
flex-direction: column;
gap: 5px;
}
button {
background: #333;
color: white;
border: 1px solid #555;
padding: 8px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #444;
}
button:active {
background: #222;
}
#debug-info {
position: absolute;
bottom: 20px;
left: 20px;
color: rgba(255, 255, 255, 0.7);
font-size: 12px;
pointer-events: none;
background: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 4px;
}
</style>
</head>
<body>
<div id="ui-container">
<div class="control-group">
<button id="btn-zoom-in">+</button>
<button id="btn-zoom-out">-</button>
</div>
<div class="control-group">
<label for="zoom-slider" style="font-size: 12px;">Zoom</label>
<input type="range" id="zoom-slider" min="0" max="100" value="50">
</div>
<button id="btn-location">📍 My Location</button>
</div>
<div id="debug-info">
Zoom: <span id="debug-zoom">--</span> | Pos: <span id="debug-pos">--</span>
</div>
<script type="module">
import init from './wasm.js';
async function run() {
try {
await init();
console.log("WASM initialized");
} catch (e) {
console.error("Failed to initialize WASM:", e);
}
}
run();
</script>
</body>
</html>

View File

@@ -192,7 +192,7 @@ fn create_road_mesh(points: &[[f64; 2]], width: f32) -> Vec<Vertex> {
async fn fetch_cached(url: &str) -> Option<String> { async fn fetch_cached(url: &str) -> Option<String> {
let window = web_sys::window()?; let window = web_sys::window()?;
let caches = window.caches().ok()?; let caches = window.caches().ok()?;
let cache_name = "map-data-v1"; let cache_name = "map-data-v2";
let cache = wasm_bindgen_futures::JsFuture::from(caches.open(cache_name)).await.ok()?; let cache = wasm_bindgen_futures::JsFuture::from(caches.open(cache_name)).await.ok()?;
let cache: web_sys::Cache = cache.dyn_into().ok()?; let cache: web_sys::Cache = cache.dyn_into().ok()?;
@@ -839,7 +839,7 @@ pub async fn run() {
let window_clone_for_fetch = window.clone(); let window_clone_for_fetch = window.clone();
wasm_bindgen_futures::spawn_local(async move { wasm_bindgen_futures::spawn_local(async move {
// Fetch nodes // Fetch nodes
let url_nodes = format!("http://localhost:3000/api/tiles/{}/{}/{}", z, x, y); let url_nodes = format!("/api/tiles/{}/{}/{}", z, x, y);
let nodes_data = if let Some(json) = fetch_cached(&url_nodes).await { let nodes_data = if let Some(json) = fetch_cached(&url_nodes).await {
serde_json::from_str::<Vec<MapNode>>(&json).ok() serde_json::from_str::<Vec<MapNode>>(&json).ok()
} else { } else {
@@ -847,7 +847,7 @@ pub async fn run() {
}; };
// Fetch ways // Fetch ways
let url_ways = format!("http://localhost:3000/api/tiles/{}/{}/{}/ways", z, x, y); let url_ways = format!("/api/tiles/{}/{}/{}/ways", z, x, y);
let ways_data = if let Some(json) = fetch_cached(&url_ways).await { let ways_data = if let Some(json) = fetch_cached(&url_ways).await {
serde_json::from_str::<Vec<MapWay>>(&json).ok() serde_json::from_str::<Vec<MapWay>>(&json).ok()
} else { } else {
@@ -855,7 +855,7 @@ pub async fn run() {
}; };
// Fetch buildings // Fetch buildings
let url_buildings = format!("http://localhost:3000/api/tiles/{}/{}/{}/buildings", z, x, y); let url_buildings = format!("/api/tiles/{}/{}/{}/buildings", z, x, y);
let buildings_data = if let Some(json) = fetch_cached(&url_buildings).await { let buildings_data = if let Some(json) = fetch_cached(&url_buildings).await {
serde_json::from_str::<Vec<MapWay>>(&json).ok() serde_json::from_str::<Vec<MapWay>>(&json).ok()
} else { } else {
@@ -863,7 +863,7 @@ pub async fn run() {
}; };
// Fetch landuse // Fetch landuse
let url_landuse = format!("http://localhost:3000/api/tiles/{}/{}/{}/landuse", z, x, y); let url_landuse = format!("/api/tiles/{}/{}/{}/landuse", z, x, y);
let landuse_data = if let Some(json) = fetch_cached(&url_landuse).await { let landuse_data = if let Some(json) = fetch_cached(&url_landuse).await {
serde_json::from_str::<Vec<MapWay>>(&json).ok() serde_json::from_str::<Vec<MapWay>>(&json).ok()
} else { } else {
@@ -871,7 +871,7 @@ pub async fn run() {
}; };
// Fetch water // Fetch water
let url_water = format!("http://localhost:3000/api/tiles/{}/{}/{}/water", z, x, y); let url_water = format!("/api/tiles/{}/{}/{}/water", z, x, y);
let water_data = if let Some(json) = fetch_cached(&url_water).await { let water_data = if let Some(json) = fetch_cached(&url_water).await {
serde_json::from_str::<Vec<MapWay>>(&json).ok() serde_json::from_str::<Vec<MapWay>>(&json).ok()
} else { } else {
@@ -879,7 +879,7 @@ pub async fn run() {
}; };
// Fetch railways // Fetch railways
let url_railways = format!("http://localhost:3000/api/tiles/{}/{}/{}/railways", z, x, y); let url_railways = format!("/api/tiles/{}/{}/{}/railways", z, x, y);
let railways_data = if let Some(json) = fetch_cached(&url_railways).await { let railways_data = if let Some(json) = fetch_cached(&url_railways).await {
serde_json::from_str::<Vec<MapWay>>(&json).ok() serde_json::from_str::<Vec<MapWay>>(&json).ok()
} else { } else {
@@ -1099,6 +1099,10 @@ fn create_pipeline(
let x = world_pos.x * camera.params.x + camera.params.z; let x = world_pos.x * camera.params.x + camera.params.z;
let y = world_pos.y * camera.params.y + camera.params.w; let y = world_pos.y * camera.params.y + camera.params.w;
// Globe Effect: Spherize
// let r2 = x*x + y*y;
// let w = 1.0 + r2 * 0.5;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0); out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
return out; return out;
} }
@@ -1184,6 +1188,10 @@ fn create_road_pipeline(
let x = world_pos.x * camera.params.x + camera.params.z; let x = world_pos.x * camera.params.x + camera.params.z;
let y = world_pos.y * camera.params.y + camera.params.w; let y = world_pos.y * camera.params.y + camera.params.w;
// Globe Effect: Spherize
// let r2 = x*x + y*y;
// let w = 1.0 + r2 * 0.5;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0); out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
return out; return out;
} }
@@ -1268,6 +1276,10 @@ fn create_building_pipeline(
let x = world_pos.x * camera.params.x + camera.params.z; let x = world_pos.x * camera.params.x + camera.params.z;
let y = world_pos.y * camera.params.y + camera.params.w; let y = world_pos.y * camera.params.y + camera.params.w;
// Globe Effect: Spherize
// let r2 = x*x + y*y;
// let w = 1.0 + r2 * 0.5;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0); out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
return out; return out;
} }
@@ -1352,6 +1364,10 @@ fn create_landuse_pipeline(
let x = world_pos.x * camera.params.x + camera.params.z; let x = world_pos.x * camera.params.x + camera.params.z;
let y = world_pos.y * camera.params.y + camera.params.w; let y = world_pos.y * camera.params.y + camera.params.w;
// Globe Effect: Spherize
// let r2 = x*x + y*y;
// let w = 1.0 + r2 * 0.5;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0); out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
return out; return out;
} }
@@ -1446,6 +1462,10 @@ fn create_water_pipeline(
let x = world_pos.x * camera.params.x + camera.params.z; let x = world_pos.x * camera.params.x + camera.params.z;
let y = world_pos.y * camera.params.y + camera.params.w; let y = world_pos.y * camera.params.y + camera.params.w;
// Globe Effect: Spherize
// let r2 = x*x + y*y;
// let w = 1.0 + r2 * 0.5;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0); out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
return out; return out;
} }
@@ -1540,6 +1560,10 @@ fn create_railway_pipeline(
let x = world_pos.x * camera.params.x + camera.params.z; let x = world_pos.x * camera.params.x + camera.params.z;
let y = world_pos.y * camera.params.y + camera.params.w; let y = world_pos.y * camera.params.y + camera.params.w;
// Globe Effect: Spherize
// let r2 = x*x + y*y;
// let w = 1.0 + r2 * 0.5;
out.clip_position = vec4<f32>(x, y, 0.0, 1.0); out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
return out; return out;
} }

View File

@@ -8,7 +8,7 @@ use std::io::{BufWriter, Write, Seek, SeekFrom};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use memmap2::Mmap; use memmap2::Mmap;
const ZOOM_LEVELS: [u32; 4] = [6, 9, 12, 14]; const ZOOM_LEVELS: [u32; 5] = [2, 6, 9, 12, 14];
struct NodeStore { struct NodeStore {
writer: Option<BufWriter<File>>, writer: Option<BufWriter<File>>,
@@ -80,6 +80,58 @@ impl NodeStore {
} }
None 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 should_include(tags: &HashMap<String, String>, zoom: u32) -> bool { fn should_include(tags: &HashMap<String, String>, zoom: u32) -> bool {
@@ -92,21 +144,28 @@ fn should_include(tags: &HashMap<String, String>, zoom: u32) -> bool {
let waterway = tags.get("waterway").map(|s| s.as_str()); let waterway = tags.get("waterway").map(|s| s.as_str());
match zoom { match zoom {
match zoom {
2 => {
// Space View: Continents and Countries
matches!(place, Some("continent" | "country")) ||
matches!(natural, Some("water")) // Major water bodies
},
6 => { 6 => {
matches!(highway, Some("motorway" | "trunk" | "primary" | "secondary")) || // Enterprise Grade: ONLY Motorways and Trunk roads. No primary/secondary.
// ONLY Cities. No nature/landuse.
matches!(highway, Some("motorway" | "trunk")) ||
matches!(place, Some("city")) || matches!(place, Some("city")) ||
matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland")) || matches!(natural, Some("water")) || // Only major water bodies
matches!(tags.get("landuse").map(|s| s.as_str()), Some("forest" | "meadow" | "grass" | "recreation_ground" | "farmland")) || matches!(waterway, Some("river")) // Only major rivers
tags.contains_key("leisure") || // Parks, nature reserves, golf courses
matches!(waterway, Some("river" | "riverbank"))
}, },
9 => { 9 => {
matches!(highway, Some("motorway" | "trunk" | "primary" | "secondary")) || // Enterprise Grade: Add Primary roads.
// Add Towns.
// Limited nature.
matches!(highway, Some("motorway" | "trunk" | "primary")) ||
matches!(place, Some("city" | "town")) || matches!(place, Some("city" | "town")) ||
matches!(railway, Some("rail")) || matches!(railway, Some("rail")) ||
matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland")) || matches!(natural, Some("water" | "wood")) ||
tags.contains_key("landuse") ||
tags.contains_key("leisure") ||
matches!(waterway, Some("river" | "riverbank")) matches!(waterway, Some("river" | "riverbank"))
}, },
12 => { 12 => {
@@ -333,19 +392,36 @@ async fn main() -> Result<()> {
// Insert into the tile of the first point // Insert into the tile of the first point
let (first_lat, first_lon) = points[0]; let (first_lat, first_lon) = points[0];
// Serialize points to blob (f64, f64) pairs
let mut blob = Vec::with_capacity(points.len() * 16);
for (lat, lon) in points {
blob.extend_from_slice(&lat.to_be_bytes());
blob.extend_from_slice(&lon.to_be_bytes());
}
for &zoom in &ZOOM_LEVELS { for &zoom in &ZOOM_LEVELS {
if !should_include(&tags, zoom) { continue; } if !should_include(&tags, zoom) { continue; }
// Apply simplification based on zoom level
let epsilon = match zoom {
2 => 0.1, // Very high simplification
6 => 0.005, // High simplification
9 => 0.001, // Medium simplification
_ => 0.0, // No simplification
};
let simplified_points = if epsilon > 0.0 {
simplify_points(&points, epsilon)
} else {
points.clone()
};
if simplified_points.len() < 2 { continue; }
let (first_lat, first_lon) = simplified_points[0];
let (x, y) = lat_lon_to_tile(first_lat, first_lon, zoom); let (x, y) = lat_lon_to_tile(first_lat, first_lon, zoom);
let zoom_i32 = zoom as i32; let zoom_i32 = zoom as i32;
// Serialize simplified points
let mut blob = Vec::with_capacity(simplified_points.len() * 16);
for (lat, lon) in simplified_points {
blob.extend_from_slice(&lat.to_be_bytes());
blob.extend_from_slice(&lon.to_be_bytes());
}
if is_highway { if is_highway {
let task = DbTask::Way { zoom: zoom_i32, table: "ways", id, tags: tags.clone(), points: blob.clone(), x, y }; let task = DbTask::Way { zoom: zoom_i32, table: "ways", id, tags: tags.clone(), points: blob.clone(), x, y };
let _ = tx.blocking_send(task); let _ = tx.blocking_send(task);

View File

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