diff --git a/src/main.rs b/src/main.rs index 2a69a85..914e792 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,6 +34,18 @@ struct Args { /// Disable OpenStreetMap base layer (use flat green background) #[arg(long)] no_basemap: bool, + + /// Map centre latitude in degrees (default: 52.56) + #[arg(long, value_name = "LAT", default_value_t = render::DEFAULT_CENTER_LAT)] + center_lat: f64, + + /// Map centre longitude in degrees (default: 13.08) + #[arg(long, value_name = "LON", default_value_t = render::DEFAULT_CENTER_LON)] + center_lon: f64, + + /// OSM tile zoom level (default: 10) + #[arg(long, value_name = "ZOOM", default_value_t = render::DEFAULT_OSM_ZOOM)] + zoom: u8, } fn main() -> Result<()> { @@ -134,12 +146,12 @@ fn main() -> Result<()> { let basemap = if args.no_basemap { None } else { - let proj = render::make_projection(); + let proj = render::make_projection(args.center_lat, args.center_lon, args.zoom); let tile_cache = tiles::fetch_tiles( &proj, render::MAP_W, render::MAP_H, - render::OSM_ZOOM, + args.zoom, &PathBuf::from(CACHE_DIR), )?; Some(tiles::rasterize_basemap( @@ -153,12 +165,12 @@ fn main() -> Result<()> { let labels = if args.no_basemap { None } else { - let proj = render::make_projection(); + let proj = render::make_projection(args.center_lat, args.center_lon, args.zoom); let label_cache = tiles::fetch_label_tiles( &proj, render::MAP_W, render::MAP_H, - render::OSM_ZOOM, + args.zoom, &PathBuf::from(CACHE_DIR), )?; Some(tiles::rasterize_labels( @@ -200,7 +212,7 @@ fn main() -> Result<()> { let out_name = format!("clct_{:06}.png", step + 1); let out_path = cache_dir.join(&out_name); - render::render_frame(&out_path, &lats, &lons, &cloud, &title, basemap.as_deref(), labels.as_deref()) + render::render_frame(&out_path, &lats, &lons, &cloud, &title, basemap.as_deref(), labels.as_deref(), args.center_lat, args.center_lon, args.zoom) .with_context(|| format!("Rendering frame {}", step))?; eprintln!(" [{:02}/{:02}] {}", step + 1, args.prediction_hours, out_name); diff --git a/src/render.rs b/src/render.rs index 847e6f3..d914296 100644 --- a/src/render.rs +++ b/src/render.rs @@ -15,27 +15,27 @@ pub const MARGIN: u32 = 8; pub const MAP_W: u32 = IMG_WIDTH - LEGEND_W - MARGIN * 2; pub const MAP_H: u32 = IMG_HEIGHT - TITLE_H - MARGIN; -/// Map image centre and OSM zoom level. -pub const CENTER_LAT: f64 = 52.56; -pub const CENTER_LON: f64 = 13.08; -/// OSM tile zoom level. The viewport half-extents are derived from this so that +/// Default map image centre and OSM zoom level. +pub const DEFAULT_CENTER_LAT: f64 = 52.56; +pub const DEFAULT_CENTER_LON: f64 = 13.08; +/// Default OSM tile zoom level. The viewport half-extents are derived from this so that /// one tile pixel maps to exactly one output pixel, eliminating interpolation /// artefacts on the OSM base layer. -pub const OSM_ZOOM: u8 = 10; +pub const DEFAULT_OSM_ZOOM: u8 = 10; /// Build the orthographic projection whose viewport is sized so that the -/// output resolution matches OSM tiles at `OSM_ZOOM` exactly. +/// output resolution matches OSM tiles at `zoom` exactly. /// /// At zoom `z`, Web-Mercator tiles cover `360 / 2^z` degrees of longitude per /// tile, each 256 px wide. Near the centre latitude `φ`, 1° of longitude ≈ /// `cos φ` arc-degrees, so the arc-degree scale is: /// `pixels_per_arc_deg = 256 · 2^z / (360 · cos φ)` /// Half-extents follow directly from the map pixel dimensions. -pub fn make_projection() -> OrthoProjection { - let scale = f64::from(256u32 << OSM_ZOOM as u32) / (360.0 * CENTER_LAT.to_radians().cos()); +pub fn make_projection(center_lat: f64, center_lon: f64, zoom: u8) -> OrthoProjection { + let scale = f64::from(256u32 << zoom as u32) / (360.0 * center_lat.to_radians().cos()); let half_w = MAP_W as f64 / (2.0 * scale); let half_h = MAP_H as f64 / (2.0 * scale); - OrthoProjection::new(CENTER_LAT, CENTER_LON, half_w, half_h) + OrthoProjection::new(center_lat, center_lon, half_w, half_h) } const BACKGROUND: RGBColor = RGBColor(240, 248, 255); // pale sky-blue @@ -179,8 +179,11 @@ pub fn render_frame( title: &str, basemap: Option<&[[u8; 3]]>, labels: Option<&[[u8; 4]]>, + center_lat: f64, + center_lon: f64, + zoom: u8, ) -> Result<()> { - let proj = make_projection(); + let proj = make_projection(center_lat, center_lon, zoom); let root = BitMapBackend::new(output_path, (IMG_WIDTH, IMG_HEIGHT)).into_drawing_area(); root.fill(&BACKGROUND).context("fill background")?;