bbox_tile_server/
endpoints.rs

1use crate::datasource::wms_fcgi::{HttpRequestParams, WmsMetrics};
2use crate::filter_params::FilterParams;
3use crate::service::{ServiceError, TileService, TileSet};
4use actix_web::{guard, http::header, web, Error, FromRequest, HttpRequest, HttpResponse};
5use bbox_core::endpoints::{abs_req_baseurl, req_parent_path};
6use bbox_core::service::ServiceEndpoints;
7use bbox_core::{Compression, Format};
8use log::error;
9use ogcapi_types::common::Link;
10use ogcapi_types::tiles::{
11    DataType, TileMatrixLimits, TileMatrixSetItem, TileMatrixSets, TileSetItem, TileSets,
12    TitleDescriptionKeywords,
13};
14use std::collections::HashMap;
15use tile_grid::{Tms, Xyz};
16
17/// XYZ tile endpoint
18// xyz/{tileset}/{z}/{x}/{y}.{format}
19async fn xyz(
20    service: web::Data<TileService>,
21    params: web::Path<(String, u8, u64, u64, String)>,
22    metrics: web::Data<WmsMetrics>,
23    req: HttpRequest,
24) -> Result<HttpResponse, Error> {
25    let (tileset, z, x, y, format) = params.into_inner();
26    let ts = service
27        .tileset(&tileset)
28        .ok_or(ServiceError::TilesetNotFound(tileset.clone()))?;
29    let tms = None;
30    let format = Format::from_suffix(&format).unwrap_or(*ts.tile_format());
31    tile_request(ts, tms, x, y, z, &format, metrics, req).await
32}
33
34/// XYZ tilejson endpoint
35/// TileJSON layer metadata (https://github.com/mapbox/tilejson-spec)
36// xyz/{tileset}.json
37async fn tilejson(
38    service: web::Data<TileService>,
39    tileset: web::Path<String>,
40    req: HttpRequest,
41) -> Result<HttpResponse, Error> {
42    let absurl = format!("{}{}", abs_req_baseurl(&req), req_parent_path(&req));
43    let ts = service
44        .tileset(&tileset)
45        .ok_or(ServiceError::TilesetNotFound(tileset.clone()))?;
46    let tms = ts.default_grid(0)?;
47    Ok(ts
48        .tilejson(tms, &absurl)
49        .await
50        .map(|tilejson| HttpResponse::Ok().json(tilejson))?)
51}
52
53/// XYZ style json endpoint
54// xyz/{tileset}.style.json
55async fn stylejson(
56    service: web::Data<TileService>,
57    tileset: web::Path<String>,
58    req: HttpRequest,
59) -> Result<HttpResponse, Error> {
60    let base_url = abs_req_baseurl(&req);
61    let base_path = req_parent_path(&req);
62    let ts = service
63        .tileset(&tileset)
64        .ok_or(ServiceError::TilesetNotFound(tileset.clone()))?;
65    Ok(ts
66        .stylejson(&base_url, &base_path)
67        .await
68        .map(|stylejson| HttpResponse::Ok().json(stylejson))?)
69}
70
71/// XYZ MBTiles metadata.json (https://github.com/mapbox/mbtiles-spec/blob/master/1.3/spec.md)
72// xyz/{tileset}/metadata.json
73async fn metadatajson(
74    service: web::Data<TileService>,
75    tileset: web::Path<String>,
76) -> Result<HttpResponse, Error> {
77    let ts = service
78        .tileset(&tileset)
79        .ok_or(ServiceError::TilesetNotFound(tileset.clone()))?;
80    Ok(ts
81        .mbtiles_metadata()
82        .await
83        .map(|metadata| HttpResponse::Ok().json(metadata))?)
84}
85
86/// Map tile endpoint
87// map/tiles/{tileMatrixSetId}/{tileMatrix}/{tileRow}/{tileCol}
88async fn map_tile(
89    service: web::Data<TileService>,
90    params: web::Path<(String, u8, u64, u64)>,
91    metrics: web::Data<WmsMetrics>,
92    req: HttpRequest,
93) -> Result<HttpResponse, Error> {
94    let (tms_id, z, x, y) = params.into_inner();
95    // This endpoint doesn't specify the tileset. Let's take the first dataset of the service.
96    let ts = service
97        .tilesets
98        .values()
99        .collect::<Vec<_>>()
100        .first()
101        .cloned()
102        .ok_or(ServiceError::TilesetNotFound("No tileset found".into()))?;
103    let tms = ts.grid(&tms_id)?;
104    let format = format_accept_header(&req, ts.source.default_format()).await;
105    tile_request(ts, Some(tms), x, y, z, &format, metrics, req).await
106}
107
108async fn format_accept_header(req: &HttpRequest, default: &Format) -> Format {
109    let mut format_mime = web::Header::<header::Accept>::extract(req)
110        .await
111        .map(|accept| accept.preference().to_string())
112        .ok();
113    // override invalid request formats (TODO: check against available formats)
114    if let Some("image/avif") = format_mime.as_deref() {
115        format_mime = None;
116    }
117    let format = format_mime
118        .as_deref()
119        .and_then(Format::from_content_type)
120        .unwrap_or(*default);
121    format
122}
123
124#[allow(clippy::too_many_arguments)]
125async fn tile_request(
126    ts: &TileSet,
127    tms: Option<&Tms>,
128    x: u64,
129    y: u64,
130    z: u8,
131    format: &Format,
132    metrics: web::Data<WmsMetrics>,
133    req: HttpRequest,
134) -> Result<HttpResponse, Error> {
135    let tile = Xyz::new(x, y, z);
136    let mut filters: HashMap<String, String> =
137        match serde_urlencoded::from_str::<Vec<(String, String)>>(req.query_string()) {
138            Ok(f) => f
139                .iter()
140                .map(|k| (k.0.to_lowercase(), k.1.to_owned()))
141                .collect(),
142            Err(_e) => return Ok(HttpResponse::BadRequest().finish()),
143        };
144
145    let datetime = filters.remove("datetime");
146    let fp = FilterParams { datetime, filters };
147    let compression = req
148        .headers()
149        .get(header::ACCEPT_ENCODING)
150        .and_then(|headerval| {
151            headerval
152                .to_str()
153                .ok()
154                .filter(|headerstr| headerstr.contains("gzip"))
155                .map(|_| Compression::Gzip)
156        })
157        .unwrap_or(Compression::None);
158    let conn_info = req.connection_info().clone();
159    let request_params = HttpRequestParams {
160        scheme: conn_info.scheme(),
161        host: conn_info.host(),
162        req_path: req.path(),
163        metrics: &metrics,
164    };
165    let tms = tms.unwrap_or(ts.default_grid(z)?);
166    match ts
167        .tile_cached(tms, &tile, &fp, format, compression, request_params)
168        .await
169    {
170        Ok(Some(tile_resp)) => {
171            let mut r = HttpResponse::Ok();
172            if let Some(content_type) = tile_resp.content_type() {
173                r.content_type(content_type);
174            }
175            for (key, value) in tile_resp.headers() {
176                r.insert_header((key, value));
177                // TODO: use append_header for "Server-Timing" and others?
178            }
179            Ok(r.streaming(tile_resp.into_stream()))
180        }
181        Ok(None) => Ok(HttpResponse::NoContent().finish()),
182        Err(e) => {
183            error!("Tile creation error: {e}");
184            Ok(HttpResponse::InternalServerError().finish())
185        }
186    }
187}
188
189/// list of available tilesets
190// tiles
191async fn get_tile_sets_list(service: web::Data<TileService>) -> HttpResponse {
192    let tile_set_items: Vec<TileSetItem> = service
193        .tilesets
194        .iter()
195        .map(|(ts_name, tileset)| {
196            let tms = tileset.default_grid(0).expect("default grid missing");
197            let tiling_scheme_links = tileset.tms.iter().map(|grid| {
198                let grid_tms = &grid.tms.tms;
199                Link {
200                    rel: "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme".to_string(),
201                    r#type: Some("application/json".to_string()),
202                    title: Some("Tile Matrix Set definition (as JSON)".to_string()),
203                    href: format!("/tileMatrixSets/{}", &grid_tms.id),
204                    hreflang: None,
205                    length: None,
206                }
207            });
208            TileSetItem {
209                title: Some(ts_name.to_string()),
210                data_type: DataType::Vector,
211                crs: tms.crs().clone(),
212                tile_matrix_set_uri: tms.tms.uri.clone(),
213                links: [
214                    Link {
215                        rel: "self".to_string(),
216                        r#type: Some("application/json".to_string()),
217                        title: Some(format!("Tileset metadata for {ts_name} (as JSON)")),
218                        href: format!("/tiles/{ts_name}"),
219                        hreflang: None,
220                        length: None,
221                    },
222                    Link {
223                        rel: "self".to_string(),
224                        r#type: Some("application/json+tilejson".to_string()),
225                        title: Some(format!(
226                            "Tileset metadata for {ts_name} (in TileJSON format)"
227                        )),
228                        href: format!("/xyz/{ts_name}.json"),
229                        hreflang: None,
230                        length: None,
231                    },
232                    Link {
233                        rel: "item".to_string(),
234                        r#type: Some("application/vnd.mapbox-vector-tile".to_string()),
235                        title: Some(format!("Tiles for {ts_name} (as MVT)")),
236                        href: format!(
237                            "/map/tiles/{}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}",
238                            &tms.tms.id
239                        ),
240                        hreflang: None,
241                        length: None,
242                    },
243                ]
244                .into_iter()
245                .chain(tiling_scheme_links)
246                .collect(),
247            }
248        })
249        .collect();
250    let tilesets = TileSets {
251        tilesets: tile_set_items,
252        links: None,
253    };
254    HttpResponse::Ok().json(tilesets)
255}
256
257/// tileset metadata
258// tiles/{tileMatrixSetId}
259async fn get_tile_set(
260    service: web::Data<TileService>,
261    tileset: web::Path<String>,
262) -> Result<HttpResponse, Error> {
263    let ts = service
264        .tileset(&tileset)
265        .ok_or(ServiceError::TilesetNotFound(tileset.clone()))?;
266    let tms = ts.default_grid(0)?;
267    let tile_matrix_set_limits = tms
268        .tms
269        .tile_matrices
270        .iter()
271        .map(|tm| TileMatrixLimits {
272            tile_matrix: tm.id.clone(),
273            min_tile_row: 0,
274            max_tile_row: tm.matrix_width.into(),
275            min_tile_col: 0,
276            max_tile_col: tm.matrix_height.into(),
277        })
278        .collect();
279
280    let tiling_scheme_links = ts.tms.iter().map(|grid| {
281        let grid_tms = &grid.tms.tms;
282        Link {
283            rel: "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme".to_string(),
284            r#type: Some("application/json".to_string()),
285            title: Some("Tile Matrix Set definition (as JSON)".to_string()),
286            href: format!("/tileMatrixSets/{}", &grid_tms.id),
287            hreflang: None,
288            length: None,
289        }
290    });
291
292    let tileset = ogcapi_types::tiles::TileSet {
293        title_description_keywords: TitleDescriptionKeywords {
294            title: Some(tileset.to_string()),
295            description: None,
296            keywords: None,
297        },
298        data_type: DataType::Vector,
299        tile_matrix_set_uri: tms.tms.uri.clone(),
300        tile_matrix_set_limits: Some(tile_matrix_set_limits),
301        crs: tms.crs().clone(),
302        epoch: None,
303        layers: None,
304        bounding_box: None,
305        style: None,
306        center_point: None,
307        license: None,
308        access_constraints: None,
309        version: None,
310        created: None,
311        updated: None,
312        point_of_contact: None,
313        media_types: None,
314        links: [
315            Link {
316                rel: "self".to_string(),
317                r#type: Some("application/json".to_string()),
318                title: Some(format!("Tileset metadata for {tileset} (as JSON)")),
319                href: format!("/tiles/{tileset}"),
320                hreflang: None,
321                length: None,
322            },
323            Link {
324                rel: "item".to_string(),
325                r#type: Some("application/vnd.mapbox-vector-tile".to_string()),
326                title: Some(format!("Tiles for {tileset} (as MVT)")),
327                href: format!("/xyz/{tileset}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}.mvt"),
328                hreflang: None,
329                length: None,
330                // TODO: "templated": true
331            },
332        ]
333        .into_iter()
334        .chain(tiling_scheme_links)
335        .collect(),
336    };
337    Ok(HttpResponse::Ok().json(tileset))
338}
339
340/// list of available tiling schemes
341// tileMatrixSets
342async fn get_tile_matrix_sets_list(service: web::Data<TileService>) -> HttpResponse {
343    let grids = service.grids();
344    let sets = TileMatrixSets {
345        tile_matrix_sets: grids
346            .iter()
347            .map(|grid| TileMatrixSetItem {
348                id: Some(grid.tms.id.clone()),
349                title: None,
350                uri: grid.tms.uri.clone(),
351                crs: Some(grid.tms.crs.clone()),
352                links: vec![Link {
353                    rel: "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme".to_string(),
354                    r#type: Some("application/json".to_string()),
355                    title: Some("Tile Matrix Set definition (as JSON)".to_string()),
356                    href: format!("/tileMatrixSets/{}", &grid.tms.id),
357                    hreflang: None,
358                    length: None,
359                }],
360            })
361            .collect(),
362    };
363    HttpResponse::Ok().json(sets)
364}
365
366/// definition of tiling scheme
367// tileMatrixSets/{tileMatrixSetId}
368async fn get_tile_matrix_set(
369    service: web::Data<TileService>,
370    tile_matrix_set_id: web::Path<String>,
371) -> Result<HttpResponse, Error> {
372    if let Some(grid) = service.grid(&tile_matrix_set_id) {
373        Ok(HttpResponse::Ok().json(grid.tms.clone()))
374    } else {
375        Err(ServiceError::TilesetGridNotFound.into())
376    }
377}
378
379impl ServiceEndpoints for TileService {
380    fn register_endpoints(&self, cfg: &mut web::ServiceConfig) {
381        cfg.app_data(web::Data::new(self.clone()))
382            .service(
383                web::resource("/xyz/{tileset}/{z}/{x}/{y}.{format}").route(
384                    web::route()
385                        .guard(guard::Any(guard::Get()).or(guard::Head()))
386                        .to(xyz),
387                ),
388            )
389            .service(web::resource("/xyz/{tileset}.style.json").route(web::get().to(stylejson)))
390            .service(web::resource("/xyz/{tileset}.json").route(web::get().to(tilejson)))
391            .service(
392                web::resource("/xyz/{tileset}/metadata.json").route(web::get().to(metadatajson)),
393            )
394            .service(
395                web::resource("/map/tiles/{tileMatrixSetId}/{tileMatrix}/{tileRow}/{tileCol}")
396                    .route(web::get().to(map_tile)),
397            )
398            .service(web::resource("/tiles/{tileMatrixSetId}").route(web::get().to(get_tile_set)))
399            .service(web::resource("/tiles").route(web::get().to(get_tile_sets_list)))
400            .service(
401                web::resource("/tileMatrixSets").route(web::get().to(get_tile_matrix_sets_list)),
402            )
403            .service(
404                web::resource("/tileMatrixSets/{tileMatrixSetId}")
405                    .route(web::get().to(get_tile_matrix_set)),
406            );
407        if cfg!(not(feature = "map-server")) {
408            cfg.app_data(web::Data::new(WmsMetrics::default()));
409        }
410    }
411}