Coverage for gws-app/gws/base/layer/util.py: 31%

104 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 12:46 +0200

1from typing import Callable 

2 

3import math 

4 

5import gws 

6import gws.base.model 

7import gws.base.search 

8import gws.lib.crs 

9import gws.lib.extent 

10import gws.gis.mpx 

11import gws.gis.source 

12import gws.gis.zoom 

13import gws.lib.image 

14import gws.base.metadata 

15import gws.lib.style 

16import gws.lib.svg 

17 

18 

19 

20def mapproxy_layer_config(layer: gws.Layer, mc, source_uid): 

21 mc.layer({ 

22 'name': layer.uid + '_NOCACHE', 

23 'sources': [source_uid] 

24 }) 

25 

26 tg = layer.grid 

27 

28 tg.uid = mc.grid(gws.u.compact({ 

29 'origin': tg.origin, 

30 'tile_size': [tg.tileSize, tg.tileSize], 

31 'res': tg.resolutions, 

32 'srs': tg.bounds.crs.epsg, 

33 'bbox': tg.bounds.extent, 

34 })) 

35 

36 front_cache_config = { 

37 'sources': [source_uid], 

38 'grids': [tg.uid], 

39 'cache': { 

40 'type': 'file', 

41 'directory_layout': 'mp' 

42 }, 

43 'meta_size': [1, 1], 

44 'meta_buffer': 0, 

45 'disable_storage': True, 

46 'minimize_meta_requests': True, 

47 'format': layer.imageFormat.name or 'png8', 

48 } 

49 

50 cache = getattr(layer, 'cache', None) 

51 if cache: 

52 front_cache_config['disable_storage'] = False 

53 if cache.requestTiles: 

54 front_cache_config['meta_size'] = [cache.requestTiles, cache.requestTiles] 

55 if cache.requestBuffer: 

56 front_cache_config['meta_buffer'] = cache.requestBuffer 

57 

58 layer.mpxCacheUid = mc.cache(front_cache_config) 

59 

60 mc.layer({ 

61 'name': layer.uid, 

62 'sources': [layer.mpxCacheUid] 

63 }) 

64 

65 

66def mapproxy_back_cache_config(layer: gws.Layer, mc, url, grid_uid): 

67 source_uid = mc.source({ 

68 'type': 'tile', 

69 'url': url, 

70 'grid': grid_uid, 

71 'concurrent_requests': layer.cfg('maxRequests', default=0) 

72 }) 

73 

74 return mc.cache(gws.u.compact({ 

75 'sources': [source_uid], 

76 'grids': [grid_uid], 

77 'cache': { 

78 'type': 'file', 

79 'directory_layout': 'mp' 

80 }, 

81 'disable_storage': True, 

82 'format': layer.imageFormat.name or 'png8', 

83 

84 })) 

85 

86 

87## 

88 

89_DEFAULT_BOX_SIZE = 1000 

90_DEFAULT_BOX_BUFFER = 200 

91 

92_GetBoxFn = Callable[[gws.Bounds, float, float], bytes] 

93 

94 

95def mpx_raster_render(layer: gws.Layer, lri: gws.LayerRenderInput): 

96 if lri.type == gws.LayerRenderInputType.box: 

97 

98 uid = layer.uid 

99 if not layer.cache: 

100 uid += '_NOCACHE' 

101 

102 def get_box(bounds, width, height): 

103 return gws.gis.mpx.wms_request(uid, bounds, width, height, forward=lri.extraParams) 

104 

105 content = generic_render_box(layer, lri, get_box) 

106 return gws.LayerRenderOutput(content=content) 

107 

108 if lri.type == gws.LayerRenderInputType.xyz: 

109 content = gws.gis.mpx.wmts_request( 

110 layer.uid, 

111 lri.x, 

112 lri.y, 

113 lri.z, 

114 tile_matrix=layer.grid.uid, 

115 tile_size=layer.grid.tileSize) 

116 

117 annotate = layer.root.app.developer_option('map.annotate_render') 

118 if annotate: 

119 content = _annotate(content, f'{lri.x} {lri.y} {lri.z}') 

120 

121 return gws.LayerRenderOutput(content=content) 

122 

123 

