update
This commit is contained in:
@@ -8,6 +8,7 @@ RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
WORKDIR /app/frontend
|
||||
# Build frontend
|
||||
RUN wasm-pack build --target web --out-name wasm --out-dir ../backend/static
|
||||
RUN cp index.html ../backend/static/index.html
|
||||
|
||||
# Build Backend
|
||||
FROM rust:latest as backend-builder
|
||||
|
||||
@@ -104,7 +104,11 @@ async fn get_tile_ways(
|
||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
|
||||
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))
|
||||
.await
|
||||
.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>>,
|
||||
) -> 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 = ?";
|
||||
|
||||
// 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))
|
||||
.await
|
||||
.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>>,
|
||||
) -> Result<Json<Vec<MapWay>>, (axum::http::StatusCode, String)> {
|
||||
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 = ?";
|
||||
println!("Executing query...");
|
||||
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)>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> 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))
|
||||
.await
|
||||
.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)>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> 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))
|
||||
.await
|
||||
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
|
||||
|
||||
107
frontend/index.html
Normal file
107
frontend/index.html
Normal 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>
|
||||
@@ -192,7 +192,7 @@ fn create_road_mesh(points: &[[f64; 2]], width: f32) -> Vec<Vertex> {
|
||||
async fn fetch_cached(url: &str) -> Option<String> {
|
||||
let window = web_sys::window()?;
|
||||
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: web_sys::Cache = cache.dyn_into().ok()?;
|
||||
|
||||
@@ -839,7 +839,7 @@ pub async fn run() {
|
||||
let window_clone_for_fetch = window.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
// 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 {
|
||||
serde_json::from_str::<Vec<MapNode>>(&json).ok()
|
||||
} else {
|
||||
@@ -847,7 +847,7 @@ pub async fn run() {
|
||||
};
|
||||
|
||||
// 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 {
|
||||
serde_json::from_str::<Vec<MapWay>>(&json).ok()
|
||||
} else {
|
||||
@@ -855,7 +855,7 @@ pub async fn run() {
|
||||
};
|
||||
|
||||
// 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 {
|
||||
serde_json::from_str::<Vec<MapWay>>(&json).ok()
|
||||
} else {
|
||||
@@ -863,7 +863,7 @@ pub async fn run() {
|
||||
};
|
||||
|
||||
// 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 {
|
||||
serde_json::from_str::<Vec<MapWay>>(&json).ok()
|
||||
} else {
|
||||
@@ -871,7 +871,7 @@ pub async fn run() {
|
||||
};
|
||||
|
||||
// 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 {
|
||||
serde_json::from_str::<Vec<MapWay>>(&json).ok()
|
||||
} else {
|
||||
@@ -879,7 +879,7 @@ pub async fn run() {
|
||||
};
|
||||
|
||||
// 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 {
|
||||
serde_json::from_str::<Vec<MapWay>>(&json).ok()
|
||||
} else {
|
||||
@@ -1099,6 +1099,10 @@ fn create_pipeline(
|
||||
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||
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);
|
||||
return out;
|
||||
}
|
||||
@@ -1184,6 +1188,10 @@ fn create_road_pipeline(
|
||||
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||
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);
|
||||
return out;
|
||||
}
|
||||
@@ -1268,6 +1276,10 @@ fn create_building_pipeline(
|
||||
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||
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);
|
||||
return out;
|
||||
}
|
||||
@@ -1352,6 +1364,10 @@ fn create_landuse_pipeline(
|
||||
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||
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);
|
||||
return out;
|
||||
}
|
||||
@@ -1446,6 +1462,10 @@ fn create_water_pipeline(
|
||||
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||
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);
|
||||
return out;
|
||||
}
|
||||
@@ -1540,6 +1560,10 @@ fn create_railway_pipeline(
|
||||
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||
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);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::io::{BufWriter, Write, Seek, SeekFrom};
|
||||
use std::path::{Path, PathBuf};
|
||||
use memmap2::Mmap;
|
||||
|
||||
const ZOOM_LEVELS: [u32; 4] = [6, 9, 12, 14];
|
||||
const ZOOM_LEVELS: [u32; 5] = [2, 6, 9, 12, 14];
|
||||
|
||||
struct NodeStore {
|
||||
writer: Option<BufWriter<File>>,
|
||||
@@ -80,6 +80,58 @@ impl NodeStore {
|
||||
}
|
||||
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 {
|
||||
@@ -92,21 +144,28 @@ fn should_include(tags: &HashMap<String, String>, zoom: u32) -> bool {
|
||||
let waterway = tags.get("waterway").map(|s| s.as_str());
|
||||
|
||||
match zoom {
|
||||
match zoom {
|
||||
2 => {
|
||||
// Space View: Continents and Countries
|
||||
matches!(place, Some("continent" | "country")) ||
|
||||
matches!(natural, Some("water")) // Major water bodies
|
||||
},
|
||||
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!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland")) ||
|
||||
matches!(tags.get("landuse").map(|s| s.as_str()), Some("forest" | "meadow" | "grass" | "recreation_ground" | "farmland")) ||
|
||||
tags.contains_key("leisure") || // Parks, nature reserves, golf courses
|
||||
matches!(waterway, Some("river" | "riverbank"))
|
||||
matches!(natural, Some("water")) || // Only major water bodies
|
||||
matches!(waterway, Some("river")) // Only major rivers
|
||||
},
|
||||
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!(railway, Some("rail")) ||
|
||||
matches!(natural, Some("water" | "wood" | "scrub" | "heath" | "wetland")) ||
|
||||
tags.contains_key("landuse") ||
|
||||
tags.contains_key("leisure") ||
|
||||
matches!(natural, Some("water" | "wood")) ||
|
||||
matches!(waterway, Some("river" | "riverbank"))
|
||||
},
|
||||
12 => {
|
||||
@@ -333,19 +392,36 @@ async fn main() -> Result<()> {
|
||||
// Insert into the tile of the first point
|
||||
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 {
|
||||
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 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 {
|
||||
let task = DbTask::Way { zoom: zoom_i32, table: "ways", id, tags: tags.clone(), points: blob.clone(), x, y };
|
||||
let _ = tx.blocking_send(task);
|
||||
|
||||
@@ -6,4 +6,3 @@ fi
|
||||
|
||||
echo "Using PBF file: ${HOST_PBF_PATH:-./europe-latest.osm.pbf}"
|
||||
docker compose -f docker-compose-remote.yml --profile import up --build importer
|
||||
docker compose -f docker-compose-remote.yml build --no-cache
|
||||
|
||||
Reference in New Issue
Block a user