1use crate::cli::Commands;
2use crate::config::*;
3use crate::datasource::wms_fcgi::{HttpRequestParams, MapService};
4use crate::datasource::{Datasources, SourceType, TileSource, TileSourceError};
5use crate::filter_params::FilterParams;
6use crate::store::{
7 store_reader_from_config, store_writer_from_config, TileReader, TileStoreError, TileWriter,
8};
9use async_trait::async_trait;
10use bbox_core::config::{error_exit, CoreServiceCfg};
11use bbox_core::metrics::{no_metrics, NoMetrics};
12use bbox_core::ogcapi::ApiLink;
13use bbox_core::service::OgcApiService;
14use bbox_core::{Compression, Format, TileResponse};
15use clap::{ArgMatches, Args, FromArgMatches};
16use log::debug;
17use martin_mbtiles::Metadata;
18use ogcapi_types::tiles::TileMatrixSet;
19use serde_json::json;
20use std::collections::HashMap;
21use std::num::NonZeroU16;
22use std::path::PathBuf;
23use tile_grid::{tms, BoundingBox, RegistryError, TileMatrixSetOps, Tms, Xyz};
24use tilejson::TileJSON;
25
26#[derive(Clone)]
27pub struct TileService {
28 pub(crate) tilesets: Tilesets,
29}
30
31pub type Tilesets = HashMap<String, TileSet>;
32
33#[derive(Clone)]
34pub struct TileSet {
35 pub name: String,
36 pub tms: Vec<TileSetGrid>,
38 pub source: Box<dyn TileSource>,
39 format: Format,
40 pub store_reader: Option<Box<dyn TileReader>>,
41 pub store_writer: Option<Box<dyn TileWriter>>,
42 config: TileSetCfg,
43 cache_cfg: Option<TileStoreCfg>,
44 cache_limits: Option<CacheLimitCfg>,
45 cache_control: Vec<CacheControlCfg>,
46}
47
48#[derive(Clone)]
49pub struct TileSetGrid {
50 pub tms: Tms,
51 pub minzoom: u8,
53 pub maxzoom: u8,
55}
56
57#[derive(thiserror::Error, Debug)]
58pub enum ServiceError {
59 #[error("Tileset `{0}` not found")]
60 TilesetNotFound(String),
61 #[error("Cache `{0}` not found")]
62 CacheNotFound(String),
63 #[error("Unknown format `{0}`")]
64 UnknownFormat(String),
65 #[error("Tileset grid not found")] TilesetGridNotFound,
67 #[error(transparent)]
68 TileRegistryError(#[from] RegistryError),
69 #[error(transparent)]
70 TileSourceError(#[from] TileSourceError),
71 #[error(transparent)]
72 TileStoreError(#[from] TileStoreError),
73 #[error(transparent)]
74 IoError(#[from] std::io::Error),
75}
76
77impl actix_web::error::ResponseError for ServiceError {}
78
79pub trait SourceLookup {
80 fn source(&self, tileset: &str) -> Option<&dyn TileSource>;
81}
82
83impl SourceLookup for Tilesets {
84 fn source(&self, tileset: &str) -> Option<&dyn TileSource> {
85 self.get(tileset).map(|ts| ts.source.as_ref())
86 }
87}
88
89type TileStoreConfigs = HashMap<String, TileCacheProviderCfg>;
90
91#[derive(Args, Debug)]
92pub struct ServiceArgs {
93 #[arg(short, long, value_name = "FILE")]
95 pub t_rex_config: Option<PathBuf>,
96}
97
98#[async_trait]
99impl OgcApiService for TileService {
100 type Config = TileServiceCfg;
101 type CliCommands = Commands;
102 type CliArgs = ServiceArgs;
103 type Metrics = NoMetrics;
104
105 async fn create(config: &Self::Config, _core_cfg: &CoreServiceCfg) -> Self {
106 let mut tilesets = HashMap::new();
107
108 let mut grids = tms().clone();
110 for grid in &config.grids {
111 let custom = TileMatrixSet::from_json_file(&grid.abs_path().to_string_lossy())
112 .unwrap_or_else(error_exit);
113 grids
114 .register(vec![custom], true)
115 .unwrap_or_else(error_exit);
116 }
117
118 let datasources = Datasources::create(&config.datasources).await;
119
120 let stores: TileStoreConfigs = config
121 .tilestores
122 .iter()
123 .cloned()
124 .map(|cfg| (cfg.name.clone(), cfg))
125 .collect();
126
127 for ts in &config.tilesets {
128 let ts_grids_cfg = if ts.tms.is_empty() {
129 vec![TilesetTmsCfg {
130 id: "WebMercatorQuad".to_string(),
131 minzoom: None,
132 maxzoom: None,
133 }]
134 } else {
135 ts.tms.clone()
136 };
137 let mut ts_grids = ts_grids_cfg
138 .iter()
139 .map(|cfg| {
140 let grid = grids.lookup(&cfg.id).unwrap_or_else(error_exit);
141 TileSetGrid {
142 tms: grid.clone(),
143 minzoom: cfg.minzoom.unwrap_or(grid.minzoom()),
144 maxzoom: cfg.maxzoom.unwrap_or(grid.maxzoom()),
145 }
146 })
147 .collect::<Vec<_>>();
148 ts_grids.sort_by_key(|tsg| tsg.minzoom);
149 let source = datasources
150 .setup_tile_source(&ts.source, &ts_grids, &ts_grids_cfg)
151 .await;
152 let format = ts
153 .cache_format
154 .as_ref()
155 .and_then(|suffix| Format::from_suffix(suffix))
156 .unwrap_or(*source.default_format()); let metadata = source
158 .mbtiles_metadata(ts, &format)
159 .await
160 .unwrap_or_else(error_exit);
161 let cache_cfg = stores
162 .get("<cli>")
163 .or(ts
164 .cache
165 .as_ref()
166 .map(|name| {
167 stores.get(name).cloned().unwrap_or_else(|| {
168 error_exit(ServiceError::CacheNotFound(name.to_string()))
169 })
170 })
171 .as_ref())
172 .cloned();
173 let store_writer = if let Some(config) = &cache_cfg {
174 Some(
175 store_writer_from_config(
176 &config.cache,
177 &config.compression,
178 &ts.name,
179 &format,
180 metadata,
181 )
182 .await,
183 )
184 } else {
185 None
186 };
187 let store_reader = if let Some(config) = &cache_cfg {
188 Some(
189 store_reader_from_config(&config.cache, &config.compression, &ts.name, &format)
190 .await,
191 )
192 } else {
193 None
194 };
195 let tileset = TileSet {
196 name: ts.name.clone(),
197 tms: ts_grids,
198 source,
199 format,
200 store_reader,
201 store_writer,
202 config: ts.clone(),
203 cache_cfg: cache_cfg.map(|cfg| cfg.cache),
204 cache_limits: ts.cache_limits.clone(),
205 cache_control: ts.cache_control.clone(),
206 };
207 tilesets.insert(ts.name.clone(), tileset);
208 }
209 TileService { tilesets }
210 }
211
212 async fn cli_run(&self, cli: &ArgMatches) -> bool {
213 match Commands::from_arg_matches(cli) {
214 Ok(Commands::Seed(seedargs)) => {
215 self.seed_by_grid(&seedargs)
216 .await
217 .unwrap_or_else(error_exit);
218 true
219 }
220 Ok(Commands::Upload(uploadargs)) => {
221 self.upload(&uploadargs).await.unwrap_or_else(error_exit);
222 true
223 }
224 _ => false,
225 }
226 }
227
228 fn landing_page_links(&self, _api_base: &str) -> Vec<ApiLink> {
229 vec![
230 ApiLink {
231 href: "/tiles".to_string(),
232 rel: Some("http://www.opengis.net/def/rel/ogc/1.0/tilesets-vector".to_string()),
233 type_: Some("application/json".to_string()),
234 title: Some("List of available vector features tilesets".to_string()),
235 hreflang: None,
236 length: None,
237 },
238 ApiLink {
239 href: "/tiles".to_string(),
240 rel: Some("http://www.opengis.net/def/rel/ogc/1.0/tilesets-map".to_string()),
241 type_: Some("application/json".to_string()),
242 title: Some("List of available map tilesets".to_string()),
243 hreflang: None,
244 length: None,
245 },
246 ]
247 }
248 fn conformance_classes(&self) -> Vec<String> {
249 vec![
250 "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/core".to_string(),
252 "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/oas30".to_string(),
266 "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/png".to_string(),
270 "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/jpeg".to_string(),
272 "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/mvt".to_string(),
280 ]
281 }
282 fn openapi_yaml(&self) -> Option<&str> {
283 Some(include_str!("openapi.yaml"))
284 }
285 fn metrics(&self) -> &'static Self::Metrics {
286 no_metrics()
287 }
288}
289
290pub struct QueryExtent {
291 pub extent: BoundingBox,
292 pub srid: i32,
293 pub tile_width: NonZeroU16,
294 pub tile_height: NonZeroU16,
295}
296
297impl TileService {
298 pub fn set_map_service(&mut self, service: &MapService) {
299 for (_, ts) in self.tilesets.iter_mut() {
300 ts.source.set_map_service(service);
301 }
302 }
303 pub fn tileset(&self, tileset: &str) -> Option<&TileSet> {
304 self.tilesets.get(tileset)
305 }
306 pub fn grids(&self) -> Vec<&Tms> {
307 self.tilesets
308 .values()
309 .flat_map(|ts| &ts.tms)
310 .map(|grid| (&grid.tms.tms.id, &grid.tms))
312 .collect::<HashMap<&String, &Tms>>()
313 .into_values()
314 .collect()
315 }
316 pub fn grid(&self, tms_id: &str) -> Option<&Tms> {
317 for ts in self.tilesets.values() {
318 if let Ok(grid) = ts.grid(tms_id) {
319 return Some(grid);
320 }
321 }
322 None
323 }
324}
325
326impl TileSet {
327 pub fn grid(&self, tms_id: &str) -> Result<&Tms, ServiceError> {
328 self.tms
329 .iter()
330 .map(|g| &g.tms)
331 .find(|tms| tms.id() == tms_id)
332 .ok_or(RegistryError::TmsNotFound(tms_id.to_string()).into())
333 }
334 pub fn default_grid(&self, zoom: u8) -> Result<&Tms, ServiceError> {
335 self.tms
336 .iter()
337 .find(|grid| zoom >= grid.minzoom && zoom <= grid.maxzoom)
338 .map(|grid| &grid.tms)
339 .ok_or(ServiceError::TilesetGridNotFound)
340 }
341 pub fn tile_format(&self) -> &Format {
342 &self.format
343 }
344 pub fn is_cachable_at(&self, zoom: u8) -> bool {
345 if self.store_reader.is_none() {
346 return false;
347 }
348 match self.cache_limits {
349 Some(ref cl) => cl.minzoom <= zoom && cl.maxzoom.unwrap_or(255) >= zoom,
350 None => true,
351 }
352 }
353 pub fn cache_control_max_age(&self, zoom: u8) -> Option<u64> {
354 let entry = self.cache_control.iter().rev().find(|entry| {
355 entry.minzoom.unwrap_or(0) <= zoom && entry.maxzoom.unwrap_or(255) >= zoom
356 });
357 entry.map(|e| e.max_age)
358 }
359 pub fn cache_config(&self) -> Option<&TileStoreCfg> {
360 self.cache_cfg.as_ref()
361 }
362 pub fn cache_compression(&self) -> Compression {
363 self.store_writer
364 .as_ref()
365 .map(|s| s.compression())
366 .unwrap_or(Compression::None)
367 }
368 pub async fn read_tile(
371 &self,
372 tms: &Tms,
373 xyz: &Xyz,
374 filter: &FilterParams,
375 format: &Format,
376 compression: Compression,
377 ) -> Result<Vec<u8>, ServiceError> {
378 let metrics = self.source.wms_metrics();
379 let request_params = HttpRequestParams {
380 scheme: "http",
381 host: "localhost",
382 req_path: "/",
383 metrics,
384 };
385 let tile = self
386 .source
387 .xyz_request(tms, xyz, filter, format, request_params)
388 .await?;
389 let data = tile.read_bytes(&compression)?;
390 Ok(data.body)
391 }
392 pub async fn tile_cached(
395 &self,
396 tms: &Tms,
397 xyz: &Xyz,
398 filter: &FilterParams,
399 format: &Format,
400 compression: Compression,
401 request_params: HttpRequestParams<'_>,
402 ) -> Result<Option<TileResponse>, ServiceError> {
403 let tileset = self;
404 if let Some(cache) = &tileset.store_reader {
405 if tileset.is_cachable_at(xyz.z) {
406 if let Some(tile) = cache.get_tile(xyz).await? {
408 debug!("Delivering tile from cache @ {xyz:?}");
409 let response = tile.with_compression(&compression);
410 return Ok(Some(response));
412 }
413 }
414 }
415 debug!("Request tile from source @ {xyz:?}");
417 let mut tiledata = tileset
418 .source
419 .xyz_request(tms, xyz, filter, format, request_params)
420 .await?;
421 if let Some(cache_max_age) = tileset.cache_control_max_age(xyz.z) {
423 tiledata.insert_header(("Cache-Control", format!("max-age={}", cache_max_age)));
424 }
425 if tileset.is_cachable_at(xyz.z) {
426 debug!("Writing tile into cache @ {xyz:?}");
427 let response_data = tiledata.read_bytes(&tileset.cache_compression())?;
429 if let Some(cache) = &tileset.store_writer {
430 cache.put_tile(xyz, response_data.body.clone()).await?;
431 }
432 let response = response_data.as_response(&compression);
433 Ok(Some(response))
434 } else {
435 let response = tiledata.with_compression(&compression);
436 Ok(Some(response))
437 }
438 }
439 pub async fn tilejson(&self, tms: &Tms, base_url: &str) -> Result<TileJSON, ServiceError> {
441 let mut tilejson = self.source.tilejson(tms, &self.format).await?;
442 let suffix = tilejson
443 .other
444 .get("format")
445 .map(|v| v.as_str().unwrap_or("pbf"))
446 .unwrap_or("pbf");
447 let format =
448 Format::from_suffix(suffix).ok_or(ServiceError::UnknownFormat(suffix.to_string()))?;
449 tilejson.tiles.push(format!(
450 "{base_url}/{tileset}/{{z}}/{{x}}/{{y}}.{format}",
451 tileset = &self.name,
452 format = format.file_suffix()
453 ));
454 Ok(tilejson)
455 }
456
457 pub async fn mbtiles_metadata(&self) -> Result<Metadata, ServiceError> {
459 Ok(self
460 .source
461 .mbtiles_metadata(&self.config, &self.format)
462 .await?)
463 }
464
465 pub async fn stylejson(
467 &self,
468 base_url: &str,
469 base_path: &str,
470 ) -> Result<serde_json::Value, ServiceError> {
471 let ts = self;
472 let tileset = &self.name;
473 let suffix = ts.tile_format().file_suffix();
474 let source_type = ts.source.source_type();
475 let ts_source = match source_type {
476 SourceType::Vector => json!({
477 "type": "vector",
478 "url": format!("{base_url}{base_path}/{tileset}.json")
479 }),
480 SourceType::Raster => json!({
481 "type": "raster",
482 "tiles": [format!("{base_url}{base_path}/{tileset}/{{z}}/{{x}}/{{y}}.{suffix}")],
483 }),
486 };
487
488 let layers = ts.source.layers().await?;
489 let mut layer_styles: Vec<serde_json::Value> = layers
490 .iter()
491 .map(|layer| {
492 let default_type = if let Some(ref geomtype) = layer.geometry_type {
494 match geomtype as &str {
495 "POINT" => "circle",
497 "fill" => "fill",
499 "line" => "line",
500 "symbol" => "symbol",
501 "circle" => "circle",
502 "heatmap" => "heatmap",
503 "fill-extrusion" => "fill-extrusion",
504 "raster" => "raster",
505 "hillshade" => "hillshade",
506 "background" => "background",
507 _ => "line",
509 }
510 } else {
511 match source_type {
512 SourceType::Vector => "line",
513 SourceType::Raster => "raster",
514 }
515 };
516
517 let mut layerjson =
518 json!({"id": layer.name, "source": tileset, "type": default_type});
519
520 if source_type == SourceType::Vector {
521 layerjson["source-layer"] = json!(layer.name);
522 }
524
525 if let Some(style) = &layer.style {
536 layerjson
537 .as_object_mut()
538 .expect("object")
539 .append(style.clone().as_object_mut().expect("object"));
540 }
541
542 layerjson
543 })
544 .collect();
545 if source_type == SourceType::Vector {
546 let background_layer = json!({
547 "id": "background_",
548 "type": "background",
549 "paint": {
550 "background-color": "rgba(255, 255, 255, 1)"
551 }
552 });
553 layer_styles.insert(0, background_layer);
554 }
555
556 let stylejson = json!({
557 "version": 8,
558 "name": tileset,
559 "metadata": {
560 "maputnik:renderer": "mbgljs"
561 },
562 "glyphs": "https://go-spatial.github.io/carto-assets/fonts/{fontstack}/{range}.pbf",
563 "sources": {
565 tileset: ts_source
566 },
567 "layers": layer_styles
568 });
569 Ok(stylejson)
570 }
571}
572
573pub trait TmsExtensions {
574 fn id(&self) -> &str;
575 fn srid(&self) -> i32;
576 fn xyz_extent(&self, xyz: &Xyz) -> Result<QueryExtent, TileSourceError>;
577}
578
579impl TmsExtensions for Tms {
580 fn id(&self) -> &str {
581 &self.tms.id
582 }
583 fn srid(&self) -> i32 {
584 self.crs().as_srid()
585 }
586 fn xyz_extent(&self, xyz: &Xyz) -> Result<QueryExtent, TileSourceError> {
587 if !self.is_valid(xyz) {
588 return Err(TileSourceError::TileXyzError);
589 }
590 let extent = self.xy_bounds(xyz);
591 let srid = self.srid();
592 let tile_matrix = self.matrix(xyz.z);
593 let tile_width = tile_matrix.as_ref().tile_width;
594 let tile_height = tile_matrix.as_ref().tile_height;
595 Ok(QueryExtent {
596 extent,
597 srid,
598 tile_width,
599 tile_height,
600 })
601 }
602}