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> = 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>>; pub struct MapTile { server: String, api_key: String, style: String, client: Client, onloading: Arc>>, cache: Arc>, zoom_level: Cell, } 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 + 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, 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, 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>, result: Vec, 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, 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, ) -> 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::>(); 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::>(); 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>> { 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 } }