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
46
48
47
49
LOGGER = logging .getLogger (__name__ )
48
50
51
+ WEBMERCATORQUAD = TileMatrixSetEnum .WEBMERCATORQUAD .value
52
+ WORLDCRS84QUAD = TileMatrixSetEnum .WORLDCRS84QUAD .value
53
+
54
+ CRS_CODES = {
55
+ 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' : 4326 ,
56
+ 'https://www.opengis.net/def/crs/OGC/0/CRS84' : 4326 ,
57
+ 'http://www.opengis.net/def/crs/EPSG/0/3857' : 3857
58
+ }
59
+
49
60
50
- class MVTPostgreSQLProvider (BaseMVTProvider ):
61
+ class MVTPostgreSQLProvider (BaseMVTProvider , PostgreSQLProvider ):
51
62
"""
52
63
MVT PostgreSQL Provider
53
64
Provider for serving tiles rendered on-the-fly from
@@ -62,48 +73,31 @@ def __init__(self, provider_def):
62
73
63
74
:returns: pygeoapi.provider.MVT.MVTPostgreSQLProvider
64
75
"""
65
-
66
- super ().__init__ (provider_def )
67
-
68
76
pg_def = deepcopy (provider_def )
69
77
# delete the zoom option before initializing the PostgreSQL provider
70
78
# that provider breaks otherwise
71
- del pg_def ["options" ]["zoom" ]
72
- self .postgres = PostgreSQLProvider (pg_def )
79
+ del pg_def ['options' ]['zoom' ]
80
+ PostgreSQLProvider .__init__ (self , pg_def )
81
+ BaseMVTProvider .__init__ (self , provider_def )
73
82
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 } '
83
-
84
- @property
85
- def service_url (self ):
86
- return self ._service_url
83
+ def get_fields (self ):
84
+ """
85
+ Get Postgrres fields
87
86
88
- @ property
89
- def service_metadata_url ( self ):
90
- return self . _service_metadata_url
87
+ :returns: `dict` of item fields
88
+ """
89
+ PostgreSQLProvider . get_fields ( self )
91
90
92
91
def get_layer (self ):
93
92
"""
94
93
Extracts layer name from url
95
94
96
95
:returns: layer name
97
96
"""
98
-
99
- return self .layer_name
97
+ return self .table
100
98
101
99
def get_tiling_schemes (self ):
102
-
103
- return [
104
- TileMatrixSetEnum .WEBMERCATORQUAD .value ,
105
- TileMatrixSetEnum .WORLDCRS84QUAD .value
106
- ]
100
+ return [WEBMERCATORQUAD , WORLDCRS84QUAD ]
107
101
108
102
def get_tiles_service (self , baseurl = None , servicepath = None ,
109
103
dirpath = None , tile_type = None ):
@@ -118,13 +112,14 @@ def get_tiles_service(self, baseurl=None, servicepath=None,
118
112
:returns: `dict` of item tile service
119
113
"""
120
114
121
- super ().get_tiles_service (baseurl , servicepath ,
122
- dirpath , tile_type )
115
+ BaseMVTProvider .get_tiles_service (self ,
116
+ baseurl , servicepath ,
117
+ dirpath , tile_type )
123
118
124
119
self ._service_url = servicepath
125
120
return self .get_tms_links ()
126
121
127
- def get_tiles (self , layer = None , tileset = None ,
122
+ def get_tiles (self , layer = 'default' , tileset = None ,
128
123
z = None , y = None , x = None , format_ = None ):
129
124
"""
130
125
Gets tile
@@ -141,64 +136,52 @@ def get_tiles(self, layer=None, tileset=None,
141
136
if format_ == 'mvt' :
142
137
format_ = self .format_type
143
138
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 ])
139
+ geom_column = getattr (self .table_model , self .geom )
140
+ tile_envelope = func .ST_TileEnvelope (z , x , y )
141
+ [tileset_schema ] = [
142
+ schema for schema in self .get_tiling_schemes ()
143
+ if tileset == schema .tileMatrixSet
144
+ ]
145
+
146
+ if not self .is_in_limits (tileset_schema , z , x , y ):
147
+ return ProviderTileNotFoundError
148
+
149
+ if tileset_schema .crs != self .storage_crs :
150
+ LOGGER .debug ('Transforming geometry' )
151
+ tile_envelope = func .ST_Transform (
152
+ tile_envelope , CRS_CODES [tileset_schema .crs ]
153
+ )
154
+ geom_column = func .ST_Transform (
155
+ geom_column , CRS_CODES [tileset_schema .crs ]
156
+ )
157
+
158
+ all_columns = [
159
+ c for c in self .table_model .__table__ .columns if c .name != 'geom'
160
+ ]
161
+ all_columns .append (
162
+ func .ST_AsMVTGeom (geom_column , tile_envelope ).label ('mvt' )
163
+ )
164
+ tile_query = select (
165
+ func .ST_AsMVT (
166
+ select (* all_columns )
167
+ .select_from (self .table_model )
168
+ .cte ('tile' )
169
+ .table_valued (),
170
+ layer
171
+ )
172
+ )
173
+
174
+ with Session (self ._engine ) as session :
175
+ result = session .execute (tile_query ).scalar ()
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