// Packaging/modules magic dance. (function (factory) { var L; if (typeof define === 'function' && define.amd) { // AMD define(['leaflet'], factory); } else if (typeof module !== 'undefined') { // Node/CommonJS L = require('leaflet'); module.exports = factory(L); } else { // Browser globals if (typeof window.L === 'undefined') throw 'Leaflet must be loaded first'; factory(window.L); } }(function (L) { "use strict"; L.Polyline._flat = L.LineUtil.isFlat || L.Polyline._flat || function (latlngs) { // true if it's a flat array of latlngs; false if nested return !L.Util.isArray(latlngs[0]) || (typeof latlngs[0][0] !== 'object' && typeof latlngs[0][0] !== 'undefined'); }; /** * @fileOverview Leaflet Geometry utilities for distances and linear referencing. * @name L.GeometryUtil */ L.GeometryUtil = L.extend(L.GeometryUtil || {}, { /** Shortcut function for planar distance between two {L.LatLng} at current zoom. @tutorial distance-length @param {L.Map} map Leaflet map to be used for this method @param {L.LatLng} latlngA geographical point A @param {L.LatLng} latlngB geographical point B @returns {Number} planar distance */ distance: function (map, latlngA, latlngB) { return map.latLngToLayerPoint(latlngA).distanceTo(map.latLngToLayerPoint(latlngB)); }, /** Shortcut function for planar distance between a {L.LatLng} and a segment (A-B). @param {L.Map} map Leaflet map to be used for this method @param {L.LatLng} latlng - The position to search @param {L.LatLng} latlngA geographical point A of the segment @param {L.LatLng} latlngB geographical point B of the segment @returns {Number} planar distance */ distanceSegment: function (map, latlng, latlngA, latlngB) { var p = map.latLngToLayerPoint(latlng), p1 = map.latLngToLayerPoint(latlngA), p2 = map.latLngToLayerPoint(latlngB); return L.LineUtil.pointToSegmentDistance(p, p1, p2); }, /** Shortcut function for converting distance to readable distance. @param {Number} distance distance to be converted @param {String} unit 'metric' or 'imperial' @returns {String} in yard or miles */ readableDistance: function (distance, unit) { var isMetric = (unit !== 'imperial'), distanceStr; if (isMetric) { // show metres when distance is < 1km, then show km if (distance > 1000) { distanceStr = (distance / 1000).toFixed(2) + ' km'; } else { distanceStr = distance.toFixed(1) + ' m'; } } else { distance *= 1.09361; if (distance > 1760) { distanceStr = (distance / 1760).toFixed(2) + ' miles'; } else { distanceStr = distance.toFixed(1) + ' yd'; } } return distanceStr; }, /** Returns true if the latlng belongs to segment A-B @param {L.LatLng} latlng - The position to search @param {L.LatLng} latlngA geographical point A of the segment @param {L.LatLng} latlngB geographical point B of the segment @param {?Number} [tolerance=0.2] tolerance to accept if latlng belongs really @returns {boolean} */ belongsSegment: function(latlng, latlngA, latlngB, tolerance) { tolerance = tolerance === undefined ? 0.2 : tolerance; var hypotenuse = latlngA.distanceTo(latlngB), delta = latlngA.distanceTo(latlng) + latlng.distanceTo(latlngB) - hypotenuse; return delta/hypotenuse < tolerance; }, /** * Returns total length of line * @tutorial distance-length * * @param {L.Polyline|Array|Array} coords Set of coordinates * @returns {Number} Total length (pixels for Point, meters for LatLng) */ length: function (coords) { var accumulated = L.GeometryUtil.accumulatedLengths(coords); return accumulated.length > 0 ? accumulated[accumulated.length-1] : 0; }, /** * Returns a list of accumulated length along a line. * @param {L.Polyline|Array|Array} coords Set of coordinates * @returns {Array} Array of accumulated lengths (pixels for Point, meters for LatLng) */ accumulatedLengths: function (coords) { if (typeof coords.getLatLngs == 'function') { coords = coords.getLatLngs(); } if (coords.length === 0) return []; var total = 0, lengths = [0]; for (var i = 0, n = coords.length - 1; i< n; i++) { total += coords[i].distanceTo(coords[i+1]); lengths.push(total); } return lengths; }, /** Returns the closest point of a {L.LatLng} on the segment (A-B) @tutorial closest @param {L.Map} map Leaflet map to be used for this method @param {L.LatLng} latlng - The position to search @param {L.LatLng} latlngA geographical point A of the segment @param {L.LatLng} latlngB geographical point B of the segment @returns {L.LatLng} Closest geographical point */ closestOnSegment: function (map, latlng, latlngA, latlngB) { var maxzoom = map.getMaxZoom(); if (maxzoom === Infinity) maxzoom = map.getZoom(); var p = map.project(latlng, maxzoom), p1 = map.project(latlngA, maxzoom), p2 = map.project(latlngB, maxzoom), closest = L.LineUtil.closestPointOnSegment(p, p1, p2); return map.unproject(closest, maxzoom); }, /** Returns the closest latlng on layer. Accept nested arrays @tutorial closest @param {L.Map} map Leaflet map to be used for this method @param {Array|Array>|L.PolyLine|L.Polygon} layer - Layer that contains the result @param {L.LatLng} latlng - The position to search @param {?boolean} [vertices=false] - Whether to restrict to path vertices. @returns {L.LatLng} Closest geographical point or null if layer param is incorrect */ closest: function (map, layer, latlng, vertices) { var latlngs, mindist = Infinity, result = null, i, n, distance, subResult; if (layer instanceof Array) { // if layer is Array> if (layer[0] instanceof Array && typeof layer[0][0] !== 'number') { // if we have nested arrays, we calc the closest for each array // recursive for (i = 0; i < layer.length; i++) { subResult = L.GeometryUtil.closest(map, layer[i], latlng, vertices); if (subResult && subResult.distance < mindist) { mindist = subResult.distance; result = subResult; } } return result; } else if (layer[0] instanceof L.LatLng || typeof layer[0][0] === 'number' || typeof layer[0].lat === 'number') { // we could have a latlng as [x,y] with x & y numbers or {lat, lng} layer = L.polyline(layer); } else { return result; } } // if we don't have here a Polyline, that means layer is incorrect // see https://github.com/makinacorpus/Leaflet.GeometryUtil/issues/23 if (! ( layer instanceof L.Polyline ) ) return result; // deep copy of latlngs latlngs = JSON.parse(JSON.stringify(layer.getLatLngs().slice(0))); // add the last segment for L.Polygon if (layer instanceof L.Polygon) { // add the last segment for each child that is a nested array var addLastSegment = function(latlngs) { if (L.Polyline._flat(latlngs)) { latlngs.push(latlngs[0]); } else { for (var i = 0; i < latlngs.length; i++) { addLastSegment(latlngs[i]); } } }; addLastSegment(latlngs); } // we have a multi polygon / multi polyline / polygon with holes // use recursive to explore and return the good result if ( ! L.Polyline._flat(latlngs) ) { for (i = 0; i < latlngs.length; i++) { // if we are at the lower level, and if we have a L.Polygon, we add the last segment subResult = L.GeometryUtil.closest(map, latlngs[i], latlng, vertices); if (subResult.distance < mindist) { mindist = subResult.distance; result = subResult; } } return result; } else { // Lookup vertices if (vertices) { for(i = 0, n = latlngs.length; i < n; i++) { var ll = latlngs[i]; distance = L.GeometryUtil.distance(map, latlng, ll); if (distance < mindist) { mindist = distance; result = ll; result.distance = distance; } } return result; } // Keep the closest point of all segments for (i = 0, n = latlngs.length; i < n-1; i++) { var latlngA = latlngs[i], latlngB = latlngs[i+1]; distance = L.GeometryUtil.distanceSegment(map, latlng, latlngA, latlngB); if (distance <= mindist) { mindist = distance; result = L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB); result.distance = distance; } } return result; } }, /** Returns the closest layer to latlng among a list of layers. @tutorial closest @param {L.Map} map Leaflet map to be used for this method @param {Array} layers Set of layers @param {L.LatLng} latlng - The position to search @returns {object} ``{layer, latlng, distance}`` or ``null`` if list is empty; */ closestLayer: function (map, layers, latlng) { var mindist = Infinity, result = null, ll = null, distance = Infinity; for (var i = 0, n = layers.length; i < n; i++) { var layer = layers[i]; if (layer instanceof L.LayerGroup) { // recursive var subResult = L.GeometryUtil.closestLayer(map, layer.getLayers(), latlng); if (subResult.distance < mindist) { mindist = subResult.distance; result = subResult; } } else { // Single dimension, snap on points, else snap on closest if (typeof layer.getLatLng == 'function') { ll = layer.getLatLng(); distance = L.GeometryUtil.distance(map, latlng, ll); } else { ll = L.GeometryUtil.closest(map, layer, latlng); if (ll) distance = ll.distance; // Can return null if layer has no points. } if (distance < mindist) { mindist = distance; result = {layer: layer, latlng: ll, distance: distance}; } } } return result; }, /** Returns the n closest layers to latlng among a list of input layers. @param {L.Map} map - Leaflet map to be used for this method @param {Array} layers - Set of layers @param {L.LatLng} latlng - The position to search @param {?Number} [n=layers.length] - the expected number of output layers. @returns {Array} an array of objects ``{layer, latlng, distance}`` or ``null`` if the input is invalid (empty list or negative n) */ nClosestLayers: function (map, layers, latlng, n) { n = typeof n === 'number' ? n : layers.length; if (n < 1 || layers.length < 1) { return null; } var results = []; var distance, ll; for (var i = 0, m = layers.length; i < m; i++) { var layer = layers[i]; if (layer instanceof L.LayerGroup) { // recursive var subResult = L.GeometryUtil.closestLayer(map, layer.getLayers(), latlng); results.push(subResult); } else { // Single dimension, snap on points, else snap on closest if (typeof layer.getLatLng == 'function') { ll = layer.getLatLng(); distance = L.GeometryUtil.distance(map, latlng, ll); } else { ll = L.GeometryUtil.closest(map, layer, latlng); if (ll) distance = ll.distance; // Can return null if layer has no points. } results.push({layer: layer, latlng: ll, distance: distance}); } } results.sort(function(a, b) { return a.distance - b.distance; }); if (results.length > n) { return results.slice(0, n); } else { return results; } }, /** * Returns all layers within a radius of the given position, in an ascending order of distance. @param {L.Map} map Leaflet map to be used for this method @param {Array} layers - A list of layers. @param {L.LatLng} latlng - The position to search @param {?Number} [radius=Infinity] - Search radius in pixels @return {object[]} an array of objects including layer within the radius, closest latlng, and distance */ layersWithin: function(map, layers, latlng, radius) { radius = typeof radius == 'number' ? radius : Infinity; var results = []; var ll = null; var distance = 0; for (var i = 0, n = layers.length; i < n; i++) { var layer = layers[i]; if (typeof layer.getLatLng == 'function') { ll = layer.getLatLng(); distance = L.GeometryUtil.distance(map, latlng, ll); } else { ll = L.GeometryUtil.closest(map, layer, latlng); if (ll) distance = ll.distance; // Can return null if layer has no points. } if (ll && distance < radius) { results.push({layer: layer, latlng: ll, distance: distance}); } } var sortedResults = results.sort(function(a, b) { return a.distance - b.distance; }); return sortedResults; }, /** Returns the closest position from specified {LatLng} among specified layers, with a maximum tolerance in pixels, providing snapping behaviour. @tutorial closest @param {L.Map} map Leaflet map to be used for this method @param {Array} layers - A list of layers to snap on. @param {L.LatLng} latlng - The position to snap @param {?Number} [tolerance=Infinity] - Maximum number of pixels. @param {?boolean} [withVertices=true] - Snap to layers vertices or segment points (not only vertex) @returns {object} with snapped {LatLng} and snapped {Layer} or null if tolerance exceeded. */ closestLayerSnap: function (map, layers, latlng, tolerance, withVertices) { tolerance = typeof tolerance == 'number' ? tolerance : Infinity; withVertices = typeof withVertices == 'boolean' ? withVertices : true; var result = L.GeometryUtil.closestLayer(map, layers, latlng); if (!result || result.distance > tolerance) return null; // If snapped layer is linear, try to snap on vertices (extremities and middle points) if (withVertices && typeof result.layer.getLatLngs == 'function') { var closest = L.GeometryUtil.closest(map, result.layer, result.latlng, true); if (closest.distance < tolerance) { result.latlng = closest; result.distance = L.GeometryUtil.distance(map, closest, latlng); } } return result; }, /** Returns the Point located on a segment at the specified ratio of the segment length. @param {L.Point} pA coordinates of point A @param {L.Point} pB coordinates of point B @param {Number} the length ratio, expressed as a decimal between 0 and 1, inclusive. @returns {L.Point} the interpolated point. */ interpolateOnPointSegment: function (pA, pB, ratio) { return L.point( (pA.x * (1 - ratio)) + (ratio * pB.x), (pA.y * (1 - ratio)) + (ratio * pB.y) ); }, /** Returns the coordinate of the point located on a line at the specified ratio of the line length. @param {L.Map} map Leaflet map to be used for this method @param {Array|L.PolyLine} latlngs Set of geographical points @param {Number} ratio the length ratio, expressed as a decimal between 0 and 1, inclusive @returns {Object} an object with latLng ({LatLng}) and predecessor ({Number}), the index of the preceding vertex in the Polyline (-1 if the interpolated point is the first vertex) */ interpolateOnLine: function (map, latLngs, ratio) { latLngs = (latLngs instanceof L.Polyline) ? latLngs.getLatLngs() : latLngs; var n = latLngs.length; if (n < 2) { return null; } // ensure the ratio is between 0 and 1; ratio = Math.max(Math.min(ratio, 1), 0); if (ratio === 0) { return { latLng: latLngs[0] instanceof L.LatLng ? latLngs[0] : L.latLng(latLngs[0]), predecessor: -1 }; } if (ratio == 1) { return { latLng: latLngs[latLngs.length -1] instanceof L.LatLng ? latLngs[latLngs.length -1] : L.latLng(latLngs[latLngs.length -1]), predecessor: latLngs.length - 2 }; } // project the LatLngs as Points, // and compute total planar length of the line at max precision var maxzoom = map.getMaxZoom(); if (maxzoom === Infinity) maxzoom = map.getZoom(); var pts = []; var lineLength = 0; for(var i = 0; i < n; i++) { pts[i] = map.project(latLngs[i], maxzoom); if(i > 0) lineLength += pts[i-1].distanceTo(pts[i]); } var ratioDist = lineLength * ratio; // follow the line segments [ab], adding lengths, // until we find the segment where the points should lie on var cumulativeDistanceToA = 0, cumulativeDistanceToB = 0; for (var i = 0; cumulativeDistanceToB < ratioDist; i++) { var pointA = pts[i], pointB = pts[i+1]; cumulativeDistanceToA = cumulativeDistanceToB; cumulativeDistanceToB += pointA.distanceTo(pointB); } if (pointA == undefined && pointB == undefined) { // Happens when line has no length var pointA = pts[0], pointB = pts[1], i = 1; } // compute the ratio relative to the segment [ab] var segmentRatio = ((cumulativeDistanceToB - cumulativeDistanceToA) !== 0) ? ((ratioDist - cumulativeDistanceToA) / (cumulativeDistanceToB - cumulativeDistanceToA)) : 0; var interpolatedPoint = L.GeometryUtil.interpolateOnPointSegment(pointA, pointB, segmentRatio); return { latLng: map.unproject(interpolatedPoint, maxzoom), predecessor: i-1 }; }, /** Returns a float between 0 and 1 representing the location of the closest point on polyline to the given latlng, as a fraction of total line length. (opposite of L.GeometryUtil.interpolateOnLine()) @param {L.Map} map Leaflet map to be used for this method @param {L.PolyLine} polyline Polyline on which the latlng will be search @param {L.LatLng} latlng The position to search @returns {Number} Float between 0 and 1 */ locateOnLine: function (map, polyline, latlng) { var latlngs = polyline.getLatLngs(); if (latlng.equals(latlngs[0])) return 0.0; if (latlng.equals(latlngs[latlngs.length-1])) return 1.0; var point = L.GeometryUtil.closest(map, polyline, latlng, false), lengths = L.GeometryUtil.accumulatedLengths(latlngs), total_length = lengths[lengths.length-1], portion = 0, found = false; for (var i=0, n = latlngs.length-1; i < n; i++) { var l1 = latlngs[i], l2 = latlngs[i+1]; portion = lengths[i]; if (L.GeometryUtil.belongsSegment(point, l1, l2, 0.001)) { portion += l1.distanceTo(point); found = true; break; } } if (!found) { throw "Could not interpolate " + latlng.toString() + " within " + polyline.toString(); } return portion / total_length; }, /** Returns a clone with reversed coordinates. @param {L.PolyLine} polyline polyline to reverse @returns {L.PolyLine} polyline reversed */ reverse: function (polyline) { return L.polyline(polyline.getLatLngs().slice(0).reverse()); }, /** Returns a sub-part of the polyline, from start to end. If start is superior to end, returns extraction from inverted line. @param {L.Map} map Leaflet map to be used for this method @param {L.PolyLine} polyline Polyline on which will be extracted the sub-part @param {Number} start ratio, expressed as a decimal between 0 and 1, inclusive @param {Number} end ratio, expressed as a decimal between 0 and 1, inclusive @returns {Array} new polyline */ extract: function (map, polyline, start, end) { if (start > end) { return L.GeometryUtil.extract(map, L.GeometryUtil.reverse(polyline), 1.0-start, 1.0-end); } // Bound start and end to [0-1] start = Math.max(Math.min(start, 1), 0); end = Math.max(Math.min(end, 1), 0); var latlngs = polyline.getLatLngs(), startpoint = L.GeometryUtil.interpolateOnLine(map, polyline, start), endpoint = L.GeometryUtil.interpolateOnLine(map, polyline, end); // Return single point if start == end if (start == end) { var point = L.GeometryUtil.interpolateOnLine(map, polyline, end); return [point.latLng]; } // Array.slice() works indexes at 0 if (startpoint.predecessor == -1) startpoint.predecessor = 0; if (endpoint.predecessor == -1) endpoint.predecessor = 0; var result = latlngs.slice(startpoint.predecessor+1, endpoint.predecessor+1); result.unshift(startpoint.latLng); result.push(endpoint.latLng); return result; }, /** Returns true if first polyline ends where other second starts. @param {L.PolyLine} polyline First polyline @param {L.PolyLine} other Second polyline @returns {bool} */ isBefore: function (polyline, other) { if (!other) return false; var lla = polyline.getLatLngs(), llb = other.getLatLngs(); return (lla[lla.length-1]).equals(llb[0]); }, /** Returns true if first polyline starts where second ends. @param {L.PolyLine} polyline First polyline @param {L.PolyLine} other Second polyline @returns {bool} */ isAfter: function (polyline, other) { if (!other) return false; var lla = polyline.getLatLngs(), llb = other.getLatLngs(); return (lla[0]).equals(llb[llb.length-1]); }, /** Returns true if first polyline starts where second ends or start. @param {L.PolyLine} polyline First polyline @param {L.PolyLine} other Second polyline @returns {bool} */ startsAtExtremity: function (polyline, other) { if (!other) return false; var lla = polyline.getLatLngs(), llb = other.getLatLngs(), start = lla[0]; return start.equals(llb[0]) || start.equals(llb[llb.length-1]); }, /** Returns horizontal angle in degres between two points. @param {L.Point} a Coordinates of point A @param {L.Point} b Coordinates of point B @returns {Number} horizontal angle */ computeAngle: function(a, b) { return (Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI); }, /** Returns slope (Ax+B) between two points. @param {L.Point} a Coordinates of point A @param {L.Point} b Coordinates of point B @returns {Object} with ``a`` and ``b`` properties. */ computeSlope: function(a, b) { var s = (b.y - a.y) / (b.x - a.x), o = a.y - (s * a.x); return {'a': s, 'b': o}; }, /** Returns LatLng of rotated point around specified LatLng center. @param {L.LatLng} latlngPoint: point to rotate @param {double} angleDeg: angle to rotate in degrees @param {L.LatLng} latlngCenter: center of rotation @returns {L.LatLng} rotated point */ rotatePoint: function(map, latlngPoint, angleDeg, latlngCenter) { var maxzoom = map.getMaxZoom(); if (maxzoom === Infinity) maxzoom = map.getZoom(); var angleRad = angleDeg*Math.PI/180, pPoint = map.project(latlngPoint, maxzoom), pCenter = map.project(latlngCenter, maxzoom), x2 = Math.cos(angleRad)*(pPoint.x-pCenter.x) - Math.sin(angleRad)*(pPoint.y-pCenter.y) + pCenter.x, y2 = Math.sin(angleRad)*(pPoint.x-pCenter.x) + Math.cos(angleRad)*(pPoint.y-pCenter.y) + pCenter.y; return map.unproject(new L.Point(x2,y2), maxzoom); }, /** Returns the bearing in degrees clockwise from north (0 degrees) from the first L.LatLng to the second, at the first LatLng @param {L.LatLng} latlng1: origin point of the bearing @param {L.LatLng} latlng2: destination point of the bearing @returns {float} degrees clockwise from north. */ bearing: function(latlng1, latlng2) { var rad = Math.PI / 180, lat1 = latlng1.lat * rad, lat2 = latlng2.lat * rad, lon1 = latlng1.lng * rad, lon2 = latlng2.lng * rad, y = Math.sin(lon2 - lon1) * Math.cos(lat2), x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1); var bearing = ((Math.atan2(y, x) * 180 / Math.PI) + 360) % 360; return bearing >= 180 ? bearing-360 : bearing; }, /** Returns the point that is a distance and heading away from the given origin point. @param {L.LatLng} latlng: origin point @param {float} heading: heading in degrees, clockwise from 0 degrees north. @param {float} distance: distance in meters @returns {L.latLng} the destination point. Many thanks to Chris Veness at http://www.movable-type.co.uk/scripts/latlong.html for a great reference and examples. */ destination: function(latlng, heading, distance) { heading = (heading + 360) % 360; var rad = Math.PI / 180, radInv = 180 / Math.PI, R = 6378137, // approximation of Earth's radius lon1 = latlng.lng * rad, lat1 = latlng.lat * rad, rheading = heading * rad, sinLat1 = Math.sin(lat1), cosLat1 = Math.cos(lat1), cosDistR = Math.cos(distance / R), sinDistR = Math.sin(distance / R), lat2 = Math.asin(sinLat1 * cosDistR + cosLat1 * sinDistR * Math.cos(rheading)), lon2 = lon1 + Math.atan2(Math.sin(rheading) * sinDistR * cosLat1, cosDistR - sinLat1 * Math.sin(lat2)); lon2 = lon2 * radInv; lon2 = lon2 > 180 ? lon2 - 360 : lon2 < -180 ? lon2 + 360 : lon2; return L.latLng([lat2 * radInv, lon2]); }, /** Returns the the angle of the given segment and the Equator in degrees, clockwise from 0 degrees north. @param {L.Map} map: Leaflet map to be used for this method @param {L.LatLng} latlngA: geographical point A of the segment @param {L.LatLng} latlngB: geographical point B of the segment @returns {Float} the angle in degrees. */ angle: function(map, latlngA, latlngB) { var pointA = map.latLngToContainerPoint(latlngA), pointB = map.latLngToContainerPoint(latlngB), angleDeg = Math.atan2(pointB.y - pointA.y, pointB.x - pointA.x) * 180 / Math.PI + 90; angleDeg += angleDeg < 0 ? 360 : 0; return angleDeg; }, /** Returns a point snaps on the segment and heading away from the given origin point a distance. @param {L.Map} map: Leaflet map to be used for this method @param {L.LatLng} latlngA: geographical point A of the segment @param {L.LatLng} latlngB: geographical point B of the segment @param {float} distance: distance in meters @returns {L.latLng} the destination point. */ destinationOnSegment: function(map, latlngA, latlngB, distance) { var angleDeg = L.GeometryUtil.angle(map, latlngA, latlngB), latlng = L.GeometryUtil.destination(latlngA, angleDeg, distance); return L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB); }, }); return L.GeometryUtil; }));