Coverage for gws-app/gws/plugin/ows_server/wms/__init__.py: 0%
119 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1"""WMS Service.
3Implements WMS 1.1.x and 1.3.0.
5Does not support SLD extensions except ``GetLegendGraphic``, for which only ``LAYERS`` is supported.
6"""
8# @TODO strict mode
9#
10# OGC 06-042 7.2.4.7.2
11# A server shall issue a service exception (code="LayerNotQueryable") if GetFeatureInfo is requested on a Layer that is not queryable.
13# OGC 06-042 7.2.4.6.3
14# A server shall throw a service exception (code="LayerNotDefined") if an invalid layer is requested.
16import gws
17import gws.base.legend
18import gws.base.ows.server as server
19import gws.base.shape
20import gws.base.web
21import gws.config.util
22import gws.gis.bounds
23import gws.gis.extent
24import gws.gis.crs
25import gws.gis.render
26import gws.lib.image
27import gws.lib.metadata
28import gws.lib.mime
29import gws.lib.uom
31gws.ext.new.owsService('wms')
33_cdir = gws.u.dirname(__file__)
35_DEFAULT_TEMPLATES = [
36 gws.Config(
37 type='py',
38 path=f'{_cdir}/templates/getCapabilities.cx.py',
39 subject='ows.GetCapabilities',
40 mimeTypes=[gws.lib.mime.XML],
41 ),
42 # NB use the wfs template with GML2 (qgis doesn't understand GML3 for WMS)
43 gws.Config(
44 type='py',
45 path=f'{_cdir}/../wfs/templates/getFeature2.cx.py',
46 subject='ows.GetFeatureInfo',
47 mimeTypes=[gws.lib.mime.GML2, gws.lib.mime.GML, gws.lib.mime.XML],
48 )
49]
51_DEFAULT_METADATA = gws.Metadata(
52 name='WMS',
53 inspireDegreeOfConformity='notEvaluated',
54 inspireMandatoryKeyword='infoMapAccessService',
55 inspireResourceType='service',
56 inspireSpatialDataServiceType='view',
57 isoServiceFunction='search',
58 isoScope='dataset',
59 isoSpatialRepresentationType='vector',
60)
63class Config(server.service.Config):
64 """WMS Service configuration"""
66 layerLimit: int = 0
67 """WMS LayerLimit. (added in 8.1)"""
68 maxPixelSize: int = 0
69 """WMS MaxWidth/MaxHeight value. (added in 8.1)"""
72class Object(server.service.Object):
73 protocol = gws.OwsProtocol.WMS
74 supportedVersions = ['1.3.0', '1.1.1', '1.1.0']
75 isRasterService = True
76 isOwsCommon = False
78 layerLimit: int = 0
79 maxPixelSize: int = 0
81 def configure(self):
82 self.layerLimit = self.cfg('layerLimit') or 0
83 self.maxPixelSize = self.cfg('layerLimit') or 0
85 def configure_templates(self):
86 return gws.config.util.configure_templates_for(self, extra=_DEFAULT_TEMPLATES)
88 def configure_metadata(self):
89 super().configure_metadata()
90 self.metadata = gws.lib.metadata.merge(_DEFAULT_METADATA, self.metadata)
92 def configure_operations(self):
93 self.supportedOperations = [
94 gws.OwsOperation(
95 verb=gws.OwsVerb.GetCapabilities,
96 formats=self.available_formats(gws.OwsVerb.GetCapabilities),
97 handlerName='handle_get_capabilities',
98 ),
99 gws.OwsOperation(
100 verb=gws.OwsVerb.GetFeatureInfo,
101 formats=self.available_formats(gws.OwsVerb.GetFeatureInfo),
102 handlerName='handle_get_feature_info',
103 ),
104 gws.OwsOperation(
105 verb=gws.OwsVerb.GetMap,
106 formats=self.available_formats(gws.OwsVerb.GetMap),
107 handlerName='handle_get_map',
108 ),
109 gws.OwsOperation(
110 verb=gws.OwsVerb.GetLegendGraphic,
111 formats=self.available_formats(gws.OwsVerb.GetLegendGraphic),
112 handlerName='handle_get_legend_graphic',
113 ),
114 ]
116 ##
118 def init_request(self, req):
119 sr = super().init_request(req)
121 sr.crs = sr.requested_crs('CRS,SRS') or sr.project.map.bounds.crs
122 sr.targetCrs = sr.crs
123 sr.alwaysXY = sr.version < '1.3'
124 sr.bounds = sr.requested_bounds('BBOX')
126 return sr
128 def layer_is_suitable(self, layer: gws.Layer):
129 return layer.isGroup or layer.canRenderBox or layer.isSearchable
131 ##
133 def handle_get_capabilities(self, sr: server.request.Object):
134 return self.template_response(
135 sr,
136 sr.requested_format('FORMAT'),
137 layerCapsList=sr.layerCapsList,
138 )
140 def handle_get_map(self, sr: server.request.Object):
141 self.set_size_and_resolution(sr)
143 lcs = self.requested_layer_caps(sr, 'LAYER,LAYERS', bottom_first=True)
144 if not lcs:
145 raise server.error.LayerNotDefined()
147 mime = sr.requested_format('FORMAT')
149 lcs = self.visible_layer_caps(sr, lcs)
150 if not lcs:
151 return self.image_response(sr, None, mime)
153 s = sr.string_param('TRANSPARENT', values={'true', 'false'}, default='true')
154 transparent = (s == 'true')
156 gws.log.debug(f'get_map: layers={[lc.layer for lc in lcs]}')
158 planes = [
159 gws.MapRenderInputPlane(
160 type=gws.MapRenderInputPlaneType.imageLayer,
161 layer=lc.layer
162 )
163 for lc in lcs
164 ]
166 mri = gws.MapRenderInput(
167 backgroundColor=None if transparent else 0,
168 bbox=sr.bounds.extent,
169 crs=sr.bounds.crs,
170 mapSize=(sr.pxWidth, sr.pxHeight, gws.Uom.px),
171 planes=planes,
172 project=self.project,
173 user=sr.req.user,
174 )
176 mro = gws.gis.render.render_map(mri)
178 return self.image_response(sr, mro.planes[0].image, mime)
180 def handle_get_legend_graphic(self, sr: server.request.Object):
181 # @TODO currently only support 'layer'
183 lcs = self.requested_layer_caps(sr, 'LAYER,LAYERS', bottom_first=False)
184 return self.render_legend(sr, lcs, sr.requested_format('FORMAT'))
186 def handle_get_feature_info(self, sr: server.request.Object):
187 # @TODO top-first or bottom-first?
189 self.set_size_and_resolution(sr)
191 lcs = self.requested_layer_caps(sr, 'QUERY_LAYERS', bottom_first=False)
192 lcs = [lc for lc in lcs if lc.isSearchable]
193 if not lcs:
194 raise server.error.LayerNotQueryable()
196 fc = self.get_features(sr, lcs)
198 return self.template_response(
199 sr,
200 sr.requested_format('INFO_FORMAT'),
201 featureCollection=fc,
202 )
204 ##
206 def requested_layer_caps(self, sr: server.request.Object, param_name: str, bottom_first=False) -> list[server.LayerCaps]:
207 # Order for GetMap is bottom-first (OGC 06-042 7.3.3.3):
208 # A WMS shall render the requested layers by drawing the leftmost in the list bottommost, the next one over that, and so on.
209 #
210 # Our layers are always top-first. So, for each requested layer, if it is a leaf, we add it to a lcs, otherwise,
211 # add group leaves in _reversed_ order. Finally, reverse the lcs list.
213 lcs = []
215 for name in sr.list_param(param_name):
216 for lc in sr.layerCapsList:
217 if not server.layer_caps.layer_name_matches(lc, name):
218 continue
219 if lc.isGroup:
220 lcs.extend(reversed(lc.leaves) if bottom_first else lc.leaves)
221 else:
222 lcs.append(lc)
224 if self.layerLimit and len(lcs) > self.layerLimit:
225 raise server.error.InvalidParameterValue('LAYER')
226 if not lcs:
227 raise server.error.LayerNotDefined()
229 return gws.u.uniq(reversed(lcs) if bottom_first else lcs)
231 def get_features(self, sr: server.request.Object, lcs: list[server.LayerCaps]):
233 lcs = self.visible_layer_caps(sr, lcs)
234 if not lcs:
235 return self.feature_collection(sr, lcs, 0, [])
237 # @TODO validate and raise InvalidPoint
239 x = sr.int_param('X,I')
240 y = sr.int_param('Y,J')
242 x = sr.bounds.extent[0] + (x * sr.xResolution)
243 y = sr.bounds.extent[3] - (y * sr.yResolution)
245 point = gws.base.shape.from_xy(x, y, sr.crs)
247 search = gws.SearchQuery(
248 project=sr.project,
249 layers=[lc.layer for lc in lcs],
250 limit=sr.requested_feature_count('FEATURE_COUNT'),
251 resolution=sr.xResolution,
252 shape=point,
253 tolerance=self.searchTolerance,
254 )
256 results = self.root.app.searchMgr.run_search(search, sr.req.user)
257 return self.feature_collection(sr, lcs, len(results), results)
259 def set_size_and_resolution(self, sr: server.request.Object):
260 if not sr.bounds:
261 raise server.error.MissingParameterValue('BBOX')
263 sr.pxWidth = sr.int_param('WIDTH')
264 sr.pxHeight = sr.int_param('HEIGHT')
265 w, h = gws.gis.extent.size(sr.bounds.extent)
267 dpi = sr.int_param('DPI', default=0) or sr.int_param('MAP_RESOLUTION', default=0)
268 if dpi:
269 # honor the dpi setting - compute the scale with "their" dpi and convert to "our" resolution
270 sr.xResolution = gws.lib.uom.scale_to_res(gws.lib.uom.mm_to_px(1000.0 * w / sr.pxWidth, dpi))
271 sr.yResolution = gws.lib.uom.scale_to_res(gws.lib.uom.mm_to_px(1000.0 * h / sr.pxHeight, dpi))
272 else:
273 sr.xResolution = w / sr.pxWidth
274 sr.yResolution = h / sr.pxHeight
276 gws.log.debug(f'set_size_and_resolution: {w=} px={sr.pxWidth}x{sr.pxHeight} {dpi=} res={sr.xResolution} 1:{gws.lib.uom.res_to_scale(sr.xResolution)}')
278 def visible_layer_caps(self, sr, lcs: list[server.LayerCaps]) -> list[server.LayerCaps]:
279 return [
280 lc for lc in lcs
281 if min(lc.layer.resolutions) <= sr.xResolution <= max(lc.layer.resolutions)
282 ]