Coverage for pygeodesy/resections.py: 97%

366 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-04-25 13:15 -0400

1 

2# -*- coding: utf-8 -*- 

3 

4u'''3-Point resection functions L{cassini}, L{collins5}, L{pierlot}, L{pierlotx} and 

5L{tienstra7}, survey functions L{snellius3} and L{wildberger3} and triangle functions 

6L{triAngle}, L{triAngle5}, L{triSide}, L{triSide2} and L{triSide4}. 

7 

8@note: Functions L{pierlot} and L{pierlotx} are transcoded to Python with permission from 

9 U{triangulationPierlot<http://www.Telecom.ULg.ac.BE/triangulation/doc/total_8c.html>} and 

10 U{Pierlot<http://www.Telecom.ULg.ac.BE/publi/publications/pierlot/Pierlot2014ANewThree>}. 

11''' 

12# make sure int/int division yields float quotient 

13from __future__ import division as _; del _ # PYCHOK semicolon 

14 

15from pygeodesy.basics import map1, map2, _zip, _ALL_LAZY, typename 

16from pygeodesy.constants import EPS, EPS0, EPS02, INT0, PI, PI2, PI_2, PI_4, \ 

17 _0_0, _0_5, _1_0, _N_1_0, _2_0, _N_2_0, _4_0, \ 

18 _16_0, _180_0, _360_0, isnear0, _over, _umod_360 

19from pygeodesy.errors import _and, _or, TriangleError, _ValueError, _xcallable, \ 

20 _xkwds, _xkwds_pop2 

21from pygeodesy.fmath import favg, Fdot, fidw, fmean, hypot, hypot2_ 

22from pygeodesy.fsums import _Fsumf_, fsumf_, fsum1, fsum1f_ 

23# from pygeodesy.internals import typename # from .basics 

24from pygeodesy.interns import _a_, _A_, _area_, _b_, _B_, _c_, _C_, _coincident_, \ 

25 _colinear_, _d_, _invalid_, _negative_, _not_, \ 

26 _rIn_, _SPACE_ 

27# from pygeodesy.lazily import _ALL_LAZY # from .basics 

28from pygeodesy.named import _NamedTuple, _Pass, Fmt 

29# from pygeodesy.streprs import Fmt # from .named 

30from pygeodesy.units import Degrees, Distance, Radians 

31from pygeodesy.utily import acos1, asin1, atan2, sincos2, sincos2_, \ 

32 sincos2d, sincos2d_ 

33from pygeodesy.vector3d import _otherV3d, Vector3d 

34 

35from math import cos, degrees, fabs, radians, sin, sqrt 

36 

37__all__ = _ALL_LAZY.resections 

38__version__ = '25.04.14' 

39 

40_concyclic_ = 'concyclic' 

41_PA_ = 'PA' 

42_PB_ = 'PB' 

43_PC_ = 'PC' 

44_pointH_ = 'pointH' 

45_pointP_ = 'pointP' 

46_positive_ = 'positive' 

47_radA_ = 'radA' 

48_radB_ = 'radB' 

49_radC_ = 'radC' 

50 

51 

52class Collins5Tuple(_NamedTuple): 

53 '''5-Tuple C{(pointP, pointH, a, b, c)} with survey C{pointP}, auxiliary 

54 C{pointH}, each an instance of B{C{pointA}}'s (sub-)class and triangle 

55 sides C{a}, C{b} and C{c} in C{meter}, conventionally. 

56 ''' 

57 _Names_ = (_pointP_, _pointH_, _a_, _b_, _c_) 

58 _Units_ = (_Pass, _Pass, Distance, Distance, Distance) 

59 

60 

61class ResectionError(_ValueError): 

62 '''Error raised for issues in L{pygeodesy.resections}. 

63 ''' 

64 pass 

65 

66 

67class Survey3Tuple(_NamedTuple): 

68 '''3-Tuple C{(PA, PB, PC)} with distance from survey point C{P} to each of 

69 the triangle corners C{A}, C{B} and C{C} in C{meter}, conventionally. 

70 ''' 

71 _Names_ = (_PA_, _PB_, _PC_) 

72 _Units_ = ( Distance, Distance, Distance) 

73 

74 

75class Tienstra7Tuple(_NamedTuple): 

76 '''7-Tuple C{(pointP, A, B, C, a, b, c)} with survey C{pointP}, interior 

77 triangle angles C{A}, C{B} and C{C} in C{degrees} and triangle sides 

78 C{a}, C{b} and C{c} in C{meter}, conventionally. 

79 ''' 

80 _Names_ = (_pointP_, _A_, _B_, _C_, _a_, _b_, _c_) 

81 _Units_ = (_Pass, Degrees, Degrees, Degrees, Distance, Distance, Distance) 

82 

83 

84class TriAngle5Tuple(_NamedTuple): 

85 '''5-Tuple C{(radA, radB, radC, rIn, area)} with the interior angles at 

86 triangle corners C{A}, C{B} and C{C} in C{radians}, the C{InCircle} 

87 radius C{rIn} aka C{inradius} in C{meter} and the triangle C{area} 

88 in C{meter} I{squared}, conventionally. 

89 ''' 

90 _Names_ = (_radA_, _radB_, _radC_, _rIn_, _area_) 

91 _Units_ = ( Radians, Radians, Radians, Distance, _Pass) 

92 

93 

94class TriSide2Tuple(_NamedTuple): 

95 '''2-Tuple C{(a, radA)} with triangle side C{a} in C{meter}, conventionally 

96 and angle C{radA} at the opposite triangle corner in C{radians}. 

97 ''' 

98 _Names_ = (_a_, _radA_) 

99 _Units_ = ( Distance, Radians) 

100 

101 

102class TriSide4Tuple(_NamedTuple): 

103 '''4-Tuple C{(a, b, radC, d)} with interior angle C{radC} at triangle corner 

104 C{C} in C{radians}with the length of triangle sides C{a} and C{b} and 

105 with triangle height C{d} perpendicular to triangle side C{c}, in the 

106 same units as triangle sides C{a} and C{b}. 

107 ''' 

108 _Names_ = (_a_, _b_, _radC_, _d_) 

109 _Units_ = ( Distance, Distance, Radians, Distance) 

110 

111 

112def _ABC3(useZ, pointA, pointB, pointC): 

113 '''(INTERNAL) Helper for L{cassini} and L{tienstra7}. 

114 ''' 

115 return (_otherV3d(useZ=useZ, pointA=pointA), 

116 _otherV3d(useZ=useZ, pointB=pointB), 

117 _otherV3d(useZ=useZ, pointC=pointC)) 

118 

119 

120def _B3(useZ, point1, point2, point3): 

121 '''(INTERNAL) Helper for L{pierlot} and L{pierlotx}. 

122 ''' 

123 return (_otherV3d(useZ=useZ, point1=point1), 

124 _otherV3d(useZ=useZ, point2=point2), 

125 _otherV3d(useZ=useZ, point3=point3)) 

