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

1"""Base layer object.""" 

2 

3from typing import Optional, cast 

4 

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 

15 

16from . import ows 

17 

18DEFAULT_TILE_SIZE = 256 

19 

20 

21class CacheConfig(gws.Config): 

22 """Cache configuration""" 

23 

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] 

30 

31 

32class GridConfig(gws.Config): 

33 """Grid configuration for caches and tiled data""" 

34 

35 crs: Optional[gws.CrsName] 

36 extent: Optional[gws.Extent] 

37 origin: Optional[gws.Origin] 

38 resolutions: Optional[list[float]] 

39 tileSize: Optional[int] 

40 

41 

42class AutoLayersOptions(gws.ConfigWithAccess): 

43 """Configuration for automatic layers.""" 

44 

45 applyTo: Optional[gws.gis.source.LayerFilter] 

46 config: dict 

47 

48 

49class ClientOptions(gws.Data): 

50 """Client options for a layer""" 

51 

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

64 

65 

66class GridProps(gws.Props): 

67 origin: str 

68 extent: gws.Extent 

69 resolutions: list[float] 

70 tileSize: int 

71 

72 

73class Config(gws.ConfigWithAccess): 

74 """Layer configuration""" 

75 

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

122 

123 

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 = '' 

142 

143 

144class Object(gws.Layer): 

145 parent: gws.Layer 

146 

147 clientOptions: gws.LayerClientOptions 

148 cssSelector: str 

149 

150 canRenderBox = False 

151 canRenderSvg = False 

152 canRenderXyz = False 

153 

154 isEnabledForOws = False 

155 isGroup = False 

156 isSearchable = False 

157 

158 hasLegend = False 

159 

160 parentBounds: gws.Bounds 

161 parentResolutions: list[float] 

162 

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

171 

172 self.parentBounds = self.cfg('_parentBounds') 

173 self.parentResolutions = self.cfg('_parentResolutions') 

174 self.mapCrs = self.parentBounds.crs 

175 

176 self.bounds = self.parentBounds 

177 self.zoomBounds = cast(gws.Bounds, None) 

178 self.resolutions = self.parentResolutions 

179 

180 self.templates = [] 

181 self.models = [] 

182 self.finders = [] 

183 

184 self.metadata = gws.Metadata() 

185 self.legend = None 

186 self.legendUrl = '' 

187 

188 self.layers = [] 

189 

190 self.grid = None 

191 self.cache = None 

192 self.ows = gws.LayerOws() 

193 

194 setattr(self, 'provider', None) 

195 self.sourceLayers = [] 

196 

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

212 

213 ## 

214 

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 

222 

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 

230 

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 

236 

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 

248 

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 

256 

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 

262 

263 def configure_models(self): 

264 return gws.config.util.configure_models_for(self) 

265 

266 def configure_provider(self): 

267 pass 

268 

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 

276 

277 def configure_search(self): 

278 if not self.cfg('withSearch'): 

279 return True 

280 return gws.config.util.configure_finders_for(self) 

281 

282 def configure_sources(self): 

283 pass 

284 

285 def configure_templates(self): 

286 return gws.config.util.configure_templates_for(self) 

287 

288 def configure_group_layers(self, layer_configs): 

289 ls = [] 

290 

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

298 

299 self.layers = gws.u.compact(ls) 

300 

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

304 

305 ## 

306 

307 def post_configure(self): 

308 self.isSearchable = bool(self.finders) 

309 self.hasLegend = bool(self.legend) 

310 

311 if self.bounds.crs != self.mapCrs: 

312 raise gws.Error(f'layer {self!r}: invalid CRS {self.bounds.crs}') 

313 

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) 

317 

318 self.wgsExtent = gws.gis.bounds.transform(self.bounds, gws.gis.crs.WGS84).extent 

319 self.zoomBounds = self.zoomBounds or self.bounds 

320 

321 if self.legend: 

322 self.legendUrl = self.url_path('legend') 

323 

324 ## 

325 

326 # @TODO use Node.find_ancestors 

327 

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 

335 

336 def descendants(self): 

337 ls = [] 

338 for la in self.layers: 

339 ls.append(la) 

340 ls.extend(la.descendants()) 

341 return ls 

342 

343 _url_path_suffix = '/gws.png' 

344 

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) 

355 

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 ) 

371 

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 ) 

379 

380 if self.displayMode == gws.LayerDisplayMode.tile: 

381 p.type = 'tile' 

382 p.url = self.url_path('tile') 

383 

384 if self.displayMode == gws.LayerDisplayMode.box: 

385 p.type = 'box' 

386 p.url = self.url_path('box') 

387 

388 return p 

389 

390 def render(self, lri): 

391 pass 

392 

393 def find_features(self, search, user): 

394 return [] 

395 

396 def render_legend(self, args=None) -> Optional[gws.LegendRenderOutput]: 

397 

398 if not self.legend: 

399 return None 

400 

401 def _get(): 

402 out = self.legend.render() 

403 return out 

404 

405 if not args: 

406 return gws.u.get_server_global('legend_' + self.uid, _get) 

407 

408 return self.legend.render(args) 

409 

410 def mapproxy_config(self, mc): 

411 pass