use crate::gui_system::high_level_tile_cache::TileCache;
use crate::tile_cache::cache_core::{generate_cache, generate_dummy_cache};
use crate::tile_cache::web_requester::RequesterError;
use dirs::cache_dir;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TileCacheError {
InvalidJson(String),
NoCacheDirectory,
InvalidUserAgent(String),
HttpClient(String),
}
impl fmt::Display for TileCacheError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TileCacheError::InvalidJson(message) => {
write!(f, "invalid tile cache configuration: {message}")
}
TileCacheError::NoCacheDirectory => {
write!(f, "the system has no cache directory")
}
TileCacheError::InvalidUserAgent(user_agent) => {
write!(
f,
"the user agent {user_agent:?} is no valid HTTP header value"
)
}
TileCacheError::HttpClient(message) => {
write!(f, "the HTTP client could not be set up: {message}")
}
}
}
}
impl std::error::Error for TileCacheError {}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum CachingDirectory {
FullyConstructed(PathBuf),
CacheDirectory(PathBuf),
CacheDirFixed,
}
impl CachingDirectory {
fn get_path(&self) -> Result<PathBuf, TileCacheError> {
match self {
CachingDirectory::FullyConstructed(path) => Ok(path.clone()),
CachingDirectory::CacheDirectory(path) => Ok(cache_dir()
.ok_or(TileCacheError::NoCacheDirectory)?
.join(path)),
CachingDirectory::CacheDirFixed => Ok(cache_dir()
.ok_or(TileCacheError::NoCacheDirectory)?
.join("Tiles")),
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum TileSource {
FullyConstructed {
start_url: String,
end_url: String,
user_agent: String,
copyright_text: String,
},
OpenStreetMap { user_agent: String },
MapTilesApi { api_key: String },
MapBoxTiles { tileset_id: String, api_key: String },
MapBoxSatellite { api_key: String },
Thunderforest { style: String, api_key: String },
TestPattern,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TileCacheConfig {
cache: CachingDirectory,
cache_size: u64,
source: TileSource,
}
const MAPBOX_SATELLITE_TILESET: &str = "mapbox.satellite";
const MAPBOX_SATELLITE_COPYRIGHT: &str = "© Mapbox © OpenStreetMap © Maxar";
pub(crate) struct QuadrupleInfo {
start_url: String,
end_url: String,
user_agent: String,
copyright_text: String,
}
impl TileSource {
pub(crate) fn get_quadruple(&self) -> Option<QuadrupleInfo> {
let quadruple = match self {
TileSource::FullyConstructed {
start_url,
end_url,
user_agent,
copyright_text,
} => QuadrupleInfo {
start_url: if start_url.ends_with('/') {
start_url.clone()
} else {
start_url.clone() + "/"
},
end_url: end_url.clone(),
user_agent: user_agent.clone(),
copyright_text: copyright_text.clone(),
},
TileSource::OpenStreetMap { user_agent } => QuadrupleInfo {
start_url: "https://tile.openstreetmap.org/".to_string(),
end_url: "".to_string(),
user_agent: user_agent.clone(),
copyright_text: "© OpenStreetMap Contributors".to_string(),
},
TileSource::MapTilesApi { api_key } => QuadrupleInfo {
start_url: "https://maptiles.p.rapidapi.com/en/map/v1/".to_string(),
end_url: "?rapidapi-key=".to_string() + api_key,
user_agent: concat!("map-iced/", env!("CARGO_PKG_VERSION")).to_string(),
copyright_text: "Map © Map Tiles API | Map data © OpenStreetMap contributors"
.to_string(),
},
TileSource::MapBoxTiles {
tileset_id,
api_key,
} => QuadrupleInfo {
start_url: "https://api.mapbox.com/v4/".to_string() + tileset_id + "/",
end_url: "?access_token=".to_string() + api_key,
user_agent: concat!("map-iced/", env!("CARGO_PKG_VERSION")).to_string(),
copyright_text: if tileset_id == MAPBOX_SATELLITE_TILESET {
MAPBOX_SATELLITE_COPYRIGHT
} else {
"© Mapbox © OpenStreetMap"
}
.to_string(),
},
TileSource::MapBoxSatellite { api_key } => QuadrupleInfo {
start_url: "https://api.mapbox.com/v4/".to_string()
+ MAPBOX_SATELLITE_TILESET
+ "/",
end_url: "?access_token=".to_string() + api_key,
user_agent: concat!("map-iced/", env!("CARGO_PKG_VERSION")).to_string(),
copyright_text: MAPBOX_SATELLITE_COPYRIGHT.to_string(),
},
TileSource::Thunderforest { style, api_key } => QuadrupleInfo {
start_url: "https://api.thunderforest.com/".to_string() + style + "/",
end_url: "?apikey=".to_string() + api_key,
user_agent: concat!("map-iced/", env!("CARGO_PKG_VERSION")).to_string(),
copyright_text: "Maps © www.thunderforest.com, Data © www.osm.org/copyright"
.to_string(),
},
TileSource::TestPattern => return None,
};
Some(quadruple)
}
}
impl TileCacheConfig {
pub const DEFAULT_CACHE_SIZE: u64 = 100_000_000;
pub fn new(source: TileSource, cache: CachingDirectory, cache_size: u64) -> Self {
Self {
cache,
cache_size,
source,
}
}
pub fn open_street_map(user_agent: impl Into<String>) -> Self {
Self::new(
TileSource::OpenStreetMap {
user_agent: user_agent.into(),
},
CachingDirectory::CacheDirectory("OpenStreetMap".into()),
Self::DEFAULT_CACHE_SIZE,
)
}
pub fn test_pattern() -> Self {
Self::new(
TileSource::TestPattern,
CachingDirectory::CacheDirectory("TestPattern".into()),
30_000,
)
}
pub fn from_json_str(s: &str) -> Result<Self, TileCacheError> {
serde_json::from_str(s).map_err(|e| TileCacheError::InvalidJson(e.to_string()))
}
pub(crate) fn build(self) -> Result<TileCache, TileCacheError> {
let path = self.cache.get_path()?;
let system = match self.source.get_quadruple() {
Some(description) => generate_cache(
&description.start_url,
&description.end_url,
&description.user_agent,
description.copyright_text,
path,
self.cache_size,
)
.map_err(|e| match e {
RequesterError::InvalidUserAgent(user_agent) => {
TileCacheError::InvalidUserAgent(user_agent)
}
RequesterError::HttpClient(message) => TileCacheError::HttpClient(message),
})?,
None => generate_dummy_cache(path, self.cache_size),
};
Ok(TileCache::new(system))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_break_in_user_agent_is_reported() {
let config = TileCacheConfig::new(
TileSource::OpenStreetMap {
user_agent: "MyApp\n(contact@example.com)".to_string(),
},
CachingDirectory::FullyConstructed("cache/never_created".into()),
1000,
);
assert!(matches!(
config.build(),
Err(TileCacheError::InvalidUserAgent(_))
));
}
#[test]
fn test_pattern_from_json() {
let config = TileCacheConfig::from_json_str(
r#"{ "cache": "CacheDirFixed", "cache_size": 1000, "source": "TestPattern" }"#,
)
.unwrap();
assert!(config.source.get_quadruple().is_none());
}
fn fully_constructed_start_url(start_url: &str) -> String {
TileSource::FullyConstructed {
start_url: start_url.to_string(),
end_url: String::new(),
user_agent: "test".to_string(),
copyright_text: String::new(),
}
.get_quadruple()
.unwrap()
.start_url
}
#[test]
fn fully_constructed_gets_trailing_slash() {
assert_eq!(
fully_constructed_start_url("https://tiles.example.com/map"),
"https://tiles.example.com/map/"
);
assert_eq!(
fully_constructed_start_url("https://tiles.example.com/map/"),
"https://tiles.example.com/map/"
);
}
#[test]
fn mapbox_satellite_credits_maxar() {
let satellite = TileSource::MapBoxSatellite {
api_key: "key".to_string(),
};
let tileset = TileSource::MapBoxTiles {
tileset_id: MAPBOX_SATELLITE_TILESET.to_string(),
api_key: "key".to_string(),
};
for source in [satellite, tileset] {
let quadruple = source.get_quadruple().unwrap();
assert_eq!(quadruple.copyright_text, MAPBOX_SATELLITE_COPYRIGHT);
assert_eq!(
quadruple.start_url,
"https://api.mapbox.com/v4/mapbox.satellite/"
);
}
}
}