Coverage for gws-app/gws/base/layer/image.py: 70%
67 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 raster layer."""
3from typing import Optional, cast
5import gws
6import gws.base.grabber
7import gws.gis.cache
8import gws.lib.crs
9import gws.lib.extent
11from . import core
13CACHE_NAME_LENGTH = 12
16class Object(core.Object):
17 """Base raster layer.
19 Renders boxes and tiles through grabbers (see ``gws.base.grabber``), one
20 per CRS supported by the application. Subclasses provide the grabber with
21 ``create_grabber`` and the default cache name with ``create_cache_name``.
22 """
24 canRenderBox = True
25 canRenderTile = True
27 def post_configure(self):
28 self.post_configure_grabbers()
30 def post_configure_grabbers(self):
31 """Create a grabber for each CRS supported by the application.
33 The cache settings come from the ``cache`` configuration. The cache
34 name defaults to ``create_cache_name`` and gets the CRS code appended.
35 Caching is disabled (``maxAge`` 0) when ``withCache`` is off, and for
36 CRSs not listed in ``cache.crs`` if that list is given. CRSs that are
37 incompatible with the layer extent are skipped with a configuration
38 warning.
39 """
40 if not (self.canRenderBox or self.canRenderTile):
41 return
43 p = cast(
44 gws.gis.cache.LayerConfig,
45 self.cfg('cache') or self.root.specs.read({}, 'gws.gis.cache.core.LayerConfig'),
46 )
47 cache_proto = gws.MapCache(
48 name=p.name,
49 maxAge=p.maxAge,
50 maxLevel=p.maxLevel,
51 requestBuffer=p.requestBuffer,
52 requestTiles=p.requestTiles,
53 )
54 if not self.cfg('withCache'):
55 cache_proto.maxAge = 0
56 if not cache_proto.name:
57 cache_proto.name = self.create_cache_name(cache_proto)
59 cache_srids = []
60 if p.crs:
61 cache_srids = [gws.lib.crs.require(c).srid for c in p.crs]
63 for crs in self.root.app.supported_crs():
64 ext = crs.clip_wgs_extent(self.wgsExtent)
65 if not ext:
66 self.root.config_warning(f'extent {self.wgsExtent} is incompatible with {crs!r}')
67 continue
68 cache = gws.MapCache(**vars(cache_proto))
69 if cache_srids and crs.srid not in cache_srids:
70 cache.maxAge = 0
71 cache.name += f'_{crs.srid}'
73 opts = gws.base.grabber.Options(
74 crs=crs,
75 cache=cache,
76 extent=gws.lib.extent.transform_from_wgs(ext, crs),
77 imageFormat=self.imageFormat,
78 )
80 gr = self.create_grabber(opts)
81 if gr:
82 self.grabbers[crs.srid] = gr
84 def create_cache_name(self, cache: gws.MapCache) -> str:
85 """Create a default cache name for the layer.
87 Subclasses must implement this.
89 Args:
90 cache: Cache settings.
92 Returns:
93 Cache name.
95 Raises:
96 ``NotImplementedError``: In the base class.
97 """
98 raise NotImplementedError(f'create_cache_name not implemented in {self!r}')
100 def create_grabber(self, opts: gws.base.grabber.Options) -> Optional[gws.Grabber]:
101 """Create a grabber for one CRS. Returns nothing in the base class.
103 Args:
104 opts: Grabber options: CRS, cache, extent and image format.
106 Returns:
107 A grabber, or ``None`` if the layer cannot serve this CRS.
108 """
109 pass
111 ##
113 def grabber_for(self, lri: gws.LayerRenderInput) -> Optional[gws.Grabber]:
114 """Find the grabber for the target CRS of a render request.
116 Args:
117 lri: Render input; without ``targetCrs`` the map CRS is used.
119 Returns:
120 The grabber, or ``None`` if there is none for the CRS.
121 """
122 crs = lri.targetCrs or self.mapCrs
123 return self.grabbers.get(crs.srid)
125 def render_tile(self, lri):
126 grabber = self.grabber_for(lri)
127 if not grabber:
128 return
130 blob = grabber.get_tile_as_bytes((lri.x, lri.y, lri.z), lri.renderParams)
131 return gws.LayerRenderOutput(content=blob)
133 def render_box(self, lri):
134 grabber = self.grabber_for(lri)
135 if not grabber:
136 return
138 params = lri.renderParams
139 w, h = lri.view.pxSize
141 if not lri.view.rotation:
142 blob = grabber.get_box_as_bytes(lri.view.bounds.extent, w, h, params)
143 return gws.LayerRenderOutput(content=blob)
145 circ = gws.lib.extent.circumsquare(lri.view.bounds.extent)
146 d = gws.u.to_rounded_int(gws.lib.extent.diagonal((0, 0, w, h)))
148 img = grabber.get_box_as_image(circ, d, d, params)
149 img.rotate(-lri.view.rotation)
150 img.crop(
151 (
152 d / 2 - w / 2,
153 d / 2 - h / 2,
154 d / 2 + w / 2,
155 d / 2 + h / 2,
156 )
157 )
158 blob = img.to_bytes(self.imageFormat.mimeTypes[0], self.imageFormat.options)
159 return gws.LayerRenderOutput(content=blob)