This commit is contained in:
Dongho Kim
2025-12-04 06:09:04 +09:00
parent 003aae2b6b
commit c9678f28ba
4 changed files with 136 additions and 43 deletions

2
.env
View File

@@ -16,5 +16,5 @@ CLIENT_PORT=8080
SERVICE_LOG_LEVEL=debug SERVICE_LOG_LEVEL=debug
HOST_PBF_PATH=../maps_data/bayern-latest.osm.pbf HOST_PBF_PATH=../maps_data/europe-latest.osm.pbf
HOST_CACHE_DIR=./cache HOST_CACHE_DIR=./cache

View File

@@ -1,5 +1,3 @@
version: '3'
services: services:
scylla: scylla:
image: scylladb/scylla:latest image: scylladb/scylla:latest

View File

@@ -162,11 +162,12 @@
.label { .label {
position: absolute; position: absolute;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
color: white; color: #333;
text-shadow: 0 0 2px black, 0 0 4px black; text-shadow: 0 0 3px white, 0 0 3px white, 0 0 3px white;
font-family: sans-serif; font-family: sans-serif;
white-space: nowrap; white-space: nowrap;
pointer-events: none; pointer-events: none;
font-weight: 600;
} }
.label-country { .label-country {
@@ -174,13 +175,13 @@
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
color: #ffdddd; color: #222;
} }
.label-city { .label-city {
font-size: 12px; font-size: 12px;
font-weight: normal; font-weight: 600;
color: #ffffff; color: #333;
} }
</style> </style>
@@ -201,7 +202,7 @@
</div> </div>
<script type="module"> <script type="module">
import init from './wasm.js'; import init from './wasm.js?v=fixed_labels_v6';
async function run() { async function run() {
try { try {

View File

@@ -1190,7 +1190,11 @@ pub async fn run() {
// Let's stick to passing `&Camera` and let it call `to_uniform`. // Let's stick to passing `&Camera` and let it call `to_uniform`.
// But `to_uniform` is a method on `Camera`. // But `to_uniform` is a method on `Camera`.
// So: // So:
update_labels(&web_sys::window().unwrap(), &cam, &state_guard, config.width as f64, config.height as f64); // Debug scale factor once (or occasionally)
if state_guard.pending_tiles.is_empty() && state_guard.loaded_tiles.len() > 0 {
// web_sys::console::log_1(&format!("Scale Factor: {}", window.scale_factor()).into());
}
update_labels(&web_sys::window().unwrap(), &cam, &state_guard, config.width as f64, config.height as f64, window.scale_factor());
} }
WindowEvent::CloseRequested => { WindowEvent::CloseRequested => {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@@ -1843,12 +1847,21 @@ fn create_road_mesh_pipeline(
}) })
} }
struct LabelCandidate {
name: String,
x: f64,
y: f64,
priority: i32,
is_country: bool,
}
fn update_labels( fn update_labels(
window: &web_sys::Window, window: &web_sys::Window,
camera: &Camera, camera: &Camera,
state: &AppState, state: &AppState,
width: f64, width: f64,
height: f64, height: f64,
scale_factor: f64,
) { ) {
let document = window.document().unwrap(); let document = window.document().unwrap();
let container = document.get_element_by_id("labels").unwrap(); let container = document.get_element_by_id("labels").unwrap();
@@ -1856,11 +1869,11 @@ fn update_labels(
// Clear existing labels // Clear existing labels
container.set_inner_html(""); container.set_inner_html("");
let show_countries = true;
let show_cities = camera.zoom > 100.0;
let visible_tiles = get_visible_tiles(camera); let visible_tiles = get_visible_tiles(camera);
let uniforms = camera.to_uniform(); // Calculate uniforms let uniforms = camera.to_uniform();
let mut candidates: Vec<LabelCandidate> = Vec::new();
let zoom = camera.zoom;
for tile in visible_tiles { for tile in visible_tiles {
if let Some(nodes) = state.nodes.get(&tile) { if let Some(nodes) = state.nodes.get(&tile) {
@@ -1869,38 +1882,119 @@ fn update_labels(
let name = node.tags.get("name").map(|s| s.as_str()); let name = node.tags.get("name").map(|s| s.as_str());
if let (Some(place), Some(name)) = (place, name) { if let (Some(place), Some(name)) = (place, name) {
let is_country = place == "country"; // 1. Zoom Level Filtering
let is_city = place == "city" || place == "town"; let should_show = match place {
"continent" | "country" => true,
"city" => zoom > 20.0, // Show cities earlier
"town" => zoom > 500.0, // Show towns earlier
"village" | "hamlet" => zoom > 2000.0,
"suburb" => zoom > 5000.0,
_ => false,
};
if (is_country && show_countries) || (is_city && show_cities) { if !should_show { continue; }
// 2. Priority Calculation
let mut priority = 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 (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 (x, y) = project(node.lat, node.lon);
// Apply camera transform using uniforms
let cx = x * uniforms.params[0] + uniforms.params[2]; let cx = x * uniforms.params[0] + uniforms.params[2];
let cy = y * uniforms.params[1] + uniforms.params[3]; let cy = y * uniforms.params[1] + uniforms.params[3];
let ndc_x = cx; // Clip check (NDC)
let ndc_y = cy; if cx < -1.2 || cx > 1.2 || cy < -1.2 || cy > 1.2 { continue; }
if ndc_x < -1.2 || ndc_x > 1.2 || ndc_y < -1.2 || ndc_y > 1.2 { continue; } // Direct NDC to CSS Pixel mapping
// This bypasses any confusion about physical vs logical pixels or scale factors.
// We map [-1, 1] directly to [0, client_width].
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 screen_x = (ndc_x as f64 + 1.0) * 0.5 * width; let css_x = (cx as f64 + 1.0) * 0.5 * client_width;
let screen_y = (1.0 - ndc_y as f64) * 0.5 * height; let css_y = (1.0 - cy as f64) * 0.5 * client_height;
candidates.push(LabelCandidate {
name: name.to_string(),
x: css_x,
y: css_y,
priority,
is_country: place == "country" || place == "continent",
});
}
}
}
}
// 4. Sort by Priority (High to Low)
candidates.sort_by(|a, b| b.priority.cmp(&a.priority));
// 5. Collision Detection & Placement
let mut placed_rects: Vec<(f64, f64, f64, f64)> = Vec::new(); // (x, y, w, h)
for candidate in candidates {
// Estimate dimensions (approximate)
// Country labels are usually larger
let (est_w, est_h) = if candidate.is_country {
(candidate.name.len() as f64 * 12.0 + 20.0, 24.0)
} else {
(candidate.name.len() as f64 * 8.0 + 10.0, 16.0)
};
// Centered label
let rect_x = candidate.x - est_w / 2.0;
let rect_y = candidate.y - est_h / 2.0;
// Check collision
let mut collision = false;
for (px, py, pw, ph) in &placed_rects {
// Simple AABB intersection with padding
let padding = 5.0; // Reduced padding
if rect_x < px + pw + padding &&
rect_x + est_w + padding > *px &&
rect_y < py + ph + padding &&
rect_y + est_h + padding > *py {
collision = true;
break;
}
}
if !collision {
placed_rects.push((rect_x, rect_y, est_w, est_h));
let div = document.create_element("div").unwrap(); let div = document.create_element("div").unwrap();
let class_name = if is_country { "label label-country" } else { "label label-city" }; let class_name = if candidate.is_country { "label label-country" } else { "label label-city" };
div.set_class_name(class_name); div.set_class_name(class_name);
div.set_text_content(Some(name)); div.set_text_content(Some(&candidate.name));
let div_html: web_sys::HtmlElement = div.dyn_into().unwrap(); let div_html: web_sys::HtmlElement = div.dyn_into().unwrap();
let style = div_html.style(); let style = div_html.style();
style.set_property("left", &format!("{}px", screen_x)).unwrap(); style.set_property("left", &format!("{}px", candidate.x)).unwrap();
style.set_property("top", &format!("{}px", screen_y)).unwrap(); style.set_property("top", &format!("{}px", candidate.y)).unwrap();
container.append_child(&div_html).unwrap(); container.append_child(&div_html).unwrap();
} }
} }
}
}
}
} }