Coverage for gws-app/gws/lib/uom/__init__.py: 83%
122 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +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(v: gws.UomValue, ppi: int) -> gws.UomValue:
127 """Convert a value with a unit to pixels.
129 Args:
130 v: 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 = v
140 if u == gws.Uom.px:
141 return v
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(s: gws.Size, ppi: int) -> gws.Size:
148 """Convert a size in millimetres to pixels.
150 Args:
151 s: Size in millimetres.
152 ppi: Pixels per inch.
154 Returns:
155 Size in pixels.
156 """
157 x, y = s
158 return mm_to_px(x, ppi), mm_to_px(y, ppi)
161def size_to_px(s: gws.UomSize, ppi: int) -> gws.UomSize:
162 """Convert a size with a unit to pixels.
164 Args:
165 s: 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 = s
175 if u == gws.Uom.px:
176 return s
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}')
182def extent_to_px(ext: gws.UomExtent, ppi: int) -> gws.UomExtent:
183 """Convert an extent with a unit to pixels.
185 Args:
186 ext: Extent in ``px`` or ``mm``.
187 ppi: Pixels per inch.
189 Returns:
190 Extent in pixels.
192 Raises:
193 ``ValueError``: If the unit is not ``px`` or ``mm``.
194 """
195 a, b, c, d, u = ext
196 if u == gws.Uom.px:
197 return ext
198 if u == gws.Uom.mm:
199 return (
200 mm_to_px(a, ppi),
201 mm_to_px(b, ppi),
202 mm_to_px(c, ppi),
203 mm_to_px(d, ppi),
204 gws.Uom.px,
205 )
206 raise ValueError(f'invalid unit {u!r}')
209##
212def px_to_mm(x: _number, ppi: int) -> float:
213 """Convert pixels to millimetres.
215 Args:
216 x: Number of pixels.
217 ppi: Pixels per inch.
219 Returns:
220 Millimetres.
221 """
222 return x * (MM_PER_IN / ppi)
225def to_mm(v: gws.UomValue, ppi: int) -> gws.UomValue:
226 """Convert a value with a unit to millimetres.
228 Args:
229 v: Value in ``mm`` or ``px``.
230 ppi: Pixels per inch.
232 Returns:
233 The value in millimetres.
235 Raises:
236 ``ValueError``: If the unit is not ``mm`` or ``px``.
237 """
238 x, u = v
239 if u == gws.Uom.mm:
240 return v
241 if u == gws.Uom.px:
242 return px_to_mm(x, ppi), gws.Uom.mm
243 raise ValueError(f'invalid unit {u!r}')
246def size_px_to_mm(s: gws.Size, ppi: int) -> gws.Size:
247 """Convert a size in pixels to millimetres.
249 Args:
250 s: Size in pixels.
251 ppi: Pixels per inch.
253 Returns:
254 Size in millimetres.
255 """
256 x, y = s
257 return px_to_mm(x, ppi), px_to_mm(y, ppi)
260def size_to_mm(s: gws.UomSize, ppi: int) -> gws.UomSize:
261 """Convert a size with a unit to millimetres.
263 Args:
264 s: Size in ``mm`` or ``px``.
265 ppi: Pixels per inch.
267 Returns:
268 Size in millimetres.
270 Raises:
271 ``ValueError``: If the unit is not ``mm`` or ``px``.
272 """
273 x, y, u = s
274 if u == gws.Uom.mm:
275 return s
276 if u == gws.Uom.px:
277 return px_to_mm(x, ppi), px_to_mm(y, ppi), gws.Uom.mm
278 raise ValueError(f'invalid unit {u!r}')
281def extent_to_mm(ext: gws.UomExtent, ppi: int) -> gws.UomExtent:
282 """Convert an extent with a unit to millimetres.
284 Args:
285 ext: Extent in ``mm`` or ``px``.
286 ppi: Pixels per inch.
288 Returns:
289 Extent in millimetres.
291 Raises:
292 ``ValueError``: If the unit is not ``mm`` or ``px``.
293 """
294 a, b, c, d, u = ext
295 if u == gws.Uom.mm:
296 return ext
297 if u == gws.Uom.px:
298 return (
299 px_to_mm(a, ppi),
300 px_to_mm(b, ppi),
301 px_to_mm(c, ppi),
302 px_to_mm(d, ppi),
303 gws.Uom.mm,
304 )
305 raise ValueError(f'invalid unit {u!r}')
308def to_str(v: gws.UomValue) -> str:
309 """Convert a value with a unit to a string.
311 Whole numbers are written without a decimal part.
313 Args:
314 v: Value with a unit.
316 Returns:
317 A string like ``5mm``.
318 """
319 x, u = v
320 sx = str(int(x)) if (x % 1 == 0) else str(x)
321 return sx + str(u)
324##
327_unit_re = re.compile(r"""(?x)
328 ^
329 (?P<number>
330 -?
331 (\d+ (\.\d*)? )
332 |
333 (\.\d+)
334 )
335 (?P<unit> \s* [a-zA-Z]*)
336 $
337""")
340def parse(val: str | float | tuple | list, default_unit: gws.Uom = None) -> gws.UomValue:
341 """Parse a value with a unit.
343 Args:
344 val: A string like ``'5mm'``, a number, or a pair like ``[5, 'mm']``.
345 default_unit: Unit for numbers and for strings without a known unit.
347 Returns:
348 The value with its unit.
350 Raises:
351 ``ValueError``: If the format is invalid, or the unit is missing or unknown and there is no default unit.
352 """
353 if isinstance(val, (list, tuple)):
354 if len(val) == 2:
355 return parse(f'{val[0]}{val[1]}', default_unit)
356 raise ValueError(f'invalid format: {val!r}')
358 if isinstance(val, (int, float)):
359 if not default_unit:
360 raise ValueError(f'missing unit: {val!r}')
361 return val, default_unit
363 val = gws.u.to_str(val).strip()
364 m = _unit_re.match(val)
365 if not m:
366 raise ValueError(f'invalid format: {val!r}')
368 n = float(m.group('number'))
369 u = getattr(gws.Uom, m.group('unit').strip().lower(), None)
371 if not u:
372 if not default_unit:
373 raise ValueError(f'invalid unit: {val!r}')
374 return n, default_unit
376 return n, u
379def parse_point(val: str | tuple | list) -> gws.UomPoint:
380 """Parse a point with a unit.
382 Args:
383 val: A comma-separated string like ``'1mm,2mm'``, a list like ``['1mm', '2mm']`` or a list like ``[1, 2, 'mm']``.
385 Returns:
386 The point with its unit.
388 Raises:
389 ``ValueError``: If the point is invalid or the units differ.
390 """
392 v = gws.u.to_list(val)
394 if len(v) == 3:
395 x, y, u = v
396 v = [f'{x}{u}', f'{y}{u}']
398 if len(v) == 2:
399 x, u1 = parse(v[0])
400 y, u2 = parse(v[1])
401 if u1 != u2:
402 raise ValueError(f'invalid point units: {u1!r} != {u2!r}')
403 return x, y, u1
405 raise ValueError(f'invalid point: {val!r}')
408def parse_extent(val: str | tuple | list) -> gws.UomExtent:
409 """Parse an extent with a unit.
411 Args:
412 val: A comma-separated string like ``'1mm,2mm,3mm,4mm'``, a list of four strings, or a list like ``[1, 2, 3, 4, 'mm']``.
414 Returns:
415 The extent with its unit.
417 Raises:
418 ``ValueError``: If the extent is invalid or the units differ.
419 """
421 v = gws.u.to_list(val)
423 if len(v) == 5:
424 a, b, c, d, u = v
425 v = [
426 f'{a}{u}',
427 f'{b}{u}',
428 f'{c}{u}',
429 f'{d}{u}',
430 ]
432 if len(v) == 4:
433 a, b, c, d = v
434 n1, u1 = parse(a)
435 n2, u2 = parse(b)
436 n3, u3 = parse(c)
437 n4, u4 = parse(d)
438 if u1 != u2 or u1 != u3 or u1 != u4:
439 raise ValueError(f'invalid extent units: {u1!r} != {u2!r} != {u3!r} != {u4!r}')
440 return n1, n2, n3, n4, u1
442 raise ValueError(f'invalid extent: {val!r}')