//! **En vivo desde un mapa real**: carga un GeoJSON (+ DEM opcional) y lo genera a //! un [`VoxelGrid`] del `dim` del mundo caminable de la app, para aterrizar y //! explorarlo a pie / en vuelo. Trim del cargador de `render_mapa` de //! `nahual-geo-voxel`, apuntado a un chunk fijo de `DIM_XZ`. //! //! `cargo run -p llimphi-voxel-app --release -- --mapa [--dem t.tif] //! [--tile z x y] [--bbox W S E N] [--mar N]` use nahual_geo_core::{parse_into, BBox, MapData, MAX_VERTICES}; use nahual_geo_voxel::{Heightfield, MapaVoxel, Opciones}; /// Lee los argumentos `--mapa …` y arma el `MapaVoxel` (mapa GRANDE, a resolución /// fija `--mpv`) que la ventana de streaming `dim` recorrerá. `None` sin `--mapa`. pub fn cargar(dim: [u32; 3]) -> Option<(MapaVoxel, [u32; 3])> { let argv: Vec = std::env::args().collect(); let mi = argv.iter().position(|a| a == "--mapa")?; let geojson_path = argv.get(mi + 1).expect("--mapa necesita "); let arg = |name: &str, n: usize| -> Option> { let p = argv.iter().position(|a| a == name)?; Some(argv[p + 1..(p + 1 + n).min(argv.len())].to_vec()) }; let src = std::fs::read_to_string(geojson_path).expect("leer geojson"); let (md, _) = parse_into(&src, MAX_VERTICES).expect("parsear geojson"); let bbox = match arg("--bbox", 4) { Some(n) => BBox { min_lon: n[0].parse().unwrap(), min_lat: n[1].parse().unwrap(), max_lon: n[2].parse().unwrap(), max_lat: n[3].parse().unwrap(), }, None => bbox_de(&md), }; // Resolución FIJA (metros por voxel): el mapa es más grande que la ventana, así // que caminando se revela terreno nuevo (no se encaja todo el mapa en el chunk). let mpv = arg("--mpv", 1).map(|v| v[0].parse().unwrap()).unwrap_or(2.0_f64).max(0.3); let height = if let (Some(t), Some(d)) = (arg("--tile", 3), arg("--dem", 1)) { let (z, x, y): (u32, u32, u32) = (t[0].parse().unwrap(), t[1].parse().unwrap(), t[2].parse().unwrap()); let bytes = std::fs::read(&d[0]).expect("leer dem"); Heightfield::desde_geotiff_en(&bytes, tile_bbox(z, x, y)).expect("geotiff (tile)") } else if let Some(d) = arg("--dem", 1) { let bytes = std::fs::read(&d[0]).expect("leer dem"); Heightfield::desde_geotiff(&bytes).expect("geotiff") } else { Heightfield::plano(bbox, 0.0) }; // Mínimo de elevación LOCAL del chunk (no del tile entero): así una zona alta // (Altamira ~900 m, el Ávila…) no se aplasta contra el techo del mundo caminable. let mut ref_min = f32::INFINITY; for i in 0..=24 { for j in 0..=24 { let lon = bbox.min_lon + (bbox.max_lon - bbox.min_lon) * i as f64 / 24.0; let lat = bbox.min_lat + (bbox.max_lat - bbox.min_lat) * j as f64 / 24.0; ref_min = ref_min.min(height.muestrear(lon, lat)); } } if !ref_min.is_finite() { ref_min = height.min_m; } let mar = arg("--mar", 1).map(|v| v[0].parse().unwrap()).unwrap_or(ref_min - 1000.0); let opts = Opciones { metros_por_voxel: mpv, escala_v: 1.0, nivel_mar_m: mar, alto_edificio_m: 12.0, ref_elev_m: Some(ref_min), ..Opciones::default() }; let mapa = MapaVoxel::desde_mapdata(bbox, height, opts, &md); eprintln!( "en vivo: {} edificios · {} calles · {} agua · {:.1} m/voxel · mapa {}×{} vox · ventana {dim:?}", mapa.edificios.len(), mapa.calles.len(), mapa.agua.len(), mpv, mapa.proj.ancho, mapa.proj.largo ); Some((mapa, dim)) } fn bbox_de(md: &MapData) -> BBox { if let Some(b) = md.bbox_override { return b; } let mut b = BBox::empty(); for &c in &md.points { b.expand(c); } for l in &md.lines { for &c in l { b.expand(c); } } for p in &md.polygons { for r in p { for &c in r { b.expand(c); } } } b } /// Caja lon/lat de un *slippy tile* `z/x/y`. fn tile_bbox(z: u32, x: u32, y: u32) -> BBox { let n = (1u64 << z) as f64; let lon = |xt: f64| xt / n * 360.0 - 180.0; let lat = |yt: f64| (std::f64::consts::PI * (1.0 - 2.0 * yt / n)).sinh().atan().to_degrees(); BBox { min_lon: lon(x as f64), max_lon: lon(x as f64 + 1.0), min_lat: lat(y as f64 + 1.0), max_lat: lat(y as f64) } }