Compare commits

...

8 Commits

Author SHA1 Message Date
louis-e
1f7e1ce45c Implement fallback for tile cache 2026-01-23 22:40:49 +01:00
louis-e
c62600e972 Fix jumping progress bar 2026-01-20 20:56:46 +01:00
louis-e
225cb79381 Feature gate send_log 2026-01-20 20:35:48 +01:00
louis-e
9fd1868d41 Parallelize terrain processing 2026-01-20 19:47:16 +01:00
louis-e
ceb0c80fba Log unexpected request errors to telemetry 2026-01-20 19:33:34 +01:00
louis-e
6444a4498a Save Bedrock .mcworld to Desktop, enable Info-level logging 2026-01-20 19:33:23 +01:00
louis-e
6ef8169d45 Add tile download retry, optimize outlier filtering, add cache cleanup 2026-01-20 19:33:04 +01:00
louis-e
568a6063f7 UI improvements and tile cache max age 2026-01-20 19:06:50 +01:00
9 changed files with 421 additions and 163 deletions

View File

@@ -1,6 +1,9 @@
use crate::coordinate_system::{geographic::LLBBox, transformation::geo_distance};
#[cfg(feature = "gui")]
use crate::telemetry::{send_log, LogLevel};
use crate::{
coordinate_system::{geographic::LLBBox, transformation::geo_distance},
progress::emit_gui_progress_update,
};
use image::Rgb;
use rayon::prelude::*;
use std::path::{Path, PathBuf};
@@ -18,6 +21,8 @@ const MIN_ZOOM: u8 = 10;
const MAX_ZOOM: u8 = 15;
/// Maximum concurrent tile downloads to be respectful to AWS
const MAX_CONCURRENT_DOWNLOADS: usize = 8;
/// Maximum age for cached tiles in days before they are cleaned up
const TILE_CACHE_MAX_AGE_DAYS: u64 = 7;
/// Holds processed elevation data and metadata
#[derive(Clone)]
@@ -35,6 +40,178 @@ type TileImage = image::ImageBuffer<Rgb<u8>, Vec<u8>>;
/// Result type for tile download operations: ((tile_x, tile_y), image) or error
type TileDownloadResult = Result<((u32, u32), TileImage), String>;
/// Cache directory name for elevation tiles
const TILE_CACHE_DIR_NAME: &str = "arnis-tile-cache";
/// Returns a writable cache directory for elevation tiles.
/// Tries current directory first, falls back to Desktop, then Home directory.
fn get_tile_cache_dir() -> PathBuf {
// Try current directory first
let local_cache = PathBuf::from("./").join(TILE_CACHE_DIR_NAME);
if try_create_cache_dir(&local_cache) {
return local_cache;
}
// Fall back to Desktop (only available with GUI feature)
#[cfg(feature = "gui")]
if let Some(desktop) = dirs::desktop_dir() {
let desktop_cache = desktop.join(TILE_CACHE_DIR_NAME);
if try_create_cache_dir(&desktop_cache) {
eprintln!(
"Note: Using Desktop for tile cache at {}",
desktop_cache.display()
);
return desktop_cache;
}
}
// Fall back to Home directory (only available with GUI feature)
#[cfg(feature = "gui")]
if let Some(home) = dirs::home_dir() {
let home_cache = home.join(TILE_CACHE_DIR_NAME);
if try_create_cache_dir(&home_cache) {
eprintln!(
"Note: Using home directory for tile cache at {}",
home_cache.display()
);
return home_cache;
}
}
// Last resort: use current directory anyway
// Log a warning since this will likely fail
eprintln!("Warning: Could not find a writable cache directory. Tile caching may fail.");
local_cache
}
/// Attempts to create the cache directory and verify it's writable.
/// Returns true if successful.
fn try_create_cache_dir(path: &Path) -> bool {
// Try to create the directory
if std::fs::create_dir_all(path).is_err() {
return false;
}
// Verify we can write to it by creating a unique test file
// Use process ID and timestamp to avoid conflicts with parallel instances
let test_filename = format!(
".arnis_write_test_{}_{:x}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
);
let test_file = path.join(test_filename);
if std::fs::write(&test_file, b"test").is_ok() {
let _ = std::fs::remove_file(&test_file);
return true;
}
false
}
/// Cleans up old cached tiles from the tile cache directory.
/// Only deletes .png files within the arnis-tile-cache directory that are older than TILE_CACHE_MAX_AGE_DAYS.
/// This function is safe and will not delete files outside the cache directory or fail on errors.
pub fn cleanup_old_cached_tiles() {
// Check all possible cache locations
let mut possible_locations: Vec<PathBuf> = vec![PathBuf::from("./").join(TILE_CACHE_DIR_NAME)];
#[cfg(feature = "gui")]
{
if let Some(desktop) = dirs::desktop_dir() {
possible_locations.push(desktop.join(TILE_CACHE_DIR_NAME));
}
if let Some(home) = dirs::home_dir() {
possible_locations.push(home.join(TILE_CACHE_DIR_NAME));
}
}
for location in possible_locations {
cleanup_cache_at_location(&location);
}
}
/// Cleans up old cached tiles at a specific location.
fn cleanup_cache_at_location(tile_cache_dir: &Path) {
if !tile_cache_dir.exists() || !tile_cache_dir.is_dir() {
return; // Nothing to clean up
}
let max_age = std::time::Duration::from_secs(TILE_CACHE_MAX_AGE_DAYS * 24 * 60 * 60);
let now = std::time::SystemTime::now();
let mut deleted_count = 0;
let mut error_count = 0;
// Read directory entries
let entries = match std::fs::read_dir(tile_cache_dir) {
Ok(entries) => entries,
Err(_) => {
return;
}
};
for entry in entries.flatten() {
let path = entry.path();
// Safety check: only process .png files within the cache directory
if !path.is_file() {
continue;
}
// Verify the file is a .png and follows our naming pattern (z{zoom}_x{x}_y{y}.png)
let file_name = match path.file_name().and_then(|n| n.to_str()) {
Some(name) => name,
None => continue,
};
if !file_name.ends_with(".png") || !file_name.starts_with('z') {
continue; // Skip files that don't match our tile naming pattern
}
// Check file age
let metadata = match std::fs::metadata(&path) {
Ok(m) => m,
Err(_) => continue,
};
let modified = match metadata.modified() {
Ok(time) => time,
Err(_) => continue,
};
let age = match now.duration_since(modified) {
Ok(duration) => duration,
Err(_) => continue, // File modified in the future? Skip it.
};
if age > max_age {
match std::fs::remove_file(&path) {
Ok(()) => deleted_count += 1,
Err(e) => {
// Log but don't fail, this is a best-effort cleanup
if error_count == 0 {
eprintln!(
"Warning: Failed to delete old cached tile {}: {e}",
path.display()
);
}
error_count += 1;
}
}
}
}
if deleted_count > 0 {
println!("Cleaned up {deleted_count} old cached elevation tiles (older than {TILE_CACHE_MAX_AGE_DAYS} days)");
}
if error_count > 1 {
eprintln!("Warning: Failed to delete {error_count} old cached tiles");
}
}
/// Calculates appropriate zoom level for the given bounding box
fn calculate_zoom_level(bbox: &LLBBox) -> u8 {
let lat_diff: f64 = (bbox.max().lat() - bbox.min().lat()).abs();
@@ -52,7 +229,13 @@ fn lat_lng_to_tile(lat: f64, lng: f64, zoom: u8) -> (u32, u32) {
(x, y)
}
/// Downloads a tile from AWS Terrain Tiles service
/// Maximum number of retry attempts for tile downloads
const TILE_DOWNLOAD_MAX_RETRIES: u32 = 3;
/// Base delay in milliseconds for exponential backoff between retries
const TILE_DOWNLOAD_RETRY_BASE_DELAY_MS: u64 = 500;
/// Downloads a tile from AWS Terrain Tiles service with retry logic
fn download_tile(
client: &reqwest::blocking::Client,
tile_x: u32,
@@ -66,7 +249,51 @@ fn download_tile(
.replace("{x}", &tile_x.to_string())
.replace("{y}", &tile_y.to_string());
let response = client.get(&url).send().map_err(|e| e.to_string())?;
let mut last_error: String = String::new();
for attempt in 0..TILE_DOWNLOAD_MAX_RETRIES {
if attempt > 0 {
// Exponential backoff: 500ms, 1000ms, 2000ms...
let delay_ms = TILE_DOWNLOAD_RETRY_BASE_DELAY_MS * (1 << (attempt - 1));
eprintln!(
"Retry attempt {}/{} for tile x={},y={},z={} after {}ms delay",
attempt,
TILE_DOWNLOAD_MAX_RETRIES - 1,
tile_x,
tile_y,
zoom,
delay_ms
);
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
}
match download_tile_once(client, &url, tile_path) {
Ok(img) => return Ok(img),
Err(e) => {
last_error = e;
if attempt < TILE_DOWNLOAD_MAX_RETRIES - 1 {
eprintln!(
"Tile download failed for x={},y={},z={}: {}",
tile_x, tile_y, zoom, last_error
);
}
}
}
}
Err(format!(
"Failed to download tile x={},y={},z={} after {} attempts: {}",
tile_x, tile_y, zoom, TILE_DOWNLOAD_MAX_RETRIES, last_error
))
}
/// Single download attempt for a tile (no retries)
fn download_tile_once(
client: &reqwest::blocking::Client,
url: &str,
tile_path: &Path,
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, String> {
let response = client.get(url).send().map_err(|e| e.to_string())?;
response.error_for_status_ref().map_err(|e| e.to_string())?;
let bytes = response.bytes().map_err(|e| e.to_string())?;
std::fs::write(tile_path, &bytes).map_err(|e| e.to_string())?;
@@ -86,35 +313,6 @@ fn fetch_or_load_tile(
tile_path: &Path,
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, String> {
if tile_path.exists() {
// Check if the cached file has a reasonable size (PNG files should be at least a few KB)
let file_size = std::fs::metadata(tile_path).map(|m| m.len()).unwrap_or(0);
if file_size < 1000 {
eprintln!(
"Warning: Cached tile at {} appears to be too small ({} bytes). Refetching tile.",
tile_path.display(),
file_size
);
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Cached tile appears too small, refetching.",
);
// Remove the potentially corrupted file
if let Err(e) = std::fs::remove_file(tile_path) {
eprintln!("Warning: Failed to remove corrupted tile file: {e}");
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Failed to remove corrupted tile file during refetching.",
);
}
// Re-download the tile
return download_tile(client, tile_x, tile_y, zoom, tile_path);
}
// Try to load cached tile, but handle corruption gracefully
match image::open(tile_path) {
Ok(img) => {
@@ -179,10 +377,8 @@ pub fn fetch_elevation_data(
let mut height_grid: Vec<Vec<f64>> = vec![vec![f64::NAN; grid_width]; grid_height];
let mut extreme_values_found = Vec::new(); // Track extreme values for debugging
let tile_cache_dir = PathBuf::from("./arnis-tile-cache");
if !tile_cache_dir.exists() {
std::fs::create_dir_all(&tile_cache_dir)?;
}
// Get a writable cache directory (tries current dir, falls back to Desktop/Home)
let tile_cache_dir = get_tile_cache_dir();
// Create a shared HTTP client for connection pooling
let client = reqwest::blocking::Client::new();
@@ -228,6 +424,7 @@ pub fn fetch_elevation_data(
}
println!("Processing {} elevation tiles...", successful_tiles.len());
emit_gui_progress_update(15.0, "Processing elevation...");
// Process tiles sequentially (writes to shared height_grid)
for ((tile_x, tile_y), rgb_img) in successful_tiles {
@@ -328,28 +525,32 @@ pub fn fetch_elevation_data(
// Release raw height grid
drop(height_grid);
let mut mc_heights: Vec<Vec<i32>> = Vec::with_capacity(blurred_heights.len());
// Find min/max in raw data
let mut min_height: f64 = f64::MAX;
let mut max_height: f64 = f64::MIN;
let mut extreme_low_count = 0;
let mut extreme_high_count = 0;
for row in &blurred_heights {
for &height in row {
min_height = min_height.min(height);
max_height = max_height.max(height);
// Count extreme values that might indicate data issues
if height < -1000.0 {
extreme_low_count += 1;
// Find min/max in raw data using parallel reduction
let (min_height, max_height, extreme_low_count, extreme_high_count) = blurred_heights
.par_iter()
.map(|row| {
let mut local_min = f64::MAX;
let mut local_max = f64::MIN;
let mut local_low = 0usize;
let mut local_high = 0usize;
for &height in row {
local_min = local_min.min(height);
local_max = local_max.max(height);
if height < -1000.0 {
local_low += 1;
}
if height > 10000.0 {
local_high += 1;
}
}
if height > 10000.0 {
extreme_high_count += 1;
}
}
}
(local_min, local_max, local_low, local_high)
})
.reduce(
|| (f64::MAX, f64::MIN, 0usize, 0usize),
|(min1, max1, low1, high1), (min2, max2, low2, high2)| {
(min1.min(min2), max1.max(max2), low1 + low2, high1 + high2)
},
);
//eprintln!("Height data range: {min_height} to {max_height} m");
if extreme_low_count > 0 {
@@ -399,27 +600,28 @@ pub fn fetch_elevation_data(
compressed_range
};
// Convert to scaled Minecraft Y coordinates
// Convert to scaled Minecraft Y coordinates (parallelized across rows)
// Lowest real elevation maps to ground_level, highest maps to ground_level + scaled_range
for row in blurred_heights {
let mc_row: Vec<i32> = row
.iter()
.map(|&h| {
// Calculate relative position within the elevation range (0.0 to 1.0)
let relative_height: f64 = if height_range > 0.0 {
(h - min_height) / height_range
} else {
0.0
};
// Scale to Minecraft blocks and add to ground level
let scaled_height: f64 = relative_height * scaled_range;
// Clamp to valid Minecraft Y range (leave buffer at top for structures)
((ground_level as f64 + scaled_height).round() as i32)
.clamp(ground_level, MAX_Y - TERRAIN_HEIGHT_BUFFER)
})
.collect();
mc_heights.push(mc_row);
}
let mc_heights: Vec<Vec<i32>> = blurred_heights
.par_iter()
.map(|row| {
row.iter()
.map(|&h| {
// Calculate relative position within the elevation range (0.0 to 1.0)
let relative_height: f64 = if height_range > 0.0 {
(h - min_height) / height_range
} else {
0.0
};
// Scale to Minecraft blocks and add to ground level
let scaled_height: f64 = relative_height * scaled_range;
// Clamp to valid Minecraft Y range (leave buffer at top for structures)
((ground_level as f64 + scaled_height).round() as i32)
.clamp(ground_level, MAX_Y - TERRAIN_HEIGHT_BUFFER)
})
.collect()
})
.collect();
let mut min_block_height: i32 = i32::MAX;
let mut max_block_height: i32 = i32::MIN;
@@ -456,48 +658,61 @@ fn apply_gaussian_blur(heights: &[Vec<f64>], sigma: f64) -> Vec<Vec<f64>> {
let kernel_size: usize = (sigma * 3.0).ceil() as usize * 2 + 1;
let kernel: Vec<f64> = create_gaussian_kernel(kernel_size, sigma);
// Apply blur
let mut blurred: Vec<Vec<f64>> = heights.to_owned();
let height_len = heights.len();
let width = heights[0].len();
// Horizontal pass
for row in blurred.iter_mut() {
let mut temp: Vec<f64> = row.clone();
for (i, val) in temp.iter_mut().enumerate() {
let mut sum: f64 = 0.0;
let mut weight_sum: f64 = 0.0;
for (j, k) in kernel.iter().enumerate() {
let idx: i32 = i as i32 + j as i32 - kernel_size as i32 / 2;
if idx >= 0 && idx < row.len() as i32 {
sum += row[idx as usize] * k;
weight_sum += k;
// Horizontal pass - parallelize across rows (each row is independent)
let after_horizontal: Vec<Vec<f64>> = heights
.par_iter()
.map(|row| {
let mut temp: Vec<f64> = vec![0.0; row.len()];
for (i, val) in temp.iter_mut().enumerate() {
let mut sum: f64 = 0.0;
let mut weight_sum: f64 = 0.0;
for (j, k) in kernel.iter().enumerate() {
let idx: i32 = i as i32 + j as i32 - kernel_size as i32 / 2;
if idx >= 0 && idx < row.len() as i32 {
sum += row[idx as usize] * k;
weight_sum += k;
}
}
*val = sum / weight_sum;
}
*val = sum / weight_sum;
}
*row = temp;
}
temp
})
.collect();
// Vertical pass
let height: usize = blurred.len();
let width: usize = blurred[0].len();
for x in 0..width {
let temp: Vec<_> = blurred
.iter()
.take(height)
.map(|row: &Vec<f64>| row[x])
.collect();
// Vertical pass - parallelize across columns (each column is independent)
// Process each column in parallel and collect results as column vectors
let blurred_columns: Vec<Vec<f64>> = (0..width)
.into_par_iter()
.map(|x| {
// Extract column from after_horizontal
let column: Vec<f64> = after_horizontal.iter().map(|row| row[x]).collect();
for (y, row) in blurred.iter_mut().enumerate().take(height) {
let mut sum: f64 = 0.0;
let mut weight_sum: f64 = 0.0;
for (j, k) in kernel.iter().enumerate() {
let idx: i32 = y as i32 + j as i32 - kernel_size as i32 / 2;
if idx >= 0 && idx < height as i32 {
sum += temp[idx as usize] * k;
weight_sum += k;
// Apply vertical blur to this column
let mut blurred_column: Vec<f64> = vec![0.0; height_len];
for (y, val) in blurred_column.iter_mut().enumerate() {
let mut sum: f64 = 0.0;
let mut weight_sum: f64 = 0.0;
for (j, k) in kernel.iter().enumerate() {
let idx: i32 = y as i32 + j as i32 - kernel_size as i32 / 2;
if idx >= 0 && idx < height_len as i32 {
sum += column[idx as usize] * k;
weight_sum += k;
}
}
*val = sum / weight_sum;
}
row[x] = sum / weight_sum;
blurred_column
})
.collect();
// Transpose columns back to row-major format
let mut blurred: Vec<Vec<f64>> = vec![vec![0.0; width]; height_len];
for (x, column) in blurred_columns.into_iter().enumerate() {
for (y, val) in column.into_iter().enumerate() {
blurred[y][x] = val;
}
}
@@ -579,15 +794,22 @@ fn filter_elevation_outliers(height_grid: &mut [Vec<f64>]) {
return;
}
// Sort to find percentiles
all_heights.sort_by(|a, b| a.partial_cmp(b).unwrap());
let len = all_heights.len();
// Use 1st and 99th percentiles to define reasonable bounds
// Using quickselect (select_nth_unstable) instead of full sort: O(n) vs O(n log n)
let p1_idx = (len as f64 * 0.01) as usize;
let p99_idx = (len as f64 * 0.99) as usize;
let min_reasonable = all_heights[p1_idx];
let max_reasonable = all_heights[p99_idx];
let p99_idx = ((len as f64 * 0.99) as usize).min(len - 1);
// Find p1 (1st percentile) - all elements before p1_idx will be <= p1
let (_, p1_val, _) =
all_heights.select_nth_unstable_by(p1_idx, |a, b| a.partial_cmp(b).unwrap());
let min_reasonable = *p1_val;
// Find p99 (99th percentile) - need to search in remaining slice or use separate call
let (_, p99_val, _) =
all_heights.select_nth_unstable_by(p99_idx, |a, b| a.partial_cmp(b).unwrap());
let max_reasonable = *p99_val;
//eprintln!("Filtering outliers outside range: {min_reasonable:.1}m to {max_reasonable:.1}m");

View File

@@ -2,6 +2,8 @@ use crate::args::Args;
use crate::coordinate_system::{cartesian::XZPoint, geographic::LLBBox};
use crate::elevation_data::{fetch_elevation_data, ElevationData};
use crate::progress::emit_gui_progress_update;
#[cfg(feature = "gui")]
use crate::telemetry::{send_log, LogLevel};
use colored::Colorize;
use image::{Rgb, RgbImage};
@@ -31,7 +33,11 @@ impl Ground {
},
Err(e) => {
eprintln!("Failed to fetch elevation data: {}", e);
emit_gui_progress_update(15.0, "Elevation unavailable, using flat ground");
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Elevation unavailable, using flat ground",
);
// Graceful fallback: disable elevation and keep provided ground_level
Self {
elevation_enabled: false,
@@ -141,7 +147,7 @@ impl Ground {
pub fn generate_ground_data(args: &Args) -> Ground {
if args.terrain {
println!("{} Fetching elevation...", "[3/7]".bold());
emit_gui_progress_update(15.0, "Fetching elevation...");
emit_gui_progress_update(14.0, "Fetching elevation...");
let ground = Ground::new_enabled(&args.bbox, args.scale, args.ground_level);
if args.debug {
ground.save_debug_image("elevation_debug");

View File

@@ -62,6 +62,13 @@ impl Drop for SessionLock {
}
}
/// Returns the Desktop directory for Bedrock .mcworld file output.
fn get_bedrock_output_directory() -> PathBuf {
dirs::desktop_dir()
.or_else(dirs::home_dir)
.unwrap_or_else(|| PathBuf::from("."))
}
/// Gets the area name for a given bounding box using the center point
fn get_area_name_for_bedrock(bbox: &LLBBox) -> String {
let center_lat = (bbox.min().lat() + bbox.max().lat()) / 2.0;
@@ -77,6 +84,9 @@ pub fn run_gui() {
// Configure thread pool with 90% CPU cap to keep system responsive
crate::floodfill_cache::configure_rayon_thread_pool(0.9);
// Clean up old cached elevation tiles on startup
crate::elevation_data::cleanup_old_cached_tiles();
// Launch the UI
println!("Launching UI...");
@@ -102,7 +112,7 @@ pub fn run_gui() {
tauri::Builder::default()
.plugin(
LogBuilder::default()
.level(LevelFilter::Warn)
.level(LevelFilter::Info)
.targets([
Target::new(TargetKind::LogDir {
file_name: Some("arnis".into()),
@@ -419,6 +429,7 @@ fn add_localized_world_name(world_path: PathBuf, bbox: &LLBBox) -> PathBuf {
if let Ok(compressed_data) = encoder.finish() {
if let Err(e) = std::fs::write(&level_path, compressed_data) {
eprintln!("Failed to update level.dat with area name: {e}");
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Failed to update level.dat with area name",
@@ -921,13 +932,12 @@ fn gui_start_generation(
(updated_path, None)
}
WorldFormat::BedrockMcWorld => {
// Bedrock: generate .mcworld in current directory with location-based name
// Bedrock: generate .mcworld on Desktop with location-based name
let area_name = get_area_name_for_bedrock(&bbox);
let filename = format!("Arnis {}.mcworld", area_name);
let lvl_name = format!("Arnis World: {}", area_name);
let output_path = std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(filename);
let output_path = get_bedrock_output_directory().join(&filename);
(output_path, Some(lvl_name))
}
};

View File

@@ -116,13 +116,23 @@ a:hover {
flex-direction: column;
}
.bbox-info-text {
.bbox-selection-text {
font-size: 0.9em;
color: #ffffff;
display: block;
font-weight: bold;
min-height: 2.5em;
line-height: 1.25em;
margin-top: 8px;
}
.progress-info-text {
font-size: 0.9em;
color: #ececec;
display: block;
font-weight: bold;
min-height: 1.5em;
line-height: 1.25em;
margin-bottom: 5px;
}
@@ -179,7 +189,6 @@ button:hover {
.progress-bar-container {
flex: 1;
max-width: 80%;
height: 20px;
background-color: #e0e0e0;
border-radius: 10px;

3
src/gui/index.html vendored
View File

@@ -57,10 +57,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</button>
</div>
<span id="bbox-selection-info" class="bbox-selection-text" data-localize="select_area_prompt">Select an area on the map using the tools.</span>
</div>
<div class="progress-section">
<span id="bbox-info" class="bbox-info-text" data-localize="select_area_prompt">Select an area on the map using the tools.</span>
<span id="progress-info" class="progress-info-text"></span>
<div class="progress-row">
<div class="progress-bar-container">
<div class="progress-bar" id="progress-bar"></div>

70
src/gui/js/main.js vendored
View File

@@ -12,14 +12,14 @@ if (window.__TAURI__) {
const DEFAULT_LOCALE_PATH = `./locales/en.json`;
// Track current bbox-info localization key for language changes
let currentBboxInfoKey = "select_area_prompt";
let currentBboxInfoColor = "#ffffff";
// Track current bbox selection info localization key for language changes
let currentBboxSelectionKey = "select_area_prompt";
let currentBboxSelectionColor = "#ffffff";
// Helper function to set bbox-info text and track it for language changes
async function setBboxInfo(bboxInfoElement, localizationKey, color) {
currentBboxInfoKey = localizationKey;
currentBboxInfoColor = color;
// Helper function to set bbox selection info text and track it for language changes
async function setBboxSelectionInfo(bboxSelectionElement, localizationKey, color) {
currentBboxSelectionKey = localizationKey;
currentBboxSelectionColor = color;
// Ensure localization is available
let localization = window.localization;
@@ -27,8 +27,8 @@ async function setBboxInfo(bboxInfoElement, localizationKey, color) {
localization = await getLocalization();
}
localizeElement(localization, { element: bboxInfoElement }, localizationKey);
bboxInfoElement.style.color = color;
localizeElement(localization, { element: bboxSelectionElement }, localizationKey);
bboxSelectionElement.style.color = color;
}
// Initialize elements and start the demo progress
@@ -132,11 +132,11 @@ async function applyLocalization(localization) {
localizeElement(localization, { selector: selector }, localizationElements[selector]);
}
// Re-apply current bbox-info text with new language
const bboxInfo = document.getElementById("bbox-info");
if (bboxInfo && currentBboxInfoKey) {
localizeElement(localization, { element: bboxInfo }, currentBboxInfoKey);
bboxInfo.style.color = currentBboxInfoColor;
// Re-apply current bbox selection info text with new language
const bboxSelectionInfo = document.getElementById("bbox-selection-info");
if (bboxSelectionInfo && currentBboxSelectionKey) {
localizeElement(localization, { element: bboxSelectionInfo }, currentBboxSelectionKey);
bboxSelectionInfo.style.color = currentBboxSelectionColor;
}
// Update error messages
@@ -210,7 +210,7 @@ function registerMessageEvent() {
// Function to set up the progress bar listener
function setupProgressListener() {
const progressBar = document.getElementById("progress-bar");
const bboxInfo = document.getElementById("bbox-info");
const progressInfo = document.getElementById("progress-info");
const progressDetail = document.getElementById("progress-detail");
window.__TAURI__.event.listen("progress-update", (event) => {
@@ -222,16 +222,16 @@ function setupProgressListener() {
}
if (message != "") {
bboxInfo.textContent = message;
progressInfo.textContent = message;
if (message.startsWith("Error!")) {
bboxInfo.style.color = "#fa7878";
progressInfo.style.color = "#fa7878";
generationButtonEnabled = true;
} else if (message.startsWith("Done!")) {
bboxInfo.style.color = "#7bd864";
progressInfo.style.color = "#7bd864";
generationButtonEnabled = true;
} else {
bboxInfo.style.color = "#ececec";
progressInfo.style.color = "#ececec";
}
}
});
@@ -535,7 +535,7 @@ function initWorldPicker() {
*/
function handleBboxInput() {
const inputBox = document.getElementById("bbox-coords");
const bboxInfo = document.getElementById("bbox-info");
const bboxSelectionInfo = document.getElementById("bbox-selection-info");
inputBox.addEventListener("input", function () {
const input = inputBox.value.trim();
@@ -547,12 +547,12 @@ function handleBboxInput() {
// Clear the info text only if no map selection exists
if (!mapSelectedBBox) {
setBboxInfo(bboxInfo, "select_area_prompt", "#ffffff");
setBboxSelectionInfo(bboxSelectionInfo, "select_area_prompt", "#ffffff");
} else {
// Restore map selection info display but don't update input field
const [lng1, lat1, lng2, lat2] = mapSelectedBBox.split(" ").map(Number);
const selectedSize = calculateBBoxSize(lng1, lat1, lng2, lat2);
displayBboxSizeStatus(bboxInfo, selectedSize);
displayBboxSizeStatus(bboxSelectionInfo, selectedSize);
}
return;
}
@@ -588,7 +588,7 @@ function handleBboxInput() {
// Update the info text and mark custom input as valid
customBBoxValid = true;
selectedBBox = bboxText.replace(/,/g, ' '); // Convert to space format for consistency
setBboxInfo(bboxInfo, "custom_selection_confirmed", "#7bd864");
setBboxSelectionInfo(bboxSelectionInfo, "custom_selection_confirmed", "#7bd864");
} else {
// Valid numbers but invalid order or range
customBBoxValid = false;
@@ -598,7 +598,7 @@ function handleBboxInput() {
} else {
selectedBBox = mapSelectedBBox;
}
setBboxInfo(bboxInfo, "error_coordinates_out_of_range", "#fecc44");
setBboxSelectionInfo(bboxSelectionInfo, "error_coordinates_out_of_range", "#fecc44");
}
} else {
// Input doesn't match the required format
@@ -609,7 +609,7 @@ function handleBboxInput() {
} else {
selectedBBox = mapSelectedBBox;
}
setBboxInfo(bboxInfo, "invalid_format", "#fecc44");
setBboxSelectionInfo(bboxSelectionInfo, "invalid_format", "#fecc44");
}
});
}
@@ -660,16 +660,16 @@ let customBBoxValid = false; // Tracks if custom input is valid
/**
* Displays the appropriate bbox size status message based on area thresholds
* @param {HTMLElement} bboxInfo - The element to display the message in
* @param {HTMLElement} bboxSelectionElement - The element to display the message in
* @param {number} selectedSize - The calculated bbox area in square meters
*/
function displayBboxSizeStatus(bboxInfo, selectedSize) {
function displayBboxSizeStatus(bboxSelectionElement, selectedSize) {
if (selectedSize > threshold2) {
setBboxInfo(bboxInfo, "area_too_large", "#fa7878");
setBboxSelectionInfo(bboxSelectionElement, "area_too_large", "#fa7878");
} else if (selectedSize > threshold1) {
setBboxInfo(bboxInfo, "area_extensive", "#fecc44");
setBboxSelectionInfo(bboxSelectionElement, "area_extensive", "#fecc44");
} else {
setBboxInfo(bboxInfo, "selection_confirmed", "#7bd864");
setBboxSelectionInfo(bboxSelectionElement, "selection_confirmed", "#7bd864");
}
}
@@ -686,12 +686,12 @@ function displayBboxInfoText(bboxText) {
selectedBBox = mapSelectedBBox;
customBBoxValid = false;
const bboxInfo = document.getElementById("bbox-info");
const bboxSelectionInfo = document.getElementById("bbox-selection-info");
const bboxCoordsInput = document.getElementById("bbox-coords");
// Reset the info text if the bbox is 0,0,0,0
if (lng1 === 0 && lat1 === 0 && lng2 === 0 && lat2 === 0) {
setBboxInfo(bboxInfo, "select_area_prompt", "#ffffff");
setBboxSelectionInfo(bboxSelectionInfo, "select_area_prompt", "#ffffff");
bboxCoordsInput.value = "";
mapSelectedBBox = "";
if (!customBBoxValid) {
@@ -706,7 +706,7 @@ function displayBboxInfoText(bboxText) {
// Calculate the size of the selected bbox
const selectedSize = calculateBBoxSize(lng1, lat1, lng2, lat2);
displayBboxSizeStatus(bboxInfo, selectedSize);
displayBboxSizeStatus(bboxSelectionInfo, selectedSize);
}
let worldPath = "";
@@ -796,8 +796,8 @@ async function startGeneration() {
}
if (!selectedBBox || selectedBBox == "0.000000 0.000000 0.000000 0.000000") {
const bboxInfo = document.getElementById('bbox-info');
setBboxInfo(bboxInfo, "select_location_first", "#fa7878");
const bboxSelectionInfo = document.getElementById('bbox-selection-info');
setBboxSelectionInfo(bboxSelectionInfo, "select_location_first", "#fa7878");
return;
}

View File

@@ -54,6 +54,9 @@ fn run_cli() {
// Configure thread pool with 90% CPU cap to keep system responsive
floodfill_cache::configure_rayon_thread_pool(0.9);
// Clean up old cached elevation tiles on startup
elevation_data::cleanup_old_cached_tiles();
let version: &str = env!("CARGO_PKG_VERSION");
let repository: &str = env!("CARGO_PKG_REPOSITORY");
println!(

View File

@@ -336,7 +336,7 @@ pub fn parse_osm_data(
}
}
emit_gui_progress_update(15.0, "");
emit_gui_progress_update(14.0, "");
drop(nodes_map);
drop(ways_map);

View File

@@ -1,6 +1,8 @@
use crate::coordinate_system::geographic::LLBBox;
use crate::osm_parser::OsmData;
use crate::progress::{emit_gui_error, emit_gui_progress_update, is_running_with_gui};
#[cfg(feature = "gui")]
use crate::telemetry::{send_log, LogLevel};
use colored::Colorize;
use rand::seq::SliceRandom;
use reqwest::blocking::Client;
@@ -44,6 +46,11 @@ fn download_with_reqwest(url: &str, query: &str) -> Result<String, Box<dyn std::
eprintln!("{}", format!("Error! {msg}").red().bold());
Err(msg.into())
} else {
#[cfg(feature = "gui")]
send_log(
LogLevel::Error,
&format!("Request error in download_with_reqwest: {e}"),
);
eprintln!("{}", format!("Error! {e:.52}").red().bold());
Err(format!("{e:.52}").into())
}