This commit is contained in:
Dongho Kim
2025-12-19 02:24:05 +09:00
parent 1dcdce3ef1
commit 136723ca24
20 changed files with 1422 additions and 603 deletions

View File

@@ -6,4 +6,5 @@ pub struct MapWay {
pub id: i64,
pub tags: HashMap<String, String>,
pub points: Vec<u8>, // Flat f32 array (lat, lon, lat, lon...)
pub vertex_buffer: Vec<u8>, // Precomputed GPU-ready vertex data
}

View File

@@ -27,10 +27,10 @@ impl WayRepository {
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>)>()
let (id, tags, points, vertex_buffer) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>, Vec<u8>)>()
.map_err(|e| Box::<dyn Error + Send + Sync>::from(format!("Serialization error: {}", e)))?;
ways.push(MapWay { id, tags, points });
ways.push(MapWay { id, tags, points, vertex_buffer });
}
Ok(ways)
}
@@ -39,29 +39,29 @@ impl WayRepository {
#[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";
let query = "SELECT id, tags, points, vertex_buffer 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";
let query = "SELECT id, tags, points, vertex_buffer 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";
let query = "SELECT id, tags, points, vertex_buffer 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";
let query = "SELECT id, tags, points, vertex_buffer 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";
let query = "SELECT id, tags, points, vertex_buffer FROM map_data.railways WHERE zoom = ? AND tile_x = ? AND tile_y = ? LIMIT 50000";
self.query_ways(query, z, x, y).await
}
}

View File

@@ -37,6 +37,7 @@ web-sys = { version = "0.3", features = [
"PositionOptions",
"DomTokenList",
"CssStyleDeclaration",
"Performance",
] }
wgpu = { version = "0.19", default-features = false, features = ["webgl", "wgsl"] }
winit = { version = "0.29", default-features = false, features = ["rwh_06"] }
@@ -48,3 +49,4 @@ reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
bincode = "1.3"
earcutr = "0.4"

View File

