Coverage for gws-app/gws/base/layer/util.py: 0%
104 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1from typing import Callable
3import math
5import gws
6import gws.base.model
7import gws.base.search
8import gws.gis.crs
9import gws.gis.extent
10import gws.gis.mpx
11import gws.gis.source
12import gws.gis.zoom
13import gws.lib.image
14import gws.lib.metadata
15import gws.lib.style
16import gws.lib.svg
20def mapproxy_layer_config(layer: gws.Layer, mc, source_uid):
21 mc.layer({
22 'name': layer.uid + '_NOCACHE',
23 'sources': [source_uid]
24 })
26 tg = layer.grid
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 }))
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,
48 }
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
58 layer.mpxCacheUid = mc.cache(front_cache_config)
60 mc.layer({
61 'name': layer.uid,
62 'sources': [layer.mpxCacheUid]
63 })
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 })
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,
83 }))
86##
88_BOX_SIZE = 1000
89_BOX_BUFFER = 200
91_GetBoxFn = Callable[[gws.Bounds, float, float], bytes]
94def mpx_raster_render(layer: gws.Layer, lri: gws.LayerRenderInput):
95 if lri.type == gws.LayerRenderInputType.box:
97 uid = layer.uid
98 if not layer.cache:
99 uid += '_NOCACHE'
101 def get_box(bounds, width, height):
102 return gws.gis.mpx.wms_request(uid, bounds, width, height, forward=lri.extraParams)
104 content = generic_render_box(layer, lri, get_box)
105 return gws.LayerRenderOutput(content=content)
107 if lri.type == gws.LayerRenderInputType.xyz:
108 content = gws.gis.mpx.wmts_request(
109 layer.uid,
110 lri.x,
111 lri.y,
112 lri.z,
113 tile_matrix=layer.grid.uid,
114 tile_size=layer.grid.tileSize)
116 annotate = layer.root.app.developer_option('map.annotate_render')
117 if annotate:
118 content = _annotate(content, f'{lri.x} {lri.y} {lri.z}')
120 return gws.LayerRenderOutput(content=content)
123def generic_render_box(layer: gws.Layer, lri: gws.LayerRenderInput, get_box: _GetBoxFn) -> bytes:
124 annotate = layer.root.app.developer_option('map.annotate_render')
126 max_box_size = lri.boxSize or _BOX_SIZE
127 box_buffer = lri.boxBuffer or _BOX_BUFFER
129 w, h = lri.view.pxSize
131 if not lri.view.rotation and w < max_box_size and h < max_box_size:
132 # fast path: no rotation, small box
133 content = get_box(lri.view.bounds, w, h)
134 if annotate:
135 content = _annotate(content, 'fast')
136 return content
138 if not lri.view.rotation:
139 # no rotation, big box
140 img = _box_to_image(lri.view.bounds, w, h, max_box_size, box_buffer, annotate, get_box)
141 return img.to_bytes()
143 # rotation: render a circumsquare around the wanted extent
145 circ = gws.gis.extent.circumsquare(lri.view.bounds.extent)
146 d = gws.gis.extent.diagonal((0, 0, w, h))
147 b = gws.Bounds(crs=lri.view.bounds.crs, extent=circ)
149 img = _box_to_image(b, d, d, max_box_size, box_buffer, annotate, get_box)
151 # rotate the square (NB: PIL rotations are counter-clockwise)
152 # and crop the square back to the wanted extent
154 img.rotate(-lri.view.rotation).crop((
155 d / 2 - w / 2,
156 d / 2 - h / 2,
157 d / 2 + w / 2,
158 d / 2 + h / 2,
159 ))
161 return img.to_bytes()
164def _box_to_image(bounds: gws.Bounds, width: float, height: float, max_size: int, buf: int, annotate: bool, get_box: _GetBoxFn) -> gws.lib.image.Image:
166 if width < max_size and height < max_size:
167 content = get_box(bounds, width, height)
168 img = gws.lib.image.from_bytes(content)
169 if annotate:
170 img = _annotate_image(img, 'small')
171 return img
173 xcount = math.ceil(width / max_size)
174 ycount = math.ceil(height / max_size)
176 ext = bounds.extent
178 xres = (ext[2] - ext[0]) / width
179 yres = (ext[3] - ext[1]) / height
181 gws.log.debug(f'_box_to_image (BIG): {xcount=} {ycount=} {xres=} {yres=}')
183 ext_w = xres * max_size
184 ext_h = yres * max_size
186 grid = []
188 for ny in range(ycount):
189 for nx in range(xcount):
190 e = (
191 ext[0] + ext_w * (nx + 0) - buf * xres,
192 ext[3] - ext_h * (ny + 1) - buf * yres,
193 ext[0] + ext_w * (nx + 1) + buf * xres,
194 ext[3] - ext_h * (ny + 0) + buf * yres,
195 )
196 bounds = gws.Bounds(crs=bounds.crs, extent=e)
197 content = get_box(bounds, max_size + buf * 2, max_size + buf * 2)
198 gws.log.debug(f'_box_to_image (BIG): {nx=}/{xcount} {ny=}/{ycount} {len(content)=}')
199 grid.append([nx, ny, content])
201 img = gws.lib.image.from_size((max_size * xcount, max_size * ycount))
203 for nx, ny, content in grid:
204 tile = gws.lib.image.from_bytes(content)
205 tile.crop((buf, buf, tile.size()[0] - buf, tile.size()[1] - buf))
206 if annotate:
207 _annotate_image(tile, f'{nx} {ny}')
208 img.paste(tile, (nx * max_size, ny * max_size))
210 img.crop((0, 0, gws.u.to_rounded_int(width), gws.u.to_rounded_int(height)))
211 return img
214def _annotate(content, text):
215 return _annotate_image(gws.lib.image.from_bytes(content), text).to_bytes()
218def _annotate_image(img, text):
219 return img.add_text(text, x=5, y=5).add_box()