126 

127 

128def cassini(pointA, pointB, pointC, alpha, beta, useZ=False, **Clas_and_kwds): 

129 '''3-Point resection using U{Cassini<https://NL.WikiPedia.org/wiki/Achterwaartse_insnijding>}'s method. 

130 

131 @arg pointA: First point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple}, 

132 C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}). 

133 @arg pointB: Second point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple}, 

134 C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}). 

135 @arg pointC: Center point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple}, 

136 C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}). 

137 @arg alpha: Angle subtended by triangle side B{C{pointA}} to B{C{pointC}} 

138 (C{degrees}, non-negative). 

139 @arg beta: Angle subtended by triangle side B{C{pointB}} to B{C{pointC}} 

140 (C{degrees}, non-negative). 

141 @kwarg useZ: If C{True}, use and interpolate the Z component, otherwise 

142 force C{z=INT0} (C{bool}). 

143 @kwarg Clas_and_kwds: Optional class C{B{Clas}=B{pointA}.classof} to 

144 return the survey point with optionally other B{C{Clas}} 

145 keyword arguments to instantiate the survey point. 

146 

147 @note: Typically, B{C{pointC}} is between B{C{pointA}} and B{C{pointB}}. 

148 

149 @return: The survey point, an instance of B{C{Clas}} or B{C{pointA}}'s 

150 (sub-)class. 

151 

152 @raise ResectionError: Near-coincident, -colinear or -concyclic points 

153 or negative or invalid B{C{alpha}} or B{C{beta}}. 

154 

155 @raise TypeError: Invalid B{C{pointA}}, B{C{pointB}} or B{C{pointM}}. 

156 

157 @see: U{Three Point Resection Problem<https://Dokumen.tips/documents/ 

158 three-point-resection-problem-introduction-kaestner-burkhardt-method.html>} 

159 and functions L{collins5}, L{pierlot}, L{pierlotx} and L{tienstra7}. 

160 ''' 

161 

162 def _H(A, C, sa): 

163 s, c = sincos2d(sa) 

164 if isnear0(s): 

165 raise ValueError(_or(_coincident_, _colinear_)) 

166 t = s, c, c 

167 x = Fdot(t, A.x, C.y, -A.y).fover(s) 

168 y = Fdot(t, A.y, -C.x, A.x).fover(s) 

169 return x, y 

170 

171 A, B, C = _ABC3(useZ, pointA, pointB, pointC) 

172 try: 

173 sa, sb = map1(float, alpha, beta) 

174 if min(sa, sb) < 0: 

175 raise ValueError(_negative_) 

176 if fsumf_(_360_0, -sa, -sb) < EPS0: 

177 raise ValueError() 

178 

179 x1, y1 = _H(A, C, sa) 

180 x2, y2 = _H(B, C, -sb) 

181 

182 x = x1 - x2 

183 y = y1 - y2 

184 if isnear0(x) or isnear0(y): 

185 raise ValueError(_SPACE_(_concyclic_, (x, y))) 

186 

187 m = y / x 

188 n = x / y 

189 N = n + m 

190 if isnear0(N): 

191 raise ValueError(_SPACE_(_concyclic_, (m, n, N))) 

192 

193 t = n, m, _1_0, _N_1_0 

194 x = Fdot(t, C.x, x1, C.y, y1).fover(N) 

195 y = Fdot(t, y1, C.y, C.x, x1).fover(N) 

196 z = _zidw(x, y, useZ, A, B, C) 

197 return _Clas(cassini, pointA, Clas_and_kwds, x, y, z) 

198 

199 except (TypeError, ValueError) as x: 

200 raise ResectionError(pointA=pointA, pointB=pointB, pointC=pointC, 

201 alpha=alpha, beta=beta, cause=x) 

202 

203 

204def _Clas(which, point, Clas_and_kwds, *args): 

205 '''(INTERNAL) Return a C{B{Clas}=point.classof} survey point. 

206 ''' 

207 Clas, kwds = _xkwds_pop2(Clas_and_kwds, Clas=point.classof) 

208 return Clas(*args, **_xkwds(kwds, name=typename(which))) 

209 

210 

211def collins5(pointA, pointB, pointC, alpha, beta, useZ=False, **Clas_and_kwds): 

212 '''3-Point resection using U{Collins<https://Dokumen.tips/documents/ 

213 three-point-resection-problem-introduction-kaestner-burkhardt-method.html>}' method. 

214 

215 @arg pointA: First point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple}, 

216 C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}). 

217 @arg pointB: Second point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple}, 

218 C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}). 

219 @arg pointC: Center point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple}, 

220 C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}). 

221 @arg alpha: Angle subtended by triangle side C{b} from B{C{pointA}} to 

222 B{C{pointC}} (C{degrees}, non-negative). 

223 @arg beta: Angle subtended by triangle side C{a} from B{C{pointB}} to 

224 B{C{pointC}} (C{degrees}, non-negative). 

225 @kwarg useZ: If C{True}, use and interpolate the Z component, otherwise 

226 force C{z=INT0} (C{bool}). 

227 @kwarg Clas_and_kwds: Optional class C{B{Clas}=B{pointA}.classof} to 

228 return the survey point with optionally other B{C{Clas}} 

229 keyword arguments to instantiate the survey point. 

230 

231 @note: Typically, B{C{pointC}} is between B{C{pointA}} and B{C{pointB}}. 

232 

233 @return: L{Collins5Tuple}C{(pointP, pointH, a, b, c)} with survey C{pointP}, 

234 auxiliary C{pointH}, each an instance of B{C{Clas}} or B{C{pointA}}'s 

235 (sub-)class and triangle sides C{a}, C{b} and C{c} in C{meter}, 

236 conventionally. 

237 

238 @raise ResectionError: Near-coincident, -colinear or -concyclic points 

239 or negative or invalid B{C{alpha}} or B{C{beta}}. 

240 

241 @raise TypeError: Invalid B{C{pointA}}, B{C{pointB}} or B{C{pointM}}. 

242 

243 @see: U{Collins' methode<https://NL.WikiPedia.org/wiki/Achterwaartse_insnijding>} 

244 and functions L{cassini}, L{pierlot}, L{pierlotx} and L{tienstra7}. 

245 ''' 

246 

247 def _azi_len2(A, B, pi2=PI2): 

248 v = B.minus(A) 

249 r = atan2(v.x, v.y) 

250 if r < 0 and pi2: 

251 r += pi2 

252 return r, v.length 

253 

254 def _xyz(d, r, A, B, C, useZ): 

255 s, c = sincos2(r) 

256 x = d * s + A.x # fma(d, s, A.x) 

257 y = d * c + A.y # fma(d, c, A.y) 

258 z = _zidw(x, y, useZ, A, B, C) 

259 return x, y, z 

260 

261 A, B, C = _ABC3(useZ, pointA, pointB, pointC) 

262 try: 

