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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
1"""Map related commands."""
3from typing import Optional
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
17gws.ext.new.action('map')
20class Config(gws.base.action.Config):
21 """Configuration for the map action."""
23 pass
26class Props(gws.base.action.Props):
27 pass
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]]
40class GetXyzRequest(gws.Request):
41 layerUid: str
42 x: int
43 y: int
44 z: int
47class GetLegendRequest(gws.Request):
48 layerUid: str
51class ImageResponse(gws.Response):
52 content: bytes
53 mime: str
56class DescribeLayerRequest(gws.Request):
57 layerUid: str
60class DescribeLayerResponse(gws.Request):
61 content: str
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]]
74class GetFeaturesResponse(gws.Response):
75 features: list[gws.FeatureProps]
78_GET_FEATURES_LIMIT = 10000
79_PIXEL_SIZE_LIMIT = 4096
82class Object(gws.base.action.Object):
83 _empty_pixel = gws.lib.mime.PNG, gws.lib.image.empty_pixel()
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)
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)
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)
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)
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)
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)
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)
124 if not tpl:
125 return DescribeLayerResponse(content='')
127 res = tpl.render(gws.TemplateRenderInput(args={'layer': layer}, locale=gws.lib.intl.locale(p.localeUid, project.localeUids), user=req.user))
129 return DescribeLayerResponse(content=res.content)
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"""
135 propses = self._get_features(req, p)
136 return GetFeaturesResponse(features=propses)
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
142 propses = self._get_features(req, p)
143 js = gws.lib.jsonx.to_string({'features': propses})
145 return gws.ContentResponse(mime=gws.lib.mime.JSON, content=js)
147 ##
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')
158 layer = req.user.require_layer(p.layerUid)
159 lri = gws.LayerRenderInput(type=gws.LayerRenderInputType.box, user=req.user, extraParams={})
161 if p.compositeLayerUids:
162 lri.extraParams['compositeLayerUids'] = p.compositeLayerUids
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 )
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()
179 return self._empty_pixel
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
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()
193 if not lro:
194 return self._empty_pixel
196 content = lro.content
198 # for public tiled layers, write tiles to the web cache
199 # so they will be subsequently served directly by nginx
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)
208 return gws.lib.mime.PNG, content
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
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())
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)
229 crs = gws.lib.crs.get(p.crs) or layer.mapCrs
231 bounds = layer.bounds
232 if p.bbox:
233 bounds = gws.lib.bounds.from_extent(p.bbox, crs)
235 search = gws.SearchQuery(
236 bounds=bounds,
237 project=project,
238 layers=[layer],
239 limit=_GET_FEATURES_LIMIT,
240 )
242 features = layer.find_features(search, req.user)
243 if not features:
244 return []
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)
251 mc = gws.ModelContext(
252 op=gws.ModelOperation.read,
253 target=gws.ModelReadTarget.map,
254 user=req.user,
255 )
257 return [f.model.feature_to_view_props(f, mc) for f in features]