/* Arbitrary precision Transverse Mercator Projection Copyright (c) Charles Karney (2009-2017) and licensed under the MIT/X11 License. For more information, see https://geographiclib.sourceforge.io/ Reference: Charles F. F. Karney, Transverse Mercator with an accuracy of a few nanometers, J. Geodesy 85(8), 475-485 (Aug. 2011). DOI 10.1007/s00190-011-0445-3 preprint https://arxiv.org/abs/1002.1417 resource page https://geographiclib.sourceforge.io/tm.html The parameters for the transformation are set by setparams(a,f,k0)$ sets the equatorial radius, inverse flattening, and central scale factor. The default is setparams(6378137b0, 1/298.257223563b0, 0.9996b0)$ appropriate for UTM applications. tm(lat,lon); takes lat and lon args (in degrees) and returns [x, y, convergence, scale] [x, y] do not include false eastings/northings but do include the scale factor k0. convergence is in degrees. ll(x,y); takes x and y args (in meters) and returns [lat, lon, convergence, scale]. Example: $ maxima Maxima 5.15.0 http://maxima.sourceforge.net Using Lisp CLISP 2.43 (2007-11-18) Distributed under the GNU Public License. See the file COPYING. Dedicated to the memory of William Schelter. The function bug_report() provides bug reporting information. (%i1) load("tm.mac")$ (%i2) tm(10b0,20b0); (%o2) [2.235209504622466691587930831718465965864199221939781808953597771095103\ 6690000464b6, 1.17529734503138466792126931904154130080533935727351398258511134\ 68541970512119385b6, 3.6194756227592979778565787394402350354250845160819430786\ 093514889500602612857052b0, 1.062074627142564335518604915718789933200854739344\ 8664109599248189291146283796933b0] (%i3) ll(%[1],%[2]); (%o3) [1.0b1, 2.0b1, 3.6194756227592979778565787394402350354250845160819430786\ 093514889500602612857053b0, 1.062074627142564335518604915718789933200854739344\ 8664109599248189291146283796933b0] (%i4) float(%o2); (%o4) [2235209.504622467, 1175297.345031385, 3.619475622759298, 1.062074627142564] (%i5) float(%o3); (%o5) [10.0, 20.0, 3.619475622759298, 1.062074627142564] This implements GeographicLib::TransverseMercatorExact (i.e., Lee, 1976) using bfloats. However fewer changes from Lee 1976 have been made since we rely more heavily on the high precision to deal with problem cases. To change the precision, change fpprec below and reload. */ fpprec:80$ load("ellint.mac")$ /* Load elliptic functions */ tol:0.1b0^fpprec$ tol1:0.1b0*sqrt(tol)$ /* For Newton's method */ tol2:sqrt(0.01*tol*tol1)$ /* Also for Newton's method but more conservative */ ahypover:log(10b0^fpprec)+2$ pi:bfloat(%pi)$ degree:pi/180$ ratprint:false$ debugprint:false$ setparams(a1,f1,k1):=(a:bfloat(a1),f:bfloat(f1),k0:bfloat(k1), e2:f*(2-f), e:sqrt(e2), kcu:kc(e2), kcv:kc(1-e2), ecu:ec(e2), ecv:ec(1-e2), n:f/(2-f), 'done)$ setparams(6378137b0, 1/298.257223563b0, 0.9996b0)$ /* WGS 84 */ /* setparams(6378388b0, 1/297b0, 0.9996b0)$ International */ /* setparams(1/ec(0.01b0), 1/(30*sqrt(11b0)+100), 1b0)$ testing, eps = 0.1*/ /* Interpret x_y(y) as x <- y, i.e., "transform quantity y to quantity x" Let phi = geodetic latitude psi = isometric latitude ( + i * lambda ) sigma = TM coords thom = Thompson coords */ /* sqrt(x^2 + y^2) -- Real only */ hypot(x,y):=sqrt(x^2 + y^2)$ /* log(1 + x) -- Real only */ log1p(x) := block([y : 1b0+x], if y = 1b0 then x else x*log(y)/(y - 1))$ /* Real only */ /* Some versions of Maxima have a buggy atanh atnh(x) := block([y : abs(x)], y : log1p(2 * y/(1 - y))/2, if x < 0 then -y else y)$ */ atnh(x) := atanh(x)$ /* exp(x)-1 -- Real only */ expm1(x) := block([y : exp(bfloat(x)),z], z : y - 1b0, if abs(x) > 1b0 then z else if z = 0b0 then x else x * z/log(y))$ /* Real only */ /* Some versions of Maxima have a buggy sinh */ snh(x) := block([u : expm1(x)], (u / (u + 1)) * (u + 2) /2); /* Real only */ psi_phi(phi):=block([s:sin(phi)], asinh(s/max(cos(phi),0.1b0*tol)) - e * atnh(e * s))$ /* Real only */ phi_psi(psi):=block([q:psi,t,dq], for i do ( t:tanh(q), dq : -(q - e * atnh(e * t) - psi) * (1 - e2 * t^2) / (1 - e2), q : q + dq, if debugprint then print(float(q), float(dq)), if abs(dq) < tol1 then return(false)), atan(snh(q)))$ psi_thom_comb(w):=block([jacr:sncndn(bfloat(realpart(w)),1-e2), jaci:sncndn(bfloat(imagpart(w)),e2),d,d1,d2], d:(1-e2)*(jaci[2]^2 + e2 * (jacr[1] * jaci[1])^2)^2, d1:sqrt(jacr[2]^2 + (1-e2) * (jacr[1] * jaci[1])^2), d2:sqrt(e2 * jacr[2]^2 + (1-e2) * jaci[2]^2), [ (if d1 > 0b0 then asinh(jacr[1]*jaci[3]/ d1) else signnum(snu) * ahypover) - (if d2 > 0b0 then e * asinh(e * jacr[1] / d2) else signnum(snu) * ahypover) + %i * (if d1 > 0b0 and d2 > 0b0 then atan2(jacr[3]*jaci[1],jacr[2]*jaci[2]) - e * atan2(e*jacr[2]*jaci[1],jacr[3]*jaci[2]) else 0), jacr[2]*jacr[3]*jaci[3]*(jaci[2]^2-e2*(jacr[1]*jaci[1])^2)/d -%i * jacr[1]*jaci[1]*jaci[2]*((jacr[3]*jaci[3])^2+e2*jacr[2]^2)/d] )$ psi_thom(w):=block([tt:psi_thom_comb(w)],tt[1])$ inv_diff_psi_thom(w):=block([tt:psi_thom_comb(w)],tt[2])$ w0a(psi):=block([lam:bfloat(imagpart(psi)),psia:bfloat(realpart(psi))], rectform(kcu/(pi/2)*( atan2(snh(psia),cos(lam)) +%i*asinh(sin(lam)/sqrt(cos(lam)^2 + snh(psia)^2)))))$ w0c(psi):=block([m,a,dlam], dlam:bfloat(imagpart(psi))-pi/2*(1-e), psi:bfloat(realpart(psi)), m:sqrt(psi^2+dlam^2)*3/(1-e2)/e, a:if m = 0b0 then 0 else atan2(dlam-psi, psi+dlam) - 0.75b0*pi, m:m^(1/3), a:a/3, m*cos(a)+%i*(m*sin(a)+kcv))$ w0d(psi):=block([psir:-realpart(psi)/e+1b0,lam:(pi/2-imagpart(psi))/e,uu,vv], uu:asinh(sin(lam)/sqrt(cos(lam)^2+snh(psir)^2))*(1+e2/2), vv:atan2(cos(lam), snh(psir)) *(1+e2/2), (-uu+kcu) + %i * (-vv+kcv))$ w0m(psi):=if realpart(psi)<-e/2*pi/2 and imagpart(psi)>pi/2*(1-2*e) and realpart(psi) < imagpart(psi)-(pi/2*(1-e)) then w0d(psi) else if realpart(psi)pi/2*(1-2*e) then w0c(psi) else w0a(psi)$ w0(psi):=w0m(psi)$ thom_psi(psi):=block([w:w0(psi),dw,v,vv], if not(abs(psi-pi/2*(1-e)*%i) < e * tol^0.6b0) then for i do ( if i > 100 then error("too many iterations"), vv:psi_thom_comb(w), v:vv[1], dw:-rectform((v-psi)*vv[2]), w:w+dw, dw:abs(dw), if debugprint then print(float(w),float(dw)), /* error is approx dw^2/2 */ if dw < tol2 then return(false) ), w )$ sigma_thom_comb(w):=block([u:bfloat(realpart(w)),v:bfloat(imagpart(w)), jacr,jaci,phi,iu,iv,den,den1,er,ei,dnr,dni], jacr:sncndn(u,1-e2),jaci:sncndn(v,e2), er:eirx(jacr[1],jacr[2],jacr[3],e2,ecu), ei:eirx(jaci[1],jaci[2],jaci[3],1-e2,ecv), den:e2*jacr[2]^2+(1-e2)*jaci[2]^2, den1:(1-e2)*(jaci[2]^2 + e2 * (jacr[1] * jaci[1])^2)^2, dnr:jacr[3]*jaci[2]*jaci[3], dni:-e2*jacr[1]*jacr[2]*jaci[1], [ er - e2*jacr[1]*jacr[2]*jacr[3]/den + %i*(v - ei + (1-e2)*jaci[1]*jaci[2]*jaci[3]/den), (dnr^2-dni^2)/den1 + %i * 2*dnr*dni/den1])$ sigma_thom(w):=block([tt:sigma_thom_comb(w)],tt[1])$ inv_diff_sigma_thom(w):=block([tt:sigma_thom_comb(w)],tt[2])$ wx0a(sigma):=rectform(sigma*kcu/ecu)$ wx0b(sigma):=block([m,aa], sigma:rectform(sigma-%i*(kcv-ecv)), m:abs(sigma)*3/(1-e2), aa:atan2(imagpart(sigma),realpart(sigma)), if aa<-pi/2 then aa:aa+2*pi, aa:aa-pi, rectform(m^(1/3)*(cos(aa/3b0)+%i*sin(aa/3b0))+%i*kcv))$ wx0c(sigma):=rectform(1/(sigma-(ecu+%i*(kcv-ecv))) + kcu+%i*kcv)$ wx0m(sigma):=block([eta:bfloat(imagpart(sigma)), xi:bfloat(realpart(sigma))], if eta > 1.25b0 * (kcv-ecv) or (xi < -0.25*ecu and xi < eta-(kcv-ecv)) then wx0c(sigma) else if (eta > 0.75b0 * (kcv-ecv) and xi < 0.25b0 * ecu) or eta > kcv-ecv or xi < 0 then wx0b(sigma) else wx0a(sigma))$ wx0(sigma):=wx0m(sigma)$ thom_sigma(sigma):=block([w:wx0(sigma),dw,v,vv], for i do ( if i > 100 then error("too many iterations"), vv:sigma_thom_comb(w), v:vv[1], dw:-rectform((v-sigma)*vv[2]), w:w+dw, dw:abs(dw), if debugprint then print(float(w),float(dw)), /* error is approx dw^2/2 */ if dw < tol2 then return(false) ), w )$ /* Lee/Thompson's method forward */ tm(phi,lam):=block([psi,thom,jacr,jaci,sigma,gam,scale,c], phi:phi*degree, lam:lam*degree, psi:psi_phi(phi), thom:thom_psi(psi+%i*lam), jacr:sncndn(bfloat(realpart(thom)),1-e2), jaci:sncndn(bfloat(imagpart(thom)),e2), sigma:sigma_thom(thom), c:cos(phi), if c > tol1 then ( gam:atan2((1-e2)*jacr[1]*jaci[1]*jaci[2], jacr[2]*jacr[3]*jaci[3]), scale:sqrt(1-e2 + e2 * c^2)/c* sqrt(((1-e2)*jaci[1]^2 + (jacr[2]*jaci[3])^2)/ (e2*jacr[2]^2 + (1-e2)*jaci[2]^2))) else (gam : lam, scale : 1b0), [imagpart(sigma)*k0*a,realpart(sigma)*k0*a,gam/degree,k0*scale])$ /* Lee/Thompson's method reverse */ ll(x,y):=block([sigma,thom,jacr,jaci,psi,lam,phi,gam,scale,c], sigma:y/(a*k0)+%i*x/(a*k0), thom:thom_sigma(sigma), jacr:sncndn(bfloat(realpart(thom)),1-e2), jaci:sncndn(bfloat(imagpart(thom)),e2), psi:psi_thom(thom), lam:bfloat(imagpart(psi)), psi:bfloat(realpart(psi)), phi:phi_psi(psi), c:cos(phi), if c > tol1 then ( gam:atan2((1-e2)*jacr[1]*jaci[1]*jaci[2], jacr[2]*jacr[3]*jaci[3]), scale:sqrt(1-e2 + e2 * c^2)/c* sqrt(((1-e2)*jaci[1]^2 + (jacr[2]*jaci[3])^2)/ (e2*jacr[2]^2 + (1-e2)*jaci[2]^2))) else (gam : lam, scale : 1b0), [phi/degree,lam/degree,gam/degree,k0*scale])$ /* Return lat/lon/x/y for a point specified in Thompson coords */ /* Pick u in [0, kcu] and v in [0, kcv] */ lltm(u,v):=block([jacr,jaci,psi,lam,phi,c,gam,scale,sigma,x,y], u:bfloat(u), v:bfloat(v), jacr:sncndn(u,1-e2), jaci:sncndn(v,e2), psi:psi_thom(u+%i*v), sigma:sigma_thom(u+%i*v), x:imagpart(sigma)*k0*a,y:realpart(sigma)*k0*a, lam:bfloat(imagpart(psi)), psi:bfloat(realpart(psi)), phi:phi_psi(psi), c:cos(phi), if c > tol1 then ( gam:atan2((1-e2)*jacr[1]*jaci[1]*jaci[2], jacr[2]*jacr[3]*jaci[3]), scale:sqrt(1-e2 + e2 * c^2)/c* sqrt(((1-e2)*jaci[1]^2 + (jacr[2]*jaci[3])^2)/ (e2*jacr[2]^2 + (1-e2)*jaci[2]^2))) else (gam : lam, scale : 1b0), [phi/degree,lam/degree,x,y,gam/degree,k0*scale])$ /* Gauss-Krueger series to order n^i forward Uses the array functions a1_a[i](n), zeta_a[i](z,n), zeta_d[i](z,n), zetap_a[i](s,n), zetap_d[i](s,n), defined in tmseries.mac. */ tms(phi,lam,i):=block([psi,xip,etap,z,sigma,sp,gam,k,b1], phi:phi*degree, lam:lam*degree, psi:psi_phi(phi), xip:atan2(snh(psi), cos(lam)), etap:asinh(sin(lam)/hypot(snh(psi),cos(lam))), k:sqrt(1 - e2*sin(phi)^2)/(cos(phi)*hypot(snh(psi),cos(lam))), gam:atan(tan(xip)*tanh(etap)), z:xip+%i*etap, b1:a1_a[i](n), sigma:rectform(b1*zeta_a[i](z,n)), sp:rectform(zeta_d[i](z,n)), gam : gam - atan2(imagpart(sp),realpart(sp)), k : k * b1 * cabs(sp), [imagpart(sigma)*k0*a,realpart(sigma)*k0*a,gam/degree,k*k0])$ /* Gauss-Krueger series to order n^i reverse */ lls(x,y,i):=block([sigma,b1,s,z,zp,xip,etap,s,c,r,gam,k,lam,psi,phi], sigma:y/(a*k0)+%i*x/(a*k0), b1:a1_a[i](n), s:rectform(sigma/b1), z:rectform(zetap_a[i](s,n)), zp:rectform(zetap_d[i](s,n)), gam : atan2(imagpart(zp), realpart(zp)), k : b1 / cabs(zp), xip:realpart(z), etap:imagpart(z), s:snh(etap), c:cos(xip), r:hypot(s, c), lam:atan2(s, c), psi : asinh(sin(xip)/r), phi :phi_psi(psi), k : k * sqrt(1 - e2*sin(phi)^2) * r/cos(phi), gam : gam + atan(tan(xip) * tanh(etap)), [phi/degree,lam/degree,gam/degree,k*k0])$ /* Approx geodesic distance valid for small displacements */ dist(phi0,lam0,phi,lam):=block([dphi,dlam,nn,hlon,hlat], dphi:(phi-phi0)*degree, dlam:(lam-lam0)*degree, phi0:phi0*degree, lam0:lam0*degree, nn : 1/sqrt(1 - e2 * sin(phi0)^2), hlon : cos(phi0) * nn, hlat : (1 - e2) * nn^3, a * hypot(dphi*hlat, dlam*hlon))$ /* Compute truncation errors for all truncation levels */ check(phi,lam):=block([vv,x,y,gam,k,vf,vb,errf,errr,err2,errlist], phi:min(90-0.01b0,phi), lam:min(90-0.01b0,lam), vv:tm(phi,lam), errlist:[], x:vv[1], y:vv[2], gam:vv[3], k:vv[4], for i:1 thru maxpow do ( vf:tms(phi,lam,i), errf:hypot(vf[1]-x,vf[2]-y)/k, errfg:abs(vf[3]-gam), errfk:abs((vf[4]-k)/k), vb:lls(x,y,i), errr:dist(phi,lam,vb[1],vb[2]), errrg:abs(vb[3]-gam), errrk:abs((vb[4]-k)/k), errlist:append(errlist, [max(errf, errr), max(errfg, errrg), max(errfk, errrk)])), errlist)$ /* Max of output of check over a set of points */ checka(lst):=block([errlist:[],errx], for i:1 thru 3*maxpow do errlist:cons(0b0,errlist), for vv in lst do ( errx:check(vv[1],vv[2]), for i:1 thru 3*maxpow do errlist[i]:max(errlist[i],errx[i])), errlist)$