/* * 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', // | : geoJson field to be edited jsonOptions: {}, // Options to be used when retreiving Json from changed: 'edit-changed' // : 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, "'cut',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 alt; var found = 0, lls = [[],[]], times = [[],[]]; for (m in this._markers) { if (this._markers[m]._latlng.alt) { lls[found].push( [ this._markers[m]._latlng.lat, this._markers[m]._latlng.lng, this._markers[m]._latlng.alt ] ); } else { lls[found].push(this._markers[m]._latlng); } if (this._markers[m]._latlng.time) { times[found].push(this._markers[m]._latlng.time); } else { times[found].push(null); } 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) { var p = new L.Polyline(lls[f]); p.gpxedit_id = this._poly.gpxedit_id; if (times[f].length === p._latlngs.length) { for (var i=0; i