diff --git a/terracotta/handlers/wmts.py b/terracotta/handlers/wmts.py new file mode 100644 index 00000000..e583d514 --- /dev/null +++ b/terracotta/handlers/wmts.py @@ -0,0 +1,79 @@ +"""handlers/wmts.py + +Handle /wmts API endpoint. +""" + +import importlib.resources +from typing import Tuple +import xml.etree.ElementTree as ET + +import terracotta.handlers as package +from terracotta import get_driver, get_settings +from terracotta.profile import trace + + +@trace('wmts_handler') +def wmts(url_root: str, dimension: str = None) -> str: + """ """ + settings = get_settings() + driver = get_driver(settings.DRIVER_PATH, provider=settings.DRIVER_PROVIDER) + + assert dimension is None or dimension in driver.key_names + if dimension: + key_indices = {key: i for i, key in enumerate(driver.key_names)} + dimension_index = key_indices[dimension] if dimension is not None else None + + def dataset_without_dimension(dataset: Tuple[str,...]) -> Tuple[str, ...]: + if dimension is None: + return dataset + else: + return dataset[:dimension_index] + dataset[dimension_index + 1:] + + datasets = driver.get_datasets() + summarised_datasets = set(map(dataset_without_dimension, datasets)) + + for _, (prefix, uri) in ET.iterparse(importlib.resources.open_text(package, 'wmts.xml'), events=['start-ns']): + ET.register_namespace(prefix, uri) + + get_capabilities_xml_tree = ET.parse(importlib.resources.open_text(package, 'wmts.xml')) + for el in get_capabilities_xml_tree.getroot().findall('.//{http://www.opengis.net/ows/1.1}Get'): + el.set('xlink:href', f'{url_root}wmts') + service_metadata_url = get_capabilities_xml_tree.find('.//{http://www.opengis.net/wmts/1.0}ServiceMetadataURL') + assert service_metadata_url is not None + service_metadata_url.set('xlink:href', f'{url_root}wmts') + contents_element = get_capabilities_xml_tree.find('.//{http://www.opengis.net/wmts/1.0}Contents') + assert contents_element is not None + + for dataset in summarised_datasets: + dimension_datasets = list(filter(lambda ds: dataset_without_dimension(ds) == dataset, datasets)) + + layer = ET.Element('Layer') + ET.SubElement(layer, 'ows:Title').text = 'Title' + ET.SubElement(layer, 'ows:Identifier').text = 'Layer name' + ET.SubElement(layer, 'ows:Abstract').text = 'Description' + bbox = ET.SubElement(layer, 'ows:WGS84BoundingBox', crs='urn:ogc:def:crs:OGC:2:84') + bounds = driver.get_metadata(dimension_datasets[0])['bounds'] + ET.SubElement(bbox, 'ows:LowerCorner').text = ' '.join(map(str, bounds[:2])) + ET.SubElement(bbox, 'ows:UpperCorner').text = ' '.join(map(str, bounds[2:])) + style = ET.SubElement(layer, 'Style', isDefault='true') + ET.SubElement(style, 'ows:Identifier').text = 'default' + ET.SubElement(layer, 'Format').text = 'image/png' + if dimension: + dimension_values = [ds[dimension_index] for ds in dimension_datasets] + dimension_element = ET.SubElement(layer, 'Dimension') + ET.SubElement(dimension_element, 'ows:Identifier').text = dimension + ET.SubElement(dimension_element, 'Default').text = dimension_values[0] + for dimension_value in dimension_values: + ET.SubElement(dimension_element, 'Value').text = dimension_value + tile_matrix_set_link = ET.SubElement(layer, 'TileMatrixSetLink') + ET.SubElement(tile_matrix_set_link, 'TileMatrixSet').text = 'WorldWebMercatorQuad' + dataset_keys = '/'.join(dataset if dimension is None else dataset[:dimension_index] + (f'{{{dimension}}}',) + dataset[dimension_index:]) + ET.SubElement( + layer, 'ResourceURL', + format='image/png', + resourceType='tile', + template=f'{url_root}singleband/{dataset_keys}/{{TileMatrix}}/{{TileCol}}/{{TileRow}}.png' + ) + contents_element.append(layer) + + return ET.tostring(get_capabilities_xml_tree.getroot(), encoding='unicode') diff --git a/terracotta/handlers/wmts.xml b/terracotta/handlers/wmts.xml new file mode 100644 index 00000000..45412483 --- /dev/null +++ b/terracotta/handlers/wmts.xml @@ -0,0 +1,220 @@ + + + + "Title" + OGC WMTS + 1.0.0 + + + + + + + + + RESTful + + + + + + + + + + + + + RESTful + + + + + + + + + + Google Maps Compatible for the World + WorldWebMercatorQuad + + -20037508.3427892 -20037508.3427892 + 20037508.3427892 20037508.3427892 + + urn:ogc:def:crs:EPSG::3857 + urn:ogc:def:wkss:OGC:1.0:GoogleMapsCompatible + + 0 + 559082264.0287178 + -20037508.3427892 20037508.3427892 + 256 + 256 + 1 + 1 + + + 1 + 279541132.0143589 + -20037508.3427892 20037508.3427892 + 256 + 256 + 2 + 2 + + + 2 + 139770566.0071794 + -20037508.3427892 20037508.3427892 + 256 + 256 + 4 + 4 + + + 3 + 69885283.00358972 + -20037508.3427892 20037508.3427892 + 256 + 256 + 8 + 8 + + + 4 + 34942641.50179486 + -20037508.3427892 20037508.3427892 + 256 + 256 + 16 + 16 + + + 5 + 17471320.75089743 + -20037508.3427892 20037508.3427892 + 256 + 256 + 32 + 32 + + + 6 + 8735660.375448715 + -20037508.3427892 20037508.3427892 + 256 + 256 + 64 + 64 + + + 7 + 4367830.187724357 + -20037508.3427892 20037508.3427892 + 256 + 256 + 128 + 128 + + + 8 + 2183915.093862179 + -20037508.3427892 20037508.3427892 + 256 + 256 + 256 + 256 + + + 9 + 1091957.546931089 + -20037508.3427892 20037508.3427892 + 256 + 256 + 512 + 512 + + + 10 + 545978.7734655447 + -20037508.3427892 20037508.3427892 + 256 + 256 + 1024 + 1024 + + + 11 + 272989.3867327723 + -20037508.3427892 20037508.3427892 + 256 + 256 + 2048 + 2048 + + + 12 + 136494.6933663862 + -20037508.3427892 20037508.3427892 + 256 + 256 + 4096 + 4096 + + + 13 + 68247.34668319309 + -20037508.3427892 20037508.3427892 + 256 + 256 + 8192 + 8192 + + + 14 + 34123.67334159654 + -20037508.3427892 20037508.3427892 + 256 + 256 + 16384 + 16384 + + + 15 + 17061.83667079827 + -20037508.3427892 20037508.3427892 + 256 + 256 + 32768 + 32768 + + + 16 + 8530.918335399136 + -20037508.3427892 20037508.3427892 + 256 + 256 + 65536 + 65536 + + + 17 + 4265.459167699568 + -20037508.3427892 20037508.3427892 + 256 + 256 + 131072 + 131072 + + + 18 + 2132.729583849784 + -20037508.3427892 20037508.3427892 + 256 + 256 + 262114 + 262114 + + + + + \ No newline at end of file diff --git a/terracotta/server/flask_api.py b/terracotta/server/flask_api.py index 4801925e..e64c1e4e 100644 --- a/terracotta/server/flask_api.py +++ b/terracotta/server/flask_api.py @@ -97,6 +97,7 @@ def create_app(debug: bool = False, profile: bool = False) -> Flask: import terracotta.server.rgb import terracotta.server.singleband import terracotta.server.compute + import terracotta.server.wmts new_app = Flask('terracotta.server') new_app.debug = debug @@ -128,6 +129,7 @@ def create_app(debug: bool = False, profile: bool = False) -> Flask: SPEC.path(view=terracotta.server.singleband.get_singleband_preview) SPEC.path(view=terracotta.server.compute.get_compute) SPEC.path(view=terracotta.server.compute.get_compute_preview) + SPEC.path(view=terracotta.server.wmts.get_wmts_capabilities) import terracotta.server.spec new_app.register_blueprint(SPEC_API, url_prefix='') diff --git a/terracotta/server/wmts.py b/terracotta/server/wmts.py new file mode 100644 index 00000000..0a5ed292 --- /dev/null +++ b/terracotta/server/wmts.py @@ -0,0 +1,26 @@ +"""server/wmts.py + +Flask route to handle /wmts calls. +""" + +from flask import jsonify, Response, request +from marshmallow import Schema, fields, EXCLUDE + +from terracotta.server.flask_api import METADATA_API + + +class DimensionOptionSchema(Schema): + class Meta: + unknown = EXCLUDE + + dimension = fields.String(required=False, default=None, description='Key to make dimension along') + + +@METADATA_API.route('/wmts', methods=['GET']) +def get_wmts_capabilities() -> Response: + from terracotta.handlers.wmts import wmts + option_schema = DimensionOptionSchema() + options = option_schema.load(request.args) + + capabilities = wmts(request.url_root, options.get('dimension')) + return Response(capabilities, mimetype='text/xml')