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

156 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-17 01:37 +0200

1"""Map related commands.""" 

2 

3from typing import Optional 

4 

5import gws 

6import gws.base.action 

7import gws.base.feature 

8import gws.base.layer 

9import gws.base.legend 

10import gws.base.model 

11import gws.base.template 

12import gws.gis.bounds 

13import gws.gis.cache 

14import gws.gis.crs 

15import gws.gis.render 

16import gws.lib.image 

17import gws.lib.intl 

18import gws.lib.jsonx 

19import gws.lib.mime 

20import gws.lib.uom 

21 

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

23 

24 

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

26 pass 

27 

28 

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

30 pass 

31 

32 

33class GetBoxRequest(gws.Request): 

34 bbox: gws.Extent 

35 width: int 

36 height: int 

37 layerUid: str 

38 crs: Optional[gws.CrsName] 

39 dpi: Optional[int] 

40 layers: Optional[list[str]] 

41 

42 

43class GetXyzRequest(gws.Request): 

44 layerUid: str 

45 x: int 

46 y: int 

47 z: int 

48 

49 

50class GetLegendRequest(gws.Request): 

51 layerUid: str 

52 

53 

54class ImageResponse(gws.Response): 

55 content: bytes 

56 mime: str 

57 

58 

59class DescribeLayerRequest(gws.Request): 

60 layerUid: str 

61 

62 

63class DescribeLayerResponse(gws.Request): 

64 content: str 

65 

66 

67class GetFeaturesRequest(gws.Request): 

68 bbox: Optional[gws.Extent] 

69 layerUid: str 

70 modelUid: Optional[str] 

71 crs: Optional[gws.CrsName] 

72 resolution: Optional[float] 

73 limit: int = 0 

74 views: Optional[list[str]] 

75 

76 

77class GetFeaturesResponse(gws.Response): 

78 features: list[gws.FeatureProps] 

79 

80 

81_GET_FEATURES_LIMIT = 10000 

82 

83 

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

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

86 

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

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

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

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

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

92 

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

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

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

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

97 

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

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

100 """Get an XYZ tile""" 

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

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

103 

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

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

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

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

108 

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

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

111 """Get a legend for a layer""" 

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

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

114 

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

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

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

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

119 

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

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

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

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

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

125 

126 if not tpl: 

127 return DescribeLayerResponse(content='') 

128 

129 res = tpl.render(gws.TemplateRenderInput( 

130 args={'layer': layer}, 

131 locale=gws.lib.intl.locale(p.localeUid, project.localeUids), 

132 user=req.user)) 

133 

134 return DescribeLayerResponse(content=res.content) 

135 

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

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

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

139 

140 propses = self._get_features(req, p) 

141 return GetFeaturesResponse(features=propses) 

142 

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

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

145 # @TODO the response should be geojson FeatureCollection 

146 

147 propses = self._get_features(req, p) 

148 js = gws.lib.jsonx.to_string({ 

149 'features': propses 

150 }) 

151 

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

153 

154 ## 

155 

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

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

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

159 

160 if p.layers: 

161 lri.extraParams['layers'] = p.layers 

162 

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

164 crs=gws.gis.crs.get(p.crs) or layer.mapCrs, 

165 bbox=p.bbox, 

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

167 dpi=gws.lib.uom.OGC_SCREEN_PPI, 

168 rotation=0 

169 ) 

170 

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

172 try: 

173 lro = layer.render(lri) 

174 if lro and lro.content: 

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

176 except: 

177 gws.log.exception() 

178 gws.debug.time_end() 

179 

180 return self._empty_pixel 

181 

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

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

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

185 lro = None 

186 

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

188 try: 

189 lro = layer.render(lri) 

190 except: 

191 gws.log.exception() 

192 gws.debug.time_end() 

193 

194 if not lro: 

195 return self._empty_pixel 

196 

197 content = lro.content 

198 

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

200 # so they will be subsequently served directly by nginx 

201 

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

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

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

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

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

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

208 

209 return gws.lib.mime.PNG, content 

210 

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

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

213 lro = layer.render_legend() 

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

215 if content: 

216 return lro.mime, content 

217 return self._empty_pixel 

218 

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

220 # @TODO content-dependent mime type 

221 # @TODO in-image errors 

222 if lro and lro.content: 

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

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

225 

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

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

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

229 

230 crs = gws.gis.crs.get(p.crs) or layer.mapCrs 

231 

232 bounds = layer.bounds 

233 if p.bbox: 

234 bounds = gws.gis.bounds.from_extent(p.bbox, crs) 

235 

236 search = gws.SearchQuery( 

237 bounds=bounds, 

238 project=project, 

239 layers=[layer], 

240 limit=_GET_FEATURES_LIMIT 

241 ) 

242 

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

244 if not features: 

245 return [] 

246 

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

248 if tpl: 

249 for feature in features: 

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

251 

252 mc = gws.ModelContext( 

253 op=gws.ModelOperation.read, 

254 target=gws.ModelReadTarget.map, 

255 user=req.user 

256 ) 

257 

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