3
3
# Authors: Prajwal Amaravati <prajwal.s@satsure.co>
4
4
# Tanvi Prasad <tanvi.prasad@cdpg.org.in>
5
5
# Bryan Robert <bryan.robert@cdpg.org.in>
6
+ # Benjamin Webb <bwebb@lincolninst.edu>
6
7
#
7
8
# Copyright (c) 2025 Prajwal Amaravati
8
9
# Copyright (c) 2025 Tanvi Prasad
9
10
# Copyright (c) 2025 Bryan Robert
11
+ # Copyright (c) 2025 Benjamin Webb
10
12
#
11
13
# Permission is hereby granted, free of charge, to any person
12
14
# obtaining a copy of this software and associated documentation
34
36
from copy import deepcopy
35
37
import logging
36
38
37
- from sqlalchemy .sql import text
38
-
39
+ from sqlalchemy .sql import func , select
40
+ from sqlalchemy . orm import Session
39
41
from pygeoapi .models .provider .base import (
40
42
TileSetMetadata , TileMatrixSetEnum , LinkType )
41
43
from pygeoapi .provider .base import ProviderConnectionError
42
44
from pygeoapi .provider .base_mvt import BaseMVTProvider
43
45
from pygeoapi .provider .postgresql import PostgreSQLProvider
44
46
from pygeoapi .provider .tile import ProviderTileNotFoundError
45
- from pygeoapi .util import url_join
47
+ from pygeoapi .util import url_join , get_crs_from_uri
46
48
47
49
LOGGER = logging .getLogger (__name__ )
48
50
49
51
50
- class MVTPostgreSQLProvider (BaseMVTProvider ):
52
+ class MVTPostgreSQLProvider (PostgreSQLProvider , BaseMVTProvider ):
51
53
"""
52
54
MVT PostgreSQL Provider
53
55
Provider for serving tiles rendered on-the-fly from
@@ -62,48 +64,37 @@ def __init__(self, provider_def):
62
64
63
65
:returns: pygeoapi.provider.MVT.MVTPostgreSQLProvider
64
66
"""
65
-
66
- super ().__init__ (provider_def )
67
-
68
67
pg_def = deepcopy (provider_def )
69
68
# delete the zoom option before initializing the PostgreSQL provider
70
69
# that provider breaks otherwise
71
- del pg_def ["options" ]["zoom" ]
72
- self .postgres = PostgreSQLProvider (pg_def )
73
-
74
- self .layer_name = provider_def ["table" ]
75
- self .table = provider_def ['table' ]
76
- self .id_field = provider_def ['id_field' ]
77
- self .geom = provider_def .get ('geom_field' , 'geom' )
78
-
79
- LOGGER .debug (f'DB connection: { repr (self .postgres ._engine .url )} ' )
80
-
81
- def __repr__ (self ):
82
- return f'<MVTPostgreSQLProvider> { self .data } '
70
+ del pg_def ['options' ]['zoom' ]
71
+ PostgreSQLProvider .__init__ (self , pg_def )
72
+ BaseMVTProvider .__init__ (self , provider_def )
83
73
84
- @ property
85
- def service_url ( self ):
86
- return self . _service_url
74
+ def get_fields ( self ):
75
+ """
76
+ Get Postgres columns
87
77
88
- @property
89
- def service_metadata_url (self ):
90
- return self ._service_metadata_url
78
+ :returns: `list` of columns
79
+ """
80
+ return [
81
+ column for column in self .table_model .__table__ .columns
82
+ if column .name != self .geom
83
+ ]
91
84
92
85
def get_layer (self ):
93
86
"""
94
- Extracts layer name from url
87
+ Use table name as layer name
95
88
96
- :returns: layer name
89
+ :returns: `str` of layer name
97
90
"""
98
-
99
- return self .layer_name
91
+ return self .table
100
92
101
93
def get_tiling_schemes (self ):
102
-
103
94
return [
104
- TileMatrixSetEnum .WEBMERCATORQUAD .value ,
105
- TileMatrixSetEnum .WORLDCRS84QUAD .value
106
- ]
95
+ TileMatrixSetEnum .WEBMERCATORQUAD .value ,
96
+ TileMatrixSetEnum .WORLDCRS84QUAD .value
97
+ ]
107
98
108
99
def get_tiles_service (self , baseurl = None , servicepath = None ,
109
100
dirpath = None , tile_type = None ):
@@ -118,13 +109,14 @@ def get_tiles_service(self, baseurl=None, servicepath=None,
118
109
:returns: `dict` of item tile service
119
110
"""
120
111
121
- super ().get_tiles_service (baseurl , servicepath ,
122
- dirpath , tile_type )
112
+ BaseMVTProvider .get_tiles_service (self ,
113
+ baseurl , servicepath ,
114
+ dirpath , tile_type )
123
115
124
116
self ._service_url = servicepath
125
117
return self .get_tms_links ()
126
118
127
- def get_tiles (self , layer = None , tileset = None ,
119
+ def get_tiles (self , layer = 'default' , tileset = None ,
128
120
z = None , y = None , x = None , format_ = None ):
129
121
"""
130
122
Gets tile
@@ -141,64 +133,55 @@ def get_tiles(self, layer=None, tileset=None,
141
133
if format_ == 'mvt' :
142
134
format_ = self .format_type
143
135
144
- fields_arr = self .postgres .get_fields ().keys ()
145
- fields = ', ' .join (['"' + f + '"' for f in fields_arr ])
146
- if len (fields ) != 0 :
147
- fields = ',' + fields
148
-
149
- query = ''
150
- if tileset == TileMatrixSetEnum .WEBMERCATORQUAD .value .tileMatrixSet :
151
- if not self .is_in_limits (TileMatrixSetEnum .WEBMERCATORQUAD .value , z , x , y ): # noqa
152
- raise ProviderTileNotFoundError
153
-
154
- query = text ("""
155
- WITH
156
- bounds AS (
157
- SELECT ST_TileEnvelope(:z, :x, :y) AS boundgeom
158
- ),
159
- mvtgeom AS (
160
- SELECT ST_AsMVTGeom(ST_Transform(ST_CurveToLine({geom}), 3857), bounds.boundgeom) AS geom {fields}
161
- FROM "{table}", bounds
162
- WHERE ST_Intersects({geom}, ST_Transform(bounds.boundgeom, 4326))
163
- )
164
- SELECT ST_AsMVT(mvtgeom, 'default') FROM mvtgeom;
165
- """ .format (geom = self .geom , table = self .table , fields = fields )) # noqa
166
-
167
- if tileset == TileMatrixSetEnum .WORLDCRS84QUAD .value .tileMatrixSet :
168
- if not self .is_in_limits (TileMatrixSetEnum .WORLDCRS84QUAD .value , z , x , y ): # noqa
169
- raise ProviderTileNotFoundError
170
-
171
- query = text ("""
172
- WITH
173
- bounds AS (
174
- SELECT ST_TileEnvelope(:z, :x, :y,
175
- 'SRID=4326;POLYGON((-180 -90,-180 90,180 90,180 -90,-180 -90))'::geometry) AS boundgeom
176
- ),
177
- mvtgeom AS (
178
- SELECT ST_AsMVTGeom(ST_CurveToLine({geom}), bounds.boundgeom) AS geom {fields}
179
- FROM "{table}", bounds
180
- WHERE ST_Intersects({geom}, bounds.boundgeom)
181
- )
182
- SELECT ST_AsMVT(mvtgeom, 'default') FROM mvtgeom;
183
- """ .format (geom = self .geom , table = self .table , fields = fields )) # noqa
184
-
185
- with self .postgres ._engine .connect () as session :
186
- result = session .execute (query , {
187
- 'z' : z ,
188
- 'y' : y ,
189
- 'x' : x
190
- }).fetchone ()
191
-
192
- if len (bytes (result [0 ])) == 0 :
193
- return None
194
- return bytes (result [0 ])
136
+ [tileset_schema ] = [
137
+ schema for schema in self .get_tiling_schemes ()
138
+ if tileset == schema .tileMatrixSet
139
+ ]
140
+ if not self .is_in_limits (tileset_schema , z , x , y ):
141
+ LOGGER .warning (f'Tile { z } /{ x } /{ y } not found' )
142
+ return ProviderTileNotFoundError
143
+
144
+ storage_srid = get_crs_from_uri (self .storage_crs ).to_string ()
145
+ out_srid = get_crs_from_uri (tileset_schema .crs ).to_string ()
146
+
147
+ tile_envelope = func .ST_TileEnvelope (z , x , y )
148
+ envelope = (
149
+ select (tile_envelope .label ('bounds' ))
150
+ .cte ('envelope' )
151
+ )
152
+
153
+ geom_column = getattr (self .table_model , self .geom )
154
+ mvtgeom = (
155
+ func .ST_AsMVTGeom (
156
+ func .ST_Transform (geom_column , out_srid ),
157
+ func .ST_Transform (envelope .c .bounds , out_srid ))
158
+ .label ('mvtgeom' )
159
+ )
160
+
161
+ geom_filter = geom_column .intersects (
162
+ func .ST_Transform (tile_envelope , storage_srid ))
163
+ mvtrow = (
164
+ select (* self .get_fields (), mvtgeom )
165
+ .filter (geom_filter )
166
+ .select_from (self .table_model )
167
+ .cte ('mvtrow' )
168
+ .table_valued ()
169
+ )
170
+
171
+ mvt_query = select (func .ST_AsMVT (mvtrow , layer ))
172
+
173
+ with Session (self ._engine ) as session :
174
+ result = session .execute (mvt_query ).scalar ()
175
+
176
+ return bytes (result ) or None
195
177
196
178
def get_html_metadata (self , dataset , server_url , layer , tileset ,
197
179
title , description , keywords , ** kwargs ):
198
180
199
181
service_url = url_join (
200
182
server_url ,
201
- f'collections/{ dataset } /tiles/{ tileset } /{{tileMatrix}}/{{tileRow}}/{{tileCol}}?f=mvt' ) # noqa
183
+ f'collections/{ dataset } /tiles/{ tileset } '
184
+ '{tileMatrix}/{tileRow}/{tileCol}?f=mvt' )
202
185
metadata_url = url_join (
203
186
server_url ,
204
187
f'collections/{ dataset } /tiles/{ tileset } /metadata' )
@@ -217,9 +200,10 @@ def get_default_metadata(self, dataset, server_url, layer, tileset,
217
200
218
201
service_url = url_join (
219
202
server_url ,
220
- f'collections/{ dataset } /tiles/{ tileset } /{{tileMatrix}}/{{tileRow}}/{{tileCol}}?f=mvt' ) # noqa
203
+ f'collections/{ dataset } /tiles/{ tileset } ' ,
204
+ '{tileMatrix}/{tileRow}/{tileCol}?f=mvt'
205
+ )
221
206
222
- content = {}
223
207
tiling_schemes = self .get_tiling_schemes ()
224
208
# Default values
225
209
tileMatrixSetURI = tiling_schemes [0 ].tileMatrixSetURI
@@ -231,17 +215,18 @@ def get_default_metadata(self, dataset, server_url, layer, tileset,
231
215
tileMatrixSetURI = schema .tileMatrixSetURI
232
216
233
217
tiling_scheme_url = url_join (
234
- server_url , f'/ TileMatrixSets/ { schema .tileMatrixSet } ' )
235
- tiling_scheme_url_type = " application/json"
218
+ server_url , ' TileMatrixSets' , schema .tileMatrixSet )
219
+ tiling_scheme_url_type = ' application/json'
236
220
tiling_scheme_url_title = f'{ schema .tileMatrixSet } tile matrix set definition' # noqa
237
221
238
- tiling_scheme = LinkType (href = tiling_scheme_url ,
239
- rel = "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme" , # noqa
240
- type_ = tiling_scheme_url_type ,
241
- title = tiling_scheme_url_title )
222
+ tiling_scheme = LinkType (
223
+ href = tiling_scheme_url ,
224
+ rel = 'http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme' ,
225
+ type_ = tiling_scheme_url_type ,
226
+ title = tiling_scheme_url_title )
242
227
243
228
if tiling_scheme is None :
244
- msg = f 'Could not identify a valid tiling schema' # noqa
229
+ msg = 'Could not identify a valid tiling schema'
245
230
LOGGER .error (msg )
246
231
raise ProviderConnectionError (msg )
247
232
@@ -250,9 +235,9 @@ def get_default_metadata(self, dataset, server_url, layer, tileset,
250
235
tileMatrixSetURI = tileMatrixSetURI )
251
236
252
237
links = []
253
- service_url_link_type = " application/vnd.mapbox-vector-tile"
238
+ service_url_link_type = ' application/vnd.mapbox-vector-tile'
254
239
service_url_link_title = f'{ tileset } vector tiles for { layer } '
255
- service_url_link = LinkType (href = service_url , rel = " item" ,
240
+ service_url_link = LinkType (href = service_url , rel = ' item' ,
256
241
type_ = service_url_link_type ,
257
242
title = service_url_link_title )
258
243
@@ -261,4 +246,7 @@ def get_default_metadata(self, dataset, server_url, layer, tileset,
261
246
262
247
content .links = links
263
248
264
- return content .dict (exclude_none = True )
249
+ return content .model_dump (exclude_none = True )
250
+
251
+ def __repr__ (self ):
252
+ return f'<MVTPostgreSQLProvider> { self .data } '
0 commit comments