use crate::annotation_system::annotation_support::MercatorRectangle;
use crate::gui_system::internal_math::{TILE_SIZE_PIXEL, TileCoordinates};
use crate::gui_system::latitude_longitude::LatitudeLongitude;
use crate::gui_system::map_widget_system::MapWidgetSystem;
use iced::Size;
const MAXIMUM_AREA_ZOOM_LEVEL: f32 = 17.0;
#[derive(Debug, Clone, Copy)]
pub(crate) struct FocalPoint {
pub(crate) position: LatitudeLongitude,
pub(crate) continuous_zoom_level: f32,
}
#[derive(Debug, Clone)]
pub enum FocusTarget {
Position(LatitudeLongitude),
Area {
bounds: MercatorRectangle,
fill: f32,
},
}
impl From<LatitudeLongitude> for FocusTarget {
fn from(position: LatitudeLongitude) -> Self {
FocusTarget::Position(position)
}
}
impl FocusTarget {
pub fn around(center: LatitudeLongitude, radius_meter: f64) -> Self {
FocusTarget::Area {
bounds: MercatorRectangle::create_around(center, radius_meter, 0.0),
fill: MapWidgetSystem::DEFAULT_FOCUS_FILL,
}
}
}
#[derive(Debug, Clone)]
pub(crate) enum FocusRequest {
None,
Republish,
Target(FocusTarget),
}
impl FocusRequest {
pub(crate) fn is_pending(&self) -> bool {
!matches!(self, FocusRequest::None)
}
pub(crate) fn republish(&mut self) {
if let FocusRequest::None = self {
*self = FocusRequest::Republish;
}
}
pub(crate) fn focal_point(&self, current: FocalPoint, view_size: Size) -> FocalPoint {
match self {
FocusRequest::Target(target) => resolve_target(target, current, view_size),
FocusRequest::None | FocusRequest::Republish => current,
}
}
}
pub(crate) fn resolve_target(
target: &FocusTarget,
current: FocalPoint,
view_size: Size,
) -> FocalPoint {
let (bounds, fill) = match target {
FocusTarget::Position(position) => {
return FocalPoint {
position: *position,
..current
};
}
FocusTarget::Area { bounds, fill } => (bounds, *fill as f64),
};
let top_left =
LatitudeLongitude::new(bounds.min_max_lat.1, bounds.min_max_long.0).get_tile_coordinates(0);
let bottom_right =
LatitudeLongitude::new(bounds.min_max_lat.0, bounds.min_max_long.1).get_tile_coordinates(0);
let position = TileCoordinates {
x: (top_left.x + bottom_right.x) * 0.5,
y: (top_left.y + bottom_right.y) * 0.5,
zoom: 0,
}
.into();
let padding = 2.0 * bounds.pixel_padding;
let level = |extent: f64, view_extent: f64| {
let available = (view_extent * fill - padding).max(1.0);
let extent = extent * TILE_SIZE_PIXEL as f64;
if extent <= 0.0 {
f64::INFINITY
} else {
f64::log2(available / extent)
}
};
let zoom = level(bottom_right.x - top_left.x, view_size.width as f64)
.min(level(bottom_right.y - top_left.y, view_size.height as f64));
FocalPoint {
position,
continuous_zoom_level: (zoom as f32).clamp(0.0, MAXIMUM_AREA_ZOOM_LEVEL),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gui_system::map_widget::focus_animation::pixel_offset;
fn trier() -> FocalPoint {
FocalPoint {
position: LatitudeLongitude::new(49.75, 6.63),
continuous_zoom_level: 12.0,
}
}
const VIEW: Size = Size {
width: 594.0,
height: 492.0,
};
fn area(half_lat: f64, half_long: f64, fill: f32) -> FocusTarget {
FocusTarget::Area {
bounds: MercatorRectangle {
min_max_lat: (49.75 - half_lat, 49.75 + half_lat),
min_max_long: (6.63 - half_long, 6.63 + half_long),
pixel_padding: 0.0,
},
fill,
}
}
fn extent_in_pixels(target: &FocusTarget, view: &FocalPoint) -> (f64, f64) {
let FocusTarget::Area { bounds, .. } = target else {
panic!("an area is expected");
};
let corner = |lat, long| LatitudeLongitude::new(lat, long);
let at_view = FocalPoint {
position: corner(bounds.min_max_lat.1, bounds.min_max_long.0),
..*view
};
let (dx, dy) = pixel_offset(
&at_view,
&corner(bounds.min_max_lat.0, bounds.min_max_long.1),
);
(dx.abs(), dy.abs())
}
#[test]
fn wide_area_fits_the_width() {
let target = area(0.01, 1.0, 0.8);
let end = resolve_target(&target, trier(), VIEW);
let (width, height) = extent_in_pixels(&target, &end);
assert!(
(width - VIEW.width as f64 * 0.8).abs() < 0.5,
"width {width}"
);
assert!(height < VIEW.height as f64 * 0.8);
}
#[test]
fn tall_area_fits_the_height() {
let target = area(1.0, 0.01, 0.8);
let end = resolve_target(&target, trier(), VIEW);
let (width, height) = extent_in_pixels(&target, &end);
assert!(
(height - VIEW.height as f64 * 0.8).abs() < 0.5,
"height {height}"
);
assert!(width < VIEW.width as f64 * 0.8);
assert!(end.position.latitude > 49.75);
}
#[test]
fn circle_fills_the_shorter_side() {
let target = FocusTarget::around(LatitudeLongitude::new(49.75, 6.63), 200.0);
let end = resolve_target(&target, trier(), VIEW);
let (width, height) = extent_in_pixels(&target, &end);
let expected = VIEW.height as f64 * MapWidgetSystem::DEFAULT_FOCUS_FILL as f64;
assert!((height - expected).abs() < 0.5, "height {height}");
assert!((width - height).abs() < 0.5, "width {width}");
assert!((end.position.latitude - 49.75).abs() < 1e-6);
assert!((end.position.longitude - 6.63).abs() < 1e-9);
}
#[test]
fn tiny_area() {
let point = resolve_target(&area(0.0, 0.0, 0.8), trier(), VIEW);
assert_eq!(point.continuous_zoom_level, MAXIMUM_AREA_ZOOM_LEVEL);
}
}