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
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1"""Map related commands."""
3from typing import Optional
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
22gws.ext.new.action('map')
25class Config(gws.base.action.Config):
26 pass
29class Props(gws.base.action.Props):
30 pass
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]]
43class GetXyzRequest(gws.Request):
44 layerUid: str
45 x: int
46 y: int
47 z: int
50class GetLegendRequest(gws.Request):
51 layerUid: str
54class ImageResponse(gws.Response):
55 content: bytes
56 mime: str
59class DescribeLayerRequest(gws.Request):
60 layerUid: str
63class DescribeLayerResponse(gws.Request):
64 content: str
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]]
77class GetFeaturesResponse(gws.Response):
78 features: list[gws.FeatureProps]
81_GET_FEATURES_LIMIT = 10000
84class Object(gws.base.action.Object):
85 _empty_pixel = gws.lib.mime.PNG, gws.lib.image.empty_pixel()
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)
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)
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)
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)
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)
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)
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)
126 if not tpl:
127 return DescribeLayerResponse(content='')
129 res = tpl.render(gws.TemplateRenderInput(
130 args={'layer': layer},
131 locale=gws.lib.intl.locale(p.localeUid, project.localeUids),
132 user=req.user))
134 return DescribeLayerResponse(content=res.content)
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"""
140 propses = self._get_features(req, p)
141 return GetFeaturesResponse(features=propses)
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
147 propses = self._get_features(req, p)
148 js = gws.lib.jsonx.to_string({
149 'features': propses
150 })
152 return gws.ContentResponse(mime=gws.lib.mime.JSON, content=js)
154 ##
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={})
160 if p.layers:
161 lri.extraParams['layers'] = p.layers
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 )
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()
180 return self._empty_pixel
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
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()
194 if not lro:
195 return self._empty_pixel
197 content = lro.content
199 # for public tiled layers, write tiles to the web cache
200 # so they will be subsequently served directly by nginx
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)
209 return gws.lib.mime.PNG, content
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
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())
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)
230 crs = gws.gis.crs.get(p.crs) or layer.mapCrs
232 bounds = layer.bounds
233 if p.bbox:
234 bounds = gws.gis.bounds.from_extent(p.bbox, crs)
236 search = gws.SearchQuery(
237 bounds=bounds,
238 project=project,
239 layers=[layer],
240 limit=_GET_FEATURES_LIMIT
241 )
243 features = layer.find_features(search, req.user)
244 if not features:
245 return []
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)
252 mc = gws.ModelContext(
253 op=gws.ModelOperation.read,
254 target=gws.ModelReadTarget.map,
255 user=req.user
256 )
258 return [f.model.feature_to_view_props(f, mc) for f in features]