Coverage for gws-app/gws/gis/render/__init__.py: 17%
156 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
1"""Map render utilities"""
3import math
5import gws
6import gws.gis.extent
7import gws.lib.image
8import gws.lib.svg
9import gws.lib.uom
10import gws.lib.xmlx as xmlx
12MAX_DPI = 1200
13MIN_DPI = gws.lib.uom.PDF_DPI
16# Map Views
18def map_view_from_center(
19 size: gws.UomSize,
20 center: gws.Point,
21 crs: gws.Crs,
22 dpi,
23 scale,
24 rotation=0,
25) -> gws.MapView:
26 return _map_view(None, center, crs, dpi, rotation, scale, size)
29def map_view_from_bbox(
30 size: gws.UomSize,
31 bbox: gws.Extent,
32 crs: gws.Crs,
33 dpi,
34 rotation=0,
35) -> gws.MapView:
36 return _map_view(bbox, None, crs, dpi, rotation, None, size)
39def _map_view(bbox, center, crs, dpi, rotation, scale, size):
40 view = gws.MapView(
41 dpi=dpi,
42 rotation=rotation,
43 )
45 w, h, u = size
46 if u == gws.Uom.mm:
47 view.mmSize = w, h
48 view.pxSize = gws.lib.uom.size_mm_to_px(view.mmSize, view.dpi)
49 if u == gws.Uom.px:
50 view.pxSize = w, h
51 view.mmSize = gws.lib.uom.size_px_to_mm(view.pxSize, view.dpi)
53 if bbox:
54 view.bounds = gws.Bounds(crs=crs, extent=bbox)
55 view.center = gws.gis.extent.center(bbox)
56 bw, bh = gws.gis.extent.size(bbox)
57 view.scale = gws.lib.uom.res_to_scale(bw / view.pxSize[0])
58 return view
60 if center:
61 view.center = center
62 view.scale = scale
64 # @TODO assuming projection units are 'm'
65 projection_units_per_mm = scale / 1000.0
66 size = view.mmSize[0] * projection_units_per_mm, view.mmSize[1] * projection_units_per_mm
67 bbox = gws.gis.extent.from_center(center, size)
68 view.bounds = gws.Bounds(crs=crs, extent=bbox)
69 return view
71 raise gws.Error('center or bbox required')
74def map_view_transformer(view: gws.MapView):
75 """Create a pixel transformer f(map_x, map_y) -> (pixel_x, pixel_y) for a view"""
77 # @TODO cache the transformer
79 def translate(x, y):
80 x = x - ext[0]
81 y = ext[3] - y
82 return x * m2px, y * m2px
84 def translate_int(x, y):
85 x, y = translate(x, y)
86 return int(x), int(y)
88 def rotate(x, y):
89 return (
90 cosa * (x - ox) - sina * (y - oy) + ox,
91 sina * (x - ox) + cosa * (y - oy) + oy)
93 def translate_rotate_int(x, y):
94 x, y = translate(x, y)
95 x, y = rotate(x, y)
96 return int(x), int(y)
98 m2px = 1000.0 * gws.lib.uom.mm_to_px(1 / view.scale, view.dpi)
100 ext = view.bounds.extent
102 if not view.rotation:
103 return translate_int
105 ox, oy = translate(*gws.gis.extent.center(ext))
106 cosa = math.cos(math.radians(view.rotation))
107 sina = math.sin(math.radians(view.rotation))
109 return translate_rotate_int
112# Rendering
115class _Renderer(gws.Data):
116 mri: gws.MapRenderInput
117 mro: gws.MapRenderOutput
118 rasterView: gws.MapView
119 vectorView: gws.MapView
120 imgCount: int
121 svgCount: int
124def render_map(mri: gws.MapRenderInput) -> gws.MapRenderOutput:
125 rd = _Renderer(
126 mri=mri,
127 mro=gws.MapRenderOutput(planes=[]),
128 imgCount=0,
129 svgCount=0
130 )
132 # vectors always use PDF_DPI
133 rd.vectorView = _map_view(mri.bbox, mri.center, mri.crs, gws.lib.uom.PDF_DPI, mri.rotation, mri.scale, mri.mapSize)
135 if mri.mapSize[2] == gws.Uom.px:
136 # if they want pixels, use PDF_PDI for rasters as well
137 rd.rasterView = rd.vectorView
139 elif mri.mapSize[2] == gws.Uom.mm:
140 # if they want mm, rasters should use they own dpi
141 raster_dpi = min(MAX_DPI, max(MIN_DPI, rd.mri.dpi))
142 rd.rasterView = _map_view(mri.bbox, mri.center, mri.crs, raster_dpi, mri.rotation, mri.scale, mri.mapSize)
144 else:
145 raise gws.Error(f'invalid size {mri.mapSize!r}')
147 # NB: planes are top-to-bottom
149 for n, p in enumerate(reversed(mri.planes)):
150 if mri.notify:
151 mri.notify('begin_plane', p)
152 try:
153 _render_plane(rd, p)
154 except Exception:
155 gws.log.exception(f'RENDER_FAILED: plane {len(mri.planes) - n - 1}')
156 if mri.notify:
157 mri.notify('end_plane', p)
159 rd.mro.view = rd.vectorView
160 return rd.mro
163def _render_plane(rd: _Renderer, plane: gws.MapRenderInputPlane):
164 s = plane.opacity
165 if s is not None:
166 opacity = s
167 elif plane.layer:
168 opacity = plane.layer.opacity
169 else:
170 opacity = 1
172 if plane.type == gws.MapRenderInputPlaneType.imageLayer:
173 extra_params = {}
174 if plane.subLayers:
175 extra_params = {'layers': plane.subLayers}
176 lro = plane.layer.render(gws.LayerRenderInput(
177 type=gws.LayerRenderInputType.box,
178 view=rd.rasterView,
179 extraParams=extra_params,
180 user=rd.mri.user,
181 ))
182 if lro:
183 _add_image(rd, gws.lib.image.from_bytes(lro.content), opacity)
184 return
186 if plane.type == gws.MapRenderInputPlaneType.image:
187 _add_image(rd, plane.image, opacity)
188 return
190 if plane.type == gws.MapRenderInputPlaneType.svgLayer:
191 lro = plane.layer.render(gws.LayerRenderInput(
192 type=gws.LayerRenderInputType.svg,
193 view=rd.vectorView,
194 style=plane.styles[0] if plane.styles else None,
195 user=rd.mri.user,
196 ))
197 if lro:
198 _add_svg_elements(rd, lro.tags, opacity)
199 return
201 if plane.type == gws.MapRenderInputPlaneType.features:
202 style_dct = {}
203 if plane.styles:
204 style_dct = {s.cssSelector: s for s in plane.styles}
205 for f in plane.features:
206 tags = f.to_svg(rd.vectorView, f.views.get('label', ''), style_dct.get(f.cssSelector))
207 _add_svg_elements(rd, tags, opacity)
208 return
210 if plane.type == gws.MapRenderInputPlaneType.svgSoup:
211 els = gws.lib.svg.soup_to_fragment(rd.vectorView, plane.soupPoints, plane.soupTags)
212 _add_svg_elements(rd, els, opacity)
213 return
216def _add_image(rd: _Renderer, img, opacity):
217 last_type = rd.mro.planes[-1].type if rd.mro.planes else None
219 if last_type != gws.MapRenderOutputPlaneType.image:
220 # NB use background for the first composition only
221 background = rd.mri.backgroundColor if rd.imgCount == 0 else None
222 rd.mro.planes.append(gws.MapRenderOutputPlane(
223 type=gws.MapRenderOutputPlaneType.image,
224 image=gws.lib.image.from_size(rd.rasterView.pxSize, background)))
226 rd.mro.planes[-1].image = rd.mro.planes[-1].image.compose(img, opacity)
227 rd.imgCount += 1
230def _add_svg_elements(rd: _Renderer, elements, opacity):
231 # @TODO opacity for svgs
233 last_type = rd.mro.planes[-1].type if rd.mro.planes else None
235 if last_type != gws.MapRenderOutputPlaneType.svg:
236 rd.mro.planes.append(gws.MapRenderOutputPlane(
237 type=gws.MapRenderOutputPlaneType.svg,
238 elements=[]))
240 rd.mro.planes[-1].elements.extend(elements)
241 rd.svgCount += 1
244# Output
247def output_to_html_element(mro: gws.MapRenderOutput, wrap='relative') -> gws.XmlElement:
248 w, h = mro.view.mmSize
250 css_size = f'left:0;top:0;width:{int(w)}mm;height:{int(h)}mm'
251 css_abs = f'position:absolute;{css_size}'
253 tags: list[gws.XmlElement] = []
255 for plane in mro.planes:
256 if plane.type == gws.MapRenderOutputPlaneType.image:
257 img_path = plane.image.to_path(gws.u.printtemp('mro.png'))
258 tags.append(xmlx.tag('img', {'style': css_abs, 'src': img_path}))
259 if plane.type == gws.MapRenderOutputPlaneType.path:
260 tags.append(xmlx.tag('img', {'style': css_abs, 'src': plane.path}))
261 if plane.type == gws.MapRenderOutputPlaneType.svg:
262 tags.append(gws.lib.svg.fragment_to_element(plane.elements, {'style': css_abs}))
264 if not tags:
265 tags.append(xmlx.tag('img'))
267 css_div = None
268 if wrap and wrap in {'relative', 'absolute', 'fixed'}:
269 css_div = f'position:{wrap};overflow:hidden;{css_size}'
270 return xmlx.tag('div', {'style': css_div}, *tags)
273def output_to_html_string(mro: gws.MapRenderOutput, wrap='relative') -> str:
274 div = output_to_html_element(mro, wrap)
275 return div.to_string()