bbox_tile_server/
service.rs

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    /// Tile matrix sets
37    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    /// Minimum zoom level.
52    pub minzoom: u8,
53    /// Maximum zoom level.
54    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")] // default grid missing or z out of range
66    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    /// T-Rex config file
94    #[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        // Register custom grids
109        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()); // TODO: emit warning or error
157            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            // Core
251            "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/core".to_string(),
252            // TileSet
253            // "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tileset".to_string(),
254            // Tilesets list
255            // "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tilesets-list".to_string(),
256            // Dataset tilesets
257            // "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/dataset-tilesets".to_string(),
258            // Geodata tilesets
259            // "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/geodata-tilesets".to_string(),
260            // Collections selection
261            // "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/collections-selection".to_string(),
262            // DateTime
263            // "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/datetime".to_string(),
264            // OpenAPI Specification 3.0
265            "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/oas30".to_string(),
266            // XML
267            // "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/xml".to_string(),
268            // PNG
269            "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/png".to_string(),
270            // JPEG
271            "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/jpeg".to_string(),
272            // TIFF
273            // "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tiff".to_string(),
274            // NetCDF
275            // "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/netcdf".to_string(),
276            // GeoJSON
277            // "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/geojson".to_string(),
278            // Mapbox Vector Tiles
279            "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            // remove duplicates
311            .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    /// Tile request
369    // Used for seeding, compresses tiles according to target store
370    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    /// Get tile with cache lookup
393    // Used for serving
394    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                // TODO: support separate caches for different grids
407                if let Some(tile) = cache.get_tile(xyz).await? {
408                    debug!("Delivering tile from cache @ {xyz:?}");
409                    let response = tile.with_compression(&compression);
410                    //TODO: check returned format
411                    return Ok(Some(response));
412                }
413            }
414        }
415        // Request tile and write into cache
416        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        // TODO: if tiledata.empty() { return Ok(None) }
422        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            // Read tile into memory
428            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    /// TileJSON layer metadata (<https://github.com/mapbox/tilejson-spec>)
440    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    /// MBTiles metadata.json
458    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    /// Autogenerated Style JSON (<https://www.mapbox.com/mapbox-gl-style-spec/>)
466    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                 // "minzoom": 0,
484                 // "maxzoom": 24
485            }),
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                // Default paint type
493                let default_type = if let Some(ref geomtype) = layer.geometry_type {
494                    match geomtype as &str {
495                        // Geometry types
496                        "POINT" => "circle",
497                        // MVT layer rendering types
498                        "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                        // Other geometry types
508                        _ => "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                    // Note: source-layer referencing other layers not supported
523                }
524
525                // minzoom:
526                // The minimum zoom level for the layer. At zoom levels less than the minzoom, the layer will be hidden.
527                // Optional number between 0 and 24 inclusive.
528                // maxzoom:
529                // The maximum zoom level for the layer. At zoom levels equal to or greater than the maxzoom, the layer will be hidden.
530                // Optional number between 0 and 24 inclusive.
531                // Note: We could use source data min-/maxzoom as default to prevent overzooming
532                // or we could add style.minzoom, style.maxzoom elements
533
534                // Insert optional style
535                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            // "glyphs": format!("{base_url}/fonts/{{fontstack}}/{{range}}.pbf"),
564            "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}