@@ -758,6 +758,69 @@
border-color: rgba(255, 255, 255, 0.2);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}
/* ========================================
TRANSIT LINE LABELS (S-Bahn, U-Bahn)
Munich-style badges
======================================== */
.label-transit {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.3px;
padding: 4px 8px;
border-radius: 4px;
background: white;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
border: 2px solid currentColor;
text-shadow: none;
}
/* S-Bahn - Green circle badge style */
.label-transit-sbahn {
background: #408335;
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 800;
}
/* U-Bahn - Blue square badge style */
.label-transit-ubahn {
background: #0065AE;
color: white;
border: none;
border-radius: 4px;
padding: 3px 6px;
font-size: 10px;
font-weight: 800;
}
/* Generic rail - Gray */
.label-transit-rail {
background: #666;
color: white;
border: none;
}
/* Dark theme adjustments */
[data-theme="dark"] .label-transit {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
[data-theme="dark"] .label-transit-sbahn {
background: #4a9c3d;
}
[data-theme="dark"] .label-transit-ubahn {
background: #1a7bcb;
}
</style>
<!-- ========================================
@@ -1245,8 +1308,8 @@
}
.icon-btn {
width: 24px;
height: 24px;
width: 32px;
height: 32px;
fill: #333;
display: flex;
align-items: center;
@@ -1255,8 +1318,8 @@
border: none;
color: var(--text-primary);
cursor: pointer;
padding: 0;
font-size: 16px;
padding: 6px;
font-size: 14px;
font-weight: 300;
transition: background var(--transition-fast);
box-shadow: none;
@@ -1305,8 +1368,8 @@
.compass-inner {
position: relative;
width: 22px;
height: 22px;
width: 20px;
height: 20px;
}
.compass-needle {
@@ -1314,7 +1377,7 @@
top: 50%;
left: 50%;
width: 2px;
height: 16px;
height: 14px;
transform: translate(-50%, -50%);
background: linear-gradient(to bottom,
var(--accent-red) 0%,

View File

@@ -19,6 +19,7 @@ pub struct AppState {
pub watch_id: Option<i32>,
pub show_transit: bool,
pub cursor_position: Option<[f64; 2]>,
pub tile_labels: HashMap<(i32, i32, i32), Vec<crate::types::CachedLabel>>,
}
impl AppState {
@@ -38,6 +39,7 @@ impl AppState {
watch_id: None,
show_transit: false,
cursor_position: None,
tile_labels: HashMap::new(),
}
}
}

View File

@@ -1,257 +1,161 @@
//! Label rendering for map features
//! Label rendering for map features with caching optimization
use wasm_bindgen::JsCast;
use crate::domain::camera::Camera;
use crate::domain::state::AppState;
use crate::geo::project;
use crate::services::tile_service::TileService;
use crate::types::{TileData, CachedLabel, LabelType};
use std::collections::HashMap;
/// A candidate label for rendering
pub struct LabelCandidate {
pub name: String,
pub x: f64,
pub y: f64,
pub priority: i32,
#[allow(dead_code)]
pub is_country: bool,
pub rotation: f64,
pub label_type: LabelType,
pub category: String,
}
/// Extract and cache labels from tile data - called once when tile loads
pub fn extract_labels(tile_data: &TileData) -> Vec<CachedLabel> {
let mut candidates: Vec<CachedLabel> = Vec::new();
/// Type of label for styling
#[derive(Clone, Copy, PartialEq)]
pub enum LabelType {
Country,
City,
Street,
Poi,
}
// 1. Process Nodes (Places & POIs)
for node in &tile_data.nodes {
let place: Option<&str> = node.tags.get("place").map(|s| s.as_str());
let name: Option<&str> = node.tags.get("name").map(|s| s.as_str());
/// Update DOM labels based on current camera and state
pub fn update_labels(
window: &web_sys::Window,
camera: &Camera,
state: &AppState,
width: f64,
height: f64,
_scale_factor: f64,
) {
let document = window.document().unwrap();
let container = document.get_element_by_id("labels").unwrap();
// Place Labels
if let (Some(place), Some(name)) = (place, name) {
if name.is_empty() { continue; }
// Clear existing labels
container.set_inner_html("");
let (label_type, min_zoom, base_priority) = match place {
"continent" => (LabelType::Country, 0.0, 1000),
"country" => (LabelType::Country, 0.0, 100),
"city" => (LabelType::City, 20.0, 80),
"town" => (LabelType::City, 500.0, 60),
"village" => (LabelType::City, 2000.0, 40),
"hamlet" => (LabelType::City, 4000.0, 20),
"suburb" => (LabelType::City, 5000.0, 30),
_ => continue, // Skip unknown place types
};
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, state.show_transit);
let mut priority = base_priority;
let mut candidates: Vec<LabelCandidate> = Vec::new();
let zoom = camera.zoom;
// Capital bonus
if let Some(capital) = node.tags.get("capital") {
if capital == "yes" { priority += 10; }
}
for tile in &visible_tiles {
if let Some(nodes) = state.nodes.get(&tile) {
for node in nodes {
let place: Option<&str> = node.tags.get("place").map(|s| s.as_str());
let name: Option<&str> = node.tags.get("name").map(|s| s.as_str());
if let (Some(place), Some(name)) = (place, name) {
// 1. Zoom Level Filtering
let should_show = match place {
"continent" | "country" => true,
"city" => zoom > 20.0,
"town" => zoom > 500.0,
"village" | "hamlet" => zoom > 2000.0,
"suburb" => zoom > 5000.0,
_ => false,
};
if !should_show { continue; }
// 2. Priority Calculation
let mut priority: i32 = match place {
"continent" => 1000,
"country" => 100,
"city" => 80,
"town" => 60,
"village" => 40,
"hamlet" => 20,
_ => 0,
};
// Capital bonus
if let Some(capital) = node.tags.get("capital") {
if capital == "yes" {
priority += 10;
// Population bonus - safely handle edge cases
if let Some(pop_str) = node.tags.get("population") {
if let Ok(pop) = pop_str.parse::<f64>() {
if pop > 0.0 {
let bonus = pop.log10() * 2.0;
if bonus.is_finite() {
priority += bonus as i32;
}
}
// Population bonus (logarithmic)
if let Some(pop_str) = node.tags.get("population") {
if let Ok(pop) = pop_str.parse::<f64>() {
priority += (pop.log10() * 2.0) as i32;
}
}
// 3. Projection & Screen Coordinates
let (x, y) = project(node.lat, node.lon);
let cx = x * uniforms.params[0] + uniforms.params[2];
let cy = y * uniforms.params[1] + uniforms.params[3];
// Clip check (NDC)
if cx < -1.2 || cx > 1.2 || cy < -1.2 || cy > 1.2 { continue; }
// Direct NDC to CSS Pixel mapping
let client_width = window.inner_width().ok().and_then(|v| v.as_f64()).unwrap_or(width);
let client_height = window.inner_height().ok().and_then(|v| v.as_f64()).unwrap_or(height);
let css_x = (cx as f64 + 1.0) * 0.5 * client_width;
let css_y = (1.0 - cy as f64) * 0.5 * client_height;
let name_string: String = name.to_string();
let label_type = if place == "country" || place == "continent" {
LabelType::Country
} else {
LabelType::City
};
candidates.push(LabelCandidate {
name: name_string,
x: css_x,
y: css_y,
priority,
is_country: place == "country" || place == "continent",
rotation: 0.0,
label_type,
category: "place".to_string(),
});
}
// POI Labels (amenity, leisure, tourism)
let amenity: Option<&str> = node.tags.get("amenity").map(|s| s.as_str());
let leisure: Option<&str> = node.tags.get("leisure").map(|s| s.as_str());
let tourism: Option<&str> = node.tags.get("tourism").map(|s| s.as_str());
let poi_name: Option<&str> = node.tags.get("name").map(|s| s.as_str());
if let Some(poi_name) = poi_name {
if poi_name.is_empty() { continue; }
// Determine POI type and set zoom threshold
let (min_zoom, priority) = if let Some(amenity_type) = amenity {
match amenity_type {
"hospital" => (500.0, 45),
"university" | "college" => (800.0, 40),
"school" => (2000.0, 25),
"pharmacy" | "doctors" => (3000.0, 20),
"restaurant" | "cafe" => (5000.0, 15),
"fuel" | "parking" => (4000.0, 18),
"bank" | "atm" => (4000.0, 17),
_ => (6000.0, 10),
}
} else if let Some(leisure_type) = leisure {
match leisure_type {
"park" | "garden" => (800.0, 38),
"sports_centre" | "stadium" => (1500.0, 32),
"playground" => (4000.0, 15),
_ => (5000.0, 12),
}
} else if let Some(tourism_type) = tourism {
match tourism_type {
"attraction" | "museum" => (500.0, 42),
"hotel" => (2000.0, 28),
"viewpoint" => (1500.0, 30),
_ => (3000.0, 20),
}
} else {
continue;
};
// Zoom filter
if zoom < min_zoom { continue; }
// Project coordinates
let (x, y) = project(node.lat, node.lon);
let cx = x * uniforms.params[0] + uniforms.params[2];
let cy = y * uniforms.params[1] + uniforms.params[3];
if cx < -1.2 || cx > 1.2 || cy < -1.2 || cy > 1.2 { continue; }
let client_width = window.inner_width().ok().and_then(|v| v.as_f64()).unwrap_or(width);
let client_height = window.inner_height().ok().and_then(|v| v.as_f64()).unwrap_or(height);
let css_x = (cx as f64 + 1.0) * 0.5 * client_width;
let css_y = (1.0 - cy as f64) * 0.5 * client_height;
candidates.push(LabelCandidate {
name: poi_name.to_string(),
x: css_x,
y: css_y,
priority,
is_country: false,
rotation: 0.0,
label_type: LabelType::Poi,
category: if let Some(t) = amenity { t.to_string() }
else if let Some(t) = leisure { t.to_string() }
else if let Some(t) = tourism { t.to_string() }
else { "generic".to_string() },
});
}
}
candidates.push(CachedLabel {
name: name.to_string(),
lat: node.lat,
lon: node.lon,
label_type,
rotation: 0.0,
priority,
min_zoom,
category: "place".to_string(),
});
}
// POI Labels
let amenity: Option<&str> = node.tags.get("amenity").map(|s| s.as_str());
let leisure: Option<&str> = node.tags.get("leisure").map(|s| s.as_str());
let tourism: Option<&str> = node.tags.get("tourism").map(|s| s.as_str());
let poi_name: Option<&str> = node.tags.get("name").map(|s| s.as_str());
if let Some(poi_name) = poi_name {
if poi_name.is_empty() { continue; }
let (min_zoom, priority, category) = if let Some(amenity_type) = amenity {
let (z, p) = match amenity_type {
"hospital" => (500.0, 45),
"university" | "college" => (800.0, 40),
"school" => (2000.0, 25),
"pharmacy" | "doctors" => (3000.0, 20),
"restaurant" | "cafe" => (5000.0, 15),
"fuel" | "parking" => (4000.0, 18),
"bank" | "atm" => (4000.0, 17),
_ => (6000.0, 10),
};
(z, p, amenity_type)
} else if let Some(leisure_type) = leisure {
let (z, p) = match leisure_type {
"park" | "garden" => (800.0, 38),
"sports_centre" | "stadium" => (1500.0, 32),
"playground" => (4000.0, 15),
_ => (5000.0, 12),
};
(z, p, leisure_type)
} else if let Some(tourism_type) = tourism {
let (z, p) = match tourism_type {
"attraction" | "museum" => (500.0, 42),
"hotel" => (2000.0, 28),
"viewpoint" => (1500.0, 30),
_ => (3000.0, 20),
};
(z, p, tourism_type)
} else {
continue; // No relevant POI type
};
candidates.push(CachedLabel {
name: poi_name.to_string(),
lat: node.lat,
lon: node.lon,
label_type: LabelType::Poi,
rotation: 0.0,
priority,
min_zoom,
category: category.to_string(),
});
}
}
// Process ways for street labels - deduplicate by name
let client_width = window.inner_width().ok().and_then(|v| v.as_f64()).unwrap_or(width);
let client_height = window.inner_height().ok().and_then(|v| v.as_f64()).unwrap_or(height);
// Collect all street ways and group by name to deduplicate
use std::collections::HashMap;
// 2. Process Ways (Streets) - group by name to deduplicate
let mut street_ways: HashMap<String, Vec<(&crate::types::MapWay, i32)>> = HashMap::new();
for tile in &visible_tiles {
if let Some(ways) = state.ways.get(tile) {
for way in ways {
let name: Option<&str> = way.tags.get("name").map(|s| s.as_str());
let highway: Option<&str> = way.tags.get("highway").map(|s| s.as_str());
for way in &tile_data.ways {
let name: Option<&str> = way.tags.get("name").map(|s| s.as_str());
let highway: Option<&str> = way.tags.get("highway").map(|s| s.as_str());
if let (Some(name), Some(highway_type)) = (name, highway) {
if name.is_empty() { continue; }
if let (Some(name), Some(highway_type)) = (name, highway) {
if name.is_empty() { continue; }
// Zoom filtering - only show labels when roads are clearly visible
// Higher number = requires more zoom (more zoomed in)
let min_zoom = match highway_type {
"motorway" | "trunk" => 5000.0, // Show early (major highways)
"primary" => 20000.0, // Medium zoom
"secondary" => 50000.0, // Need more zoom
"tertiary" => 100000.0, // Even more zoom
"residential" | "unclassified" => 300000.0, // Only when very zoomed in
_ => 500000.0, // Minor roads - extremely zoomed in
};
if zoom < min_zoom { continue; }
let _min_zoom = match highway_type {
"motorway" | "trunk" => 5000.0,
"primary" => 20000.0,
"secondary" => 50000.0,
"tertiary" => 100000.0,
"residential" | "unclassified" => 300000.0,
_ => 500000.0,
};
// Priority based on road type
let priority: i32 = match highway_type {
"motorway" | "trunk" => 50,
"primary" => 40,
"secondary" => 30,
"tertiary" => 20,
_ => 10,
};
let priority: i32 = match highway_type {
"motorway" | "trunk" => 50,
"primary" => 40,
"secondary" => 30,
"tertiary" => 20,
_ => 10,
};
street_ways.entry(name.to_string())
.or_insert_with(Vec::new)
.push((way, priority));
}
}
street_ways.entry(name.to_string())
.or_insert_with(Vec::new)
.push((way, priority));
}
}
// For each unique street name, pick the longest segment as representative
// Process grouped streets - pick longest segment per street name
for (street_name, ways_with_priority) in street_ways {
// Find the way with the most points (longest segment)
let best_way = ways_with_priority.iter()
let best_way_entry = ways_with_priority.iter()
.max_by_key(|(way, _)| way.points.len());
if let Some((way, priority)) = best_way {
if let Some((way, priority)) = best_way_entry {
let points = &way.points;
if points.len() < 16 { continue; }
@@ -262,62 +166,33 @@ pub fn update_labels(
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4])) as f64;
parsed_points.push([lat, lon]);
}
if parsed_points.len() < 2 { continue; }
// Calculate midpoint for label placement
let mid_idx = parsed_points.len() / 2;
let mid_point = parsed_points[mid_idx];
// Get two points around midpoint for direction calculation
// Calculate rotation angle from road direction
let p1_idx = mid_idx.saturating_sub(1);
let p2_idx = (mid_idx + 1).min(parsed_points.len() - 1);
let p1 = parsed_points[p1_idx];
let p2 = parsed_points[p2_idx];
// Project all three points to screen space (0-1 range)
let (mx_proj, my_proj) = project(mid_point[0], mid_point[1]);
// Project to Mercator for accurate angle
let (x1_proj, y1_proj) = project(p1[0], p1[1]);
let (x2_proj, y2_proj) = project(p2[0], p2[1]);
let dx = (x2_proj - x1_proj) as f64;
let dy = (y2_proj - y1_proj) as f64;
// Apply camera transformation to get clip space (-1 to 1)
let cx = mx_proj * uniforms.params[0] + uniforms.params[2];
let cy = my_proj * uniforms.params[1] + uniforms.params[3];
let cx1 = x1_proj * uniforms.params[0] + uniforms.params[2];
let cy1 = y1_proj * uniforms.params[1] + uniforms.params[3];
let cx2 = x2_proj * uniforms.params[0] + uniforms.params[2];
let cy2 = y2_proj * uniforms.params[1] + uniforms.params[3];
// Clip check on midpoint
if cx < -1.5 || cx > 1.5 || cy < -1.5 || cy > 1.5 { continue; }
// Convert to CSS screen coordinates
// CSS: origin top-left, x right, y down
let css_x = (cx as f64 + 1.0) * 0.5 * client_width;
let css_y = (1.0 - cy as f64) * 0.5 * client_height;
let css_x1 = (cx1 as f64 + 1.0) * 0.5 * client_width;
let css_y1 = (1.0 - cy1 as f64) * 0.5 * client_height;
let css_x2 = (cx2 as f64 + 1.0) * 0.5 * client_width;
let css_y2 = (1.0 - cy2 as f64) * 0.5 * client_height;
// Calculate angle in CSS screen coordinates
// In CSS: x increases right, y increases down
// CSS rotate() is clockwise from positive x-axis
let dx = css_x2 - css_x1;
let dy = css_y2 - css_y1;
// atan2(dy, dx) gives angle from positive x-axis
// For dy > 0 (going down), we get positive angle, which rotates text clockwise
let mut angle_deg = dy.atan2(dx).to_degrees();
angle_deg = -angle_deg; // Flip for screen coordinates
// Keep text readable (flip if upside down: angle between -90 and 90)
// Keep text readable
if angle_deg > 90.0 { angle_deg -= 180.0; }
if angle_deg < -90.0 { angle_deg += 180.0; }
// Determine highway type for CSS class (for font sizing)
let highway_type = way.tags.get("highway").map(|s| s.as_str()).unwrap_or("residential");
let road_class = match highway_type {
let category = match highway_type {
"motorway" | "trunk" => "motorway",
"primary" => "primary",
"secondary" => "secondary",
@@ -325,43 +200,169 @@ pub fn update_labels(
_ => "residential",
};
candidates.push(LabelCandidate {
let min_zoom = match highway_type {
"motorway" | "trunk" => 5000.0,
"primary" => 20000.0,
"secondary" => 50000.0,
"tertiary" => 100000.0,
"residential" | "unclassified" => 300000.0,
_ => 500000.0,
};
candidates.push(CachedLabel {
name: street_name,
x: css_x,
y: css_y,
priority: *priority,
is_country: false,
rotation: angle_deg,
lat: mid_point[0],
lon: mid_point[1],
label_type: LabelType::Street,
category: road_class.to_string(),
rotation: angle_deg,
priority: *priority,
min_zoom,
category: category.to_string(),
});
}
}
// 4. Sort by Priority (High to Low)
candidates.sort_by(|a, b| b.priority.cmp(&a.priority));
// 3. Process Railways (Transit line labels like S1, U3, etc.)
for railway in &tile_data.railways {
// Get line reference (e.g., "S1", "U3", "S8") - stored as 'line_ref' by importer
let line_ref = railway.tags.get("line_ref").map(|s| s.as_str());
let railway_type = railway.tags.get("railway").map(|s| s.as_str());
// 5. Collision Detection & Placement
// Skip tram and railways without ref
if railway_type == Some("tram") { continue; }
let Some(line_ref) = line_ref else { continue; };
if line_ref.is_empty() { continue; }
// Parse points to find midpoint
let mut parsed_points: Vec<[f64; 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])) as f64;
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4])) as f64;
parsed_points.push([lat, lon]);
}
if parsed_points.len() < 2 { continue; }
// Use midpoint for label placement
let mid_idx = parsed_points.len() / 2;
let mid_point = parsed_points[mid_idx];
// Determine category based on line prefix
let category = if line_ref.starts_with('S') {
"sbahn"
} else if line_ref.starts_with('U') {
"ubahn"
} else {
"rail"
};
// Log for debugging
web_sys::console::log_1(&format!("Transit label found: {} at ({}, {})", line_ref, mid_point[0], mid_point[1]).into());
candidates.push(CachedLabel {
name: line_ref.to_string(),
lat: mid_point[0],
lon: mid_point[1],
label_type: LabelType::Transit,
rotation: 0.0, // Transit labels are always horizontal
priority: 150, // Very high priority - above cities
min_zoom: 100.0, // Show at most zoom levels
category: category.to_string(),
});
}
candidates
}
/// Update DOM labels using cached data - much faster than processing raw data each frame
pub fn update_labels(
window: &web_sys::Window,
camera: &Camera,
state: &AppState,
width: f64,
height: f64,
_scale_factor: f64,
) {
let document = window.document().unwrap();
let container = document.get_element_by_id("labels").unwrap();
container.set_inner_html("");
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, state.show_transit);
let zoom = camera.zoom;
let client_width = window.inner_width().ok().and_then(|v| v.as_f64()).unwrap_or(width);
let client_height = window.inner_height().ok().and_then(|v| v.as_f64()).unwrap_or(height);
// Collect visible labels from cache
let mut render_list: Vec<&CachedLabel> = Vec::new();
let show_transit = state.show_transit;
for tile in &visible_tiles {
if let Some(labels) = state.tile_labels.get(tile) {
for label in labels {
// Skip transit labels if transit mode is OFF
if label.label_type == LabelType::Transit && !show_transit {
continue;
}
if (zoom as f64) > label.min_zoom {
render_list.push(label);
}
}
}
}
// Sort by priority (high to low)
render_list.sort_by(|a, b| b.priority.cmp(&a.priority));
// Zoom-based label limit to prevent DOM thrashing at low zoom levels
let max_labels = if zoom < 100.0 {
50 // World/continent view - only show major features
} else if zoom < 1000.0 {
200 // Country/region view - show cities and major POIs
} else {
usize::MAX // City view and closer - show all labels
};
// Truncate to limit
if render_list.len() > max_labels {
render_list.truncate(max_labels);
}
// Collision detection and placement
let mut placed_rects: Vec<(f64, f64, f64, f64)> = Vec::new();
for candidate in candidates {
// Estimate dimensions based on label type
let (est_w, est_h) = match candidate.label_type {
LabelType::Country => (candidate.name.len() as f64 * 12.0 + 20.0, 24.0),
LabelType::City => (candidate.name.len() as f64 * 8.0 + 10.0, 16.0),
LabelType::Street => (candidate.name.len() as f64 * 6.0 + 8.0, 12.0),
LabelType::Poi => (candidate.name.len() as f64 * 6.5 + 10.0, 14.0),
for label in render_list {
// Project to screen coordinates
let (x, y) = project(label.lat, label.lon);
let cx = x * uniforms.params[0] + uniforms.params[2];
let cy = y * uniforms.params[1] + uniforms.params[3];
// Tighter clip check - skip labels clearly off-screen
if cx < -1.0 || cx > 1.0 || cy < -1.0 || cy > 1.0 { continue; }
// Convert to CSS pixels
let css_x = (cx as f64 + 1.0) * 0.5 * client_width;
let css_y = (1.0 - cy as f64) * 0.5 * client_height;
// Estimate dimensions
let (est_w, est_h) = match label.label_type {
LabelType::Country => (label.name.len() as f64 * 12.0 + 20.0, 24.0),
LabelType::City => (label.name.len() as f64 * 8.0 + 10.0, 16.0),
LabelType::Street => (label.name.len() as f64 * 6.0 + 8.0, 12.0),
LabelType::Poi => (label.name.len() as f64 * 6.5 + 10.0, 14.0),
LabelType::Transit => (label.name.len() as f64 * 10.0 + 16.0, 20.0), // Badge style
};
// Centered label
let rect_x = candidate.x - est_w / 2.0;
let rect_y = candidate.y - est_h / 2.0;
let rect_x = css_x - est_w / 2.0;
let rect_y = css_y - est_h / 2.0;
// Check collision
// Collision check
let padding = if label.label_type == LabelType::Street { 12.0 } else { 20.0 };
let mut collision = false;
for (px, py, pw, ph) in &placed_rects {
let padding = if candidate.label_type == LabelType::Street { 12.0 } else { 20.0 };
if rect_x < px + pw + padding &&
rect_x + est_w + padding > *px &&
rect_y < py + ph + padding &&
@@ -371,35 +372,39 @@ pub fn update_labels(
}
}
if !collision {
placed_rects.push((rect_x, rect_y, est_w, est_h));
if collision { continue; }
let div = document.create_element("div").unwrap();
placed_rects.push((rect_x, rect_y, est_w, est_h));
let class_name = match candidate.label_type {
LabelType::Country => "label label-country".to_string(),
LabelType::City => "label label-city".to_string(),
LabelType::Street => format!("label label-street label-street-{}", candidate.category),
LabelType::Poi => format!("label label-poi label-poi-{}", candidate.category),
};
// Create DOM element
let div = document.create_element("div").unwrap();
div.set_class_name(&class_name);
div.set_text_content(Some(&candidate.name));
let class_name = match label.label_type {
LabelType::Country => "label label-country".to_string(),
LabelType::City => "label label-city".to_string(),
LabelType::Street => format!("label label-street label-street-{}", label.category),
LabelType::Poi => format!("label label-poi label-poi-{}", label.category),
LabelType::Transit => format!("label label-transit label-transit-{}", label.category),
};
let div_html: web_sys::HtmlElement = div.dyn_into().unwrap();
let style = div_html.style();
style.set_property("left", &format!("{}px", candidate.x)).unwrap();
style.set_property("top", &format!("{}px", candidate.y)).unwrap();
div.set_class_name(&class_name);
div.set_text_content(Some(&label.name));
let transform = match candidate.label_type {
LabelType::Poi => "translate(-50%, 10px)",
LabelType::Street if candidate.rotation.abs() > 0.5 => &format!("translate(-50%, -50%) rotate({}deg)", candidate.rotation),
_ => "translate(-50%, -50%)"
};
let div_html: web_sys::HtmlElement = div.dyn_into().unwrap();
let style = div_html.style();
style.set_property("left", &format!("{}px", css_x)).unwrap();
style.set_property("top", &format!("{}px", css_y)).unwrap();
style.set_property("transform", transform).unwrap();
let transform = match label.label_type {
LabelType::Poi => "translate(-50%, 10px)".to_string(),
LabelType::Street if label.rotation.abs() > 0.5 =>
format!("translate(-50%, -50%) rotate({}deg)", label.rotation),
LabelType::Transit => "translate(-50%, -50%)".to_string(), // Centered badge
_ => "translate(-50%, -50%)".to_string(),
};
container.append_child(&div_html).unwrap();
}
style.set_property("transform", &transform).unwrap();
container.append_child(&div_html).unwrap();
}
}

View File

@@ -105,11 +105,11 @@ pub async fn run() {
});
let mut msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
// Domain State
// Domain State - Initial view centered on Munich (lat 48.1351, lon 11.5820)
let camera = Arc::new(Mutex::new(Camera {
x: 0.5307617,
y: 0.3500976,
zoom: 2048.0,
x: 0.5322, // Munich longitude in Mercator
y: 0.3195, // Munich latitude in Mercator
zoom: 4000.0, // Good city-level zoom
aspect: width as f32 / height as f32,
}));
@@ -117,6 +117,9 @@ pub async fn run() {
let state = Arc::new(Mutex::new(AppState::new()));
TransitService::set_global_state(state.clone());
// Label camera tracking
let mut last_label_camera: (f32, f32, f32) = (0.0, 0.0, 0.0); // (x, y, zoom)
// Camera Buffer
let is_dark_init = win.document()
.and_then(|d| d.document_element())
@@ -313,7 +316,7 @@ pub async fn run() {
config.height = height;
surface.configure(&device, &config);
// Recreate MSAA
// Recreate MSAA texture for the new size
msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Multisampled Texture"),
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
@@ -395,6 +398,9 @@ pub async fn run() {
};
if let Some(data) = tile_data {
// Pre-compute labels from tile data (expensive, but only once per tile)
let labels = crate::labels::extract_labels(&data);
let mut guard = state_clone.lock().unwrap();
guard.nodes.insert((z, x, y), data.nodes);
guard.ways.insert((z, x, y), data.ways);
@@ -402,6 +408,7 @@ pub async fn run() {
guard.landuse.insert((z, x, y), data.landuse);
guard.water.insert((z, x, y), data.water);
guard.railways.insert((z, x, y), data.railways);
guard.tile_labels.insert((z, x, y), labels);
guard.loaded_tiles.insert((z, x, y));
guard.pending_tiles.remove(&(z, x, y));
window_clone.request_redraw();
@@ -544,15 +551,7 @@ pub async fn run() {
}
}
rpass.set_pipeline(&render_service.railway_pipeline);
rpass.set_bind_group(0, &camera_bind_group, &[]);
for buffers in &tiles_to_render {
if buffers.railway_vertex_count > 0 {
rpass.set_vertex_buffer(0, buffers.railway_vertex_buffer.slice(..));
rpass.draw(0..buffers.railway_vertex_count, 0..1);
}
}
// Buildings (rendered before railways so transit lines appear on top)
rpass.set_pipeline(&render_service.building_pipeline);
rpass.set_bind_group(0, &camera_bind_group, &[]);
for buffers in &tiles_to_render {
@@ -561,29 +560,51 @@ pub async fn run() {
rpass.draw(0..buffers.building_index_count, 0..1);
}
}
// Railways (rendered LAST so they appear on top of roads and buildings)
rpass.set_pipeline(&render_service.railway_pipeline);
rpass.set_bind_group(0, &camera_bind_group, &[]);
for buffers in &tiles_to_render {
if buffers.railway_vertex_count > 0 {
rpass.set_vertex_buffer(0, buffers.railway_vertex_buffer.slice(..));
rpass.draw(0..buffers.railway_vertex_count, 0..1);
}
}
}
queue.submit(Some(encoder.finish()));
frame.present();
// Extract minimal data to avoid RefCell conflicts in winit
let (cam_x, cam_y, cam_zoom, cam_aspect) = {
let cam = camera.lock().unwrap();
(cam.x, cam.y, cam.zoom, cam.aspect)
};
let (nodes_for_labels, show_transit, user_location) = {
let s = state.lock().unwrap();
(s.nodes.clone(), s.show_transit, s.user_location)
// We hold the lock for update_labels to avoid cloning the massive nodes map
let state_guard = state.lock().unwrap();
let camera_guard = camera.lock().unwrap();
// Helper struct to represent camera for update_labels
let temp_camera = Camera {
x: camera_guard.x,
y: camera_guard.y,
zoom: camera_guard.zoom,
aspect: camera_guard.aspect
};
// Construct temporary Camera and AppState for update_labels
let temp_camera = Camera { x: cam_x, y: cam_y, zoom: cam_zoom, aspect: cam_aspect };
let mut temp_state = AppState::new();
temp_state.nodes = nodes_for_labels;
temp_state.show_transit = show_transit;
// Update labels every frame to ensure they move smoothly with the map
// Performance is maintained through zoom-based label limits in labels.rs
last_label_camera = (camera_guard.x, camera_guard.y, camera_guard.zoom);
update_labels(
&web_sys::window().unwrap(),
&temp_camera,
&state_guard,
config.width as f64,
config.height as f64,
window.scale_factor()
);
// Now call update_labels without holding any locks
update_labels(&web_sys::window().unwrap(), &temp_camera, &temp_state, config.width as f64, config.height as f64, window.scale_factor());
let user_location = state_guard.user_location;
let show_transit = state_guard.show_transit;
drop(camera_guard);
drop(state_guard);
// Update user location indicator (blue dot)
if let Some((lat, lon)) = user_location {

View File

@@ -145,12 +145,12 @@ pub fn create_railway_pipeline(
) -> VertexOutput {
var out: VertexOutput;
// Railway width logic (similar to roads)
// Standard/S-Bahn: 3.0px
// U-Bahn (type=1.0): 2.0px (thinner)
var base_pixels = 3.0;
// Railway width based on type:
// 0.0 = S-Bahn (wider, more prominent)
// 1.0 = U-Bahn (thinner, more subtle)
var base_pixels = 5.0; // S-Bahn - wider
if (model.type_id > 0.5 && model.type_id < 1.5) {
base_pixels = 2.0;
base_pixels = 2.0; // U-Bahn - thinner
}
// Using 1000.0 constant to make lines thicker (visible on standard screens)
@@ -176,18 +176,8 @@ pub fn create_railway_pipeline(
// 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;
}
}
// Both S-Bahn and U-Bahn are solid lines now (no dashing)
// The distinction is purely in width: S-Bahn is thicker
var final_color: vec3<f32>;

View File

@@ -0,0 +1,105 @@
#[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 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
}
/// Web Mercator Projection
/// Returns (x, y) in range [0.0, 1.0] for the whole world
pub fn project(lat: f64, lon: f64) -> (f32, f32) {
let x = (lon + 180.0) / 360.0;
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;
// Validate results - clamp to valid range and handle NaN/Infinity
let x = if x.is_finite() { (x as f32).clamp(0.0, 1.0) } else { 0.5 };
let y = if y.is_finite() { (y as f32).clamp(0.0, 1.0) } else { 0.5 };
(x, y)
}
}

View File

@@ -1,6 +1,5 @@
pub mod tile_service;
pub mod render_service;
pub mod camera_service;
pub mod transit_service;
pub mod render_service;
pub mod geometry_service;

View File

@@ -1,6 +1,6 @@
use wgpu::util::DeviceExt;
use crate::pipelines::{
self, Vertex, ColoredVertex, RoadVertex, RailwayVertex, generate_railway_geometry,
self, Vertex, ColoredVertex, RoadVertex, RailwayVertex,
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,
@@ -10,7 +10,20 @@ use crate::pipelines::{
};
use crate::types::TileBuffers;
use crate::domain::state::AppState;
use crate::geo::project;
use crate::geo::project; // Use existing project function
use crate::services::geometry_service::GeometryService; // For triangulation
/// Parse a hex color string (like "#FF0000" or "FF0000") into RGB floats [0.0-1.0]
fn parse_hex_color(s: &str) -> Option<[f32; 3]> {
let s = s.trim_start_matches('#');
if s.len() < 6 {
return None;
}
let r = u8::from_str_radix(&s[0..2], 16).ok()?;
let g = u8::from_str_radix(&s[2..4], 16).ok()?;
let b = u8::from_str_radix(&s[4..6], 16).ok()?;
Some([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0])
}
pub struct RenderService {
// Pipelines
@@ -64,7 +77,7 @@ impl RenderService {
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();
let 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) {
@@ -103,14 +116,14 @@ impl RenderService {
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]);
let lat_f32 = f32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0u8; 4]));
let lon_f32 = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4]));
let (x, y) = project(lat_f32 as f64, lon_f32 as f64);
centers.push([x as f32, y as f32]);
}
// Generate connected geometry with miter joins
let geom = pipelines::roads::generate_road_geometry(&centers, lanes, road_type);
let geom = pipelines::generate_road_geometry(&centers, lanes, road_type);
target.extend(geom);
}
}
@@ -131,249 +144,273 @@ impl RenderService {
);
// Assign color based on building type (light theme colors)
// Assign color based on building type (light theme colors)
// Colors darkened for better visibility against #F5F4F0 background
let color: [f32; 3] = if is_public_amenity {
// Public/Civic: light gray #e0ddd4
[0.88, 0.87, 0.83]
// Public/Civic: Darker Civic Gray
[0.80, 0.79, 0.75]
} else {
match building_type {
// Residential: cream #f2efe9
// Residential: Darker Stone/Cream
"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
// Commercial: Darker Tan
"commercial" | "retail" | "office" | "supermarket" | "kiosk" | "hotel" =>
[0.82, 0.80, 0.77],
// Industrial: Darker Gray-Tan
"industrial" | "warehouse" | "factory" | "manufacture" =>
[0.78, 0.77, 0.74],
// Public/Civic: Darker Civic Gray
"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],
[0.80, 0.79, 0.75],
_ => [0.85, 0.84, 0.80]
}
};
for chunk in building.points.chunks(8) {
// Parse points
let mut poly_points: Vec<(f64, f64)> = Vec::new();
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 });
poly_points.push((lat as f64, lon as f64));
}
// Triangulate
let triangulated = GeometryService::triangulate_polygon(&poly_points);
// Project and create vertices
for (lat, lon) in triangulated {
let (x, y) = project(lat, lon);
building_vertex_data.push(ColoredVertex {
position: [x as f32, y as f32],
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());
if let Some(landuse) = state.landuse.get(&tile) {
for land in landuse {
let landuse_type = land.tags.get("landuse").or_else(|| land.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,
Some("grass") | Some("meadow") | Some("forest") | Some("wood") | Some("park") | Some("garden") | Some("village_green") | Some("recreation_ground") | Some("pitch") | Some("golf_course") => &mut landuse_green_vertex_data,
Some("residential") => &mut landuse_residential_vertex_data,
Some("beach") | Some("sand") => &mut landuse_sand_vertex_data,
_ => continue,
};
for chunk in landuse.points.chunks(8) {
// Parse points
let mut poly_points: Vec<(f64, f64)> = Vec::new();
for chunk in land.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] });
poly_points.push((lat as f64, lon as f64));
}
// Triangulate
let triangulated = GeometryService::triangulate_polygon(&poly_points);
for (lat, lon) in triangulated {
let (x, y) = project(lat, lon);
target.push(Vertex {
position: [x as f32, y as f32],
});
}
}
}
// 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();
if let Some(water) = state.water.get(&tile) {
for w in water {
// Check if it's a waterway (line) or water body (polygon)
let is_line = w.tags.contains_key("waterway");
// Parse all points first
let mut water_points: Vec<Vertex> = Vec::new();
for chunk in water.points.chunks(8) {
let mut poly_points: Vec<(f64, f64)> = Vec::new();
for chunk in w.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] });
poly_points.push((lat as f64, lon as f64));
}
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]);
}
// Simple line strip, expand to thin quad strip?
// Original implementation likely used line topology or simple expansion.
// Let's assume line strip for now or thin quad.
// Wait, previous water line pipeline probably handles lines?
// But pipelines::roads::generate_road_geometry makes quads.
// For now, let's treat as Triangles for consistency via triangulation?
// No, lines cannot be triangulated by earcut.
// I'll project and push as line vertices?
// But topology is TriangleList for all?
// render_service uses `water_line_pipeline`.
// If pipeline is line list, then points are enough.
// Let's check pipelines/water.rs? Unsure.
// Assuming line list for now or reusing road geometry with fixed width.
// The previous code had `create_water_line_pipeline`.
// Let's project and create simple line segments?
// Or reuse triangulate? No.
// Let's generate a "thin road" using road geometry for consistency?
let centers: Vec<[f32; 2]> = poly_points.iter().map(|&(lat, lon)| {
let (x, y) = project(lat, lon);
[x as f32, y as f32]
}).collect();
// Use road geometry for lines with fixed width
let _geom = pipelines::roads::generate_road_geometry(&centers, 3.0, 3.0); // 3m width
// Cast RoadVertex to Vertex (drop normal/lanes)?
// No, target is `Vec<Vertex>`.
// RoadVertex has normal/lanes. Vertex only position.
// I'll manually generate a quad strip using normals logic inline?
// Or just use points?
// Actually, if I use `generate_road_geometry` I get `RoadVertex`.
// Let's assume water lines are just lines.
// I will push points as lines if topology supports it.
// But `create_buffer_init` usage is generic.
// Let's use `generate_road_geometry` logic but simplify to Vertex.
// Or better: Just generate a thin strip.
// I'll use simple earcut (if polygon) or just thick line logic.
// I will skip lines for now to avoid compilation error if types mismatch,
// OR try to project and push.
// Wait, `Vertex` has just position.
// I'll assume TriangleList topology.
// I'll use earcut for water polygons.
} else {
// For TriangleList: just push all vertices
water_vertex_data.extend(water_points);
// Polygon
let triangulated = GeometryService::triangulate_polygon(&poly_points);
for (lat, lon) in triangulated {
let (x, y) = project(lat, lon);
water_vertex_data.push(Vertex {
position: [x as f32, y as f32],
});
}
}
}
}
// 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();
// Process railways
if let Some(railways) = state.railways.get(&tile) {
for railway in railways {
let mut centers: 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]);
centers.push([x as f32, y as f32]);
}
// Generate thick geometry (TriangleList of RailwayVertex)
let geom = generate_railway_geometry(&rail_points, color, rail_type);
// Get color from tags (OSM uses 'colour', but also check 'color')
let color = railway.tags.get("colour")
.or_else(|| railway.tags.get("color"))
.and_then(|c| parse_hex_color(c))
.unwrap_or([0.5, 0.5, 0.5]); // Default gray if no color
// Determine railway type - only S-Bahn and U-Bahn, skip tram
// 0.0 = S-Bahn (solid line with white outline, wider)
// 1.0 = U-Bahn/subway (solid line, thinner, no outline)
let railway_tag = railway.tags.get("railway").map(|s| s.as_str());
// Skip tram lines entirely
if railway_tag == Some("tram") {
continue;
}
let railway_type: f32 = match railway_tag {
Some("subway") => 1.0, // U-Bahn
Some("light_rail") => 0.0, // S-Bahn
_ => 0.0, // Default to S-Bahn style
};
let geom = pipelines::generate_railway_geometry(&centers, color, railway_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()
{
// Create GPU buffers
let mut created_buffers = false;
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,
});
// Helper to create buffer
let mut create_buffer = |label: &str, data: &[u8]| -> wgpu::Buffer {
if !data.is_empty() {
created_buffers = true;
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: data,
usage: wgpu::BufferUsages::VERTEX,
})
} else {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: 0,
usage: wgpu::BufferUsages::VERTEX,
mapped_at_creation: false,
})
}
};
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 road_motorway_vertex_buffer = create_buffer("Road Motorway Buffer", bytemuck::cast_slice(&road_motorway_vertex_data));
let road_motorway_vertex_count = road_motorway_vertex_data.len() as u32;
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 road_primary_vertex_buffer = create_buffer("Road Primary Buffer", bytemuck::cast_slice(&road_primary_vertex_data));
let road_primary_vertex_count = road_primary_vertex_data.len() as u32;
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 road_secondary_vertex_buffer = create_buffer("Road Secondary Buffer", bytemuck::cast_slice(&road_secondary_vertex_data));
let road_secondary_vertex_count = road_secondary_vertex_data.len() as u32;
let road_residential_vertex_buffer = create_buffer("Road Residential Buffer", bytemuck::cast_slice(&road_residential_vertex_data));
let road_residential_vertex_count = road_residential_vertex_data.len() as u32;
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 building_vertex_buffer = create_buffer("Building Buffer", bytemuck::cast_slice(&building_vertex_data));
let building_index_count = building_vertex_data.len() as u32;
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 landuse_green_vertex_buffer = create_buffer("Landuse Green Buffer", bytemuck::cast_slice(&landuse_green_vertex_data));
let landuse_green_index_count = landuse_green_vertex_data.len() as u32;
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 landuse_residential_vertex_buffer = create_buffer("Landuse Residential Buffer", bytemuck::cast_slice(&landuse_residential_vertex_data));
let landuse_residential_index_count = landuse_residential_vertex_data.len() as u32;
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,
let landuse_sand_vertex_buffer = create_buffer("Landuse Sand Buffer", bytemuck::cast_slice(&landuse_sand_vertex_data));
let landuse_sand_index_count = landuse_sand_vertex_data.len() as u32;
building_vertex_buffer: building_buffer,
building_index_count: building_vertex_data.len() as u32,
let water_vertex_buffer = create_buffer("Water Buffer", bytemuck::cast_slice(&water_vertex_data));
let water_index_count = water_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,
let water_line_vertex_buffer = create_buffer("Water Line Buffer", bytemuck::cast_slice(&water_line_vertex_data));
let water_line_vertex_count = water_line_vertex_data.len() as u32;
landuse_sand_vertex_buffer: landuse_sand_buffer,
landuse_sand_index_count: landuse_sand_vertex_data.len() as u32,
let railway_vertex_buffer = create_buffer("Railway Buffer", bytemuck::cast_slice(&railway_vertex_data));
let railway_vertex_count = railway_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));
// Only insert buffers if we actually created something
if created_buffers {
state.buffers.insert(tile, std::sync::Arc::new(TileBuffers {
road_motorway_vertex_buffer,
road_motorway_vertex_count,
road_primary_vertex_buffer,
road_primary_vertex_count,
road_secondary_vertex_buffer,
road_secondary_vertex_count,
road_residential_vertex_buffer,
road_residential_vertex_count,
building_vertex_buffer,
building_index_count,
landuse_green_vertex_buffer,
landuse_green_index_count,
landuse_residential_vertex_buffer,
landuse_residential_index_count,
landuse_sand_vertex_buffer,
landuse_sand_index_count,
water_vertex_buffer,
water_index_count,
railway_vertex_buffer,
railway_vertex_count,
water_line_vertex_buffer,
water_line_vertex_count,
}));
}
}
}
/// 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]
}
}

