Coverage for gws-app/gws/lib/uom/__init__.py: 69%
105 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"""Units of measure.
3Conversions between map scales and resolutions, between millimetres and pixels,
4and parsing of values with units.
6Values with units are represented as tuples, see the ``gws.Uom*`` types:
8- ``gws.UomValue``: ``(5, gws.Uom.mm)``,
9- ``gws.UomPoint`` and ``gws.UomSize``: ``(10, 20, gws.Uom.mm)``,
10- ``gws.UomExtent``: ``(0, 0, 100, 200, gws.Uom.mm)``.
12Conversions between millimetres and pixels need a resolution in pixels per inch.
13Scale and resolution conversions use the OGC standard pixel size of 0.28 mm.
15Example::
17 import gws.lib.uom
19 v = gws.lib.uom.parse('5mm') # (5.0, gws.Uom.mm)
20 gws.lib.uom.to_px(v, 96) # (18.89..., gws.Uom.px)
21 gws.lib.uom.to_str(v) # '5mm'
22 gws.lib.uom.parse_point('10mm,20mm') # (10.0, 20.0, gws.Uom.mm)
23 gws.lib.uom.res_to_scale(0.28) # 1000
24"""
26import re
28import gws
30MM_PER_IN = 25.4
31"""Conversion factor from inch to millimetre."""
33PT_PER_IN = 72
34"""Conversion factor from inch to points."""
36OGC_M_PER_PX = 0.00028
37"""OGC meter per pixel (OGC 06-042, 7.2.4.6.9: 1px = 0.28mm)."""
39OGC_SCREEN_PPI = MM_PER_IN / (OGC_M_PER_PX * 1000) # 90.71
40"""Screen pixels per inch according to the OGC standard pixel size."""
42PDF_DPI = 96
43"""Dots per inch in a PDF file."""
45# 1 centimeter precision
47DEFAULT_PRECISION = {
48 gws.Uom.deg: 7,
49 gws.Uom.m: 2,
50}
52_number = int | float
55def scale_to_res(x: _number) -> float:
56 """Convert a scale denominator to a resolution.
58 Args:
59 x: Scale denominator.
61 Returns:
62 Resolution in metres per pixel, using the OGC pixel size.
63 """
64 # return round(x * OGC_M_PER_PX, 4)
65 return x * OGC_M_PER_PX
68def res_to_scale(x: _number) -> int:
69 """Convert a resolution to a scale denominator.
71 Args:
72 x: Resolution in metres per pixel.
74 Returns:
75 Scale denominator, using the OGC pixel size.
76 """
77 return int(x / OGC_M_PER_PX)
80# @TODO imperial units not used yet
81#
82# def mm_to_in(x: _number) -> float:
83# return x / MM_PER_IN
84#
85#
86# def m_to_in(x: _number) -> float:
87# return (x / MM_PER_IN) * 1000
88#
89#
90# def in_to_mm(x: _number) -> float:
91# return x * MM_PER_IN
92#
93#
94# def in_to_m(x: _number) -> float:
95# return (x * MM_PER_IN) / 1000
96#
97#
98# def in_to_px(x, ppi):
99# return x * ppi
100#
101#
102# def mm_to_pt(x: _number) -> float:
103# return (x / MM_PER_IN) * PT_PER_IN
104#
105#
106# def pt_to_mm(x: _number) -> float:
107# return (x / PT_PER_IN) * MM_PER_IN
108#
110##
113def mm_to_px(x: _number, ppi: int) -> float:
114 """Convert millimetres to pixels.
116 Args:
117 x: Millimetres.
118 ppi: Pixels per inch.
120 Returns:
121 Number of pixels.
122 """
123 return x * (ppi / MM_PER_IN)
126def to_px(xu: gws.UomValue, ppi: int) -> gws.UomValue:
127 """Convert a value with a unit to pixels.
129 Args:
130 xu: Value in ``px`` or ``mm``.
131 ppi: Pixels per inch.
133 Returns:
134 The value in pixels.
136 Raises:
137 ``ValueError``: If the unit is not ``px`` or ``mm``.
138 """
139 x, u = xu
140 if u == gws.Uom.px:
141 return xu
142 if u == gws.Uom.mm:
143 return mm_to_px(x, ppi), gws.Uom.px
144 raise ValueError(f'invalid unit {u!r}')
147def size_mm_to_px(xy: gws.Size, ppi: int) -> gws.Size:
148 """Convert a size in millimetres to pixels.
150 Args:
151 xy: Size in millimetres.
152 ppi: Pixels per inch.
154 Returns:
155 Size in pixels.
156 """
157 x, y = xy
158 return mm_to_px(x, ppi), mm_to_px(y, ppi)
161def size_to_px(xyu: gws.UomSize, ppi: int) -> gws.UomSize:
162 """Convert a size with a unit to pixels.
164 Args:
165 xyu: Size in ``px`` or ``mm``.
166 ppi: Pixels per inch.
168 Returns:
169 Size in pixels.
171 Raises:
172 ``ValueError``: If the unit is not ``px`` or ``mm``.
173 """
174 x, y, u = xyu
175 if u == gws.Uom.px:
176 return xyu
177 if u == gws.Uom.mm:
178 return mm_to_px(x, ppi), mm_to_px(y, ppi), gws.Uom.px
179 raise ValueError(f'invalid unit {u!r}')
182##
185def px_to_mm(x: _number, ppi: int) -> float:
186 """Convert pixels to millimetres.
188 Args:
189 x: Number of pixels.
190 ppi: Pixels per inch.
192 Returns:
193 Millimetres.
194 """
195 return x * (MM_PER_IN / ppi)
198def to_mm(xu: gws.UomValue, ppi: int) -> gws.UomValue:
199 """Convert a value with a unit to millimetres.
201 Args:
202 xu: Value in ``mm`` or ``px``.
203 ppi: Pixels per inch.
205 Returns:
206 The value in millimetres.
208 Raises:
209 ``ValueError``: If the unit is not ``mm`` or ``px``.
210 """
211 x, u = xu
212 if u == gws.Uom.mm:
213 return xu
214 if u == gws.Uom.px:
215 return px_to_mm(x, ppi), gws.Uom.mm
216 raise ValueError(f'invalid unit {u!r}')
219def size_px_to_mm(xy: gws.Size, ppi: int) -> gws.Size:
220 """Convert a size in pixels to millimetres.
222 Args:
223 xy: Size in pixels.
224 ppi: Pixels per inch.
226 Returns:
227 Size in millimetres.
228 """
229 x, y = xy
230 return px_to_mm(x, ppi), px_to_mm(y, ppi)
233def size_to_mm(xyu: gws.UomSize, ppi: int) -> gws.UomSize:
234 """Convert a size with a unit to millimetres.
236 Args:
237 xyu: Size in ``mm`` or ``px``.
238 ppi: Pixels per inch.
240 Returns:
241 Size in millimetres.
243 Raises:
244 ``ValueError``: If the unit is not ``mm`` or ``px``.
245 """
246 x, y, u = xyu
247 if u == gws.Uom.mm:
248 return xyu
249 if u == gws.Uom.px:
250 return px_to_mm(x, ppi), px_to_mm(y, ppi), gws.Uom.mm
251 raise ValueError(f'invalid unit {u!r}')
254def to_str(xu: gws.UomValue) -> str:
255 """Convert a value with a unit to a string.
257 Whole numbers are written without a decimal part.
259 Args:
260 xu: Value with a unit.
262 Returns:
263 A string like ``5mm``.
264 """
265 x, u = xu
266 sx = str(int(x)) if (x % 1 == 0) else str(x)
267 return sx + str(u)
270##
273_unit_re = re.compile(r"""(?x)
274 ^
275 (?P<number>
276 -?
277 (\d+ (\.\d*)? )
278 |
279 (\.\d+)
280 )
281 (?P<unit> \s* [a-zA-Z]*)
282 $
283""")
286def parse(val: str | int | float | tuple | list, default_unit: gws.Uom = None) -> gws.UomValue:
287 """Parse a value with a unit.
289 Args:
290 val: A string like ``'5mm'``, a number, or a pair like ``[5, 'mm']``.
291 default_unit: Unit for numbers and for strings without a known unit.
293 Returns:
294 The value with its unit.
296 Raises:
297 ``ValueError``: If the format is invalid, or the unit is missing or unknown and there is no default unit.
298 """
299 if isinstance(val, (list, tuple)):
300 if len(val) == 2:
301 return parse(f'{val[0]}{val[1]}')
302 raise ValueError(f'invalid format: {val!r}')
304 if isinstance(val, (int, float)):
305 if not default_unit:
306 raise ValueError(f'missing unit: {val!r}')
307 return val, default_unit
309 val = gws.u.to_str(val).strip()
310 m = _unit_re.match(val)
311 if not m:
312 raise ValueError(f'invalid format: {val!r}')
314 n = float(m.group('number'))
315 u = getattr(gws.Uom, m.group('unit').strip().lower(), None)
317 if not u:
318 if not default_unit:
319 raise ValueError(f'invalid unit: {val!r}')
320 return n, default_unit
322 return n, u
325def parse_point(val: str | tuple | list) -> gws.UomPoint:
326 """Parse a point with a unit.
328 Args:
329 val: A comma-separated string like ``'1mm,2mm'``, a list like ``['1mm', '2mm']`` or a list like ``[1, 2, 'mm']``.
331 Returns:
332 The point with its unit.
334 Raises:
335 ``ValueError``: If the point is invalid or the units differ.
336 """
338 v = gws.u.to_list(val)
340 if len(v) == 3:
341 v = [f'{v[0]}{v[2]}', f'{v[1]}{v[2]}']
343 if len(v) == 2:
344 n1, u1 = parse(v[0])
345 n2, u2 = parse(v[1])
346 if u1 != u2:
347 raise ValueError(f'invalid point units: {u1!r} != {u2!r}')
348 return n1, n2, u1
350 raise ValueError(f'invalid point: {val!r}')
353def parse_extent(val: str | tuple | list) -> gws.UomExtent:
354 """Parse an extent with a unit.
356 Args:
357 val: A comma-separated string like ``'1mm,2mm,3mm,4mm'``, a list of four strings, or a list like ``[1, 2, 3, 4, 'mm']``.
359 Returns:
360 The extent with its unit.
362 Raises:
363 ``ValueError``: If the extent is invalid or the units differ.
364 """
366 v = gws.u.to_list(val)
368 if len(v) == 5:
369 v = [f'{v[0]}{v[4]}', f'{v[1]}{v[2]}', f'{v[2]}{v[4]}', f'{v[3]}{v[4]}']
371 if len(v) == 4:
372 n1, u1 = parse(v[0])
373 n2, u2 = parse(v[1])
374 n3, u3 = parse(v[2])
375 n4, u4 = parse(v[3])
376 if u1 != u2 or u1 != u3 or u1 != u4:
377 raise ValueError(f'invalid extent units: {u1!r} != {u2!r} != {u3!r} != {u4!r}')
378 return n1, n2, n3, n4, u1
380 raise ValueError(f'invalid extent: {val!r}')