Coverage for gws-app/gws/base/grabber/tile.py: 100%
41 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"""Base grabber for tiled sources."""
3import gws
4import gws.lib.extent
5import gws.lib.grid
6import gws.lib.image
8from . import core
11class Object(core.Object):
12 """Base grabber for sources addressed as tile pyramids.
14 Composes boxes and tiles from source tiles, mosaicked and warped into the
15 target grid. Provides subclasses with the composition and the caching of
16 source tiles.
18 Subclasses set ``sourceTms`` and implement ``fetch_tile_as_bytes``.
19 """
21 sourceTms: gws.TileMatrixSet
22 """Source tile matrix set."""
24 def compose_box_as_image(self, extent, w, h, params=None):
25 w = gws.u.to_rounded_int(w)
26 h = gws.u.to_rounded_int(h)
28 if self.targetCrs == self.sourceCrs:
29 src_extent = extent
30 else:
31 src_extent = self.extent_to_source_crs(extent)
32 if not src_extent:
33 gws.log.debug(f'grabber {self.cache.name!r}: empty image: box {extent!r} outside the source CRS area')
34 return self.empty_image(w, h)
36 src_res = gws.lib.extent.w(src_extent) / w
37 tm = gws.lib.grid.matrix_for_resolution(self.sourceTms, src_res)
38 if self.targetCrs != self.sourceCrs:
39 src_extent = gws.lib.extent.buffer(src_extent, tm.resolution * 2)
41 mtr = gws.lib.grid.matrix_range_for_extent(tm, src_extent)
42 if not mtr:
43 gws.log.debug(f'grabber {self.cache.name!r}: empty image: box {extent!r} outside the source matrix {tm.identifier!r}')
44 return self.empty_image(w, h)
46 x0, y0, x1, y1, _ = mtr
47 mw = (x1 - x0 + 1) * tm.tileWidth
48 mh = (y1 - y0 + 1) * tm.tileHeight
49 mosaic = gws.lib.image.from_size((mw, mh))
51 for x, y, _ in gws.lib.grid.enum_tiles((x0, y0, x1, y1, 0)):
52 blob = self._fetch_and_cache_source_tile(tm, x, y)
53 img = self.to_image(blob)
54 px = (x - x0) * tm.tileWidth
55 py = (y - y0) * tm.tileHeight
56 mosaic.paste(img, (px, py))
58 src_extent = gws.lib.grid.matrix_extent_for_range(tm, mtr)
59 return self.warp_image(
60 mosaic,
61 gws.Bounds(crs=self.sourceCrs, extent=src_extent),
62 gws.Bounds(crs=self.targetCrs, extent=extent),
63 w,
64 h,
65 )
67 def fetch_tile_as_bytes(self, tm: gws.TileMatrix, col: int, row: int) -> bytes:
68 """Fetch a source tile with exactly one request, as encoded bytes.
70 Subclasses must implement this.
72 Args:
73 tm: Source tile matrix.
74 col: Tile column.
75 row: Tile row.
77 Returns:
78 The encoded tile image.
80 Raises:
81 ``NotImplementedError``: In the base class.
82 """
84 raise NotImplementedError(f'fetch_tile_as_bytes not implemented in {self!r}')
86 ##
88 def _fetch_and_cache_source_tile(self, tm: gws.TileMatrix, col: int, row: int) -> bytes:
89 """Return a source tile, fetched once and kept in the ephemeral store for neighbouring requests."""
91 key = gws.u.sha256([self.cache.name, tm.identifier, col, row])
92 return gws.u.get_ephemeral_content(
93 f'source_tile_{key}',
94 lambda: self.fetch_tile_as_bytes(tm, col, row),
95 )