use std::{
path::PathBuf,
sync::{Arc, mpsc},
thread,
};
use crate::{Tile, math};
pub struct Client
{
cache_dir: PathBuf,
sender: mpsc::Sender<(u32, u32, u32, PathBuf)>,
}
pub struct ClientBuilder
{
user_agent: String,
url: String,
qualifier: String,
organization: String,
application: String,
tiles_cache: String,
on_download_complted: Box<dyn Fn() + Sync + Send>,
}
impl ClientBuilder
{
pub fn new(user_agent: impl Into<String>, url: impl Into<String>) -> Self
{
Self {
user_agent: user_agent.into(),
url: url.into(),
qualifier: "com".into(),
organization: "cyloncore".into(),
application: "tiles-client".into(),
tiles_cache: "tiles_cache".into(),
on_download_complted: Box::new(|| {}),
}
}
pub fn application_metainfo(
mut self,
qualifier: impl Into<String>,
organization: impl Into<String>,
application: impl Into<String>,
) -> Self
{
self.qualifier = qualifier.into();
self.organization = organization.into();
self.application = application.into();
self
}
pub fn tiles_cache(mut self, tiles_cache: String) -> Self
{
self.tiles_cache = tiles_cache;
self
}
pub fn build(self) -> Client
{
let (sender, receiver) = mpsc::channel::<(u32, u32, u32, PathBuf)>();
let user_agent = self.user_agent;
let on_download_completed = Arc::new(self.on_download_complted);
thread::spawn(move || {
while let Ok(msg) = receiver.recv()
{
let url = self
.url
.replace("{x}", &msg.0.to_string())
.replace("{y}", &msg.1.to_string())
.replace("{z}", &msg.2.to_string());
let mut request = ehttp::Request::get(url);
request.headers.insert("User-Agent", user_agent.to_owned());
let on_download_completed = on_download_completed.clone();
ehttp::fetch(
request,
move |result: ehttp::Result<ehttp::Response>| match result
{
Ok(result) =>
{
if result.ok
{
if let Some(parent) = msg.3.parent()
{
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(msg.3, &result.bytes).expect("Failed to write file");
on_download_completed();
}
else
{
log::error!(
"Response: {:?} ({:?}) for {}",
result.status_text,
result.status,
result.url
);
}
}
Err(e) =>
{
log::error!("Error: {:?}", e);
}
},
);
}
});
let cache_dir =
directories::ProjectDirs::from(&self.qualifier, &self.organization, &self.application)
.unwrap()
.cache_dir()
.join(&self.tiles_cache);
Client { cache_dir, sender }
}
}
impl Client
{
fn tile_filename(&self, x: u32, y: u32, z: u32) -> PathBuf
{
self
.cache_dir
.join(z.to_string())
.join(y.to_string())
.join(format!("{}.png", x))
}
fn clamp_tile_index(index: Option<u32>, max_index: u32) -> u32
{
index.unwrap_or(0).clamp(0, max_index - 1)
}
pub fn tiles(&self, rect: geo::Rect, zoom_level: u32, download: bool) -> Vec<Tile>
{
let max_index = 2u32.pow(zoom_level);
let x_min = Self::clamp_tile_index(math::lon_to_tile_x(rect.min().x, zoom_level), max_index);
let x_max = Self::clamp_tile_index(math::lon_to_tile_x(rect.max().x, zoom_level), max_index);
let y_min = Self::clamp_tile_index(math::lat_to_tile_y(rect.max().y, zoom_level), max_index);
let y_max = Self::clamp_tile_index(math::lat_to_tile_y(rect.min().y, zoom_level), max_index);
let mut tiles = Vec::new();
for tile_x in x_min..=x_max
{
for tile_y in y_min..=y_max
{
let min_lon = math::tile_x_to_lon(tile_x, zoom_level);
let max_lon = math::tile_x_to_lon(tile_x + 1, zoom_level);
let min_lat = math::tile_y_to_lat(tile_y, zoom_level);
let max_lat = math::tile_y_to_lat(tile_y + 1, zoom_level);
let tile = Tile {
tile_x,
tile_y,
tile_z: zoom_level,
filename: self.tile_filename(tile_x, tile_y, zoom_level),
bounding_box: geo::Rect::new(
geo::Coord {
x: min_lon,
y: min_lat,
},
geo::Coord {
x: max_lon,
y: max_lat,
},
),
image: Default::default(),
};
if !tile.is_cached() && download
{
self
.sender
.send((
tile.tile_x(),
tile.tile_y(),
tile.tile_z(),
tile.filename().to_owned(),
))
.unwrap();
}
tiles.push(tile);
}
}
tiles
}
}