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 22:59 +0200

1import re 

2 

3import gws 

4 

5MM_PER_IN = 25.4 

6"""Conversion factor from inch to millimetre""" 

7 

8PT_PER_IN = 72 

9"""Conversion factor from inch to points""" 

10 

11OGC_M_PER_PX = 0.00028 

12"""OGC meter per pixel (OGC 06-042, 7.2.4.6.9: 1px = 0.28mm).""" 

13 

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""" 

16 

17PDF_DPI = 96 

18"""Dots per inch in a pdf file""" 

19 

20# 1 centimeter precision 

21 

22DEFAULT_PRECISION = { 

23 gws.Uom.deg: 7, 

24 gws.Uom.m: 2, 

25} 

26 

27_number = int | float 

28 

29 

30def scale_to_res(x: _number) -> float: 

31 """Converts the scale to the user's resolution. 

32 

33 Args: 

34 x: Scale. 

35 

36 Returns: 

37 Resolution in pixel. 

38 """ 

39 # return round(x * OGC_M_PER_PX, 4) 

40 return x * OGC_M_PER_PX 

41 

42 

43def res_to_scale(x: _number) -> int: 

44 """Converts the user's resolution to the scale. 

45 

46 Args: 

47 x: Resolution in pixel per inch. 

48 

49 Returns: 

50 Scale. 

51 """ 

52 return int(x / OGC_M_PER_PX) 

53 

54 

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# 

84 

85## 

86 

87 

88def mm_to_px(x: _number, ppi: int) -> float: 

89 """Converts millimetres to pixel. 

90 

91 Args: 

92 x: Millimetres. 

93 ppi: Pixels per inch. 

94 

95 Returns: 

96 Amount of pixels.""" 

97 return x * (ppi / MM_PER_IN) 

98 

99 

100def to_px(xu: gws.UomValue, ppi: int) -> gws.UomValue: 

101 """Converts a measurement to amount of pixels. 

102 

103 Args: 

104 xu: A measurement to convert to pixels. 

105 ppi: Pixels per inch. 

106 

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}') 

116 

117 

118def size_mm_to_px(xy: gws.Size, ppi: int) -> gws.Size: 

119 """Converts a rectangle description in millimetres to pixels. 

120 

121 Args: 

122 xy: A rectangle measurements in mm. 

123 ppi: Pixels per inch. 

124 

125 Returns: 

126 A rectangle in pixel. 

127 """ 

128 x, y = xy 

129 return mm_to_px(x, ppi), mm_to_px(y, ppi) 

130 

131 

132def size_to_px(xyu: gws.UomSize, ppi: int) -> gws.UomSize: 

133 """Converts a rectangle description of any unit to pixels. 

134 

135 Args: 

136 xyu: A rectangle measurements with its unit. 

137 ppi: Pixels per inch. 

138 

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}') 

148 

149 

150## 

151 

152 

153def px_to_mm(x: _number, ppi: int) -> float: 

154 """Converts pixel to millimetres. 

155 

156 Args: 

157 x: Amount of pixels. 

158 ppi: Pixel per inch. 

159 

160 Returns: 

161 Amount of millimetres. 

162 """ 

163 return x * (MM_PER_IN / ppi) 

164 

165 

166def to_mm(xu: gws.UomValue, ppi: int) -> gws.UomValue: 

167 """Converts a measurement of any unit to millimetres. 

168 

169 Args: 

170 xu: A measurement to convert. 

171 ppi: Pixels per inch. 

172 

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}') 

182 

183 

184def size_px_to_mm(xy: gws.Size, ppi: int) -> gws.Size: 

185 """Converts a rectangle description in pixel to millimetres. 

186 

187 Args: 

188 xy: A rectangle measurements in pixels. 

189 ppi: Pixel per inch 

190 

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) 

196 

197 

198def size_to_mm(xyu: gws.UomSize, ppi: int) -> gws.UomSize: 

199 """Converts a rectangle description of any unit to millimetres. 

200 

201 Args: 

202 xyu: A rectangle measurements with its unit. 

203 ppi: Pixels per inch. 

204 

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}') 

216 

217 

218def to_str(xu: gws.UomValue) -> str: 

219 """Converts a to a string. 

220 

221 Args: 

222 xu: A measurement to convert. 

223 

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) 

229 

230 

231## 

232 

233 

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""") 

245 

246 

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. 

249 

250 Args: 

251 val: A measurement to parse (e.g. '5mm', 5, [5, 'mm']). 

252 default_unit: Default unit. 

253 

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}') 

261 

262 if isinstance(val, (int, float)): 

263 if not default_unit: 

264 raise ValueError(f'missing unit: {val!r}') 

265 return val, default_unit 

266 

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}') 

271 

272 n = float(m.group('number')) 

273 u = getattr(gws.Uom, m.group('unit').strip().lower(), None) 

274 

275 if not u: 

276 if not default_unit: 

277 raise ValueError(f'invalid unit: {val!r}') 

278 return n, default_unit 

279 

280 return n, u 

281 

282 

283def parse_point(val: str | tuple | list) -> gws.UomPoint: 

284 """Parse a point in the string or numeric form. 

285 

286 Args: 

287 val: A point to parse, either a string '1mm 2mm' or a list [1, 2, 'mm']. 

288 

289 Raises: 

290 ``ValueError``: if the point is invalid. 

291 """ 

292 

293 v = gws.u.to_list(val) 

294 

295 if len(v) == 3: 

296 v = [f'{v[0]}{v[2]}', f'{v[1]}{v[2]}'] 

297 

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 

304 

305 raise ValueError(f'invalid point: {val!r}') 

306 

307 

308def parse_extent(val: str | tuple | list) -> gws.UomExtent: 

309 """Parse an extent in the string or numeric form. 

310 

311 Args: 

312 val: An extent to parse, either a string '1mm 2mm 3mm 4mm' or a list [1, 2, 3, 4, 'mm']. 

313 

314 Raises: 

315 ``ValueError``: if the extent is invalid. 

316 """ 

317 

318 v = gws.u.to_list(val) 

319 

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]}'] 

322 

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 

331 

332 raise ValueError(f'invalid extent: {val!r}')