Coverage for gws-app/gws/base/layer/core.py: 0%
288 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"""Base layer object."""
3from typing import Optional, cast
5import gws
6import gws.base.model
7import gws.config.util
8import gws.gis.bounds
9import gws.gis.crs
10import gws.gis.extent
11import gws.gis.source
12import gws.gis.zoom
13import gws.lib.metadata
14import gws.lib.xmlx
16from . import ows
18DEFAULT_TILE_SIZE = 256
21class CacheConfig(gws.Config):
22 """Cache configuration"""
24 maxAge: gws.Duration = '7d'
25 """cache max. age"""
26 maxLevel: int = 1
27 """max. zoom level to cache"""
28 requestBuffer: Optional[int]
29 requestTiles: Optional[int]
32class GridConfig(gws.Config):
33 """Grid configuration for caches and tiled data"""
35 crs: Optional[gws.CrsName]
36 extent: Optional[gws.Extent]
37 origin: Optional[gws.Origin]
38 resolutions: Optional[list[float]]
39 tileSize: Optional[int]
42class AutoLayersOptions(gws.ConfigWithAccess):
43 """Configuration for automatic layers."""
45 applyTo: Optional[gws.gis.source.LayerFilter]
46 config: dict
49class ClientOptions(gws.Data):
50 """Client options for a layer"""
52 expanded: bool = False
53 """the layer is expanded in the list view"""
54 unlisted: bool = False
55 """the layer is hidden in the list view"""
56 selected: bool = False
57 """the layer is initially selected"""
58 hidden: bool = False
59 """the layer is initially hidden"""
60 unfolded: bool = False
61 """the layer is not listed, but its children are"""
62 exclusive: bool = False
63 """only one of this layer's children is visible at a time"""
66class GridProps(gws.Props):
67 origin: str
68 extent: gws.Extent
69 resolutions: list[float]
70 tileSize: int
73class Config(gws.ConfigWithAccess):
74 """Layer configuration"""
76 cache: Optional[CacheConfig]
77 """Cache configuration."""
78 clientOptions: ClientOptions = {}
79 """Options for the layer display in the client."""
80 cssSelector: str = ''
81 """Css selector for feature layers."""
82 display: gws.LayerDisplayMode = gws.LayerDisplayMode.box
83 """Layer display mode."""
84 extent: Optional[gws.Extent]
85 """Layer extent."""
86 zoomExtent: Optional[gws.Extent]
87 """Layer zoom extent. (added in 8.1)"""
88 extentBuffer: Optional[int]
89 """Extent buffer."""
90 finders: Optional[list[gws.ext.config.finder]]
91 """Search providers."""
92 grid: Optional[GridConfig]
93 """Client grid."""
94 imageFormat: gws.ImageFormat = gws.ImageFormat.png8
95 """Image format."""
96 legend: Optional[gws.ext.config.legend]
97 """Legend configuration."""
98 loadingStrategy: gws.FeatureLoadingStrategy = gws.FeatureLoadingStrategy.all
99 """Feature loading strategy."""
100 metadata: Optional[gws.Metadata]
101 """Layer metadata."""
102 models: Optional[list[gws.ext.config.model]]
103 """Data models."""
104 opacity: float = 1
105 """Layer opacity."""
106 ows: Optional[ows.Config]
107 """Configuration for OWS services."""
108 templates: Optional[list[gws.ext.config.template]]
109 """Layer templates."""
110 title: str = ''
111 """Layer title."""
112 zoom: Optional[gws.gis.zoom.Config]
113 """Layer resolutions and scales."""
114 withSearch: Optional[bool] = True
115 """Layer is searchable."""
116 withLegend: Optional[bool] = True
117 """Layer has a legend."""
118 withCache: Optional[bool] = False
119 """Layer is cached."""
120 withOws: Optional[bool] = True
121 """Layer is enabled for OWS services."""
124class Props(gws.Props):
125 clientOptions: gws.LayerClientOptions
126 cssSelector: str
127 displayMode: str
128 extent: Optional[gws.Extent]
129 zoomExtent: Optional[gws.Extent]
130 geometryType: Optional[gws.GeometryType]
131 grid: GridProps
132 layers: Optional[list['Props']]
133 loadingStrategy: gws.FeatureLoadingStrategy
134 metadata: gws.lib.metadata.Props
135 model: Optional[gws.base.model.Props]
136 opacity: Optional[float]
137 resolutions: Optional[list[float]]
138 title: str = ''
139 type: str
140 uid: str
141 url: str = ''
144class Object(gws.Layer):
145 parent: gws.Layer
147 clientOptions: gws.LayerClientOptions
148 cssSelector: str
150 canRenderBox = False
151 canRenderSvg = False
152 canRenderXyz = False
154 isEnabledForOws = False
155 isGroup = False
156 isSearchable = False
158 hasLegend = False
160 parentBounds: gws.Bounds
161 parentResolutions: list[float]
163 def configure(self):
164 self.clientOptions = self.cfg('clientOptions') or gws.Data()
165 self.cssSelector = self.cfg('cssSelector')
166 self.displayMode = self.cfg('display')
167 self.loadingStrategy = self.cfg('loadingStrategy')
168 self.imageFormat = self.cfg('imageFormat')
169 self.opacity = self.cfg('opacity')
170 self.title = self.cfg('title')
172 self.parentBounds = self.cfg('_parentBounds')
173 self.parentResolutions = self.cfg('_parentResolutions')
174 self.mapCrs = self.parentBounds.crs
176 self.bounds = self.parentBounds
177 self.zoomBounds = cast(gws.Bounds, None)
178 self.resolutions = self.parentResolutions
180 self.templates = []
181 self.models = []
182 self.finders = []
184 self.metadata = gws.Metadata()
185 self.legend = None
186 self.legendUrl = ''
188 self.layers = []
190 self.grid = None
191 self.cache = None
192 self.ows = gws.LayerOws()
194 setattr(self, 'provider', None)
195 self.sourceLayers = []
197 def configure_layer(self):
198 """Layer configuration protocol."""
199 self.configure_provider()
200 self.configure_sources()
201 self.configure_models()
202 self.configure_bounds()
203 self.configure_zoom_bounds()
204 self.configure_resolutions()
205 self.configure_grid()
206 self.configure_legend()
207 self.configure_cache()
208 self.configure_metadata()
209 self.configure_templates()
210 self.configure_search()
211 self.configure_ows()
213 ##
215 def configure_bounds(self):
216 p = self.cfg('extent')
217 if p:
218 self.bounds = gws.Bounds(
219 crs=self.mapCrs,
220 extent=gws.gis.extent.from_list(p))
221 return True
223 def configure_zoom_bounds(self):
224 p = self.cfg('zoomExtent')
225 if p:
226 self.zoomBounds = gws.Bounds(
227 crs=self.mapCrs,
228 extent=gws.gis.extent.from_list(p))
229 return True
231 def configure_cache(self):
232 if not self.cfg('withCache'):
233 return True
234 self.cache = gws.LayerCache(self.cfg('cache'))
235 return True
237 def configure_grid(self):
238 p = self.cfg('grid')
239 if p:
240 if p.crs and p.crs != self.bounds.crs:
241 raise gws.Error(f'layer {self!r}: invalid target grid crs')
242 self.grid = gws.TileGrid(
243 origin=p.origin or gws.Origin.nw,
244 tileSize=p.tileSize or DEFAULT_TILE_SIZE,
245 bounds=gws.Bounds(crs=self.bounds.crs, extent=p.extent),
246 resolutions=p.resolutions)
247 return True
249 def configure_legend(self):
250 if not self.cfg('withLegend'):
251 return True
252 p = self.cfg('legend')
253 if p:
254 self.legend = self.create_child(gws.ext.object.legend, p)
255 return True
257 def configure_metadata(self):
258 p = self.cfg('metadata')
259 if p:
260 self.metadata = gws.lib.metadata.from_config(p)
261 return True
263 def configure_models(self):
264 return gws.config.util.configure_models_for(self)
266 def configure_provider(self):
267 pass
269 def configure_resolutions(self):
270 p = self.cfg('zoom')
271 if p:
272 self.resolutions = gws.gis.zoom.resolutions_from_config(p, self.cfg('_parentResolutions'))
273 if not self.resolutions:
274 raise gws.Error(f'layer {self!r}: no resolutions, config={p!r} parent={self.parentResolutions!r}')
275 return True
277 def configure_search(self):
278 if not self.cfg('withSearch'):
279 return True
280 return gws.config.util.configure_finders_for(self)
282 def configure_sources(self):
283 pass
285 def configure_templates(self):
286 return gws.config.util.configure_templates_for(self)
288 def configure_group_layers(self, layer_configs):
289 ls = []
291 for cfg in layer_configs:
292 cfg = gws.u.merge(
293 cfg,
294 _parentBounds=self.bounds,
295 _parentResolutions=self.resolutions,
296 )
297 ls.append(self.create_child(gws.ext.object.layer, cfg))
299 self.layers = gws.u.compact(ls)
301 def configure_ows(self):
302 self.isEnabledForOws = self.cfg('withOws', default=True)
303 self.ows = self.create_child(ows.Object, self.cfg('ows'), _defaultName=gws.u.to_uid(self.title))
305 ##
307 def post_configure(self):
308 self.isSearchable = bool(self.finders)
309 self.hasLegend = bool(self.legend)
311 if self.bounds.crs != self.mapCrs:
312 raise gws.Error(f'layer {self!r}: invalid CRS {self.bounds.crs}')
314 if not gws.gis.bounds.intersect(self.bounds, self.parentBounds):
315 gws.log.warning(f'layer {self!r}: bounds outside of the parent bounds b={self.bounds.extent} parent={self.parentBounds.extent}')
316 self.bounds = gws.gis.bounds.copy(self.parentBounds)
318 self.wgsExtent = gws.gis.bounds.transform(self.bounds, gws.gis.crs.WGS84).extent
319 self.zoomBounds = self.zoomBounds or self.bounds
321 if self.legend:
322 self.legendUrl = self.url_path('legend')
324 ##
326 # @TODO use Node.find_ancestors
328 def ancestors(self):
329 ls = []
330 p = self.parent
331 while isinstance(p, Object):
332 ls.append(p)
333 p = p.parent
334 return ls
336 def descendants(self):
337 ls = []
338 for la in self.layers:
339 ls.append(la)
340 ls.extend(la.descendants())
341 return ls
343 _url_path_suffix = '/gws.png'
345 def url_path(self, kind):
346 # layer urls, handled by the map action (base/map/action.py)
347 if kind == 'box':
348 return gws.u.action_url_path('mapGetBox', layerUid=self.uid) + self._url_path_suffix
349 if kind == 'tile':
350 return gws.u.action_url_path('mapGetXYZ', layerUid=self.uid) + '/z/{z}/x/{x}/y/{y}' + self._url_path_suffix
351 if kind == 'legend':
352 return gws.u.action_url_path('mapGetLegend', layerUid=self.uid) + self._url_path_suffix
353 if kind == 'features':
354 return gws.u.action_url_path('mapGetFeatures', layerUid=self.uid)
356 def props(self, user):
357 p = Props(
358 clientOptions=self.clientOptions,
359 cssSelector=self.cssSelector,
360 displayMode=self.displayMode,
361 extent=self.bounds.extent,
362 zoomExtent=self.zoomBounds.extent,
363 layers=self.layers,
364 loadingStrategy=self.loadingStrategy,
365 metadata=gws.lib.metadata.props(self.metadata),
366 opacity=self.opacity,
367 resolutions=sorted(self.resolutions, reverse=True),
368 title=self.title,
369 uid=self.uid,
370 )
372 if self.grid:
373 p.grid = GridProps(
374 origin=self.grid.origin,
375 extent=self.grid.bounds.extent,
376 resolutions=sorted(self.grid.resolutions, reverse=True),
377 tileSize=self.grid.tileSize,
378 )
380 if self.displayMode == gws.LayerDisplayMode.tile:
381 p.type = 'tile'
382 p.url = self.url_path('tile')
384 if self.displayMode == gws.LayerDisplayMode.box:
385 p.type = 'box'
386 p.url = self.url_path('box')
388 return p
390 def render(self, lri):
391 pass
393 def find_features(self, search, user):
394 return []
396 def render_legend(self, args=None) -> Optional[gws.LegendRenderOutput]:
398 if not self.legend:
399 return None
401 def _get():
402 out = self.legend.render()
403 return out
405 if not args:
406 return gws.u.get_server_global('legend_' + self.uid, _get)
408 return self.legend.render(args)
410 def mapproxy_config(self, mc):
411 pass