185 lines
5.7 KiB
Rust
185 lines
5.7 KiB
Rust
use crate::geo::Country;
|
|
|
|
/// Render a zoomed-in ASCII map centered on the target country, showing borders and labels.
|
|
pub fn render_map(
|
|
countries: &[Country],
|
|
target_code: &str,
|
|
width: usize,
|
|
height: usize,
|
|
) -> String {
|
|
// Find the target country and compute its bbox
|
|
let target_idx = countries
|
|
.iter()
|
|
.position(|c| c.iso_a2 == target_code)
|
|
.expect("Target country not found in GeoJSON");
|
|
|
|
let (min_lon, min_lat, max_lon, max_lat) = countries[target_idx].bbox;
|
|
|
|
// Add generous padding so the surrounding continent is visible
|
|
let lon_span = (max_lon - min_lon).max(4.0);
|
|
let lat_span = (max_lat - min_lat).max(4.0);
|
|
let pad_lon = lon_span * 1.0;
|
|
let pad_lat = lat_span * 1.0;
|
|
|
|
let view_min_lon = (min_lon - pad_lon).max(-180.0);
|
|
let view_max_lon = (max_lon + pad_lon).min(180.0);
|
|
let view_min_lat = (min_lat - pad_lat).max(-90.0);
|
|
let view_max_lat = (max_lat + pad_lat).min(90.0);
|
|
|
|
// Adjust aspect ratio: terminal chars are ~2x taller than wide
|
|
let lon_range = view_max_lon - view_min_lon;
|
|
let lat_range = view_max_lat - view_min_lat;
|
|
let char_aspect = 2.0;
|
|
|
|
let (final_lon_range, final_lat_range);
|
|
let desired_lon = lat_range * (width as f64) / (height as f64) / char_aspect;
|
|
let desired_lat = lon_range * (height as f64) / (width as f64) * char_aspect;
|
|
|
|
if desired_lon > lon_range {
|
|
final_lon_range = desired_lon;
|
|
final_lat_range = lat_range;
|
|
} else {
|
|
final_lon_range = lon_range;
|
|
final_lat_range = desired_lat;
|
|
}
|
|
|
|
let center_lon = (view_min_lon + view_max_lon) / 2.0;
|
|
let center_lat = (view_min_lat + view_max_lat) / 2.0;
|
|
|
|
let vp_min_lon = (center_lon - final_lon_range / 2.0).max(-180.0);
|
|
let vp_max_lat = (center_lat + final_lat_range / 2.0).min(90.0);
|
|
|
|
let lon_per_col = final_lon_range / width as f64;
|
|
let lat_per_row = final_lat_range / height as f64;
|
|
|
|
// Grid of characters
|
|
let mut grid = vec![vec![' '; width]; height];
|
|
|
|
// Rasterize polygon edges onto the grid
|
|
for (i, country) in countries.iter().enumerate() {
|
|
let (c_min_lon, c_min_lat, c_max_lon, c_max_lat) = country.bbox;
|
|
if c_max_lon < vp_min_lon
|
|
|| c_min_lon > vp_min_lon + final_lon_range
|
|
|| c_max_lat < vp_max_lat - final_lat_range
|
|
|| c_min_lat > vp_max_lat
|
|
{
|
|
continue;
|
|
}
|
|
|
|
let border_ch = if i == target_idx { '#' } else { '\u{00b7}' };
|
|
|
|
for poly in &country.polygons {
|
|
for ring in poly {
|
|
if ring.len() < 2 {
|
|
continue;
|
|
}
|
|
for edge in ring.windows(2) {
|
|
rasterize_edge(
|
|
edge[0][0],
|
|
edge[0][1],
|
|
edge[1][0],
|
|
edge[1][1],
|
|
vp_min_lon,
|
|
vp_max_lat,
|
|
lon_per_col,
|
|
lat_per_row,
|
|
width,
|
|
height,
|
|
border_ch,
|
|
i == target_idx,
|
|
&mut grid,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Place country code labels at bbox center
|
|
for (i, country) in countries.iter().enumerate() {
|
|
if country.iso_a2 == "-99" {
|
|
continue;
|
|
}
|
|
|
|
let (label_lon, label_lat) = country.label_pos;
|
|
|
|
let col = ((label_lon - vp_min_lon) / lon_per_col) as isize;
|
|
let row = ((vp_max_lat - label_lat) / lat_per_row) as isize;
|
|
|
|
let label = &country.iso_a2;
|
|
let start_col = col - (label.len() as isize / 2);
|
|
|
|
for (j, ch) in label.chars().enumerate() {
|
|
let c = start_col + j as isize;
|
|
if c >= 0 && (c as usize) < width && row >= 0 && (row as usize) < height {
|
|
let r = row as usize;
|
|
let c = c as usize;
|
|
let existing = grid[r][c];
|
|
// Target label always writes; neighbor labels only on empty or neighbor border
|
|
if i == target_idx || existing == ' ' || existing == '\u{00b7}' {
|
|
grid[r][c] = ch;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Render grid to string
|
|
grid.iter()
|
|
.map(|row| row.iter().collect::<String>())
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
|
|
/// Rasterize a line segment onto the grid using Bresenham's algorithm.
|
|
fn rasterize_edge(
|
|
lon0: f64,
|
|
lat0: f64,
|
|
lon1: f64,
|
|
lat1: f64,
|
|
vp_min_lon: f64,
|
|
vp_max_lat: f64,
|
|
lon_per_col: f64,
|
|
lat_per_row: f64,
|
|
width: usize,
|
|
height: usize,
|
|
ch: char,
|
|
is_target: bool,
|
|
grid: &mut [Vec<char>],
|
|
) {
|
|
let c0 = ((lon0 - vp_min_lon) / lon_per_col) as i32;
|
|
let r0 = ((vp_max_lat - lat0) / lat_per_row) as i32;
|
|
let c1 = ((lon1 - vp_min_lon) / lon_per_col) as i32;
|
|
let r1 = ((vp_max_lat - lat1) / lat_per_row) as i32;
|
|
|
|
let mut x = c0;
|
|
let mut y = r0;
|
|
let dx = (c1 - c0).abs();
|
|
let dy = -(r1 - r0).abs();
|
|
let sx = if c0 < c1 { 1 } else { -1 };
|
|
let sy = if r0 < r1 { 1 } else { -1 };
|
|
let mut err = dx + dy;
|
|
|
|
loop {
|
|
if x >= 0 && (x as usize) < width && y >= 0 && (y as usize) < height {
|
|
let cell = &mut grid[y as usize][x as usize];
|
|
// Target borders overwrite neighbor borders; neighbor borders only on empty
|
|
if is_target || *cell == ' ' {
|
|
*cell = ch;
|
|
}
|
|
}
|
|
|
|
if x == c1 && y == r1 {
|
|
break;
|
|
}
|
|
|
|
let e2 = 2 * err;
|
|
if e2 >= dy {
|
|
err += dy;
|
|
x += sx;
|
|
}
|
|
if e2 <= dx {
|
|
err += dx;
|
|
y += sy;
|
|
}
|
|
}
|
|
}
|