use super::TILE_EXTENT;
use crate::ext::MercatorExt;
use crate::geo::GeoFeature;
use geo::{Area, Euclidean, Length};
use geo_types::Geometry;
use versatiles_core::WORLD_SIZE;
#[must_use]
pub fn auto_max_zoom(features_wgs84: &[GeoFeature]) -> u8 {
let projected: Vec<GeoFeature> = features_wgs84
.iter()
.map(|f| GeoFeature {
id: f.id.clone(),
geometry: f.geometry.clone().to_mercator(),
properties: f.properties.clone(),
})
.collect();
auto_max_zoom_projected(&projected)
}
pub(super) fn auto_max_zoom_projected(features_mercator: &[GeoFeature]) -> u8 {
const MAX_ZOOM: u8 = 14;
const TARGET_PX: f64 = 4.0;
let mut sizes: Vec<f64> = features_mercator
.iter()
.filter_map(|f| feature_size_mercator(&f.geometry))
.filter(|s| s.is_finite() && *s > 0.0)
.collect();
if sizes.is_empty() {
return MAX_ZOOM;
}
sizes.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median = sizes[sizes.len() / 2];
let zoom_f = (TARGET_PX * WORLD_SIZE / (median * f64::from(TILE_EXTENT))).log2();
if !zoom_f.is_finite() {
return MAX_ZOOM;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let z = zoom_f.round().clamp(0.0, f64::from(MAX_ZOOM)) as u8;
z
}
fn feature_size_mercator(g: &Geometry<f64>) -> Option<f64> {
match g {
Geometry::LineString(ls) => Some(Euclidean.length(ls)),
Geometry::MultiLineString(ml) => Some(Euclidean.length(ml)),
Geometry::Polygon(p) => Some(p.unsigned_area().sqrt()),
Geometry::MultiPolygon(mp) => Some(mp.unsigned_area().sqrt()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use geo_types::{LineString, Point, Polygon};
fn ls_feature(coords: Vec<[f64; 2]>) -> GeoFeature {
GeoFeature::new(Geometry::LineString(LineString::from(coords)))
}
fn point_feature(x: f64, y: f64) -> GeoFeature {
GeoFeature::new(Geometry::Point(Point::new(x, y)))
}
fn square_polygon_feature(side_meters: f64) -> GeoFeature {
let s = side_meters;
let exterior = LineString::from(vec![[0.0, 0.0], [s, 0.0], [s, s], [0.0, s], [0.0, 0.0]]);
GeoFeature::new(Geometry::Polygon(Polygon::new(exterior, vec![])))
}
#[test]
fn empty_input_returns_max_zoom() {
assert_eq!(auto_max_zoom(&[]), 14);
}
#[test]
fn point_only_input_returns_max_zoom() {
let features = vec![point_feature(0.0, 0.0), point_feature(13.4, 52.5)];
assert_eq!(auto_max_zoom(&features), 14);
}
#[test]
fn very_large_features_pick_zoom_zero() {
let big = square_polygon_feature(WORLD_SIZE);
assert_eq!(auto_max_zoom_projected(&[big]), 0);
}
#[test]
fn tiny_features_pick_max_zoom() {
let tiny = square_polygon_feature(1.0);
assert_eq!(auto_max_zoom_projected(&[tiny]), 14);
}
#[test]
fn skips_non_finite_and_zero_sizes() {
let zero_len = ls_feature(vec![[0.0, 0.0], [0.0, 0.0]]); let one_m = ls_feature(vec![[0.0, 0.0], [1.0, 0.0]]); let result = auto_max_zoom_projected(&[zero_len, one_m]);
assert_eq!(result, 14);
}
}