Coverage for gws-app/gws/plugin/ows_client/wmts/layer.py: 0%

119 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""WMTS layer.""" 

2 

3from typing import Optional 

4 

5import gws 

6import gws.base.layer 

7import gws.config.util 

8import gws.lib.bounds 

9import gws.lib.crs 

10import gws.lib.extent 

11import gws.lib.uom 

12import gws.gis.source 

13import gws.gis.zoom 

14 

15from . import grabber, provider 

16 

17 

18@gws.ext.config.layer('wmts') 

19class Config(gws.base.layer.Config): 

20 """Layer that shows tiles from a WMTS service.""" 

21 

22 provider: provider.Config 

23 """WMTS service the layer is loaded from.""" 

24 display: gws.LayerDisplayMode = gws.LayerDisplayMode.tile 

25 """The way the layer is rendered in the client.""" 

26 sourceLayers: Optional[gws.gis.source.LayerFilter] 

27 """Source layer to show.""" 

28 style: Optional[str] 

29 """Source style to request.""" 

30 

31 

32class TileMatrixSetProps(gws.Props): 

33 """Tile matrix set, as needed by the client to address source tiles directly.""" 

34 

35 origin: gws.Point 

36 """Top-left corner of the matrices.""" 

37 resolutions: list[float] 

38 """Resolutions, coarsest first.""" 

39 matrixIds: list[str] 

40 """Matrix identifiers, in the order of ``resolutions``.""" 

41 tileSize: gws.Size 

42 """Tile size in pixels.""" 

43 

44 

45@gws.ext.props.layer('wmts') 

46class Props(gws.base.layer.core.Props): 

47 tileMatrixSet: Optional[TileMatrixSetProps] 

48 """Tile matrix set for the client display mode.""" 

49 

50 

51@gws.ext.object.layer('wmts') 

52class Object(gws.base.layer.image.Object): 

53 """Image layer that shows the tiles of a WMTS source layer. 

54 

55 The tiles are fetched by the server through a WMTS grabber, or loaded by 

56 the client directly from the service in the ``client`` display mode. 

57 """ 

58 

59 provider: provider.Object 

60 """WMTS service provider.""" 

61 sourceLayers: list[gws.SourceLayer] 

62 """Source layers selected for this layer.""" 

63 

64 activeLayer: gws.SourceLayer 

65 """Source layer shown by this layer.""" 

66 activeStyle: gws.SourceStyle 

67 """Source style requested for the tiles.""" 

68 activeTms: gws.TileMatrixSet 

69 """Tile matrix set used for the tiles.""" 

70 

71 canRenderInClient = True 

72 

73 def configure(self): 

74 self.configure_layer() 

75 if self.displayMode == gws.LayerDisplayMode.client and self.activeTms.crs != self.mapCrs: 

76 raise gws.ConfigurationError(f'display mode "client" needs a tile matrix set in the map CRS') 

77 

78 def create_cache_name(self, cache): 

79 return gws.u.sha256( 

80 [ 

81 self.provider.cache_hash(), 

82 self.activeLayer.name, 

83 self.activeStyle.name, 

84 self.activeTms.identifier, 

85 vars(self.imageFormat), 

86 list(self.wgsExtent), 

87 cache.requestBuffer, 

88 cache.requestTiles, 

89 ] 

90 )[: gws.base.layer.image.CACHE_NAME_LENGTH] 

91 

92 def create_grabber(self, opts): 

93 return grabber.Object( 

94 opts, 

95 provider=self.provider, 

96 tms=self.activeTms, 

97 urlTemplate=self.provider.tile_url_template(self.activeLayer, self.activeTms, self.activeStyle), 

98 ) 

99 

100 def configure_provider(self): 

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

102 

103 def props(self, user): 

104 p = super().props(user) 

105 if self.displayMode != gws.LayerDisplayMode.client: 

106 return p 

107 tms = self.activeTms 

108 m0 = tms.matrices[0] 

109 return gws.u.merge( 

110 p, 

111 type='wmts', 

112 url=self.provider.tile_url_template(self.activeLayer, tms, self.activeStyle), 

113 tileMatrixSet=TileMatrixSetProps( 

114 origin=(m0.x, m0.y), 

115 resolutions=[tm.resolution for tm in tms.matrices], 

116 matrixIds=[tm.identifier for tm in tms.matrices], 

117 tileSize=(int(m0.tileWidth), int(m0.tileHeight)), 

118 ), 

119 ) 

120 

121 def configure_sources(self): 

122 if super().configure_sources(): 

123 return True 

124 

125 gws.u.require(self.provider, 'failed to configure service provider') 

126 

127 self.configure_source_layers() 

128 if not self.sourceLayers: 

129 raise gws.Error('no source layers found') 

130 

131 self.activeLayer = self.sourceLayers[0] 

132 self.configure_tms() 

133 self.configure_style() 

134 

135 def configure_source_layers(self): 

136 """Select the source layers from the image layers of the provider. 