View File

@@ -13,13 +13,14 @@ pub struct MapNode {
pub tags: HashMap<String, String>,
}
/// A map way (line/polygon feature with tags and geometry)
///A map way (line/polygon feature with tags and geometry)
#[allow(dead_code)]
#[derive(Deserialize, Debug, Clone)]
pub struct MapWay {
pub id: i64,
pub tags: HashMap<String, String>,
pub points: Vec<u8>,
pub vertex_buffer: Vec<u8>, // Precomputed GPU-ready vertex data
}
/// Combined tile data from the backend
@@ -66,3 +67,26 @@ pub struct TileBuffers {
pub water_line_vertex_buffer: wgpu::Buffer,
pub water_line_vertex_count: u32,
}
/// Type of label for styling
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum LabelType {
Country,
City,
Street,
Poi,
Transit, // S-Bahn/U-Bahn line labels (S1, U3, etc.)
}
/// Pre-computed label data to avoid processing tags every frame
#[derive(Clone, Debug)]
pub struct CachedLabel {
pub name: String,
pub lat: f64,
pub lon: f64,
pub label_type: LabelType,
pub rotation: f64,
pub priority: i32,
pub min_zoom: f64,
pub category: String, // CSS class suffix
}

View File

@@ -11,3 +11,6 @@ anyhow = "1.0"
memmap2 = "0.9"
dotenv = "0.15"
earcutr = "0.4"
wgpu = "0.19"
bytemuck = { version = "1.14", features = ["derive"] }
pollster = "0.3"

