diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 4aaf2d2..0000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "examples/lib/Leaflet"] - path = examples/lib/Leaflet - url = https://github.com/Leaflet/Leaflet.git -[submodule "examples/lib/leaflet-tilejson"] - path = examples/lib/leaflet-tilejson - url = https://github.com/kartena/leaflet-tilejson.git diff --git a/README.md b/README.md index ceeeca7..10ffeae 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,44 @@ -[Leaflet.Geodesic](http://fragger.github.io/Leaflet.Geodesic/) +[Leaflet.Geodesic][example] ================ -### Description -Geodesic polylines and polygons for [Leaflet](https://github.com/Leaflet/Leaflet) - -Adds: -``` -L.GeodesicPolyline( latlngs, options? ) -L.GeodesicPolygon( latlngs, options? ) -L.GeodesicMultiPolyline( latlngs, options? ) -L.GeodesicMultiPolygon( latlngs, options? ) -``` -These will follow the curvature of the Earth and are used just like the normal versions of the constructors that are rendered as straight lines on the screen \ No newline at end of file +Geodesic polylines, polygons and circles for [Leaflet](https://github.com/Leaflet/Leaflet) 1.x. + +These will follow the curvature of the Earth and are used just like the normal versions of the constructors that are rendered as straight lines on the screen. + +[example]: https://raw.githack.com/IITC-CE/Leaflet.Geodesic/master/examples/index.html + + +`L.GeodesicPolyline` +------------------ + +**L.geodesicPolyline**( _latlngs_, <[Polyline options]> _options?_) + +- [Example] + +[Polyline options]: https://leafletjs.com/reference-1.5.0.html#polyline-option + + +`L.GeodesicPolygon` +------------------ + +**L.geodesicPolygon**( _latlngs_, <[Polyline options]> _options?_) + + +`L.GeodesicCircle` +------------------ + +**L.geodesicCircle**(<[LatLng]> _latlng_, <[Circle options]> _options?_) + +- [Example][example-circle] + +[example-circle]: https://raw.githack.com/IITC-CE/Leaflet.Geodesic/master/examples/circle.html + +
+(obsolete way) + + +**L.geodesicCircle**(<[LatLng]> _latlng_, _radius_, <[Circle options]> _options?_) +
+ +[LatLng]: https://leafletjs.com/reference-1.5.0.html#latlng +[Circle options]: https://leafletjs.com/reference-1.5.0.html#circle-option diff --git a/examples/circle.html b/examples/circle.html new file mode 100644 index 0000000..7198f1d --- /dev/null +++ b/examples/circle.html @@ -0,0 +1,50 @@ + + + + Leaflet.Geodesic example + + + + + + + +
+ + + diff --git a/examples/index.html b/examples/index.html index 1542c08..c4b7a0e 100644 --- a/examples/index.html +++ b/examples/index.html @@ -1,22 +1,31 @@ + Leaflet.Geodesic example - - - + + + +
- - - - - - \ No newline at end of file + diff --git a/examples/lib/Leaflet b/examples/lib/Leaflet deleted file mode 160000 index c1d410f..0000000 --- a/examples/lib/Leaflet +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c1d410f2703f0832618c997225e7360f6a292c58 diff --git a/examples/lib/leaflet-tilejson b/examples/lib/leaflet-tilejson deleted file mode 160000 index 94eead1..0000000 --- a/examples/lib/leaflet-tilejson +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 94eead167b606af9cb736eefe5c3b04fc7a75e2f diff --git a/examples/osm.tilejson.js b/examples/osm.tilejson.js deleted file mode 100644 index ddc579c..0000000 --- a/examples/osm.tilejson.js +++ /dev/null @@ -1,17 +0,0 @@ -window.osmTileJSON = { - "tilejson": "2.0.0", - "name": "OpenStreetMap", - "description": "A free editable map of the whole world.", - "version": "1.0.0", - "attribution": "© OpenStreetMap contributors, CC-BY-SA", - "scheme": "xyz", - "tiles": [ - "http://a.tile.openstreetmap.org/${z}/${x}/${y}.png", - "http://b.tile.openstreetmap.org/${z}/${x}/${y}.png", - "http://c.tile.openstreetmap.org/${z}/${x}/${y}.png" - ], - "minzoom": 2, - "maxzoom": 18, - "bounds": [ -180, -85, 180, 85 ], - "center": [ 11.9, 57.7, 8 ] -}; \ No newline at end of file diff --git a/examples/style.css b/examples/style.css deleted file mode 100644 index 4a3966b..0000000 --- a/examples/style.css +++ /dev/null @@ -1,11 +0,0 @@ -html, body { - height: 100%; - padding: 0; - margin: 0; -} - -#map { - position: absolute; - width: 100%; - height: 100%; -} \ No newline at end of file diff --git a/src/L.Geodesic.js b/src/L.Geodesic.js index b23ff88..5276ea0 100644 --- a/src/L.Geodesic.js +++ b/src/L.Geodesic.js @@ -1,105 +1,209 @@ (function () { - function geodesicPoly(Klass, fill) { - return Klass.extend({ - initialize: function (latlngs, options) { - Klass.prototype.initialize.call(this, L.geodesicConvertLines(latlngs, fill), options); - this._latlngsinit = this._convertLatLngs(latlngs); - }, - getLatLngs: function () { - return this._latlngsinit; - }, - setLatLngs: function (latlngs) { - this._latlngsinit = this._convertLatLngs(latlngs); - this._latlngs = this._convertLatLngs(L.geodesicConvertLines(this._latlngsinit, fill)); - return this.redraw(); - }, - addLatLng: function (latlng) { - this._latlngsinit.push(L.latLng(latlng)); - this._latlngs = this._convertLatLngs(L.geodesicConvertLines(this._latlngsinit, fill)); - return this.redraw(); - }, - spliceLatLngs: function () { // (Number index, Number howMany) - var removed = [].splice.apply(this._latlngsinit, arguments); - this._convertLatLngs(this._latlngsinit); - this._latlngs = this._convertLatLngs(L.geodesicConvertLines(this._latlngsinit, fill)); - this.redraw(); - return removed; - } - }); - } - - function geodesicConvertLine(startLatlng, endLatlng, convertedPoints) { - var i, - R = 6378137, // earth radius in meters (doesn't have to be exact) - maxlength = 5000, // meters before splitting - d2r = L.LatLng.DEG_TO_RAD, - r2d = L.LatLng.RAD_TO_DEG, - lat1, lat2, lng1, lng2, dLng, d, segments, - f, A, B, x, y, z, fLat, fLng; - - dLng = Math.abs(endLatlng.lng - startLatlng.lng) * d2r; - lat1 = startLatlng.lat * d2r; - lat2 = endLatlng.lat * d2r; - lng1 = startLatlng.lng * d2r; - lng2 = endLatlng.lng * d2r; - - // http://en.wikipedia.org/wiki/Great-circle_distance - d = Math.atan2(Math.sqrt( Math.pow(Math.cos(lat2) * Math.sin(dLng), 2) + Math.pow(Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLng), 2) ), Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos(dLng)); - - segments = Math.ceil(d * R / maxlength); - for (i = 1; i <= segments; i++) { - // http://williams.best.vwh.net/avform.htm#Intermediate - f = i / segments; - A = Math.sin((1-f)*d) / Math.sin(d); - B = Math.sin(f*d) / Math.sin(d); - x = A * Math.cos(lat1) * Math.cos(lng1) + B * Math.cos(lat2) * Math.cos(lng2); - y = A * Math.cos(lat1) * Math.sin(lng1) + B * Math.cos(lat2) * Math.sin(lng2); - z = A * Math.sin(lat1) + B * Math.sin(lat2); - fLat = r2d * Math.atan2(z, Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))); - fLng = r2d * Math.atan2(y, x); - - convertedPoints.push(L.latLng([fLat, fLng])); + // constants + var d2r = Math.PI/180.0; + var r2d = 180.0/Math.PI; + var earthR = 6367000.0; // earth radius in meters (doesn't have to be exact) + + // alternative geodesic line intermediate points function + // as north/south lines have very little curvature in the projection, we can use longitude (east/west) seperation + // to calculate intermediate points. hopefully this will avoid the rounding issues seen in the full intermediate + // points code that have been seen + function geodesicConvertLine (start, end, convertedPoints) { // push intermediate points into convertedPoints + + var lng1 = start.lng * d2r; + var lng2 = end.lng * d2r; + var dLng = lng1-lng2; + + var segments = Math.floor(Math.abs(dLng * earthR / this.options.segmentsCoeff)); + if (segments < 2) { return; } + + // maths based on https://edwilliams.org/avform.htm#Int + + // pre-calculate some constant values for the loop + var lat1 = start.lat * d2r; + var lat2 = end.lat * d2r; + var sinLat1 = Math.sin(lat1); + var sinLat2 = Math.sin(lat2); + var cosLat1 = Math.cos(lat1); + var cosLat2 = Math.cos(lat2); + var sinLat1CosLat2 = sinLat1*cosLat2; + var sinLat2CosLat1 = sinLat2*cosLat1; + var cosLat1CosLat2SinDLng = cosLat1*cosLat2*Math.sin(dLng); + + for (var i=1; i < segments; i++) { + var iLng = lng1-dLng*(i/segments); + var iLat = Math.atan( + (sinLat1CosLat2 * Math.sin(iLng-lng2) - sinLat2CosLat1 * Math.sin(iLng-lng1)) + / cosLat1CosLat2SinDLng + ); + convertedPoints.push(L.latLng(iLat*r2d, iLng*r2d)); } } - L.geodesicConvertLines = function (latlngs, fill) { - var i, j, len, geodesiclatlngs = []; - for (i = 0, len = latlngs.length; i < len; i++) { - if (L.Util.isArray(latlngs[i]) && typeof latlngs[i][0] !== 'number') { - return; - } - latlngs[i] = L.latLng(latlngs[i]); - } - - if(!fill) { - geodesiclatlngs.push(latlngs[0]); + + // iterate pairs of connected vertices with fn(), adding new intermediate vertices (if returned) + function processPoly (latlngs, fn) { + var result = []; + + // var isPolygon = this.options.fill; // !wrong: L.Draw use options.fill with polylines + var isPolygon = this instanceof L.Polygon; + if (isPolygon) { + latlngs.push(latlngs[0]); + } else { + result.push(latlngs[0]); } - for (i = 0, len = latlngs.length - 1; i < len; i++) { - geodesicConvertLine(latlngs[i], latlngs[i+1], geodesiclatlngs); + for (var i = 0, len = latlngs.length - 1; i < len; i++) { + fn.call(this, latlngs[i], latlngs[i+1], result); + result.push(latlngs[i+1]); } - if(fill) { - geodesicConvertLine(latlngs[len], latlngs[0], geodesiclatlngs); + return result; + } + + function geodesicConvertLines (latlngs) { + if (latlngs.length === 0) { + return []; } + + // geodesic calculations have issues when crossing the anti-meridian. so offset the points + // so this isn't an issue, then add back the offset afterwards + // a center longitude would be ideal - but the start point longitude will be 'good enough' + var lngOffset = latlngs[0].lng; + + // points are wrapped after being offset relative to the first point coordinate, so they're + // within +-180 degrees + latlngs = latlngs.map(function (a) { return L.latLng(a.lat, a.lng-lngOffset).wrap(); }); + + var geodesiclatlngs = this._processPoly(latlngs,this._geodesicConvertLine); + + // now add back the offset subtracted above. no wrapping here - the drawing code handles + // things better when there's no sudden jumps in coordinates. yes, lines will extend + // beyond +-180 degrees - but they won't be 'broken' + geodesiclatlngs = geodesiclatlngs.map(function (a) { return L.latLng(a.lat, a.lng+lngOffset); }); + return geodesiclatlngs; } - - L.GeodesicPolyline = geodesicPoly(L.Polyline, 0); - L.GeodesicPolygon = geodesicPoly(L.Polygon, 1); - //L.GeodesicMultiPolyline = createMulti(L.GeodesicPolyline); - //L.GeodesicMultiPolygon = createMulti(L.GeodesicPolygon); + var polyOptions = { + segmentsCoeff: 5000 + }; + + var PolyMixin = { + _geodesicConvertLine: geodesicConvertLine, + + _processPoly: processPoly, + + _geodesicConvertLines: geodesicConvertLines, + + _geodesicConvert: function () { + this._latlngs = this._geodesicConvertLines(this._latlngsinit); + this._convertLatLngs(this._latlngs); // update bounds + }, + + options: polyOptions, - /*L.GeodesicMultiPolyline = L.MultiPolyline.extend({ initialize: function (latlngs, options) { - L.MultiPolyline.prototype.initialize.call(this, L.geodesicConvertLines(latlngs), options); + L.Polyline.prototype.initialize.call(this, latlngs, options); + this._geodesicConvert(); + }, + + getLatLngs: function () { + return this._latlngsinit; + }, + + _setLatLngs: function (latlngs) { + this._bounds = L.latLngBounds(); + this._latlngsinit = this._convertLatLngs(latlngs); + }, + + _defaultShape: function () { + var latlngs = this._latlngsinit; + return L.LineUtil.isFlat(latlngs) ? latlngs : latlngs[0]; + }, + + redraw: function () { + this._geodesicConvert(); + return L.Path.prototype.redraw.call(this); } - });*/ + }; - /*L.GeodesicMultiPolygon = L.MultiPolygon.extend({ - initialize: function (latlngs, options) { - L.MultiPolygon.prototype.initialize.call(this, L.geodesicConvertLines(latlngs), options); + L.GeodesicPolyline = L.Polyline.extend(PolyMixin); + + PolyMixin.options = polyOptions; // workaround for https://github.com/Leaflet/Leaflet/pull/6766/ + L.GeodesicPolygon = L.Polygon.extend(PolyMixin); + + L.GeodesicCircle = L.Polygon.extend({ + options: { + segmentsCoeff: 1000, + segmentsMin: 48 + }, + + initialize: function (latlng, options, legacyOptions) { + if (typeof options === 'number') { + // Backwards compatibility with 0.7.x factory (latlng, radius, options?) + options = L.extend({}, legacyOptions, {radius: options}); + } + this._latlng = L.latLng(latlng); + this._radius = options.radius; // note: https://github.com/Leaflet/Leaflet/issues/6656 + var points = this._calcPoints(); + L.Polygon.prototype.initialize.call(this, points, options); + }, + + setLatLng: function (latlng) { + this._latlng = L.latLng(latlng); + var points = this._calcPoints(); + this.setLatLngs(points); + }, + + setRadius: function (radius) { + this._radius = radius; + var points = this._calcPoints(); + this.setLatLngs(points); + }, + + getLatLng: function () { + return this._latlng; + }, + + getRadius: function () { + return this._radius; + }, + + _calcPoints: function () { + + // circle radius as an angle from the centre of the earth + var radRadius = this._radius / earthR; + + // pre-calculate various values used for every point on the circle + var centreLat = this._latlng.lat * d2r; + var centreLng = this._latlng.lng * d2r; + + var cosCentreLat = Math.cos(centreLat); + var sinCentreLat = Math.sin(centreLat); + + var cosRadRadius = Math.cos(radRadius); + var sinRadRadius = Math.sin(radRadius); + + var calcLatLngAtAngle = function (angle) { + var lat = Math.asin(sinCentreLat*cosRadRadius + cosCentreLat*sinRadRadius*Math.cos(angle)); + var lng = centreLng + Math.atan2(Math.sin(angle)*sinRadRadius*cosCentreLat, cosRadRadius-sinCentreLat*Math.sin(lat)); + + return L.latLng(lat * r2d,lng * r2d); + }; + + var o = this.options; + var segments = Math.max(o.segmentsMin,Math.floor(this._radius/o.segmentsCoeff)); + var points = []; + for (var i=0; i