Coverage for gws-app/gws/base/map/action.py: 61%

153 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 12:46 +0200

1"""Map related commands.""" 

2 

3from typing import Optional 

4 

5import gws 

6import gws.base.action 

7import gws.base.legend 

8import gws.lib.bounds 

9import gws.lib.crs 

10import gws.gis.render 

11import gws.lib.image 

12import gws.lib.intl 

13import gws.lib.jsonx 

14import gws.lib.mime 

15import gws.lib.uom 

16 

17gws.ext.new.action('map') 

18 

19 

20class Config(gws.base.action.Config): 

21 """Configuration for the map action.""" 

22 

23 pass 

24 

25 

26class Props(gws.base.action.Props): 

27 pass 

28 

29 

30class GetBoxRequest(gws.Request): 

31 bbox: gws.Extent 

32 width: int 

33 height: int 

34 layerUid: str 

35 crs: Optional[gws.CrsName] 

36 dpi: Optional[int] 

37 compositeLayerUids: Optional[list[str]] 

38 

39 

40class GetXyzRequest(gws.Request): 

41 layerUid: str 

42 x: int 

43 y: int 

44 z: int 

45 

46 

47class GetLegendRequest(gws.Request): 

48 layerUid: str 

49 

50 

51class ImageResponse(gws.Response): 

52 content: bytes 

53 mime: str 

54 

55 

56class DescribeLayerRequest(gws.Request): 

57 layerUid: str 

58 

59 

60class DescribeLayerResponse(gws.Request): 

61 content: str 

62 

63 

64class GetFeaturesRequest(gws.Request): 

65 bbox: Optional[gws.Extent] 

66 layerUid: str 

67 modelUid: Optional[str] 

68 crs: Optional[gws.CrsName] 

69 resolution: Optional[float] 

70 limit: int = 0 

71 views: Optional[list[str]] 

72 

73 

74class GetFeaturesResponse(gws.Response): 

75 features: list[gws.FeatureProps] 

76 

77 

78_GET_FEATURES_LIMIT = 10000 

79_PIXEL_SIZE_LIMIT = 4096 

80 

81 

82class Object(gws.base.action.Object): 

83 _empty_pixel = gws.lib.mime.PNG, gws.lib.image.empty_pixel() 

84 

85 @gws.ext.command.api('mapGetBox') 

86 def api_get_box(self, req: gws.WebRequester, p: GetBoxRequest) -> ImageResponse: 

87 """Get a part of the map inside a bounding box""" 

88 mime, content = self._get_box(req, p) 

89 return ImageResponse(mime=mime, content=content) 

90 

91 @gws.ext.command.get('mapGetBox') 

92 def http_get_box(self, req: gws.WebRequester, p: GetBoxRequest) -> gws.ContentResponse: 

93 mime, content = self._get_box(req, p) 

94 return gws.ContentResponse(mime=mime, content=content) 

95 

96 @gws.ext.command.api('mapGetXYZ') 

97 def api_get_xyz(self, req: gws.WebRequester, p: GetXyzRequest) -> ImageResponse: 

98 """Get an XYZ tile""" 

99 mime, content = self._get_xyz(req, p) 

100 return ImageResponse(mime=mime, content=content) 

101 

102 @gws.ext.command.get('mapGetXYZ') 

103 def http_get_xyz(self, req: gws.WebRequester, p: GetXyzRequest) -> gws.ContentResponse: 

104 mime, content = self._get_xyz(req, p) 

105 return gws.ContentResponse(mime=mime, content=content) 

106 

107 @gws.ext.command.api('mapGetLegend') 

108 def api_get_legend(self, req: gws.WebRequester, p: GetLegendRequest) -> ImageResponse: 

109 """Get a legend for a layer""" 

110 mime, content = self._get_legend(req, p) 

111 return ImageResponse(mime=mime, content=content) 

112 

113 @gws.ext.command.get('mapGetLegend') 

114 def http_get_legend(self, req: gws.WebRequester, p: GetLegendRequest) -> gws.ContentResponse: 

115 mime, content = self._get_legend(req, p) 

116 return gws.ContentResponse(mime=mime, content=content) 

117 

118 @gws.ext.command.api('mapDescribeLayer') 

119 def describe_layer(self, req: gws.WebRequester, p: DescribeLayerRequest) -> DescribeLayerResponse: 

120 project = req.user.require_project(p.projectUid) 

121 layer = req.user.require_layer(p.layerUid) 

122 tpl = self.root.app.templateMgr.find_template('layer.description', where=[layer, project], user=req.user) 

123 

124 if not tpl: 

125 return DescribeLayerResponse(content='') 

126 

127 res = tpl.render(gws.TemplateRenderInput(args={'layer': layer}, locale=gws.lib.intl.locale(p.localeUid, project.localeUids), user=req.user)) 

128 

129 return DescribeLayerResponse(content=res.content) 

130 

131 @gws.ext.command.api('mapGetFeatures') 

132 def api_get_features(self, req: gws.WebRequester, p: GetFeaturesRequest) -> GetFeaturesResponse: 

133 """Get a list of features in a bounding box""" 

134 

135 propses = self._get_features(req, p) 