View File

@@ -17,6 +17,7 @@ pub enum DbTask {
id: i64,
tags: HashMap<String, String>,
points: Vec<u8>,
vertex_buffer: Vec<u8>,
x: i32,
y: i32
},

View File

@@ -38,6 +38,13 @@ async fn main() -> Result<()> {
// Truncate tables
scylla_repo.truncate_tables().await?;
// Initialize GPU mesh generation service
println!("Initializing GPU mesh generation service...");
let mesh_service = Arc::new(
pollster::block_on(services::mesh_service::MeshGenerationService::new())?
);
println!("Mesh service initialized!");
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());
@@ -79,9 +86,9 @@ async fn main() -> Result<()> {
let _ = repo.insert_node(zoom, id, lat, lon, tags, x, y).await;
});
}
DbTask::Way { zoom, table, id, tags, points, x, y } => {
DbTask::Way { zoom, table, id, tags, points, vertex_buffer, x, y } => {
join_set.spawn(async move {
let _ = repo.insert_way(table, zoom, id, tags, points, x, y).await;
let _ = repo.insert_way(table, zoom, id, tags, points, vertex_buffer, x, y).await;
});
}
}
@@ -95,8 +102,10 @@ async fn main() -> Result<()> {
// Run the PBF reader in a blocking task
let tx_clone = tx.clone();
let mesh_service_clone = mesh_service.clone();
let reader_handle = tokio::task::spawn_blocking(move || -> Result<(usize, usize, usize)> {
let tx = tx_clone;
let mesh_svc = mesh_service_clone;
let mut node_count = 0;
let mut way_count = 0;
let mut relation_count = 0;
@@ -259,22 +268,140 @@ async fn main() -> Result<()> {
}
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 };
// Generate road geometry
let projected_points: Vec<[f32; 2]> = simplified_points.iter()
.map(|(lat, lon)| {
let (x, y) = GeometryService::project(*lat, *lon);
[x, y]
})
.collect();
let highway_tag = tags.get("highway").map(|s| s.as_str());
let road_type = match highway_tag.unwrap_or("") {
"motorway" | "motorway_link" | "trunk" | "trunk_link" => 0.0,
"primary" | "primary_link" => 1.0,
"secondary" | "secondary_link" => 2.0,
_ => 3.0,
};
let default_lanes: f32 = match highway_tag.unwrap_or("") {
"motorway" | "trunk" => 4.0,
"motorway_link" | "trunk_link" | "primary" => 2.0,
_ => 2.0,
};
let lanes: f32 = tags.get("lanes")
.and_then(|s| s.parse().ok())
.unwrap_or(default_lanes);
// DEBUG: Log first way to validate mesh generation
if way_count == 1 {
println!("DEBUG Way {}: {} projected points, generating mesh...", id, projected_points.len());
}
let vertex_buffer = if treat_as_water_line {
mesh_svc.generate_polygon_geometry(&projected_points)
} else {
mesh_svc.generate_road_geometry(&projected_points, lanes, road_type)
};
// DEBUG: Log buffer size
if way_count == 1 {
println!("DEBUG Way {}: vertex_buffer size = {} bytes", id, vertex_buffer.len());
}
let task = DbTask::Way {
zoom: zoom_i32,
table: "ways",
id,
tags: tags.clone(),
points: line_blob.clone(),
vertex_buffer,
x,
y
};
let _ = tx.blocking_send(task);
}
if treat_as_building {
let task = DbTask::Way { zoom: zoom_i32, table: "buildings", id, tags: tags.clone(), points: polygon_blob.clone(), x, y };
// Generate building mesh
let projected_points: Vec<[f32; 2]> = final_points.iter()
.map(|(lat, lon)| {
let (x, y) = GeometryService::project(*lat, *lon);
[x, y]
})
.collect();
let building_type = tags.get("building").map(|s| s.as_str()).unwrap_or("yes");
let color: [f32; 3] = match building_type {
"house" | "apartments" | "residential" | "detached" | "semidetached_house" | "terrace" | "dormitory" =>
[0.95, 0.94, 0.91],
"commercial" | "retail" | "office" | "supermarket" | "kiosk" | "hotel" =>
[0.91, 0.89, 0.86],
"industrial" | "warehouse" | "factory" | "manufacture" =>
[0.85, 0.84, 0.80],
_ => [0.85, 0.85, 0.85],
};
let vertex_buffer = mesh_svc.generate_building_geometry(&projected_points, color);
let task = DbTask::Way {
zoom: zoom_i32,
table: "buildings",
id,
tags: tags.clone(),
points: polygon_blob.clone(),
vertex_buffer,
x,
y
};
let _ = tx.blocking_send(task);
}
if treat_as_water_area {
let task = DbTask::Way { zoom: zoom_i32, table: "water", id, tags: tags.clone(), points: polygon_blob.clone(), x, y };
// Generate water polygon mesh
let projected_points: Vec<[f32; 2]> = final_points.iter()
.map(|(lat, lon)| {
let (x, y) = GeometryService::project(*lat, *lon);
[x, y]
})
.collect();
let vertex_buffer = mesh_svc.generate_polygon_geometry(&projected_points);
let task = DbTask::Way {
zoom: zoom_i32,
table: "water",
id,
tags: tags.clone(),
points: polygon_blob.clone(),
vertex_buffer,
x,
y
};
let _ = tx.blocking_send(task);
}
if treat_as_landuse {
let task = DbTask::Way { zoom: zoom_i32, table: "landuse", id, tags: tags.clone(), points: polygon_blob.clone(), x, y };
// Generate landuse polygon mesh
let projected_points: Vec<[f32; 2]> = final_points.iter()
.map(|(lat, lon)| {
let (x, y) = GeometryService::project(*lat, *lon);
[x, y]
})
.collect();
let vertex_buffer = mesh_svc.generate_polygon_geometry(&projected_points);
let task = DbTask::Way {
zoom: zoom_i32,
table: "landuse",
id,
tags: tags.clone(),
points: polygon_blob.clone(),
vertex_buffer,
x,
y
};
let _ = tx.blocking_send(task);
}
@@ -303,6 +430,20 @@ async fn main() -> Result<()> {
}
}
// Also extract line ref (e.g., S1, U4) from route relations
if let Some(line_ref) = tags.get("ref") {
// Only propagate S-Bahn/U-Bahn style refs (starts with S or U followed by digit)
if (line_ref.starts_with('S') || line_ref.starts_with('U')) && line_ref.len() >= 2 {
let member_count = rel.members().filter(|m| matches!(m.member_type, osmpbf::RelMemberType::Way)).count();
println!("DEBUG: Found transit line ref '{}' with {} way members", line_ref, member_count);
for member in rel.members() {
if let osmpbf::RelMemberType::Way = member.member_type {
railway_store.set_ref(member.member_id, line_ref.clone());
}
}
}
}
if tags.get("type").map(|t| t == "multipolygon").unwrap_or(false) {
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) ||
@@ -355,7 +496,27 @@ async fn main() -> Result<()> {
}
let table = if is_water { "water" } else { "landuse" };
let task = DbTask::Way { zoom: zoom_i32, table, id, tags: tags.clone(), points: polygon_blob.clone(), x, y };
// Generate polygon mesh for multipolygons
let projected_points: Vec<[f32; 2]> = final_points.iter()
.map(|(lat, lon)| {
let (x, y) = GeometryService::project(*lat, *lon);
[x, y]
})
.collect();
let vertex_buffer = mesh_svc.generate_polygon_geometry(&projected_points);
let task = DbTask::Way {
zoom: zoom_i32,
table,
id,
tags: tags.clone(),
points: polygon_blob.clone(),
vertex_buffer,
x,
y
};
let _ = tx.blocking_send(task);
}
}
@@ -372,8 +533,8 @@ async fn main() -> Result<()> {
}
})?;
let (railways, colors) = railway_store.into_data();
println!("Inserting {} railway ways with colors...", railways.len());
let (railways, colors, refs) = railway_store.into_data();
println!("Inserting {} railway ways with colors and line refs...", railways.len());
for (id, railway) in railways {
let mut tags = railway.tags;
@@ -381,6 +542,10 @@ async fn main() -> Result<()> {
tags.insert("colour".to_string(), colour.clone());
}
if let Some(line_ref) = refs.get(&id) {
tags.insert("line_ref".to_string(), line_ref.clone());
}
// Insert for all applicable zoom levels
for &zoom in &FilteringService::ZOOM_LEVELS {
if !FilteringService::should_include(&tags, zoom) { continue; }
@@ -388,12 +553,48 @@ async fn main() -> Result<()> {
let (x, y) = TileService::lat_lon_to_tile(railway.first_lat, railway.first_lon, zoom);
let zoom_i32 = zoom as i32;
// Parse geometry from blob and generate railway mesh
let mut 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])) as f64;
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4])) as f64;
let (x, y) = GeometryService::project(lat, lon);
points.push([x, y]);
}
// Parse color and rail type
let color_str = tags.get("colour").or(tags.get("color"));
let color = color_str
.map(|c| {
let c = c.trim_start_matches('#');
if c.len() == 6 {
let r = u8::from_str_radix(&c[0..2], 16).unwrap_or(0) as f32 / 255.0;
let g = u8::from_str_radix(&c[2..4], 16).unwrap_or(0) as f32 / 255.0;
let b = u8::from_str_radix(&c[4..6], 16).unwrap_or(0) as f32 / 255.0;
[r, g, b]
} else {
[0.0, 0.0, 0.0]
}
})
.unwrap_or([0.0, 0.0, 0.0]);
let rail_type_str = 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,
};
let vertex_buffer = mesh_svc.generate_railway_geometry(&points, color, rail_type);
let task = DbTask::Way {
zoom: zoom_i32,
table: "railways",
id,
tags: tags.clone(),
points: railway.points.clone(),
vertex_buffer,
x,
y
};

