-
-
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
Open
ThorodanBrom
wants to merge
9
commits into
geopython:master
Choose a base branch
from
ThorodanBrom:add-mvt-postgres-provider
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
cc4bba3
Initial commit for adding MVT Postgres provider
ThorodanBrom 029316e
Added get_tile method for MVT-postgres for on-the-fly tile generation
ThorodanBrom a1fe9d5
Added documentation for the MVTPostgresProvider class
ThorodanBrom 97ba024
Updates to MVT postgres provider
ThorodanBrom 6f22762
Fix flake8 issues in `base_mvt.py`
ThorodanBrom e142aef
Fix MVT-postgres documentation - fix example config
ThorodanBrom 9a320cc
Changed name of MVT-postgres plugin to MVT-postgresql
ThorodanBrom a034d15
MVT-postgresql : Add check if the indices are not in limits of the TM…
ThorodanBrom 9f596a1
Fix flake8 errors
ThorodanBrom File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
# ================================================================= | ||
# | ||
# Authors: Prajwal Amaravati <prajwal.s@satsure.co> | ||
# Tanvi Prasad <tanvi.prasad@cdpg.org.in> | ||
# Bryan Robert <bryan.robert@cdpg.org.in> | ||
# | ||
# Copyright (c) 2025 Prajwal Amaravati | ||
# Copyright (c) 2025 Tanvi Prasad | ||
# Copyright (c) 2025 Bryan Robert | ||
# | ||
# 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. | ||
# | ||
# ================================================================= | ||
|
||
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.provider.tile import ProviderTileNotFoundError | ||
from pygeoapi.util import url_join | ||
|
||
LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class MVTPostgreSQLProvider(BaseMVTProvider): | ||
""" | ||
MVT PostgreSQL Provider | ||
Provider for serving tiles rendered on-the-fly from | ||
feature tables in PostgreSQL | ||
""" | ||
|
||
def __init__(self, provider_def): | ||
""" | ||
Initialize object | ||
|
||
:param provider_def: provider definition | ||
|
||
:returns: pygeoapi.provider.MVT.MVTPostgreSQLProvider | ||
""" | ||
|
||
super().__init__(provider_def) | ||
|
||
pg_def = deepcopy(provider_def) | ||
# delete the zoom option before initializing the PostgreSQL provider | ||
# that provider breaks otherwise | ||
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'<MVTPostgreSQLProvider> {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: | ||
if not self.is_in_limits(TileMatrixSetEnum.WEBMERCATORQUAD.value, z, x, y): # noqa | ||
raise ProviderTileNotFoundError | ||
|
||
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: | ||
if not self.is_in_limits(TileMatrixSetEnum.WORLDCRS84QUAD.value, z, x, y): # noqa | ||
raise ProviderTileNotFoundError | ||
|
||
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) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
I understand that this note is outdated for the postgresql provider. Is the note valid for this case?