Coverage for pygeoapi/api/tiles.py: 15%
164 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 08:15 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 08:15 +0000
1# =================================================================
3# Authors: Tom Kralidis <tomkralidis@gmail.com>
4# Francesco Bartoli <xbartolone@gmail.com>
5# Sander Schaminee <sander.schaminee@geocat.net>
6# John A Stevenson <jostev@bgs.ac.uk>
7# Colin Blackburn <colb@bgs.ac.uk>
8# Ricardo Garcia Silva <ricardo.garcia.silva@geobeyond.it>
9# Bernhard Mallinger <bernhard.mallinger@eox.at>
10#
11# Copyright (c) 2026 Tom Kralidis
12# Copyright (c) 2025 Francesco Bartoli
13# Copyright (c) 2022 John A Stevenson and Colin Blackburn
14# Copyright (c) 2023 Ricardo Garcia Silva
15# Copyright (c) 2024 Bernhard Mallinger
16#
17# Permission is hereby granted, free of charge, to any person
18# obtaining a copy of this software and associated documentation
19# files (the "Software"), to deal in the Software without
20# restriction, including without limitation the rights to use,
21# copy, modify, merge, publish, distribute, sublicense, and/or sell
22# copies of the Software, and to permit persons to whom the
23# Software is furnished to do so, subject to the following
24# conditions:
25#
26# The above copyright notice and this permission notice shall be
27# included in all copies or substantial portions of the Software.
28#
29# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
30# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
31# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
32# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
33# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
34# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
35# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
36# OTHER DEALINGS IN THE SOFTWARE.
37#
38# =================================================================
41import logging
42from http import HTTPStatus
43from typing import Tuple
45from pygeoapi import l10n
46from pygeoapi.formats import FORMAT_TYPES, F_JSON, F_HTML, F_JSONLD
47from pygeoapi.plugin import load_plugin
48from pygeoapi.models.provider.base import (TilesMetadataFormat,
49 TileMatrixSetEnum)
50from pygeoapi.provider import get_provider_by_type, filter_providers_by_type
51from pygeoapi.provider.base import (
52 ProviderGenericError, ProviderTypeError
53)
54from pygeoapi.provider.tile import ProviderTileNotFoundError
56from pygeoapi.util import to_json, filter_dict_by_key_value, render_j2_template
58from . import APIRequest, API, SYSTEM_LOCALE
60LOGGER = logging.getLogger(__name__)
62CONFORMANCE_CLASSES = [
63 'http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/core',
64 'http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/mvt',
65 'http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tileset',
66 'http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tilesets-list',
67 'http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/oas30',
68 'http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/geodata-tilesets'
69]
72def get_collection_tiles(api: API, request: APIRequest,
73 dataset: str | None = None) -> Tuple[dict, int, str]:
74 """
75 Provide collection tiles
77 :param request: A request object
78 :param dataset: name of collection
80 :returns: tuple of headers, status code, content
81 """
83 headers = request.get_response_headers(SYSTEM_LOCALE,
84 **api.api_headers)
85 if any([dataset is None,
86 dataset not in api.config['resources'].keys()]):
88 msg = 'Collection not found'
89 return api.get_exception(
90 HTTPStatus.NOT_FOUND, headers, request.format, 'NotFound', msg)
92 LOGGER.debug('Creating collection tiles')
93 LOGGER.debug('Loading provider')
94 try:
95 t = get_provider_by_type(
96 api.config['resources'][dataset]['providers'], 'tile')
97 p = load_plugin('provider', t)
98 except (KeyError, ProviderTypeError):
99 msg = 'Invalid collection tiles'
100 return api.get_exception(
101 HTTPStatus.BAD_REQUEST, headers, request.format,
102 'InvalidParameterValue', msg)
103 except ProviderGenericError as err:
104 return api.get_exception(
105 err.http_status_code, headers, request.format,
106 err.ogc_exception_code, err.message)
108 tiles = {
109 'links': [],
110 'tilesets': []
111 }
113 tiles['links'].append({
114 'type': FORMAT_TYPES[F_JSON],
115 'rel': request.get_linkrel(F_JSON),
116 'title': l10n.translate('This document as JSON', request.locale),
117 'href': f'{api.get_collections_url()}/{dataset}/tiles?f={F_JSON}'
118 })
119 tiles['links'].append({
120 'type': FORMAT_TYPES[F_JSONLD],
121 'rel': request.get_linkrel(F_JSONLD),
122 'title': l10n.translate('This document as RDF (JSON-LD)', request.locale), # noqa
123 'href': f'{api.get_collections_url()}/{dataset}/tiles?f={F_JSONLD}'
124 })
125 tiles['links'].append({
126 'type': FORMAT_TYPES[F_HTML],
127 'rel': request.get_linkrel(F_HTML),
128 'title': l10n.translate('This document as HTML', request.locale),
129 'href': f'{api.get_collections_url()}/{dataset}/tiles?f={F_HTML}'
130 })
132 tile_services = p.get_tiles_service(
133 baseurl=api.base_url,
134 servicepath=f'{api.get_collections_url()}/{dataset}/tiles/{{tileMatrixSetId}}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}?f={p.format_type}' # noqa
135 )
137 for service in tile_services['links']:
138 tiles['links'].append(service)
140 tiling_schemes = p.get_tiling_schemes()
142 datatype = 'vector'
144 if t['format']['mimetype'].startswith('image'):
145 datatype = 'map'
147 for matrix in tiling_schemes:
148 tile_matrix = {
149 'title': dataset,
150 'tileMatrixSetURI': matrix.tileMatrixSetURI,
151 'crs': matrix.crs,
152 'dataType': datatype,
153 'links': []
154 }
155 tile_matrix['links'].append({
156 'type': FORMAT_TYPES[F_JSON],
157 'rel': 'http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme',
158 'title': l10n.translate('TileMatrixSet definition in JSON', request.locale), # noqa
159 'href': f'{api.base_url}/TileMatrixSets/{matrix.tileMatrixSet}?f={F_JSON}' # noqa
160 })
161 tile_matrix['links'].append({
162 'type': FORMAT_TYPES[F_JSON],
163 'rel': request.get_linkrel(F_JSON),
164 'title': f'{dataset} - {matrix.tileMatrixSet} - {F_JSON}',
165 'href': f'{api.get_collections_url()}/{dataset}/tiles/{matrix.tileMatrixSet}?f={F_JSON}' # noqa
166 })
167 tile_matrix['links'].append({
168 'type': FORMAT_TYPES[F_HTML],
169 'rel': request.get_linkrel(F_HTML),
170 'title': f'{dataset} - {matrix.tileMatrixSet} - {F_HTML}',
171 'href': f'{api.get_collections_url()}/{dataset}/tiles/{matrix.tileMatrixSet}?f={F_HTML}' # noqa
172 })
174 tiles['tilesets'].append(tile_matrix)
176 if request.format == F_HTML: # render
177 tpl_config = api.get_dataset_templates(dataset)
178 tiles['id'] = dataset
179 tiles['title'] = l10n.translate(
180 api.config['resources'][dataset]['title'], SYSTEM_LOCALE)
181 tiles['tilesets'] = [
182 scheme.tileMatrixSet for scheme in p.get_tiling_schemes()]
183 tiles['bounds'] = \
184 api.config['resources'][dataset]['extents']['spatial']['bbox']
185 tiles['minzoom'] = p.options['zoom']['min']
186 tiles['maxzoom'] = p.options['zoom']['max']
187 tiles['collections_path'] = api.get_collections_url()
188 tiles['tile_type'] = p.tile_type
190 content = render_j2_template(api.tpl_config, tpl_config,
191 'collections/tiles/index.html', tiles,
192 request.locale)
194 return headers, HTTPStatus.OK, content
196 return headers, HTTPStatus.OK, to_json(tiles, api.pretty_print)
199def get_collection_tiles_data(
200 api: API, request: APIRequest,
201 dataset: str | None = None,
202 matrix_id: str | None = None,
203 z_idx: int | None = None,
204 y_idx: int | None = None,
205 x_idx: int | None = None) -> Tuple[dict, int, str]:
206 """
207 Get collection items tiles
209 :param request: A request object
210 :param dataset: dataset name
211 :param matrix_id: matrix identifier
212 :param z_idx: z index
213 :param y_idx: y index
214 :param x_idx: x index
216 :returns: tuple of headers, status code, content
217 """
219 format_ = request.format
220 if not format_:
221 return api.get_format_exception(request)
222 headers = request.get_response_headers(SYSTEM_LOCALE,
223 **api.api_headers)
224 LOGGER.debug('Processing tiles')
226 collections = filter_dict_by_key_value(api.config['resources'],
227 'type', 'collection')
229 if dataset not in collections.keys():
230 msg = 'Collection not found'
231 return api.get_exception(
232 HTTPStatus.NOT_FOUND, headers, request.format, 'NotFound', msg)
234 LOGGER.debug('Loading tile provider')
235 try:
236 t = get_provider_by_type(
237 api.config['resources'][dataset]['providers'], 'tile')
238 p = load_plugin('provider', t)
240 format_ = p.format_type
241 headers['Content-Type'] = t['format']['mimetype']
243 LOGGER.debug(f'Fetching tileset id {matrix_id} and tile {z_idx}/{y_idx}/{x_idx}') # noqa
244 content = p.get_tiles(layer=p.get_layer(), tileset=matrix_id,
245 z=z_idx, y=y_idx, x=x_idx, format_=format_)
246 if content is None:
247 msg = 'identifier not found'
248 return api.get_exception(
249 HTTPStatus.NO_CONTENT, headers, format_, 'NoContent', msg)
250 else:
251 return headers, HTTPStatus.OK, content
253 # @TODO: figure out if the spec requires to return json errors
254 except KeyError:
255 msg = 'Invalid collection tiles'
256 return api.get_exception(
257 HTTPStatus.BAD_REQUEST, headers, format_,
258 'InvalidParameterValue', msg)
259 except ProviderTileNotFoundError:
260 msg = 'Tile not found'
261 LOGGER.info(msg)
262 return headers, HTTPStatus.NOT_FOUND, msg
263 except ProviderGenericError as err:
264 return api.get_exception(
265 err.http_status_code, headers, request.format,
266 err.ogc_exception_code, err.message)
269def get_collection_tiles_metadata(api: API, request: APIRequest,
270 dataset: str | None = None,
271 matrix_id: str | None = None
272 ) -> Tuple[dict, int, str]:
273 """
274 Get collection items tiles
276 :param request: A request object
277 :param dataset: dataset name
278 :param matrix_id: matrix identifier
280 :returns: tuple of headers, status code, content
281 """
283 if not request.is_valid([TilesMetadataFormat.TILEJSON]):
284 return api.get_format_exception(request)
285 headers = request.get_response_headers(**api.api_headers)
287 if any([dataset is None,
288 dataset not in api.config['resources'].keys()]):
290 msg = 'Collection not found'
291 return api.get_exception(
292 HTTPStatus.NOT_FOUND, headers, request.format, 'NotFound', msg)
294 LOGGER.debug('Creating collection tiles')
295 LOGGER.debug('Loading provider')
296 try:
297 t = get_provider_by_type(
298 api.config['resources'][dataset]['providers'], 'tile')
299 p = load_plugin('provider', t)
300 except KeyError:
301 msg = 'Invalid collection tiles'
302 return api.get_exception(
303 HTTPStatus.BAD_REQUEST, headers, request.format,
304 'InvalidParameterValue', msg)
305 except ProviderGenericError as err:
306 return api.get_exception(
307 err.http_status_code, headers, request.format,
308 err.ogc_exception_code, err.message)
310 # Get provider language (if any)
311 prv_locale = l10n.get_plugin_locale(t, request.raw_locale)
313 # Set response language to requested provider locale
314 # (if it supports language) and/or otherwise the requested pygeoapi
315 # locale (or fallback default locale)
316 l10n.set_response_language(headers, prv_locale, request.locale)
318 tiles_metadata = p.get_metadata(
319 dataset=dataset, server_url=api.base_url,
320 layer=p.get_layer(), tileset=matrix_id,
321 metadata_format=request._format, title=l10n.translate(
322 api.config['resources'][dataset]['title'],
323 request.locale),
324 description=l10n.translate(
325 api.config['resources'][dataset]['description'],
326 request.locale),
327 language=prv_locale)
329 if request.format == F_HTML: # render
330 tpl_config = api.get_dataset_templates(dataset)
331 content = render_j2_template(api.tpl_config, tpl_config,
332 'collections/tiles/metadata.html',
333 tiles_metadata, request.locale)
335 else:
336 content = to_json(tiles_metadata, api.pretty_print)
338 return headers, HTTPStatus.OK, content
341def tilematrixsets(api: API,
342 request: APIRequest) -> Tuple[dict, int, str]:
343 """
344 Provide tileMatrixSets definition
346 :param request: A request object
348 :returns: tuple of headers, status code, content
349 """
351 headers = request.get_response_headers(**api.api_headers)
353 # Retrieve available TileMatrixSets
354 enums = [e.value for e in TileMatrixSetEnum]
356 tms = {"tileMatrixSets": []}
358 for e in enums:
359 tms['tileMatrixSets'].append({
360 "title": e.title,
361 "id": e.tileMatrixSet,
362 "uri": e.tileMatrixSetURI,
363 "links": [
364 {
365 "rel": "self",
366 "type": "text/html",
367 "title": f"The HTML representation of the {e.tileMatrixSet} tile matrix set", # noqa
368 "href": f"{api.base_url}/TileMatrixSets/{e.tileMatrixSet}?f=html" # noqa
369 },
370 {
371 "rel": "self",
372 "type": "application/json",
373 "title": f"The JSON representation of the {e.tileMatrixSet} tile matrix set", # noqa
374 "href": f"{api.base_url}/TileMatrixSets/{e.tileMatrixSet}?f=json" # noqa
375 }
376 ]
377 })
379 tms['links'] = [{
380 "rel": "alternate",
381 "type": "text/html",
382 "title": l10n.translate('This document as HTML', request.locale),
383 "href": f"{api.base_url}/tileMatrixSets?f=html"
384 }, {
385 "rel": "self",
386 "type": "application/json",
387 "title": l10n.translate('This document as JSON', request.locale),
388 "href": f"{api.base_url}/tileMatrixSets?f=json"
389 }]
391 if request.format == F_HTML: # render
392 content = render_j2_template(api.tpl_config, api.tpl_config,
393 'tilematrixsets/index.html',
394 tms, request.locale)
395 return headers, HTTPStatus.OK, content
397 return headers, HTTPStatus.OK, to_json(tms, api.pretty_print)
400def tilematrixset(api: API,
401 request: APIRequest,
402 tileMatrixSetId: str) -> Tuple[dict, int, str]:
403 """
404 Provide tile matrix definition
406 :param request: A request object
408 :returns: tuple of headers, status code, content
409 """
411 headers = request.get_response_headers(**api.api_headers)
413 # Retrieve relevant TileMatrixSet
414 enums = [e.value for e in TileMatrixSetEnum]
415 enum = None
417 try:
418 for e in enums:
419 if tileMatrixSetId == e.tileMatrixSet:
420 enum = e
421 if not enum:
422 raise ValueError('could not find this tilematrixset')
423 except ValueError as err:
424 return api.get_exception(
425 HTTPStatus.BAD_REQUEST, headers, request.format,
426 'InvalidParameterValue', str(err))
428 tms = {
429 "title": enum.tileMatrixSet,
430 "crs": enum.crs,
431 "id": enum.tileMatrixSet,
432 "uri": enum.tileMatrixSetURI,
433 "orderedAxes": enum.orderedAxes,
434 "wellKnownScaleSet": enum.wellKnownScaleSet,
435 "tileMatrices": enum.tileMatrices
436 }
438 if request.format == F_HTML: # render
439 content = render_j2_template(api.tpl_config, api.tpl_config,
440 'tilematrixsets/tilematrixset.html',
441 tms, request.locale)
442 return headers, HTTPStatus.OK, content
444 return headers, HTTPStatus.OK, to_json(tms, api.pretty_print)
446def get_oas_30(cfg: dict, locale: str) -> tuple[list[dict[str, str]], dict[str, dict]]: # noqa
447 """
448 Get OpenAPI fragments
450 :param cfg: `dict` of configuration
451 :param locale: `str` of locale
453 :returns: `tuple` of `list` of tag objects, and `dict` of path objects
454 """
456 from pygeoapi.openapi import OPENAPI_YAML, get_visible_collections
458 paths = {}
460 LOGGER.debug('setting up tiles endpoints')
461 collections = filter_dict_by_key_value(cfg['resources'],
462 'type', 'collection')
464 for k, v in get_visible_collections(cfg).items():
465 tile_extension = filter_providers_by_type(
466 collections[k]['providers'], 'tile')
468 if tile_extension: 468 ↛ 469line 468 didn't jump to line 469 because the condition on line 468 was never true
469 tp = load_plugin('provider', tile_extension)
471 tiles_path = f'/collections/{k}/tiles'
472 title = l10n.translate(v['title'], locale)
473 description = l10n.translate(v['description'], locale)
475 datatype = 'vector'
477 if tile_extension['format']['mimetype'].startswith('image'):
478 datatype = 'map'
480 paths[tiles_path] = {
481 'get': {
482 'summary': f'Fetch a {title} tiles description',
483 'description': description,
484 'tags': [k],
485 'operationId': f'describe{k.capitalize()}.collection.{datatype}.getTileSetsList', # noqa
486 'parameters': [
487 {'$ref': '#/components/parameters/f'},
488 {'$ref': '#/components/parameters/lang'}
489 ],
490 'responses': {
491 '200': {'$ref': '#/components/responses/Tiles'},
492 '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa
493 '404': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/NotFound"}, # noqa
494 '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"} # noqa
495 }
496 }
497 }
499 tiles_data_path = f'{tiles_path}/{{tileMatrixSetId}}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}' # noqa
501 paths[tiles_data_path] = {
502 'get': {
503 'summary': f'Get a {title} tile',
504 'description': description,
505 'tags': [k],
506 'operationId': f'get{k.capitalize()}.collection.{datatype}.getTile', # noqa
507 'parameters': [
508 {'$ref': f"{OPENAPI_YAML['oapit']}#/components/parameters/tileMatrixSetId"}, # noqa
509 {'$ref': f"{OPENAPI_YAML['oapit']}#/components/parameters/tileMatrix"}, # noqa
510 {'$ref': f"{OPENAPI_YAML['oapit']}#/components/parameters/tileRow"}, # noqa
511 {'$ref': f"{OPENAPI_YAML['oapit']}#/components/parameters/tileCol"}, # noqa
512 {
513 'name': 'f',
514 'in': 'query',
515 'description': 'The optional f parameter indicates the output format which the server shall provide as part of the response document.', # noqa
516 'required': False,
517 'schema': {
518 'type': 'string',
519 'enum': [tp.format_type],
520 'default': tp.format_type
521 },
522 'style': 'form',
523 'explode': False
524 }
525 ],
526 'responses': {
527 '400': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/InvalidParameter"}, # noqa
528 '404': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/NotFound"}, # noqa
529 '500': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/ServerError"} # noqa
530 }
531 }
532 }
533 mimetype = tile_extension['format']['mimetype']
534 paths[tiles_data_path]['get']['responses']['200'] = {
535 'description': 'successful operation',
536 'content': {
537 mimetype: {
538 'schema': {
539 'type': 'string',
540 'format': 'binary'
541 }
542 }
543 }
544 }
546 return [{'name': 'tiles'}], {'paths': paths}