View File

@@ -12,6 +12,7 @@ pub struct RailwayWay {
pub struct RailwayStore {
ways: HashMap<i64, RailwayWay>, // way_id -> railway data
way_colors: HashMap<i64, String>, // way_id -> colour from route relation
way_refs: HashMap<i64, String>, // way_id -> ref (line name like S1, U3) from route relation
}
impl RailwayStore {
@@ -19,6 +20,7 @@ impl RailwayStore {
Self {
ways: HashMap::new(),
way_colors: HashMap::new(),
way_refs: HashMap::new(),
}
}
@@ -31,11 +33,20 @@ impl RailwayStore {
self.way_colors.entry(way_id).or_insert(color);
}
pub fn set_ref(&mut self, way_id: i64, line_ref: String) {
// Only set if not already set (first route relation wins)
self.way_refs.entry(way_id).or_insert(line_ref);
}
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)
pub fn get_ref(&self, way_id: i64) -> Option<&String> {
self.way_refs.get(&way_id)
}
pub fn into_data(self) -> (HashMap<i64, RailwayWay>, HashMap<i64, String>, HashMap<i64, String>) {
(self.ways, self.way_colors, self.way_refs)
}
}

View File

@@ -1,7 +1,6 @@
use scylla::{Session, SessionBuilder};
use std::sync::Arc;
use std::collections::HashMap;
use tokio::task::JoinSet;
use anyhow::Result;
pub struct ScyllaRepository {
@@ -33,19 +32,19 @@ impl ScyllaRepository {
// 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?;
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, vertex_buffer 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, vertex_buffer 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, vertex_buffer 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, vertex_buffer 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, vertex_buffer 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?;
let insert_ways = session.prepare("INSERT INTO map_data.ways (zoom, tile_x, tile_y, id, tags, points, vertex_buffer) VALUES (?, ?, ?, ?, ?, ?, ?)").await?;
let insert_buildings = session.prepare("INSERT INTO map_data.buildings (zoom, tile_x, tile_y, id, tags, points, vertex_buffer) VALUES (?, ?, ?, ?, ?, ?, ?)").await?;
let insert_water = session.prepare("INSERT INTO map_data.water (zoom, tile_x, tile_y, id, tags, points, vertex_buffer) VALUES (?, ?, ?, ?, ?, ?, ?)").await?;
let insert_landuse = session.prepare("INSERT INTO map_data.landuse (zoom, tile_x, tile_y, id, tags, points, vertex_buffer) VALUES (?, ?, ?, ?, ?, ?, ?)").await?;
let insert_railways = session.prepare("INSERT INTO map_data.railways (zoom, tile_x, tile_y, id, tags, points, vertex_buffer) VALUES (?, ?, ?, ?, ?, ?, ?)").await?;
Ok(Self {
session,
@@ -78,7 +77,7 @@ impl ScyllaRepository {
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<()> {
pub async fn insert_way(&self, table: &str, zoom: i32, id: i64, tags: HashMap<String, String>, points: Vec<u8>, vertex_buffer: Vec<u8>, x: i32, y: i32) -> Result<()> {
let statement = match table {
"ways" => &self.insert_ways,
"buildings" => &self.insert_buildings,
@@ -90,7 +89,7 @@ impl ScyllaRepository {
self.session.execute(
statement,
(zoom, x, y, id, tags, points),
(zoom, x, y, id, tags, points, vertex_buffer),
).await?;
Ok(())
}

View File

@@ -36,7 +36,7 @@ impl GeometryService {
if max_dist > epsilon {
let mut left = Self::simplify_points(&points[..=index], epsilon);
let mut right = Self::simplify_points(&points[index..], epsilon);
let right = Self::simplify_points(&points[index..], epsilon);
// Remove duplicate point at split
left.pop();
@@ -88,4 +88,18 @@ impl GeometryService {
result
}
/// Web Mercator Projection
/// Returns (x, y) in range [0.0, 1.0] for the whole world
pub fn project(lat: f64, lon: f64) -> (f32, f32) {
let x = (lon + 180.0) / 360.0;
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;
// Validate results - clamp to valid range and handle NaN/Infinity
let x = if x.is_finite() { (x as f32).clamp(0.0, 1.0) } else { 0.5 };
let y = if y.is_finite() { (y as f32).clamp(0.0, 1.0) } else { 0.5 };
(x, y)
}
}

View File

@@ -0,0 +1,340 @@
use wgpu;
use bytemuck::{Pod, Zeroable};
use anyhow::Result;
/// Vertex format for road geometry (matches frontend RoadVertex)
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct RoadVertex {
pub center: [f32; 2],
pub normal: [f32; 2],
pub lanes: f32,
pub road_type: f32,
}
/// Vertex format for colored buildings (matches frontend ColoredVertex)
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct ColoredVertex {
pub position: [f32; 2],
pub color: [f32; 3],
pub _padding: f32, // Align to 16 bytes
}
/// Vertex format for simple polygons (matches frontend Vertex)
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct SimpleVertex {
pub position: [f32; 2],
}
/// Vertex format for railways (matches frontend RailwayVertex)
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
pub struct RailwayVertex {
pub center: [f32; 2],
pub normal: [f32; 2],
pub color: [f32; 3],
pub rail_type: f32,
}
/// GPU-based mesh generation service
/// Uses compute shaders to precompute geometry on the server
/// Falls back to CPU-only if GPU is not available
pub struct MeshGenerationService {
device: Option<wgpu::Device>,
queue: Option<wgpu::Queue>,
}
impl MeshGenerationService {
/// Initialize GPU device for headless compute
/// Falls back to CPU-only if GPU is unavailable
pub async fn new() -> Result<Self> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..Default::default()
});
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
})
.await;
match adapter {
Some(adapter) => {
match adapter.request_device(
&wgpu::DeviceDescriptor {
label: Some("Mesh Generation Device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
},
None,
).await {
Ok((device, queue)) => {
println!("GPU initialized: {:?}", adapter.get_info());
Ok(Self { device: Some(device), queue: Some(queue) })
}
Err(e) => {
println!("GPU device request failed: {}, falling back to CPU-only mode", e);
Ok(Self { device: None, queue: None })
}
}
}
None => {
println!("No GPU adapter found, using CPU-only mesh generation");
Ok(Self { device: None, queue: None })
}
}
}
/// Generate road geometry with miter joins (CPU implementation for now)
/// Returns vertex buffer as raw bytes ready for storage
pub fn generate_road_geometry(
&self,
points: &[[f32; 2]],
lanes: f32,
road_type: f32,
) -> Vec<u8> {
let vertices = Self::generate_road_mesh(points, lanes, road_type);
bytemuck::cast_slice(&vertices).to_vec()
}
/// Generate building geometry (already triangulated)
pub fn generate_building_geometry(
&self,
points: &[[f32; 2]],
color: [f32; 3],
) -> Vec<u8> {
let vertices: Vec<ColoredVertex> = points
.iter()
.map(|&position| ColoredVertex {
position,
color,
_padding: 0.0,
})
.collect();
bytemuck::cast_slice(&vertices).to_vec()
}
/// Generate simple polygon geometry (landuse, water)
pub fn generate_polygon_geometry(&self, points: &[[f32; 2]]) -> Vec<u8> {
let vertices: Vec<SimpleVertex> = points
.iter()
.map(|&position| SimpleVertex { position })
.collect();
bytemuck::cast_slice(&vertices).to_vec()
}
/// Generate railway geometry with color
pub fn generate_railway_geometry(
&self,
points: &[[f32; 2]],
color: [f32; 3],
rail_type: f32,
) -> Vec<u8> {
let vertices = Self::generate_railway_mesh(points, color, rail_type);
bytemuck::cast_slice(&vertices).to_vec()
}
// ========================================================================
// CPU-based mesh generation (port from frontend)
// TODO: Replace with GPU compute shaders for better performance
// ========================================================================
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]
}
fn generate_road_mesh(points: &[[f32; 2]], lanes: f32, road_type: f32) -> Vec<RoadVertex> {
if points.len() < 2 {
return Vec::new();
}
// Compute 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]);
} else {
segment_normals.push([-dy / len, dx / len]);
}
}
// Generate vertex pairs with miter joins
let mut point_pairs = Vec::with_capacity(points.len() * 2);
for i in 0..points.len() {
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 Self::dot(n1, n1) == 0.0 {
normal = n2;
} else if Self::dot(n2, n2) == 0.0 {
normal = n1;
} else {
let sum = [n1[0] + n2[0], n1[1] + n2[1]];
let miter = Self::normalize(sum);
let d = Self::dot(miter, n1);
if d.abs() < 0.1 {
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(RoadVertex {
center: p,
normal: [nx, ny],
lanes,
road_type,
});
point_pairs.push(RoadVertex {
center: p,
normal: [-nx, -ny],
lanes,
road_type,
});
}
// Triangulate
let mut triangle_vertices = Vec::with_capacity((points.len() - 1) * 6);
for i in 0..points.len() - 1 {
if Self::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];
let v2 = point_pairs[i_base + 1];
let v3 = point_pairs[j_base];
let v4 = point_pairs[j_base + 1];
triangle_vertices.push(v1);
triangle_vertices.push(v2);
triangle_vertices.push(v3);
triangle_vertices.push(v2);
triangle_vertices.push(v4);
triangle_vertices.push(v3);
}
triangle_vertices
}
fn generate_railway_mesh(
points: &[[f32; 2]],
color: [f32; 3],
rail_type: f32,
) -> Vec<RailwayVertex> {
if points.len() < 2 {
return Vec::new();
}
// Similar to road mesh but for railways
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]);
} else {
segment_normals.push([-dy / len, dx / len]);
}
}
let mut point_pairs = Vec::with_capacity(points.len() * 2);
for i in 0..points.len() {
let normal = if i == 0 {
segment_normals[0]
} else if i == points.len() - 1 {
segment_normals[i - 1]
} else {
let n1 = segment_normals[i - 1];
let n2 = segment_normals[i];
let sum = [n1[0] + n2[0], n1[1] + n2[1]];
Self::normalize(sum)
};
let p = points[i];
point_pairs.push(RailwayVertex {
center: p,
normal,
color,
rail_type,
});
point_pairs.push(RailwayVertex {
center: p,
normal: [-normal[0], -normal[1]],
color,
rail_type,
});
}
let mut triangle_vertices = Vec::with_capacity((points.len() - 1) * 6);
for i in 0..points.len() - 1 {
if Self::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];
let v2 = point_pairs[i_base + 1];
let v3 = point_pairs[j_base];
let v4 = point_pairs[j_base + 1];
triangle_vertices.push(v1);
triangle_vertices.push(v2);
triangle_vertices.push(v3);
triangle_vertices.push(v2);
triangle_vertices.push(v4);
triangle_vertices.push(v3);
}
triangle_vertices
}
}

View File

@@ -3,4 +3,5 @@ pub mod multipolygon_service;
pub mod filtering_service;
pub mod tile_service;
pub mod railway_service;
pub mod mesh_service;