use crate::cache::TileCache;
use crate::projection::lonlat_to_tile;
use crate::source::TileKey;
use crate::sources::osm::{fetch_bytes, OSM_TILE_URL};
use crate::sources::util::format_url;
pub const MAX_BULK_FETCH_ZOOM: u8 = 16;
pub fn tile_range_for_bbox(
lon_min: f64,
lat_min: f64,
lon_max: f64,
lat_max: f64,
zoom: u8,
) -> (u32, u32, u32, u32) {
let z = zoom as f64;
let (x0, y_at_min_lat) = lonlat_to_tile(lon_min, lat_min, z);
let (x1, y_at_max_lat) = lonlat_to_tile(lon_max, lat_max, z);
let max_tile = (1u64 << zoom).saturating_sub(1) as u32;
let xs = (x0.floor() as i64, x1.floor() as i64);
let ys = (y_at_min_lat.floor() as i64, y_at_max_lat.floor() as i64);
let (x_min, x_max) = (xs.0.min(xs.1).max(0) as u32, xs.0.max(xs.1).max(0) as u32);
let (y_min, y_max) = (ys.0.min(ys.1).max(0) as u32, ys.0.max(ys.1).max(0) as u32);
(
x_min.min(max_tile),
x_max.min(max_tile),
y_min.min(max_tile),
y_max.min(max_tile),
)
}
pub fn keys_for_region(
lon_min: f64,
lat_min: f64,
lon_max: f64,
lat_max: f64,
zoom_min: u8,
zoom_max: u8,
) -> Vec<TileKey> {
let mut out = Vec::new();
for z in zoom_min..=zoom_max {
let (x_min, x_max, y_min, y_max) =
tile_range_for_bbox(lon_min, lat_min, lon_max, lat_max, z);
for x in x_min..=x_max {
for y in y_min..=y_max {
out.push(TileKey { x, y, z });
}
}
}
out
}
pub enum FetchOutcome {
Cached,
Fetched { bytes: usize },
Failed(String),
}
pub struct RegionConfig {
pub url_template: String,
pub user_agent: String,
pub sleep_ms: u64,
pub max_zoom: u8,
}
impl Default for RegionConfig {
fn default() -> Self {
Self {
url_template: OSM_TILE_URL.to_string(),
user_agent: "slint-mapping/0.1 (https://github.com/slint-rs/slint-mapping)".to_string(),
sleep_ms: 300,
max_zoom: MAX_BULK_FETCH_ZOOM,
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn region(
cache: &dyn TileCache,
lon_min: f64,
lat_min: f64,
lon_max: f64,
lat_max: f64,
zoom_min: u8,
zoom_max: u8,
config: &RegionConfig,
mut progress: impl FnMut(usize, usize, TileKey, &FetchOutcome),
) -> Result<usize, String> {
if zoom_max > config.max_zoom {
return Err(format!(
"zoom_max {zoom_max} exceeds the configured cap ({}); raise RegionConfig::max_zoom \
if you've cleared bulk download with the provider",
config.max_zoom
));
}
let keys = keys_for_region(lon_min, lat_min, lon_max, lat_max, zoom_min, zoom_max);
let total = keys.len();
let sleep = std::time::Duration::from_millis(config.sleep_ms);
for (i, key) in keys.iter().enumerate() {
let outcome = if cache.contains(*key) {
FetchOutcome::Cached
} else {
let url = format_url(&config.url_template, *key);
match fetch_bytes(&url, &config.user_agent) {
Ok(bytes) => {
let len = bytes.len();
match cache.put(*key, &bytes) {
Ok(()) => FetchOutcome::Fetched { bytes: len },
Err(e) => FetchOutcome::Failed(format!("cache put: {e}")),
}
}
Err(e) => FetchOutcome::Failed(e),
}
};
progress(i + 1, total, *key, &outcome);
if matches!(outcome, FetchOutcome::Fetched { .. }) {
std::thread::sleep(sleep);
}
}
Ok(total)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keys_for_region_world_z2_is_16_tiles() {
let keys = keys_for_region(-180.0, -85.0, 180.0, 85.0, 2, 2);
assert_eq!(keys.len(), 16);
}
#[test]
fn keys_for_region_single_zoom_london() {
let keys = keys_for_region(-0.5, 51.25, 0.3, 51.75, 12, 12);
assert!(
keys.len() > 50 && keys.len() < 150,
"got {} tiles",
keys.len()
);
assert!(keys.iter().all(|k| k.z == 12));
}
#[test]
fn keys_for_region_spans_zoom_range() {
let keys = keys_for_region(-1.0, 51.0, 1.0, 52.0, 4, 6);
let zooms: std::collections::HashSet<u8> = keys.iter().map(|k| k.z).collect();
assert_eq!(zooms, [4u8, 5, 6].into_iter().collect());
}
}