diff --git a/martin/src/config.rs b/martin/src/config.rs index b477fd6f4..5856bed23 100644 --- a/martin/src/config.rs +++ b/martin/src/config.rs @@ -13,16 +13,16 @@ use crate::file_config::{resolve_files, FileConfigEnum}; use crate::mbtiles::MbtSource; use crate::pg::PgConfig; use crate::pmtiles::PmtSource; -use crate::source::Sources; -use crate::sprites::{resolve_sprites, SpriteSources}; +use crate::source::{TileInfoSources, TileSources}; +use crate::sprites::SpriteSources; use crate::srv::SrvConfig; -use crate::utils::{IdResolver, OneOrMany, Result}; use crate::Error::{ConfigLoadError, ConfigParseError, NoSources}; +use crate::{IdResolver, OneOrMany, Result}; pub type UnrecognizedValues = HashMap; -pub struct AllSources { - pub sources: Sources, +pub struct ServerState { + pub tiles: TileSources, pub sprites: SpriteSources, } @@ -90,16 +90,24 @@ impl Config { } } - pub async fn resolve(&mut self, idr: IdResolver) -> Result { + pub async fn resolve(&mut self, idr: IdResolver) -> Result { + Ok(ServerState { + tiles: self.resolve_tile_sources(idr).await?, + sprites: SpriteSources::resolve(&mut self.sprites)?, + }) + } + + async fn resolve_tile_sources(&mut self, idr: IdResolver) -> Result { let create_pmt_src = &mut PmtSource::new_box; let create_mbt_src = &mut MbtSource::new_box; + let mut sources: Vec>>>> = Vec::new(); - let mut sources: Vec>>>> = Vec::new(); if let Some(v) = self.postgres.as_mut() { for s in v.iter_mut() { sources.push(Box::pin(s.resolve(idr.clone()))); } } + if self.pmtiles.is_some() { let val = resolve_files(&mut self.pmtiles, idr.clone(), "pmtiles", create_pmt_src); sources.push(Box::pin(val)); @@ -110,20 +118,7 @@ impl Config { sources.push(Box::pin(val)); } - // Minor in-efficiency: - // Sources are added to a BTreeMap, then iterated over into a sort structure and convert back to a BTreeMap. - // Ideally there should be a vector of values, which is then sorted (in-place?) and converted to a BTreeMap. - Ok(AllSources { - sources: try_join_all(sources) - .await? - .into_iter() - .fold(Sources::default(), |mut acc, hashmap| { - acc.extend(hashmap); - acc - }) - .sort(), - sprites: resolve_sprites(&mut self.sprites)?, - }) + Ok(TileSources::new(try_join_all(sources).await?)) } } diff --git a/martin/src/file_config.rs b/martin/src/file_config.rs index b290356cf..9cb55a359 100644 --- a/martin/src/file_config.rs +++ b/martin/src/file_config.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use crate::config::{copy_unrecognized_config, UnrecognizedValues}; use crate::file_config::FileError::{InvalidFilePath, InvalidSourceFilePath, IoError}; -use crate::source::{Source, Sources}; +use crate::source::{Source, TileInfoSources}; use crate::utils::{sorted_opt_map, Error, IdResolver, OneOrMany}; use crate::OneOrMany::{Many, One}; @@ -152,7 +152,7 @@ pub async fn resolve_files( idr: IdResolver, extension: &str, create_source: &mut impl FnMut(String, PathBuf) -> Fut, -) -> Result +) -> Result where Fut: Future, FileError>>, { @@ -166,16 +166,16 @@ async fn resolve_int( idr: IdResolver, extension: &str, create_source: &mut impl FnMut(String, PathBuf) -> Fut, -) -> Result +) -> Result where Fut: Future, FileError>>, { let Some(cfg) = config else { - return Ok(Sources::default()); + return Ok(TileInfoSources::default()); }; let cfg = cfg.extract_file_config(); - let mut results = Sources::default(); + let mut results = TileInfoSources::default(); let mut configs = HashMap::new(); let mut files = HashSet::new(); let mut directories = Vec::new(); @@ -198,7 +198,7 @@ where FileConfigSrc::Obj(pmt) => pmt.path, FileConfigSrc::Path(path) => path, }; - results.insert(id.clone(), create_source(id, path).await?); + results.push(create_source(id, path).await?); } } @@ -244,7 +244,7 @@ where FileConfigSrc::Obj(pmt) => pmt.path, FileConfigSrc::Path(path) => path, }; - results.insert(id.clone(), create_source(id, path).await?); + results.push(create_source(id, path).await?); } } } diff --git a/martin/src/lib.rs b/martin/src/lib.rs index bdc115c76..6915d0517 100644 --- a/martin/src/lib.rs +++ b/martin/src/lib.rs @@ -18,6 +18,7 @@ mod source; pub mod sprites; pub mod srv; mod utils; +pub use utils::Xyz; #[cfg(test)] #[path = "utils/test_utils.rs"] @@ -27,8 +28,8 @@ mod test_utils; // Must make it accessible as carte::Env from both places when testing. #[cfg(test)] pub use crate::args::Env; -pub use crate::config::{read_config, Config}; -pub use crate::source::{Source, Sources, Xyz}; +pub use crate::config::{read_config, Config, ServerState}; +pub use crate::source::Source; pub use crate::utils::{ decode_brotli, decode_gzip, BoolOrObject, Error, IdResolver, OneOrMany, Result, }; diff --git a/martin/src/mbtiles/mod.rs b/martin/src/mbtiles/mod.rs index 79dbb50e3..efbd74c94 100644 --- a/martin/src/mbtiles/mod.rs +++ b/martin/src/mbtiles/mod.rs @@ -12,7 +12,6 @@ use tilejson::TileJSON; use crate::file_config::FileError; use crate::file_config::FileError::{AquireConnError, InvalidMetadata, IoError}; use crate::source::{Tile, UrlQuery}; -use crate::utils::is_valid_zoom; use crate::{Error, Source, Xyz}; #[derive(Clone)] @@ -66,8 +65,12 @@ impl MbtSource { #[async_trait] impl Source for MbtSource { - fn get_tilejson(&self) -> TileJSON { - self.tilejson.clone() + fn get_id(&self) -> &str { + &self.id + } + + fn get_tilejson(&self) -> &TileJSON { + &self.tilejson } fn get_tile_info(&self) -> TileInfo { @@ -78,10 +81,6 @@ impl Source for MbtSource { Box::new(self.clone()) } - fn is_valid_zoom(&self, zoom: u8) -> bool { - is_valid_zoom(zoom, self.tilejson.minzoom, self.tilejson.maxzoom) - } - fn support_url_query(&self) -> bool { false } diff --git a/martin/src/pg/config.rs b/martin/src/pg/config.rs index 1a46bb90c..1934756cb 100644 --- a/martin/src/pg/config.rs +++ b/martin/src/pg/config.rs @@ -10,7 +10,7 @@ use crate::pg::config_function::FuncInfoSources; use crate::pg::config_table::TableInfoSources; use crate::pg::configurator::PgBuilder; use crate::pg::Result; -use crate::source::Sources; +use crate::source::TileInfoSources; use crate::utils::{on_slow, sorted_opt_map, BoolOrObject, IdResolver, OneOrMany}; pub trait PgInfo { @@ -111,7 +111,7 @@ impl PgConfig { Ok(res) } - pub async fn resolve(&mut self, id_resolver: IdResolver) -> crate::Result { + pub async fn resolve(&mut self, id_resolver: IdResolver) -> crate::Result { let pg = PgBuilder::new(self, id_resolver).await?; let inst_tables = on_slow(pg.instantiate_tables(), Duration::from_secs(5), || { if pg.disable_bounds() { diff --git a/martin/src/pg/configurator.rs b/martin/src/pg/configurator.rs index 5acdeb65b..0cd51d615 100755 --- a/martin/src/pg/configurator.rs +++ b/martin/src/pg/configurator.rs @@ -17,7 +17,7 @@ use crate::pg::table_source::{ use crate::pg::utils::{find_info, find_kv_ignore_case, normalize_key, InfoMap}; use crate::pg::PgError::InvalidTableExtent; use crate::pg::Result; -use crate::source::Sources; +use crate::source::TileInfoSources; use crate::utils::{BoolOrObject, IdResolver, OneOrMany}; pub type SqlFuncInfoMapMap = InfoMap>; @@ -73,7 +73,7 @@ impl PgBuilder { // FIXME: this function has gotten too long due to the new formatting rules, need to be refactored #[allow(clippy::too_many_lines)] - pub async fn instantiate_tables(&self) -> Result<(Sources, TableInfoSources)> { + pub async fn instantiate_tables(&self) -> Result<(TileInfoSources, TableInfoSources)> { let mut db_tables_info = query_available_tables(&self.pool).await?; // Match configured sources with the discovered ones and add them to the pending list. @@ -170,7 +170,7 @@ impl PgBuilder { } } - let mut res = Sources::default(); + let mut res = TileInfoSources::default(); let mut info_map = TableInfoSources::new(); let pending = join_all(pending).await; for src in pending { @@ -190,9 +190,9 @@ impl PgBuilder { Ok((res, info_map)) } - pub async fn instantiate_functions(&self) -> Result<(Sources, FuncInfoSources)> { + pub async fn instantiate_functions(&self) -> Result<(TileInfoSources, FuncInfoSources)> { let mut db_funcs_info = query_available_function(&self.pool).await?; - let mut res = Sources::default(); + let mut res = TileInfoSources::default(); let mut info_map = FuncInfoSources::new(); let mut used = HashSet::<(&str, &str)>::new(); @@ -262,14 +262,16 @@ impl PgBuilder { self.id_resolver.resolve(id, signature) } - fn add_func_src(&self, sources: &mut Sources, id: String, info: &impl PgInfo, sql: PgSqlInfo) { - let source = PgSource::new( - id.clone(), - sql, - info.to_tilejson(id.clone()), - self.pool.clone(), - ); - sources.insert(id, Box::new(source)); + fn add_func_src( + &self, + sources: &mut TileInfoSources, + id: String, + info: &impl PgInfo, + sql: PgSqlInfo, + ) { + let tilejson = info.to_tilejson(id.clone()); + let source = PgSource::new(id, sql, tilejson, self.pool.clone()); + sources.push(Box::new(source)); } } diff --git a/martin/src/pg/pg_source.rs b/martin/src/pg/pg_source.rs index 05d7090ea..735d747af 100644 --- a/martin/src/pg/pg_source.rs +++ b/martin/src/pg/pg_source.rs @@ -11,8 +11,8 @@ use tilejson::TileJSON; use crate::pg::pool::PgPool; use crate::pg::utils::query_to_json; use crate::pg::PgError::{GetTileError, GetTileWithQueryError, PrepareQueryError}; -use crate::source::{Source, Tile, UrlQuery, Xyz}; -use crate::utils::{is_valid_zoom, Result}; +use crate::source::{Source, Tile, UrlQuery}; +use crate::{Result, Xyz}; #[derive(Clone, Debug)] pub struct PgSource { @@ -36,8 +36,12 @@ impl PgSource { #[async_trait] impl Source for PgSource { - fn get_tilejson(&self) -> TileJSON { - self.tilejson.clone() + fn get_id(&self) -> &str { + &self.id + } + + fn get_tilejson(&self) -> &TileJSON { + &self.tilejson } fn get_tile_info(&self) -> TileInfo { @@ -48,10 +52,6 @@ impl Source for PgSource { Box::new(self.clone()) } - fn is_valid_zoom(&self, zoom: u8) -> bool { - is_valid_zoom(zoom, self.tilejson.minzoom, self.tilejson.maxzoom) - } - fn support_url_query(&self) -> bool { self.info.use_url_query } diff --git a/martin/src/pmtiles/mod.rs b/martin/src/pmtiles/mod.rs index 98e3a86b6..2af8cf9e5 100644 --- a/martin/src/pmtiles/mod.rs +++ b/martin/src/pmtiles/mod.rs @@ -13,9 +13,8 @@ use tilejson::TileJSON; use crate::file_config::FileError; use crate::file_config::FileError::{InvalidMetadata, IoError}; -use crate::source::{Source, Tile, UrlQuery, Xyz}; -use crate::utils::is_valid_zoom; -use crate::Error; +use crate::source::{Source, Tile, UrlQuery}; +use crate::{Error, Xyz}; #[derive(Clone)] pub struct PmtSource { @@ -114,8 +113,12 @@ impl PmtSource { #[async_trait] impl Source for PmtSource { - fn get_tilejson(&self) -> TileJSON { - self.tilejson.clone() + fn get_id(&self) -> &str { + &self.id + } + + fn get_tilejson(&self) -> &TileJSON { + &self.tilejson } fn get_tile_info(&self) -> TileInfo { @@ -126,10 +129,6 @@ impl Source for PmtSource { Box::new(self.clone()) } - fn is_valid_zoom(&self, zoom: u8) -> bool { - is_valid_zoom(zoom, self.tilejson.minzoom, self.tilejson.maxzoom) - } - fn support_url_query(&self) -> bool { false } diff --git a/martin/src/source.rs b/martin/src/source.rs index 0292166fb..bc6767120 100644 --- a/martin/src/source.rs +++ b/martin/src/source.rs @@ -1,5 +1,5 @@ use std::collections::{BTreeMap, HashMap}; -use std::fmt::{Debug, Display, Formatter}; +use std::fmt::Debug; use actix_web::error::ErrorNotFound; use async_trait::async_trait; @@ -9,83 +9,42 @@ use martin_tile_utils::TileInfo; use serde::{Deserialize, Serialize}; use tilejson::TileJSON; -use crate::utils::Result; - -#[derive(Debug, Copy, Clone)] -pub struct Xyz { - pub z: u8, - pub x: u32, - pub y: u32, -} - -impl Display for Xyz { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if f.alternate() { - write!(f, "{}/{}/{}", self.z, self.x, self.y) - } else { - write!(f, "{},{},{}", self.z, self.x, self.y) - } - } -} +use crate::{Result, Xyz}; pub type Tile = Vec; pub type UrlQuery = HashMap; -#[derive(Default, Clone)] -pub struct Sources { - tiles: HashMap>, - catalog: SourceCatalog, -} +pub type TileInfoSource = Box; -impl Sources { - #[must_use] - pub fn sort(self) -> Self { - Self { - tiles: self.tiles, - catalog: SourceCatalog { - tiles: self - .catalog - .tiles - .into_iter() - .sorted_by(|a, b| a.0.cmp(&b.0)) - .collect(), - }, - } - } -} +pub type TileInfoSources = Vec; -impl Sources { - pub fn insert(&mut self, id: String, source: Box) { - let tilejson = source.get_tilejson(); - let info = source.get_tile_info(); - self.catalog.tiles.insert( - id.clone(), - SourceEntry { - content_type: info.format.content_type().to_string(), - content_encoding: info.encoding.content_encoding().map(ToString::to_string), - name: tilejson.name.filter(|v| v != &id), - description: tilejson.description, - attribution: tilejson.attribution, - }, - ); - self.tiles.insert(id, source); - } +#[derive(Default, Clone)] +pub struct TileSources(HashMap>); +pub type TileCatalog = BTreeMap; - pub fn extend(&mut self, other: Sources) { - for (k, v) in other.catalog.tiles { - self.catalog.tiles.insert(k, v); - } - self.tiles.extend(other.tiles); +impl TileSources { + #[must_use] + pub fn new(sources: Vec) -> Self { + Self( + sources + .into_iter() + .flatten() + .map(|src| (src.get_id().to_string(), src)) + .collect(), + ) } - #[must_use] - pub fn get_catalog(&self) -> &SourceCatalog { - &self.catalog + pub fn get_catalog(&self) -> TileCatalog { + self.0 + .iter() + .map(|(id, src)| (id.to_string(), src.get_catalog_entry())) + .sorted_by(|(id1, _), (id2, _)| id1.cmp(id2)) + .collect() } pub fn get_source(&self, id: &str) -> actix_web::Result<&dyn Source> { Ok(self - .tiles + .0 .get(id) .ok_or_else(|| ErrorNotFound(format!("Source {id} does not exist")))? .as_ref()) @@ -138,17 +97,36 @@ impl Sources { #[async_trait] pub trait Source: Send + Debug { - fn get_tilejson(&self) -> TileJSON; + fn get_id(&self) -> &str; + + fn get_tilejson(&self) -> &TileJSON; fn get_tile_info(&self) -> TileInfo; fn clone_source(&self) -> Box; - fn is_valid_zoom(&self, zoom: u8) -> bool; - fn support_url_query(&self) -> bool; async fn get_tile(&self, xyz: &Xyz, query: &Option) -> Result; + + fn is_valid_zoom(&self, zoom: u8) -> bool { + let tj = self.get_tilejson(); + tj.minzoom.map_or(true, |minzoom| zoom >= minzoom) + && tj.maxzoom.map_or(true, |maxzoom| zoom <= maxzoom) + } + + fn get_catalog_entry(&self) -> CatalogSourceEntry { + let id = self.get_id(); + let tilejson = self.get_tilejson(); + let info = self.get_tile_info(); + CatalogSourceEntry { + content_type: info.format.content_type().to_string(), + content_encoding: info.encoding.content_encoding().map(ToString::to_string), + name: tilejson.name.as_ref().filter(|v| *v != id).cloned(), + description: tilejson.description.clone(), + attribution: tilejson.attribution.clone(), + } + } } impl Clone for Box { @@ -158,12 +136,7 @@ impl Clone for Box { } #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] -pub struct SourceCatalog { - tiles: BTreeMap, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] -pub struct SourceEntry { +pub struct CatalogSourceEntry { pub content_type: String, #[serde(skip_serializing_if = "Option::is_none")] pub content_encoding: Option, diff --git a/martin/src/sprites/mod.rs b/martin/src/sprites/mod.rs index c1b5943ed..cb120d4b9 100644 --- a/martin/src/sprites/mod.rs +++ b/martin/src/sprites/mod.rs @@ -1,10 +1,12 @@ use std::collections::hash_map::Entry; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt::Debug; use std::path::PathBuf; use futures::future::try_join_all; +use itertools::Itertools; use log::{info, warn}; +use serde::{Deserialize, Serialize}; use spreet::fs::get_svg_input_paths; use spreet::resvg::usvg::{Error as ResvgError, Options, Tree, TreeParsing}; use spreet::sprite::{sprite_name, Sprite, Spritesheet, SpritesheetBuilder}; @@ -42,68 +44,86 @@ pub enum SpriteError { UnableToGenerateSpritesheet, } -pub fn resolve_sprites(config: &mut Option) -> Result { - let Some(cfg) = config else { - return Ok(SpriteSources::default()); - }; +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct CatalogSpriteEntry { + pub images: Vec, +} - let cfg = cfg.extract_file_config(); - let mut results = SpriteSources::default(); - let mut directories = Vec::new(); - let mut configs = HashMap::new(); +pub type SpriteCatalog = BTreeMap; - if let Some(sources) = cfg.sources { - for (id, source) in sources { - configs.insert(id.clone(), source.clone()); - add_source(id, source.abs_path()?, &mut results); - } - }; - - if let Some(paths) = cfg.paths { - for path in paths { - let Some(name) = path.file_name() else { - warn!( - "Ignoring sprite source with no name from {}", - path.display() - ); - continue; - }; - directories.push(path.clone()); - add_source(name.to_string_lossy().to_string(), path, &mut results); - } - } +#[derive(Debug, Clone, Default)] +pub struct SpriteSources(HashMap); - *config = FileConfigEnum::new_extended(directories, configs, cfg.unrecognized); +impl SpriteSources { + pub fn resolve(config: &mut Option) -> Result { + let Some(cfg) = config else { + return Ok(Self::default()); + }; - Ok(results) -} + let cfg = cfg.extract_file_config(); + let mut results = Self::default(); + let mut directories = Vec::new(); + let mut configs = HashMap::new(); -fn add_source(id: String, path: PathBuf, results: &mut SpriteSources) { - let disp_path = path.display(); - if path.is_file() { - warn!("Ignoring non-directory sprite source {id} from {disp_path}"); - } else { - match results.0.entry(id) { - Entry::Occupied(v) => { - warn!("Ignoring duplicate sprite source {} from {disp_path} because it was already configured for {}", - v.key(), v.get().path.display()); + if let Some(sources) = cfg.sources { + for (id, source) in sources { + configs.insert(id.clone(), source.clone()); + results.add_source(id, source.abs_path()?); } - Entry::Vacant(v) => { - info!("Configured sprite source {} from {disp_path}", v.key()); - v.insert(SpriteSource { path }); + }; + + if let Some(paths) = cfg.paths { + for path in paths { + let Some(name) = path.file_name() else { + warn!( + "Ignoring sprite source with no name from {}", + path.display() + ); + continue; + }; + directories.push(path.clone()); + results.add_source(name.to_string_lossy().to_string(), path); } } - }; -} -#[derive(Debug, Clone, Default)] -pub struct SpriteSources(HashMap); + *config = FileConfigEnum::new_extended(directories, configs, cfg.unrecognized); -impl SpriteSources { - pub fn get_sprite_source(&self, id: &str) -> Result<&SpriteSource, SpriteError> { - self.0 - .get(id) - .ok_or_else(|| SpriteError::SpriteNotFound(id.to_string())) + Ok(results) + } + + pub fn get_catalog(&self) -> Result { + // TODO: all sprite generation should be pre-cached + Ok(self + .0 + .iter() + .sorted_by(|(id1, _), (id2, _)| id1.cmp(id2)) + .map(|(id, source)| { + let mut images = get_svg_input_paths(&source.path, true) + .into_iter() + .map(|svg_path| sprite_name(svg_path, &source.path)) + .collect::>(); + images.sort(); + (id.clone(), CatalogSpriteEntry { images }) + }) + .collect()) + } + + fn add_source(&mut self, id: String, path: PathBuf) { + let disp_path = path.display(); + if path.is_file() { + warn!("Ignoring non-directory sprite source {id} from {disp_path}"); + } else { + match self.0.entry(id) { + Entry::Occupied(v) => { + warn!("Ignoring duplicate sprite source {} from {disp_path} because it was already configured for {}", + v.key(), v.get().path.display()); + } + Entry::Vacant(v) => { + info!("Configured sprite source {} from {disp_path}", v.key()); + v.insert(SpriteSource { path }); + } + } + }; } /// Given a list of IDs in a format "id1,id2,id3", return a spritesheet with them all. @@ -114,10 +134,16 @@ impl SpriteSources { } else { (ids, 1) }; + let sprite_ids = ids .split(',') - .map(|id| self.get_sprite_source(id)) + .map(|id| { + self.0 + .get(id) + .ok_or_else(|| SpriteError::SpriteNotFound(id.to_string())) + }) .collect::, SpriteError>>()?; + get_spritesheet(sprite_ids.into_iter(), dpi).await } } @@ -187,7 +213,7 @@ mod tests { PathBuf::from("../tests/fixtures/sprites/src2"), ]); - let sprites = resolve_sprites(&mut cfg).unwrap().0; + let sprites = SpriteSources::resolve(&mut cfg).unwrap().0; assert_eq!(sprites.len(), 2); test_src(sprites.values(), 1, "all_1").await; diff --git a/martin/src/srv/mod.rs b/martin/src/srv/mod.rs index f88dbe558..c637ca755 100644 --- a/martin/src/srv/mod.rs +++ b/martin/src/srv/mod.rs @@ -4,4 +4,4 @@ mod server; pub use config::{SrvConfig, KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT}; pub use server::{new_server, router, RESERVED_KEYWORDS}; -pub use crate::source::SourceEntry; +pub use crate::source::CatalogSourceEntry; diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index 345b992ca..f058a72c0 100755 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -17,17 +17,19 @@ use actix_web::{ Result, }; use futures::future::try_join_all; +use itertools::Itertools as _; use log::error; use martin_tile_utils::{Encoding, Format, TileInfo}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use tilejson::{tilejson, TileJSON}; -use crate::config::AllSources; -use crate::source::{Source, Sources, UrlQuery, Xyz}; -use crate::sprites::{SpriteError, SpriteSources}; +use crate::config::ServerState; +use crate::source::{Source, TileCatalog, TileSources, UrlQuery}; +use crate::sprites::{SpriteCatalog, SpriteError, SpriteSources}; use crate::srv::config::{SrvConfig, KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT}; use crate::utils::{decode_brotli, decode_gzip, encode_brotli, encode_gzip}; use crate::Error::BindingError; +use crate::Xyz; /// List of keywords that cannot be used as source IDs. Some of these are reserved for future use. /// Reserved keywords must never end in a "dot number" (e.g. ".1"). @@ -43,6 +45,12 @@ static SUPPORTED_ENCODINGS: &[HeaderEnc] = &[ HeaderEnc::identity(), ]; +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct Catalog { + pub tiles: TileCatalog, + pub sprites: SpriteCatalog, +} + #[derive(Deserialize)] struct TileJsonRequest { source_ids: String, @@ -95,8 +103,8 @@ async fn get_health() -> impl Responder { wrap = "middleware::Compress::default()" )] #[allow(clippy::unused_async)] -async fn get_catalog(sources: Data) -> impl Responder { - HttpResponse::Ok().json(sources.get_catalog()) +async fn get_catalog(catalog: Data) -> impl Responder { + HttpResponse::Ok().json(catalog) } #[route("/sprite/{source_ids}.png", method = "GET", method = "HEAD")] @@ -140,7 +148,7 @@ async fn get_sprite_json( async fn git_source_info( req: HttpRequest, path: Path, - sources: Data, + sources: Data, ) -> Result { let sources = sources.get_sources(&path.source_ids, None)?.0; @@ -174,7 +182,7 @@ fn get_tiles_url(scheme: &str, host: &str, query_string: &str, tiles_path: &str) fn merge_tilejson(sources: Vec<&dyn Source>, tiles_url: String) -> TileJSON { if sources.len() == 1 { - let mut tj = sources[0].get_tilejson(); + let mut tj = sources[0].get_tilejson().clone(); tj.tiles = vec![tiles_url]; return tj; } @@ -189,15 +197,15 @@ fn merge_tilejson(sources: Vec<&dyn Source>, tiles_url: String) -> TileJSON { for src in sources { let tj = src.get_tilejson(); - if let Some(vector_layers) = tj.vector_layers { + if let Some(vector_layers) = &tj.vector_layers { if let Some(ref mut a) = result.vector_layers { - a.extend(vector_layers); + a.extend(vector_layers.iter().cloned()); } else { - result.vector_layers = Some(vector_layers); + result.vector_layers = Some(vector_layers.clone()); } } - if let Some(v) = tj.attribution { + if let Some(v) = &tj.attribution { if !attributions.contains(&v) { attributions.push(v); } @@ -216,7 +224,7 @@ fn merge_tilejson(sources: Vec<&dyn Source>, tiles_url: String) -> TileJSON { result.center = tj.center; } - if let Some(v) = tj.description { + if let Some(v) = &tj.description { if !descriptions.contains(&v) { descriptions.push(v); } @@ -242,7 +250,7 @@ fn merge_tilejson(sources: Vec<&dyn Source>, tiles_url: String) -> TileJSON { } } - if let Some(name) = tj.name { + if let Some(name) = &tj.name { if !names.contains(&name) { names.push(name); } @@ -250,15 +258,15 @@ fn merge_tilejson(sources: Vec<&dyn Source>, tiles_url: String) -> TileJSON { } if !attributions.is_empty() { - result.attribution = Some(attributions.join("\n")); + result.attribution = Some(attributions.into_iter().join("\n")); } if !descriptions.is_empty() { - result.description = Some(descriptions.join("\n")); + result.description = Some(descriptions.into_iter().join("\n")); } if !names.is_empty() { - result.name = Some(names.join(",")); + result.name = Some(names.into_iter().join(",")); } result @@ -268,7 +276,7 @@ fn merge_tilejson(sources: Vec<&dyn Source>, tiles_url: String) -> TileJSON { async fn get_tile( req: HttpRequest, path: Path, - sources: Data, + sources: Data, ) -> Result { let xyz = Xyz { z: path.z, @@ -306,7 +314,7 @@ async fn get_tile( let id = &path.source_ids; let zoom = xyz.z; let src = sources.get_source(id)?; - if !Sources::check_zoom(src, id, zoom) { + if !TileSources::check_zoom(src, id, zoom) { return Err(ErrorNotFound(format!( "Zoom {zoom} is not valid for source {id}", ))); @@ -413,21 +421,27 @@ pub fn router(cfg: &mut web::ServiceConfig) { } /// Create a new initialized Actix `App` instance together with the listening address. -pub fn new_server(config: SrvConfig, all_sources: AllSources) -> crate::Result<(Server, String)> { +pub fn new_server(config: SrvConfig, all_sources: ServerState) -> crate::Result<(Server, String)> { let keep_alive = Duration::from_secs(config.keep_alive.unwrap_or(KEEP_ALIVE_DEFAULT)); let worker_processes = config.worker_processes.unwrap_or_else(num_cpus::get); let listen_addresses = config .listen_addresses .unwrap_or_else(|| LISTEN_ADDRESSES_DEFAULT.to_owned()); + let catalog = Catalog { + tiles: all_sources.tiles.get_catalog(), + sprites: all_sources.sprites.get_catalog()?, + }; + let server = HttpServer::new(move || { let cors_middleware = Cors::default() .allow_any_origin() .allowed_methods(vec!["GET"]); App::new() - .app_data(Data::new(all_sources.sources.clone())) + .app_data(Data::new(all_sources.tiles.clone())) .app_data(Data::new(all_sources.sprites.clone())) + .app_data(Data::new(catalog.clone())) .wrap(cors_middleware) .wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly)) .wrap(middleware::Logger::default()) @@ -469,19 +483,19 @@ mod tests { #[async_trait] impl Source for TestSource { - fn get_tilejson(&self) -> TileJSON { - self.tj.clone() + fn get_id(&self) -> &str { + "id" } - fn get_tile_info(&self) -> TileInfo { - unimplemented!() + fn get_tilejson(&self) -> &TileJSON { + &self.tj } - fn clone_source(&self) -> Box { + fn get_tile_info(&self) -> TileInfo { unimplemented!() } - fn is_valid_zoom(&self, _zoom: u8) -> bool { + fn clone_source(&self) -> Box { unimplemented!() } diff --git a/martin/src/utils/mod.rs b/martin/src/utils/mod.rs index 43aff165d..85534e500 100644 --- a/martin/src/utils/mod.rs +++ b/martin/src/utils/mod.rs @@ -2,8 +2,10 @@ mod error; mod id_resolver; mod one_or_many; mod utilities; +mod xyz; pub use error::*; pub use id_resolver::IdResolver; pub use one_or_many::OneOrMany; pub use utilities::*; +pub use xyz::Xyz; diff --git a/martin/src/utils/utilities.rs b/martin/src/utils/utilities.rs index 6635990bb..e05e79276 100644 --- a/martin/src/utils/utilities.rs +++ b/martin/src/utils/utilities.rs @@ -9,12 +9,6 @@ use futures::pin_mut; use serde::{Deserialize, Serialize, Serializer}; use tokio::time::timeout; -#[must_use] -pub fn is_valid_zoom(zoom: u8, minzoom: Option, maxzoom: Option) -> bool { - minzoom.map_or(true, |minzoom| zoom >= minzoom) - && maxzoom.map_or(true, |maxzoom| zoom <= maxzoom) -} - /// A serde helper to store a boolean as an object. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(untagged)] diff --git a/martin/src/utils/xyz.rs b/martin/src/utils/xyz.rs new file mode 100644 index 000000000..599ebd5e6 --- /dev/null +++ b/martin/src/utils/xyz.rs @@ -0,0 +1,18 @@ +use std::fmt::{Display, Formatter}; + +#[derive(Debug, Copy, Clone)] +pub struct Xyz { + pub z: u8, + pub x: u32, + pub y: u32, +} + +impl Display for Xyz { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if f.alternate() { + write!(f, "{}/{}/{}", self.z, self.x, self.y) + } else { + write!(f, "{},{},{}", self.z, self.x, self.y) + } + } +} diff --git a/tests/expected/auto/catalog_auto.json b/tests/expected/auto/catalog_auto.json index 1de39eb3b..3ce4162f5 100644 --- a/tests/expected/auto/catalog_auto.json +++ b/tests/expected/auto/catalog_auto.json @@ -162,5 +162,6 @@ "name": "Major cities from Natural Earth data", "description": "Major cities from Natural Earth data" } - } + }, + "sprites": {} } diff --git a/tests/expected/configured/catalog_cfg.json b/tests/expected/configured/catalog_cfg.json index 03372aca3..2fb48ab5d 100644 --- a/tests/expected/configured/catalog_cfg.json +++ b/tests/expected/configured/catalog_cfg.json @@ -39,5 +39,19 @@ "content_type": "application/x-protobuf", "description": "public.table_source.geom" } + }, + "sprites": { + "mysrc": { + "images": [ + "bicycle" + ] + }, + "src1": { + "images": [ + "another_bicycle", + "bear", + "sub/circle" + ] + } } }