refactor path to leaflet plugins

This commit is contained in:
liamcottle
2024-05-16 20:43:24 +12:00
parent fbe45c8d7c
commit 6ce078f984
12 changed files with 12 additions and 8 deletions

View File

@ -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 '<number>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 '<number>%'
* @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 '<number>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
);
},
});

View File

@ -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<L.Point>|Array<L.LatLng>} 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<L.Point>|Array<L.LatLng>} coords Set of coordinates
* @returns {Array<Number>} 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<L.LatLng>|Array<Array<L.LatLng>>|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<Array<T>>
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<L.ILayer>} 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<L.ILayer>} 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<object>} 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 {
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.
}
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<ILayer>} 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<ILayer>} 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.LatLng>|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<L.LatLng>} 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 = L.CRS.Earth.R, // 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;
}));

View File

@ -0,0 +1,13 @@
.leaflet-control-layers-group-name {
font-weight: bold;
margin-bottom: .2em;
}
.leaflet-control-layers-group {
margin-bottom: .5em;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
padding-right: 10px;
}

View File

@ -0,0 +1,374 @@
/* global L */
// A layer control which provides for layer groupings.
// Author: Ishmael Smyrnow
L.Control.GroupedLayers = L.Control.extend({
options: {
collapsed: true,
position: 'topright',
autoZIndex: true,
exclusiveGroups: [],
groupCheckboxes: false
},
initialize: function (baseLayers, groupedOverlays, options) {
var i, j;
L.Util.setOptions(this, options);
this._layers = [];
this._lastZIndex = 0;
this._handlingClick = false;
this._groupList = [];
this._domGroups = [];
for (i in baseLayers) {
this._addLayer(baseLayers[i], i);
}
for (i in groupedOverlays) {
for (j in groupedOverlays[i]) {
this._addLayer(groupedOverlays[i][j], j, i, true);
}
}
},
onAdd: function (map) {
this._initLayout();
this._update();
map
.on('layeradd', this._onLayerChange, this)
.on('layerremove', this._onLayerChange, this);
return this._container;
},
onRemove: function (map) {
map
.off('layeradd', this._onLayerChange, this)
.off('layerremove', this._onLayerChange, this);
},
addBaseLayer: function (layer, name) {
this._addLayer(layer, name);
this._update();
return this;
},
addOverlay: function (layer, name, group) {
this._addLayer(layer, name, group, true);
this._update();
return this;
},
removeLayer: function (layer) {
var id = L.Util.stamp(layer);
var _layer = this._getLayer(id);
if (_layer) {
delete this._layers[this._layers.indexOf(_layer)];
}
this._update();
return this;
},
_getLayer: function (id) {
for (var i = 0; i < this._layers.length; i++) {
if (this._layers[i] && L.stamp(this._layers[i].layer) === id) {
return this._layers[i];
}
}
},
_initLayout: function () {
var className = 'leaflet-control-layers',
container = this._container = L.DomUtil.create('div', className);
// Makes this work on IE10 Touch devices by stopping it from firing a mouseout event when the touch is released
container.setAttribute('aria-haspopup', true);
if (L.Browser.touch) {
L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation);
} else {
L.DomEvent.disableClickPropagation(container);
L.DomEvent.on(container, 'wheel', L.DomEvent.stopPropagation);
}
var form = this._form = L.DomUtil.create('form', className + '-list');
if (this.options.collapsed) {
if (!L.Browser.android) {
L.DomEvent
.on(container, 'mouseover', this._expand, this)
.on(container, 'mouseout', this._collapse, this);
}
var link = this._layersLink = L.DomUtil.create('a', className + '-toggle', container);
link.href = '#';
link.title = 'Layers';
if (L.Browser.touch) {
L.DomEvent
.on(link, 'click', L.DomEvent.stop)
.on(link, 'click', this._expand, this);
} else {
L.DomEvent.on(link, 'focus', this._expand, this);
}
this._map.on('click', this._collapse, this);
// TODO keyboard accessibility
} else {
this._expand();
}
this._baseLayersList = L.DomUtil.create('div', className + '-base', form);
this._separator = L.DomUtil.create('div', className + '-separator', form);
this._overlaysList = L.DomUtil.create('div', className + '-overlays', form);
container.appendChild(form);
},
_addLayer: function (layer, name, group, overlay) {
var id = L.Util.stamp(layer);
var _layer = {
layer: layer,
name: name,
overlay: overlay
};
this._layers.push(_layer);
group = group || '';
var groupId = this._indexOf(this._groupList, group);
if (groupId === -1) {
groupId = this._groupList.push(group) - 1;
}
var exclusive = (this._indexOf(this.options.exclusiveGroups, group) !== -1);
_layer.group = {
name: group,
id: groupId,
exclusive: exclusive
};
if (this.options.autoZIndex && layer.setZIndex) {
this._lastZIndex++;
layer.setZIndex(this._lastZIndex);
}
},
_update: function () {
if (!this._container) {
return;
}
this._baseLayersList.innerHTML = '';
this._overlaysList.innerHTML = '';
this._domGroups.length = 0;
var baseLayersPresent = false,
overlaysPresent = false,
i, obj;
for (var i = 0; i < this._layers.length; i++) {
obj = this._layers[i];
this._addItem(obj);
overlaysPresent = overlaysPresent || obj.overlay;
baseLayersPresent = baseLayersPresent || !obj.overlay;
}
this._separator.style.display = overlaysPresent && baseLayersPresent ? '' : 'none';
},
_onLayerChange: function (e) {
var obj = this._getLayer(L.Util.stamp(e.layer)),
type;
if (!obj) {
return;
}
if (!this._handlingClick) {
this._update();
}
if (obj.overlay) {
type = e.type === 'layeradd' ? 'overlayadd' : 'overlayremove';
} else {
type = e.type === 'layeradd' ? 'baselayerchange' : null;
}
if (type) {
this._map.fire(type, obj);
}
},
// IE7 bugs out if you create a radio dynamically, so you have to do it this hacky way (see http://bit.ly/PqYLBe)
_createRadioElement: function (name, checked) {
var radioHtml = '<input type="radio" class="leaflet-control-layers-selector" name="' + name + '"';
if (checked) {
radioHtml += ' checked="checked"';
}
radioHtml += '/>';
var radioFragment = document.createElement('div');
radioFragment.innerHTML = radioHtml;
return radioFragment.firstChild;
},
_addItem: function (obj) {
var label = document.createElement('label'),
input,
checked = this._map.hasLayer(obj.layer),
container,
groupRadioName;
if (obj.overlay) {
if (obj.group.exclusive) {
groupRadioName = 'leaflet-exclusive-group-layer-' + obj.group.id;
input = this._createRadioElement(groupRadioName, checked);
} else {
input = document.createElement('input');
input.type = 'checkbox';
input.className = 'leaflet-control-layers-selector';
input.defaultChecked = checked;
}
} else {
input = this._createRadioElement('leaflet-base-layers', checked);
}
input.layerId = L.Util.stamp(obj.layer);
input.groupID = obj.group.id;
L.DomEvent.on(input, 'click', this._onInputClick, this);
var name = document.createElement('span');
name.innerHTML = ' ' + obj.name;
label.appendChild(input);
label.appendChild(name);
if (obj.overlay) {
container = this._overlaysList;
var groupContainer = this._domGroups[obj.group.id];
// Create the group container if it doesn't exist
if (!groupContainer) {
groupContainer = document.createElement('div');
groupContainer.className = 'leaflet-control-layers-group';
groupContainer.id = 'leaflet-control-layers-group-' + obj.group.id;
var groupLabel = document.createElement('label');
groupLabel.className = 'leaflet-control-layers-group-label';
if (obj.group.name !== '' && !obj.group.exclusive) {
// ------ add a group checkbox with an _onInputClickGroup function
if (this.options.groupCheckboxes) {
var groupInput = document.createElement('input');
groupInput.type = 'checkbox';
groupInput.className = 'leaflet-control-layers-group-selector';
groupInput.groupID = obj.group.id;
groupInput.legend = this;
L.DomEvent.on(groupInput, 'click', this._onGroupInputClick, groupInput);
groupLabel.appendChild(groupInput);
}
}
var groupName = document.createElement('span');
groupName.className = 'leaflet-control-layers-group-name';
groupName.innerHTML = obj.group.name;
groupLabel.appendChild(groupName);
groupContainer.appendChild(groupLabel);
container.appendChild(groupContainer);
this._domGroups[obj.group.id] = groupContainer;
}
container = groupContainer;
} else {
container = this._baseLayersList;
}
container.appendChild(label);
return label;
},
_onGroupInputClick: function () {
var i, input, obj;
var this_legend = this.legend;
this_legend._handlingClick = true;
var inputs = this_legend._form.getElementsByTagName('input');
var inputsLen = inputs.length;
for (i = 0; i < inputsLen; i++) {
input = inputs[i];
if (input.groupID === this.groupID && input.className === 'leaflet-control-layers-selector') {
input.checked = this.checked;
obj = this_legend._getLayer(input.layerId);
if (input.checked && !this_legend._map.hasLayer(obj.layer)) {
this_legend._map.addLayer(obj.layer);
} else if (!input.checked && this_legend._map.hasLayer(obj.layer)) {
this_legend._map.removeLayer(obj.layer);
}
}
}
this_legend._handlingClick = false;
},
_onInputClick: function () {
var i, input, obj,
inputs = this._form.getElementsByTagName('input'),
inputsLen = inputs.length;
this._handlingClick = true;
for (i = 0; i < inputsLen; i++) {
input = inputs[i];
if (input.className === 'leaflet-control-layers-selector') {
obj = this._getLayer(input.layerId);
if (input.checked && !this._map.hasLayer(obj.layer)) {
this._map.addLayer(obj.layer);
} else if (!input.checked && this._map.hasLayer(obj.layer)) {
this._map.removeLayer(obj.layer);
}
}
}
this._handlingClick = false;
},
_expand: function () {
L.DomUtil.addClass(this._container, 'leaflet-control-layers-expanded');
// permits to have a scrollbar if overlays heighter than the map.
var acceptableHeight = this._map._size.y - (this._container.offsetTop * 4);
if (acceptableHeight < this._form.clientHeight) {
L.DomUtil.addClass(this._form, 'leaflet-control-layers-scrollbar');
this._form.style.height = acceptableHeight + 'px';
}
},
_collapse: function () {
this._container.className = this._container.className.replace(' leaflet-control-layers-expanded', '');
},
_indexOf: function (arr, obj) {
for (var i = 0, j = arr.length; i < j; i++) {
if (arr[i] === obj) {
return i;
}
}
return -1;
}
});
L.control.groupedLayers = function (baseLayers, groupedOverlays, options) {
return new L.Control.GroupedLayers(baseLayers, groupedOverlays, options);
};

