use crate::{
affine::GeoTransform, errors::MapEngineError, raster::Raster, windows::Window, MAXZOOMLEVEL,
SUPPORTED_FORMATS,
};
use gdal::spatial_ref::{CoordTransform, SpatialRef};
use std::cmp;
use std::f64::consts::PI;
const RE: f64 = 6378137.0;
const EPSILON: f64 = 1e-14;
pub const TILE_SIZE: usize = 256;
#[derive(Debug, PartialEq)]
pub struct Tile {
pub x: u32,
pub y: u32,
pub z: u32,
ext: Option<String>,
}
impl Tile {
pub fn new(x: u32, y: u32, z: u32) -> Self {
Self {
x,
y,
z,
ext: Some("png".to_string()),
}
}
pub fn set_extension(&mut self, ext: &str) -> Result<(), MapEngineError> {
if !SUPPORTED_FORMATS.contains(&ext) {
return Err(MapEngineError::Msg(format!(
"The extension {:?} is not yet supported",
ext
)));
}
self.ext = Some(ext.to_string());
Ok(())
}
pub fn to_tuple(&self) -> (u32, u32, u32) {
(self.x, self.y, self.z)
}
pub fn ul(&self) -> (f64, f64) {
let (xtile, ytile, zoom) = self.to_tuple();
let z2: f64 = 2u32.pow(zoom).into();
let lon_deg = (xtile as f64) / z2 * 360.0 - 180.0;
let lat_rad = (PI * (1.0 - 2.0 * (ytile as f64) / z2)).sinh().atan();
let lat_deg = lat_rad.to_degrees();
(lon_deg, lat_deg)
}
pub fn ul_xy(&self) -> (f64, f64) {
let (lon_deg, lat_deg) = self.ul();
xy(lon_deg, lat_deg)
}
pub fn bounds(&self) -> (f64, f64, f64, f64) {
let (xtile, ytile, zoom) = self.to_tuple();
let z2: f64 = 2u32.pow(zoom).into();
let min_lng_deg = (xtile as f64) / z2 * 360.0 - 180.0;
let max_lat_rad = (PI * (1.0 - 2.0 * (ytile as f64) / z2)).sinh().atan();
let max_lat_deg = max_lat_rad.to_degrees();
let max_lng_deg = ((xtile + 1) as f64) / z2 * 360.0 - 180.0;
let min_lat_rad = (PI * (1.0 - 2.0 * ((ytile + 1) as f64) / z2)).sinh().atan();
let min_lat_deg = min_lat_rad.to_degrees();
(min_lng_deg, max_lat_deg, max_lng_deg, min_lat_deg)
}
pub fn bounds_xy(&self) -> (f64, f64, f64, f64) {
let (min_lng_deg, max_lat_deg, max_lng_deg, min_lat_deg) = self.bounds();
let (min_x, max_y) = xy(min_lng_deg, max_lat_deg);
let (max_x, min_y) = xy(max_lng_deg, min_lat_deg);
(min_x, max_y, max_x, min_y)
}
pub fn vertices(&self) -> [(f64, f64); 4] {
let (min_x, max_y, max_x, min_y) = self.bounds();
[
(min_x, max_y), (max_x, max_y), (max_x, min_y), (min_x, min_y), ]
}
pub fn zoom_out(&self, zoom: Option<u32>) -> Option<Self> {
if self.z == 0 {
return None;
};
let target_zoom = zoom.unwrap_or(self.z - 1);
let mut return_tile = Tile::new(self.x, self.y, self.z);
while return_tile.z > target_zoom {
let (xtile, ytile, ztile) = (return_tile.x, return_tile.y, return_tile.z);
let newx = if xtile % 2 == 0 {
xtile / 2
} else {
(xtile - 1) / 2
};
let newy = if ytile % 2 == 0 {
ytile / 2
} else {
(ytile - 1) / 2
};
let newz = ztile - 1;
return_tile = Tile::new(newx, newy, newz);
}
Some(return_tile)
}
pub fn zoom_in(&self, zoom: Option<u32>) -> Option<Vec<Self>> {
if self.z == MAXZOOMLEVEL {
return None;
}
let target_zoom = zoom.unwrap_or(self.z + 1);
let mut tiles = vec![Tile::new(self.x, self.y, self.z)];
while tiles[0].z < target_zoom {
tiles = tiles
.iter()
.map(|tile| {
[
Tile::new(tile.x * 2, tile.y * 2, tile.z + 1),
Tile::new(tile.x * 2 + 1, tile.y * 2, tile.z + 1),
Tile::new(tile.x * 2 + 1, tile.y * 2 + 1, tile.z + 1),
Tile::new(tile.x * 2, tile.y * 2 + 1, tile.z + 1),
]
})
.flatten()
.collect()
}
Some(tiles)
}
pub fn from_lat_lng(lng: f64, lat: f64, zoom: u32) -> Self {
let (x, y) = _xy(lng, lat);
let z2 = 2u32.pow(zoom);
let xtile = if x <= 0.0 {
0u32
} else if x >= 1.0 {
z2 - 1
} else {
((x + EPSILON) * (z2 as f64)).floor() as u32
};
let ytile = if y <= 0.0 {
0u32
} else if y >= 1.0 {
z2 - 1
} else {
((y + EPSILON) * (z2 as f64)).floor() as u32
};
Self::new(xtile, ytile, zoom)
}
pub fn to_window(&self, raster: &Raster) -> Result<(Window, bool), MapEngineError> {
let geo = raster.geo();
let spatial_ref = raster.spatial_ref()?;
let src_ref_code = spatial_ref.auth_code().unwrap_or(3857) as u32;
let src_spatial_units = spatial_ref
.linear_units_name()
.unwrap_or_else(|_| "metre".to_string());
let wgs84_crs = gdal::spatial_ref::SpatialRef::from_epsg(4326)?;
let src_crs = SpatialRef::from_epsg(src_ref_code)?;
let vertex_trans = gdal::spatial_ref::CoordTransform::new(&wgs84_crs, &src_crs)?;
let vertices = self.vertices();
let mut xs = [vertices[0].0, vertices[1].0, vertices[2].0, vertices[3].0];
let mut ys = [vertices[0].1, vertices[1].1, vertices[2].1, vertices[3].1];
let mut zs = [0.0f64; 4];
vertex_trans.transform_coords(&mut ys, &mut xs, &mut zs)?;
let offset = get_vertices_offset(self.z, &src_spatial_units)?;
let row_cols = get_row_cols(&xs, &ys, &offset, geo, &src_spatial_units);
let is_skewed = (row_cols[0].0 != row_cols[1].0)
|| (row_cols[2].0 != row_cols[3].0 || (row_cols[0].1 != row_cols[3].1))
|| (row_cols[1].1 != row_cols[2].1);
let win = Window::new(
cmp::min(row_cols[0].1, row_cols[3].1) as isize,
cmp::min(row_cols[0].0, row_cols[1].0) as isize,
cmp::max(row_cols[1].1 - row_cols[0].1, row_cols[2].1 - row_cols[3].1) as usize + 1,
cmp::max(row_cols[3].0 - row_cols[0].0, row_cols[2].0 - row_cols[1].0) as usize + 1,
);
Ok((win, is_skewed))
}
}
fn get_row_cols(
xs: &[f64],
ys: &[f64],
offset: &[(f64, f64)],
geo: &GeoTransform,
src_spatial_units: &str,
) -> Vec<(i32, i32)> {
xs.iter()
.zip(ys)
.zip(offset)
.map(|((x, y), (x_off, y_off))| {
if src_spatial_units == "metre" {
geo.rowcol(y + y_off, x + x_off).unwrap()
} else {
geo.rowcol(x + x_off, y + y_off).unwrap()
}
})
.collect()
}
fn get_vertices_offset(
zoom: u32,
src_spatial_units: &str,
) -> Result<[(f64, f64); 4], MapEngineError> {
let mercator_crs = SpatialRef::from_epsg(3857)?;
let wgs84_crs = gdal::spatial_ref::SpatialRef::from_epsg(4326).unwrap();
let tile_res_trans = CoordTransform::new(&mercator_crs, &wgs84_crs).unwrap();
let tile_z2: f64 = 2u32.pow(zoom).into();
let tile_res = (2.0 * PI * RE / TILE_SIZE as f64) / tile_z2;
let tile_res_m = ([tile_res], [tile_res], [0.0]);
let mut tile_res_deg = ([tile_res], [tile_res], [0.0]);
tile_res_trans
.transform_coords(
&mut tile_res_deg.0,
&mut tile_res_deg.1,
&mut tile_res_deg.2,
)
.unwrap();
let offset_prop = 0.01;
let offset = if src_spatial_units == "metre" {
[
(
-tile_res_m.0[0] * offset_prop,
tile_res_m.1[0] * offset_prop,
),
(
-tile_res_m.0[0] * offset_prop,
-tile_res_m.1[0] * offset_prop,
),
(
tile_res_m.0[0] * offset_prop,
-tile_res_m.1[0] * offset_prop,
),
(tile_res_m.0[0] * offset_prop, tile_res_m.1[0] * offset_prop),
]
} else {
[
(
tile_res_deg.1[0] * offset_prop,
-tile_res_deg.0[0] * offset_prop,
),
(
-tile_res_deg.1[0] * offset_prop,
-tile_res_deg.0[0] * offset_prop,
),
(
-tile_res_deg.1[0] * offset_prop,
tile_res_deg.0[0] * offset_prop,
),
(
tile_res_deg.1[0] * offset_prop,
tile_res_deg.0[0] * offset_prop,
),
]
};
Ok(offset)
}
fn xy(lng: f64, lat: f64) -> (f64, f64) {
let x = RE * lng.to_radians();
let y = if lat <= -90.0 {
std::f64::NEG_INFINITY
} else if lat >= 90.0 {
std::f64::INFINITY
} else {
RE * (PI * 0.25 + lat.to_radians() * 0.5).tan().ln()
};
(x, y)
}
fn _xy(lng: f64, lat: f64) -> (f64, f64) {
let x = lng / 360.0 + 0.5;
let sinlat = lat.to_radians().sin();
let y = 0.5 - 0.25 * (sinlat + 1.0).ln() / (1.0 - sinlat) / std::f64::consts::PI;
(x, y)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mercator_xy() {
let tile = Tile::new(1, 2, 3);
let (lng, lat) = tile.ul();
assert_eq!(xy(lng, lat), (-15028131.257091932, 10018754.171394626));
}
}