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

1"""Base raster layer.""" 

2 

3from typing import Optional, cast 

4 

5import gws 

6import gws.base.grabber 

7import gws.gis.cache 

8import gws.lib.crs 

9import gws.lib.extent 

10 

11from . import core 

12 

13CACHE_NAME_LENGTH = 12 

14 

15 

16class Object(core.Object): 

17 """Base raster layer. 

18 

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

23 

24 canRenderBox = True 

25 canRenderTile = True 

26 

27 def post_configure(self): 

28 self.post_configure_grabbers() 

29 

30 def post_configure_grabbers(self): 

31 """Create a grabber for each CRS supported by the application. 

32 

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 

42 

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) 

58 

59 cache_srids = [] 

60 if p.crs: 

61 cache_srids = [gws.lib.crs.require(c).srid for c in p.crs] 

62 

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

72 

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 ) 

79 

80 gr = self.create_grabber(opts) 

81 if gr: 

82 self.grabbers[crs.srid] = gr 

83 

84 def create_cache_name(self, cache: gws.MapCache) -> str: 

85 """Create a default cache name for the layer. 

86 

87 Subclasses must implement this. 

88 

89 Args: 

90 cache: Cache settings. 

91 

92 Returns: 

93 Cache name. 

94 

95 Raises: 

96 ``NotImplementedError``: In the base class. 

97 """ 

98 raise NotImplementedError(f'create_cache_name not implemented in {self!r}') 

99 

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. 

102 

103 Args: 

104 opts: Grabber options: CRS, cache, extent and image format. 

105 

106 Returns: 

107 A grabber, or ``None`` if the layer cannot serve this CRS. 

108 """ 

109 pass 

110 

111 ## 

112 

113 def grabber_for(self, lri: gws.LayerRenderInput) -> Optional[gws.Grabber]: 

114 """Find the grabber for the target CRS of a render request. 

115 

116 Args: 

117 lri: Render input; without ``targetCrs`` the map CRS is used. 

118 

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) 

124 

125 def render_tile(self, lri): 

126 grabber = self.grabber_for(lri) 

127 if not grabber: 

128 return 

129 

130 blob = grabber.get_tile_as_bytes((lri.x, lri.y, lri.z), lri.renderParams) 

131 return gws.LayerRenderOutput(content=blob) 

132 

133 def render_box(self, lri): 

134 grabber = self.grabber_for(lri) 

135 if not grabber: 

136 return 

137 

138 params = lri.renderParams 

139 w, h = lri.view.pxSize 

140 

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) 

144 

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

147 

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)