UNPKG

leaflet-draw

Version:
1,906 lines (1,598 loc) 125 kB
/* Leaflet.draw 1.0.4, a plugin that adds drawing and editing tools to Leaflet powered maps. (c) 2012-2017, Jacob Toye, Jon West, Smartrak, Leaflet https://github.com/Leaflet/Leaflet.draw http://leafletjs.com */ (function (window, document, undefined) {/** * Leaflet.draw assumes that you have already included the Leaflet library. */ L.drawVersion = "1.0.4"; /** * @class L.Draw * @aka Draw * * * To add the draw toolbar set the option drawControl: true in the map options. * * @example * ```js * var map = L.map('map', {drawControl: true}).setView([51.505, -0.09], 13); * * L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { * attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' * }).addTo(map); * ``` * * ### Adding the edit toolbar * To use the edit toolbar you must initialise the Leaflet.draw control and manually add it to the map. * * ```js * var map = L.map('map').setView([51.505, -0.09], 13); * * L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { * attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' * }).addTo(map); * * // FeatureGroup is to store editable layers * var drawnItems = new L.FeatureGroup(); * map.addLayer(drawnItems); * * var drawControl = new L.Control.Draw({ * edit: { * featureGroup: drawnItems * } * }); * map.addControl(drawControl); * ``` * * The key here is the featureGroup option. This tells the plugin which FeatureGroup contains the layers that * should be editable. The featureGroup can contain 0 or more features with geometry types Point, LineString, and Polygon. * 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). */ L.Draw = {}; /** * @class L.drawLocal * @aka L.drawLocal * * The core toolbar class of the API — it is used to create the toolbar ui * * @example * ```js * var modifiedDraw = L.drawLocal.extend({ * draw: { * toolbar: { * buttons: { * polygon: 'Draw an awesome polygon' * } * } * } * }); * ``` * * The default state for the control is the draw toolbar just below the zoom control. * This will allow map users to draw vectors and markers. * **Please note the edit toolbar is not enabled by default.** */ L.drawLocal = { // format: { // numeric: { // delimiters: { // thousands: ',', // decimal: '.' // } // } // }, draw: { toolbar: { // #TODO: this should be reorganized where actions are nested in actions // ex: actions.undo or actions.cancel actions: { title: 'Cancel drawing', text: 'Cancel' }, finish: { title: 'Finish drawing', text: 'Finish' }, undo: { title: 'Delete last point drawn', text: 'Delete last point' }, buttons: { polyline: 'Draw a polyline', polygon: 'Draw a polygon', rectangle: 'Draw a rectangle', circle: 'Draw a circle', marker: 'Draw a marker', circlemarker: 'Draw a circlemarker' } }, handlers: { circle: { tooltip: { start: 'Click and drag to draw circle.' }, radius: 'Radius' }, circlemarker: { tooltip: { start: 'Click map to place circle marker.' } }, marker: { tooltip: { start: 'Click map to place marker.' } }, polygon: { tooltip: { start: 'Click to start drawing shape.', cont: 'Click to continue drawing shape.', end: 'Click first point to close this shape.' } }, polyline: { error: '<strong>Error:</strong> shape edges cannot cross!', tooltip: { start: 'Click to start drawing line.', cont: 'Click to continue drawing line.', end: 'Click last point to finish line.' } }, rectangle: { tooltip: { start: 'Click and drag to draw rectangle.' } }, simpleshape: { tooltip: { end: 'Release mouse to finish drawing.' } } } }, edit: { toolbar: { actions: { save: { title: 'Save changes', text: 'Save' }, cancel: { title: 'Cancel editing, discards all changes', text: 'Cancel' }, clearAll: { title: 'Clear all layers', text: 'Clear All' } }, buttons: { edit: 'Edit layers', editDisabled: 'No layers to edit', remove: 'Delete layers', removeDisabled: 'No layers to delete' } }, handlers: { edit: { tooltip: { text: 'Drag handles or markers to edit features.', subtext: 'Click cancel to undo changes.' } }, remove: { tooltip: { text: 'Click on a feature to remove.' } } } } }; /** * ### Events * Once you have successfully added the Leaflet.draw plugin to your map you will want to respond to the different * actions users can initiate. The following events will be triggered on the map: * * @class L.Draw.Event * @aka Draw.Event * * Use `L.Draw.Event.EVENTNAME` constants to ensure events are correct. * * @example * ```js * map.on(L.Draw.Event.CREATED; function (e) { * var type = e.layerType, * layer = e.layer; * * if (type === 'marker') { * // Do marker specific actions * } * * // Do whatever else you need to. (save to db; add to map etc) * map.addLayer(layer); *}); * ``` */ L.Draw.Event = {}; /** * @event draw:created: PolyLine; Polygon; Rectangle; Circle; Marker | String * * Layer that was just created. * The type of layer this is. One of: `polyline`; `polygon`; `rectangle`; `circle`; `marker` * Triggered when a new vector or marker has been created. * */ L.Draw.Event.CREATED = 'draw:created'; /** * @event draw:edited: LayerGroup * * List of all layers just edited on the map. * * * Triggered when layers in the FeatureGroup; initialised with the plugin; have been edited and saved. * * @example * ```js * map.on('draw:edited', function (e) { * var layers = e.layers; * layers.eachLayer(function (layer) { * //do whatever you want; most likely save back to db * }); * }); * ``` */ L.Draw.Event.EDITED = 'draw:edited'; /** * @event draw:deleted: LayerGroup * * List of all layers just removed from the map. * * Triggered when layers have been removed (and saved) from the FeatureGroup. */ L.Draw.Event.DELETED = 'draw:deleted'; /** * @event draw:drawstart: String * * The type of layer this is. One of:`polyline`; `polygon`; `rectangle`; `circle`; `marker` * * Triggered when the user has chosen to draw a particular vector or marker. */ L.Draw.Event.DRAWSTART = 'draw:drawstart'; /** * @event draw:drawstop: String * * The type of layer this is. One of: `polyline`; `polygon`; `rectangle`; `circle`; `marker` * * Triggered when the user has finished a particular vector or marker. */ L.Draw.Event.DRAWSTOP = 'draw:drawstop'; /** * @event draw:drawvertex: LayerGroup * * List of all layers just being added from the map. * * Triggered when a vertex is created on a polyline or polygon. */ L.Draw.Event.DRAWVERTEX = 'draw:drawvertex'; /** * @event draw:editstart: String * * The type of edit this is. One of: `edit` * * Triggered when the user starts edit mode by clicking the edit tool button. */ L.Draw.Event.EDITSTART = 'draw:editstart'; /** * @event draw:editmove: ILayer * * Layer that was just moved. * * Triggered as the user moves a rectangle; circle or marker. */ L.Draw.Event.EDITMOVE = 'draw:editmove'; /** * @event draw:editresize: ILayer * * Layer that was just moved. * * Triggered as the user resizes a rectangle or circle. */ L.Draw.Event.EDITRESIZE = 'draw:editresize'; /** * @event draw:editvertex: LayerGroup * * List of all layers just being edited from the map. * * Triggered when a vertex is edited on a polyline or polygon. */ L.Draw.Event.EDITVERTEX = 'draw:editvertex'; /** * @event draw:editstop: String * * The type of edit this is. One of: `edit` * * Triggered when the user has finshed editing (edit mode) and saves edits. */ L.Draw.Event.EDITSTOP = 'draw:editstop'; /** * @event draw:deletestart: String * * The type of edit this is. One of: `remove` * * Triggered when the user starts remove mode by clicking the remove tool button. */ L.Draw.Event.DELETESTART = 'draw:deletestart'; /** * @event draw:deletestop: String * * The type of edit this is. One of: `remove` * * Triggered when the user has finished removing shapes (remove mode) and saves. */ L.Draw.Event.DELETESTOP = 'draw:deletestop'; /** * @event draw:toolbaropened: String * * Triggered when a toolbar is opened. */ L.Draw.Event.TOOLBAROPENED = 'draw:toolbaropened'; /** * @event draw:toolbarclosed: String * * Triggered when a toolbar is closed. */ L.Draw.Event.TOOLBARCLOSED = 'draw:toolbarclosed'; /** * @event draw:markercontext: String * * Triggered when a marker is right clicked. */ L.Draw.Event.MARKERCONTEXT = 'draw:markercontext'; L.Draw = L.Draw || {}; /** * @class L.Draw.Feature * @aka Draw.Feature */ L.Draw.Feature = L.Handler.extend({ // @method initialize(): void initialize: function (map, options) { this._map = map; this._container = map._container; this._overlayPane = map._panes.overlayPane; this._popupPane = map._panes.popupPane; // Merge default shapeOptions options with custom shapeOptions if (options && options.shapeOptions) { options.shapeOptions = L.Util.extend({}, this.options.shapeOptions, options.shapeOptions); } L.setOptions(this, options); var version = L.version.split('.'); //If Version is >= 1.2.0 if (parseInt(version[0], 10) === 1 && parseInt(version[1], 10) >= 2) { L.Draw.Feature.include(L.Evented.prototype); } else { L.Draw.Feature.include(L.Mixin.Events); } }, // @method enable(): void // Enables this handler enable: function () { if (this._enabled) { return; } L.Handler.prototype.enable.call(this); this.fire('enabled', {handler: this.type}); this._map.fire(L.Draw.Event.DRAWSTART, {layerType: this.type}); }, // @method disable(): void disable: function () { if (!this._enabled) { return; } L.Handler.prototype.disable.call(this); this._map.fire(L.Draw.Event.DRAWSTOP, {layerType: this.type}); this.fire('disabled', {handler: this.type}); }, // @method addHooks(): void // Add's event listeners to this handler addHooks: function () { var map = this._map; if (map) { L.DomUtil.disableTextSelection(); map.getContainer().focus(); this._tooltip = new L.Draw.Tooltip(this._map); L.DomEvent.on(this._container, 'keyup', this._cancelDrawing, this); } }, // @method removeHooks(): void // Removes event listeners from this handler removeHooks: function () { if (this._map) { L.DomUtil.enableTextSelection(); this._tooltip.dispose(); this._tooltip = null; L.DomEvent.off(this._container, 'keyup', this._cancelDrawing, this); } }, // @method setOptions(object): void // Sets new options to this handler setOptions: function (options) { L.setOptions(this, options); }, _fireCreatedEvent: function (layer) { this._map.fire(L.Draw.Event.CREATED, {layer: layer, layerType: this.type}); }, // Cancel drawing when the escape key is pressed _cancelDrawing: function (e) { if (e.keyCode === 27) { this._map.fire('draw:canceled', {layerType: this.type}); this.disable(); } } }); /** * @class L.Draw.Polyline * @aka Draw.Polyline * @inherits L.Draw.Feature */ L.Draw.Polyline = L.Draw.Feature.extend({ statics: { TYPE: 'polyline' }, Poly: L.Polyline, options: { allowIntersection: true, repeatMode: false, drawError: { color: '#b00b00', timeout: 2500 }, icon: new L.DivIcon({ iconSize: new L.Point(8, 8), className: 'leaflet-div-icon leaflet-editing-icon' }), touchIcon: new L.DivIcon({ iconSize: new L.Point(20, 20), className: 'leaflet-div-icon leaflet-editing-icon leaflet-touch-icon' }), guidelineDistance: 20, maxGuideLineLength: 4000, shapeOptions: { stroke: true, color: '#3388ff', weight: 4, opacity: 0.5, fill: false, clickable: true }, metric: true, // Whether to use the metric measurement system or imperial feet: true, // When not metric, to use feet instead of yards for display. nautic: false, // When not metric, not feet use nautic mile for display showLength: true, // Whether to display distance in the tooltip zIndexOffset: 2000, // This should be > than the highest z-index any map layers factor: 1, // To change distance calculation maxPoints: 0 // Once this number of points are placed, finish shape }, // @method initialize(): void initialize: function (map, options) { // if touch, switch to touch icon if (L.Browser.touch) { this.options.icon = this.options.touchIcon; } // Need to set this here to ensure the correct message is used. this.options.drawError.message = L.drawLocal.draw.handlers.polyline.error; // Merge default drawError options with custom options if (options && options.drawError) { options.drawError = L.Util.extend({}, this.options.drawError, options.drawError); } // Save the type so super can fire, need to do this as cannot do this.TYPE :( this.type = L.Draw.Polyline.TYPE; L.Draw.Feature.prototype.initialize.call(this, map, options); }, // @method addHooks(): void // Add listener hooks to this handler addHooks: function () { L.Draw.Feature.prototype.addHooks.call(this); if (this._map) { this._markers = []; this._markerGroup = new L.LayerGroup(); this._map.addLayer(this._markerGroup); this._poly = new L.Polyline([], this.options.shapeOptions); this._tooltip.updateContent(this._getTooltipText()); // Make a transparent marker that will used to catch click events. These click // events will create the vertices. We need to do this so we can ensure that // we can create vertices over other map layers (markers, vector layers). We // also do not want to trigger any click handlers of objects we are clicking on // while drawing. if (!this._mouseMarker) { this._mouseMarker = L.marker(this._map.getCenter(), { icon: L.divIcon({ className: 'leaflet-mouse-marker', iconAnchor: [20, 20], iconSize: [40, 40] }), opacity: 0, zIndexOffset: this.options.zIndexOffset }); } this._mouseMarker .on('mouseout', this._onMouseOut, this) .on('mousemove', this._onMouseMove, this) // Necessary to prevent 0.8 stutter .on('mousedown', this._onMouseDown, this) .on('mouseup', this._onMouseUp, this) // Necessary for 0.8 compatibility .addTo(this._map); this._map .on('mouseup', this._onMouseUp, this) // Necessary for 0.7 compatibility .on('mousemove', this._onMouseMove, this) .on('zoomlevelschange', this._onZoomEnd, this) .on('touchstart', this._onTouch, this) .on('zoomend', this._onZoomEnd, this); } }, // @method removeHooks(): void // Remove listener hooks from this handler. removeHooks: function () { L.Draw.Feature.prototype.removeHooks.call(this); this._clearHideErrorTimeout(); this._cleanUpShape(); // remove markers from map this._map.removeLayer(this._markerGroup); delete this._markerGroup; delete this._markers; this._map.removeLayer(this._poly); delete this._poly; this._mouseMarker .off('mousedown', this._onMouseDown, this) .off('mouseout', this._onMouseOut, this) .off('mouseup', this._onMouseUp, this) .off('mousemove', this._onMouseMove, this); this._map.removeLayer(this._mouseMarker); delete this._mouseMarker; // clean up DOM this._clearGuides(); this._map .off('mouseup', this._onMouseUp, this) .off('mousemove', this._onMouseMove, this) .off('zoomlevelschange', this._onZoomEnd, this) .off('zoomend', this._onZoomEnd, this) .off('touchstart', this._onTouch, this) .off('click', this._onTouch, this); }, // @method deleteLastVertex(): void // Remove the last vertex from the polyline, removes polyline from map if only one point exists. deleteLastVertex: function () { if (this._markers.length <= 1) { return; } var lastMarker = this._markers.pop(), poly = this._poly, // Replaces .spliceLatLngs() latlngs = poly.getLatLngs(), latlng = latlngs.splice(-1, 1)[0]; this._poly.setLatLngs(latlngs); this._markerGroup.removeLayer(lastMarker); if (poly.getLatLngs().length < 2) { this._map.removeLayer(poly); } this._vertexChanged(latlng, false); }, // @method addVertex(): void // Add a vertex to the end of the polyline addVertex: function (latlng) { var markersLength = this._markers.length; // markersLength must be greater than or equal to 2 before intersections can occur if (markersLength >= 2 && !this.options.allowIntersection && this._poly.newLatLngIntersects(latlng)) { this._showErrorTooltip(); return; } else if (this._errorShown) { this._hideErrorTooltip(); } this._markers.push(this._createMarker(latlng)); this._poly.addLatLng(latlng); if (this._poly.getLatLngs().length === 2) { this._map.addLayer(this._poly); } this._vertexChanged(latlng, true); }, // @method completeShape(): void // Closes the polyline between the first and last points completeShape: function () { if (this._markers.length <= 1 || !this._shapeIsValid()) { return; } this._fireCreatedEvent(); this.disable(); if (this.options.repeatMode) { this.enable(); } }, _finishShape: function () { var latlngs = this._poly._defaultShape ? this._poly._defaultShape() : this._poly.getLatLngs(); var intersects = this._poly.newLatLngIntersects(latlngs[latlngs.length - 1]); if ((!this.options.allowIntersection && intersects) || !this._shapeIsValid()) { this._showErrorTooltip(); return; } this._fireCreatedEvent(); this.disable(); if (this.options.repeatMode) { this.enable(); } }, // Called to verify the shape is valid when the user tries to finish it // Return false if the shape is not valid _shapeIsValid: function () { return true; }, _onZoomEnd: function () { if (this._markers !== null) { this._updateGuide(); } }, _onMouseMove: function (e) { var newPos = this._map.mouseEventToLayerPoint(e.originalEvent); var latlng = this._map.layerPointToLatLng(newPos); // Save latlng // should this be moved to _updateGuide() ? this._currentLatLng = latlng; this._updateTooltip(latlng); // Update the guide line this._updateGuide(newPos); // Update the mouse marker position this._mouseMarker.setLatLng(latlng); L.DomEvent.preventDefault(e.originalEvent); }, _vertexChanged: function (latlng, added) { this._map.fire(L.Draw.Event.DRAWVERTEX, {layers: this._markerGroup}); this._updateFinishHandler(); this._updateRunningMeasure(latlng, added); this._clearGuides(); this._updateTooltip(); }, _onMouseDown: function (e) { if (!this._clickHandled && !this._touchHandled && !this._disableMarkers) { this._onMouseMove(e); this._clickHandled = true; this._disableNewMarkers(); var originalEvent = e.originalEvent; var clientX = originalEvent.clientX; var clientY = originalEvent.clientY; this._startPoint.call(this, clientX, clientY); } }, _startPoint: function (clientX, clientY) { this._mouseDownOrigin = L.point(clientX, clientY); }, _onMouseUp: function (e) { var originalEvent = e.originalEvent; var clientX = originalEvent.clientX; var clientY = originalEvent.clientY; this._endPoint.call(this, clientX, clientY, e); this._clickHandled = null; }, _endPoint: function (clientX, clientY, e) { if (this._mouseDownOrigin) { var dragCheckDistance = L.point(clientX, clientY) .distanceTo(this._mouseDownOrigin); var lastPtDistance = this._calculateFinishDistance(e.latlng); if (this.options.maxPoints > 1 && this.options.maxPoints == this._markers.length + 1) { this.addVertex(e.latlng); this._finishShape(); } else if (lastPtDistance < 10 && L.Browser.touch) { this._finishShape(); } else if (Math.abs(dragCheckDistance) < 9 * (window.devicePixelRatio || 1)) { this.addVertex(e.latlng); } this._enableNewMarkers(); // after a short pause, enable new markers } this._mouseDownOrigin = null; }, // ontouch prevented by clickHandled flag because some browsers fire both click/touch events, // causing unwanted behavior _onTouch: function (e) { var originalEvent = e.originalEvent; var clientX; var clientY; if (originalEvent.touches && originalEvent.touches[0] && !this._clickHandled && !this._touchHandled && !this._disableMarkers) { clientX = originalEvent.touches[0].clientX; clientY = originalEvent.touches[0].clientY; this._disableNewMarkers(); this._touchHandled = true; this._startPoint.call(this, clientX, clientY); this._endPoint.call(this, clientX, clientY, e); this._touchHandled = null; } this._clickHandled = null; }, _onMouseOut: function () { if (this._tooltip) { this._tooltip._onMouseOut.call(this._tooltip); } }, // calculate if we are currently within close enough distance // of the closing point (first point for shapes, last point for lines) // this is semi-ugly code but the only reliable way i found to get the job done // note: calculating point.distanceTo between mouseDownOrigin and last marker did NOT work _calculateFinishDistance: function (potentialLatLng) { var lastPtDistance; if (this._markers.length > 0) { var finishMarker; if (this.type === L.Draw.Polyline.TYPE) { finishMarker = this._markers[this._markers.length - 1]; } else if (this.type === L.Draw.Polygon.TYPE) { finishMarker = this._markers[0]; } else { return Infinity; } var lastMarkerPoint = this._map.latLngToContainerPoint(finishMarker.getLatLng()), potentialMarker = new L.Marker(potentialLatLng, { icon: this.options.icon, zIndexOffset: this.options.zIndexOffset * 2 }); var potentialMarkerPint = this._map.latLngToContainerPoint(potentialMarker.getLatLng()); lastPtDistance = lastMarkerPoint.distanceTo(potentialMarkerPint); } else { lastPtDistance = Infinity; } return lastPtDistance; }, _updateFinishHandler: function () { var markerCount = this._markers.length; // The last marker should have a click handler to close the polyline if (markerCount > 1) { this._markers[markerCount - 1].on('click', this._finishShape, this); } // Remove the old marker click handler (as only the last point should close the polyline) if (markerCount > 2) { this._markers[markerCount - 2].off('click', this._finishShape, this); } }, _createMarker: function (latlng) { var marker = new L.Marker(latlng, { icon: this.options.icon, zIndexOffset: this.options.zIndexOffset * 2 }); this._markerGroup.addLayer(marker); return marker; }, _updateGuide: function (newPos) { var markerCount = this._markers ? this._markers.length : 0; if (markerCount > 0) { newPos = newPos || this._map.latLngToLayerPoint(this._currentLatLng); // draw the guide line this._clearGuides(); this._drawGuide( this._map.latLngToLayerPoint(this._markers[markerCount - 1].getLatLng()), newPos ); } }, _updateTooltip: function (latLng) { var text = this._getTooltipText(); if (latLng) { this._tooltip.updatePosition(latLng); } if (!this._errorShown) { this._tooltip.updateContent(text); } }, _drawGuide: function (pointA, pointB) { var length = Math.floor(Math.sqrt(Math.pow((pointB.x - pointA.x), 2) + Math.pow((pointB.y - pointA.y), 2))), guidelineDistance = this.options.guidelineDistance, maxGuideLineLength = this.options.maxGuideLineLength, // Only draw a guideline with a max length i = length > maxGuideLineLength ? length - maxGuideLineLength : guidelineDistance, fraction, dashPoint, dash; //create the guides container if we haven't yet if (!this._guidesContainer) { this._guidesContainer = L.DomUtil.create('div', 'leaflet-draw-guides', this._overlayPane); } //draw a dash every GuildeLineDistance for (; i < length; i += this.options.guidelineDistance) { //work out fraction along line we are fraction = i / length; //calculate new x,y point dashPoint = { x: Math.floor((pointA.x * (1 - fraction)) + (fraction * pointB.x)), y: Math.floor((pointA.y * (1 - fraction)) + (fraction * pointB.y)) }; //add guide dash to guide container dash = L.DomUtil.create('div', 'leaflet-draw-guide-dash', this._guidesContainer); dash.style.backgroundColor = !this._errorShown ? this.options.shapeOptions.color : this.options.drawError.color; L.DomUtil.setPosition(dash, dashPoint); } }, _updateGuideColor: function (color) { if (this._guidesContainer) { for (var i = 0, l = this._guidesContainer.childNodes.length; i < l; i++) { this._guidesContainer.childNodes[i].style.backgroundColor = color; } } }, // removes all child elements (guide dashes) from the guides container _clearGuides: function () { if (this._guidesContainer) { while (this._guidesContainer.firstChild) { this._guidesContainer.removeChild(this._guidesContainer.firstChild); } } }, _getTooltipText: function () { var showLength = this.options.showLength, labelText, distanceStr; if (this._markers.length === 0) { labelText = { text: L.drawLocal.draw.handlers.polyline.tooltip.start }; } else { distanceStr = showLength ? this._getMeasurementString() : ''; if (this._markers.length === 1) { labelText = { text: L.drawLocal.draw.handlers.polyline.tooltip.cont, subtext: distanceStr }; } else { labelText = { text: L.drawLocal.draw.handlers.polyline.tooltip.end, subtext: distanceStr }; } } return labelText; }, _updateRunningMeasure: function (latlng, added) { var markersLength = this._markers.length, previousMarkerIndex, distance; if (this._markers.length === 1) { this._measurementRunningTotal = 0; } else { previousMarkerIndex = markersLength - (added ? 2 : 1); // Calculate the distance based on the version if (L.GeometryUtil.isVersion07x()) { distance = latlng.distanceTo(this._markers[previousMarkerIndex].getLatLng()) * (this.options.factor || 1); } else { distance = this._map.distance(latlng, this._markers[previousMarkerIndex].getLatLng()) * (this.options.factor || 1); } this._measurementRunningTotal += distance * (added ? 1 : -1); } }, _getMeasurementString: function () { var currentLatLng = this._currentLatLng, previousLatLng = this._markers[this._markers.length - 1].getLatLng(), distance; // Calculate the distance from the last fixed point to the mouse position based on the version if (L.GeometryUtil.isVersion07x()) { distance = previousLatLng && currentLatLng && currentLatLng.distanceTo ? this._measurementRunningTotal + currentLatLng.distanceTo(previousLatLng) * (this.options.factor || 1) : this._measurementRunningTotal || 0; } else { distance = previousLatLng && currentLatLng ? this._measurementRunningTotal + this._map.distance(currentLatLng, previousLatLng) * (this.options.factor || 1) : this._measurementRunningTotal || 0; } return L.GeometryUtil.readableDistance(distance, this.options.metric, this.options.feet, this.options.nautic, this.options.precision); }, _showErrorTooltip: function () { this._errorShown = true; // Update tooltip this._tooltip .showAsError() .updateContent({text: this.options.drawError.message}); // Update shape this._updateGuideColor(this.options.drawError.color); this._poly.setStyle({color: this.options.drawError.color}); // Hide the error after 2 seconds this._clearHideErrorTimeout(); this._hideErrorTimeout = setTimeout(L.Util.bind(this._hideErrorTooltip, this), this.options.drawError.timeout); }, _hideErrorTooltip: function () { this._errorShown = false; this._clearHideErrorTimeout(); // Revert tooltip this._tooltip .removeError() .updateContent(this._getTooltipText()); // Revert shape this._updateGuideColor(this.options.shapeOptions.color); this._poly.setStyle({color: this.options.shapeOptions.color}); }, _clearHideErrorTimeout: function () { if (this._hideErrorTimeout) { clearTimeout(this._hideErrorTimeout); this._hideErrorTimeout = null; } }, // disable new markers temporarily; // this is to prevent duplicated touch/click events in some browsers _disableNewMarkers: function () { this._disableMarkers = true; }, // see _disableNewMarkers _enableNewMarkers: function () { setTimeout(function () { this._disableMarkers = false; }.bind(this), 50); }, _cleanUpShape: function () { if (this._markers.length > 1) { this._markers[this._markers.length - 1].off('click', this._finishShape, this); } }, _fireCreatedEvent: function () { var poly = new this.Poly(this._poly.getLatLngs(), this.options.shapeOptions); L.Draw.Feature.prototype._fireCreatedEvent.call(this, poly); } }); /** * @class L.Draw.Polygon * @aka Draw.Polygon * @inherits L.Draw.Polyline */ L.Draw.Polygon = L.Draw.Polyline.extend({ statics: { TYPE: 'polygon' }, Poly: L.Polygon, options: { showArea: false, showLength: false, shapeOptions: { stroke: true, color: '#3388ff', weight: 4, opacity: 0.5, fill: true, fillColor: null, //same as color by default fillOpacity: 0.2, clickable: true }, // Whether to use the metric measurement system (truthy) or not (falsy). // Also defines the units to use for the metric system as an array of // strings (e.g. `['ha', 'm']`). metric: true, feet: true, // When not metric, to use feet instead of yards for display. nautic: false, // When not metric, not feet use nautic mile for display // Defines the precision for each type of unit (e.g. {km: 2, ft: 0} precision: {} }, // @method initialize(): void initialize: function (map, options) { L.Draw.Polyline.prototype.initialize.call(this, map, options); // Save the type so super can fire, need to do this as cannot do this.TYPE :( this.type = L.Draw.Polygon.TYPE; }, _updateFinishHandler: function () { var markerCount = this._markers.length; // The first marker should have a click handler to close the polygon if (markerCount === 1) { this._markers[0].on('click', this._finishShape, this); } // Add and update the double click handler if (markerCount > 2) { this._markers[markerCount - 1].on('dblclick', this._finishShape, this); // Only need to remove handler if has been added before if (markerCount > 3) { this._markers[markerCount - 2].off('dblclick', this._finishShape, this); } } }, _getTooltipText: function () { var text, subtext; if (this._markers.length === 0) { text = L.drawLocal.draw.handlers.polygon.tooltip.start; } else if (this._markers.length < 3) { text = L.drawLocal.draw.handlers.polygon.tooltip.cont; subtext = this._getMeasurementString(); } else { text = L.drawLocal.draw.handlers.polygon.tooltip.end; subtext = this._getMeasurementString(); } return { text: text, subtext: subtext }; }, _getMeasurementString: function () { var area = this._area, measurementString = ''; if (!area && !this.options.showLength) { return null; } if (this.options.showLength) { measurementString = L.Draw.Polyline.prototype._getMeasurementString.call(this); } if (area) { measurementString += '<br>' + L.GeometryUtil.readableArea(area, this.options.metric, this.options.precision); } return measurementString; }, _shapeIsValid: function () { return this._markers.length >= 3; }, _vertexChanged: function (latlng, added) { var latLngs; // Check to see if we should show the area if (!this.options.allowIntersection && this.options.showArea) { latLngs = this._poly.getLatLngs(); this._area = L.GeometryUtil.geodesicArea(latLngs); } L.Draw.Polyline.prototype._vertexChanged.call(this, latlng, added); }, _cleanUpShape: function () { var markerCount = this._markers.length; if (markerCount > 0) { this._markers[0].off('click', this._finishShape, this); if (markerCount > 2) { this._markers[markerCount - 1].off('dblclick', this._finishShape, this); } } } }); L.SimpleShape = {}; /** * @class L.Draw.SimpleShape * @aka Draw.SimpleShape * @inherits L.Draw.Feature */ L.Draw.SimpleShape = L.Draw.Feature.extend({ options: { repeatMode: false }, // @method initialize(): void initialize: function (map, options) { this._endLabelText = L.drawLocal.draw.handlers.simpleshape.tooltip.end; L.Draw.Feature.prototype.initialize.call(this, map, options); }, // @method addHooks(): void // Add listener hooks to this handler. addHooks: function () { L.Draw.Feature.prototype.addHooks.call(this); if (this._map) { this._mapDraggable = this._map.dragging.enabled(); if (this._mapDraggable) { this._map.dragging.disable(); } //TODO refactor: move cursor to styles this._container.style.cursor = 'crosshair'; this._tooltip.updateContent({text: this._initialLabelText}); this._map .on('mousedown', this._onMouseDown, this) .on('mousemove', this._onMouseMove, this) .on('touchstart', this._onMouseDown, this) .on('touchmove', this._onMouseMove, this); // we should prevent default, otherwise default behavior (scrolling) will fire, // and that will cause document.touchend to fire and will stop the drawing // (circle, rectangle) in touch mode. // (update): we have to send passive now to prevent scroll, because by default it is {passive: true} now, which means, // handler can't event.preventDefault // check the news https://developers.google.com/web/updates/2016/06/passive-event-listeners document.addEventListener('touchstart', L.DomEvent.preventDefault, {passive: false}); } }, // @method removeHooks(): void // Remove listener hooks from this handler. removeHooks: function () { L.Draw.Feature.prototype.removeHooks.call(this); if (this._map) { if (this._mapDraggable) { this._map.dragging.enable(); } //TODO refactor: move cursor to styles this._container.style.cursor = ''; this._map .off('mousedown', this._onMouseDown, this) .off('mousemove', this._onMouseMove, this) .off('touchstart', this._onMouseDown, this) .off('touchmove', this._onMouseMove, this); L.DomEvent.off(document, 'mouseup', this._onMouseUp, this); L.DomEvent.off(document, 'touchend', this._onMouseUp, this); document.removeEventListener('touchstart', L.DomEvent.preventDefault); // If the box element doesn't exist they must not have moved the mouse, so don't need to destroy/return if (this._shape) { this._map.removeLayer(this._shape); delete this._shape; } } this._isDrawing = false; }, _getTooltipText: function () { return { text: this._endLabelText }; }, _onMouseDown: function (e) { this._isDrawing = true; this._startLatLng = e.latlng; L.DomEvent .on(document, 'mouseup', this._onMouseUp, this) .on(document, 'touchend', this._onMouseUp, this) .preventDefault(e.originalEvent); }, _onMouseMove: function (e) { var latlng = e.latlng; this._tooltip.updatePosition(latlng); if (this._isDrawing) { this._tooltip.updateContent(this._getTooltipText()); this._drawShape(latlng); } }, _onMouseUp: function () { if (this._shape) { this._fireCreatedEvent(); } this.disable(); if (this.options.repeatMode) { this.enable(); } } }); /** * @class L.Draw.Rectangle * @aka Draw.Rectangle * @inherits L.Draw.SimpleShape */ L.Draw.Rectangle = L.Draw.SimpleShape.extend({ statics: { TYPE: 'rectangle' }, options: { shapeOptions: { stroke: true, color: '#3388ff', weight: 4, opacity: 0.5, fill: true, fillColor: null, //same as color by default fillOpacity: 0.2, clickable: true }, showArea: true, //Whether to show the area in the tooltip metric: true // Whether to use the metric measurement system or imperial }, // @method initialize(): void initialize: function (map, options) { // Save the type so super can fire, need to do this as cannot do this.TYPE :( this.type = L.Draw.Rectangle.TYPE; this._initialLabelText = L.drawLocal.draw.handlers.rectangle.tooltip.start; L.Draw.SimpleShape.prototype.initialize.call(this, map, options); }, // @method disable(): void disable: function () { if (!this._enabled) { return; } this._isCurrentlyTwoClickDrawing = false; L.Draw.SimpleShape.prototype.disable.call(this); }, _onMouseUp: function (e) { if (!this._shape && !this._isCurrentlyTwoClickDrawing) { this._isCurrentlyTwoClickDrawing = true; return; } // Make sure closing click is on map if (this._isCurrentlyTwoClickDrawing && !_hasAncestor(e.target, 'leaflet-pane')) { return; } L.Draw.SimpleShape.prototype._onMouseUp.call(this); }, _drawShape: function (latlng) { if (!this._shape) { this._shape = new L.Rectangle(new L.LatLngBounds(this._startLatLng, latlng), this.options.shapeOptions); this._map.addLayer(this._shape); } else { this._shape.setBounds(new L.LatLngBounds(this._startLatLng, latlng)); } }, _fireCreatedEvent: function () { var rectangle = new L.Rectangle(this._shape.getBounds(), this.options.shapeOptions); L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, rectangle); }, _getTooltipText: function () { var tooltipText = L.Draw.SimpleShape.prototype._getTooltipText.call(this), shape = this._shape, showArea = this.options.showArea, latLngs, area, subtext; if (shape) { latLngs = this._shape._defaultShape ? this._shape._defaultShape() : this._shape.getLatLngs(); area = L.GeometryUtil.geodesicArea(latLngs); subtext = showArea ? L.GeometryUtil.readableArea(area, this.options.metric) : ''; } return { text: tooltipText.text, subtext: subtext }; } }); function _hasAncestor(el, cls) { while ((el = el.parentElement) && !el.classList.contains(cls)) { ; } return el; } /** * @class L.Draw.Marker * @aka Draw.Marker * @inherits L.Draw.Feature */ L.Draw.Marker = L.Draw.Feature.extend({ statics: { TYPE: 'marker' }, options: { icon: new L.Icon.Default(), repeatMode: false, zIndexOffset: 2000 // This should be > than the highest z-index any markers }, // @method initialize(): void initialize: function (map, options) { // Save the type so super can fire, need to do this as cannot do this.TYPE :( this.type = L.Draw.Marker.TYPE; this._initialLabelText = L.drawLocal.draw.handlers.marker.tooltip.start; L.Draw.Feature.prototype.initialize.call(this, map, options); }, // @method addHooks(): void // Add listener hooks to this handler. addHooks: function () { L.Draw.Feature.prototype.addHooks.call(this); if (this._map) { this._tooltip.updateContent({text: this._initialLabelText}); // Same mouseMarker as in Draw.Polyline if (!this._mouseMarker) { this._mouseMarker = L.marker(this._map.getCenter(), { icon: L.divIcon({ className: 'leaflet-mouse-marker', iconAnchor: [20, 20], iconSize: [40, 40] }), opacity: 0, zIndexOffset: this.options.zIndexOffset }); } this._mouseMarker .on('click', this._onClick, this) .addTo(this._map); this._map.on('mousemove', this._onMouseMove, this); this._map.on('click', this._onTouch, this); } }, // @method removeHooks(): void // Remove listener hooks from this handler. removeHooks: function () { L.Draw.Feature.prototype.removeHooks.call(this); if (this._map) { this._map .off('click', this._onClick, this) .off('click', this._onTouch, this); if (this._marker) { this._marker.off('click', this._onClick, this); this._map .removeLayer(this._marker); delete this._marker; } this._mouseMarker.off('click', this._onClick, this); this._map.removeLayer(this._mouseMarker); delete this._mouseMarker; this._map.off('mousemove', this._onMouseMove, this); } }, _onMouseMove: function (e) { var latlng = e.latlng; this._tooltip.updatePosition(latlng); this._mouseMarker.setLatLng(latlng); if (!this._marker) { this._marker = this._createMarker(latlng); // Bind to both marker and map to make sure we get the click event. this._marker.on('click', this._onClick, this); this._map .on('click', this._onClick, this) .addLayer(this._marker); } else { latlng = this._mouseMarker.getLatLng(); this._marker.setLatLng(latlng); } }, _createMarker: function (latlng) { return new L.Marker(latlng, { icon: this.options.icon, zIndexOffset: this.options.zIndexOffset }); }, _onClick: function () { this._fireCreatedEvent(); this.disable(); if (this.options.repeatMode) { this.enable(); } }, _onTouch: function (e) { // called on click & tap, only really does any thing on tap this._onMouseMove(e); // creates & places marker this._onClick(); // permanently places marker & ends interaction }, _fireCreatedEvent: function () { var marker = new L.Marker.Touch(this._marker.getLatLng(), {icon: this.options.icon}); L.Draw.Feature.prototype._fireCreatedEvent.call(this, marker); } }); /** * @class L.Draw.CircleMarker * @aka Draw.CircleMarker * @inherits L.Draw.Marker */ L.Draw.CircleMarker = L.Draw.Marker.extend({ statics: { TYPE: 'circlemarker' }, options: { stroke: true, color: '#3388ff', weight: 4, opacity: 0.5, fill: true, fillColor: null, //same as color by default fillOpacity: 0.2, clickable: true, zIndexOffset: 2000 // This should be > than the highest z-index any markers }, // @method initialize(): void initialize: function (map, options) { // Save the type so super can fire, need to do this as cannot do this.TYPE :( this.type = L.Draw.CircleMarker.TYPE; this._initialLabelText = L.drawLocal.draw.handlers.circlemarker.tooltip.start; L.Draw.Feature.prototype.initialize.call(this, map, options); }, _fireCreatedEvent: function () { var circleMarker = new L.CircleMarker(this._marker.getLatLng(), this.options); L.Draw.Feature.prototype._fireCreatedEvent.call(this, circleMarker); }, _createMarker: function (latlng) { return new L.CircleMarker(latlng, this.options); } }); /** * @class L.Draw.Circle * @aka Draw.Circle * @inherits L.Draw.SimpleShape */ L.Draw.Circle = L.Draw.SimpleShape.extend({ statics: { TYPE: 'circle' }, options: { shapeOptions: { stroke: true, color: '#3388ff', weight: 4, opacity: 0.5, fill: true, fillColor: null, //same as color by default fillOpacity: 0.2, clickable: true }, showRadius: true, metric: true, // Whether to use the metric measurement system or imperial feet: true, // When not metric, use feet instead of yards for display nautic: false // When not metric, not feet use nautic mile for display }, // @method initialize(): void initialize: function (map, options) { // Save the type so super can fire, need to do this as cannot do this.TYPE :( this.type = L.Draw.Circle.TYPE; this._initialLabelText = L.drawLocal.draw.handlers.circle.tooltip.start; L.Draw.SimpleShape.prototype.initialize.call(this, map, options); }, _drawShape: function (latlng) { // Calculate the distance based on the version if (L.GeometryUtil.isVersion07x()) { var distance = this._startLatLng.distanceTo(latlng); } else { var distance = this._map.distance(this._startLatLng, latlng); } if (!this._shape) { this._shape = new L.Circle(this._startLatLng, distance, this.options.shapeOptions); this._map.addLayer(this._shape); } else { this._shape.setRadius(distance); } }, _fireCreatedEvent: function () { var circle = new L.Circle(this._startLatLng, this._shape.getRadius(), this.options.shapeOptions); L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, circle); }, _onMouseMove: function (e) { var latlng = e.latlng, showRadius = this.options.showRadius, useMetric = this.options.metric, radius; this._tooltip.updatePosition(latlng); if (this._isDrawing) { this._drawShape(latlng); // Get the new radius (rounded to 1 dp) radius = this._shape.getRadius().toFixed(1); var subtext = ''; if (showRadius) { subtext = L.drawLocal.draw.handlers.circle.radius + ': ' + L.GeometryUtil.readableDistance(radius, useMetric, this.options.feet, this.options.nautic); } this._tooltip.updateContent({ text: this._endLabelText, subtext: subtext }); } } }); L.Edit = L.Edit || {}; /** * @class L.Edit.Marker * @aka Edit.Marker */ L.Edit.Marker = L.Handler.extend({ // @method initialize(): void initialize: function (marker, options) { this._marker = marker; L.setOptions(this, options); }, // @method addHooks(): void // Add listener hooks to this handler addHooks: function () { var marker = this._marker; marker.dragging.enable(); marker.on('dragend', this._onDragEnd, marker); this._toggleMarkerHighlight(); }, // @method removeHooks(): void // Remove listener hooks from this handler removeHooks: function () { var marker = this._marker; marker.dragging.disable(); marker.off('dragend', this._onDragEnd, marker); this._toggleMarkerHighlight(); }, _onDragEnd: function (e) { var layer = e.target; layer.edited = true; this._map.fire(L.Draw.Event.EDITMOVE, {layer: layer}); }, _toggleMarkerHighlight: function () { var icon = this._marker._icon; // Don't do anything if this layer is a marker but doesn't have an icon. Markers // should usually have icons. If using Leaflet.draw with Leaflet.markercluster there // is a chance that a marker doesn't. if (!icon) { return; } // This is quite naughty, but I don't see another way of doing it. (short of setting a new icon) icon.style.display = 'none'; if (L.DomUtil.hasClass(icon, 'leaflet-edit-marker-selected')) { L.DomUtil.removeClass(icon, 'leaflet-edit-marker-selected'); // Offset as the border will make the icon move. this._offsetMarker(icon, -4); } else { L.DomUtil.addClass(icon, 'leaflet-edit-marker-selected'); // Offset as the border will make the icon move. this._offsetMarker(icon, 4); } icon.style.display = ''; }, _offsetMarker: function (icon, offset) { var iconMarginTop = parseInt(icon.style.marginTop, 10) - offset, iconMarginLeft = parseInt(icon.style.marginLeft, 10) - offset; icon.style.marginTop = iconMarginTop + 'px'; icon.style.marginLeft = iconMarginLeft + 'px'; } }); L.Marker.addInitHook(function () { if (L.Edit.Marker) { this.editing = new L.Edit.Marker(this); if (this.options.editable) { this.editing.enable(); } } }); L.Edit = L.Edit || {}; /** * @class L.Edit.Polyline * @aka L.Edit.Poly * @aka Edit.Poly */ L.Edit.Poly = L.Handler.extend({ // @method initialize(): void initialize: function (poly) { this.latlngs = [poly._latlngs]; if (poly._holes) { this.latlngs = this.latlngs.concat(poly._holes); } this._poly = poly; this._poly.on('revert-edited', this._updateLatLngs, this); }, // Compatibility method to normalize Poly* objects // between 0.7.x and 1.0+ _defaultShape: function () { if (!L.Polyline._flat) { return this._poly._latlngs; } return L.Polyline._flat(this._poly._latlngs) ? this._poly._latlngs : this._poly._latlngs[0]; }, _eachVertexHandler: function (callback) { for (var i = 0; i < this._verticesHandlers.length; i++) { callback(this._verticesHandlers[i]); } }, // @method addHooks(): void // Add listener hooks to this handler addHooks: function () { this._initHandlers(); this._eachVertexHandler(function (handler) { handler.addHooks(); }); }, // @method removeHooks(): void // Remove listener hooks from this handler removeHooks: function () { this._eachVertexHandler(function (handler) { handler.removeHooks(); }); }, // @method updateMarkers(): void // Fire an update for each vertex handler updateMarkers: function () { this._eachVertexHandler(function (handler) { handler.updateMarkers(); }); }, _initHandlers: function () { this._verticesHandlers = []; for (var i = 0; i < this.latlngs.length; i++) { this._verticesHandlers.push(new L.Edit.PolyVerticesEdit(this._poly, this.latlngs[i], this._poly.options.poly)); } }, _updateLatLngs: function (e) { this.latlngs = [e.layer._latlngs]; if (e.layer._holes) { this.latlngs = this.latlngs.concat(e.layer._holes); } } }); /** * @class L.Edit.PolyVerticesEdit * @aka Edit.PolyVerticesEdit */ L.Edit.PolyVerticesEdit = L.Handler.extend({ options: { icon: new L.DivIcon({ iconSize: new L.Point(8, 8), className: 'leaflet-div-icon leaflet-editing-icon' }), touchIcon: new L.DivIcon({ iconSize: new L.Point(20, 20), className: 'leaflet-div-icon leaflet-editing-icon leaflet-touch-icon' }), drawError: { color: '#b00b00', timeout: 1000 } }, // @method intialize(): void initialize: function (poly, latlngs, options) { // if touch, switch to touch icon if (L.Browser.touch) { this.options.icon = this.options.touchIcon; } this._poly = poly; if (options && options.drawError) { options.drawError = L.Util.extend({}, this.options.drawError, options.drawError); } this._latlngs = latlngs; L.setOptions(this, options); }, // Compatibility method to normalize Poly* objects // between 0.7.x and 1.0+ _defaultShape: function () { if (!L.Polyline._flat) { return this._latlngs; } return