1use crate::geo::LonLat;
4use crate::projection::{from_pixels, to_pixels, TILE_SIZE};
5
6#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct View {
9 pub center: LonLat,
10 pub zoom: f64,
12 pub bearing: f64,
14 pub pitch: f64,
16}
17
18impl Default for View {
19 fn default() -> Self {
20 View {
21 center: LonLat::new(105.85, 21.02), zoom: 12.0,
23 bearing: 0.0,
24 pitch: 0.0,
25 }
26 }
27}
28
29impl View {
30 pub fn new(center: LonLat, zoom: f64) -> Self {
31 View {
32 center,
33 zoom,
34 bearing: 0.0,
35 pitch: 0.0,
36 }
37 }
38
39 pub fn set_pitch(&mut self, pitch: f64) {
41 self.pitch = pitch.clamp(0.0, 60.0);
42 }
43
44 pub fn set_bearing(&mut self, bearing: f64) {
46 self.bearing = ((bearing % 360.0) + 360.0) % 360.0;
47 }
48
49 pub fn scale(&self) -> f64 {
51 let z_floor = self.zoom.floor();
52 let frac = self.zoom - z_floor;
53 2f64.powf(frac)
54 }
55
56 pub fn project(&self, p: LonLat, viewport_w: f64, viewport_h: f64) -> (f64, f64) {
59 let z = self.zoom.floor() as u8;
60 let (cx, cy) = to_pixels(self.center.lon, self.center.lat, z);
61 let (px, py) = to_pixels(p.lon, p.lat, z);
62 let s = self.scale();
63 let mut dx = (px - cx) * s;
64 let mut dy = (py - cy) * s;
65 if self.bearing != 0.0 {
66 let r = self.bearing.to_radians();
67 let (sin, cos) = r.sin_cos();
68 let rx = dx * cos - dy * sin;
69 let ry = dx * sin + dy * cos;
70 dx = rx;
71 dy = ry;
72 }
73 if self.pitch != 0.0 {
75 dy *= self.pitch.to_radians().cos().max(0.3);
76 }
77 (viewport_w / 2.0 + dx, viewport_h / 2.0 + dy)
78 }
79
80 pub fn unproject(&self, sx: f64, sy: f64, viewport_w: f64, viewport_h: f64) -> LonLat {
82 let z = self.zoom.floor() as u8;
83 let (cx, cy) = to_pixels(self.center.lon, self.center.lat, z);
84 let s = self.scale();
85 let mut dx = (sx - viewport_w / 2.0) / s;
86 let mut dy = (sy - viewport_h / 2.0) / s;
87 if self.bearing != 0.0 {
88 let r = (-self.bearing).to_radians();
89 let (sin, cos) = r.sin_cos();
90 let rx = dx * cos - dy * sin;
91 let ry = dx * sin + dy * cos;
92 dx = rx;
93 dy = ry;
94 }
95 let (lon, lat) = from_pixels(cx + dx, cy + dy, z);
96 LonLat::new(lon, lat)
97 }
98
99 pub fn visible_tiles(&self, viewport_w: f64, viewport_h: f64) -> (u32, u32, u32, u32) {
101 let z = self.zoom.floor() as u8;
102 let n = 2u32.pow(z as u32);
103 let size = TILE_SIZE as f64 * self.scale();
104 let (cx, cy) = to_pixels(self.center.lon, self.center.lat, z);
105 let x0 = ((cx * self.scale() - viewport_w / 2.0) / size)
106 .floor()
107 .max(0.0) as u32;
108 let y0 = ((cy * self.scale() - viewport_h / 2.0) / size)
109 .floor()
110 .max(0.0) as u32;
111 let x1 = ((cx * self.scale() + viewport_w / 2.0) / size)
112 .floor()
113 .min((n - 1) as f64) as u32;
114 let y1 = ((cy * self.scale() + viewport_h / 2.0) / size)
115 .floor()
116 .min((n - 1) as f64) as u32;
117 (x0, y0, x1, y1)
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn center_projects_to_viewport_center() {
127 let v = View::default();
128 let (sx, sy) = v.project(v.center, 800.0, 600.0);
129 assert!((sx - 400.0).abs() < 1e-6);
130 assert!((sy - 300.0).abs() < 1e-6);
131 }
132
133 #[test]
134 fn project_unproject_roundtrip() {
135 let v = View::new(LonLat::new(105.85, 21.02), 13.5);
136 let p = LonLat::new(105.90, 21.05);
137 let (sx, sy) = v.project(p, 800.0, 600.0);
138 let back = v.unproject(sx, sy, 800.0, 600.0);
139 assert!((back.lon - p.lon).abs() < 1e-6, "lon");
140 assert!((back.lat - p.lat).abs() < 1e-6, "lat");
141 }
142
143 #[test]
144 fn pitch_clamped() {
145 let mut v = View::default();
146 v.set_pitch(90.0);
147 assert_eq!(v.pitch, 60.0);
148 v.set_pitch(-10.0);
149 assert_eq!(v.pitch, 0.0);
150 }
151
152 #[test]
153 fn bearing_normalized() {
154 let mut v = View::default();
155 v.set_bearing(370.0);
156 assert!((v.bearing - 10.0).abs() < 1e-9);
157 v.set_bearing(-30.0);
158 assert!((v.bearing - 330.0).abs() < 1e-9);
159 }
160}