use cgmath::{Matrix3, Point3, Transform, vec3}; use coord::{LatLonRad, ScreenCoord, TextureRect, TileCoord}; use map_view::MapView; use std::collections::HashSet; use std::f64::consts::{PI, FRAC_1_PI}; use std::f64; #[derive(Clone, Debug)] pub struct VisibleTile { pub tile: TileCoord, } impl From for VisibleTile { fn from(tc: TileCoord) -> Self { VisibleTile { tile: tc, } } } #[derive(Clone, Debug)] pub struct TexturedVisibleTile { pub tile_coord: TileCoord, pub tex_rect: TextureRect, pub tex_minmax: TextureRect, } #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] pub enum TileNeighbor { Coord(TileCoord), NorthPole, SouthPole, } /// Tile neighbors using sphere topology pub fn tile_neighbors(origin: TileCoord, result: &mut Vec) { result.clear(); let zoom_level_tiles = TileCoord::get_zoom_level_tiles(origin.zoom); if origin.y < 0 || origin.y >= zoom_level_tiles { // Tile is out of bounds return; } // Normalize x coordinate let origin = TileCoord { zoom: origin.zoom, x: ((origin.x % zoom_level_tiles) + zoom_level_tiles) % zoom_level_tiles, y: origin.y, }; match (origin.zoom, origin.y) { (0, _) => {}, (1, _) => { result.extend(&[ TileNeighbor::Coord(TileCoord::new( origin.zoom, (origin.x + 1) % zoom_level_tiles, origin.y) ), TileNeighbor::Coord(TileCoord::new( origin.zoom, origin.x, (origin.y + 1) % zoom_level_tiles), ), ]); }, (_, 0) => { result.extend(&[ TileNeighbor::NorthPole, TileNeighbor::Coord(TileCoord::new( origin.zoom, origin.x, origin.y + 1, )), TileNeighbor::Coord(TileCoord::new( origin.zoom, (origin.x + 1) % zoom_level_tiles, origin.y, )), TileNeighbor::Coord(TileCoord::new( origin.zoom, (origin.x + zoom_level_tiles - 1) % zoom_level_tiles, origin.y, )), ]); }, (_, y) if y == zoom_level_tiles - 1 => { result.extend(&[ TileNeighbor::SouthPole, TileNeighbor::Coord(TileCoord::new( origin.zoom, origin.x, origin.y - 1, )), TileNeighbor::Coord(TileCoord::new( origin.zoom, (origin.x + 1) % zoom_level_tiles, origin.y, )), TileNeighbor::Coord(TileCoord::new( origin.zoom, (origin.x + zoom_level_tiles - 1) % zoom_level_tiles, origin.y, )), ]); }, _ => { result.extend(&[ TileNeighbor::Coord(TileCoord::new( origin.zoom, origin.x, origin.y + 1, )), TileNeighbor::Coord(TileCoord::new( origin.zoom, origin.x, origin.y - 1, )), TileNeighbor::Coord(TileCoord::new( origin.zoom, (origin.x + 1) % zoom_level_tiles, origin.y, )), TileNeighbor::Coord(TileCoord::new( origin.zoom, (origin.x + zoom_level_tiles - 1) % zoom_level_tiles, origin.y, )), ]); }, } } #[derive(Clone, Debug)] pub struct OrthograficView { } impl OrthograficView { /// Returns true if the rendering covers the whole viewport. pub fn covers_viewport(map_view: &MapView) -> bool { let sphere_diameter = 2.0f64.powf(map_view.zoom) * (f64::consts::FRAC_1_PI * f64::from(map_view.tile_size)); // Add a little safety margin (the constant factor) since the rendered globe is not a // perfect sphere and its screen area is underestimated by the tesselation. map_view.width.hypot(map_view.height) < sphere_diameter * 0.9 } /// Returns the tile zoom value that is used for rendering with the current zoom. //TODO Insert real implementation. Add TileCoord parameter -> lower resolution at the poles pub fn tile_zoom(map_view: &MapView) -> u32 { (map_view.zoom + map_view.tile_zoom_offset).floor().max(0.0) as u32 } //TODO Return the transformation matrix that is used here to avoid redundant calculation. /// Returns a `Vec` of all tiles that are visible in the current viewport. pub fn visible_tiles(map_view: &MapView) -> Vec { let uzoom = Self::tile_zoom(map_view); match uzoom { 0 => return vec![TileCoord::new(0, 0, 0).into()], 1 => { // return every tile return vec![ TileCoord::new(1, 0, 0).into(), TileCoord::new(1, 0, 1).into(), TileCoord::new(1, 1, 0).into(), TileCoord::new(1, 1, 1).into(), ]}, _ => {}, } let center_tile = map_view.center.on_tile_at_zoom(uzoom).nearest_valid(); let transform = Self::transformation_matrix(map_view); let tile_is_visible = |tc: TileCoord| -> bool { let nw = tc.latlon_rad_north_west(); let se = tc.latlon_rad_south_east(); let vertices = [ transform.transform_point(nw.to_sphere_point3()), transform.transform_point(se.to_sphere_point3()), transform.transform_point(LatLonRad::new(nw.lat, se.lon).to_sphere_point3()), transform.transform_point(LatLonRad::new(se.lat, nw.lon).to_sphere_point3()), ]; if vertices.iter().all(|v| v.z > 0.0) { // Tile is on the backside of the sphere false } else { // Check bounding box of vertices against screen. //TODO Create true bounding box of tile that also accounts for curved borders. vertices.iter().fold(false, |acc, v| acc || v.x >= -1.0) && vertices.iter().fold(false, |acc, v| acc || v.x <= 1.0) && vertices.iter().fold(false, |acc, v| acc || v.y >= -1.0) && vertices.iter().fold(false, |acc, v| acc || v.y <= 1.0) } }; let mut tiles = vec![center_tile.into()]; let mut stack: Vec = vec![]; tile_neighbors(center_tile, &mut stack); let mut visited: HashSet = HashSet::new(); visited.insert(TileNeighbor::Coord(center_tile)); visited.extend(stack.iter()); let mut neighbors = Vec::with_capacity(4); while let Some(tn) = stack.pop() { if let TileNeighbor::Coord(tc) = tn { if tile_is_visible(tc) { tiles.push(tc.into()); tile_neighbors(tc, &mut neighbors); for tn in &neighbors { if !visited.contains(tn) { visited.insert(*tn); stack.push(*tn); } } } } } tiles } pub fn diameter_physical_pixels(map_view: &MapView) -> f64 { 2.0f64.powf(map_view.zoom) * (FRAC_1_PI * f64::from(map_view.tile_size)) } pub fn transformation_matrix(map_view: &MapView) -> Matrix3 { let (scale_x, scale_y) = { let diam = Self::diameter_physical_pixels(map_view); (diam / map_view.width, diam / map_view.height) }; let scale_mat: Matrix3 = Matrix3::from_cols( vec3(scale_x, 0.0, 0.0), vec3(0.0, scale_y, 0.0), vec3(0.0, 0.0, 1.0), ); let center_latlon = map_view.center.to_latlon_rad(); let rot_mat_x: Matrix3 = { let alpha = center_latlon.lon + (PI * 0.5); let cosa = alpha.cos(); let sina = alpha.sin(); Matrix3::from_cols( vec3(cosa, 0.0, -sina), vec3(0.0, 1.0, 0.0), vec3(sina, 0.0, cosa), ) }; let rot_mat_y: Matrix3 = { let alpha = -center_latlon.lat; let cosa = alpha.cos(); let sina = alpha.sin(); Matrix3::from_cols( vec3(1.0, 0.0, 0.0), vec3(0.0, cosa, sina), vec3(0.0, -sina, cosa), ) }; Transform::>::concat( &scale_mat, &Transform::>::concat(&rot_mat_y, &rot_mat_x) ) } // Returns the inverse rotation matrix of the given view. pub fn inv_rotation_matrix(map_view: &MapView) -> Matrix3 { let center_latlon = map_view.center.to_latlon_rad(); let rot_mat_x: Matrix3 = { let alpha = -center_latlon.lon - (PI * 0.5); let cosa = alpha.cos(); let sina = alpha.sin(); Matrix3::from_cols( vec3(cosa, 0.0, -sina), vec3(0.0, 1.0, 0.0), vec3(sina, 0.0, cosa), ) }; let rot_mat_y: Matrix3 = { let alpha = center_latlon.lat; let cosa = alpha.cos(); let sina = alpha.sin(); Matrix3::from_cols( vec3(1.0, 0.0, 0.0), vec3(0.0, cosa, sina), vec3(0.0, -sina, cosa), ) }; Transform::>::concat(&rot_mat_x, &rot_mat_y) } // Returns the coordinates of the location that is nearest to the given `ScreenCoord`. pub fn screen_coord_to_sphere_point(map_view: &MapView, screen_coord: ScreenCoord) -> Point3 { // Point on unit sphere let sphere_point = { let recip_radius = 2.0 * Self::diameter_physical_pixels(map_view).recip(); let sx = (screen_coord.x - map_view.width * 0.5) * recip_radius; let sy = (screen_coord.y - map_view.height * 0.5) * -recip_radius; let t = 1.0 - sx * sx - sy * sy; if t >= 0.0 { // screen_coord is on the sphere Point3::new( sx, sy, -(t.sqrt()), ) } else { // screen_coord is outside of sphere -> pick nearest. let scale = sx.hypot(sy).recip(); Point3::new( sx * scale, sy * scale, 0.0, ) } }; // Rotate let inv_trans = Self::inv_rotation_matrix(map_view); inv_trans.transform_point(sphere_point) } // Returns the coordinates of the location that is nearest to the given `ScreenCoord`. pub fn screen_coord_to_latlonrad(map_view: &MapView, screen_coord: ScreenCoord) -> LatLonRad { let p = Self::screen_coord_to_sphere_point(map_view, screen_coord); // Transform to latitude, longitude LatLonRad::new(p.y.asin(), p.z.atan2(p.x)) } /// Change zoom value by `zoom_delta` and zoom to a position given in screen coordinates. pub fn zoom_at(map_view: &mut MapView, pos: ScreenCoord, zoom_delta: f64) { //TODO Do something sophisticated: Increase zoom and rotate slightly so that the given // ScreenCoord points to the same geographical location. /* let latlon = Self::screen_coord_to_latlonrad(map_view, pos); let delta_x = pos.x - map_view.width * 0.5; let delta_y = pos.y - map_view.height * 0.5; map_view.center = latlon.into(); */ map_view.zoom += zoom_delta; } /// Change zoom value by `zoom_delta` and zoom to a position given in screen coordinates. pub fn set_zoom_at(map_view: &mut MapView, pos: ScreenCoord, zoom: f64) { //TODO Do something sophisticated, just like with Self::zoom_at map_view.zoom = zoom; } } #[cfg(test)] mod tests { use orthografic_view::*; #[test] fn tilecoord_neighbors() { let mut result = vec![]; tile_neighbors(TileCoord::new(0, 0, 0), &mut result); assert!(result.is_empty()); tile_neighbors(TileCoord::new(0, 0, -1), &mut result); assert!(result.is_empty()); tile_neighbors(TileCoord::new(3, 0, -1), &mut result); assert!(result.is_empty()); tile_neighbors(TileCoord::new(1, 0, 0), &mut result); assert_eq!(result.len(), 2); assert!(result.iter().find(|&&x| x == TileNeighbor::Coord(TileCoord::new(1, 1, 0))).is_some()); assert!(result.iter().find(|&&x| x == TileNeighbor::Coord(TileCoord::new(1, 0, 1))).is_some()); assert!(result.iter().find(|&&x| x == TileNeighbor::Coord(TileCoord::new(1, 1, 1))).is_none()); tile_neighbors(TileCoord::new(2, 0, 0), &mut result); assert_eq!(result.len(), 4); assert!(result.iter().find(|&&x| x == TileNeighbor::NorthPole).is_some()); tile_neighbors(TileCoord::new(2, 0, 3), &mut result); assert_eq!(result.len(), 4); assert!(result.iter().find(|&&x| x == TileNeighbor::SouthPole).is_some()); tile_neighbors(TileCoord::new(2, 3, 1), &mut result); assert_eq!(result.len(), 4); assert!(result.iter().find(|&&x| x == TileNeighbor::Coord(TileCoord::new(2, 2, 1))).is_some()); assert!(result.iter().find(|&&x| x == TileNeighbor::Coord(TileCoord::new(2, 0, 1))).is_some()); assert!(result.iter().find(|&&x| x == TileNeighbor::Coord(TileCoord::new(2, 3, 0))).is_some()); assert!(result.iter().find(|&&x| x == TileNeighbor::Coord(TileCoord::new(2, 3, 2))).is_some()); } }