refs #14 beginning to add cut line feature by using leaflet.draw.plus
parent
b37dad5ca6
commit
f140907740
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* Copyright (c) 2015 Dominique Cavailhez
|
||||
* Leaflet extension for Leaflet.draw
|
||||
* Markers, polylines, polygons, rectangles & circle editor
|
||||
* Snap on others markers, lines & polygons including the edited shape itself
|
||||
* Requires https://github.com/Leaflet/Leaflet.draw & https://github.com/makinacorpus/Leaflet.Snap
|
||||
*/
|
||||
|
||||
L.Control.Draw.Plus = L.Control.Draw.extend({
|
||||
snapLayers: new L.FeatureGroup(), // Container for layers used for snap
|
||||
editLayers: new L.FeatureGroup(), // Container for editable layers
|
||||
|
||||
options: { // Force default to false to have to declare only required commands
|
||||
draw: {
|
||||
marker: false, // Capability to create a marker
|
||||
polyline: false, // Capability to create a polyline
|
||||
polygon: false, // Capability to create a polygon
|
||||
rectangle: false, // Capability to create a rectangle
|
||||
circle: false // Capability to create a circle
|
||||
},
|
||||
edit: {
|
||||
edit: false, // Capability to edit a feature
|
||||
remove: false // Capability to remove a feature
|
||||
},
|
||||
entry: 'edit-json', // <textarea id="edit-json">JSON</textarea> | <input type="hidden" id="edit-json" name="xxx" value="JSON"> : geoJson field to be edited
|
||||
jsonOptions: {}, // Options to be used when retreiving Json from <input />
|
||||
changed: 'edit-changed' // <span id="edit-changed" style="display:none">changed</span> : warn changes to be saved
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
// Allign drawing style on display
|
||||
L.Util.extend(L.Draw.Polyline.prototype.options.shapeOptions, L.Polyline.prototype.options);
|
||||
L.Util.extend(L.Draw.Polygon.prototype.options.shapeOptions, L.Polygon.prototype.options);
|
||||
|
||||
options.edit = L.extend(this.options.edit, options.edit); // Init false non chosen options
|
||||
options.draw = L.extend(this.options.draw, options.draw);
|
||||
for (var o in options.draw)
|
||||
if (options.draw[o])
|
||||
options.draw[o] = {
|
||||
guideLayers: [this.snapLayers] // Allow snap on creating elements
|
||||
};
|
||||
|
||||
L.Control.Draw.prototype.initialize.call(this, options);
|
||||
},
|
||||
|
||||
onAdd: function(map) {
|
||||
this._toolbars['edit'].options.featureGroup = this.editLayers; // Link the layers to edit
|
||||
this.editLayers.addTo(this.snapLayers); // Cascade to snapLayers & add the map
|
||||
this.snapLayers.addTo(map); // Make all this visble
|
||||
|
||||
// Add new features to the editor
|
||||
map.on('draw:created', function(e) {
|
||||
this.addLayer(e.layer);
|
||||
}, this);
|
||||
|
||||
// Remove deleted features from the editor
|
||||
map.on('layerremove', function(e) {
|
||||
this.editLayers.removeLayer(e.layer);
|
||||
}, this);
|
||||
|
||||
// Read geoJson field to be edited
|
||||
var ele = document.getElementById(this.options.entry);
|
||||
if (ele) {
|
||||
var elei = typeof ele.value != 'undefined' ? 'value' : 'innerHTML',
|
||||
gjs = JSON.parse(
|
||||
ele[elei].replace(/\s/g, '') || // Get & clean geoJson input
|
||||
'{"type":"FeatureCollection","features":[]}' // Default
|
||||
);
|
||||
|
||||
new L.GeoJSON(
|
||||
this.explodeMultiFeatures(gjs),
|
||||
this.options.jsonOptions
|
||||
).addTo(this);
|
||||
}
|
||||
|
||||
// Clean features & rewrite json field
|
||||
map.on('draw:created draw:editvertex', this._optimSavGeom, this); // When something has changed
|
||||
this._optimSavGeom(false); // At the init
|
||||
|
||||
return L.Control.Draw.prototype.onAdd.call(this, map);
|
||||
},
|
||||
|
||||
// Leaflet.draw does not work with multigeometry features such as MultiPoint, MultiLineString, MultiPolygon, or GeometryCollection.
|
||||
// If you need to add multigeometry features to the draw plugin, convert them to a FeatureCollection of non-multigeometries (Points, LineStrings, or Polygons).
|
||||
explodeMultiFeatures: function(f) {
|
||||
// Prepare replacement structure
|
||||
var r = {
|
||||
type: 'FeatureCollection',
|
||||
features: []
|
||||
};
|
||||
|
||||
switch (f.type) {
|
||||
case 'FeatureCollection': // Recurse in FeatureCollection
|
||||
for (var i = 0; i < f.features.length; i++)
|
||||
r.features.push(this.explodeMultiFeatures(f.features[i]));
|
||||
return r;
|
||||
|
||||
case 'Feature': // Recurse in Feature
|
||||
return this.explodeMultiFeatures(f.geometry);
|
||||
|
||||
case 'MultiPoint': // Convert Multi* geoms
|
||||
case 'MultiLineString':
|
||||
case 'MultiPolygon':
|
||||
for (var i = 0; i < f.coordinates.length; i++)
|
||||
r.features.push({
|
||||
type: f.type.replace('Multi', ''),
|
||||
coordinates: f.coordinates[i]
|
||||
});
|
||||
return r;
|
||||
}
|
||||
return f;
|
||||
},
|
||||
|
||||
addLayer: function(layer) {
|
||||
// Récurse in GeometryCollection
|
||||
if (layer._layers) {
|
||||
for (var l in layer._layers)
|
||||
this.addLayer(layer._layers[l]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Change color when hover (to be able to see different poly)
|
||||
layer.on('mouseover mouseout', function(e) {
|
||||
if (typeof e.target.setStyle == 'function')
|
||||
e.target.setStyle({
|
||||
color: e.type == 'mouseover' ? 'red' : L.Polyline.prototype.options.color
|
||||
});
|
||||
});
|
||||
|
||||
// Add snapping to vectors layers
|
||||
layer.addTo(this.editLayers);
|
||||
if (layer._latlng) // Point
|
||||
layer.snapediting = new L.Handler.MarkerSnap(this._map, layer);
|
||||
else if (layer._latlngs) // Polyline, Polygon, Rectangle
|
||||
layer.snapediting = new L.Handler.PolylineSnap(this._map, layer);
|
||||
else // ?? protection
|
||||
return;
|
||||
|
||||
layer.snapediting.addGuideLayer(this.snapLayers);
|
||||
layer.snapediting.enable();
|
||||
this._optimSavGeom(); // Optimize & write full json on output element
|
||||
|
||||
// Close enables edit toolbar handlers & save changes
|
||||
layer.on('deleted', function() {
|
||||
for (m in this._toolbars['edit']._modes)
|
||||
this._toolbars['edit']._modes[m].handler.disable();
|
||||
this._optimSavGeom();
|
||||
}, this);
|
||||
},
|
||||
|
||||
_optimSavGeom: function(changed) {
|
||||
// Optimize the edited layers
|
||||
var ls = this.editLayers._layers;
|
||||
if (!this._map.noOptim) // To optimize "cut" !!
|
||||
for (var il1 in ls) { // For all layers being edited
|
||||
var ll1 = ls[il1]._latlngs;
|
||||
if (ll1 && !ls[il1].options.fill) { // Only polylines
|
||||
|
||||
// Transform polyline whose the 2 ends match into polygon
|
||||
if (ll1[0].equals(ll1[ll1.length - 1]) && // The 2 ends match
|
||||
ll1.length > 3) { // If it will make at least a triangle (4 summits line).
|
||||
this.editLayers.removeLayer(ls[il1]); //DCCM TODO bug : create a bug while finishing dragend after the poly removal
|
||||
this.addLayer(new L.Polygon(ll1)); // Create a new polygon & restart optimization from scratch
|
||||
return; // End here the current optimization
|
||||
}
|
||||
|
||||
// Merge polylines having ends at the same position
|
||||
for (var il2 in ls) {
|
||||
var ll2 = ls[il2]._latlngs,
|
||||
lladd = null; // List of points to move to another polyline
|
||||
if (il1 < il2 && // Not the same & only once each pair
|
||||
ll2 && !ls[il2].options.fill) { // The 2nd is also a polyline
|
||||
if (ll1[0].equals(ll2[0])) {
|
||||
ll1.reverse();
|
||||
lladd = ll2;
|
||||
} else if (ll1[0].equals(ll2[ll2.length - 1])) {
|
||||
ll1.reverse();
|
||||
lladd = ll2.reverse();
|
||||
} else if (ll1[ll1.length - 1].equals(ll2[0])) {
|
||||
lladd = ll2;
|
||||
} else if (ll1[ll1.length - 1].equals(ll2[ll2.length - 1])) {
|
||||
lladd = ll2.reverse();
|
||||
}
|
||||
if (lladd) {
|
||||
lladd.shift(); // We avoid the first point as it's already on the first poly
|
||||
this.editLayers.removeLayer(ls[il1]); // Erase the initial polylines
|
||||
this.editLayers.removeLayer(ls[il2]);
|
||||
this.addLayer(new L.Polyline(ll1.concat(lladd))); // Create a new poly & restart optimization from scratch
|
||||
return; // End here the current optimization
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save edited data to the json output field
|
||||
var ele = document.getElementById(this.options.entry),
|
||||
elc = document.getElementById(this.options.changed);
|
||||
if (ele) {
|
||||
var elei = typeof ele.value != 'undefined' ? 'value' : 'innerHTML';
|
||||
ele[elei] = JSON.stringify(this.editLayers.toGeoJSON());
|
||||
}
|
||||
this._map.fire('draw:entry-changed'); // For user's usage
|
||||
|
||||
// Unmask "changed" message
|
||||
if (elc)
|
||||
elc.style.display = changed === false ? 'none' : '';
|
||||
}
|
||||
});
|
||||
|
||||
// Cut a polyline by removing a segment whose the middle marker is cliqued
|
||||
// Cut a polygon by removing a segment whose the middle marker is cliqued & transform it into polyline
|
||||
|
||||
// Horrible hack : modify onClick action on MiddleMarkers Leaflet.draw/Edit.Poly.js & generated files
|
||||
eval('L.Edit.PolyVerticesEdit.prototype._createMiddleMarker = ' +
|
||||
L.Edit.PolyVerticesEdit.prototype._createMiddleMarker.toString()
|
||||
.replace(/'click', onClick, this|'click',[a-z],this/g, "'click',this._cut,this")
|
||||
);
|
||||
|
||||
// Resize the too big summits markers
|
||||
L.Edit.PolyVerticesEdit.prototype.options.touchIcon.options.iconSize = new L.Point(8, 8);
|
||||
|
||||
L.Edit.PolyVerticesEdit.include({
|
||||
_cut: function(e) {
|
||||
// Split markers on each side of the cut
|
||||
var found = 0,
|
||||
lls = [[],[]];
|
||||
for (m in this._markers) {
|
||||
lls[found].push(this._markers[m]._latlng);
|
||||
if (this._markers[m]._middleRight && this._markers[m]._middleRight._leaflet_id == e.target._leaflet_id)
|
||||
found = 1; // We find the cut point
|
||||
}
|
||||
|
||||
// Remove the old poly
|
||||
this._map.removeLayer(this._poly);
|
||||
|
||||
// This is a Polygon, we will remove the clicked segment & transform it into a Polyline
|
||||
if (this._poly.options.fill)
|
||||
this._map.fire('draw:created', { // Create a new Polyline with these summits & optimize
|
||||
layer: new L.Polyline(lls[1].concat(lls[0]))
|
||||
});
|
||||
|
||||
// This is a polyline
|
||||
else
|
||||
for (f in lls)
|
||||
if (lls[f].length > 1)
|
||||
this._map.fire('draw:created', { // Create a new Polyline with the splited summits if any
|
||||
layer: new L.Polyline(lls[f])
|
||||
});
|
||||
|
||||
// Optimize
|
||||
this._map.fire('draw:editvertex');
|
||||
}
|
||||
});
|
|
@ -358,18 +358,13 @@
|
|||
).addTo(gpxedit.map);
|
||||
gpxedit.minimapControl._toggleDisplayButtonClicked();
|
||||
|
||||
gpxedit.editableLayers = new L.FeatureGroup();
|
||||
gpxedit.map.addLayer(gpxedit.editableLayers);
|
||||
//gpxedit.editableLayers = new L.FeatureGroup();
|
||||
//gpxedit.map.addLayer(gpxedit.editableLayers);
|
||||
|
||||
var options = {
|
||||
position: 'bottomleft',
|
||||
draw: {
|
||||
polyline: {
|
||||
shapeOptions: {
|
||||
color: '#f357a1',
|
||||
weight: 7
|
||||
}
|
||||
},
|
||||
polyline:true,
|
||||
polygon: false,
|
||||
circle: false,
|
||||
rectangle: false,
|
||||
|
@ -378,8 +373,11 @@
|
|||
}
|
||||
},
|
||||
edit: {
|
||||
featureGroup: gpxedit.editableLayers, //REQUIRED!!
|
||||
}
|
||||
edit: true,
|
||||
remove: true,
|
||||
//featureGroup: gpxedit.editableLayers,
|
||||
},
|
||||
entry: 'edit-json'
|
||||
};
|
||||
|
||||
L.drawLocal.draw.toolbar.buttons.polyline = t('gpxedit', 'Draw a track');
|
||||
|
@ -405,7 +403,7 @@
|
|||
L.drawLocal.draw.toolbar.finish.title = t('gpxedit', 'Finish drawing');
|
||||
L.drawLocal.draw.toolbar.undo.text = t('gpxedit', 'Delete last point');
|
||||
L.drawLocal.draw.toolbar.undo.title = t('gpxedit', 'Delete last point drawn');
|
||||
var drawControl = new L.Control.Draw(options);
|
||||
var drawControl = new L.Control.Draw.Plus(options);
|
||||
gpxedit.drawControl = drawControl;
|
||||
gpxedit.map.addControl(drawControl);
|
||||
|
||||
|
@ -526,7 +524,7 @@
|
|||
time: '',
|
||||
layer: layer
|
||||
};
|
||||
gpxedit.editableLayers.addLayer(layer);
|
||||
gpxedit.drawControl.editLayers.addLayer(layer);
|
||||
gpxedit.id++;
|
||||
return layer;
|
||||
}
|
||||
|
@ -577,7 +575,8 @@
|
|||
gpxText = gpxText + '</metadata>\n';
|
||||
|
||||
var layerArray = [];
|
||||
gpxedit.editableLayers.eachLayer(function(layer) {
|
||||
gpxedit.drawControl.editLayers.eachLayer(function(layer) {
|
||||
console.log(JSON.stringify(gpxedit.drawControl.editLayers.toGeoJSON()));
|
||||
layerArray.push(layer);
|
||||
});
|
||||
// sort
|
||||
|
@ -641,6 +640,7 @@
|
|||
var comment = gpxedit.layersData[id].comment;
|
||||
var description = gpxedit.layersData[id].description;
|
||||
var time = gpxedit.layersData[id].time;
|
||||
console.log('aa '+layer.type);
|
||||
if (layer.type === 'marker') {
|
||||
var symbol = gpxedit.layersData[id].symbol;
|
||||
lat = layer._latlng.lat;
|
||||
|
@ -670,7 +670,7 @@
|
|||
}
|
||||
gpxText = gpxText + ' </wpt>\n';
|
||||
}
|
||||
else if(layer.type === 'track') {
|
||||
else if(!layer.type || layer.type === 'track') {
|
||||
gpxText = gpxText + ' <trk>\n';
|
||||
if (name) {
|
||||
gpxText = gpxText + ' <name>' + name + '</name>\n';
|
||||
|
@ -871,14 +871,14 @@
|
|||
function clear() {
|
||||
var i;
|
||||
var layersToRemove = [];
|
||||
gpxedit.editableLayers.eachLayer(function (layer) {
|
||||
gpxedit.drawControl.editLayers.eachLayer(function (layer) {
|
||||
layer.unbindTooltip();
|
||||
delete gpxedit.layersData[layer.gpxedit_id];
|
||||
layersToRemove.push(layer);
|
||||
});
|
||||
|
||||
for(i = 0; i < layersToRemove.length; i++) {
|
||||
gpxedit.editableLayers.removeLayer(layersToRemove[i]);
|
||||
gpxedit.drawControl.editLayers.removeLayer(layersToRemove[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1001,7 +1001,7 @@
|
|||
parseGpx(response.gpxs[i]);
|
||||
}
|
||||
try {
|
||||
var bounds = gpxedit.editableLayers.getBounds();
|
||||
var bounds = gpxedit.drawControl.editLayers.getBounds();
|
||||
gpxedit.map.fitBounds(
|
||||
bounds,
|
||||
{
|
||||
|
@ -1081,7 +1081,7 @@
|
|||
else {
|
||||
parseGpx(response.gpx);
|
||||
try {
|
||||
var bounds = gpxedit.editableLayers.getBounds();
|
||||
var bounds = gpxedit.drawControl.editLayers.getBounds();
|
||||
gpxedit.map.fitBounds(
|
||||
bounds,
|
||||
{
|
||||
|
@ -1236,7 +1236,7 @@
|
|||
});
|
||||
|
||||
var symboo = $('#symboloverwrite').is(':checked');
|
||||
gpxedit.editableLayers.eachLayer(function(layer) {
|
||||
gpxedit.drawControl.editLayers.eachLayer(function(layer) {
|
||||
var id = layer.gpxedit_id;
|
||||
var name = gpxedit.layersData[id].name;
|
||||
var symbol = gpxedit.layersData[id].symbol;
|
||||
|
@ -1531,7 +1531,7 @@
|
|||
});
|
||||
|
||||
$('button#saveButton').click(function(e) {
|
||||
if (gpxedit.editableLayers.getLayers().length === 0) {
|
||||
if (gpxedit.drawControl.editLayers.getLayers().length === 0) {
|
||||
showFailAnimation(t('gpxedit', 'There is nothing to save'));
|
||||
}
|
||||
else{
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,736 @@
|
|||
// 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.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 = Math.ceil(distance) + ' m';
|
||||
}
|
||||
}
|
||||
else {
|
||||
distance *= 1.09361;
|
||||
if (distance > 1760) {
|
||||
distanceStr = (distance / 1760).toFixed(2) + ' miles';
|
||||
}
|
||||
else {
|
||||
distanceStr = Math.ceil(distance) + ' 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 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;
|
||||
|
||||
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 (var i = 0; i < layer.length; i++) {
|
||||
var subResult = L.GeometryUtil.closest(map, layer[i], latlng, vertices);
|
||||
if (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 (var 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
|
||||
var 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 {
|
||||
// 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 {
|
||||
// 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;
|
||||
var a = pts[0],
|
||||
b = pts[1],
|
||||
distA = 0,
|
||||
distB = a.distanceTo(b);
|
||||
// follow the line segments [ab], adding lengths,
|
||||
// until we find the segment where the points should lie on
|
||||
var index = 1;
|
||||
for (; index < n && distB < ratioDist; index++) {
|
||||
a = b;
|
||||
distA = distB;
|
||||
b = pts[index];
|
||||
distB += a.distanceTo(b);
|
||||
}
|
||||
// compute the ratio relative to the segment [ab]
|
||||
var segmentRatio = ((distB - distA) !== 0) ? ((ratioDist - distA) / (distB - distA)) : 0;
|
||||
var interpolatedPoint = L.GeometryUtil.interpolateOnPointSegment(a, b, segmentRatio);
|
||||
return {
|
||||
latLng: map.unproject(interpolatedPoint, maxzoom),
|
||||
predecessor: index-2
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
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)) {
|
||||
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 in degrees, clockwise from 0 degrees north.
|
||||
@param {float}: distance in meters
|
||||
@returns {L.latLng} the destination point.
|
||||
Many thanks to Chris Veness at http://www.movable-type.co.uk/scripts/latlong.html
|
||||
for a great reference and examples.
|
||||
*/
|
||||
destination: function(latlng, heading, distance) {
|
||||
heading = (heading + 360) % 360;
|
||||
var rad = Math.PI / 180,
|
||||
radInv = 180 / Math.PI,
|
||||
R = 6378137, // approximation of Earth's radius
|
||||
lon1 = latlng.lng * rad,
|
||||
lat1 = latlng.lat * rad,
|
||||
rheading = heading * rad,
|
||||
sinLat1 = Math.sin(lat1),
|
||||
cosLat1 = Math.cos(lat1),
|
||||
cosDistR = Math.cos(distance / R),
|
||||
sinDistR = Math.sin(distance / R),
|
||||
lat2 = Math.asin(sinLat1 * cosDistR + cosLat1 *
|
||||
sinDistR * Math.cos(rheading)),
|
||||
lon2 = lon1 + Math.atan2(Math.sin(rheading) * sinDistR *
|
||||
cosLat1, cosDistR - sinLat1 * Math.sin(lat2));
|
||||
lon2 = lon2 * radInv;
|
||||
lon2 = lon2 > 180 ? lon2 - 360 : lon2 < -180 ? lon2 + 360 : lon2;
|
||||
return L.latLng([lat2 * radInv, lon2]);
|
||||
}
|
||||
});
|
||||
|
||||
return L.GeometryUtil;
|
||||
|
||||
}));
|
|
@ -0,0 +1,360 @@
|
|||
(function () {
|
||||
|
||||
L.Handler.MarkerSnap = L.Handler.extend({
|
||||
options: {
|
||||
snapDistance: 15, // in pixels
|
||||
snapVertices: true
|
||||
},
|
||||
|
||||
initialize: function (map, marker, options) {
|
||||
L.Handler.prototype.initialize.call(this, map);
|
||||
this._markers = [];
|
||||
this._guides = [];
|
||||
|
||||
if (arguments.length == 2) {
|
||||
if (!(marker instanceof L.Class)) {
|
||||
options = marker;
|
||||
marker = null;
|
||||
}
|
||||
}
|
||||
|
||||
L.Util.setOptions(this, options || {});
|
||||
|
||||
if (marker) {
|
||||
// new markers should be draggable !
|
||||
if (!marker.dragging) marker.dragging = new L.Handler.MarkerDrag(marker);
|
||||
marker.dragging.enable();
|
||||
this.watchMarker(marker);
|
||||
}
|
||||
|
||||
// Convert snap distance in pixels into buffer in degres, for searching around mouse
|
||||
// It changes at each zoom change.
|
||||
function computeBuffer() {
|
||||
this._buffer = map.layerPointToLatLng(new L.Point(0,0)).lat -
|
||||
map.layerPointToLatLng(new L.Point(this.options.snapDistance, 0)).lat;
|
||||
}
|
||||
map.on('zoomend', computeBuffer, this);
|
||||
map.whenReady(computeBuffer, this);
|
||||
computeBuffer.call(this);
|
||||
},
|
||||
|
||||
enable: function () {
|
||||
this.disable();
|
||||
for (var i=0; i<this._markers.length; i++) {
|
||||
this.watchMarker(this._markers[i]);
|
||||
}
|
||||
},
|
||||
|
||||
disable: function () {
|
||||
for (var i=0; i<this._markers.length; i++) {
|
||||
this.unwatchMarker(this._markers[i]);
|
||||
}
|
||||
},
|
||||
|
||||
watchMarker: function (marker) {
|
||||
if (this._markers.indexOf(marker) == -1)
|
||||
this._markers.push(marker);
|
||||
marker.on('move', this._snapMarker, this);
|
||||
},
|
||||
|
||||
unwatchMarker: function (marker) {
|
||||
marker.off('move', this._snapMarker, this);
|
||||
delete marker['snap'];
|
||||
},
|
||||
|
||||
addGuideLayer: function (layer) {
|
||||
for (var i=0, n=this._guides.length; i<n; i++)
|
||||
if (L.stamp(layer) === L.stamp(this._guides[i]))
|
||||
return;
|
||||
this._guides.push(layer);
|
||||
},
|
||||
|
||||
_snapMarker: function(e) {
|
||||
var marker = e.target,
|
||||
latlng = marker.getLatLng(),
|
||||
snaplist = [];
|
||||
|
||||
function isDifferentLayer(layer) {
|
||||
if (layer.getLatLng) {
|
||||
return L.stamp(marker) !== L.stamp(layer);
|
||||
} else {
|
||||
if (layer.editing && layer.editing._enabled) {
|
||||
var points = layer.editing._verticesHandlers[0]._markerGroup.getLayers();
|
||||
for(var i = 0, n = points.length; i < n; i++) {
|
||||
if (L.stamp(points[i]) === L.stamp(marker)) { return false; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function processGuide(guide) {
|
||||
if ((guide._layers !== undefined) &&
|
||||
(typeof guide.searchBuffer !== 'function')) {
|
||||
// Guide is a layer group and has no L.LayerIndexMixin (from Leaflet.LayerIndex)
|
||||
for (var id in guide._layers) {
|
||||
processGuide(guide._layers[id]);
|
||||
}
|
||||
}
|
||||
else if (typeof guide.searchBuffer === 'function') {
|
||||
// Search snaplist around mouse
|
||||
var nearlayers = guide.searchBuffer(latlng, this._buffer);
|
||||
snaplist = snaplist.concat(nearlayers.filter(function(layer) {
|
||||
return isDifferentLayer(layer);
|
||||
}));
|
||||
}
|
||||
// Make sure the marker doesn't snap to itself or the associated polyline layer
|
||||
else if (isDifferentLayer(guide)) {
|
||||
snaplist.push(guide);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i=0, n = this._guides.length; i < n; i++) {
|
||||
var guide = this._guides[i];
|
||||
processGuide.call(this, guide);
|
||||
}
|
||||
|
||||
var closest = this._findClosestLayerSnap(this._map,
|
||||
snaplist,
|
||||
latlng,
|
||||
this.options.snapDistance,
|
||||
this.options.snapVertices);
|
||||
|
||||
closest = closest || {layer: null, latlng: null};
|
||||
this._updateSnap(marker, closest.layer, closest.latlng);
|
||||
},
|
||||
|
||||
_findClosestLayerSnap: function (map, layers, latlng, tolerance, withVertices) {
|
||||
return L.GeometryUtil.closestLayerSnap(map, layers, latlng, tolerance, withVertices);
|
||||
},
|
||||
|
||||
_updateSnap: function (marker, layer, latlng) {
|
||||
if (layer && latlng) {
|
||||
marker._latlng = L.latLng(latlng);
|
||||
marker.update();
|
||||
if (marker.snap != layer) {
|
||||
marker.snap = layer;
|
||||
if (marker._icon) L.DomUtil.addClass(marker._icon, 'marker-snapped');
|
||||
marker.fire('snap', {layer:layer, latlng: latlng});
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (marker.snap) {
|
||||
if (marker._icon) L.DomUtil.removeClass(marker._icon, 'marker-snapped');
|
||||
marker.fire('unsnap', {layer:marker.snap});
|
||||
}
|
||||
delete marker['snap'];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (!L.Edit) {
|
||||
// Leaflet.Draw not available.
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
L.Handler.PolylineSnap = L.Edit.Poly.extend({
|
||||
|
||||
initialize: function (map, poly, options) {
|
||||
var that = this;
|
||||
|
||||
L.Edit.Poly.prototype.initialize.call(this, poly, options);
|
||||
this._snapper = new L.Handler.MarkerSnap(map, options);
|
||||
poly.on('remove', function() {
|
||||
that.disable();
|
||||
});
|
||||
},
|
||||
|
||||
addGuideLayer: function (layer) {
|
||||
this._snapper.addGuideLayer(layer);
|
||||
},
|
||||
|
||||
_initHandlers: function () {
|
||||
this._verticesHandlers = [];
|
||||
for (var i = 0; i < this.latlngs.length; i++) {
|
||||
this._verticesHandlers.push(new L.Edit.PolyVerticesEditSnap(this._poly, this.latlngs[i], this.options));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
L.Edit.PolyVerticesEditSnap = L.Edit.PolyVerticesEdit.extend({
|
||||
_createMarker: function (latlng, index) {
|
||||
var marker = L.Edit.PolyVerticesEdit.prototype._createMarker.call(this, latlng, index);
|
||||
|
||||
// Treat middle markers differently
|
||||
var isMiddle = index === undefined;
|
||||
if (isMiddle) {
|
||||
// Snap middle markers, only once they were touched
|
||||
marker.on('dragstart', function () {
|
||||
this._poly.snapediting._snapper.watchMarker(marker);
|
||||
}, this);
|
||||
}
|
||||
else {
|
||||
this._poly.snapediting._snapper.watchMarker(marker);
|
||||
}
|
||||
return marker;
|
||||
}
|
||||
});
|
||||
|
||||
L.EditToolbar.SnapEdit = L.EditToolbar.Edit.extend({
|
||||
snapOptions: {
|
||||
snapDistance: 15, // in pixels
|
||||
snapVertices: true
|
||||
},
|
||||
|
||||
initialize: function(map, options) {
|
||||
L.EditToolbar.Edit.prototype.initialize.call(this, map, options);
|
||||
|
||||
if (options.snapOptions) {
|
||||
L.Util.extend(this.snapOptions, options.snapOptions);
|
||||
}
|
||||
|
||||
if (Array.isArray(this.snapOptions.guideLayers)) {
|
||||
this._guideLayers = this.snapOptions.guideLayers;
|
||||
} else if (options.guideLayers instanceof L.LayerGroup) {
|
||||
this._guideLayers = this.snapOptions.guideLayers.getLayers();
|
||||
} else {
|
||||
this._guideLayers = [];
|
||||
}
|
||||
},
|
||||
|
||||
addGuideLayer: function(layer) {
|
||||
var index = this._guideLayers.findIndex(function(guideLayer) {
|
||||
return L.stamp(layer) === L.stamp(guideLayer);
|
||||
});
|
||||
|
||||
if (index === -1) {
|
||||
this._guideLayers.push(layer);
|
||||
this._featureGroup.eachLayer(function(layer) {
|
||||
if (layer.snapediting) { layer.snapediting._guides.push(layer); }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
removeGuideLayer: function(layer) {
|
||||
var index = this._guideLayers.findIndex(function(guideLayer) {
|
||||
return L.stamp(layer) === L.stamp(guideLayer);
|
||||
});
|
||||
|
||||
if (index !== -1) {
|
||||
this._guideLayers.splice(index, 1);
|
||||
this._featureGroup.eachLayer(function(layer) {
|
||||
if (layer.snapediting) { layer.snapediting._guides.splice(index, 1); }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
clearGuideLayers: function() {
|
||||
this._guideLayers = [];
|
||||
this._featureGroup.eachLayer(function(layer) {
|
||||
if (layer.snapediting) { layer.snapediting._guides = []; }
|
||||
});
|
||||
},
|
||||
|
||||
_enableLayerEdit: function(e) {
|
||||
L.EditToolbar.Edit.prototype._enableLayerEdit.call(this, e);
|
||||
|
||||
var layer = e.layer || e.target || e;
|
||||
|
||||
if (!layer.snapediting) {
|
||||
if (layer.getLatLng) {
|
||||
layer.snapediting = new L.Handler.MarkerSnap(layer._map, layer, this.snapOptions);
|
||||
} else {
|
||||
if (layer.editing) {
|
||||
layer.editing._verticesHandlers[0]._markerGroup.clearLayers();
|
||||
delete layer.editing;
|
||||
}
|
||||
|
||||
layer.editing = layer.snapediting = new L.Handler.PolylineSnap(layer._map, layer, this.snapOptions);
|
||||
}
|
||||
|
||||
for (var i = 0, n = this._guideLayers.length; i < n; i++) {
|
||||
layer.snapediting.addGuideLayer(this._guideLayers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
layer.snapediting.enable();
|
||||
}
|
||||
});
|
||||
|
||||
L.Draw.Feature.SnapMixin = {
|
||||
_snap_initialize: function () {
|
||||
this.on('enabled', this._snap_on_enabled, this);
|
||||
this.on('disabled', this._snap_on_disabled, this);
|
||||
},
|
||||
|
||||
_snap_on_enabled: function () {
|
||||
if (!this.options.guideLayers) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._mouseMarker) {
|
||||
this._map.on('layeradd', this._snap_on_enabled, this);
|
||||
return;
|
||||
}else{
|
||||
this._map.off('layeradd', this._snap_on_enabled, this);
|
||||
}
|
||||
|
||||
if (!this._snapper) {
|
||||
this._snapper = new L.Handler.MarkerSnap(this._map);
|
||||
if (this.options.snapDistance) {
|
||||
this._snapper.options.snapDistance = this.options.snapDistance;
|
||||
}
|
||||
if (this.options.snapVertices) {
|
||||
this._snapper.options.snapVertices = this.options.snapVertices;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i=0, n=this.options.guideLayers.length; i<n; i++)
|
||||
this._snapper.addGuideLayer(this.options.guideLayers[i]);
|
||||
|
||||
var marker = this._mouseMarker;
|
||||
|
||||
this._snapper.watchMarker(marker);
|
||||
|
||||
// Show marker when (snap for user feedback)
|
||||
var icon = marker.options.icon;
|
||||
marker.on('snap', function (e) {
|
||||
marker.setIcon(this.options.icon);
|
||||
marker.setOpacity(1);
|
||||
}, this)
|
||||
.on('unsnap', function (e) {
|
||||
marker.setIcon(icon);
|
||||
marker.setOpacity(0);
|
||||
}, this);
|
||||
|
||||
marker.on('click', this._snap_on_click, this);
|
||||
},
|
||||
|
||||
_snap_on_click: function (e) {
|
||||
if (this._markers) {
|
||||
var markerCount = this._markers.length,
|
||||
marker = this._markers[markerCount - 1];
|
||||
if (this._mouseMarker.snap) {
|
||||
if(e){
|
||||
// update the feature being drawn to reflect the snapped location:
|
||||
marker.setLatLng(e.target._latlng);
|
||||
if(this._poly){
|
||||
var polyPointsCount = this._poly._latlngs.length;
|
||||
this._poly._latlngs[polyPointsCount - 1] = e.target._latlng;
|
||||
this._poly.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
L.DomUtil.addClass(marker._icon, 'marker-snapped');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_snap_on_disabled: function () {
|
||||
delete this._snapper;
|
||||
},
|
||||
};
|
||||
|
||||
L.Draw.Feature.include(L.Draw.Feature.SnapMixin);
|
||||
L.Draw.Feature.addInitHook('_snap_initialize');
|
||||
|
||||
})();
|
|
@ -11,7 +11,10 @@ script('gpxedit', 'jquery.mousewheel');
|
|||
script('gpxedit', 'detect_timezone');
|
||||
script('gpxedit', 'jquery.detect_timezone');
|
||||
script('gpxedit', 'moment-timezone-with-data.min');
|
||||
script('gpxedit', 'leaflet.draw');
|
||||
script('gpxedit', 'leaflet.draw-src');
|
||||
script('gpxedit', 'leaflet.geometryutil');
|
||||
script('gpxedit', 'leaflet.snap');
|
||||
script('gpxedit', 'Control.Draw.Plus');
|
||||
script('gpxedit', 'leaflet.measurecontrol');
|
||||
script('gpxedit', 'gpxedit');
|
||||
|
||||
|
|
Loading…
Reference in New Issue