implement showing neighbours for a specific node
This commit is contained in:
@ -21,6 +21,8 @@
|
|||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css" integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI=" crossorigin="" />
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css" integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI=" crossorigin="" />
|
||||||
<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js" integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM=" crossorigin=""></script>
|
<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js" integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM=" crossorigin=""></script>
|
||||||
<script src="plugins/leaflet.polylineoffset.js"></script>
|
<script src="plugins/leaflet.polylineoffset.js"></script>
|
||||||
|
<script src="plugins/leaflet.geometryutil.js"></script>
|
||||||
|
<script src="plugins/leaflet-arrowheads.js"></script>
|
||||||
<script src="plugins/leaflet.markercluster/leaflet.markercluster.js"></script>
|
<script src="plugins/leaflet.markercluster/leaflet.markercluster.js"></script>
|
||||||
<link rel="stylesheet" href="plugins/leaflet.markercluster/MarkerCluster.css"/>
|
<link rel="stylesheet" href="plugins/leaflet.markercluster/MarkerCluster.css"/>
|
||||||
<link rel="stylesheet" href="plugins/leaflet.markercluster/MarkerCluster.Default.css"/>
|
<link rel="stylesheet" href="plugins/leaflet.markercluster/MarkerCluster.Default.css"/>
|
||||||
@ -1872,6 +1874,7 @@
|
|||||||
// create layer groups
|
// create layer groups
|
||||||
var nodesLayerGroup = new L.LayerGroup();
|
var nodesLayerGroup = new L.LayerGroup();
|
||||||
var neighboursLayerGroup = new L.LayerGroup();
|
var neighboursLayerGroup = new L.LayerGroup();
|
||||||
|
var nodeNeighboursLayerGroup = new L.LayerGroup();
|
||||||
var nodesClusteredLayerGroup = L.markerClusterGroup({
|
var nodesClusteredLayerGroup = L.markerClusterGroup({
|
||||||
showCoverageOnHover: false,
|
showCoverageOnHover: false,
|
||||||
disableClusteringAtZoom: 10, // zoom level where goToNode zooms to
|
disableClusteringAtZoom: 10, // zoom level where goToNode zooms to
|
||||||
@ -2140,6 +2143,234 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanUpNodeNeighbours() {
|
||||||
|
|
||||||
|
// close tooltips and popups
|
||||||
|
closeAllPopups();
|
||||||
|
closeAllTooltips();
|
||||||
|
|
||||||
|
// turn off neighbours layer
|
||||||
|
neighboursLayerGroup.removeFrom(map);
|
||||||
|
|
||||||
|
// setup node neighbours layer
|
||||||
|
nodeNeighboursLayerGroup.clearLayers();
|
||||||
|
nodeNeighboursLayerGroup.removeFrom(map);
|
||||||
|
nodeNeighboursLayerGroup.addTo(map);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNodeNeighboursThatWeHeard(id) {
|
||||||
|
|
||||||
|
cleanUpNodeNeighbours();
|
||||||
|
|
||||||
|
// find node
|
||||||
|
const node = findNodeById(id);
|
||||||
|
if(!node){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find node marker
|
||||||
|
const nodeMarker = findNodeMarkerById(node.node_id);
|
||||||
|
if(!nodeMarker){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure we have neighbours to show
|
||||||
|
const neighbours = node.neighbours ?? [];
|
||||||
|
if(neighbours.length === 0){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add node neighbours
|
||||||
|
for(const neighbour of neighbours){
|
||||||
|
|
||||||
|
// fixme: skipping zero snr? saw some crazy long neighbours with zero snr...
|
||||||
|
if(neighbour.snr === 0){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find neighbour node
|
||||||
|
const neighbourNode = findNodeById(neighbour.node_id);
|
||||||
|
if(!neighbourNode){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find neighbour node marker
|
||||||
|
const neighbourNodeMarker = findNodeMarkerById(neighbour.node_id);
|
||||||
|
if(!neighbourNodeMarker){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate distance in meters between nodes (rounded to 2 decimal places)
|
||||||
|
const distanceInMeters = nodeMarker.getLatLng().distanceTo(neighbourNodeMarker.getLatLng()).toFixed(2);
|
||||||
|
|
||||||
|
// don't show this neighbour connection if further than config allows
|
||||||
|
const configNeighboursMaxDistanceInMeters = getConfigNeighboursMaxDistanceInMeters();
|
||||||
|
if(configNeighboursMaxDistanceInMeters != null && parseFloat(distanceInMeters) > configNeighboursMaxDistanceInMeters){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add neighbour line to map
|
||||||
|
const line = L.polyline([
|
||||||
|
neighbourNodeMarker.getLatLng(), // from neighbour
|
||||||
|
nodeMarker.getLatLng(), // to us
|
||||||
|
], {
|
||||||
|
color: '#2563eb',
|
||||||
|
opacity: 0.5,
|
||||||
|
}).arrowheads({
|
||||||
|
size: '10px',
|
||||||
|
fill: true,
|
||||||
|
offsets: {
|
||||||
|
start: '25px',
|
||||||
|
end: '25px',
|
||||||
|
},
|
||||||
|
}).addTo(nodeNeighboursLayerGroup);
|
||||||
|
|
||||||
|
// default to showing distance in meters
|
||||||
|
var distance = `${distanceInMeters} meters`;
|
||||||
|
|
||||||
|
// scale to distance in kms
|
||||||
|
if(distanceInMeters >= 1000){
|
||||||
|
const distanceInKilometers = (distanceInMeters / 1000).toFixed(2);
|
||||||
|
distance = `${distanceInKilometers} kilometers`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltip = `<b>${escapeString(node.long_name)}</b> heard <b>${escapeString(neighbourNode.long_name)}</b>`
|
||||||
|
+ `<br/>SNR: ${neighbour.snr}dB`
|
||||||
|
+ `<br/>Distance: ${distance}`
|
||||||
|
+ `<br/><br/>ID: ${node.node_id} heard ${neighbourNode.node_id}`
|
||||||
|
+ `<br/>Hex ID: ${node.node_id_hex} heard ${neighbourNode.node_id_hex}`
|
||||||
|
+ (node.neighbours_updated_at ? `<br/>Updated: ${moment(new Date(node.neighbours_updated_at)).fromNow()}` : '');
|
||||||
|
|
||||||
|
line.bindTooltip(tooltip, {
|
||||||
|
sticky: true,
|
||||||
|
opacity: 1,
|
||||||
|
interactive: true,
|
||||||
|
})
|
||||||
|
.bindPopup(tooltip)
|
||||||
|
.on('click', function(event) {
|
||||||
|
// close tooltip on click to prevent tooltip and popup showing at same time
|
||||||
|
event.target.closeTooltip();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNodeNeighboursThatHeardUs(id) {
|
||||||
|
|
||||||
|
cleanUpNodeNeighbours();
|
||||||
|
|
||||||
|
// find node
|
||||||
|
const node = findNodeById(id);
|
||||||
|
if(!node){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find node marker
|
||||||
|
const nodeMarker = findNodeMarkerById(node.node_id);
|
||||||
|
if(!nodeMarker){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find all nodes that have us as a neighbour
|
||||||
|
const neighbourNodeInfos = [];
|
||||||
|
for(const nodeThatMayHaveHeardUs of nodes){
|
||||||
|
|
||||||
|
// find our node in this nodes neighbours
|
||||||
|
const nodeNeighbours = nodeThatMayHaveHeardUs.neighbours ?? [];
|
||||||
|
const neighbour = nodeNeighbours.find(function(neighbour) {
|
||||||
|
return neighbour.node_id.toString() === node.node_id.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
// we exist as a neighbour
|
||||||
|
if(neighbour){
|
||||||
|
neighbourNodeInfos.push({
|
||||||
|
node: nodeThatMayHaveHeardUs,
|
||||||
|
neighbour: neighbour,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure we have neighbours to show
|
||||||
|
if(neighbourNodeInfos.length === 0){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add node neighbours
|
||||||
|
for(const neighbourNodeInfo of neighbourNodeInfos){
|
||||||
|
|
||||||
|
const neighbourNode = neighbourNodeInfo.node;
|
||||||
|
const neighbour = neighbourNodeInfo.neighbour;
|
||||||
|
|
||||||
|
// fixme: skipping zero snr? saw some crazy long neighbours with zero snr...
|
||||||
|
if(neighbour.snr === 0){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find neighbour node marker
|
||||||
|
const neighbourNodeMarker = findNodeMarkerById(neighbourNode.node_id);
|
||||||
|
if(!neighbourNodeMarker){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate distance in meters between nodes (rounded to 2 decimal places)
|
||||||
|
const distanceInMeters = neighbourNodeMarker.getLatLng().distanceTo(nodeMarker.getLatLng()).toFixed(2);
|
||||||
|
|
||||||
|
// don't show this neighbour connection if further than config allows
|
||||||
|
const configNeighboursMaxDistanceInMeters = getConfigNeighboursMaxDistanceInMeters();
|
||||||
|
if(configNeighboursMaxDistanceInMeters != null && parseFloat(distanceInMeters) > configNeighboursMaxDistanceInMeters){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add neighbour line to map
|
||||||
|
const line = L.polyline([
|
||||||
|
nodeMarker.getLatLng(), // from us
|
||||||
|
neighbourNodeMarker.getLatLng(), // to neighbour
|
||||||
|
], {
|
||||||
|
color: '#2563eb',
|
||||||
|
opacity: 0.5,
|
||||||
|
}).arrowheads({
|
||||||
|
size: '10px',
|
||||||
|
fill: true,
|
||||||
|
offsets: {
|
||||||
|
start: '25px',
|
||||||
|
end: '25px',
|
||||||
|
},
|
||||||
|
}).addTo(nodeNeighboursLayerGroup);
|
||||||
|
|
||||||
|
// default to showing distance in meters
|
||||||
|
var distance = `${distanceInMeters} meters`;
|
||||||
|
|
||||||
|
// scale to distance in kms
|
||||||
|
if(distanceInMeters >= 1000){
|
||||||
|
const distanceInKilometers = (distanceInMeters / 1000).toFixed(2);
|
||||||
|
distance = `${distanceInKilometers} kilometers`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltip = `<b>${escapeString(neighbourNode.long_name)}</b> heard <b>${escapeString(node.long_name)}</b>`
|
||||||
|
+ `<br/>SNR: ${neighbour.snr}dB`
|
||||||
|
+ `<br/>Distance: ${distance}`
|
||||||
|
+ `<br/><br/>ID: ${neighbourNode.node_id} heard ${node.node_id}`
|
||||||
|
+ `<br/>Hex ID: ${neighbourNode.node_id_hex} heard ${node.node_id_hex}`
|
||||||
|
+ (neighbourNode.neighbours_updated_at ? `<br/>Updated: ${moment(new Date(neighbourNode.neighbours_updated_at)).fromNow()}` : '');
|
||||||
|
|
||||||
|
line.bindTooltip(tooltip, {
|
||||||
|
sticky: true,
|
||||||
|
opacity: 1,
|
||||||
|
interactive: true,
|
||||||
|
})
|
||||||
|
.bindPopup(tooltip)
|
||||||
|
.on('click', function(event) {
|
||||||
|
// close tooltip on click to prevent tooltip and popup showing at same time
|
||||||
|
event.target.closeTooltip();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
function clearMap() {
|
function clearMap() {
|
||||||
closeAllPopups();
|
closeAllPopups();
|
||||||
closeAllTooltips();
|
closeAllTooltips();
|
||||||
@ -2574,7 +2805,9 @@
|
|||||||
tooltip += (node.position_updated_at ? `<br/>Position Updated: ${moment(new Date(node.position_updated_at)).fromNow()}` : '');
|
tooltip += (node.position_updated_at ? `<br/>Position Updated: ${moment(new Date(node.position_updated_at)).fromNow()}` : '');
|
||||||
|
|
||||||
// show details button
|
// show details button
|
||||||
tooltip += `<br/><br/><button onclick="showNodeDetails(${node.node_id});" class="border border-gray-300 bg-gray-100 p-1 w-full rounded hover:bg-gray-200">Show Full Details</button>`;
|
tooltip += `<br/><br/><button onclick="showNodeDetails(${node.node_id});" class="border border-gray-300 bg-gray-100 p-1 w-full rounded hover:bg-gray-200 mb-1">Show Full Details</button>`;
|
||||||
|
tooltip += `<br/><button onclick="showNodeNeighboursThatWeHeard(${node.node_id});" class="border border-gray-300 bg-gray-100 p-1 w-full rounded hover:bg-gray-200 mb-1">Show Neighbours (We Heard)</button>`;
|
||||||
|
tooltip += `<br/><button onclick="showNodeNeighboursThatHeardUs(${node.node_id});" class="border border-gray-300 bg-gray-100 p-1 w-full rounded hover:bg-gray-200">Show Neighbours (Heard Us)</button>`;
|
||||||
|
|
||||||
return tooltip;
|
return tooltip;
|
||||||
|
|
||||||
|
691
src/public/plugins/leaflet-arrowheads.js
Normal file
691
src/public/plugins/leaflet-arrowheads.js
Normal 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
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
807
src/public/plugins/leaflet.geometryutil.js
Normal file
807
src/public/plugins/leaflet.geometryutil.js
Normal 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;
|
||||||
|
|
||||||
|
}));
|
Reference in New Issue
Block a user