-
-
Notifications
You must be signed in to change notification settings - Fork 282
Add provider for MVT tile generation from Postgres #1979
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
base: master
Are you sure you want to change the base?
Changes from 6 commits
cc4bba3
029316e
a1fe9d5
97ba024
6f22762
e142aef
9a320cc
a034d15
9f596a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ pygeoapi core tile providers are listed below, along with supported features. | |
`MVT-elastic`_,✅,✅,✅,❌,❌,✅ | ||
`MVT-proxy`_,❓,❓,❓,❓,❌,✅ | ||
`WMTSFacade`_,✅,❌,✅,✅,✅,❌ | ||
`MVT-postgres`_,✅,✅,✅,✅,❌,✅ | ||
|
||
Below are specific connection examples based on supported providers. | ||
|
||
|
@@ -130,6 +131,46 @@ Following code block shows how to configure pygeoapi to read Mapbox vector tiles | |
name: pbf | ||
mimetype: application/vnd.mapbox-vector-tile | ||
|
||
MVT-postgres | ||
^^^^^^^^^^^^ | ||
|
||
.. note:: | ||
Requires Python packages sqlalchemy, geoalchemy2 and psycopg2-binary | ||
|
||
Must have PostGIS installed with protobuf-c support. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Turn into an additional note |
||
|
||
.. note:: | ||
Geometry must be using EPSG:4326 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand that this note is outdated for the postgresql provider. Is the note valid for this case? |
||
|
||
This provider gives support to serving tiles generated using `Postgres <https://www.postgresql.org/>`_ with `PostGIS <https://postgis.net/>`_. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
The tiles are rendered on-the-fly using `ST_AsMVT <https://postgis.net/docs/ST_AsMVT.html>`_ and related methods. | ||
|
||
This code block shows how to configure pygeoapi to render Mapbox vector tiles from a PostGIS table. | ||
|
||
.. code-block:: yaml | ||
|
||
providers: | ||
- type: tile | ||
name: MVT-postgres | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
data: | ||
host: 127.0.0.1 | ||
port: 3010 # Default 5432 if not provided | ||
dbname: test | ||
user: postgres | ||
password: postgres | ||
search_path: [osm, public] | ||
id_field: osm_id | ||
table: hotosm_bdi_waterways | ||
geom_field: foo_geom | ||
options: | ||
zoom: | ||
min: 0 | ||
max: 15 | ||
format: | ||
name: pbf | ||
mimetype: application/vnd.mapbox-vector-tile | ||
|
||
PostgreSQL-related connection options can also be added to `options`. Please refer to the :ref:`PostgreSQL OGC Features Provider<PostgreSQL>` documentation for more information. | ||
|
||
WMTSFacade | ||
^^^^^^^^^^ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,6 +53,7 @@ | |
'MVT-tippecanoe': 'pygeoapi.provider.mvt_tippecanoe.MVTTippecanoeProvider', # noqa: E501 | ||
'MVT-elastic': 'pygeoapi.provider.mvt_elastic.MVTElasticProvider', | ||
'MVT-proxy': 'pygeoapi.provider.mvt_proxy.MVTProxyProvider', | ||
'MVT-postgres': 'pygeoapi.provider.mvt_postgres.MVTPostgresProvider', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
'OracleDB': 'pygeoapi.provider.oracle.OracleProvider', | ||
'OGR': 'pygeoapi.provider.ogr.OGRProvider', | ||
'OpenSearch': 'pygeoapi.provider.opensearch_.OpenSearchProvider', | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rename to |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,246 @@ | ||
# ================================================================= | ||
# | ||
# Authors: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add yourself as author. Example in https://github.com/geopython/pygeoapi/blob/master/setup.py#L3 |
||
# | ||
# Copyright (c) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add copyright. Example in https://github.com/geopython/pygeoapi/blob/master/setup.py#L5 |
||
# | ||
# Permission is hereby granted, free of charge, to any person | ||
# obtaining a copy of this software and associated documentation | ||
# files (the "Software"), to deal in the Software without | ||
# restriction, including without limitation the rights to use, | ||
# copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
# copies of the Software, and to permit persons to whom the | ||
# Software is furnished to do so, subject to the following | ||
# conditions: | ||
# | ||
# The above copyright notice and this permission notice shall be | ||
# included in all copies or substantial portions of the Software. | ||
# | ||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | ||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | ||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | ||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | ||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | ||
# OTHER DEALINGS IN THE SOFTWARE. | ||
# | ||
# ================================================================= | ||
|
||
import logging | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Order imports by standard library, 3rd party packages, then local imports, alphabetically, separated by blank line. from copy import deepcopy
import logging
from sqlalchemy.sql import text
from pygeoapi.models.provider.base import (
TileSetMetadata, TileMatrixSetEnum, LinkType)
from pygeoapi.provider.base import ProviderConnectionError
from pygeoapi.provider.base_mvt import BaseMVTProvider
from pygeoapi.provider.postgresql import PostgreSQLProvider
from pygeoapi.util import url_join |
||
|
||
from pygeoapi.provider.base_mvt import BaseMVTProvider | ||
from pygeoapi.provider.postgresql import PostgreSQLProvider | ||
from pygeoapi.provider.base import ProviderConnectionError | ||
from pygeoapi.models.provider.base import ( | ||
TileSetMetadata, TileMatrixSetEnum, LinkType) | ||
from pygeoapi.util import url_join | ||
|
||
from sqlalchemy.sql import text | ||
|
||
from copy import deepcopy | ||
|
||
LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class MVTPostgresProvider(BaseMVTProvider): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rename to |
||
|
||
def __init__(self, provider_def): | ||
""" | ||
Initialize object | ||
|
||
:param provider_def: provider definition | ||
|
||
:returns: pygeoapi.provider.MVT.MVTPostgresProvider | ||
""" | ||
|
||
super().__init__(provider_def) | ||
|
||
pg_def = deepcopy(provider_def) | ||
del pg_def["options"]["zoom"] | ||
self.postgres = PostgreSQLProvider(pg_def) | ||
self.layer_name = provider_def["table"] | ||
self.table = provider_def['table'] | ||
self.id_field = provider_def['id_field'] | ||
self.geom = provider_def.get('geom_field', 'geom') | ||
|
||
LOGGER.debug(f'DB connection: {repr(self.postgres._engine.url)}') | ||
|
||
def __repr__(self): | ||
return f'<MVTPostgresProvider> {self.data}' | ||
|
||
@property | ||
def service_url(self): | ||
return self._service_url | ||
|
||
@property | ||
def service_metadata_url(self): | ||
return self._service_metadata_url | ||
|
||
def get_layer(self): | ||
""" | ||
Extracts layer name from url | ||
|
||
:returns: layer name | ||
""" | ||
|
||
return self.layer_name | ||
|
||
def get_tiling_schemes(self): | ||
|
||
return [ | ||
TileMatrixSetEnum.WEBMERCATORQUAD.value, | ||
TileMatrixSetEnum.WORLDCRS84QUAD.value | ||
] | ||
|
||
def get_tiles_service(self, baseurl=None, servicepath=None, | ||
dirpath=None, tile_type=None): | ||
""" | ||
Gets mvt service description | ||
|
||
:param baseurl: base URL of endpoint | ||
:param servicepath: base path of URL | ||
:param dirpath: directory basepath (equivalent of URL) | ||
:param tile_type: tile format type | ||
|
||
:returns: `dict` of item tile service | ||
""" | ||
|
||
super().get_tiles_service(baseurl, servicepath, | ||
dirpath, tile_type) | ||
|
||
self._service_url = servicepath | ||
return self.get_tms_links() | ||
|
||
def get_tiles(self, layer=None, tileset=None, | ||
z=None, y=None, x=None, format_=None): | ||
""" | ||
Gets tile | ||
|
||
:param layer: mvt tile layer | ||
:param tileset: mvt tileset | ||
:param z: z index | ||
:param y: y index | ||
:param x: x index | ||
:param format_: tile format | ||
|
||
:returns: an encoded mvt tile | ||
""" | ||
if format_ == 'mvt': | ||
format_ = self.format_type | ||
|
||
fields_arr = self.postgres.get_fields().keys() | ||
fields = ', '.join(['"' + f + '"' for f in fields_arr]) | ||
if len(fields) != 0: | ||
fields = ',' + fields | ||
|
||
query = '' | ||
if tileset == TileMatrixSetEnum.WEBMERCATORQUAD.value.tileMatrixSet: | ||
query = text(""" | ||
WITH | ||
bounds AS ( | ||
SELECT ST_TileEnvelope(:z, :x, :y) AS boundgeom | ||
), | ||
mvtgeom AS ( | ||
SELECT ST_AsMVTGeom(ST_Transform(ST_CurveToLine({geom}), 3857), bounds.boundgeom) AS geom {fields} | ||
FROM "{table}", bounds | ||
WHERE ST_Intersects({geom}, ST_Transform(bounds.boundgeom, 4326)) | ||
) | ||
SELECT ST_AsMVT(mvtgeom, 'default') FROM mvtgeom; | ||
""".format(geom=self.geom, table=self.table, fields=fields)) # noqa | ||
|
||
if tileset == TileMatrixSetEnum.WORLDCRS84QUAD.value.tileMatrixSet: | ||
query = text(""" | ||
WITH | ||
bounds AS ( | ||
SELECT ST_TileEnvelope(:z, :x, :y, | ||
'SRID=4326;POLYGON((-180 -90,-180 90,180 90,180 -90,-180 -90))'::geometry) AS boundgeom | ||
), | ||
mvtgeom AS ( | ||
SELECT ST_AsMVTGeom(ST_CurveToLine({geom}), bounds.boundgeom) AS geom {fields} | ||
FROM "{table}", bounds | ||
WHERE ST_Intersects({geom}, bounds.boundgeom) | ||
) | ||
SELECT ST_AsMVT(mvtgeom, 'default') FROM mvtgeom; | ||
""".format(geom=self.geom, table=self.table, fields=fields)) # noqa | ||
|
||
with self.postgres._engine.connect() as session: | ||
result = session.execute(query, { | ||
'z': z, | ||
'y': y, | ||
'x': x | ||
}).fetchone() | ||
|
||
if len(bytes(result[0])) == 0: | ||
return None | ||
return bytes(result[0]) | ||
|
||
def get_html_metadata(self, dataset, server_url, layer, tileset, | ||
title, description, keywords, **kwargs): | ||
|
||
service_url = url_join( | ||
server_url, | ||
f'collections/{dataset}/tiles/{tileset}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}?f=mvt') # noqa | ||
metadata_url = url_join( | ||
server_url, | ||
f'collections/{dataset}/tiles/{tileset}/metadata') | ||
|
||
metadata = dict() | ||
metadata['id'] = dataset | ||
metadata['title'] = title | ||
metadata['tileset'] = tileset | ||
metadata['collections_path'] = service_url | ||
metadata['json_url'] = f'{metadata_url}?f=json' | ||
|
||
return metadata | ||
|
||
def get_default_metadata(self, dataset, server_url, layer, tileset, | ||
title, description, keywords, **kwargs): | ||
|
||
service_url = url_join( | ||
server_url, | ||
f'collections/{dataset}/tiles/{tileset}/{{tileMatrix}}/{{tileRow}}/{{tileCol}}?f=mvt') # noqa | ||
|
||
content = {} | ||
tiling_schemes = self.get_tiling_schemes() | ||
# Default values | ||
tileMatrixSetURI = tiling_schemes[0].tileMatrixSetURI | ||
crs = tiling_schemes[0].crs | ||
# Checking the selected matrix in configured tiling_schemes | ||
for schema in tiling_schemes: | ||
if (schema.tileMatrixSet == tileset): | ||
crs = schema.crs | ||
tileMatrixSetURI = schema.tileMatrixSetURI | ||
|
||
tiling_scheme_url = url_join( | ||
server_url, f'/TileMatrixSets/{schema.tileMatrixSet}') | ||
tiling_scheme_url_type = "application/json" | ||
tiling_scheme_url_title = f'{schema.tileMatrixSet} tile matrix set definition' # noqa | ||
|
||
tiling_scheme = LinkType(href=tiling_scheme_url, | ||
rel="http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", # noqa | ||
type_=tiling_scheme_url_type, | ||
title=tiling_scheme_url_title) | ||
|
||
if tiling_scheme is None: | ||
msg = f'Could not identify a valid tiling schema' # noqa | ||
LOGGER.error(msg) | ||
raise ProviderConnectionError(msg) | ||
|
||
content = TileSetMetadata(title=title, description=description, | ||
keywords=keywords, crs=crs, | ||
tileMatrixSetURI=tileMatrixSetURI) | ||
|
||
links = [] | ||
service_url_link_type = "application/vnd.mapbox-vector-tile" | ||
service_url_link_title = f'{tileset} vector tiles for {layer}' | ||
service_url_link = LinkType(href=service_url, rel="item", | ||
type_=service_url_link_type, | ||
title=service_url_link_title) | ||
|
||
links.append(tiling_scheme) | ||
links.append(service_url_link) | ||
|
||
content.links = links | ||
|
||
return content.dict(exclude_none=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename to
MVT-postgresql