1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
mod geo_line_strings;
mod geo_points;

pub use geo_line_strings::GeoLineStringsVisualizer;
pub use geo_points::GeoPointsVisualizer;

/// Helper to track an area span in latitude and longitude.
#[derive(Debug, Clone)]
pub struct GeoSpan {
    pub min_latitude: f64,
    pub max_latitude: f64,
    pub min_longitude: f64,
    pub max_longitude: f64,
}

impl GeoSpan {
    pub fn from_lat_long(mut lat_lon: impl Iterator<Item = (f64, f64)>) -> Option<Self> {
        if let Some((lat, lon)) = lat_lon.next() {
            let mut span = Self {
                min_latitude: lat,
                max_latitude: lat,
                min_longitude: lon,
                max_longitude: lon,
            };

            for (lat, lon) in lat_lon {
                span.min_latitude = span.min_latitude.min(lat);
                span.max_latitude = span.max_latitude.max(lat);
                span.min_longitude = span.min_longitude.min(lon);
                span.max_longitude = span.max_longitude.max(lon);
            }

            Some(span)
        } else {
            None
        }
    }

    pub fn center(&self) -> walkers::Position {
        walkers::Position::from_lat_lon(
            (self.min_latitude + self.max_latitude) / 2.0,
            (self.min_longitude + self.max_longitude) / 2.0,
        )
    }

    pub fn zoom_for_screen_size(&self, screen_size: egui::Vec2) -> Option<f64> {
        // Thanks, Claude: https://claude.site/artifacts/cb4f7f53-07a6-4ad0-bce3-eee3cb7e3177

        if self.min_latitude == self.max_latitude || self.min_longitude == self.max_longitude {
            return None;
        }

        //TODO(ab): should use the actual tile size from the map provider (always 256 in practice)
        const TILE_SIZE: f64 = 256.0;

        // Convert latitude to y coordinate in mercator projection (scaled to 0..1)
        fn lat_to_y(lat: f64) -> f64 {
            let lat_rad = lat.to_radians();
            let y = (1.0 + lat_rad.tan().asinh() / std::f64::consts::PI) / 2.0;
            y.clamp(0.0, 1.0)
        }

        // Calculate ranges
        let lat_range = lat_to_y(self.max_latitude) - lat_to_y(self.min_latitude);
        let lon_range = (self.max_longitude - self.min_longitude) / 360.0;

        // Calculate the required number of tiles for both dimensions
        let tiles_x = lon_range * TILE_SIZE;
        let tiles_y = lat_range * TILE_SIZE;

        // Calculate zoom levels needed for both dimensions
        let zoom_x = (screen_size.x as f64 / tiles_x).ln() / 2.0_f64.ln();
        let zoom_y = (screen_size.y as f64 / tiles_y).ln() / 2.0_f64.ln();

        // Use the minimum zoom level to ensure the entire range fits
        Some(zoom_x.min(zoom_y))
    }
}

/// Extend a span to include another span, if any.
pub fn update_span(span: &mut Option<GeoSpan>, other: Option<GeoSpan>) {
    if let Some(other) = other {
        match span {
            Some(span) => {
                span.min_latitude = span.min_latitude.min(other.min_latitude);
                span.max_latitude = span.max_latitude.max(other.max_latitude);
                span.min_longitude = span.min_longitude.min(other.min_longitude);
                span.max_longitude = span.max_longitude.max(other.max_longitude);
            }
            None => *span = Some(other),
        }
    }
}

/// Convert a [`re_types::components::Radius`] to a [`re_renderer::Size`], considering scene units
/// as meters.
#[inline]
pub fn radius_to_size(
    radius: re_types::components::Radius,
    projector: &walkers::Projector,
    position: walkers::Position,
) -> re_renderer::Size {
    re_renderer::Size(
        radius
            .scene_units()
            .map(|radius_meter| projector.scale_pixel_per_meter(position) * radius_meter)
            .or(radius.ui_points())
            .unwrap_or_default(),
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_zoom_for_screen_size() {
        // smaller area
        let span = GeoSpan {
            min_latitude: 46.0,
            max_latitude: 47.0,
            min_longitude: 6.0,
            max_longitude: 7.0,
        };

        let zoom = span
            .zoom_for_screen_size(egui::Vec2::new(1024.0, 500.0))
            .unwrap();
        assert!(zoom >= 8.0 && zoom <= 9.0);

        // whole world
        let span = GeoSpan {
            min_latitude: -85.0,
            max_latitude: 85.0,
            min_longitude: -180.0,
            max_longitude: 180.0,
        };

        let zoom = span
            .zoom_for_screen_size(egui::Vec2::new(1024.0, 512.0))
            .unwrap();
        assert!(zoom >= 0.0 && zoom <= 2.0); // Should be very zoomed out

        // single point
        let span = GeoSpan {
            min_latitude: 46.0,
            max_latitude: 46.0,
            min_longitude: 6.0,
            max_longitude: 6.0,
        };
        assert_eq!(
            None,
            span.zoom_for_screen_size(egui::Vec2::new(1024.0, 512.0))
        );
    }
}