Coverage for gws-app/gws/lib/uom/__init__.py: 69%
105 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
1import re
3import gws
5MM_PER_IN = 25.4
6"""Conversion factor from inch to millimetre"""
8PT_PER_IN = 72
9"""Conversion factor from inch to points"""
11OGC_M_PER_PX = 0.00028
12"""OGC meter per pixel (OGC 06-042, 7.2.4.6.9: 1px = 0.28mm)."""
14OGC_SCREEN_PPI = MM_PER_IN / (OGC_M_PER_PX * 1000) # 90.71
15"""Pixel per inch on screen using the Open Geospatial Consortium standard"""
17PDF_DPI = 96
18"""Dots per inch in a pdf file"""
20# 1 centimeter precision
22DEFAULT_PRECISION = {
23 gws.Uom.deg: 7,
24 gws.Uom.m: 2,
25}
27_number = int | float
30def scale_to_res(x: _number) -> float:
31 """Converts the scale to the user's resolution.
33 Args:
34 x: Scale.
36 Returns:
37 Resolution in pixel.
38 """
39 # return round(x * OGC_M_PER_PX, 4)
40 return x * OGC_M_PER_PX
43def res_to_scale(x: _number) -> int:
44 """Converts the user's resolution to the scale.
46 Args:
47 x: Resolution in pixel per inch.
49 Returns:
50 Scale.
51 """
52 return int(x / OGC_M_PER_PX)
55# @TODO imperial units not used yet
56#
57# def mm_to_in(x: _number) -> float:
58# return x / MM_PER_IN
59#
60#
61# def m_to_in(x: _number) -> float:
62# return (x / MM_PER_IN) * 1000
63#
64#
65# def in_to_mm(x: _number) -> float:
66# return x * MM_PER_IN
67#
68#
69# def in_to_m(x: _number) -> float:
70# return (x * MM_PER_IN) / 1000
71#
72#
73# def in_to_px(x, ppi):
74# return x * ppi
75#
76#
77# def mm_to_pt(x: _number) -> float:
78# return (x / MM_PER_IN) * PT_PER_IN
79#
80#
81# def pt_to_mm(x: _number) -> float:
82# return (x / PT_PER_IN) * MM_PER_IN
83#
85##
88def mm_to_px(x: _number, ppi: int) -> float:
89 """Converts millimetres to pixel.
91 Args:
92 x: Millimetres.
93 ppi: Pixels per inch.
95 Returns:
96 Amount of pixels."""
97 return x * (ppi / MM_PER_IN)
100def to_px(xu: gws.UomValue, ppi: int) -> gws.UomValue:
101 """Converts a measurement to amount of pixels.
103 Args:
104 xu: A measurement to convert to pixels.
105 ppi: Pixels per inch.
107 Returns:
108 A measurement.
109 """
110 x, u = xu
111 if u == gws.Uom.px:
112 return xu
113 if u == gws.Uom.mm:
114 return mm_to_px(x, ppi), gws.Uom.px
115 raise ValueError(f'invalid unit {u!r}')
118def size_mm_to_px(xy: gws.Size, ppi: int) -> gws.Size:
119 """Converts a rectangle description in millimetres to pixels.
121 Args:
122 xy: A rectangle measurements in mm.
123 ppi: Pixels per inch.
125 Returns:
126 A rectangle in pixel.
127 """
128 x, y = xy
129 return mm_to_px(x, ppi), mm_to_px(y, ppi)
132def size_to_px(xyu: gws.UomSize, ppi: int) -> gws.UomSize:
133 """Converts a rectangle description of any unit to pixels.
135 Args:
136 xyu: A rectangle measurements with its unit.
137 ppi: Pixels per inch.
139 Returns:
140 The rectangle measurements in pixels.
141 """
142 x, y, u = xyu
143 if u == gws.Uom.px:
144 return xyu
145 if u == gws.Uom.mm:
146 return mm_to_px(x, ppi), mm_to_px(y, ppi), gws.Uom.px
147 raise ValueError(f'invalid unit {u!r}')
150##
153def px_to_mm(x: _number, ppi: int) -> float:
154 """Converts pixel to millimetres.
156 Args:
157 x: Amount of pixels.
158 ppi: Pixel per inch.
160 Returns:
161 Amount of millimetres.
162 """
163 return x * (MM_PER_IN / ppi)
166def to_mm(xu: gws.UomValue, ppi: int) -> gws.UomValue:
167 """Converts a measurement of any unit to millimetres.
169 Args:
170 xu: A measurement to convert.
171 ppi: Pixels per inch.
173 Returns:
174 A measurement.
175 """
176 x, u = xu
177 if u == gws.Uom.mm:
178 return xu
179 if u == gws.Uom.px:
180 return px_to_mm(x, ppi), gws.Uom.mm
181 raise ValueError(f'invalid unit {u!r}')
184def size_px_to_mm(xy: gws.Size, ppi: int) -> gws.Size:
185 """Converts a rectangle description in pixel to millimetres.
187 Args:
188 xy: A rectangle measurements in pixels.
189 ppi: Pixel per inch
191 Returns:
192 The rectangle measurements in millimetres.
193 """
194 x, y = xy
195 return px_to_mm(x, ppi), px_to_mm(y, ppi)
198def size_to_mm(xyu: gws.UomSize, ppi: int) -> gws.UomSize:
199 """Converts a rectangle description of any unit to millimetres.
201 Args:
202 xyu: A rectangle measurements with its unit.
203 ppi: Pixels per inch.
205 Returns:
206 The rectangle measurements in millimetres.
207 Raises:
208 ``ValueError``: if the unit is invalid.
209 """
210 x, y, u = xyu
211 if u == gws.Uom.mm:
212 return xyu
213 if u == gws.Uom.px:
214 return px_to_mm(x, ppi), px_to_mm(y, ppi), gws.Uom.mm
215 raise ValueError(f'invalid unit {u!r}')
218def to_str(xu: gws.UomValue) -> str:
219 """Converts a to a string.
221 Args:
222 xu: A measurement to convert.
224 Returns:
225 The input tuple as a string, like '5mm'."""
226 x, u = xu
227 sx = str(int(x)) if (x % 1 == 0) else str(x)
228 return sx + str(u)
231##
234_unit_re = re.compile(r"""(?x)
235 ^
236 (?P<number>
237 -?
238 (\d+ (\.\d*)? )
239 |
240 (\.\d+)
241 )
242 (?P<unit> \s* [a-zA-Z]*)
243 $
244""")
247def parse(val: str | int | float | tuple | list, default_unit: gws.Uom = None) -> gws.UomValue:
248 """Parse a measurement in the string or numeric form.
250 Args:
251 val: A measurement to parse (e.g. '5mm', 5, [5, 'mm']).
252 default_unit: Default unit.
254 Raises:
255 ``ValueError``: if the unit is missing, if the formatting is wrong or if the unit is invalid.
256 """
257 if isinstance(val, (list, tuple)):
258 if len(val) == 2:
259 return parse(f'{val[0]}{val[1]}')
260 raise ValueError(f'invalid format: {val!r}')
262 if isinstance(val, (int, float)):
263 if not default_unit:
264 raise ValueError(f'missing unit: {val!r}')
265 return val, default_unit
267 val = gws.u.to_str(val).strip()
268 m = _unit_re.match(val)
269 if not m:
270 raise ValueError(f'invalid format: {val!r}')
272 n = float(m.group('number'))
273 u = getattr(gws.Uom, m.group('unit').strip().lower(), None)
275 if not u:
276 if not default_unit:
277 raise ValueError(f'invalid unit: {val!r}')
278 return n, default_unit
280 return n, u
283def parse_point(val: str | tuple | list) -> gws.UomPoint:
284 """Parse a point in the string or numeric form.
286 Args:
287 val: A point to parse, either a string '1mm 2mm' or a list [1, 2, 'mm'].
289 Raises:
290 ``ValueError``: if the point is invalid.
291 """
293 v = gws.u.to_list(val)
295 if len(v) == 3:
296 v = [f'{v[0]}{v[2]}', f'{v[1]}{v[2]}']
298 if len(v) == 2:
299 n1, u1 = parse(v[0])
300 n2, u2 = parse(v[1])
301 if u1 != u2:
302 raise ValueError(f'invalid point units: {u1!r} != {u2!r}')
303 return n1, n2, u1
305 raise ValueError(f'invalid point: {val!r}')
308def parse_extent(val: str | tuple | list) -> gws.UomExtent:
309 """Parse an extent in the string or numeric form.
311 Args:
312 val: An extent to parse, either a string '1mm 2mm 3mm 4mm' or a list [1, 2, 3, 4, 'mm'].
314 Raises:
315 ``ValueError``: if the extent is invalid.
316 """
318 v = gws.u.to_list(val)
320 if len(v) == 5:
321 v = [f'{v[0]}{v[4]}', f'{v[1]}{v[2]}', f'{v[2]}{v[4]}', f'{v[3]}{v[4]}']
323 if len(v) == 4:
324 n1, u1 = parse(v[0])
325 n2, u2 = parse(v[1])
326 n3, u3 = parse(v[2])
327 n4, u4 = parse(v[3])
328 if u1 != u2 or u1 != u3 or u1 != u4:
329 raise ValueError(f'invalid extent units: {u1!r} != {u2!r} != {u3!r} != {u4!r}')
330 return n1, n2, n3, n4, u1
332 raise ValueError(f'invalid extent: {val!r}')