bbox_tile_server/datasource/
mod.rs

1//! Tile source implementations.
2
3pub mod mbtiles;
4mod mvt;
5pub mod pmtiles;
6pub mod postgis;
7mod postgis_queries;
8#[cfg(feature = "map-server")]
9pub mod wms_fcgi;
10pub mod wms_http;
11
12use crate::config::{SourceParamCfg, TileSetCfg, TilesetTmsCfg};
13use crate::filter_params::FilterParams;
14use crate::service::{TileSetGrid, TmsExtensions};
15use crate::store::mbtiles::MbtilesStore;
16use crate::store::pmtiles::PmtilesStoreReader;
17use async_trait::async_trait;
18use bbox_core::config::{error_exit, DatasourceCfg, NamedDatasourceCfg};
19use bbox_core::{Format, NamedObjectStore, TileResponse};
20use dyn_clone::{clone_trait_object, DynClone};
21use geozero::error::GeozeroError;
22use martin_mbtiles::Metadata;
23use once_cell::sync::OnceCell;
24use std::env;
25use tile_grid::{tms, RegistryError, Tms, Xyz};
26use tilejson::TileJSON;
27
28#[derive(thiserror::Error, Debug)]
29pub enum TileSourceError {
30    #[error("tileserver.source `{0}` not found")]
31    TileSourceNotFound(String),
32    #[error("tileserver.source of type {0} expected")]
33    TileSourceTypeError(String),
34    #[error("missing filter parameter")]
35    FilterParamError,
36    #[error("tile not found / out of bounds")]
37    TileXyzError,
38    #[error(transparent)]
39    RegistryError(#[from] RegistryError),
40    #[error(transparent)]
41    FcgiError(#[from] wms_fcgi::FcgiError),
42    #[error("FCGI for suffix `{0}` not found")]
43    SuffixNotFound(String),
44    #[error(transparent)]
45    DbError(#[from] sqlx::Error),
46    #[error("Source field type detection failed")]
47    TypeDetectionError,
48    #[error("Integer out of range")]
49    IntRangeError(#[from] std::num::TryFromIntError),
50    #[error(transparent)]
51    GeozeroError(#[from] GeozeroError),
52    #[error("MVT encoding error")]
53    MvtEncodeError, // prost::error::EncodeError
54    #[error(transparent)]
55    WmsHttpError(#[from] reqwest::Error),
56    #[error(transparent)]
57    MbtilesError(#[from] martin_mbtiles::MbtError),
58    #[error(transparent)]
59    PmtilesError(#[from] ::pmtiles::error::Error),
60}
61
62#[derive(PartialEq, Clone, Debug)]
63pub enum SourceType {
64    Vector,
65    Raster,
66}
67
68pub struct LayerInfo {
69    pub name: String,
70    pub geometry_type: Option<String>,
71    // MB JSON style
72    pub style: Option<serde_json::Value>,
73}
74
75#[async_trait]
76pub trait TileSource: DynClone + Send + Sync {
77    /// Request tile from source
78    async fn xyz_request(
79        &self,
80        tms: &Tms,
81        tile: &Xyz,
82        filter: &FilterParams,
83        format: &Format,
84        request_params: wms_fcgi::HttpRequestParams<'_>,
85    ) -> Result<TileResponse, TileSourceError>;
86    /// Type information
87    fn source_type(&self) -> SourceType;
88    /// Default tile format
89    fn default_format(&self) -> &Format {
90        match self.source_type() {
91            SourceType::Vector => &Format::Mvt,
92            SourceType::Raster => &Format::Png, // TODO: support for "image/png; mode=8bit"
93        }
94    }
95    /// Set MapService for WmsFcgiSource
96    fn set_map_service(&mut self, _service: &wms_fcgi::MapService) {}
97    /// MapService metrics
98    fn wms_metrics(&self) -> &'static wms_fcgi::WmsMetrics {
99        static DUMMY_METRICS: OnceCell<wms_fcgi::WmsMetrics> = OnceCell::new();
100        DUMMY_METRICS.get_or_init(wms_fcgi::WmsMetrics::default)
101    }
102    /// TileJSON layer metadata (<https://github.com/mapbox/tilejson-spec>)
103    async fn tilejson(&self, tms: &Tms, format: &Format) -> Result<TileJSON, TileSourceError>;
104    /// Layer metadata
105    async fn layers(&self) -> Result<Vec<LayerInfo>, TileSourceError>;
106    /// MBTiles metadata.json (<https://github.com/mapbox/mbtiles-spec/blob/master/1.3/spec.md>)
107    async fn mbtiles_metadata(
108        &self,
109        tileset: &TileSetCfg,
110        format: &Format,
111    ) -> Result<Metadata, TileSourceError> {
112        let tms = tms().lookup("WebMercatorQuad").unwrap(); // FIXME: Use TMS from tilegrid
113        Ok(Metadata {
114            id: tileset.name.clone(),
115            tile_info: martin_tile_utils::TileInfo {
116                format: martin_tile_utils::Format::parse(format.file_suffix())
117                    .unwrap_or(martin_tile_utils::Format::Mvt),
118                encoding: martin_tile_utils::Encoding::Uncompressed,
119            },
120            tilejson: self.tilejson(&tms, format).await?,
121            layer_type: None,
122            json: None,
123            agg_tiles_hash: None,
124        })
125    }
126}
127
128clone_trait_object!(TileSource);
129
130/// Datasource connection pools
131#[derive(Default)]
132pub struct Datasources {
133    pg_datasources: NamedObjectStore<postgis::Datasource>,
134    // Store config for non-pooled sources
135    config_sources: NamedObjectStore<DatasourceCfg>,
136}
137
138impl Datasources {
139    /// Setup datasource connection pools
140    pub async fn create(datasources: &Vec<NamedDatasourceCfg>) -> Self {
141        // TODO: setup referenced datasources only (?)
142        let mut ds_handler = Datasources::default();
143        for named_ds in datasources {
144            // TODO: check duplicate names
145            // TODO: move into core, combined with feature-server Datasource
146            let envar = env::var(format!("BBOX_DATASOURCE_{}", &named_ds.name.to_uppercase())).ok();
147            let ds = &named_ds.datasource;
148            match ds {
149                DatasourceCfg::Postgis(cfg) => ds_handler.pg_datasources.add(
150                    &named_ds.name,
151                    postgis::Datasource::from_config(cfg, envar)
152                        .await
153                        .unwrap_or_else(error_exit),
154                ),
155                _ => ds_handler.config_sources.add(&named_ds.name, ds.clone()),
156            }
157        }
158        ds_handler
159    }
160    /// Setup tile source instance
161    pub async fn setup_tile_source(
162        &self,
163        cfg: &SourceParamCfg,
164        ts_grids: &[TileSetGrid],
165        tms_cfg: &[TilesetTmsCfg],
166    ) -> Box<dyn TileSource> {
167        // -- raster sources --
168        // wms_fcgi::WmsFcgiSource,
169        // wms_http::WmsHttpSource,
170        // // GdalData(GdalSource),
171        // // RasterData(GeorasterSource),
172        // -- vector sources --
173        // postgis::PgSource,
174        // // OgrData(OgrQueries),
175        // // VectorData(GeozeroSource),
176        // // OsmData(OsmSource),
177        // -- direct tile sources --
178        // mbtiles::MbtilesSource,
179        // // Pmtiles(PmtilesSource),
180        // // PgTile(PgTileQueries),
181        // /// dummy source for disabled features
182        // Empty,
183        match cfg {
184            SourceParamCfg::WmsHttp(cfg) => {
185                let DatasourceCfg::WmsHttp(provider) =
186                    self.config_sources.get(&cfg.source).unwrap_or_else(|| {
187                        error_exit(TileSourceError::TileSourceNotFound(cfg.source.clone()))
188                    })
189                else {
190                    error_exit(TileSourceError::TileSourceTypeError(
191                        "wms_proxy".to_string(),
192                    ))
193                };
194                let first_srid = ts_grids.first().expect("default grid missing").tms.srid(); // TODO: Support multiple grids
195                Box::new(wms_http::WmsHttpSource::from_config(
196                    provider, cfg, first_srid,
197                ))
198            }
199            #[cfg(feature = "map-server")]
200            SourceParamCfg::WmsFcgi(cfg) => Box::new(wms_fcgi::WmsFcgiSource::from_config(cfg)),
201            #[cfg(not(feature = "map-server"))]
202            SourceParamCfg::WmsFcgi(cfg) => {
203                bbox_core::config::config_error_exit(
204                    &format!("Cannot add map service tile source with project `{}` - Map service feature is not active.", cfg.project));
205                unreachable!()
206            }
207            SourceParamCfg::Postgis(pg_cfg) => {
208                let ds = self
209                    .pg_datasources
210                    .get_or_default(pg_cfg.datasource.as_deref())
211                    .unwrap_or_else(|| {
212                        error_exit(TileSourceError::TileSourceNotFound(
213                            pg_cfg
214                                .datasource
215                                .as_ref()
216                                .unwrap_or(&"(default)".to_string())
217                                .clone(),
218                        ))
219                    });
220                Box::new(postgis::PgSource::create(ds, pg_cfg, ts_grids, tms_cfg).await)
221            }
222            SourceParamCfg::Mbtiles(cfg) => Box::new(
223                MbtilesStore::from_config(cfg)
224                    .await
225                    .unwrap_or_else(error_exit),
226            ),
227            SourceParamCfg::Pmtiles(cfg) => Box::new(
228                PmtilesStoreReader::from_config(cfg)
229                    .await
230                    .unwrap_or_else(error_exit),
231            ),
232        }
233    }
234}
235
236pub fn source_config_from_cli_arg(file_or_url: &str) -> Option<SourceParamCfg> {
237    MbtilesStore::config_from_cli_arg(file_or_url)
238        .map(SourceParamCfg::Mbtiles)
239        .or(PmtilesStoreReader::config_from_cli_arg(file_or_url).map(SourceParamCfg::Pmtiles))
240}
241
242#[cfg(not(feature = "map-server"))]
243pub mod wms_fcgi {
244    // Replacements for bbox_map_server types
245    #[derive(Default)]
246    pub struct WmsMetrics;
247    #[derive(Clone)]
248    pub struct MapService;
249    impl MapService {
250        pub fn metrics(&self) -> &'static WmsMetrics {
251            unimplemented!()
252        }
253    }
254    pub type FcgiError = std::io::Error;
255
256    pub struct HttpRequestParams<'a> {
257        pub scheme: &'a str,
258        pub host: &'a str,
259        pub req_path: &'a str,
260        pub metrics: &'a WmsMetrics,
261    }
262}