use crate::gui_system::distance_units::{DistanceUnits, METRES_PER_FOOT, METRES_PER_MILE};
use crate::gui_system::internal_math::TILE_SIZE_PIXEL;
use crate::gui_system::latitude_longitude::EARTH_CIRCUMFERENCE;
use crate::gui_system::map_widget::map_widget_components::{FONT_SIZE, TEXT_COLOR};
use crate::gui_system::map_widget::map_widget_support::{HALO_COLOR, fill_text_with_halo};
use iced::advanced::graphics::geometry::Frame;
use iced::alignment::Vertical;
use iced::widget::canvas::{Path, Stroke, Text, stroke};
use iced::{Color, Point, Renderer, Size};
const MAXIMUM_LENGTH: f32 = 120.0;
const MARGIN: f32 = 10.0;
const TICK_HEIGHT: f32 = 6.0;
const LABEL_GAP: f32 = 6.0;
const BAR_COLOR: Color = TEXT_COLOR;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ScaleBar {
pub length: f32,
pub label: String,
}
pub(crate) fn metres_per_pixel(latitude: f64, zoom: f32) -> f64 {
let world_size = TILE_SIZE_PIXEL as f64 * f64::exp2(zoom as f64);
EARTH_CIRCUMFERENCE * latitude.to_radians().cos() / world_size
}
pub(crate) fn scale_bar(latitude: f64, zoom: f32, units: DistanceUnits) -> Option<ScaleBar> {
let per_pixel = metres_per_pixel(latitude, zoom);
if per_pixel <= 0.0 {
return None;
}
let maximum = per_pixel * MAXIMUM_LENGTH as f64;
let (unit_metres, unit_name) = match units {
DistanceUnits::Metric if maximum >= 1000.0 => (1000.0, "km"),
DistanceUnits::Metric => (1.0, "m"),
DistanceUnits::Imperial if maximum >= METRES_PER_MILE => (METRES_PER_MILE, "mi"),
DistanceUnits::Imperial => (METRES_PER_FOOT, "ft"),
};
let distance = round_distance(maximum / unit_metres)?;
Some(ScaleBar {
length: (distance * unit_metres / per_pixel) as f32,
label: format!("{distance} {unit_name}"),
})
}
fn round_distance(maximum: f64) -> Option<f64> {
let magnitude = f64::powi(10.0, maximum.log10().floor() as i32);
[5.0, 2.0, 1.0]
.into_iter()
.map(|factor| factor * magnitude)
.find(|&distance| distance <= maximum)
}
pub(crate) fn draw_scale_bar(frame: &mut Frame<Renderer>, size: Size, bar: &ScaleBar) {
let left = MARGIN;
let right = MARGIN + bar.length;
let bottom = size.height - MARGIN;
let top = bottom - TICK_HEIGHT;
let shape = Path::new(|builder| {
builder.move_to(Point::new(left, top));
builder.line_to(Point::new(left, bottom));
builder.line_to(Point::new(right, bottom));
builder.line_to(Point::new(right, top));
});
for (width, color) in [(4.0, HALO_COLOR), (1.5, BAR_COLOR)] {
frame.stroke(
&shape,
Stroke {
width,
style: stroke::Style::Solid(color),
..Stroke::default()
},
);
}
fill_text_with_halo(
frame,
Text {
content: bar.label.clone(),
position: Point::new(right + LABEL_GAP, bottom - TICK_HEIGHT * 0.5),
color: BAR_COLOR,
size: FONT_SIZE.into(),
align_y: Vertical::Center,
..Default::default()
},
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn equator_at_zoom_zero_is_the_whole_earth() {
let per_pixel = metres_per_pixel(0.0, 0.0);
assert!((per_pixel * TILE_SIZE_PIXEL as f64 - EARTH_CIRCUMFERENCE).abs() < 1e-3);
}
#[test]
fn scale_shrinks_with_latitude() {
let ratio = metres_per_pixel(60.0, 12.0) / metres_per_pixel(0.0, 12.0);
assert!((ratio - 0.5).abs() < 1e-9);
}
#[test]
fn bar_picks_a_round_distance_that_fits() {
let bar = scale_bar(49.75, 12.0, DistanceUnits::Metric).unwrap();
assert_eq!(bar.label, "2 km");
assert!(bar.length > 60.0 && bar.length <= MAXIMUM_LENGTH);
let bar = scale_bar(49.75, 19.0, DistanceUnits::Metric).unwrap();
assert_eq!(bar.label, "20 m");
assert!(bar.length <= MAXIMUM_LENGTH);
}
#[test]
fn imperial_bar_rounds_in_feet_and_miles() {
let bar = scale_bar(49.75, 12.0, DistanceUnits::Imperial).unwrap();
assert_eq!(bar.label, "1 mi");
assert!(bar.length > 60.0 && bar.length <= MAXIMUM_LENGTH);
let bar = scale_bar(49.75, 13.0, DistanceUnits::Imperial).unwrap();
assert_eq!(bar.label, "2000 ft");
assert!(bar.length > 40.0 && bar.length <= MAXIMUM_LENGTH);
}
}