136 return GetFeaturesResponse(features=propses) 

137 

138 @gws.ext.command.get('mapGetFeatures') 

139 def http_get_features(self, req: gws.WebRequester, p: GetFeaturesRequest) -> gws.ContentResponse: 

140 # @TODO the response should be geojson FeatureCollection 

141 

142 propses = self._get_features(req, p) 

143 js = gws.lib.jsonx.to_string({'features': propses}) 

144 

145 return gws.ContentResponse(mime=gws.lib.mime.JSON, content=js) 

146 

147 ## 

148 

149 def _get_box(self, req: gws.WebRequester, p: GetBoxRequest): 

150 ok = ( 

151 0 < p.width <= _PIXEL_SIZE_LIMIT and 

152 0 < p.height <= _PIXEL_SIZE_LIMIT and 

153 p.width * p.height <= _PIXEL_SIZE_LIMIT * _PIXEL_SIZE_LIMIT 

154 ) # fmt: skip 

155 if not ok: 

156 raise gws.BadRequestError(f'invalid image size') 

157 

158 layer = req.user.require_layer(p.layerUid) 

159 lri = gws.LayerRenderInput(type=gws.LayerRenderInputType.box, user=req.user, extraParams={}) 

160 

161 if p.compositeLayerUids: 

162 lri.extraParams['compositeLayerUids'] = p.compositeLayerUids 

163 

164 lri.view = gws.gis.render.map_view_from_bbox( 

165 crs=gws.lib.crs.get(p.crs) or layer.mapCrs, 

166 bbox=p.bbox, 

167 size=(p.width, p.height, gws.Uom.px), 

168 dpi=gws.lib.uom.OGC_SCREEN_PPI, 

169 rotation=0, 

170 ) 

171 

172 try: 

173 lro = layer.render(lri) 

174 if lro and lro.content: 

175 return gws.lib.mime.PNG, lro.content 

176 except Exception: 

177 gws.log.exception() 

178 

179 return self._empty_pixel 

180 

181 def _get_xyz(self, req: gws.WebRequester, p: GetXyzRequest): 

182 layer = req.user.require_layer(p.layerUid) 

183 lri = gws.LayerRenderInput(type=gws.LayerRenderInputType.xyz, user=req.user, x=p.x, y=p.y, z=p.z) 

184 lro = None 

185 

186 gws.debug.time_start(f'RENDER_XYZ layer={p.layerUid} lri={lri!r}') 

187 try: 

188 lro = layer.render(lri) 

189 except: 

190 gws.log.exception() 

191 gws.debug.time_end() 

192 

193 if not lro: 

194 return self._empty_pixel 

195 

196 content = lro.content 

197 

198 # for public tiled layers, write tiles to the web cache 

199 # so they will be subsequently served directly by nginx 

200 

201 # if content and gws.u.is_public_object(layer) and layer.has_cache: 

202 # path = layer.url_path('tile') 

203 # path = path.replace('{x}', str(p.x)) 

204 # path = path.replace('{y}', str(p.y)) 

205 # path = path.replace('{z}', str(p.z)) 

206 # gws.gis.cache.store_in_web_cache(path, content) 

207 

208 return gws.lib.mime.PNG, content 

209 

210 def _get_legend(self, req: gws.WebRequester, p: GetLegendRequest): 

211 layer = req.user.require_layer(p.layerUid) 

212 lro = layer.render_legend() 

213 content = gws.base.legend.output_to_bytes(lro) 

214 if content: 

215 return lro.mime, content 

216 return self._empty_pixel 

217 

218 def _image_response(self, lro: gws.LayerRenderOutput) -> ImageResponse: 

219 # @TODO content-dependent mime type 

220 # @TODO in-image errors 

221 if lro and lro.content: 

222 return ImageResponse(mime='image/png', content=lro.content) 

223 return ImageResponse(mime='image/png', content=gws.lib.image.empty_pixel()) 

224 

225 def _get_features(self, req: gws.WebRequester, p: GetFeaturesRequest) -> list[gws.FeatureProps]: 

226 layer = req.user.require_layer(p.layerUid) 

227 project = layer.find_closest(gws.ext.object.project) 

228 

229 crs = gws.lib.crs.get(p.crs) or layer.mapCrs 

230 

231 bounds = layer.bounds 

232 if p.bbox: 

233 bounds = gws.lib.bounds.from_extent(p.bbox, crs) 

234 

235 search = gws.SearchQuery( 

236 bounds=bounds, 

237 project=project, 

238 layers=[layer], 

239 limit=_GET_FEATURES_LIMIT, 

240 ) 

241 

242 features = layer.find_features(search, req.user) 

243 if not features: 

244 return [] 

245 

246 tpl = self.root.app.templateMgr.find_template(f'feature.label', where=[layer, project], user=req.user) 

247 if tpl: 

248 for feature in features: 

249 feature.render_views([tpl], project=project, layer=self, user=req.user) 

250 

251 mc = gws.ModelContext( 

252 op=gws.ModelOperation.read, 

253 target=gws.ModelReadTarget.map, 

254 user=req.user, 

255 ) 

256 

257 return [f.model.feature_to_view_props(f, mc) for f in features]