diff --git a/docs/favicon.ico b/docs/favicon.ico
new file mode 100644
index 0000000..dc1612f
Binary files /dev/null and b/docs/favicon.ico differ
diff --git a/docs/index.html b/docs/index.html
index 7729f29..5631834 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -8,15 +8,15 @@
-
+
Isolation Demo
-
-
+
+
@@ -25,7 +25,7 @@
-
+
diff --git a/docs/leaflet-arrowheads.js b/docs/leaflet-arrowheads.js
new file mode 100644
index 0000000..bbb2600
--- /dev/null
+++ b/docs/leaflet-arrowheads.js
@@ -0,0 +1,691 @@
+function modulus(i, n) {
+ return ((i % n) + n) % n;
+}
+
+function definedProps(obj) {
+ return Object.fromEntries(
+ Object.entries(obj).filter(([k, v]) => v !== undefined)
+ );
+}
+
+/**
+ * Whether or not a string is in the format 'm'
+ * @param {string} value
+ * @returns Boolean
+ */
+function isInMeters(value) {
+ return (
+ value
+ .toString()
+ .trim()
+ .slice(value.toString().length - 1, value.toString().length) === 'm'
+ );
+}
+
+/**
+ * Whether or not a string is in the format '%'
+ * @param {string} value
+ * @returns Boolean
+ */
+function isInPercent(value) {
+ return (
+ value
+ .toString()
+ .trim()
+ .slice(value.toString().length - 1, value.toString().length) === '%'
+ );
+}
+
+/**
+ * Whether or not a string is in the format 'px'
+ * @param {string} value
+ * @returns Boolean
+ */
+function isInPixels(value) {
+ return (
+ value
+ .toString()
+ .trim()
+ .slice(value.toString().length - 2, value.toString().length) === 'px'
+ );
+}
+
+function pixelsToMeters(pixels, map) {
+ let refPoint1 = map.getCenter();
+ let xy1 = map.latLngToLayerPoint(refPoint1);
+ let xy2 = {
+ x: xy1.x + Number(pixels),
+ y: xy1.y,
+ };
+ let refPoint2 = map.layerPointToLatLng(xy2);
+ let derivedMeters = map.distance(refPoint1, refPoint2);
+ return derivedMeters;
+}
+
+L.Polyline.include({
+ /**
+ * Adds arrowheads to an L.polyline
+ * @param {object} options The options for the arrowhead. See documentation for details
+ * @returns The L.polyline instance that they arrowheads are attached to
+ */
+ arrowheads: function (options = {}) {
+ // Merge user input options with default options:
+ const defaults = {
+ yawn: 60,
+ size: '15%',
+ frequency: 'allvertices',
+ proportionalToTotal: false,
+ };
+
+ this.options.noClip = true;
+
+ let actualOptions = Object.assign({}, defaults, options);
+ this._arrowheadOptions = actualOptions;
+
+ this._hatsApplied = true;
+ return this;
+ },
+
+ buildVectorHats: function (options) {
+ // Reset variables from previous this._update()
+ if (this._arrowheads) {
+ this._arrowheads.remove();
+ }
+
+ if (this._ghosts) {
+ this._ghosts.remove();
+ }
+
+ // -------------------------------------------------------- //
+ // ------------ FILTER THE OPTIONS ----------------------- //
+ /*
+ * The next 3 lines folds the options of the parent polyline into the default options for all polylines
+ * The options for the arrowhead are then folded in as well
+ * All options defined in parent polyline will be inherited by the arrowhead, unless otherwise specified in the arrowhead(options) call
+ */
+
+ let defaultOptionsOfParent = Object.getPrototypeOf(
+ Object.getPrototypeOf(this.options)
+ );
+
+ // merge default options of parent polyline (this.options's prototype's prototype) with options passed to parent polyline (this.options).
+ let parentOptions = Object.assign({}, defaultOptionsOfParent, this.options);
+
+ // now merge in the options the user has put in the arrowhead call
+ let hatOptions = Object.assign({}, parentOptions, options);
+
+ // ...with a few exceptions:
+ hatOptions.smoothFactor = 1;
+ hatOptions.fillOpacity = 1;
+ hatOptions.fill = options.fill ? true : false;
+ hatOptions.interactive = false;
+
+ // ------------ FILTER THE OPTIONS END -------------------- //
+ // --------------------------------------------------------- //
+
+ // --------------------------------------------------------- //
+ // ------ LOOP THROUGH EACH POLYLINE SEGMENT --------------- //
+ // ------ TO CALCULATE HAT SIZES AND CAPTURE IN ARRAY ------ //
+
+ let size = options.size.toString(); // stringify if its a number
+ let allhats = []; // empty array to receive hat polylines
+ const { frequency, offsets } = options;
+
+ if (offsets?.start || offsets?.end) {
+ this._buildGhosts({ start: offsets.start, end: offsets.end });
+ }
+
+ const lineToTrace = this._ghosts || this;
+
+ lineToTrace._parts.forEach((peice, index) => {
+ // Immutable variables for each peice
+ const latlngs = peice.map((point) => this._map.layerPointToLatLng(point));
+
+ const totalLength = (() => {
+ let total = 0;
+ for (var i = 0; i < peice.length - 1; i++) {
+ total += this._map.distance(latlngs[i], latlngs[i + 1]);
+ }
+ return total;
+ })();
+
+ // TBD by options if tree below
+ let derivedLatLngs;
+ let derivedBearings;
+ let spacing;
+ let noOfPoints;
+
+ // Determining latlng and bearing arrays based on frequency choice:
+ if (!isNaN(frequency)) {
+ spacing = 1 / frequency;
+ noOfPoints = frequency;
+ } else if (isInPercent(frequency)) {
+ console.error(
+ 'Error: arrowhead frequency option cannot be given in percent. Try another unit.'
+ );
+ } else if (isInMeters(frequency)) {
+ spacing = frequency.slice(0, frequency.length - 1) / totalLength;
+ noOfPoints = 1 / spacing;
+ // round things out for more even spacing:
+ noOfPoints = Math.floor(noOfPoints);
+ spacing = 1 / noOfPoints;
+ } else if (isInPixels(frequency)) {
+ spacing = (() => {
+ let chosenFrequency = frequency.slice(0, frequency.length - 2);
+ let derivedMeters = pixelsToMeters(chosenFrequency, this._map);
+ return derivedMeters / totalLength;
+ })();
+
+ noOfPoints = 1 / spacing;
+
+ // round things out for more even spacing:
+ noOfPoints = Math.floor(noOfPoints);
+ spacing = 1 / noOfPoints;
+ }
+
+ if (options.frequency === 'allvertices') {
+ derivedBearings = (() => {
+ let bearings = [];
+ for (var i = 1; i < latlngs.length; i++) {
+ let bearing =
+ L.GeometryUtil.angle(
+ this._map,
+ latlngs[modulus(i - 1, latlngs.length)],
+ latlngs[i]
+ ) + 180;
+ bearings.push(bearing);
+ }
+ return bearings;
+ })();
+
+ derivedLatLngs = latlngs;
+ derivedLatLngs.shift();
+ } else if (options.frequency === 'endonly' && latlngs.length >= 2) {
+ derivedLatLngs = [latlngs[latlngs.length - 1]];
+
+ derivedBearings = [
+ L.GeometryUtil.angle(
+ this._map,
+ latlngs[latlngs.length - 2],
+ latlngs[latlngs.length - 1]
+ ) + 180,
+ ];
+ } else {
+ derivedLatLngs = [];
+ let interpolatedPoints = [];
+ for (var i = 0; i < noOfPoints; i++) {
+ let interpolatedPoint = L.GeometryUtil.interpolateOnLine(
+ this._map,
+ latlngs,
+ spacing * (i + 1)
+ );
+
+ if (interpolatedPoint) {
+ interpolatedPoints.push(interpolatedPoint);
+ derivedLatLngs.push(interpolatedPoint.latLng);
+ }
+ }
+
+ derivedBearings = (() => {
+ let bearings = [];
+
+ for (var i = 0; i < interpolatedPoints.length; i++) {
+ let bearing = L.GeometryUtil.angle(
+ this._map,
+ latlngs[interpolatedPoints[i].predecessor + 1],
+ latlngs[interpolatedPoints[i].predecessor]
+ );
+ bearings.push(bearing);
+ }
+ return bearings;
+ })();
+ }
+
+ let hats = [];
+
+ // Function to build hats based on index and a given hatsize in meters
+ const pushHats = (size, localHatOptions = {}) => {
+ let yawn = localHatOptions.yawn ?? options.yawn;
+
+ let leftWingPoint = L.GeometryUtil.destination(
+ derivedLatLngs[i],
+ derivedBearings[i] - yawn / 2,
+ size
+ );
+
+ let rightWingPoint = L.GeometryUtil.destination(
+ derivedLatLngs[i],
+ derivedBearings[i] + yawn / 2,
+ size
+ );
+
+ let hatPoints = [
+ [leftWingPoint.lat, leftWingPoint.lng],
+ [derivedLatLngs[i].lat, derivedLatLngs[i].lng],
+ [rightWingPoint.lat, rightWingPoint.lng],
+ ];
+
+ let hat = options.fill
+ ? L.polygon(hatPoints, { ...hatOptions, ...localHatOptions })
+ : L.polyline(hatPoints, { ...hatOptions, ...localHatOptions });
+
+ hats.push(hat);
+ }; // pushHats()
+
+ // Function to build hats based on pixel input
+ const pushHatsFromPixels = (size, localHatOptions = {}) => {
+ let sizePixels = size.slice(0, size.length - 2);
+ let yawn = localHatOptions.yawn ?? options.yawn;
+
+ let derivedXY = this._map.latLngToLayerPoint(derivedLatLngs[i]);
+
+ let bearing = derivedBearings[i];
+
+ let thetaLeft = (180 - bearing - yawn / 2) * (Math.PI / 180),
+ thetaRight = (180 - bearing + yawn / 2) * (Math.PI / 180);
+
+ let dxLeft = sizePixels * Math.sin(thetaLeft),
+ dyLeft = sizePixels * Math.cos(thetaLeft),
+ dxRight = sizePixels * Math.sin(thetaRight),
+ dyRight = sizePixels * Math.cos(thetaRight);
+
+ let leftWingXY = {
+ x: derivedXY.x + dxLeft,
+ y: derivedXY.y + dyLeft,
+ };
+ let rightWingXY = {
+ x: derivedXY.x + dxRight,
+ y: derivedXY.y + dyRight,
+ };
+
+ let leftWingPoint = this._map.layerPointToLatLng(leftWingXY),
+ rightWingPoint = this._map.layerPointToLatLng(rightWingXY);
+
+ let hatPoints = [
+ [leftWingPoint.lat, leftWingPoint.lng],
+ [derivedLatLngs[i].lat, derivedLatLngs[i].lng],
+ [rightWingPoint.lat, rightWingPoint.lng],
+ ];
+
+ let hat = options.fill
+ ? L.polygon(hatPoints, { ...hatOptions, ...localHatOptions })
+ : L.polyline(hatPoints, { ...hatOptions, ...localHatOptions });
+
+ hats.push(hat);
+ }; // pushHatsFromPixels()
+
+ // ------- LOOP THROUGH POINTS IN EACH SEGMENT ---------- //
+ for (var i = 0; i < derivedLatLngs.length; i++) {
+ let { perArrowheadOptions, ...globalOptions } = options;
+
+ perArrowheadOptions = perArrowheadOptions ? perArrowheadOptions(i) : {};
+ perArrowheadOptions = Object.assign(
+ globalOptions,
+ definedProps(perArrowheadOptions)
+ );
+
+ size = perArrowheadOptions.size ?? size;
+
+ // ---- If size is chosen in meters -------------------------
+ if (isInMeters(size)) {
+ let hatSize = size.slice(0, size.length - 1);
+ pushHats(hatSize, perArrowheadOptions);
+
+ // ---- If size is chosen in percent ------------------------
+ } else if (isInPercent(size)) {
+ let sizePercent = size.slice(0, size.length - 1);
+ let hatSize = (() => {
+ if (
+ options.frequency === 'endonly' &&
+ options.proportionalToTotal
+ ) {
+ return (totalLength * sizePercent) / 100;
+ } else {
+ let averageDistance = totalLength / (peice.length - 1);
+ return (averageDistance * sizePercent) / 100;
+ }
+ })(); // hatsize calculation
+
+ pushHats(hatSize, perArrowheadOptions);
+
+ // ---- If size is chosen in pixels --------------------------
+ } else if (isInPixels(size)) {
+ pushHatsFromPixels(options.size, perArrowheadOptions);
+
+ // ---- If size unit is not given -----------------------------
+ } else {
+ console.error(
+ 'Error: Arrowhead size unit not defined. Check your arrowhead options.'
+ );
+ } // if else block for Size
+ } // for loop for each point witin a peice
+
+ allhats.push(...hats);
+ }); // forEach peice
+
+ // --------- LOOP THROUGH EACH POLYLINE END ---------------- //
+ // --------------------------------------------------------- //
+
+ let arrowheads = L.layerGroup(allhats);
+ this._arrowheads = arrowheads;
+
+ return this;
+ },
+
+ getArrowheads: function () {
+ if (this._arrowheads) {
+ return this._arrowheads;
+ } else {
+ return console.error(
+ `Error: You tried to call '.getArrowheads() on a shape that does not have a arrowhead. Use '.arrowheads()' to add a arrowheads before trying to call '.getArrowheads()'`
+ );
+ }
+ },
+
+ /**
+ * Builds ghost polylines that are clipped versions of the polylines based on the offsets
+ * If offsets are used, arrowheads are drawn from 'this._ghosts' rather than 'this'
+ */
+ _buildGhosts: function ({ start, end }) {
+ if (start || end) {
+ let latlngs = this.getLatLngs();
+
+ latlngs = Array.isArray(latlngs[0]) ? latlngs : [latlngs];
+
+ const newLatLngs = latlngs.map((segment) => {
+ // Get total distance of original latlngs
+ const totalLength = (() => {
+ let total = 0;
+ for (var i = 0; i < segment.length - 1; i++) {
+ total += this._map.distance(segment[i], segment[i + 1]);
+ }
+ return total;
+ })();
+
+ // Modify latlngs to end at interpolated point
+ if (start) {
+ let endOffsetInMeters = (() => {
+ if (isInMeters(start)) {
+ return Number(start.slice(0, start.length - 1));
+ } else if (isInPixels(start)) {
+ let pixels = Number(start.slice(0, start.length - 2));
+ return pixelsToMeters(pixels, this._map);
+ }
+ })();
+
+ let newStart = L.GeometryUtil.interpolateOnLine(
+ this._map,
+ segment,
+ endOffsetInMeters / totalLength
+ );
+
+ segment = segment.slice(
+ newStart.predecessor === -1 ? 1 : newStart.predecessor + 1,
+ segment.length
+ );
+ segment.unshift(newStart.latLng);
+ }
+
+ if (end) {
+ let endOffsetInMeters = (() => {
+ if (isInMeters(end)) {
+ return Number(end.slice(0, end.length - 1));
+ } else if (isInPixels(end)) {
+ let pixels = Number(end.slice(0, end.length - 2));
+ return pixelsToMeters(pixels, this._map);
+ }
+ })();
+
+ let newEnd = L.GeometryUtil.interpolateOnLine(
+ this._map,
+ segment,
+ (totalLength - endOffsetInMeters) / totalLength
+ );
+
+ segment = segment.slice(0, newEnd.predecessor + 1);
+ segment.push(newEnd.latLng);
+ }
+
+ return segment;
+ });
+
+ this._ghosts = L.polyline(newLatLngs, {
+ ...this.options,
+ color: 'rgba(0,0,0,0)',
+ stroke: 0,
+ smoothFactor: 0,
+ interactive: false,
+ });
+ this._ghosts.addTo(this._map);
+ }
+ },
+
+ deleteArrowheads: function () {
+ if (this._arrowheads) {
+ this._arrowheads.remove();
+ delete this._arrowheads;
+ delete this._arrowheadOptions;
+ this._hatsApplied = false;
+ }
+ if (this._ghosts) {
+ this._ghosts.remove();
+ }
+ },
+
+ _update: function () {
+ if (!this._map) {
+ return;
+ }
+
+ this._clipPoints();
+ this._simplifyPoints();
+ this._updatePath();
+
+ if (this._hatsApplied) {
+ this.buildVectorHats(this._arrowheadOptions);
+ this._map.addLayer(this._arrowheads);
+ }
+ },
+
+ remove: function () {
+ if (this._arrowheads) {
+ this._arrowheads.remove();
+ }
+ if (this._ghosts) {
+ this._ghosts.remove();
+ }
+ return this.removeFrom(this._map || this._mapToAdd);
+ },
+});
+
+L.LayerGroup.include({
+ removeLayer: function (layer) {
+ var id = layer in this._layers ? layer : this.getLayerId(layer);
+
+ if (this._map && this._layers[id]) {
+ if (this._layers[id]._arrowheads) {
+ this._layers[id]._arrowheads.remove();
+ }
+ this._map.removeLayer(this._layers[id]);
+ }
+
+ delete this._layers[id];
+
+ return this;
+ },
+
+ onRemove: function (map, layer) {
+ for (var layer in this._layers) {
+ if (this._layers[layer]) {
+ this._layers[layer].remove();
+ }
+ }
+
+ this.eachLayer(map.removeLayer, map);
+ },
+});
+
+L.Map.include({
+ removeLayer: function (layer) {
+ var id = L.Util.stamp(layer);
+
+ if (layer._arrowheads) {
+ layer._arrowheads.remove();
+ }
+ if (layer._ghosts) {
+ layer._ghosts.remove();
+ }
+
+ if (!this._layers[id]) {
+ return this;
+ }
+
+ if (this._loaded) {
+ layer.onRemove(this);
+ }
+
+ if (layer.getAttribution && this.attributionControl) {
+ this.attributionControl.removeAttribution(layer.getAttribution());
+ }
+
+ delete this._layers[id];
+
+ if (this._loaded) {
+ this.fire('layerremove', { layer: layer });
+ layer.fire('remove');
+ }
+
+ layer._map = layer._mapToAdd = null;
+
+ return this;
+ },
+});
+
+L.GeoJSON.include({
+ geometryToLayer: function (geojson, options) {
+ var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson,
+ coords = geometry ? geometry.coordinates : null,
+ layers = [],
+ pointToLayer = options && options.pointToLayer,
+ _coordsToLatLng =
+ (options && options.coordsToLatLng) || L.GeoJSON.coordsToLatLng,
+ latlng,
+ latlngs,
+ i,
+ len;
+
+ if (!coords && !geometry) {
+ return null;
+ }
+
+ switch (geometry.type) {
+ case 'Point':
+ latlng = _coordsToLatLng(coords);
+ return this._pointToLayer(pointToLayer, geojson, latlng, options);
+
+ case 'MultiPoint':
+ for (i = 0, len = coords.length; i < len; i++) {
+ latlng = _coordsToLatLng(coords[i]);
+ layers.push(
+ this._pointToLayer(pointToLayer, geojson, latlng, options)
+ );
+ }
+ return new L.FeatureGroup(layers);
+
+ case 'LineString':
+ case 'MultiLineString':
+ latlngs = L.GeoJSON.coordsToLatLngs(
+ coords,
+ geometry.type === 'LineString' ? 0 : 1,
+ _coordsToLatLng
+ );
+ var polyline = new L.Polyline(latlngs, options);
+ if (options.arrowheads) {
+ polyline.arrowheads(options.arrowheads);
+ }
+ return polyline;
+
+ case 'Polygon':
+ case 'MultiPolygon':
+ latlngs = L.GeoJSON.coordsToLatLngs(
+ coords,
+ geometry.type === 'Polygon' ? 1 : 2,
+ _coordsToLatLng
+ );
+ return new L.Polygon(latlngs, options);
+
+ case 'GeometryCollection':
+ for (i = 0, len = geometry.geometries.length; i < len; i++) {
+ var layer = this.geometryToLayer(
+ {
+ geometry: geometry.geometries[i],
+ type: 'Feature',
+ properties: geojson.properties,
+ },
+ options
+ );
+
+ if (layer) {
+ layers.push(layer);
+ }
+ }
+ return new L.FeatureGroup(layers);
+
+ default:
+ throw new Error('Invalid GeoJSON object.');
+ }
+ },
+
+ addData: function (geojson) {
+ var features = L.Util.isArray(geojson) ? geojson : geojson.features,
+ i,
+ len,
+ feature;
+
+ if (features) {
+ for (i = 0, len = features.length; i < len; i++) {
+ // only add this if geometry or geometries are set and not null
+ feature = features[i];
+ if (
+ feature.geometries ||
+ feature.geometry ||
+ feature.features ||
+ feature.coordinates
+ ) {
+ this.addData(feature);
+ }
+ }
+ return this;
+ }
+
+ var options = this.options;
+
+ if (options.filter && !options.filter(geojson)) {
+ return this;
+ }
+
+ var layer = this.geometryToLayer(geojson, options);
+ if (!layer) {
+ return this;
+ }
+ layer.feature = L.GeoJSON.asFeature(geojson);
+
+ layer.defaultOptions = layer.options;
+ this.resetStyle(layer);
+
+ if (options.onEachFeature) {
+ options.onEachFeature(geojson, layer);
+ }
+
+ return this.addLayer(layer);
+ },
+
+ _pointToLayer: function (pointToLayerFn, geojson, latlng, options) {
+ return pointToLayerFn
+ ? pointToLayerFn(geojson, latlng)
+ : new L.Marker(
+ latlng,
+ options && options.markersInheritOptions && options
+ );
+ },
+});
diff --git a/docs/leaflet.geometryutil.js b/docs/leaflet.geometryutil.js
new file mode 100644
index 0000000..f506d3d
--- /dev/null
+++ b/docs/leaflet.geometryutil.js
@@ -0,0 +1,807 @@
+// 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 point of a {L.LatLng} on a {L.Circle}
+
+ @tutorial closest
+
+ @param {L.LatLng} latlng - The position to search
+ @param {L.Circle} circle - A Circle defined by a center and a radius
+ @returns {L.LatLng} Closest geographical point on the circle circumference
+ */
+ closestOnCircle: function (circle, latLng) {
+ const center = circle.getLatLng();
+ const circleRadius = circle.getRadius();
+ const radius = typeof circleRadius === 'number' ? circleRadius : circleRadius.radius;
+ const x = latLng.lng;
+ const y = latLng.lat;
+ const cx = center.lng;
+ const cy = center.lat;
+ // dx and dy is the vector from the circle's center to latLng
+ const dx = x - cx;
+ const dy = y - cy;
+
+ // distance between the point and the circle's center
+ const distance = Math.sqrt(dx * dx + dy * dy)
+
+ // Calculate the closest point on the circle by adding the normalized vector to the center
+ const tx = cx + (dx / distance) * radius;
+ const ty = cy + (dy / distance) * radius;
+
+ return new L.LatLng(ty, tx);
+ },
+
+
+ /**
+ 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 {
+ if (layer instanceof L.Circle){
+ ll = this.closestOnCircle(layer, latlng);
+ distance = L.GeometryUtil.distance(map, latlng, ll);
+ } 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