Coverage for gws-app/gws/plugin/qgis/layer.py: 42%
120 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
1"""The ``qgis`` layer."""
3from typing import Optional, cast
5import gws
6import gws.base.layer
7import gws.config.util
9from . import grabber, provider, flatlayer
12@gws.ext.config.layer('qgis')
13class Config(gws.base.layer.Config, gws.base.layer.tree.Config):
14 """Layer group that shows a QGIS project as a tree of its layers."""
16 provider: Optional[provider.Config]
17 """QGIS project the layers are loaded from."""
18 withCompositeRender: bool = False
19 """Render all sublayers as one image instead of one image per layer. (added in 8.5)"""
20 compositeRender: Optional[bool]
21 """Use ``withCompositeRender``. (deprecated in 8.5)"""
22 sqlFilters: Optional[dict]
23 """SQL filters for source layers, passed to QGIS Server."""
24 withCache: Optional[bool] = False
25 """Store rendered tiles of this layer and its sublayers in the persistent cache. (changed in 8.5)"""
28@gws.ext.object.layer('qgis')
29class Object(gws.base.layer.group.Object):
30 """Group layer that shows a QGIS project as a tree of layers.
32 The child layers are created from the project layer tree. With
33 ``withCompositeRender``, the layer renders its visible children as one image.
34 """
36 provider: provider.Object
37 """QGIS provider."""
38 withCompositeRender: bool = False
39 """Render all visible sublayers as one image."""
40 sqlFilters: dict
41 """SQL filters passed to the child layers, mapping a source layer name (or ``*``) to a filter expression."""
43 def configure(self):
44 self.withCompositeRender = self.cfg('withCompositeRender', default=False)
45 p = self.cfg('compositeRender')
46 if p is not None:
47 self.root.config_warning('"compositeRender" is deprecated, use "withCompositeRender"')
48 self.withCompositeRender = self.withCompositeRender or p
49 self.sqlFilters = self.cfg('sqlFilters', default={})
50 if self.withCompositeRender:
51 self.canRenderBox = True
52 self.canRenderTile = True
54 def post_configure(self):
55 if self.withCompositeRender:
56 gws.base.layer.image.Object.post_configure_grabbers(self)
58 def create_cache_name(self, cache):
59 return gws.u.sha256(
60 [
61 self.provider.cache_hash(),
62 vars(self.imageFormat),
63 list(self.wgsExtent),
64 cache.requestBuffer,
65 cache.requestTiles,
66 ]
67 )[: gws.base.layer.image.CACHE_NAME_LENGTH]
69 def create_grabber(self, opts):
70 return grabber.Object(opts, provider=self.provider, params={})
72 def configure_provider(self):
73 return gws.config.util.configure_provider_for(self, provider.Object)
75 def configure_group(self):
76 if super().configure_group():
77 return True
79 def create_leaf_layer_config(source_layers):
80 # propagate the withCache and cache settings to the leaf layer configuration
81 cfg = self.provider.create_leaf_layer_config(source_layers)
82 if self.cfg('withCache'):
83 cfg['withCache'] = True
84 cache = self.cfg('cache')
85 if cache:
86 cfg['cache'] = gws.u.merge(gws.u.to_dict(cache), name='')
87 return cfg
89 gws.base.layer.tree.configure_group_layers_for(
90 self,
91 self.provider.sourceLayers,
92 create_leaf_layer_config,
93 )
94 return True
96 def configure_metadata(self):
97 if super().configure_metadata():
98 return True
99 self.metadata = self.provider.metadata
100 return True
102 def props(self, user):
103 p = super().props(user)
104 if not self.withCompositeRender:
105 return p
107 def _to_leaf(la):
108 pla = la.props(user)
109 if not pla:
110 return pla
111 if pla.get('type') == 'group':
112 pla['layers'] = [_to_leaf(la) for la in pla['layers']]
113 elif pla.get('type') in ('box', 'tile'):
114 pla['type'] = 'compositeLeaf'
115 return pla
117 p = gws.u.merge(
118 p,
119 type='compositeBox',
120 url=self.url_path_for('box'),
121 layers=[_to_leaf(la) for la in p['layers']],
122 )
123 if self.displayMode == gws.LayerDisplayMode.tile:
124 p.type = 'compositeTile'
125 p.url = self.url_path_for('tile').replace('/z/', '/compositeLayerUids/{c}/z/')
126 return p
128 def render_box(self, lri):
129 if not self.withCompositeRender:
130 return
131 lri.renderParams = self.composite_render_params(lri)
132 if not lri.renderParams:
133 return
134 return gws.base.layer.image.Object.render_box(self, lri)
136 def render_tile(self, lri):
137 if not self.withCompositeRender:
138 return
139 lri.renderParams = self.composite_render_params(lri)
140 if not lri.renderParams:
141 return
142 return gws.base.layer.image.Object.render_tile(self, lri)
144 def composite_render_params(self, lri: gws.LayerRenderInput) -> Optional[dict]:
145 """Build the GetMap parameters for a composite render.
147 The parameters combine ``LAYERS`` and ``FILTER`` of the ``qgisflat``
148 descendants listed in ``compositeLayerUids`` of the extra render
149 parameters. Layers the user cannot read are skipped.
151 Args:
152 lri: Render input.
154 Returns:
155 GetMap parameters, or ``None`` if no readable layers are requested.
156 """
157 leaves = dict(lri.extraParams or {}).get('compositeLayerUids', [])
158 if not leaves:
159 return
161 layers = []
162 filters = []
164 for la in self.find_descendants(gws.ext.object.layer):
165 if la.uid not in leaves:
166 continue
167 if not lri.user.can_read(la):
168 gws.log.debug(f'skipping {la.uid=} forbidden')
169 continue
170 if la.extType != 'qgisflat':
171 gws.log.debug(f'skipping {la.uid=} {la.extType=}')
172 continue
173 p = cast(flatlayer.Object, la).render_params(lri, self.sqlFilters)
174 if not p:
175 gws.log.debug(f'skipping {la.uid=} no params')
176 continue
177 layers.extend(reversed(p.get('LAYERS', [])))
178 f = p.get('FILTER')
179 if f:
180 filters.append(f)
182 if not layers:
183 gws.log.debug(f'no layers')
184 return
186 params = {}
187 params['LAYERS'] = list(reversed(layers))
188 if filters:
189 params['FILTER'] = ';'.join(filters)
191 return params