263 ra, rb = radians(alpha), radians(beta) 

264 if min(ra, rb) < 0: 

265 raise ValueError(_negative_) 

266 

267 sra, srH = sin(ra), sin(ra + rb - PI) # rH = PI - ((PI - ra) + (PI - rb)) 

268 if isnear0(sra) or isnear0(srH): 

269 raise ValueError(_or(_coincident_, _colinear_, _concyclic_)) 

270 

271# za, a = _azi_len2(C, B) 

272 zb, b = _azi_len2(C, A) 

273 zc, c = _azi_len2(A, B, 0) 

274 

275# d = c * sin(PI - rb) / srH # B.minus(H).length 

276 d = c * sin(PI - ra) / srH # A.minus(H).length 

277 r = zc + PI - rb # zh = zc + (PI - rb) 

278 H = _xyz(d, r, A, B, C, useZ) 

279 

280 zh, _ = _azi_len2(C, Vector3d(*H)) 

281 

282# d = a * sin(za - zh) / sin(rb) # B.minus(P).length 

283 d = b * sin(zb - zh) / sra # A.minus(P).length 

284 r = zh - ra # zb - PI + (PI - ra - (zb - zh)) 

285 P = _xyz(d, r, A, B, C, useZ) 

286 

287 P = _Clas(collins5, pointA, Clas_and_kwds, *P) 

288 H = _Clas(collins5, pointA, Clas_and_kwds, *H) 

289 a = B.minus(C).length 

290 return Collins5Tuple(P, H, a, b, c, name=typename(collins5)) 

291 

292 except (TypeError, ValueError) as x: 

293 raise ResectionError(pointA=pointA, pointB=pointB, pointC=pointC, 

294 alpha=alpha, beta=beta, cause=x) 

295 

296 

297def pierlot(point1, point2, point3, alpha12, alpha23, useZ=False, eps=EPS, 

298 **Clas_and_kwds): 

299 '''3-Point resection using U{Pierlot<http://www.Telecom.ULg.ac.BE/publi/publications/ 

300 pierlot/Pierlot2014ANewThree>}'s method C{ToTal} with I{approximate} limits for 

301 the (pseudo-)singularities. 

302 

303 @arg point1: First point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple}, 

304 C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}). 

305 @arg point2: Second point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple}, 

306 C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}). 

307 @arg point3: Third point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple}, 

308 C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}). 

309 @arg alpha12: Angle subtended from B{C{point1}} to B{C{point2}} or 

310 B{C{alpha2 - alpha1}} (C{degrees}). 

311 @arg alpha23: Angle subtended from B{C{point2}} to B{C{point3}} or 

312 B{C{alpha3 - alpha2}}(C{degrees}). 

313 @kwarg useZ: If C{True}, interpolate the survey point's Z component, 

314 otherwise use C{z=INT0} (C{bool}). 

315 @kwarg eps: Tolerance for C{cot}angent (pseudo-)singularities (C{float}). 

316 @kwarg Clas_and_kwds: Optional class C{B{Clas}=B{point1}.classof} to 

317 return the survey point with optionally other B{C{Clas}} 

318 keyword arguments to instantiate the survey point. 

319 

320 @note: Typically, B{C{point1}}, B{C{point2}} and B{C{point3}} are ordered 

321 by angle, modulo 360, counter-clockwise. 

322 

323 @return: The survey (or robot) point, an instance of B{C{Clas}} or B{C{point1}}'s 

324 (sub-)class. 

325 

326 @raise ResectionError: Near-coincident, -colinear or -concyclic points 

327 or invalid B{C{alpha12}} or B{C{alpha23}} or 

328 non-positive B{C{eps}}. 

329 

330 @raise TypeError: Invalid B{C{point1}}, B{C{point2}} or B{C{point3}}. 

331 

332 @see: I{Pierlot}'s C function U{triangulationPierlot<http://www.Telecom.ULg.ac.BE/ 

333 triangulation/doc/total_8c_source.html>}, U{V. Pierlot, M. Van Droogenbroeck, 

334 "A New Three Object Triangulation Algorithm for Mobile Robot Positioning" 

335 <https://ORBi.ULiege.BE/bitstream/2268/157469/1/Pierlot2014ANewThree.pdf>}, 

336 U{Vincent Pierlot, Marc Van Droogenbroeck, "18 Triangulation Algorithms for 2D 

337 Positioning (also known as the Resection Problem)"<http://www.Telecom.ULg.ac.BE/ 

338 triangulation>} and functions L{pierlotx}, L{cassini}, L{collins5} and L{tienstra7}. 

339 ''' 

340 

341 def _cot(s, c): # -eps < I{approximate} cotangent < eps 

342 if eps > 0: 

343 return c / (min(s, -eps) if s < 0 else max(s, eps)) 

344 t = Fmt.PARENSPACED(eps=eps) 

345 raise ValueError(_SPACE_(t, _not_, _positive_)) 

346 

347 B1, B2, B3 = _B3(useZ, point1, point2, point3) 

348 try: 

349 xyz = _pierlot3(B1, B2, B3, alpha12, alpha23, useZ, _cot) 

350 return _Clas(pierlot, point1, Clas_and_kwds, *xyz) 

351 

352 except (TypeError, ValueError) as x: 

353 raise ResectionError(point1=point1, point2=point2, point3=point3, 

354 alpha12=alpha12, alpha23=alpha23, eps=eps, cause=x) 

355 

356 

357def _pierlot3(B1, B2, B3, a12, a23, useZ, _cot): 

358 '''(INTERNAL) Shared L{pierlot} and L{pierlotx}. 

359 ''' 

360 x1_, y1_, _ = B1.minus(B2).xyz3 

361 x3_, y3_, _ = B3.minus(B2).xyz3 

362 

363 s12, c12, s23, c23 = sincos2d_(a12, a23) 

364 # cot31 = (1 - cot12 * cot23) / (cot12 + cot32) 

365 # = (1 - c12 / s12 * c23 / s23) / (c12 / s12 + c23 / s23) 

366 # = (1 - (c12 * c23) / (s12 * s23)) / (c12 * s23 + s12 * c23) / (s12 * s23) 

367 # = (s12 * s23 - c12 * c23) / (c12 * s23 + s12 * c23) 

368 # = c31 / s31 

369 cot31 = _cot(fsum1f_(c12 * s23, s12 * c23), # s31 

370 fsum1f_(s12 * s23, -c12 * c23)) # c31 

371 

372 K = _Fsumf_(x3_ * x1_, cot31 * (y3_ * x1_), 

373 y3_ * y1_, -cot31 * (x3_ * y1_)) 

374 if K: 

375 cot12 = _cot(s12, c12) 

376 cot23 = _cot(s23, c23) 

377 

378 # x12 = x1_ + cot12 * y1_ 

379 # y12 = y1_ - cot12 * x1_ 

380 

381 # x23 = x3_ - cot23 * y3_ 

