refs #14 beginning to add cut line feature by using leaflet.draw.plus

merge-requests/1/head
Julien Veyssier 2017-05-08 03:31:49 +02:00
parent b37dad5ca6
commit f140907740
6 changed files with 5761 additions and 21 deletions

View File

@ -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');
}
});

View File

@ -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{

4387
js/leaflet.draw-src.js vendored 100644

File diff suppressed because it is too large Load Diff

View File

@ -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;
}));

360
js/leaflet.snap.js 100644
View File

@ -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');
})();

View File

@ -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');