Coverage for gws-app/gws/plugin/ows_server/wmts/__init__.py: 50%
96 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +0200
1"""WMTS Service.
3Implements WMTS 1.0.0.
4This implementation only supports ``GET`` requests with ``KVP`` encoding.
6References:
7 - OGC 07-057r7 (https://portal.ogc.org/files/?artifact_id=35326)
8"""
10import gws
11import gws.config.util
12import gws.base.ows.server as server
13import gws.lib.extent
14import gws.lib.mime
15import gws.gis.render
16import gws.lib.uom
18gws.ext.new.owsService('wmts')
21class Config(server.service.Config):
22 """WMTS Service configuration"""
24 pass
27_DEFAULT_TEMPLATES = [
28 gws.Config(
29 type='py',
30 path=gws.u.dirname(__file__) + '/templates/getCapabilities.cx.py',
31 subject='ows.GetCapabilities',
32 mimeTypes=[gws.lib.mime.XML],
33 access=gws.c.PUBLIC,
34 ),
35]
37_DEFAULT_METADATA = gws.Metadata(
38 inspireDegreeOfConformity='notEvaluated',
39 inspireMandatoryKeyword='infoMapAccessService',
40 inspireResourceType='service',
41 inspireSpatialDataServiceType='view',
42 isoScope='dataset',
43 isoSpatialRepresentationType='vector',
44)
47class Object(server.service.Object):
48 protocol = gws.OwsProtocol.WMTS
49 supportedVersions = ['1.0.0']
50 isRasterService = True
51 isOwsCommon = True
53 tileMatrixSets: list[gws.TileMatrixSet]
54 tileSize = 256
56 def configure(self):
57 gws.config.util.configure_templates_for(self, extra=_DEFAULT_TEMPLATES)
59 # @TODO different matrix sets per layer
60 self.tileMatrixSets = []
61 for b in self.supportedBounds:
62 # see https://docs.opengeospatial.org/is/13-082r2/13-082r2.html#29
63 self.tileMatrixSets.append(
64 gws.TileMatrixSet(
65 uid=f'TMS_{b.crs.srid}',
66 crs=b.crs,
67 matrices=self.make_tile_matrices(b.extent, 0, 16, self.tileSize),
68 )
69 )
71 def configure_operations(self):
72 self.supportedOperations = [
73 gws.OwsOperation(
74 verb=gws.OwsVerb.GetCapabilities,
75 formats=self.available_formats(gws.OwsVerb.GetCapabilities),
76 handlerName='handle_get_capabilities',
77 ),
78 gws.OwsOperation(
79 verb=gws.OwsVerb.GetLegendGraphic,
80 formats=self.available_formats(gws.OwsVerb.GetLegendGraphic),
81 handlerName='handle_get_legend_graphic',
82 ),
83 gws.OwsOperation(
84 verb=gws.OwsVerb.GetTile,
85 formats=self.available_formats(gws.OwsVerb.GetTile),
86 handlerName='handle_get_tile',
87 ),
88 ]
90 def make_tile_matrices(self, extent, min_zoom, max_zoom, tile_size):
91 ms = []
93 w, h = gws.lib.extent.size(extent)
95 for z in range(min_zoom, max_zoom + 1):
96 size = 1 << z
97 res = w / (tile_size * size)
98 ms.append(
99 gws.TileMatrix(
100 uid=f'{z:02d}',
101 scale=gws.lib.uom.res_to_scale(res),
102 x=extent[0],
103 y=extent[3], # north origin
104 tileWidth=tile_size,
105 tileHeight=tile_size,
106 width=size,
107 height=size,
108 extent=extent,
109 )
110 )
112 return ms
114 ##
116 def init_request(self, req):
117 sr = super().init_request(req)
118 sr.require_project()
119 return sr
121 def layer_is_compatible(self, layer: gws.Layer):
122 return not layer.isGroup and layer.canRenderBox
124 ##
126 def handle_get_capabilities(self, sr: server.request.Object):
127 return self.template_response(
128 sr,
129 sr.requested_format('FORMAT'),
130 layerCapsList=sr.layerCapsList,
131 tileMatrixSets=self.tileMatrixSets,
132 )
134 def handle_get_tile(self, sr: server.request.Object):
135 lcs = self.requested_layer_caps(sr)
136 if len(lcs) != 1:
137 raise server.error.InvalidParameterValue('LAYER')
139 tms_uid = sr.string_param('TILEMATRIXSET')
140 tm_uid = sr.string_param('TILEMATRIX')
141 row = sr.int_param('TILEROW')
142 col = sr.int_param('TILECOL')
144 bounds = self.bounds_for_tile(tms_uid, tm_uid, row, col)
145 if not bounds:
146 raise server.error.TileOutOfRange()
147 gws.log.debug(f'WMTS: bounds for tile {tms_uid=} {tm_uid=} {row=} {col=}: {bounds}')
149 mime = sr.requested_format('FORMAT')
151 mri = gws.MapRenderInput(
152 backgroundColor=None,
153 bbox=bounds.extent,
154 crs=bounds.crs,
155 mapSize=(self.tileSize, self.tileSize, gws.Uom.px),
156 planes=[
157 gws.MapRenderInputPlane(
158 type=gws.MapRenderInputPlaneType.imageLayer,
159 layer=lc.layer,
160 )
161 for lc in lcs
162 ],
163 )
165 mro = gws.gis.render.render_map(mri)
167 if self.root.app.developer_option('ows.annotate_wmts'):
168 e = bounds.extent
169 text = f'{tm_uid} {row} {col}\n{e[0]}\n{e[1]}\n{e[2]}\n{e[3]}'
170 mro.planes[0].image = mro.planes[0].image.add_text(text, x=10, y=10).add_box()
172 return self.image_response(sr, mro.planes[0].image, mime)
174 def handle_get_legend_graphic(self, sr: server.request.Object):
175 lcs = self.requested_layer_caps(sr)
176 return self.render_legend(sr, lcs, sr.requested_format('FORMAT'))
178 ##
180 def requested_layer_caps(self, sr: server.request.Object):
181 lcs = []
183 for name in sr.list_param('LAYER'):
184 for lc in sr.layerCapsList:
185 if not server.layer_caps.layer_name_matches(lc, name):
186 continue
187 lcs.append(lc)
189 if not lcs:
190 raise server.error.LayerNotDefined()
192 return gws.u.uniq(lcs)
194 def bounds_for_tile(self, tms_uid, tm_uid, row, col):
195 tms = self.get_matrix_set(tms_uid)
196 if not tms:
197 return
198 tm = self.get_matrix(tms, tm_uid)
199 if not tm:
200 return
202 w, h = gws.lib.extent.size(tm.extent)
203 span = w / tm.width
205 x = tm.x + col * span
206 y = tm.y - row * span
208 bbox = x, y - span, x + span, y
209 return gws.Bounds(crs=tms.crs, extent=bbox)
211 def get_matrix_set(self, tms_uid):
212 for tms in self.tileMatrixSets:
213 if tms.uid == tms_uid:
214 return tms
216 def get_matrix(self, tms: gws.TileMatrixSet, tm_uid):
217 for tm in tms.matrices:
218 if tm.uid == tm_uid:
219 return tm