382 # y23 = y3_ + cot23 * x3_ 

383 

384 # x31 = x3_ + x1_ + cot31 * (y3_ - y1_) 

385 # y31 = y3_ + y1_ - cot31 * (x3_ - x1_) 

386 

387 # x12 - x23 = x1_ + cot12 * y1_ - x3_ + cot23 * y3_ 

388 X12_23 = _Fsumf_(x1_, cot12 * y1_, -x3_, cot23 * y3_) 

389 # y12 - y23 = y1_ - cot12 * x1_ - y3_ - cot23 * x3_ 

390 Y12_23 = _Fsumf_(y1_, -cot12 * x1_, -y3_, -cot23 * x3_) 

391 

392 # x31 - x23 = x3_ + x1_ + cot31 * (y3_ - y1_) - x3_ + cot23 * y3_ 

393 # = x1_ + cot31 * y3_ - cot31 * y1_ + cot23 * y3_ 

394 X31_23 = _Fsumf_(x1_, -cot31 * y1_, cot31 * y3_, cot23 * y3_) 

395 # y31 - y23 = y3_ + y1_ - cot31 * (x3_ - x1_) - y3_ - cot23 * x3_ 

396 # = y1_ - cot31 * x3_ + cot31 * x1_ - cot23 * x3_ 

397 Y31_23 = _Fsumf_(y1_, cot31 * x1_, -cot31 * x3_, -cot23 * x3_) 

398 

399 # d = (x12 - x23) * (y23 - y31) + (x31 - x23) * (y12 - y23) 

400 # = (x31 - x23) * (y12 - y23) - (x12 - x23) * (y31 - y23) 

401 # x = (d * B2.x + K * Y12_23).fover(d) 

402 # y = (d * B2.y - K * X12_23).fover(d) 

403 x, y = _pierlotxy2(B2, -K, Y12_23, X12_23, (X31_23 * Y12_23 - 

404 X12_23 * Y31_23)) 

405 else: 

406 x, y, _ = B2.xyz3 

407 return x, y, _zidw(x, y, useZ, B1, B2, B3) 

408 

409 

410def pierlotx(point1, point2, point3, alpha1, alpha2, alpha3, useZ=False, 

411 **Clas_and_kwds): 

412 '''3-Point resection using U{Pierlot<http://www.Telecom.ULg.ac.BE/publi/ 

413 publications/pierlot/Pierlot2014ANewThree>}'s method C{ToTal} with 

414 I{exact} limits for the (pseudo-)singularities. 

415 

416 @arg point1: First point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple}, 

417 C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}). 

418 @arg point2: Second point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple}, 

419 C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}). 

420 @arg point3: Third point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple}, 

421 C{Vector4Tuple} or C{Vector2Tuple} if C{B{useZ}=False}). 

422 @arg alpha1: Angle at B{C{point1}} (C{degrees}, counter-clockwise). 

423 @arg alpha2: Angle at B{C{point2}} (C{degrees}, counter-clockwise). 

424 @arg alpha3: Angle at B{C{point3}} (C{degrees}, counter-clockwise). 

425 @kwarg useZ: If C{True}, interpolate the survey point's Z component, 

426 otherwise use C{z=INT0} (C{bool}). 

427 @kwarg Clas_and_kwds: Optional class C{B{Clas}=B{point1}.classof} to 

428 return the survey point with optionally other B{C{Clas}} 

429 keyword arguments to instantiate the survey point. 

430 

431 @return: The survey (or robot) point, an instance of B{C{Clas}} or 

432 B{C{point1}}'s (sub-)class. 

433 

434 @raise ResectionError: Near-coincident, -colinear or -concyclic points or 

435 invalid B{C{alpha1}}, B{C{alpha2}} or B{C{alpha3}}. 

436 

437 @raise TypeError: Invalid B{C{point1}}, B{C{point2}} or B{C{point3}}. 

438 

439 @see: I{Pierlot}'s C function U{triangulationPierlot2<http://www.Telecom.ULg.ac.BE/ 

440 triangulation/doc/total_8c_source.html>} and function L{pierlot}, L{cassini}, 

441 L{collins5} and L{tienstra7}. 

442 ''' 

443 

444 def _a_z_Bs(Bs, *alphas): 

445 ds = map2(_umod_360, alphas) # 0 <= alphas < 360 

446 ds, Bs = zip(*sorted(_zip(ds, Bs))) # unzip 

447 for p, d, B in _zip(ds, _rotate(ds), Bs): 

448 d -= p # a12 = a2 - a1, ... 

449 z = isnear0(fabs(d) % _180_0) 

450 yield d, z, B 

451 

452 def _cot(s, c): # I{exact} cotangent 

453 try: 

454 return (c / s) # if c else _copysign_0_0(s) 

455 except ZeroDivisionError: 

456 raise ValueError(_or(_coincident_, _colinear_)) 

457 

458 Bs = _B3(useZ, point1, point2, point3) 

459 try: 

460 Cs = [0] # pseudo-global, passing the exception Case 

461 xyz = _pierlotx3(_a_z_Bs(Bs, alpha1, alpha2, alpha3), 

462 useZ, _cot, Cs.append) 

463 return _Clas(pierlotx, point1, Clas_and_kwds, *xyz) 

464 

465 except (TypeError, ValueError) as x: 

466 raise ResectionError(point1=point1, point2=point2, point3=point3, C=Cs.pop(), 

467 alpha1=alpha1, alpha2=alpha2, alpha3=alpha3, cause=x) 

468 

469 

470def _pierlotx3(a_z_Bs, useZ, _cot, Cs): 

471 '''(INTERNAL) Core of L{pierlotx}. 

472 ''' 

473 (a12, z12, B1), \ 

474 (a23, z23, B2), \ 

475 (a31, z31, B3) = a_z_Bs 

476 if z12 and not z23: 

477 Cs(1) 

478 elif z23 and not z31: 

479 Cs(2) 

480 a23, B1, B2, B3 = a31, B2, B3, B1 

481 elif z31 and not z12: 

482 Cs(3) 

483 a23, B2, B3 = a12, B3, B2 

484 else: 

485 Cs(4) 

486 return _pierlot3(B1, B2, B3, a12, a23, useZ, _cot) 

487 

488 x1_, y1_, _ = B1.minus(B3).xyz3 

489 x2_, y2_, _ = B2.minus(B3).xyz3 

490 

491 K = _Fsumf_(y1_ * x2_, -x1_ * y2_) 

492 if K: 

493 cot23 = _cot(*sincos2d(a23)) 

494 

495 # x23 = x2_ + cot23 * y2_ 

496 # y23 = y2_ - cot23 * x2_ 

497 

498 # x31 = x1_ + cot23 * y1_ 

499 # y31 = y1_ - cot23 * x1_ 

500 

501 # x31 - x23 = x1_ + cot23 * y1_ - x2_ - cot23 * y2_ 

