1
0
Fork 0

Phi: Partial Fix #1769 route manager path

Use geodesic plugin for leaflet do correctly display great circle
paths between waypoints and handle wrapping across -180deg of latitude.

The entire leaflet library seems to have an issue wrapping around
-180/+180 deg of latitude :-/
This commit is contained in:
Torsten Dreyer 2015-06-22 17:21:41 +02:00
parent 6a327df616
commit e0b415ad74
3 changed files with 71 additions and 13 deletions

View file

@ -0,0 +1,49 @@
/*! Leaflet.Geodesic https://github.com/henrythasler/Leaflet.Geodesic*/
void 0===Number.prototype.toRadians&&(Number.prototype.toRadians=function(){return this*Math.PI/180}),void 0===Number.prototype.toDegrees&&(Number.prototype.toDegrees=function(){return 180*this/Math.PI})
var INTERSECT_LNG=179.999
L.Geodesic=L.MultiPolyline.extend({options:{color:"blue",steps:10,dash:1,wrap:!0},initialize:function(t,a){this.options=this._merge_options(this.options,a),this.datum={},this.datum.ellipsoid={a:6378137,b:6356752.3142,f:1/298.257223563},L.MultiPolyline.prototype.initialize.call(this,t,this.options)},setLatLngs:function(t){this._latlngs=this.options.dash<1?this._generate_GeodesicDashed(t):this._generate_Geodesic(t),L.MultiPolyline.prototype.setLatLngs.call(this,this._latlngs)},getStats:function(){var t,a,n={distance:0,points:0,polygons:this._latlngs.length}
for(t=0;t<this._latlngs.length;t++)for(n.points+=this._latlngs[t].length,a=0;a<this._latlngs[t].length-1;a++)n.distance+=this._vincenty_inverse(this._latlngs[t][a],this._latlngs[t][a+1]).distance
return n},createCircle:function(t,a){var n,i=[],s=0,e={lat:0,lng:0,brg:0}
for(i[s]=[],n=0;n<=this.options.steps;){var h=this._vincenty_direct(L.latLng(t),360/this.options.steps*n,a,!0),l=L.latLng(h.lat,h.lng)
if(Math.abs(l.lng-e.lng)>180){var o=this._vincenty_inverse(e,l),r=this._intersection(e,o.initialBearing,{lat:-89,lng:l.lng-e.lng>0?-INTERSECT_LNG:INTERSECT_LNG},0)
r?(i[s].push(L.latLng(r.lat,r.lng)),s++,i[s]=[],e=L.latLng(r.lat,-r.lng),i[s].push(e)):(s++,i[s]=[],i[s].push(l),e=l,n++)}else i[s].push(l),e=l,n++}this._latlngs=i,L.MultiPolyline.prototype.setLatLngs.call(this,this._latlngs)},_generate_Geodesic:function(t){var a,n,i,s=[],e=0
for(n=0;n<t.length;n++){for(s[e]=[],i=0;i<t[n].length-1;i++){var h=this._vincenty_inverse(L.latLng(t[n][i]),L.latLng(t[n][i+1])),l=L.latLng(t[n][i])
for(s[e].push(l),a=1;a<=this.options.steps;){var o=this._vincenty_direct(L.latLng(t[n][i]),h.initialBearing,h.distance/this.options.steps*a,this.options.wrap),r=L.latLng(o.lat,o.lng)
if(Math.abs(r.lng-l.lng)>180){var g=this._intersection(L.latLng(t[n][i]),h.initialBearing,{lat:-89,lng:r.lng-l.lng>0?-INTERSECT_LNG:INTERSECT_LNG},0)
g?(s[e].push(L.latLng(g.lat,g.lng)),e++,s[e]=[],l=L.latLng(g.lat,-g.lng),s[e].push(l)):(e++,s[e]=[],s[e].push(r),l=r,a++)}else s[e].push(r),l=r,a++}}e++}return s},_generate_GeodesicDashed:function(t){var a,n,i,s=[],e=0
for(n=0;n<t.length;n++){for(s[e]=[],i=0;i<t[n].length-1;i++){var h=this._vincenty_inverse(L.latLng(t[n][i]),L.latLng(t[n][i+1])),l=L.latLng(t[n][i])
for(s[e].push(l),a=1;a<=this.options.steps;){var o=this._vincenty_direct(L.latLng(t[n][i]),h.initialBearing,h.distance/this.options.steps*a-h.distance/this.options.steps*(1-this.options.dash),this.options.wrap),r=L.latLng(o.lat,o.lng)
if(Math.abs(r.lng-l.lng)>180){var g=this._intersection(L.latLng(t[n][i]),h.initialBearing,{lat:-89,lng:r.lng-l.lng>0?-INTERSECT_LNG:INTERSECT_LNG},0)
g?(s[e].push(L.latLng(g.lat,g.lng)),e++,s[e]=[],l=L.latLng(g.lat,-g.lng),s[e].push(l)):(e++,s[e]=[],s[e].push(r),l=r,a++)}else{s[e].push(r),e++
var M=this._vincenty_direct(L.latLng(t[n][i]),h.initialBearing,h.distance/this.options.steps*a,this.options.wrap)
s[e]=[],s[e].push(L.latLng(M.lat,M.lng)),a++}}}e++}return s},_vincenty_direct:function(t,a,n,i){var s,e=t.lat.toRadians(),h=t.lng.toRadians(),l=a.toRadians(),o=n,r=this.datum.ellipsoid.a,g=this.datum.ellipsoid.b,M=this.datum.ellipsoid.f,c=Math.sin(l),p=Math.cos(l),u=(1-M)*Math.tan(e),L=1/Math.sqrt(1+u*u),d=u*L,v=Math.atan2(u,p),_=L*c,f=1-_*_,y=f*(r*r-g*g)/(g*g),I=1+y/16384*(4096+y*(-768+y*(320-175*y))),N=y/1024*(256+y*(-128+y*(74-47*y))),R=o/(g*I),P=0
do{var m=Math.cos(2*v+R),b=Math.sin(R),E=Math.cos(R),T=N*b*(m+N/4*(E*(-1+2*m*m)-N/6*m*(-3+4*b*b)*(-3+4*m*m)))
s=R,R=o/(g*I)+T}while(Math.abs(R-s)>1e-12&&++P)
var G=d*b-L*E*p,D=Math.atan2(d*E+L*b*p,(1-M)*Math.sqrt(_*_+G*G)),B=Math.atan2(b*c,L*E-d*b*p),C=M/16*f*(4+M*(4-3*f)),S=B-(1-C)*M*_*(R+C*b*(m+C*E*(-1+2*m*m)))
if(i)var w=(h+S+3*Math.PI)%(2*Math.PI)-Math.PI
else var w=h+S
var q=Math.atan2(_,-G)
return{lat:D.toDegrees(),lng:w.toDegrees(),finalBearing:q.toDegrees()}},_vincenty_inverse:function(t,a){var n,i=t.lat.toRadians(),s=t.lng.toRadians(),e=a.lat.toRadians(),h=a.lng.toRadians(),l=this.datum.ellipsoid.a,o=this.datum.ellipsoid.b,r=this.datum.ellipsoid.f,g=h-s,M=(1-r)*Math.tan(i),c=1/Math.sqrt(1+M*M),p=M*c,u=(1-r)*Math.tan(e),L=1/Math.sqrt(1+u*u),d=u*L,v=g,_=0
do{var f=Math.sin(v),y=Math.cos(v),I=L*f*L*f+(c*d-p*L*y)*(c*d-p*L*y),N=Math.sqrt(I)
if(0==N)return 0
var R=p*d+c*L*y,P=Math.atan2(N,R),m=c*L*f/N,b=1-m*m,E=R-2*p*d/b
isNaN(E)&&(E=0)
var T=r/16*b*(4+r*(4-3*b))
n=v,v=g+(1-T)*r*m*(P+T*N*(E+T*R*(-1+2*E*E)))}while(Math.abs(v-n)>1e-12&&++_<100)
if(_>=100)return console.log("Formula failed to converge. Altering target position."),this._vincenty_inverse(t,{lat:a.lat,lng:a.lng-.01})
var G=b*(l*l-o*o)/(o*o),D=1+G/16384*(4096+G*(-768+G*(320-175*G))),B=G/1024*(256+G*(-128+G*(74-47*G))),C=B*N*(E+B/4*(R*(-1+2*E*E)-B/6*E*(-3+4*N*N)*(-3+4*E*E))),S=o*D*(P-C),w=Math.atan2(L*f,c*d-p*L*y),q=Math.atan2(c*f,-p*L+c*d*y)
return S=+S.toFixed(3),{distance:S,initialBearing:w.toDegrees(),finalBearing:q.toDegrees()}},_intersection:function(t,a,n,i){var s=t.lat.toRadians(),e=t.lng.toRadians(),h=n.lat.toRadians(),l=n.lng.toRadians(),o=(+a).toRadians(),r=(+i).toRadians(),g=h-s,M=l-e,c=2*Math.asin(Math.sqrt(Math.sin(g/2)*Math.sin(g/2)+Math.cos(s)*Math.cos(h)*Math.sin(M/2)*Math.sin(M/2)))
if(0==c)return null
var p=Math.acos((Math.sin(h)-Math.sin(s)*Math.cos(c))/(Math.sin(c)*Math.cos(s)))
isNaN(p)&&(p=0)
var u=Math.acos((Math.sin(s)-Math.sin(h)*Math.cos(c))/(Math.sin(c)*Math.cos(h)))
if(Math.sin(l-e)>0)var L=p,d=2*Math.PI-u
else var L=2*Math.PI-p,d=u
var v=(o-L+Math.PI)%(2*Math.PI)-Math.PI,_=(d-r+Math.PI)%(2*Math.PI)-Math.PI
if(0==Math.sin(v)&&0==Math.sin(_))return null
if(Math.sin(v)*Math.sin(_)<0)return null
var f=Math.acos(-Math.cos(v)*Math.cos(_)+Math.sin(v)*Math.sin(_)*Math.cos(c)),y=Math.atan2(Math.sin(c)*Math.sin(v)*Math.sin(_),Math.cos(_)+Math.cos(v)*Math.cos(f)),I=Math.asin(Math.sin(s)*Math.cos(y)+Math.cos(s)*Math.sin(y)*Math.cos(o)),N=Math.atan2(Math.sin(o)*Math.sin(y)*Math.cos(s),Math.cos(y)-Math.sin(s)*Math.sin(I)),R=e+N
return R=(R+3*Math.PI)%(2*Math.PI)-Math.PI,{lat:I.toDegrees(),lng:R.toDegrees()}},_merge_options:function(t,a){var n={}
for(var i in t)n[i]=t[i]
for(var i in a)n[i]=a[i]
return n}}),L.geodesic=function(t,a){return new L.Geodesic(t,a)}