124def generic_render_box(layer: gws.Layer, lri: gws.LayerRenderInput, get_box: _GetBoxFn, box_size: int = 0, box_buffer: int = 0) -> bytes: 

125 annotate = layer.root.app.developer_option('map.annotate_render') 

126 

127 box_size = box_size or _DEFAULT_BOX_SIZE 

128 box_buffer = box_buffer or _DEFAULT_BOX_BUFFER 

129 

130 w, h = lri.view.pxSize 

131 

132 if not lri.view.rotation and w < box_size and h < box_size: 

133 # fast path: no rotation, small box 

134 content = get_box(lri.view.bounds, w, h) 

135 if annotate: 

136 content = _annotate(content, 'fast') 

137 return content 

138 

139 if not lri.view.rotation: 

140 # no rotation, big box 

141 img = _box_to_image(lri.view.bounds, w, h, box_size, box_buffer, annotate, get_box) 

142 return img.to_bytes() 

143 

144 # rotation: render a circumsquare around the wanted extent 

145 

146 circ = gws.lib.extent.circumsquare(lri.view.bounds.extent) 

147 d = gws.lib.extent.diagonal((0, 0, w, h)) 

148 b = gws.Bounds(crs=lri.view.bounds.crs, extent=circ) 

149 

150 img = _box_to_image(b, d, d, box_size, box_buffer, annotate, get_box) 

151 

152 # rotate the square (NB: PIL rotations are counter-clockwise) 

153 # and crop the square back to the wanted extent 

154 

155 img.rotate(-lri.view.rotation).crop(( 

156 d / 2 - w / 2, 

157 d / 2 - h / 2, 

158 d / 2 + w / 2, 

159 d / 2 + h / 2, 

160 )) 

161 

162 return img.to_bytes() 

163 

164 

165def _box_to_image(bounds: gws.Bounds, width: float, height: float, max_size: int, buffer: int, annotate: bool, get_box: _GetBoxFn) -> gws.lib.image.Image: 

166 

167 if width < max_size and height < max_size: 

168 content = get_box(bounds, width, height) 

169 img = gws.lib.image.from_bytes(content) 

170 if annotate: 

171 img = _annotate_image(img, 'small') 

172 return img 

173 

174 xcount = math.ceil(width / max_size) 

175 ycount = math.ceil(height / max_size) 

176 

177 ext = bounds.extent 

178 

179 xres = (ext[2] - ext[0]) / width 

180 yres = (ext[3] - ext[1]) / height 

181 

182 gws.log.debug(f'_box_to_image (BIG): {xcount=} {ycount=} {xres=} {yres=}') 

183 

184 ext_w = xres * max_size 

185 ext_h = yres * max_size 

186 

187 grid = [] 

188 

189 for ny in range(ycount): 

190 for nx in range(xcount): 

191 e = ( 

192 ext[0] + ext_w * (nx + 0) - buffer * xres, 

193 ext[3] - ext_h * (ny + 1) - buffer * yres, 

194 ext[0] + ext_w * (nx + 1) + buffer * xres, 

195 ext[3] - ext_h * (ny + 0) + buffer * yres, 

196 ) 

197 bounds = gws.Bounds(crs=bounds.crs, extent=e) 

198 content = get_box(bounds, max_size + buffer * 2, max_size + buffer * 2) 

199 gws.log.debug(f'_box_to_image (BIG): {nx=}/{xcount} {ny=}/{ycount} {len(content)=}') 

200 grid.append([nx, ny, content]) 

201 

202 img = gws.lib.image.from_size((max_size * xcount, max_size * ycount)) 

203 

204 for nx, ny, content in grid: 

205 tile = gws.lib.image.from_bytes(content) 

206 tile.crop((buffer, buffer, tile.size()[0] - buffer, tile.size()[1] - buffer)) 

207 if annotate: 

208 _annotate_image(tile, f'{nx} {ny}') 

209 img.paste(tile, (nx * max_size, ny * max_size)) 

210 

211 img.crop((0, 0, gws.u.to_rounded_int(width), gws.u.to_rounded_int(height))) 

212 return img 

213 

214 

215def _annotate(content, text): 

216 return _annotate_image(gws.lib.image.from_bytes(content), text).to_bytes() 

217 

218 

219def _annotate_image(img, text): 

220 return img.add_text(text, x=5, y=5).add_box()