502 X31_23 = _Fsumf_(x1_, cot23 * y1_, -x2_, -cot23 * y2_) 

503 # y31 - y23 = y1_ - cot23 * x1_ - y2_ + cot23 * x2_ 

504 Y31_23 = _Fsumf_(y1_, -cot23 * x1_, -y2_, cot23 * x2_) 

505 

506 # d = (x31 - x23) * (x2_ - x1_) + (y31 - y23) * (y2_ - y1_) 

507 # x = (D * B3.x - K * Y31_23).fover(d) 

508 # y = (D * B3.y + K * X31_23).fover(d) 

509 x, y = _pierlotxy2(B3, K, Y31_23, X31_23, (X31_23 * _Fsumf_(x2_, -x1_) + 

510 Y31_23 * _Fsumf_(y2_, -y1_))) 

511 else: 

512 x, y, _ = B3.xyz3 

513 return x, y, _zidw(x, y, useZ, B1, B2, B3) 

514 

515 

516def _pierlotxy2(B, K, X, Y, D): 

517 '''(INTERNAL) Helper for C{_pierlot3} and C{_pierlotx3}. 

518 ''' 

519 d = float(D) 

520 if isnear0(d): 

521 raise ValueError(_or(_coincident_, _colinear_, _concyclic_)) 

522 x = (D * B.x - K * X).fover(d) 

523 y = (D * B.y + K * Y).fover(d) 

524 return x, y 

525 

526 

527def _rotate(xs, n=1): 

528 '''Rotate list or tuple C{xs} by C{n} items, right if C{n > 0} else left. 

529 ''' 

530 return xs[n:] + xs[:n] 

531 

532 

533def snellius3(a, b, degC, alpha, beta): 

534 '''Snellius' surveying using U{Snellius Pothenot<https://WikiPedia.org/wiki/Snellius–Pothenot_problem>}. 

535 

536 @arg a: Length of the triangle side between corners C{B} and C{C} and opposite of 

537 triangle corner C{A} (C{scalar}, non-negative C{meter}, conventionally). 

538 @arg b: Length of the triangle side between corners C{C} and C{A} and opposite of 

539 triangle corner C{B} (C{scalar}, non-negative C{meter}, conventionally). 

540 @arg degC: Angle at triangle corner C{C}, opposite triangle side C{c} (non-negative C{degrees}). 

541 @arg alpha: Angle subtended by triangle side B{C{b}} (non-negative C{degrees}). 

542 @arg beta: Angle subtended by triangle side B{C{a}} (non-negative C{degrees}). 

543 

544 @return: L{Survey3Tuple}C{(PA, PB, PC)} with distance from survey point C{P} to 

545 each of the triangle corners C{A}, C{B} and C{C}, same units as triangle 

546 sides B{C{a}}, B{C{b}} and B{C{c}}. 

547 

548 @raise TriangleError: Invalid B{C{a}}, B{C{b}} or B{C{degC}} or negative B{C{alpha}} 

549 or B{C{beta}}. 

550 

551 @see: Function L{wildberger3}. 

552 ''' 

553 try: 

554 a, b, degC, alpha, beta = t = map1(float, a, b, degC, alpha, beta) 

555 if min(t) < 0: 

556 raise ValueError(_negative_) 

557 ra, rb, rC = map1(radians, alpha, beta, degC) 

558 

559 r = fsum1f_(ra, rb, rC) * _0_5 

560 k = PI - r 

561 if min(k, r) < 0: 

562 raise ValueError(_or(_coincident_, _colinear_)) 

563 

564 sa, sb = map1(sin, ra, rb) 

565 p = atan2(sa * a, sb * b) 

566 sp, cp, sr, cr = sincos2_(PI_4 - p, r) 

567 p = atan2(sp * sr, cp * cr) 

568 pa = k + p 

569 pb = k - p 

570 

571 if fabs(sb) > fabs(sa): 

572 pc = fabs(a * sin(pb) / sb) 

573 elif sa: 

574 pc = fabs(b * sin(pa) / sa) 

575 else: 

576 raise ValueError(_or(_colinear_, _coincident_)) 

577 

578 pa = _triSide(b, pc, fsumf_(PI, -ra, -pa)) 

579 pb = _triSide(a, pc, fsumf_(PI, -rb, -pb)) 

580 return Survey3Tuple(pa, pb, pc, name=typename(snellius3)) 

581 

582 except (TypeError, ValueError) as x: 

583 raise TriangleError(a=a, b=b, degC=degC, alpha=alpha, beta=beta, cause=x) 

584 

585 

586def tienstra7(pointA, pointB, pointC, alpha, beta=None, gamma=None, 

587 useZ=False, **Clas_and_kwds): 

588 '''3-Point resection using U{Tienstra<https://WikiPedia.org/wiki/Tienstra_formula>}'s formula. 

589 

590 @arg pointA: First point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple}, C{Vector4Tuple} or 

591 C{Vector2Tuple} if C{B{useZ}=False}). 

592 @arg pointB: Second point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple}, C{Vector4Tuple} or 

593 C{Vector2Tuple} if C{B{useZ}=False}). 

594 @arg pointC: Third point (C{Cartesian}, L{Vector3d}, C{Vector3Tuple}, C{Vector4Tuple} or 

595 C{Vector2Tuple} if C{B{useZ}=False}). 

596 @arg alpha: Angle subtended by triangle side C{a} from B{C{pointB}} to B{C{pointC}} 

597 (C{degrees}, non-negative). 

598 @kwarg beta: Angle subtended by triangle side C{b} from B{C{pointA}} to B{C{pointC}} 

599 (C{degrees}, non-negative) or C{None} if C{B{gamma} is not None}. 

600 @kwarg gamma: Angle subtended by triangle side C{c} from B{C{pointA}} to B{C{pointB}} 

601 (C{degrees}, non-negative) or C{None} if C{B{beta} is not None}. 

602 @kwarg useZ: If C{True}, use and interpolate the Z component, otherwise force C{z=INT0} 

603 (C{bool}). 

604 @kwarg Clas_and_kwds: Optional class C{B{Clas}=B{pointA}.classof} to return the survey 

605 point with optionally other B{C{Clas}} keyword arguments to instantiate 

606 the survey point. 

607 

608 @note: Points B{C{pointA}}, B{C{pointB}} and B{C{pointC}} are ordered clockwise. 

609 

610 @return: L{Tienstra7Tuple}C{(pointP, A, B, C, a, b, c)} with survey C{pointP}, an 

611 instance of B{C{Clas}} or B{C{pointA}}'s (sub-)class, with triangle angles C{A} 

612 at B{C{pointA}}, C{B} at B{C{pointB}} and C{C} at B{C{pointC}} in C{degrees} 

613 and with triangle sides C{a}, C{b} and C{c} in C{meter}, conventionally. 

614 

615 @raise ResectionError: Near-coincident, -colinear or -concyclic points or sum of 

616 B{C{alpha}}, B{C{beta}} and B{C{gamma}} not C{360} or negative 

617 B{C{alpha}}, B{C{beta}} or B{C{gamma}}. 

618 

619 @raise TypeError: Invalid B{C{pointA}}, B{C{pointB}} or B{C{pointC}}. 

620 

621 @see: U{3-Point Resection Solver<http://MesaMike.org/geocache/GC1B0Q9/tienstra/>}, 

622 U{V. Pierlot, M. Van Droogenbroeck, "A New Three Object Triangulation..." 

623 <http://www.Telecom.ULg.ac.BE/publi/publications/pierlot/Pierlot2014ANewThree/>}, 

624 U{18 Triangulation Algorithms...<http://www.Telecom.ULg.ac.BE/triangulation/>} and 

625 functions L{cassini}, L{collins5}, L{pierlot} and L{pierlotx}. 

626 ''' 

