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
17async 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
34async 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
53async 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
71async 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
86async 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 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 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 }
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
189async 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
257async 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 },
332 ]
333 .into_iter()
334 .chain(tiling_scheme_links)
335 .collect(),
336 };
337 Ok(HttpResponse::Ok().json(tileset))
338}
339
340async 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
366async 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}