Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WMTS support #274

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions terracotta/handlers/wmts.py
Original file line number Diff line number Diff line change
@@ -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')
220 changes: 220 additions & 0 deletions terracotta/handlers/wmts.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
<?xml version="1.0" encoding="UTF-8"?>
<Capabilities xmlns="http://www.opengis.net/wmts/1.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml" xsi:schemaLocation="http://www.opengis.net/wmts/1.0 http://schemas.opengis.net/wmts/1.0/wmtsGetCapabilities_response.xsd" version="1.0.0">
<ows:ServiceIdentification>
<ows:Title>"Title"</ows:Title>
<ows:ServiceType>OGC WMTS</ows:ServiceType>
<ows:ServiceTypeVersion>1.0.0</ows:ServiceTypeVersion>
</ows:ServiceIdentification>
<ows:OperationsMetadata>
<ows:Operation name="GetCapabilities">
<ows:DCP>
<ows:HTTP>
<ows:Get>
<ows:Constraint name="GetEncoding">
<ows:AllowedValues>
<ows:Value>RESTful</ows:Value>
</ows:AllowedValues>
</ows:Constraint>
</ows:Get>
</ows:HTTP>
</ows:DCP>
</ows:Operation>
<ows:Operation name="GetTile">
<ows:DCP>
<ows:HTTP>
<ows:Get>
<ows:Constraint name="GetEncoding">
<ows:AllowedValues>
<ows:Value>RESTful</ows:Value>
</ows:AllowedValues>
</ows:Constraint>
</ows:Get>
</ows:HTTP>
</ows:DCP>
</ows:Operation>
</ows:OperationsMetadata>
<Contents>
<TileMatrixSet>
<ows:Title>Google Maps Compatible for the World</ows:Title>
<ows:Identifier>WorldWebMercatorQuad</ows:Identifier>
<ows:BoundingBox crs="urn:ogc:def:crs:EPSG::3857">
<ows:LowerCorner>-20037508.3427892 -20037508.3427892</ows:LowerCorner>
<ows:UpperCorner>20037508.3427892 20037508.3427892</ows:UpperCorner>
</ows:BoundingBox>
<ows:SupportedCRS>urn:ogc:def:crs:EPSG::3857</ows:SupportedCRS>
<WellKnownScaleSet>urn:ogc:def:wkss:OGC:1.0:GoogleMapsCompatible</WellKnownScaleSet>
<TileMatrix>
<ows:Identifier>0</ows:Identifier>
<ScaleDenominator>559082264.0287178</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>1</MatrixWidth>
<MatrixHeight>1</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>1</ows:Identifier>
<ScaleDenominator>279541132.0143589</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>2</MatrixWidth>
<MatrixHeight>2</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>2</ows:Identifier>
<ScaleDenominator>139770566.0071794</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>4</MatrixWidth>
<MatrixHeight>4</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>3</ows:Identifier>
<ScaleDenominator>69885283.00358972</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>8</MatrixWidth>
<MatrixHeight>8</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>4</ows:Identifier>
<ScaleDenominator>34942641.50179486</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>16</MatrixWidth>
<MatrixHeight>16</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>5</ows:Identifier>
<ScaleDenominator>17471320.75089743</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>32</MatrixWidth>
<MatrixHeight>32</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>6</ows:Identifier>
<ScaleDenominator>8735660.375448715</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>64</MatrixWidth>
<MatrixHeight>64</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>7</ows:Identifier>
<ScaleDenominator>4367830.187724357</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>128</MatrixWidth>
<MatrixHeight>128</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>8</ows:Identifier>
<ScaleDenominator>2183915.093862179</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>256</MatrixWidth>
<MatrixHeight>256</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>9</ows:Identifier>
<ScaleDenominator>1091957.546931089</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>512</MatrixWidth>
<MatrixHeight>512</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>10</ows:Identifier>
<ScaleDenominator>545978.7734655447</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>1024</MatrixWidth>
<MatrixHeight>1024</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>11</ows:Identifier>
<ScaleDenominator>272989.3867327723</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>2048</MatrixWidth>
<MatrixHeight>2048</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>12</ows:Identifier>
<ScaleDenominator>136494.6933663862</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>4096</MatrixWidth>
<MatrixHeight>4096</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>13</ows:Identifier>
<ScaleDenominator>68247.34668319309</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>8192</MatrixWidth>
<MatrixHeight>8192</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>14</ows:Identifier>
<ScaleDenominator>34123.67334159654</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>16384</MatrixWidth>
<MatrixHeight>16384</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>15</ows:Identifier>
<ScaleDenominator>17061.83667079827</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>32768</MatrixWidth>
<MatrixHeight>32768</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>16</ows:Identifier>
<ScaleDenominator>8530.918335399136</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>65536</MatrixWidth>
<MatrixHeight>65536</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>17</ows:Identifier>
<ScaleDenominator>4265.459167699568</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>131072</MatrixWidth>
<MatrixHeight>131072</MatrixHeight>
</TileMatrix>
<TileMatrix>
<ows:Identifier>18</ows:Identifier>
<ScaleDenominator>2132.729583849784</ScaleDenominator>
<TopLeftCorner>-20037508.3427892 20037508.3427892</TopLeftCorner>
<TileWidth>256</TileWidth>
<TileHeight>256</TileHeight>
<MatrixWidth>262114</MatrixWidth>
<MatrixHeight>262114</MatrixHeight>
</TileMatrix>
</TileMatrixSet>
</Contents>
<ServiceMetadataURL/>
</Capabilities>
2 changes: 2 additions & 0 deletions terracotta/server/flask_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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='')
Expand Down
26 changes: 26 additions & 0 deletions terracotta/server/wmts.py
Original file line number Diff line number Diff line change
@@ -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')