627 

628 def _deg_ks(r, s, ks, N): 

629 if isnear0(fsumf_(PI, r, -s)): # r + (PI2 - s) == PI 

630 raise ValueError(Fmt.PARENSPACED(concyclic=N)) 

631 # k = 1 / (cot(r) - cot(s)) 

632 # = 1 / (cos(r) / sin(r) - cos(s) / sin(s)) 

633 # = 1 / (cos(r) * sin(s) - cos(s) * sin(r)) / (sin(r) * sin(s)) 

634 # = sin(r) * sin(s) / (cos(r) * sin(s) - cos(s) * sin(r)) 

635 sr, cr, ss, cs = sincos2_(r, s) 

636 c = fsum1f_(cr * ss, -cs * sr) 

637 if isnear0(c): 

638 raise ValueError(Fmt.PARENSPACED(cotan=N)) 

639 ks.append(sr * ss / c) 

640 return Degrees(degrees(r), name=N) # C degrees 

641 

642 A, B, C = _ABC3(useZ, pointA, pointB, pointC) 

643 try: 

644 sa, sb, sc = map1(radians, alpha, (beta or 0), (gamma or 0)) 

645 if beta is None: 

646 if gamma is None: 

647 raise ValueError(_and(Fmt.EQUAL(beta=beta), Fmt.EQUAL(gamma=gamma))) 

648 sb = fsumf_(PI2, -sa, -sc) 

649 elif gamma is None: 

650 sc = fsumf_(PI2, -sa, -sb) 

651 else: # subtended angles must add to 360 degrees 

652 r = fsum1f_(sa, sb, sc) 

653 if fabs(r - PI2) > EPS: 

654 raise ValueError(Fmt.EQUAL(sum=degrees(r))) 

655 if min(sa, sb, sc) < 0: 

656 raise ValueError(_negative_) 

657 

658 # triangle sides 

659 a = B.minus(C).length 

660 b = A.minus(C).length 

661 c = A.minus(B).length 

662 

663 ks = [] # 3 Ks and triangle angles 

664 dA = _deg_ks(_triAngle(b, c, a), sa, ks, _A_) 

665 dB = _deg_ks(_triAngle(a, c, b), sb, ks, _B_) 

666 dC = _deg_ks(_triAngle(a, b, c), sc, ks, _C_) 

667 

668 k = fsum1(ks) 

669 if isnear0(k): 

670 raise ValueError(Fmt.EQUAL(K=k)) 

671 x = Fdot(ks, A.x, B.x, C.x).fover(k) 

672 y = Fdot(ks, A.y, B.y, C.y).fover(k) 

673 z = _zidw(x, y, useZ, A, B, C) 

674 

675 P = _Clas(tienstra7, pointA, Clas_and_kwds, x, y, z) 

676 return Tienstra7Tuple(P, dA, dB, dC, a, b, c, name=typename(tienstra7)) 

677 

678 except (TypeError, ValueError) as x: 

679 raise ResectionError(pointA=pointA, pointB=pointB, pointC=pointC, 

680 alpha=alpha, beta=beta, gamma=gamma, cause=x) 

681 

682 

683def triAngle(a, b, c): 

684 '''Compute one angle of a triangle. 

685 

686 @arg a: Adjacent triangle side length (C{scalar}, non-negative 

687 C{meter}, conventionally). 

688 @arg b: Adjacent triangle side length (C{scalar}, non-negative 

689 C{meter}, conventionally). 

690 @arg c: Opposite triangle side length (C{scalar}, non-negative 

691 C{meter}, conventionally). 

692 

693 @return: Angle in C{radians} at triangle corner C{C}, opposite 

694 triangle side B{C{c}}. 

695 

696 @raise TriangleError: Invalid or negative B{C{a}}, B{C{b}} or B{C{c}}. 

697 

698 @see: Functions L{triAngle5} and L{triSide}. 

699 ''' 

700 try: 

701 return _triAngle(a, b, c) 

702 except (TypeError, ValueError) as x: 

703 raise TriangleError(a=a, b=b, c=c, cause=x) 

704 

705 

706def _triAngle(a, b, c): 

707 # (INTERNAL) To allow callers to embellish errors 

708 a, b, c = map1(float, a, b, c) 

709 if a < b: 

710 a, b = b, a 

711 if b < 0 or c < 0: 

712 raise ValueError(_negative_) 

713 if a < EPS0: 

714 raise ValueError(_coincident_) 

715 b_a = b / a 

716 if b_a < EPS0: 

717 raise ValueError(_coincident_) 

718 t = fsumf_(_1_0, b_a**2, -(c / a)**2) / (b_a * _2_0) 

719 return acos1(t) 

720 

721 

722def triAngle5(a, b, c): 

723 '''Compute the angles of a triangle. 

724 

725 @arg a: Length of the triangle side opposite of triangle corner C{A} 

726 (C{scalar}, non-negative C{meter}, conventionally). 

727 @arg b: Length of the triangle side opposite of triangle corner C{B} 

728 (C{scalar}, non-negative C{meter}, conventionally). 

729 @arg c: Length of the triangle side opposite of triangle corner C{C} 

730 (C{scalar}, non-negative C{meter}, conventionally). 

731 

732 @return: L{TriAngle5Tuple}C{(radA, radB, radC, rIn, area)} with angles 

733 C{radA}, C{radB} and C{radC} at triangle corners C{A}, C{B} 

734 and C{C}, all in C{radians}, the C{InCircle} radius C{rIn} 

735 aka C{inradius}, same units as triangle sides B{C{a}}, 

736 B{C{b}} and B{C{c}} and the triangle C{area} in those same 

737 units I{squared}. 

738 

739 @raise TriangleError: Invalid or negative B{C{a}}, B{C{b}} or B{C{c}}. 

740 

741 @see: Functions L{triAngle} and L{triArea}. 

742 ''' 

743 try: 

744 x, y, z = map1(float, a, b, c) 

745 ab = x < y 

746 if ab: 

747 x, y = y, x 

748 bc = y < z 

749 if bc: 

750 y, z = z, y 

751 

752 if z > EPS0: # z = min(a, b, c) 

