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

1"""The ``qgis`` layer.""" 

2 

3from typing import Optional, cast 

4 

5import gws 

6import gws.base.layer 

7import gws.config.util 

8 

9from . import grabber, provider, flatlayer 

10 

11 

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.""" 

15 

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)""" 

26 

27 

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. 

31 

32 The child layers are created from the project layer tree. With 

33 ``withCompositeRender``, the layer renders its visible children as one image. 

34 """ 

35 

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.""" 

42 

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 

53 

54 def post_configure(self): 

55 if self.withCompositeRender: 

56 gws.base.layer.image.Object.post_configure_grabbers(self) 

57 

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] 

68 

69 def create_grabber(self, opts): 

70 return grabber.Object(opts, provider=self.provider, params={}) 

71 

72 def configure_provider(self): 

73 return gws.config.util.configure_provider_for(self, provider.Object) 

74 

75 def configure_group(self): 

76 if super().configure_group(): 

77 return True 

78 

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 

88 

89 gws.base.layer.tree.configure_group_layers_for( 

90 self, 

91 self.provider.sourceLayers, 

92 create_leaf_layer_config, 

93 ) 

94 return True 

95 

96 def configure_metadata(self): 

97 if super().configure_metadata(): 

98 return True 

99 self.metadata = self.provider.metadata 

100 return True 

101 

102 def props(self, user): 

103 p = super().props(user) 

104 if not self.withCompositeRender: 

105 return p 

106 

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 

116 

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 

127 

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) 

135 

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) 

143 

144 def composite_render_params(self, lri: gws.LayerRenderInput) -> Optional[dict]: 

145 """Build the GetMap parameters for a composite render. 

146 

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. 

150 

151 Args: 

152 lri: Render input. 

153 

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 

160 

161 layers = [] 

162 filters = [] 

163 

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) 

181 

182 if not layers: 

183 gws.log.debug(f'no layers') 

184 return 

185 

186 params = {} 

187 params['LAYERS'] = list(reversed(layers)) 

188 if filters: 

189 params['FILTER'] = ';'.join(filters) 

190 

191 return params