#![doc = include_str!("../README.md")]
mod systems;
use std::path::{Path, PathBuf};
use bevy::{
prelude::{App, Component, Event, Plugin, Resource, Update},
tasks::Task,
utils::hashbrown::HashMap,
};
pub const EARTH_CIRCUMFERENCE: f64 = 40_075_016.686;
pub const EARTH_RADIUS: f64 = 6_378_137_f64;
pub const DEGREES_PER_METER: f64 = 360.0 / EARTH_CIRCUMFERENCE;
pub const METERS_PER_DEGREE: f64 = EARTH_CIRCUMFERENCE / 360.0;
pub fn meters_per_pixel(zoom_level: ZoomLevel) -> f64 {
match zoom_level {
ZoomLevel::L0 => 156543.03,
ZoomLevel::L1 => 78271.52,
ZoomLevel::L2 => 39135.76,
ZoomLevel::L3 => 19567.88,
ZoomLevel::L4 => 9783.94,
ZoomLevel::L5 => 4891.97,
ZoomLevel::L6 => 2445.98,
ZoomLevel::L7 => 1222.99,
ZoomLevel::L8 => 611.50,
ZoomLevel::L9 => 305.75,
ZoomLevel::L10 => 152.87,
ZoomLevel::L11 => 76.437,
ZoomLevel::L12 => 38.219,
ZoomLevel::L13 => 19.109,
ZoomLevel::L14 => 9.5546,
ZoomLevel::L15 => 4.7773,
ZoomLevel::L16 => 2.3887,
ZoomLevel::L17 => 1.1943,
ZoomLevel::L18 => 0.5972,
ZoomLevel::L19 => 0.2986,
ZoomLevel::L20 => 0.1493,
ZoomLevel::L21 => 0.0747,
ZoomLevel::L22 => 0.0374,
ZoomLevel::L23 => 0.0187,
ZoomLevel::L24 => 0.0094,
ZoomLevel::L25 => 0.0047,
}
}
#[derive(Clone, Resource)]
pub struct SlippyTilesSettings {
endpoint: String,
tiles_directory: PathBuf,
}
impl SlippyTilesSettings {
pub fn new(endpoint: &str, tiles_directory: &str) -> SlippyTilesSettings {
std::fs::create_dir_all(format!("assets/{tiles_directory}")).unwrap();
SlippyTilesSettings {
endpoint: endpoint.to_owned(),
tiles_directory: PathBuf::from(tiles_directory),
}
}
pub fn get_endpoint(&self) -> String {
self.endpoint.clone()
}
pub fn get_tiles_directory(&self) -> PathBuf {
self.tiles_directory.clone()
}
pub fn get_tiles_directory_string(&self) -> String {
self.tiles_directory.as_path().to_str().unwrap().to_string()
}
}
impl Default for SlippyTilesSettings {
fn default() -> Self {
Self::new("http://localhost:8080", "tiles/")
}
}
#[derive(Eq, Hash, PartialEq, Clone, Copy, Debug)]
pub enum ZoomLevel {
L0,
L1,
L2,
L3,
L4,
L5,
L6,
L7,
L8,
L9,
L10,
L11,
L12,
L13,
L14,
L15,
L16,
L17,
L18,
L19,
L20,
L21,
L22,
L23,
L24,
L25,
}
impl ZoomLevel {
pub fn to_u8(&self) -> u8 {
match self {
ZoomLevel::L0 => 0,
ZoomLevel::L1 => 1,
ZoomLevel::L2 => 2,
ZoomLevel::L3 => 3,
ZoomLevel::L4 => 4,
ZoomLevel::L5 => 5,
ZoomLevel::L6 => 6,
ZoomLevel::L7 => 7,
ZoomLevel::L8 => 8,
ZoomLevel::L9 => 9,
ZoomLevel::L10 => 10,
ZoomLevel::L11 => 11,
ZoomLevel::L12 => 12,
ZoomLevel::L13 => 13,
ZoomLevel::L14 => 14,
ZoomLevel::L15 => 15,
ZoomLevel::L16 => 16,
ZoomLevel::L17 => 17,
ZoomLevel::L18 => 18,
ZoomLevel::L19 => 19,
ZoomLevel::L20 => 20,
ZoomLevel::L21 => 21,
ZoomLevel::L22 => 22,
ZoomLevel::L23 => 23,
ZoomLevel::L24 => 24,
ZoomLevel::L25 => 25,
}
}
}
#[derive(Eq, Hash, PartialEq, Clone, Copy, Debug)]
pub enum TileSize {
Normal,
Large,
VeryLarge,
}
impl TileSize {
pub fn new(tile_pixels: u32) -> TileSize {
match tile_pixels {
768 => TileSize::VeryLarge,
512 => TileSize::Large,
_ => TileSize::Normal,
}
}
pub fn to_pixels(&self) -> u32 {
match self {
TileSize::Normal => 256,
TileSize::Large => 512,
TileSize::VeryLarge => 768,
}
}
pub fn get_url_postfix(&self) -> String {
match self {
TileSize::Normal => "".into(),
TileSize::Large => "@2x".into(),
TileSize::VeryLarge => "@3x".into(),
}
}
}
#[derive(Debug)]
pub struct Radius(pub u8);
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct SlippyTileDownloadTaskKey {
slippy_tile_coordinates: SlippyTileCoordinates,
zoom_level: ZoomLevel,
tile_size: TileSize,
}
#[derive(Resource)]
pub struct SlippyTileDownloadStatus(HashMap<SlippyTileDownloadTaskKey, TileDownloadStatus>);
impl SlippyTileDownloadStatus {
pub fn new() -> SlippyTileDownloadStatus {
SlippyTileDownloadStatus(HashMap::new())
}
pub fn insert(
&mut self,
x: u32,
y: u32,
zoom_level: ZoomLevel,
tile_size: TileSize,
filename: String,
download_status: DownloadStatus,
) {
self.insert_with_coords(
SlippyTileCoordinates { x, y },
zoom_level,
tile_size,
filename,
download_status,
);
}
pub fn insert_with_coords(
&mut self,
slippy_tile_coordinates: SlippyTileCoordinates,
zoom_level: ZoomLevel,
tile_size: TileSize,
filename: String,
download_status: DownloadStatus,
) {
self.0.insert(
SlippyTileDownloadTaskKey {
slippy_tile_coordinates,
zoom_level,
tile_size,
},
TileDownloadStatus {
path: Path::new(&filename).to_path_buf(),
load_status: download_status,
},
);
}
pub fn contains_key(&self, x: u32, y: u32, zoom_level: ZoomLevel, tile_size: TileSize) -> bool {
self.contains_key_with_coords(SlippyTileCoordinates { x, y }, zoom_level, tile_size)
}
pub fn contains_key_with_coords(
&self,
slippy_tile_coordinates: SlippyTileCoordinates,
zoom_level: ZoomLevel,
tile_size: TileSize,
) -> bool {
self.0.contains_key(&SlippyTileDownloadTaskKey {
slippy_tile_coordinates,
zoom_level,
tile_size,
})
}
}
impl Default for SlippyTileDownloadStatus {
fn default() -> Self {
Self::new()
}
}
pub struct TileDownloadStatus {
pub path: PathBuf,
pub load_status: DownloadStatus,
}
#[derive(Clone)]
pub struct SlippyTileDownloadTaskResult {
pub path: PathBuf,
}
#[derive(Resource)]
pub struct SlippyTileDownloadTasks(
HashMap<SlippyTileDownloadTaskKey, Task<SlippyTileDownloadTaskResult>>,
);
impl SlippyTileDownloadTasks {
pub fn new() -> SlippyTileDownloadTasks {
SlippyTileDownloadTasks(HashMap::new())
}
pub fn insert(
&mut self,
x: u32,
y: u32,
zoom_level: ZoomLevel,
tile_size: TileSize,
task: Task<SlippyTileDownloadTaskResult>,
) {
self.insert_with_coords(SlippyTileCoordinates { x, y }, zoom_level, tile_size, task);
}
pub fn insert_with_coords(
&mut self,
slippy_tile_coordinates: SlippyTileCoordinates,
zoom_level: ZoomLevel,
tile_size: TileSize,
task: Task<SlippyTileDownloadTaskResult>,
) {
self.0.insert(
SlippyTileDownloadTaskKey {
slippy_tile_coordinates,
zoom_level,
tile_size,
},
task,
);
}
}
impl Default for SlippyTileDownloadTasks {
fn default() -> Self {
Self::new()
}
}
pub enum DownloadStatus {
Downloading,
Downloaded,
}
#[derive(Debug, Event)]
pub struct DownloadSlippyTilesEvent {
pub tile_size: TileSize,
pub zoom_level: ZoomLevel,
pub coordinates: Coordinates,
pub radius: Radius,
pub use_cache: bool,
}
impl DownloadSlippyTilesEvent {
pub fn get_slippy_tile_coordinates(&self) -> SlippyTileCoordinates {
self.coordinates
.get_slippy_tile_coordinates(self.zoom_level)
}
}
#[derive(Debug, Event)]
pub struct SlippyTileDownloadedEvent {
pub tile_size: TileSize,
pub zoom_level: ZoomLevel,
pub coordinates: Coordinates,
pub path: PathBuf,
}
impl SlippyTileDownloadedEvent {
pub fn get_slippy_tile_coordinates(&self) -> SlippyTileCoordinates {
self.coordinates
.get_slippy_tile_coordinates(self.zoom_level)
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Component)]
pub struct SlippyTileCoordinates {
pub x: u32,
pub y: u32,
}
impl SlippyTileCoordinates {
pub fn from_latitude_longitude(
lat: f64,
lon: f64,
zoom_level: ZoomLevel,
) -> SlippyTileCoordinates {
let x = longitude_to_tile_x(lon, zoom_level.to_u8());
let y = latitude_to_tile_y(lat, zoom_level.to_u8());
SlippyTileCoordinates { x, y }
}
pub fn to_latitude_longitude(&self, zoom_level: ZoomLevel) -> LatitudeLongitudeCoordinates {
let lon = tile_x_to_longitude(self.x, zoom_level.to_u8());
let lat = tile_y_to_latitude(self.y, zoom_level.to_u8());
LatitudeLongitudeCoordinates {
latitude: lat,
longitude: lon,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Component)]
pub struct LatitudeLongitudeCoordinates {
pub latitude: f64,
pub longitude: f64,
}
impl LatitudeLongitudeCoordinates {
pub fn to_slippy_tile_coordinates(&self, zoom_level: ZoomLevel) -> SlippyTileCoordinates {
SlippyTileCoordinates::from_latitude_longitude(self.latitude, self.longitude, zoom_level)
}
}
#[derive(Debug, Clone, PartialEq, Component)]
pub enum Coordinates {
SlippyTile(SlippyTileCoordinates),
LatitudeLongitude(LatitudeLongitudeCoordinates),
}
impl Coordinates {
pub fn from_slippy_tile_coordinates(x: u32, y: u32) -> Coordinates {
Coordinates::SlippyTile(SlippyTileCoordinates { x, y })
}
pub fn from_latitude_longitude(latitude: f64, longitude: f64) -> Coordinates {
Coordinates::LatitudeLongitude(LatitudeLongitudeCoordinates {
latitude,
longitude,
})
}
pub fn get_slippy_tile_coordinates(&self, zoom_level: ZoomLevel) -> SlippyTileCoordinates {
match &self {
Coordinates::LatitudeLongitude(coords) => coords.to_slippy_tile_coordinates(zoom_level),
Coordinates::SlippyTile(coords) => *coords,
}
}
}
pub enum UseCache {
Yes,
No,
}
impl UseCache {
pub fn new(value: bool) -> UseCache {
match value {
true => UseCache::Yes,
_ => UseCache::No,
}
}
}
pub enum AlreadyDownloaded {
Yes,
No,
}
impl AlreadyDownloaded {
pub fn new(value: bool) -> AlreadyDownloaded {
match value {
true => AlreadyDownloaded::Yes,
_ => AlreadyDownloaded::No,
}
}
}
pub enum FileExists {
Yes,
No,
}
impl FileExists {
pub fn new(value: bool) -> FileExists {
match value {
true => FileExists::Yes,
_ => FileExists::No,
}
}
}
pub fn latitude_to_tile_y(lat: f64, zoom: u8) -> u32 {
((1.0
- ((lat * std::f64::consts::PI / 180.0).tan()
+ 1.0 / (lat * std::f64::consts::PI / 180.0).cos())
.ln()
/ std::f64::consts::PI)
/ 2.0
* f64::powf(2.0, zoom as f64))
.floor() as u32
}
pub fn longitude_to_tile_x(lon: f64, zoom: u8) -> u32 {
((lon + 180.0) / 360.0 * f64::powf(2.0, zoom as f64)).floor() as u32
}
pub fn tile_y_to_latitude(y: u32, zoom: u8) -> f64 {
let n =
std::f64::consts::PI - 2.0 * std::f64::consts::PI * y as f64 / f64::powf(2.0, zoom as f64);
let intermediate: f64 = 0.5 * (n.exp() - (-n).exp());
180.0 / std::f64::consts::PI * intermediate.atan()
}
pub fn tile_x_to_longitude(x: u32, z: u8) -> f64 {
x as f64 / f64::powf(2.0, z as f64) * 360.0 - 180.0
}
pub fn max_tiles_in_dimension(zoom_level: ZoomLevel) -> f64 {
(1 << zoom_level.to_u8()) as f64
}
pub fn max_pixels_in_dimension(zoom_level: ZoomLevel, tile_size: TileSize) -> f64 {
tile_size.to_pixels() as f64 * max_tiles_in_dimension(zoom_level)
}
pub fn world_pixel_to_world_coords(
x_pixel: f64,
y_pixel: f64,
tile_size: TileSize,
zoom_level: ZoomLevel,
) -> LatitudeLongitudeCoordinates {
let max_pixels = max_pixels_in_dimension(zoom_level, tile_size);
let y_pixel = max_pixels - y_pixel;
let (longitude, latitude) =
googleprojection::Mercator::with_size(tile_size.to_pixels() as usize)
.from_pixel_to_ll(&(x_pixel, y_pixel), zoom_level.to_u8().into())
.unwrap_or_default();
LatitudeLongitudeCoordinates {
latitude,
longitude,
}
}
pub fn world_coords_to_world_pixel(
coords: &LatitudeLongitudeCoordinates,
tile_size: TileSize,
zoom_level: ZoomLevel,
) -> (f64, f64) {
let (x, y) = googleprojection::Mercator::with_size(tile_size.to_pixels() as usize)
.from_ll_to_subpixel(
&(coords.longitude, coords.latitude),
zoom_level.to_u8().into(),
)
.unwrap_or_default();
let max_pixels = max_pixels_in_dimension(zoom_level, tile_size);
let y = max_pixels - y;
(x, y)
}
pub struct SlippyTilesPlugin;
impl Plugin for SlippyTilesPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(SlippyTileDownloadStatus::new())
.insert_resource(SlippyTileDownloadTasks::new())
.add_event::<DownloadSlippyTilesEvent>()
.add_event::<SlippyTileDownloadedEvent>()
.add_systems(Update, systems::download_slippy_tiles)
.add_systems(Update, systems::download_slippy_tiles_completed);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tile_size_new() {
assert_eq!(TileSize::new(256), TileSize::Normal);
assert_eq!(TileSize::new(512), TileSize::Large);
assert_eq!(TileSize::new(1024), TileSize::Normal);
}
#[test]
fn test_slippy_tile_settings() {
let sts = SlippyTilesSettings::new("endpoint", "tiles_directory");
assert_eq!(sts.get_endpoint(), "endpoint");
assert_eq!(sts.get_tiles_directory(), PathBuf::from("tiles_directory"));
assert_eq!(sts.get_tiles_directory_string(), "tiles_directory");
assert!(std::path::Path::try_exists("assets/tiles_directory".as_ref()).is_ok());
}
#[test]
fn test_slippy_tile_coordinates() {
assert_eq!(
SlippyTileCoordinates::from_latitude_longitude(85.0511287798066, 0.0, ZoomLevel::L1),
SlippyTileCoordinates { x: 1, y: 0 }
);
assert_eq!(
SlippyTileCoordinates::to_latitude_longitude(
&SlippyTileCoordinates { x: 1, y: 0 },
ZoomLevel::L1
),
LatitudeLongitudeCoordinates {
latitude: 85.0511287798066,
longitude: 0.0
}
);
assert_eq!(
SlippyTileCoordinates::from_latitude_longitude(0.0, 0.0, ZoomLevel::L10),
SlippyTileCoordinates { x: 512, y: 512 }
);
assert_eq!(
SlippyTileCoordinates::to_latitude_longitude(
&SlippyTileCoordinates { x: 512, y: 512 },
ZoomLevel::L10
),
LatitudeLongitudeCoordinates {
latitude: 0.0,
longitude: 0.0
}
);
assert_eq!(
SlippyTileCoordinates::from_latitude_longitude(
48.81590713080016,
2.2686767578125,
ZoomLevel::L17
),
SlippyTileCoordinates { x: 66362, y: 45115 }
);
assert_eq!(
SlippyTileCoordinates::to_latitude_longitude(
&SlippyTileCoordinates { x: 66362, y: 45115 },
ZoomLevel::L17
),
LatitudeLongitudeCoordinates {
latitude: 48.81590713080016,
longitude: 2.2686767578125
}
);
assert_eq!(
SlippyTileCoordinates::from_latitude_longitude(
0.004806518549043551,
0.004119873046875,
ZoomLevel::L19
),
SlippyTileCoordinates {
x: 262150,
y: 262137
}
);
assert_eq!(
SlippyTileCoordinates::to_latitude_longitude(
&SlippyTileCoordinates {
x: 262150,
y: 262137
},
ZoomLevel::L19
),
LatitudeLongitudeCoordinates {
latitude: 0.004806518549043551,
longitude: 0.004119873046875
}
);
assert_eq!(
SlippyTileCoordinates::from_latitude_longitude(
26.850416392948524,
72.57980346679688,
ZoomLevel::L19
),
SlippyTileCoordinates {
x: 367846,
y: 221525
}
);
assert_eq!(
SlippyTileCoordinates::to_latitude_longitude(
&SlippyTileCoordinates {
x: 367846,
y: 221525
},
ZoomLevel::L19
),
LatitudeLongitudeCoordinates {
latitude: 26.850416392948524,
longitude: 72.57980346679688
}
);
}
#[test]
fn test_slippy_tile_download_status() {
let mut stds = SlippyTileDownloadStatus::default();
stds.insert(
100,
50,
ZoomLevel::L10,
TileSize::Normal,
"filename".into(),
DownloadStatus::Downloading,
);
assert!(!stds.contains_key(100, 50, ZoomLevel::L1, TileSize::Normal));
assert!(!stds.contains_key(100, 50, ZoomLevel::L10, TileSize::Large));
assert!(!stds.contains_key(100, 100, ZoomLevel::L10, TileSize::Normal));
assert!(stds.contains_key(100, 50, ZoomLevel::L10, TileSize::Normal));
stds.insert(
50,
100,
ZoomLevel::L18,
TileSize::Large,
"filename".into(),
DownloadStatus::Downloaded,
);
assert!(!stds.contains_key(50, 100, ZoomLevel::L1, TileSize::Large));
assert!(!stds.contains_key(50, 100, ZoomLevel::L18, TileSize::Normal));
assert!(!stds.contains_key(100, 50, ZoomLevel::L18, TileSize::Large));
assert!(stds.contains_key(50, 100, ZoomLevel::L18, TileSize::Large));
}
#[test]
fn test_pixel_to_world_coords() {
let tile_size = TileSize::Normal;
let zoom_level = ZoomLevel::L18;
let world_coords = world_pixel_to_world_coords(42_125.0, 101_661.0, tile_size, zoom_level);
let rounded = LatitudeLongitudeCoordinates {
latitude: format!("{:.5}", world_coords.latitude).parse().unwrap(),
longitude: format!("{:.5}", world_coords.longitude).parse().unwrap(),
};
let check = LatitudeLongitudeCoordinates {
latitude: -85.00386,
longitude: -179.77402,
};
assert_eq!(rounded, check);
let pixel = world_coords_to_world_pixel(&world_coords, tile_size, zoom_level);
let rounded = (pixel.0.round(), pixel.1.round());
assert_eq!(rounded, (42_125.0, 101_661.0));
}
#[test]
fn test_world_coords_to_pixel() {
let world_coords = LatitudeLongitudeCoordinates {
latitude: 45.41098,
longitude: -75.69854,
};
let tile_size = TileSize::Normal;
let zoom_level = ZoomLevel::L18;
let pixel = world_coords_to_world_pixel(&world_coords, tile_size, zoom_level);
assert_eq!((pixel.0 as u32, pixel.1 as u32), (19_443_201, 43_076_862));
let world_coords2 = world_pixel_to_world_coords(pixel.0, pixel.1, tile_size, zoom_level);
let rounded = LatitudeLongitudeCoordinates {
latitude: format!("{:.5}", world_coords2.latitude).parse().unwrap(),
longitude: format!("{:.5}", world_coords2.longitude).parse().unwrap(),
};
assert_eq!(world_coords, rounded);
}
}