use crate::geo::LonLat;
use crate::projection::{from_pixels, to_pixels, TILE_SIZE};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct View {
pub center: LonLat,
pub zoom: f64,
pub bearing: f64,
pub pitch: f64,
}
impl Default for View {
fn default() -> Self {
View {
center: LonLat::new(105.85, 21.02), zoom: 12.0,
bearing: 0.0,
pitch: 0.0,
}
}
}
impl View {
pub fn new(center: LonLat, zoom: f64) -> Self {
View {
center,
zoom,
bearing: 0.0,
pitch: 0.0,
}
}
pub fn set_pitch(&mut self, pitch: f64) {
self.pitch = pitch.clamp(0.0, 60.0);
}
pub fn set_bearing(&mut self, bearing: f64) {
self.bearing = ((bearing % 360.0) + 360.0) % 360.0;
}
pub fn scale(&self) -> f64 {
let z_floor = self.zoom.floor();
let frac = self.zoom - z_floor;
2f64.powf(frac)
}
pub fn project(&self, p: LonLat, viewport_w: f64, viewport_h: f64) -> (f64, f64) {
let z = self.zoom.floor() as u8;
let (cx, cy) = to_pixels(self.center.lon, self.center.lat, z);
let (px, py) = to_pixels(p.lon, p.lat, z);
let s = self.scale();
let mut dx = (px - cx) * s;
let mut dy = (py - cy) * s;
if self.bearing != 0.0 {
let r = self.bearing.to_radians();
let (sin, cos) = r.sin_cos();
let rx = dx * cos - dy * sin;
let ry = dx * sin + dy * cos;
dx = rx;
dy = ry;
}
if self.pitch != 0.0 {
dy *= self.pitch.to_radians().cos().max(0.3);
}
(viewport_w / 2.0 + dx, viewport_h / 2.0 + dy)
}
pub fn unproject(&self, sx: f64, sy: f64, viewport_w: f64, viewport_h: f64) -> LonLat {
let z = self.zoom.floor() as u8;
let (cx, cy) = to_pixels(self.center.lon, self.center.lat, z);
let s = self.scale();
let mut dx = (sx - viewport_w / 2.0) / s;
let mut dy = (sy - viewport_h / 2.0) / s;
if self.bearing != 0.0 {
let r = (-self.bearing).to_radians();
let (sin, cos) = r.sin_cos();
let rx = dx * cos - dy * sin;
let ry = dx * sin + dy * cos;
dx = rx;
dy = ry;
}
let (lon, lat) = from_pixels(cx + dx, cy + dy, z);
LonLat::new(lon, lat)
}
pub fn visible_tiles(&self, viewport_w: f64, viewport_h: f64) -> (u32, u32, u32, u32) {
let z = self.zoom.floor() as u8;
let n = 2u32.pow(z as u32);
let size = TILE_SIZE as f64 * self.scale();
let (cx, cy) = to_pixels(self.center.lon, self.center.lat, z);
let x0 = ((cx * self.scale() - viewport_w / 2.0) / size)
.floor()
.max(0.0) as u32;
let y0 = ((cy * self.scale() - viewport_h / 2.0) / size)
.floor()
.max(0.0) as u32;
let x1 = ((cx * self.scale() + viewport_w / 2.0) / size)
.floor()
.min((n - 1) as f64) as u32;
let y1 = ((cy * self.scale() + viewport_h / 2.0) / size)
.floor()
.min((n - 1) as f64) as u32;
(x0, y0, x1, y1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn center_projects_to_viewport_center() {
let v = View::default();
let (sx, sy) = v.project(v.center, 800.0, 600.0);
assert!((sx - 400.0).abs() < 1e-6);
assert!((sy - 300.0).abs() < 1e-6);
}
#[test]
fn project_unproject_roundtrip() {
let v = View::new(LonLat::new(105.85, 21.02), 13.5);
let p = LonLat::new(105.90, 21.05);
let (sx, sy) = v.project(p, 800.0, 600.0);
let back = v.unproject(sx, sy, 800.0, 600.0);
assert!((back.lon - p.lon).abs() < 1e-6, "lon");
assert!((back.lat - p.lat).abs() < 1e-6, "lat");
}
#[test]
fn pitch_clamped() {
let mut v = View::default();
v.set_pitch(90.0);
assert_eq!(v.pitch, 60.0);
v.set_pitch(-10.0);
assert_eq!(v.pitch, 0.0);
}
#[test]
fn bearing_normalized() {
let mut v = View::default();
v.set_bearing(370.0);
assert!((v.bearing - 10.0).abs() < 1e-9);
v.set_bearing(-30.0);
assert!((v.bearing - 330.0).abs() < 1e-9);
}
}