137 

138 Returns: 

139 Always ``True``. 

140 """ 

141 return gws.config.util.configure_source_layers_for(self, self.provider.sourceLayers, is_image=True) 

142 

143 def configure_tms(self): 

144 """Select the tile matrix set of the active source layer. 

145 

146 The tile matrix set must be in the forced CRS of the provider or, if 

147 none is forced, in the CRS that best matches the map CRS. 

148 

149 Raises: 

150 ``gws.Error``: If there is no tile matrix set in that CRS. 

151 """ 

152 crs = self.provider.forceCrs 

153 if not crs: 

154 crs = gws.lib.crs.best_match(self.mapCrs, [tms.crs for tms in self.activeLayer.tileMatrixSets]) 

155 tms_list = [tms for tms in self.activeLayer.tileMatrixSets if tms.crs == crs] 

156 if not tms_list: 

157 raise gws.Error(f'no TMS for {crs} in {self.provider.url}') 

158 self.activeTms = tms_list[0] 

159 

160 def configure_style(self): 

161 """Select the style of the active source layer. 

162 

163 The style is the one named by the ``styleName`` configuration value, else 

164 the default style of the source layer, else a style named ``default``. 

165 

166 Returns: 

167 Always ``True``. 

168 

169 Raises: 

170 ``gws.Error``: If the configured style does not exist. 

171 """ 

172 p = self.cfg('styleName') 

173 if p: 

174 for style in self.activeLayer.styles: 

175 if style.name == p: 

176 self.activeStyle = style 

177 return True 

178 raise gws.Error(f'style {p!r} not found') 

179 

180 for style in self.activeLayer.styles: 

181 if style.isDefault: 

182 self.activeStyle = style 

183 return True 

184 

185 self.activeStyle = gws.SourceStyle(name='default') 

186 return True 

187 

188 def configure_extent(self): 

189 if super().configure_extent(): 

190 return True 

191 tms_crs = self.activeTms.crs 

192 ext = gws.lib.extent.transform_to_wgs(self.activeTms.matrices[0].extent, tms_crs) 

193 if gws.lib.extent.is_valid_wgs(ext): 

194 self.wgsExtent = tms_crs.clip_wgs_extent(ext) or tms_crs.wgsMaxExtent 

195 else: 

196 self.wgsExtent = tms_crs.wgsMaxExtent 

197 

198 layer_ext = gws.gis.source.combined_wgs_extent([self.activeLayer]) 

199 if layer_ext: 

200 ext = gws.lib.extent.intersection(self.wgsExtent, layer_ext) 

201 if ext and gws.lib.extent.is_valid_wgs(ext): 

202 self.wgsExtent = ext 

203 

204 return True 

205 

206 def configure_resolutions(self): 

207 if super().configure_resolutions(): 

208 return True 

209 scales = [m.scale for m in self.activeTms.matrices] 

210 self.resolutions = gws.gis.zoom.resolutions_from_scale_range( 

211 min(scales), 

212 max(scales), 

213 self.cfg('_parentResolutions'), 

214 self.mapCrs, 

215 ) 

216 if self.resolutions: 

217 return True 

218 raise gws.Error(f'layer {self!r}: no matching resolutions') 

219 

220 def configure_legend(self): 

221 if super().configure_legend(): 

222 return True 

223 url = self.activeStyle.legendUrl 

224 if url: 

225 self.legend = self.create_child(gws.ext.object.legend, type='remote', urls=[url]) 

226 return True 

227 

228 def configure_metadata(self): 

229 if super().configure_metadata(): 

230 return True 

231 self.metadata = self.provider.metadata 

232 return True 

233 

234 ##