radar-g/src/map_tile.rs
2024-04-17 18:35:47 +08:00

250 lines
8.8 KiB
Rust

use crate::components::messages::MonitorInputMsg;
use crate::components::{MonitorCommand, MonitorModel};
use crate::coords::Range;
use crate::map_tile_utils::lat_lon_to_zoom;
use crate::pipeline::{Target, TargetType};
use dirs::cache_dir;
use femtovg::ImageSource;
use futures::future::BoxFuture;
use gtk::ffi::gtk_about_dialog_add_credit_section;
use once_cell::sync::Lazy;
use quick_cache::sync::Cache;
use relm4::{ComponentSender, Sender};
use reqwest::{Client, Error, Url};
use slippy_map_tiles::{merc_location_to_tile_coords, BBox, Tile};
use smallvec::SmallVec;
use sorted_vec::SortedSet;
use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::task;
use tracing::{debug, info};
static TILE_CACHE_PATH: Lazy<std::sync::Mutex<PathBuf>> = Lazy::new(|| {
let new_path = cache_dir()
.unwrap_or(PathBuf::from("./"))
.join("radar-g/tiles");
if !new_path.exists() {
info!("Create cache dir: {:?}", new_path);
std::fs::create_dir_all(&new_path).unwrap();
}
std::sync::Mutex::new(new_path)
});
type TileCache = Cache<Tile, Arc<std::sync::Mutex<Target>>>;
pub struct MapTile {
server: String,
api_key: String,
style: String,
client: Client,
onloading: Arc<std::sync::Mutex<HashSet<Tile>>>,
cache: Arc<std::sync::Mutex<TileCache>>,
zoom_level: Cell<u8>,
}
impl Default for MapTile {
fn default() -> Self {
Self {
server: "https://tiles.stadiamaps.com/tiles/".to_string(),
api_key: "06f1aeed-5d91-48e3-9ce5-1e99063f7f73".to_string(),
// style: "stamen_toner".to_string(),
style: "alidade_smooth_dark".to_string(),
client: Client::new(),
onloading: Arc::new(std::sync::Mutex::new(HashSet::new())),
cache: Arc::new(std::sync::Mutex::new(Cache::new(32))),
zoom_level: Cell::new(4),
}
}
}
impl MapTile {
pub fn new_tiles(
&self,
zoom: u8,
lat_range: (f32, f32),
lon_range: (f32, f32),
) -> impl Iterator<Item = Tile> + Sized {
let bbox = BBox::new(lat_range.1, lon_range.0, lat_range.0, lon_range.1).unwrap();
let tiles = bbox.tiles_for_zoom(zoom);
tiles
}
fn get_tile_task(&self, tile: &Tile) -> (bool, BoxFuture<'static, Result<Vec<u8>, Error>>) {
let _cache_path = TILE_CACHE_PATH.lock().unwrap().clone().join(format!(
"{}-{}-{}.png",
tile.zoom(),
tile.x(),
tile.y()
));
let exists_cache = _cache_path.exists();
if exists_cache {
let client = self.client.clone();
let key = self.api_key.clone();
(
false,
Box::pin(async move {
info!("Read from cache: {:?}", _cache_path);
let result = tokio::fs::read(_cache_path).await.unwrap();
Ok(result)
}),
)
} else {
let base_url = Url::parse(&self.server).unwrap();
let mut request_url = base_url
.join(&format!("{}/", self.style))
.unwrap()
.join(&format!("{}/{}/{}@2x.png", tile.zoom(), tile.x(), tile.y()))
.unwrap();
let client = self.client.clone();
let key = self.api_key.clone();
(
true,
Box::pin(async move {
let result = client
.get(request_url)
.query(&[("api_key", &key)])
.send()
.await?;
let bytes = result.bytes().await?;
Ok(bytes.to_vec())
}),
)
}
}
pub async fn get_tile(&self, tile: &Tile) -> Result<Vec<u8>, Error> {
let base_url = Url::parse(&self.server).unwrap();
let mut request_url = base_url
.join(&self.style)
.unwrap()
.join(&format!("{}/{}/{}@2x.png", tile.zoom(), tile.x(), tile.y()))
.unwrap();
let result = self
.client
.get(request_url)
.query(&[("api_key", &self.api_key)])
.send()
.await?;
let bytes = result.bytes().await?;
Ok(bytes.to_vec())
}
pub fn set_zoom(&self, zoom: u8) {
self.zoom_level.set(zoom);
}
fn insert_to_cache(cache: Arc<std::sync::Mutex<TileCache>>, result: Vec<u8>, tile: Tile) {
let origin = tile.nw_corner();
let rb = tile.se_corner();
let bounds = (
(origin.lon() as f64..rb.lon() as f64).into(),
(origin.lat() as f64..rb.lat() as f64).into(),
);
let result = Target::new(TargetType::Mem(result), 256.0, 256.0, bounds, None);
let cache = cache.lock().unwrap();
cache.insert(tile, Arc::new(std::sync::Mutex::new(result)));
}
async fn save_to_cache_path(result: &Vec<u8>, tile: Tile) -> Result<(), std::io::Error> {
let _cache_path = TILE_CACHE_PATH.lock().unwrap().clone();
let path = _cache_path.join(format!("{}-{}-{}.png", tile.zoom(), tile.x(), tile.y()));
tokio::fs::write(path, result).await
}
pub fn load_tiles(
&self,
range: ((f32, f32), (f32, f32)),
sender: ComponentSender<MonitorModel>,
) -> BoxFuture<'static, ()> {
let zoom = self.zoom_level.get();
let new_tiles = self.new_tiles(zoom, range.0, range.1);
let cache = (*self.cache).lock().unwrap();
let mut bindings = self.onloading.lock().unwrap();
let new_tiles = new_tiles
.filter(|x| cache.peek(x).is_none() && !bindings.contains(x))
.collect::<Vec<_>>();
if new_tiles.len() > 0 {
bindings.extend(new_tiles.iter().cloned());
info!("Load new tiles");
let tasks = new_tiles
.into_iter()
.map(move |tile| {
let (need_save, _task) = self.get_tile_task(&tile);
let sender = sender.clone();
let cache = self.cache.clone();
let onloading = self.onloading.clone();
task::spawn(async move {
let result = _task.await;
if let Ok(result) = result {
if let Err(e) = Self::save_to_cache_path(&result, tile).await {
info!("Error saving to cache: {}", e);
}
if need_save {
Self::save_to_cache_path(&result, tile);
}
Self::insert_to_cache(cache, result, tile);
onloading.lock().unwrap().remove(&tile);
sender.command_sender().emit(MonitorCommand::LoadedTile);
}
})
})
.collect::<Vec<_>>();
return Box::pin(async move {
for task in tasks {
task.await.unwrap();
}
});
} else {
info!("No new tiles need to download");
return Box::pin(async move {
sender.command_sender().emit(MonitorCommand::LoadedTile);
});
}
}
pub fn current_tiles(
&self,
range: ((f32, f32), (f32, f32)),
) -> Vec<Arc<std::sync::Mutex<Target>>> {
let zoom = self.zoom_level.get();
let current = self.new_tiles(zoom, range.0, range.1);
let mut total_len = 0;
let mut results = Vec::new();
let mut cached = Vec::new();
let cache = self.cache.lock().unwrap();
for tile in current {
total_len += 1;
if let Some(target) = cache.get(&tile) {
results.push(target);
} else {
let center = tile.center_point();
let mut start_zoom = zoom - 1;
while start_zoom > 0 {
let (tile, (x, y)) = merc_location_to_tile_coords(
center.lon() as f64,
center.lat() as f64,
start_zoom,
);
let tile = Tile::new(start_zoom, tile.0, tile.1).unwrap();
if cached.contains(&tile) {
break;
} else {
if let Some(target) = cache.get(&tile) {
results.insert(0, target);
cached.push(tile);
break;
}
}
start_zoom -= 1;
}
}
}
results
}
}