753 s = fsum1f_(z, y, x) * _0_5 

754 sa, sb, r = (s - x), (s - y), (s - z) 

755 r *= _over(sa * sb, s) 

756 if r < EPS02: 

757 raise ValueError(_coincident_) 

758 r = sqrt(r) 

759 rA = atan2(r, sa) * _2_0 

760 rB = atan2(r, sb) * _2_0 

761 rC = fsumf_(PI, -rA, -rB) 

762 if min(rA, rB, rC) < 0: 

763 raise ValueError(_colinear_) 

764 s *= r # Heron's area 

765 elif z < 0: 

766 raise ValueError(_negative_) 

767 else: # 0 <= c <= EPS0 

768 rA = rB = PI_2 

769 rC = r = s = _0_0 

770 

771 if bc: 

772 rB, rC = rC, rB 

773 if ab: 

774 rA, rB = rB, rA 

775 return TriAngle5Tuple(rA, rB, rC, r, s, name=typename(triAngle5)) 

776 

777 except (TypeError, ValueError) as x: 

778 raise TriangleError(a=a, b=b, c=c, cause=x) 

779 

780 

781def triArea(a, b, c): 

782 '''Compute the area of a triangle using U{Heron's<https:// 

783 WikiPedia.org/wiki/Heron%27s_formula>} C{stable} formula. 

784 

785 @arg a: Length of the triangle side opposite of triangle corner C{A} 

786 (C{scalar}, non-negative C{meter}, conventionally). 

787 @arg b: Length of the triangle side opposite of triangle corner C{B} 

788 (C{scalar}, non-negative C{meter}, conventionally). 

789 @arg c: Length of the triangle side opposite of triangle corner C{C} 

790 (C{scalar}, non-negative C{meter}, conventionally). 

791 

792 @return: The triangle area (C{float}, conventionally C{meter} or 

793 same units as B{C{a}}, B{C{b}} and B{C{c}} I{squared}). 

794 

795 @raise TriangleError: Invalid or negative B{C{a}}, B{C{b}} or B{C{c}}. 

796 ''' 

797 try: 

798 r, y, x = sorted(map1(float, a, b, c)) 

799 if r > 0: # r = min(a, b, c) 

800 ab = x - y 

801 bc = y - r 

802 y += r 

803 r = (x + y) * (r - ab) * (r + ab) * (x + bc) 

804 if r: 

805 r = sqrt(r / _16_0) 

806 elif r < 0: 

807 raise ValueError(_negative_) 

808 return r 

809 

810 except (TypeError, ValueError) as x: 

811 raise TriangleError(a=a, b=b, c=c, cause=x) 

812 

813 

814def triSide(a, b, radC): 

815 '''Compute one side of a triangle. 

816 

817 @arg a: Adjacent triangle side length (C{scalar}, 

818 non-negative C{meter}, conventionally). 

819 @arg b: Adjacent triangle side length (C{scalar}, 

820 non-negative C{meter}, conventionally). 

821 @arg radC: Angle included by sides B{C{a}} and B{C{b}}, 

822 opposite triangle side C{c} (C{radians}). 

823 

824 @return: Length of triangle side C{c}, opposite triangle 

825 corner C{C} and angle B{C{radC}}, same units as 

826 B{C{a}} and B{C{b}}. 

827 

828 @raise TriangleError: Invalid B{C{a}}, B{C{b}} or B{C{radC}}. 

829 

830 @see: Functions L{sqrt_a}, L{triAngle}, L{triSide2} and L{triSide4}. 

831 ''' 

832 try: 

833 return _triSide(a, b, radC) 

834 except (TypeError, ValueError) as x: 

835 raise TriangleError(a=a, b=b, radC=radC, cause=x) 

836 

837 

838def _triSide(a, b, radC): 

839 # (INTERNAL) To allow callers to embellish errors 

840 a, b, r = t = map1(float, a, b, radC) 

841 if min(t) < 0: 

842 raise ValueError(_negative_) 

843 

844 if a < b: 

845 a, b = b, a 

846 if a > EPS0: 

847 ba = b / a 

848 c2 = fsumf_(_1_0, ba**2, _N_2_0 * ba * cos(r)) 

849 if c2 > EPS02: 

850 return a * sqrt(c2) 

851 elif c2 < 0: 

852 raise ValueError(_invalid_) 

853 return hypot(a, b) 

854 

855 

856def triSide2(b, c, radB): 

857 '''Compute a side and its opposite angle of a triangle. 

858 

859 @arg b: Adjacent triangle side length (C{scalar}, 

860 non-negative C{meter}, conventionally). 

861 @arg c: Adjacent triangle side length (C{scalar}, 

862 non-negative C{meter}, conventionally). 

863 @arg radB: Angle included by sides B{C{a}} and B{C{c}}, 

864 opposite triangle side C{b} (C{radians}). 

865 

866 @return: L{TriSide2Tuple}C{(a, radA)} with triangle angle 

867 C{radA} in C{radians} and length of the opposite 

868 triangle side C{a}, same units as B{C{b}} and B{C{c}}. 

869 

870 @raise TriangleError: Invalid B{C{b}} or B{C{c}} or either 

871 B{C{b}} or B{C{radB}} near zero. 

872 

873 @see: Functions L{sqrt_a}, L{triSide} and L{triSide4}. 

874 ''' 

875 try: 

876 return _triSide2(b, c, radB) 

877 except (TypeError, ValueError) as x: 

878 raise TriangleError(b=b, c=c, radB=radB, cause=x) 

879 

880 

881def _triSide2(b, c, radB): 

882 # (INTERNAL) To allow callers to embellish errors 

883 b, c, rB = map1(float, b, c, radB) 

884 if min(b, c, rB) < 0: 

885 raise ValueError(_negative_) 

886 sB, cB = sincos2(rB) 

887 if isnear0(sB): 

888 if not isnear0(b): 

889 raise ValueError(_invalid_) 

890 a, rA = ((b + c), PI) if cB < 0 else (fabs(b - c), _0_0) 

891 elif isnear0(b): 

892 raise ValueError(_invalid_) 

893 else: 

894 rA = fsumf_(PI, -rB, -asin1(c * sB / b)) 

895 a = sin(rA) * b / sB 

896 return TriSide2Tuple(a, rA, name=typename(triSide2)) 

897 

898 

899def triSide4(radA, radB, c): 

