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

1"""WMS Service. 

2 

3Implements WMS 1.1.x and 1.3.0. 

4 

5Does not support SLD extensions except ``GetLegendGraphic``, for which only ``LAYERS`` is supported. 

6""" 

7 

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. 

12 

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. 

15 

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 

30 

31gws.ext.new.owsService('wms') 

32 

33_cdir = gws.u.dirname(__file__) 

34 

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] 

50 

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) 

61 

62 

63class Config(server.service.Config): 

64 """WMS Service configuration""" 

65 

66 layerLimit: int = 0 

67 """WMS LayerLimit. (added in 8.1)""" 

68 maxPixelSize: int = 0 

69 """WMS MaxWidth/MaxHeight value. (added in 8.1)""" 

70 

71 

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 

77 

78 layerLimit: int = 0 

79 maxPixelSize: int = 0 

80 

81 def configure(self): 

82 self.layerLimit = self.cfg('layerLimit') or 0 

83 self.maxPixelSize = self.cfg('layerLimit') or 0 

84 

85 def configure_templates(self): 

86 return gws.config.util.configure_templates_for(self, extra=_DEFAULT_TEMPLATES) 

87 

88 def configure_metadata(self): 

89 super().configure_metadata() 

90 self.metadata = gws.lib.metadata.merge(_DEFAULT_METADATA, self.metadata) 

91 

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 ] 

115 

116 ## 

117 

118 def init_request(self, req): 

119 sr = super().init_request(req) 

120 

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') 

125 

126 return sr 

127 

128 def layer_is_suitable(self, layer: gws.Layer): 

129 return layer.isGroup or layer.canRenderBox or layer.isSearchable 

130 

131 ## 

132 

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 ) 

139 

140 def handle_get_map(self, sr: server.request.Object): 

141 self.set_size_and_resolution(sr) 

142 

143 lcs = self.requested_layer_caps(sr, 'LAYER,LAYERS', bottom_first=True) 

144 if not lcs: 

145 raise server.error.LayerNotDefined() 

146 

147 mime = sr.requested_format('FORMAT') 

148 

149 lcs = self.visible_layer_caps(sr, lcs) 

150 if not lcs: 

151 return self.image_response(sr, None, mime) 

152 

153 s = sr.string_param('TRANSPARENT', values={'true', 'false'}, default='true') 

154 transparent = (s == 'true') 

155 

156 gws.log.debug(f'get_map: layers={[lc.layer for lc in lcs]}') 

157 

158 planes = [ 

159 gws.MapRenderInputPlane( 

160 type=gws.MapRenderInputPlaneType.imageLayer, 

161 layer=lc.layer 

162 ) 

163 for lc in lcs 

164 ] 

165 

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 ) 

175 

176 mro = gws.gis.render.render_map(mri) 

177 

178 return self.image_response(sr, mro.planes[0].image, mime) 

179 

180 def handle_get_legend_graphic(self, sr: server.request.Object): 

181 # @TODO currently only support 'layer' 

182 

183 lcs = self.requested_layer_caps(sr, 'LAYER,LAYERS', bottom_first=False) 

184 return self.render_legend(sr, lcs, sr.requested_format('FORMAT')) 

185 

186 def handle_get_feature_info(self, sr: server.request.Object): 

187 # @TODO top-first or bottom-first? 

188 

189 self.set_size_and_resolution(sr) 

190 

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() 

195 

196 fc = self.get_features(sr, lcs) 

197 

198 return self.template_response( 

199 sr, 

200 sr.requested_format('INFO_FORMAT'), 

201 featureCollection=fc, 

202 ) 

203 

204 ## 

205 

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. 

212 

213 lcs = [] 

214 

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) 

223 

224 if self.layerLimit and len(lcs) > self.layerLimit: 

225 raise server.error.InvalidParameterValue('LAYER') 

226 if not lcs: 

227 raise server.error.LayerNotDefined() 

228 

229 return gws.u.uniq(reversed(lcs) if bottom_first else lcs) 

230 

231 def get_features(self, sr: server.request.Object, lcs: list[server.LayerCaps]): 

232 

233 lcs = self.visible_layer_caps(sr, lcs) 

234 if not lcs: 

235 return self.feature_collection(sr, lcs, 0, []) 

236 

237 # @TODO validate and raise InvalidPoint 

238 

239 x = sr.int_param('X,I') 

240 y = sr.int_param('Y,J') 

241 

242 x = sr.bounds.extent[0] + (x * sr.xResolution) 

243 y = sr.bounds.extent[3] - (y * sr.yResolution) 

244 

245 point = gws.base.shape.from_xy(x, y, sr.crs) 

246 

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 ) 

255 

256 results = self.root.app.searchMgr.run_search(search, sr.req.user) 

257 return self.feature_collection(sr, lcs, len(results), results) 

258 

259 def set_size_and_resolution(self, sr: server.request.Object): 

260 if not sr.bounds: 

261 raise server.error.MissingParameterValue('BBOX') 

262 

263 sr.pxWidth = sr.int_param('WIDTH') 

264 sr.pxHeight = sr.int_param('HEIGHT') 

265 w, h = gws.gis.extent.size(sr.bounds.extent) 

266 

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 

275 

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)}') 

277 

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 ]