View file

@ -7,6 +7,7 @@ require.config({
kojqui : '3rdparty/knockout-jqueryui', kojqui : '3rdparty/knockout-jqueryui',
sprintf : '3rdparty/sprintf/sprintf.min', sprintf : '3rdparty/sprintf/sprintf.min',
leaflet : '3rdparty/leaflet-0.7.3/leaflet', leaflet : '3rdparty/leaflet-0.7.3/leaflet',
geodesic : '3rdparty/leaflet-0.7.3/Leaflet.Geodesic.min',
text : '3rdparty/require/text', text : '3rdparty/require/text',
flot : '3rdparty/flot/jquery.flot', flot : '3rdparty/flot/jquery.flot',
flotresize : '3rdparty/flot/jquery.flot.resize', flotresize : '3rdparty/flot/jquery.flot.resize',

View file

@ -2,7 +2,7 @@
if (typeof define === "function" && define.amd) { if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module. // AMD. Register as an anonymous module.
define([ define([
'leaflet', 'props', './MapIcons', 'knockout' 'leaflet', 'props', './MapIcons', 'knockout', 'geodesic'
], factory); ], factory);
} else { } else {
// Browser globals // Browser globals
@ -31,13 +31,21 @@
}); });
self.waypointCountSubscription = self.waypointCount.subscribe(function() { self.waypointCountSubscription = self.waypointCount.subscribe(function() {
self.update(); self.update();
});
}) self.geodesic = L.geodesic([], {
weight: 5,
opacity: 0.5,
color: 'blue',
steps: 20,
}).addTo(map);
}, },
onRemove : function(map) { onRemove : function(map) {
this.waypointCountSubscription.dispose(); var self = this;
this.waypointCount.dispose(); self.waypointCountSubscription.dispose();
self.waypointCount.dispose();
map.removeLayer(self.geodesic);
leaflet.GeoJSON.prototype.onRemove.call(this, map); leaflet.GeoJSON.prototype.onRemove.call(this, map);
}, },
@ -55,8 +63,15 @@
var jqxhr = $.get(url).done(function(data) { var jqxhr = $.get(url).done(function(data) {
self.clearLayers(); self.clearLayers();
var geoJSON = self.routePropsToGeoJson(data); var geoJSON = self.routePropsToGeoJson(data);
if (geoJSON) if (geoJSON) {
self.addData(geoJSON); self.addData(geoJSON);
var latlngs = [];
geoJSON.features.forEach( function(f) {
if( f.geometry && f.geometry.coordinates )
latlngs.push( new L.LatLng(f.geometry.coordinates[1], f.geometry.coordinates[0] ) );
});
self.geodesic.setLatLngs([latlngs]);
}
}).fail(function(a, b) { }).fail(function(a, b) {
// self.stop(); // TODO: Should we? // self.stop(); // TODO: Should we?
alert('failed to load RouteManager data'); alert('failed to load RouteManager data');
@ -69,7 +84,6 @@
type : "FeatureCollection", type : "FeatureCollection",
features : [], features : [],
}; };
var lineString = [];
var root = new SGPropertyNode(props); var root = new SGPropertyNode(props);
root.getChildren("wp").forEach(function(wp) { root.getChildren("wp").forEach(function(wp) {
@ -80,7 +94,6 @@
var position = [ var position = [
lon, lat lon, lat
]; ];
lineString.push(position);
geoJSON.features.push({ geoJSON.features.push({
"type" : "Feature", "type" : "Feature",
@ -93,12 +106,7 @@
}); });
}); });
geoJSON.features.push({ if (geoJSON.features.length >= 2)
"type" : "LineString",
"coordinates" : lineString,
});
if (lineString.length >= 2)
return geoJSON; return geoJSON;
}, },