900 '''Compute two sides and the height of a triangle. 

901 

902 @arg radA: Angle at triangle corner C{A}, opposite triangle side C{a} 

903 (non-negative C{radians}). 

904 @arg radB: Angle at triangle corner C{B}, opposite triangle side C{b} 

905 (non-negative C{radians}). 

906 @arg c: Length of triangle side between triangle corners C{A} and C{B}, 

907 (C{scalar}, non-negative C{meter}, conventionally). 

908 

909 @return: L{TriSide4Tuple}C{(a, b, radC, d)} with triangle sides C{a} and 

910 C{b} and triangle height C{d} perpendicular to triangle side 

911 B{C{c}}, all in the same units as B{C{c}} and interior angle 

912 C{radC} in C{radians} at triangle corner C{C}, opposite 

913 triangle side B{C{c}}. 

914 

915 @raise TriangleError: Invalid or negative B{C{radA}}, B{C{radB}} or B{C{c}}. 

916 

917 @see: U{Triangulation, Surveying<https://WikiPedia.org/wiki/Triangulation_(surveying)>} 

918 and functions L{sqrt_a}, L{triSide} and L{triSide2}. 

919 ''' 

920 try: 

921 rA, rB, c = map1(float, radA, radB, c) 

922 rC = fsumf_(PI, -rA, -rB) 

923 if min(rC, rA, rB, c) < 0: 

924 raise ValueError(_negative_) 

925 sa, ca, sb, cb = sincos2_(rA, rB) 

926 sc = fsum1f_(sa * cb, sb * ca) 

927 if sc < EPS0 or min(sa, sb) < 0: 

928 raise ValueError(_invalid_) 

929 sc = c / sc 

930 return TriSide4Tuple((sa * sc), (sb * sc), rC, (sa * sb * sc), 

931 name=typename(triSide4)) 

932 

933 except (TypeError, ValueError) as x: 

934 raise TriangleError(radA=radA, radB=radB, c=c, cause=x) 

935 

936 

937def wildberger3(a, b, c, alpha, beta, R3=min): 

938 '''Snellius' surveying using U{Rational Trigonometry 

939 <https://WikiPedia.org/wiki/Snellius–Pothenot_problem>}. 

940 

941 @arg a: Length of the triangle side between corners C{B} and C{C} and opposite of 

942 triangle corner C{A} (C{scalar}, non-negative C{meter}, conventionally). 

943 @arg b: Length of the triangle side between corners C{C} and C{A} and opposite of 

944 triangle corner C{B} (C{scalar}, non-negative C{meter}, conventionally). 

945 @arg c: Length of the triangle side between corners C{A} and C{B} and opposite of 

946 triangle corner C{C} (C{scalar}, non-negative C{meter}, conventionally). 

947 @arg alpha: Angle subtended by triangle side B{C{b}} (C{degrees}, non-negative). 

948 @arg beta: Angle subtended by triangle side B{C{a}} (C{degrees}, non-negative). 

949 @kwarg R3: Callable to determine C{R3} from C{(R3 - C)**2 = D}, typically standard 

950 Python function C{min} or C{max}, invoked with 2 arguments. 

951 

952 @return: L{Survey3Tuple}C{(PA, PB, PC)} with distance from survey point C{P} to 

953 each of the triangle corners C{A}, C{B} and C{C}, same units as B{C{a}}, 

954 B{C{b}} and B{C{c}}. 

955 

956 @raise TriangleError: Invalid B{C{a}}, B{C{b}} or B{C{c}} or negative B{C{alpha}} or 

957 B{C{beta}} or B{C{R3}} not C{callable}. 

958 

959 @see: U{Wildberger, Norman J.<https://Math.Sc.Chula.ac.TH/cjm/content/ 

960 survey-article-greek-geometry-rational-trigonometry-and-snellius-–-pothenot-surveying>}, 

961 U{Devine Proportions, page 252<http://www.MS.LT/derlius/WildbergerDivineProportions.pdf>} 

962 and function L{snellius3}. 

963 ''' 

964 def _s(x): 

965 return sin(x)**2 

966 

967 def _vpa(r3, q2, q3, s2, s3): 

968 r1 = s2 * q3 / s3 

969 r = r1 * r3 * _4_0 

970 n = (r - _Fsumf_(r1, r3, -q2)**2).fover(s3) 

971 if n < 0 or r < EPS0: 

972 raise ValueError(_coincident_) 

973 return sqrt((n / r) * q3) if n else _0_0 

974 

975 try: 

976 a, b, c, da, db = q = map1(float, a, b, c, alpha, beta) 

977 if min(q) < 0: 

978 raise ValueError(_negative_) 

979 

980 q1, q2, q3 = q = a**2, b**2, c**2 

981 if min(q) < EPS02: 

982 raise ValueError(_coincident_) 

983 

984 ra, rb = map1(radians, da, db) 

985 s1, s2, s3 = s = map1(_s, rb, ra, ra + rb) # rb, ra! 

986 if min(s) < EPS02: 

987 raise ValueError(_or(_coincident_, _colinear_)) 

988 

989 q4 = hypot2_(*q) * _2_0 # a**4 + ... 

990 Qs = _Fsumf_(*q) # == hypot2_(a, b, c) 

991 d0 = (Qs**2 - q4).fmul(s1 * s2).fover(s3) 

992 if d0 < 0: 

993 raise ValueError(_negative_) 

994 s += _Fsumf_(*s), # == fsum1(s), 

995 C0 = Fdot(s, q1, q2, q3, -Qs * _0_5) 

996 r3 = C0.fover(-s3) # C0 /= -s3 

997 if d0 > EPS02: # > c0 

998 _xcallable(R3=R3) 

999 d0 = sqrt(d0) 

1000 r3 = R3(float(C0 + d0), float(C0 - d0)) # XXX min or max 

1001 

1002 pa = _vpa(r3, q2, q3, s2, s3) 

1003 pb = _vpa(r3, q1, q3, s1, s3) 

1004 pc = favg(_triSide2(b, pa, ra).a, 

1005 _triSide2(a, pb, rb).a) 

1006 return Survey3Tuple(pa, pb, pc, name=typename(wildberger3)) 

1007 

1008 except (TypeError, ValueError) as x: 

1009 raise TriangleError(a=a, b=b, c=c, alpha=alpha, beta=beta, R3=R3, cause=x) 

1010 

1011 

1012def _zidw(x, y, useZ, *ABC): 

1013 if useZ: # interpolate z or coplanar with A, B and C? 

1014 t = tuple(_.z for _ in ABC) 

1015 v = Vector3d(x, y, fmean(t)) 

1016 z = fidw(t, (v.minus(T).length for T in ABC)) 

1017 else: 

1018 z = INT0 

1019 return z 

1020 

1021# **) MIT License 

1022# 

1023# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved. 

1024# 

1025# Permission is hereby granted, free of charge, to any person obtaining a 

1026# copy of this software and associated documentation files (the "Software"), 

1027# to deal in the Software without restriction, including without limitation 

1028# the rights to use, copy, modify, merge, publish, distribute, sublicense, 

1029# and/or sell copies of the Software, and to permit persons to whom the 

1030# Software is furnished to do so, subject to the following conditions: 

1031# 

1032# The above copyright notice and this permission notice shall be included 

1033# in all copies or substantial portions of the Software. 

1034# 

1035# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 

1036# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

1037# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 

1038# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 

1039# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 

1040# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

1041# OTHER DEALINGS IN THE SOFTWARE.