mirror of
https://github.com/louis-e/arnis.git
synced 2026-01-24 05:58:04 -05:00
Compare commits
8 Commits
v2.4.1
...
tilecache-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f7e1ce45c | ||
|
|
c62600e972 | ||
|
|
225cb79381 | ||
|
|
9fd1868d41 | ||
|
|
ceb0c80fba | ||
|
|
6444a4498a | ||
|
|
6ef8169d45 | ||
|
|
568a6063f7 |
@@ -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");
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
20
src/gui.rs
20
src/gui.rs
@@ -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))
|
||||
}
|
||||
};
|
||||
|
||||
13
src/gui/css/styles.css
vendored
13
src/gui/css/styles.css
vendored
@@ -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
3
src/gui/index.html
vendored
@@ -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
70
src/gui/js/main.js
vendored
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user