From 276f5db4c68d5e1585f19852d8b9e0269ee71d21 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 19 Oct 2023 00:42:13 -0400 Subject: [PATCH] Implement sprite catalog reporting The `/catalog` now shows available sprites, which also paves way for the future font support. Lots of small refactorings to streamline tile source management. Now each tile source can produce its own catalog entry, making the whole thing much simpler. --- martin/src/config.rs | 37 +++--- martin/src/file_config.rs | 14 +-- martin/src/lib.rs | 5 +- martin/src/mbtiles/mod.rs | 13 +- martin/src/pg/config.rs | 4 +- martin/src/pg/configurator.rs | 28 +++-- martin/src/pg/pg_source.rs | 16 +-- martin/src/pmtiles/mod.rs | 17 ++- martin/src/source.rs | 121 +++++++----------- martin/src/sprites/mod.rs | 139 +++++++++++++-------- martin/src/srv/mod.rs | 2 +- martin/src/srv/server.rs | 68 ++++++---- martin/src/utils/mod.rs | 2 + martin/src/utils/utilities.rs | 6 - martin/src/utils/xyz.rs | 18 +++ tests/expected/auto/catalog_auto.json | 3 +- tests/expected/configured/catalog_cfg.json | 14 +++ 17 files changed, 274 insertions(+), 233 deletions(-) create mode 100644 martin/src/utils/xyz.rs 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..5f34844c4 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,89 @@ 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)| { + ( + id.clone(), + CatalogSpriteEntry { + images: get_svg_input_paths(&source.path, true) + .into_iter() + .map(|svg_path| sprite_name(svg_path, &source.path)) + .collect::>(), + }, + ) + }) + .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 +137,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 +216,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" + ] + } } }