diff --git a/src/render.rs b/src/render.rs index 7b13907..01c947f 100644 --- a/src/render.rs +++ b/src/render.rs @@ -2,6 +2,7 @@ use crate::projection::OrthoProjection; use anyhow::{Context, Result}; use font8x8::{UnicodeFonts, BASIC_FONTS}; use plotters::prelude::*; +use std::collections::VecDeque; use std::path::Path; const IMG_WIDTH: u32 = 900; @@ -118,32 +119,58 @@ pub fn render_frame( map_area.fill(&LAND_BG).context("fill map area")?; - // Plot grid points + // Nearest-neighbour fill via multi-source BFS. + // + // Each data point seeds one pixel in a flat grid. BFS then propagates each + // value outward to all 4-connected NaN neighbours, so every reachable pixel + // ends up holding the value of the closest source point (by Manhattan distance). + // Complexity: O(map_w × map_h) — ~410 k array ops, no hashing. + let grid_w = map_w as usize; + let grid_h = map_h as usize; + let mut grid = vec![f32::NAN; grid_w * grid_h]; + let mut queue: VecDeque<(i32, i32)> = VecDeque::new(); + + // Seed the grid with projected data points. for i in 0..lats.len() { - let lat = lats[i] as f64; - let lon = lons[i] as f64; - - let Some((px, py)) = proj.project(lat, lon) else { continue }; - let Some((col, row)) = proj.to_pixel(px, py, map_w, map_h) else { continue }; - let cover = cloud_cover[i]; if cover.is_nan() { - continue; // missing value (bitmap) — keep background + continue; } - let color = match cloud_color(cover) { - Some(c) => c, - None => continue, // clear sky — keep land background - }; + let Some((px, py)) = proj.project(lats[i] as f64, lons[i] as f64) else { continue }; + let Some((col, row)) = proj.to_pixel(px, py, map_w, map_h) else { continue }; + let idx = row as usize * grid_w + col as usize; + if grid[idx].is_nan() { + grid[idx] = cover; + queue.push_back((col, row)); + } + } - // Paint a 2×2 block to avoid gaps between grid points - for dy in 0..2i32 { - for dx in 0..2i32 { - let c = col + dx; - let r = row + dy; - if c >= 0 && c < map_w as i32 && r >= 0 && r < map_h as i32 { - map_area.draw_pixel((c, r), &color).ok(); - } + // BFS flood fill. + const DIRS: [(i32, i32); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)]; + while let Some((col, row)) = queue.pop_front() { + let cover = grid[row as usize * grid_w + col as usize]; + for (dc, dr) in DIRS { + let nc = col + dc; + let nr = row + dr; + if nc < 0 || nc >= map_w as i32 || nr < 0 || nr >= map_h as i32 { + continue; } + let nidx = nr as usize * grid_w + nc as usize; + if grid[nidx].is_nan() { + grid[nidx] = cover; + queue.push_back((nc, nr)); + } + } + } + + // Paint the filled grid. + for row in 0..map_h as i32 { + for col in 0..map_w as i32 { + let cover = grid[row as usize * grid_w + col as usize]; + if let Some(color) = cloud_color(cover) { + map_area.draw_pixel((col, row), &color).ok(); + } + // NaN or clear sky: keep the land background } }