leaflet.geodesic
Version:
Add-on to draw geodesic lines with leaflet
3 lines (2 loc) • 10.8 kB
JavaScript
/*! leaflet.geodesic 3.0.0-alpha.2 - (c) Henry Thasler - https://github.com/henrythasler/Leaflet.Geodesic#readme */
import{LatLng as t,Polyline as s,Util as n,GeoJSON as i}from"leaflet";class e{constructor(t){this.options={wrap:!0,steps:3},this.ellipsoid={a:6378137,b:6356752.3142,f:1/298.257223563},this.options={...this.options,...t}}toRadians(t){return t*Math.PI/180}toDegrees(t){return 180*t/Math.PI}mod(t,s){const n=t%s;return n<0?n+s:n}wrap360(t){return 0<=t&&t<360?t:this.mod(t,360)}wrap(t,s=360){return-s<=t&&t<=s?t:this.mod(t+s,2*s)-s}direct(t,s,n,i=100){const e=this.toRadians(t.lat),a=this.toRadians(t.lng),o=this.toRadians(s),r=n,h=1e3*Number.EPSILON,{a:l,b:c,f:g}=this.ellipsoid,u=Math.sin(o),p=Math.cos(o),M=(1-g)*Math.tan(e),d=1/Math.sqrt(1+M*M),f=M*d,w=Math.atan2(M,p),m=d*u,L=1-m*m,y=L*(l*l-c*c)/(c*c),b=1+y/16384*(4096+y*(y*(320-175*y)-768)),v=y/1024*(256+y*(y*(74-47*y)-128));let D=r/(c*b),R=null,S=null,E=null,x=null,P=null,$=0;do{x=Math.cos(2*w+D),R=Math.sin(D),S=Math.cos(D),E=v*R*(x+v/4*(S*(2*x*x-1)-v/6*x*(4*R*R-3)*(4*x*x-3))),P=D,D=r/(c*b)+E}while(Math.abs(D-P)>h&&++$<i);if($>=i)throw new EvalError(`Direct vincenty formula failed to converge after ${i} iterations \n (start=${t.lat}/${t.lng}; bearing=${s}; distance=${n})`);const N=f*R-d*S*p,O=Math.atan2(f*S+d*R*p,(1-g)*Math.sqrt(m*m+N*N)),A=g/16*L*(4+g*(4-3*L)),G=a+(Math.atan2(R*u,d*S-f*R*p)-(1-A)*g*m*(D+A*R*(x+A*S*(2*x*x-1)))),I=Math.atan2(m,-N);return{lat:this.toDegrees(O),lng:this.toDegrees(G),bearing:this.wrap360(this.toDegrees(I))}}inverse(s,n,i=100,e=!0){const a=s,o=n,r=this.toRadians(a.lat),h=this.toRadians(a.lng),l=this.toRadians(o.lat),c=this.toRadians(o.lng),g=Math.PI,u=Number.EPSILON,{a:p,b:M,f:d}=this.ellipsoid,f=c-h,w=(1-d)*Math.tan(r),m=1/Math.sqrt(1+w*w),L=w*m,y=(1-d)*Math.tan(l),b=1/Math.sqrt(1+y*y),v=y*b,D=Math.abs(f)>g/2||Math.abs(l-r)>g/2;let R=f,S=null,E=null,x=D?g:0,P=0,$=D?-1:1,N=null,O=1,A=null,G=1,I=null,q=null,k=0;do{if(S=Math.sin(R),E=Math.cos(R),N=b*S*(b*S)+(m*v-L*b*E)*(m*v-L*b*E),Math.abs(N)<u)break;P=Math.sqrt(N),$=L*v+m*b*E,x=Math.atan2(P,$),A=m*b*S/P,G=1-A*A,O=0!==G?$-2*L*v/G:0,I=d/16*G*(4+d*(4-3*G)),q=R,R=f+(1-I)*d*A*(x+I*P*(O+I*$*(2*O*O-1)));if((D?Math.abs(R)-g:Math.abs(R))>g)throw new EvalError("λ > π")}while(Math.abs(R-q)>1e-12&&++k<i);if(k>=i){if(e)return this.inverse(s,new t(n.lat,n.lng-.01),i,e);throw new EvalError(`Inverse vincenty formula failed to converge after ${i} iterations \n (start=${s.lat}/${s.lng}; dest=${n.lat}/${n.lng})`)}const B=G*(p*p-M*M)/(M*M),T=B/1024*(256+B*(B*(74-47*B)-128)),C=M*(1+B/16384*(4096+B*(B*(320-175*B)-768)))*(x-T*P*(O+T/4*($*(2*O*O-1)-T/6*O*(4*P*P-3)*(4*O*O-3)))),j=Math.abs(N)<u?0:Math.atan2(b*S,m*v-L*b*E),F=Math.abs(N)<u?g:Math.atan2(m*S,-L*b+m*v*E);return{distance:C,initialBearing:Math.abs(C)<u?NaN:this.wrap360(this.toDegrees(j)),finalBearing:Math.abs(C)<u?NaN:this.wrap360(this.toDegrees(F))}}intersection(s,n,i,e){const a=this.toRadians(s.lat),o=this.toRadians(s.lng),r=this.toRadians(i.lat),h=this.toRadians(i.lng),l=this.toRadians(n),c=this.toRadians(e),g=r-a,u=h-o,p=Math.PI,M=Number.EPSILON,d=2*Math.asin(Math.sqrt(Math.sin(g/2)*Math.sin(g/2)+Math.cos(a)*Math.cos(r)*Math.sin(u/2)*Math.sin(u/2)));if(Math.abs(d)<M)return s;const f=(Math.sin(r)-Math.sin(a)*Math.cos(d))/(Math.sin(d)*Math.cos(a)),w=(Math.sin(a)-Math.sin(r)*Math.cos(d))/(Math.sin(d)*Math.cos(r)),m=Math.acos(Math.min(Math.max(f,-1),1)),L=Math.acos(Math.min(Math.max(w,-1),1)),y=l-(Math.sin(h-o)>0?m:2*p-m),b=(Math.sin(h-o)>0?2*p-L:L)-c;if(0===Math.sin(y)&&0===Math.sin(b))return null;if(Math.sin(y)*Math.sin(b)<0)return null;const v=-Math.cos(y)*Math.cos(b)+Math.sin(y)*Math.sin(b)*Math.cos(d),D=Math.atan2(Math.sin(d)*Math.sin(y)*Math.sin(b),Math.cos(b)+Math.cos(y)*v),R=Math.asin(Math.min(Math.max(Math.sin(a)*Math.cos(D)+Math.cos(a)*Math.sin(D)*Math.cos(l),-1),1)),S=o+Math.atan2(Math.sin(l)*Math.sin(D)*Math.cos(a),Math.cos(D)-Math.sin(a)*Math.sin(R));return new t(this.toDegrees(R),this.toDegrees(S))}midpoint(s,n){const i=this.toRadians(s.lat),e=this.toRadians(s.lng),a=this.toRadians(n.lat),o=this.toRadians(n.lng-s.lng),r=Math.cos(i),h=0,l=Math.sin(i),c={x:r+Math.cos(a)*Math.cos(o),y:h+Math.cos(a)*Math.sin(o),z:l+Math.sin(a)},g=Math.atan2(c.z,Math.sqrt(c.x*c.x+c.y*c.y)),u=e+Math.atan2(c.y,c.x);return new t(this.toDegrees(g),this.toDegrees(u))}}class a{constructor(t){this.geodesic=new e,this.steps=t?.steps??3}recursiveMidpoint(t,s,n){const i=[t,s],e=this.geodesic.midpoint(t,s);return n>0?(i.splice(0,1,...this.recursiveMidpoint(t,e,n-1)),i.splice(i.length-2,2,...this.recursiveMidpoint(e,s,n-1))):i.splice(1,0,e),i}line(t,s){return this.recursiveMidpoint(t,s,Math.min(8,this.steps))}multiLineString(t){const s=[];for(const n of t){const t=[];for(let s=1;s<n.length;s++)t.splice(t.length-1,1,...this.line(n[s-1],n[s]));s.push(t)}return s}lineString(t){return this.multiLineString([t])[0]}splitLine(s,n){const i={point:new t(89.9,-180.0000001),bearing:180},e={point:new t(89.9,180.0000001),bearing:180},a=new t(s.lat,s.lng,s.alt),o=new t(n.lat,n.lng,n.alt);a.lng=this.geodesic.wrap(a.lng,360),o.lng=this.geodesic.wrap(o.lng,360),o.lng-a.lng>180?o.lng=o.lng-360:o.lng-a.lng<-180&&(o.lng=o.lng+360);let r=[[new t(a.lat,this.geodesic.wrap(a.lng,180),a.alt),new t(o.lat,this.geodesic.wrap(o.lng,180),o.alt)]];if(a.lng>=-180&&a.lng<=180){if(o.lng<-180){const s=this.geodesic.inverse(a,o).initialBearing,n=this.geodesic.intersection(a,s,i.point,i.bearing);n&&(r=[[a,n],[new t(n.lat,n.lng+360),new t(o.lat,o.lng+360,o.alt)]])}else if(o.lng>180){const s=this.geodesic.inverse(a,o).initialBearing,n=this.geodesic.intersection(a,s,e.point,e.bearing);n&&(r=[[a,n],[new t(n.lat,n.lng-360),new t(o.lat,o.lng-360,o.alt)]])}}else if(o.lng>=-180&&o.lng<=180)if(a.lng<-180){const s=this.geodesic.inverse(a,o).initialBearing,n=this.geodesic.intersection(a,s,i.point,i.bearing);n&&(r=[[new t(a.lat,a.lng+360,a.alt),new t(n.lat,n.lng+360)],[n,o]])}else if(a.lng>180){const s=this.geodesic.inverse(a,o).initialBearing,n=this.geodesic.intersection(a,s,i.point,i.bearing);n&&(r=[[new t(a.lat,a.lng-360,a.alt),new t(n.lat,n.lng-360)],[n,o]])}return r}splitMultiLineString(t){const s=[];for(const n of t){if(1===n.length){s.push(n);continue}let t=[];for(let i=1;i<n.length;i++){const e=this.splitLine(n[i-1],n[i]);t.pop(),t=t.concat(e[0]),e.length>1&&(s.push(t),t=e[1])}s.push(t)}return s}wrapMultiLineString(s){const n=[];for(const i of s){const s=[];let e=null;for(const n of i)if(null===e)s.push(new t(n.lat,n.lng)),e=new t(n.lat,n.lng);else{const i=Math.round((n.lng-e.lng)/360);s.push(new t(n.lat,n.lng-360*i)),e=new t(n.lat,n.lng-360*i)}n.push(s)}return n}circle(s,n){const i=[];for(let e=0;e<this.steps;e++){const a=this.geodesic.direct(s,360/this.steps*e,n);i.push(new t(a.lat,a.lng))}return i.push(new t(i[0].lat,i[0].lng)),i}splitCircle(t){const s=this.splitMultiLineString([t]);return 3===s.length&&(s[2]=[...s[2],...s[0]],s.shift()),s}distance(s,n){return this.geodesic.inverse(new t(s.lat,this.geodesic.wrap(s.lng,180)),new t(n.lat,this.geodesic.wrap(n.lng,180))).distance}multilineDistance(t){const s=[];for(const n of t){let t=0;for(let s=1;s<n.length;s++)t+=this.distance(n[s-1],n[s]);s.push(t)}return s}updateStatistics(t,s){const n={distanceArray:[],totalDistance:0,points:0,vertices:0};n.distanceArray=this.multilineDistance(t),n.totalDistance=n.distanceArray.reduce(((t,s)=>t+s),0),n.points=0;for(const s of t)n.points+=s.reduce((t=>t+1),0);n.vertices=0;for(const t of s)n.vertices+=t.reduce((t=>t+1),0);return n}}function o(t){return"object"==typeof t&&null!==t&&"lat"in t&&"lng"in t&&"number"==typeof t.lat&&"number"==typeof t.lng}function r(t){return t instanceof Array&&"number"==typeof t[0]&&"number"==typeof t[1]}function h(s){return s instanceof t||r(s)||o(s)}function l(s){if(s instanceof t)return s;if(r(s))return new t(s[0],s[1],s[2]);if(o(s))return new t(s.lat,s.lng,s.alt);throw new Error("LatLngExpression expected. Unknown object found.")}class c extends s{constructor(t,s){super([],s),this.defaultOptions={wrap:!0,steps:3},this.statistics={distanceArray:[],totalDistance:0,points:0,vertices:0},this.points=[],n.setOptions(this,{...this.defaultOptions,...s}),this.geom=new a(this.options),void 0!==t&&this.setLatLngs(t)}updateGeometry(){let t=[];if(t=this.geom.multiLineString(this.points),this.statistics=this.geom.updateStatistics(this.points,t),this.options.wrap){const s=this.geom.splitMultiLineString(t);super.setLatLngs(s)}else super.setLatLngs(this.geom.wrapMultiLineString(t))}setLatLngs(t){return this.points=function(t){const s=[],n=h(t[0])?[t]:t,i=new Error("LatLngExpression[] | LatLngExpression[][] expected. Unknown object found.");if(!(n instanceof Array))throw i;for(const t of n){if(!(t instanceof Array))throw i;const n=[];for(const s of t){if(!h(s))throw i;n.push(l(s))}s.push(n)}return s}(t),this.updateGeometry(),this}addLatLng(t,s){const n=l(t);return 0===this.points.length?this.points.push([n]):void 0===s?this.points[this.points.length-1].push(n):s.push(n),this.updateGeometry(),this}fromGeoJson(t){let s=[],n=[];return"FeatureCollection"===t.type?n=t.features:"Feature"===t.type?n=[t]:["MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon"].includes(t.type)?n=[{type:"Feature",geometry:t,properties:{}}]:console.log(`[Leaflet.Geodesic] fromGeoJson() - Type "${t.type}" not supported.`),n.forEach((t=>{switch(t.geometry.type){case"MultiPoint":case"LineString":s=[...s,i.coordsToLatLngs(t.geometry.coordinates,0)];break;case"MultiLineString":case"Polygon":s=[...s,...i.coordsToLatLngs(t.geometry.coordinates,1)];break;case"MultiPolygon":t.geometry.coordinates.forEach((t=>{s=[...s,...i.coordsToLatLngs(t,1)]}));break;default:console.log(`[Leaflet.Geodesic] fromGeoJson() - Type "${t.geometry.type}" not supported.`)}})),s.length&&this.setLatLngs(s),this}distance(t,s){return this.geom.distance(l(t),l(s))}}class g extends s{constructor(s,i){super([],i),this.defaultOptions={wrap:!0,steps:24,fill:!0,noClip:!0},this.statistics={distanceArray:[],totalDistance:0,points:0,vertices:0},n.setOptions(this,{...this.defaultOptions,...i});const e=this.options;this.radius=e.radius??1e6,this.center=void 0===s?new t(0,0):l(s),this.geom=new a(this.options),this.update()}update(){const t=this.geom.circle(this.center,this.radius);if(this.statistics=this.geom.updateStatistics([[this.center]],[t]),this.statistics.totalDistance=this.geom.multilineDistance([t]).reduce(((t,s)=>t+s),0),this.options.wrap){const s=this.geom.splitCircle(t);super.setLatLngs(s)}else super.setLatLngs(t)}distanceTo(t){const s=l(t);return this.geom.distance(this.center,s)}setLatLng(t,s){this.center=l(t),this.radius=s??this.radius,this.update()}setRadius(t,s){this.radius=t,this.center=s?l(s):this.center,this.update()}}export{g as GeodesicCircle,c as GeodesicLine};