use crate::cli::Commands;
use crate::config::*;
use crate::datasource::wms_fcgi::{HttpRequestParams, MapService};
use crate::datasource::{Datasources, SourceType, TileSource, TileSourceError};
use crate::filter_params::FilterParams;
use crate::store::{
store_reader_from_config, store_writer_from_config, TileReader, TileStoreError, TileWriter,
};
use async_trait::async_trait;
use bbox_core::config::{error_exit, CoreServiceCfg};
use bbox_core::metrics::{no_metrics, NoMetrics};
use bbox_core::ogcapi::ApiLink;
use bbox_core::service::OgcApiService;
use bbox_core::{Compression, Format, TileResponse};
use clap::{ArgMatches, Args, FromArgMatches};
use log::debug;
use martin_mbtiles::Metadata;
use ogcapi_types::tiles::TileMatrixSet;
use serde_json::json;
use std::collections::HashMap;
use std::num::NonZeroU16;
use std::path::PathBuf;
use tile_grid::{tms, BoundingBox, RegistryError, TileMatrixSetOps, Tms, Xyz};
use tilejson::TileJSON;
#[derive(Clone)]
pub struct TileService {
pub(crate) tilesets: Tilesets,
}
pub type Tilesets = HashMap<String, TileSet>;
#[derive(Clone)]
pub struct TileSet {
pub name: String,
pub tms: Vec<TileSetGrid>,
pub source: Box<dyn TileSource>,
format: Format,
pub store_reader: Option<Box<dyn TileReader>>,
pub store_writer: Option<Box<dyn TileWriter>>,
config: TileSetCfg,
cache_cfg: Option<TileStoreCfg>,
cache_limits: Option<CacheLimitCfg>,
cache_control: Vec<CacheControlCfg>,
}
#[derive(Clone)]
pub struct TileSetGrid {
pub tms: Tms,
pub minzoom: u8,
pub maxzoom: u8,
}
#[derive(thiserror::Error, Debug)]
pub enum ServiceError {
#[error("Tileset `{0}` not found")]
TilesetNotFound(String),
#[error("Cache `{0}` not found")]
CacheNotFound(String),
#[error("Unknown format `{0}`")]
UnknownFormat(String),
#[error("Tileset grid not found")] TilesetGridNotFound,
#[error(transparent)]
TileRegistryError(#[from] RegistryError),
#[error(transparent)]
TileSourceError(#[from] TileSourceError),
#[error(transparent)]
TileStoreError(#[from] TileStoreError),
#[error(transparent)]
IoError(#[from] std::io::Error),
}
impl actix_web::error::ResponseError for ServiceError {}
pub trait SourceLookup {
fn source(&self, tileset: &str) -> Option<&dyn TileSource>;
}
impl SourceLookup for Tilesets {
fn source(&self, tileset: &str) -> Option<&dyn TileSource> {
self.get(tileset).map(|ts| ts.source.as_ref())
}
}
type TileStoreConfigs = HashMap<String, TileCacheProviderCfg>;
#[derive(Args, Debug)]
pub struct ServiceArgs {
#[arg(short, long, value_name = "FILE")]
pub t_rex_config: Option<PathBuf>,
}
#[async_trait]
impl OgcApiService for TileService {
type Config = TileServiceCfg;
type CliCommands = Commands;
type CliArgs = ServiceArgs;
type Metrics = NoMetrics;
async fn create(config: &Self::Config, _core_cfg: &CoreServiceCfg) -> Self {
let mut tilesets = HashMap::new();
let mut grids = tms().clone();
for grid in &config.grids {
let custom = TileMatrixSet::from_json_file(&grid.abs_path().to_string_lossy())
.unwrap_or_else(error_exit);
grids
.register(vec![custom], true)
.unwrap_or_else(error_exit);
}
let datasources = Datasources::create(&config.datasources).await;
let stores: TileStoreConfigs = config
.tilestores
.iter()
.cloned()
.map(|cfg| (cfg.name.clone(), cfg))
.collect();
for ts in &config.tilesets {
let ts_grids_cfg = if ts.tms.is_empty() {
vec![TilesetTmsCfg {
id: "WebMercatorQuad".to_string(),
minzoom: None,
maxzoom: None,
}]
} else {
ts.tms.clone()
};
let ts_grids = ts_grids_cfg
.iter()
.map(|cfg| {
let grid = grids.lookup(&cfg.id).unwrap_or_else(error_exit);
TileSetGrid {
tms: grid.clone(),
minzoom: cfg.minzoom.unwrap_or(grid.minzoom()),
maxzoom: cfg.maxzoom.unwrap_or(grid.maxzoom()),
}
})
.collect::<Vec<_>>();
let source = datasources
.setup_tile_source(&ts.source, &ts_grids, &ts_grids_cfg)
.await;
let format = ts
.cache_format
.as_ref()
.and_then(|suffix| Format::from_suffix(suffix))
.unwrap_or(*source.default_format()); let metadata = source
.mbtiles_metadata(ts, &format)
.await
.unwrap_or_else(error_exit);
let cache_cfg = stores
.get("<cli>")
.or(ts
.cache
.as_ref()
.map(|name| {
stores.get(name).cloned().unwrap_or_else(|| {
error_exit(ServiceError::CacheNotFound(name.to_string()))
})
})
.as_ref())
.cloned();
let store_writer = if let Some(config) = &cache_cfg {
Some(
store_writer_from_config(
&config.cache,
&config.compression,
&ts.name,
&format,
metadata,
)
.await,
)
} else {
None
};
let store_reader = if let Some(config) = &cache_cfg {
Some(
store_reader_from_config(&config.cache, &config.compression, &ts.name, &format)
.await,
)
} else {
None
};
let tileset = TileSet {
name: ts.name.clone(),
tms: ts_grids,
source,
format,
store_reader,
store_writer,
config: ts.clone(),
cache_cfg: cache_cfg.map(|cfg| cfg.cache),
cache_limits: ts.cache_limits.clone(),
cache_control: ts.cache_control.clone(),
};
tilesets.insert(ts.name.clone(), tileset);
}
TileService { tilesets }
}
async fn cli_run(&self, cli: &ArgMatches) -> bool {
match Commands::from_arg_matches(cli) {
Ok(Commands::Seed(seedargs)) => {
self.seed_by_grid(&seedargs)
.await
.unwrap_or_else(error_exit);
true
}
Ok(Commands::Upload(uploadargs)) => {
self.upload(&uploadargs).await.unwrap_or_else(error_exit);
true
}
_ => false,
}
}
fn landing_page_links(&self, _api_base: &str) -> Vec<ApiLink> {
vec![
ApiLink {
href: "/tiles".to_string(),
rel: Some("http://www.opengis.net/def/rel/ogc/1.0/tilesets-vector".to_string()),
type_: Some("application/json".to_string()),
title: Some("List of available vector features tilesets".to_string()),
hreflang: None,
length: None,
},
ApiLink {
href: "/tiles".to_string(),
rel: Some("http://www.opengis.net/def/rel/ogc/1.0/tilesets-map".to_string()),
type_: Some("application/json".to_string()),
title: Some("List of available map tilesets".to_string()),
hreflang: None,
length: None,
},
]
}
fn conformance_classes(&self) -> Vec<String> {
vec![
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/core".to_string(),
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/oas30".to_string(),
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/png".to_string(),
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/jpeg".to_string(),
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/mvt".to_string(),
]
}
fn openapi_yaml(&self) -> Option<&str> {
Some(include_str!("openapi.yaml"))
}
fn metrics(&self) -> &'static Self::Metrics {
no_metrics()
}
}
pub struct QueryExtent {
pub extent: BoundingBox,
pub srid: i32,
pub tile_width: NonZeroU16,
pub tile_height: NonZeroU16,
}
impl TileService {
pub fn set_map_service(&mut self, service: &MapService) {
for (_, ts) in self.tilesets.iter_mut() {
ts.source.set_map_service(service);
}
}
pub fn tileset(&self, tileset: &str) -> Option<&TileSet> {
self.tilesets.get(tileset)
}
}
impl TileSet {
pub fn grid(&self, tms_id: &str) -> Result<&Tms, tile_grid::Error> {
self.tms
.iter()
.map(|g| &g.tms)
.find(|tms| tms.id() == tms_id)
.ok_or(RegistryError::TmsNotFound(tms_id.to_string()))
}
pub fn default_grid(&self, zoom: u8) -> Result<&Tms, ServiceError> {
self.tms
.iter()
.find(|grid| zoom >= grid.minzoom && zoom <= grid.maxzoom)
.map(|grid| &grid.tms)
.ok_or(ServiceError::TilesetGridNotFound)
}
pub fn tile_format(&self) -> &Format {
&self.format
}
pub fn is_cachable_at(&self, zoom: u8) -> bool {
if self.store_reader.is_none() {
return false;
}
match self.cache_limits {
Some(ref cl) => cl.minzoom <= zoom && cl.maxzoom.unwrap_or(255) >= zoom,
None => true,
}
}
pub fn cache_control_max_age(&self, zoom: u8) -> Option<u64> {
let entry = self.cache_control.iter().rev().find(|entry| {
entry.minzoom.unwrap_or(0) <= zoom && entry.maxzoom.unwrap_or(255) >= zoom
});
entry.map(|e| e.max_age)
}
pub fn cache_config(&self) -> Option<&TileStoreCfg> {
self.cache_cfg.as_ref()
}
pub fn cache_compression(&self) -> Compression {
self.store_writer
.as_ref()
.map(|s| s.compression())
.unwrap_or(Compression::None)
}
pub async fn read_tile(
&self,
tms: &Tms,
xyz: &Xyz,
filter: &FilterParams,
format: &Format,
compression: Compression,
) -> Result<Vec<u8>, ServiceError> {
let metrics = self.source.wms_metrics();
let request_params = HttpRequestParams {
scheme: "http",
host: "localhost",
req_path: "/",
metrics,
};
let tile = self
.source
.xyz_request(tms, xyz, filter, format, request_params)
.await?;
let data = tile.read_bytes(&compression)?;
Ok(data.body)
}
pub async fn tile_cached(
&self,
tms: &Tms,
xyz: &Xyz,
filter: &FilterParams,
format: &Format,
compression: Compression,
request_params: HttpRequestParams<'_>,
) -> Result<Option<TileResponse>, ServiceError> {
let tileset = self;
if let Some(cache) = &tileset.store_reader {
if tileset.is_cachable_at(xyz.z) {
if let Some(tile) = cache.get_tile(xyz).await? {
debug!("Delivering tile from cache @ {xyz:?}");
let response = tile.with_compression(&compression);
return Ok(Some(response));
}
}
}
debug!("Request tile from source @ {xyz:?}");
let mut tiledata = tileset
.source
.xyz_request(tms, xyz, filter, format, request_params)
.await?;
if let Some(cache_max_age) = tileset.cache_control_max_age(xyz.z) {
tiledata.insert_header(("Cache-Control", format!("max-age={}", cache_max_age)));
}
if tileset.is_cachable_at(xyz.z) {
debug!("Writing tile into cache @ {xyz:?}");
let response_data = tiledata.read_bytes(&tileset.cache_compression())?;
if let Some(cache) = &tileset.store_writer {
cache.put_tile(xyz, response_data.body.clone()).await?;
}
let response = response_data.as_response(&compression);
Ok(Some(response))
} else {
let response = tiledata.with_compression(&compression);
Ok(Some(response))
}
}
pub async fn tilejson(&self, tms: &Tms, base_url: &str) -> Result<TileJSON, ServiceError> {
let mut tilejson = self.source.tilejson(tms, &self.format).await?;
let suffix = tilejson
.other
.get("format")
.map(|v| v.as_str().unwrap_or("pbf"))
.unwrap_or("pbf");
let format =
Format::from_suffix(suffix).ok_or(ServiceError::UnknownFormat(suffix.to_string()))?;
tilejson.tiles.push(format!(
"{base_url}/{tileset}/{{z}}/{{x}}/{{y}}.{format}",
tileset = &self.name,
format = format.file_suffix()
));
Ok(tilejson)
}
pub async fn mbtiles_metadata(&self) -> Result<Metadata, ServiceError> {
Ok(self
.source
.mbtiles_metadata(&self.config, &self.format)
.await?)
}
pub async fn stylejson(
&self,
base_url: &str,
base_path: &str,
) -> Result<serde_json::Value, ServiceError> {
let ts = self;
let tileset = &self.name;
let suffix = ts.tile_format().file_suffix();
let source_type = ts.source.source_type();
let ts_source = match source_type {
SourceType::Vector => json!({
"type": "vector",
"url": format!("{base_url}{base_path}/{tileset}.json")
}),
SourceType::Raster => json!({
"type": "raster",
"tiles": [format!("{base_url}{base_path}/{tileset}/{{z}}/{{x}}/{{y}}.{suffix}")],
}),
};
let layers = ts.source.layers().await?;
let mut layer_styles: Vec<serde_json::Value> = layers
.iter()
.map(|layer| {
let default_type = if let Some(ref geomtype) = layer.geometry_type {
match geomtype as &str {
"POINT" => "circle",
"fill" => "fill",
"line" => "line",
"symbol" => "symbol",
"circle" => "circle",
"heatmap" => "heatmap",
"fill-extrusion" => "fill-extrusion",
"raster" => "raster",
"hillshade" => "hillshade",
"background" => "background",
_ => "line",
}
} else {
match source_type {
SourceType::Vector => "line",
SourceType::Raster => "raster",
}
};
let mut layerjson =
json!({"id": layer.name, "source": tileset, "type": default_type});
if source_type == SourceType::Vector {
layerjson["source-layer"] = json!(layer.name);
}
if let Some(style) = &layer.style {
layerjson
.as_object_mut()
.expect("object")
.append(style.clone().as_object_mut().expect("object"));
}
layerjson
})
.collect();
if source_type == SourceType::Vector {
let background_layer = json!({
"id": "background_",
"type": "background",
"paint": {
"background-color": "rgba(255, 255, 255, 1)"
}
});
layer_styles.insert(0, background_layer);
}
let stylejson = json!({
"version": 8,
"name": tileset,
"metadata": {
"maputnik:renderer": "mbgljs"
},
"glyphs": "https://go-spatial.github.io/carto-assets/fonts/{fontstack}/{range}.pbf",
"sources": {
tileset: ts_source
},
"layers": layer_styles
});
Ok(stylejson)
}
}
pub trait TmsExtensions {
fn id(&self) -> &str;
fn srid(&self) -> i32;
fn xyz_extent(&self, xyz: &Xyz) -> Result<QueryExtent, TileSourceError>;
}
impl TmsExtensions for Tms {
fn id(&self) -> &str {
&self.tms.id
}
fn srid(&self) -> i32 {
self.crs().as_srid()
}
fn xyz_extent(&self, xyz: &Xyz) -> Result<QueryExtent, TileSourceError> {
if !self.is_valid(xyz) {
return Err(TileSourceError::TileXyzError);
}
let extent = self.xy_bounds(xyz);
let srid = self.srid();
let tile_matrix = self.matrix(xyz.z);
let tile_width = tile_matrix.as_ref().tile_width;
let tile_height = tile_matrix.as_ref().tile_height;
Ok(QueryExtent {
extent,
srid,
tile_width,
tile_height,
})
}
}