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
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""WMTS layer."""
3from typing import Optional
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
15from . import grabber, provider
18@gws.ext.config.layer('wmts')
19class Config(gws.base.layer.Config):
20 """Layer that shows tiles from a WMTS service."""
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."""
32class TileMatrixSetProps(gws.Props):
33 """Tile matrix set, as needed by the client to address source tiles directly."""
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."""
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."""
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.
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 """
59 provider: provider.Object
60 """WMTS service provider."""
61 sourceLayers: list[gws.SourceLayer]
62 """Source layers selected for this layer."""
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."""
71 canRenderInClient = True
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')
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]
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 )
100 def configure_provider(self):
101 return gws.config.util.configure_provider_for(self, provider.Object)
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 )
121 def configure_sources(self):
122 if super().configure_sources():
123 return True
125 gws.u.require(self.provider, 'failed to configure service provider')
127 self.configure_source_layers()
128 if not self.sourceLayers:
129 raise gws.Error('no source layers found')
131 self.activeLayer = self.sourceLayers[0]
132 self.configure_tms()
133 self.configure_style()
135 def configure_source_layers(self):
136 """Select the source layers from the image layers of the provider.
138 Returns:
139 Always ``True``.
140 """
141 return gws.config.util.configure_source_layers_for(self, self.provider.sourceLayers, is_image=True)
143 def configure_tms(self):
144 """Select the tile matrix set of the active source layer.
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.
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]
160 def configure_style(self):
161 """Select the style of the active source layer.
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``.
166 Returns:
167 Always ``True``.
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')
180 for style in self.activeLayer.styles:
181 if style.isDefault:
182 self.activeStyle = style
183 return True
185 self.activeStyle = gws.SourceStyle(name='default')
186 return True
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
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
204 return True
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')
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
228 def configure_metadata(self):
229 if super().configure_metadata():
230 return True
231 self.metadata = self.provider.metadata
232 return True
234 ##