View File

@ -0,0 +1,60 @@
.marker-cluster-small {
background-color: rgba(181, 226, 140, 0.6);
}
.marker-cluster-small div {
background-color: rgba(110, 204, 57, 0.6);
}
.marker-cluster-medium {
background-color: rgba(241, 211, 87, 0.6);
}
.marker-cluster-medium div {
background-color: rgba(240, 194, 12, 0.6);
}
.marker-cluster-large {
background-color: rgba(253, 156, 115, 0.6);
}
.marker-cluster-large div {
background-color: rgba(241, 128, 23, 0.6);
}
/* IE 6-8 fallback colors */
.leaflet-oldie .marker-cluster-small {
background-color: rgb(181, 226, 140);
}
.leaflet-oldie .marker-cluster-small div {
background-color: rgb(110, 204, 57);
}
.leaflet-oldie .marker-cluster-medium {
background-color: rgb(241, 211, 87);
}
.leaflet-oldie .marker-cluster-medium div {
background-color: rgb(240, 194, 12);
}
.leaflet-oldie .marker-cluster-large {
background-color: rgb(253, 156, 115);
}
.leaflet-oldie .marker-cluster-large div {
background-color: rgb(241, 128, 23);
}
.marker-cluster {
background-clip: padding-box;
border-radius: 20px;
}
.marker-cluster div {
width: 30px;
height: 30px;
margin-left: 5px;
margin-top: 5px;
text-align: center;
border-radius: 15px;
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.marker-cluster span {
line-height: 30px;
}

View File

@ -0,0 +1,14 @@
.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
-webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
-moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
-o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
transition: transform 0.3s ease-out, opacity 0.3s ease-in;
}
.leaflet-cluster-spider-leg {
/* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
-webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
-moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
-o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,227 @@
(function (factory, window) {
if (typeof define === 'function' && define.amd) {
define(['leaflet'], factory);
} else if (typeof exports === 'object') {
module.exports = factory(require('leaflet'));
}
if (typeof window !== 'undefined' && window.L) {
window.L.PolylineOffset = factory(L);
}
}(function (L) {
function forEachPair(list, callback) {
if (!list || list.length < 1) { return; }
for (var i = 1, l = list.length; i < l; i++) {
callback(list[i-1], list[i]);
}
}
/**
Find the coefficients (a,b) of a line of equation y = a.x + b,
or the constant x for vertical lines
Return null if there's no equation possible
*/
function lineEquation(pt1, pt2) {
if (pt1.x === pt2.x) {
return pt1.y === pt2.y ? null : { x: pt1.x };
}
var a = (pt2.y - pt1.y) / (pt2.x - pt1.x);
return {
a: a,
b: pt1.y - a * pt1.x,
};
}
/**
Return the intersection point of two lines defined by two points each
Return null when there's no unique intersection
*/
function intersection(l1a, l1b, l2a, l2b) {
var line1 = lineEquation(l1a, l1b);
var line2 = lineEquation(l2a, l2b);
if (line1 === null || line2 === null) {
return null;
}
if (line1.hasOwnProperty('x')) {
return line2.hasOwnProperty('x')
? null
: {
x: line1.x,
y: line2.a * line1.x + line2.b,
};
}
if (line2.hasOwnProperty('x')) {
return {
x: line2.x,
y: line1.a * line2.x + line1.b,
};
}
if (line1.a === line2.a) {
return null;
}
var x = (line2.b - line1.b) / (line1.a - line2.a);
return {
x: x,
y: line1.a * x + line1.b,
};
}
function translatePoint(pt, dist, heading) {
return {
x: pt.x + dist * Math.cos(heading),
y: pt.y + dist * Math.sin(heading),
};
}
var PolylineOffset = {
offsetPointLine: function(points, distance) {
var offsetSegments = [];
forEachPair(points, L.bind(function(a, b) {
if (a.x === b.x && a.y === b.y) { return; }
// angles in (-PI, PI]
var segmentAngle = Math.atan2(a.y - b.y, a.x - b.x);
var offsetAngle = segmentAngle - Math.PI/2;
offsetSegments.push({
offsetAngle: offsetAngle,
original: [a, b],
offset: [
translatePoint(a, distance, offsetAngle),
translatePoint(b, distance, offsetAngle)
]
});
}, this));
return offsetSegments;
},
offsetPoints: function(pts, options) {
var offsetSegments = this.offsetPointLine(L.LineUtil.simplify(pts, options.smoothFactor), options.offset);
return this.joinLineSegments(offsetSegments, options.offset);
},
/**
Join 2 line segments defined by 2 points each with a circular arc
*/
joinSegments: function(s1, s2, offset) {
// TODO: different join styles
return this.circularArc(s1, s2, offset)
.filter(function(x) { return x; })
},
joinLineSegments: function(segments, offset) {
var joinedPoints = [];
var first = segments[0];
var last = segments[segments.length - 1];
if (first && last) {
joinedPoints.push(first.offset[0]);
forEachPair(segments, L.bind(function(s1, s2) {
joinedPoints = joinedPoints.concat(this.joinSegments(s1, s2, offset));
}, this));
joinedPoints.push(last.offset[1]);
}
return joinedPoints;
},
segmentAsVector: function(s) {
return {
x: s[1].x - s[0].x,
y: s[1].y - s[0].y,
};
},
getSignedAngle: function(s1, s2) {
const a = this.segmentAsVector(s1);
const b = this.segmentAsVector(s2);
return Math.atan2(a.x * b.y - a.y * b.x, a.x * b.x + a.y * b.y);
},
/**
Interpolates points between two offset segments in a circular form
*/
circularArc: function(s1, s2, distance) {
// if the segments are the same angle,
// there should be a single join point
if (s1.offsetAngle === s2.offsetAngle) {
return [s1.offset[1]];
}
const signedAngle = this.getSignedAngle(s1.offset, s2.offset);
// for inner angles, just find the offset segments intersection
if ((signedAngle * distance > 0) &&
(signedAngle * this.getSignedAngle(s1.offset, [s1.offset[0], s2.offset[1]]) > 0)) {
return [intersection(s1.offset[0], s1.offset[1], s2.offset[0], s2.offset[1])];
}
// draws a circular arc with R = offset distance, C = original meeting point
var points = [];
var center = s1.original[1];
// ensure angles go in the anti-clockwise direction
var rightOffset = distance > 0;
var startAngle = rightOffset ? s2.offsetAngle : s1.offsetAngle;
var endAngle = rightOffset ? s1.offsetAngle : s2.offsetAngle;
// and that the end angle is bigger than the start angle
if (endAngle < startAngle) {
endAngle += Math.PI * 2;
}
var step = Math.PI / 8;
for (var alpha = startAngle; alpha < endAngle; alpha += step) {
points.push(translatePoint(center, distance, alpha));
}
points.push(translatePoint(center, distance, endAngle));
return rightOffset ? points.reverse() : points;
}
}
// Modify the L.Polyline class by overwriting the projection function
L.Polyline.include({
_projectLatlngs: function (latlngs, result, projectedBounds) {
var isFlat = latlngs.length > 0 && latlngs[0] instanceof L.LatLng;
if (isFlat) {
var ring = latlngs.map(L.bind(function(ll) {
var point = this._map.latLngToLayerPoint(ll);
if (projectedBounds) {
projectedBounds.extend(point);
}
return point;
}, this));
// Offset management hack ---
if (this.options.offset) {
ring = L.PolylineOffset.offsetPoints(ring, this.options);
}
// Offset management hack END ---
result.push(ring.map(function (xy) {
return L.point(xy.x, xy.y);
}));
} else {
latlngs.forEach(L.bind(function(ll) {
this._projectLatlngs(ll, result, projectedBounds);
}, this));
}
}
});
L.Polyline.include({
setOffset: function(offset) {
this.options.offset = offset;
this.redraw();
return this;
}
});
return PolylineOffset;
}, window));