/* Leaflet.draw, a plugin that adds drawing and editing tools to Leaflet powered maps. (c) 2012-2013, Jacob Toye, Smartrak https://github.com/Leaflet/Leaflet.draw http://leafletjs.com https://github.com/jacobtoye */ (function (window, document, undefined) {/* * Leaflet.draw assumes that you have already included the Leaflet library. */ L.drawVersion = '0.2.3'; L.drawLocal = { draw: { toolbar: { actions: { title: 'Cancel drawing', text: 'Cancel' }, 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' } }, handlers: { circle: { tooltip: { start: 'Click and drag to draw circle.' } }, 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: 'Error: 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' } }, buttons: { edit: 'Edit layers.', editDisabled: 'No layers to edit.', remove: 'Delete layers.', removeDisabled: 'No layers to delete.' } }, handlers: { edit: { tooltip: { text: 'Drag handles, or marker to edit feature.', subtext: 'Click cancel to undo changes.' } }, remove: { tooltip: { text: 'Click on a feature to remove' } } } } }; L.Draw = {}; L.Draw.Feature = L.Handler.extend({ includes: L.Mixin.Events, 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); }, enable: function () { if (this._enabled) { return; } this.fire('enabled', { handler: this.type }); this._map.fire('draw:drawstart', { layerType: this.type }); L.Handler.prototype.enable.call(this); }, disable: function () { if (!this._enabled) { return; } L.Handler.prototype.disable.call(this); this._map.fire('draw:drawstop', { layerType: this.type }); this.fire('disabled', { handler: this.type }); }, addHooks: function () { var map = this._map; if (map) { L.DomUtil.disableTextSelection(); map.getContainer().focus(); this._tooltip = new L.Tooltip(this._map); L.DomEvent.on(this._container, 'keyup', this._cancelDrawing, this); } }, removeHooks: function () { if (this._map) { L.DomUtil.enableTextSelection(); this._tooltip.dispose(); this._tooltip = null; L.DomEvent.off(this._container, 'keyup', this._cancelDrawing, this); } }, setOptions: function (options) { L.setOptions(this, options); }, _fireCreatedEvent: function (layer) { this._map.fire('draw:created', { layer: layer, layerType: this.type }); }, // Cancel drawing when the escape key is pressed _cancelDrawing: function (e) { if (e.keyCode === 27) { this.disable(); } } }); 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' }), guidelineDistance: 20, maxGuideLineLength: 4000, shapeOptions: { stroke: true, color: '#f06eaa', weight: 4, opacity: 0.5, fill: false, clickable: true }, metric: true, // Whether to use the metric meaurement system or imperial showLength: true, // Whether to display distance in the tooltip zIndexOffset: 2000 // This should be > than the highest z-index any map layers }, initialize: function (map, options) { // 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); }, 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('mousedown', this._onMouseDown, this) .addTo(this._map); this._map .on('mousemove', this._onMouseMove, this) .on('mouseup', this._onMouseUp, this) .on('zoomend', this._onZoomEnd, this); } }, 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('mouseup', this._onMouseUp, this); this._map.removeLayer(this._mouseMarker); delete this._mouseMarker; // clean up DOM this._clearGuides(); this._map .off('mousemove', this._onMouseMove, this) .off('zoomend', this._onZoomEnd, this); }, deleteLastVertex: function () { if (this._markers.length <= 1) { return; } var lastMarker = this._markers.pop(), poly = this._poly, latlng = this._poly.spliceLatLngs(poly.getLatLngs().length - 1, 1)[0]; this._markerGroup.removeLayer(lastMarker); if (poly.getLatLngs().length < 2) { this._map.removeLayer(poly); } this._vertexChanged(latlng, false); }, addVertex: function (latlng) { var markersLength = this._markers.length; if (markersLength > 0 && !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); }, _finishShape: function () { var intersects = this._poly.newLatLngIntersects(this._poly.getLatLngs()[0], true); 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 () { this._updateGuide(); }, _onMouseMove: function (e) { var newPos = e.layerPoint, latlng = e.latlng; // 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._updateFinishHandler(); this._updateRunningMeasure(latlng, added); this._clearGuides(); this._updateTooltip(); }, _onMouseDown: function (e) { var originalEvent = e.originalEvent; this._mouseDownOrigin = L.point(originalEvent.clientX, originalEvent.clientY); }, _onMouseUp: function (e) { if (this._mouseDownOrigin) { // We detect clicks within a certain tolerance, otherwise let it // be interpreted as a drag by the map var distance = L.point(e.originalEvent.clientX, e.originalEvent.clientY) .distanceTo(this._mouseDownOrigin); if (Math.abs(distance) < 9 * (window.devicePixelRatio || 1)) { this.addVertex(e.latlng); } } this._mouseDownOrigin = null; }, _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.length; 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); distance = latlng.distanceTo(this._markers[previousMarkerIndex].getLatLng()); 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 distance = this._measurementRunningTotal + currentLatLng.distanceTo(previousLatLng); return L.GeometryUtil.readableDistance(distance, this.options.metric); }, _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; } }, _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); } }); L.Draw.Polygon = L.Draw.Polyline.extend({ statics: { TYPE: 'polygon' }, Poly: L.Polygon, options: { showArea: false, shapeOptions: { stroke: true, color: '#f06eaa', weight: 4, opacity: 0.5, fill: true, fillColor: null, //same as color by default fillOpacity: 0.2, clickable: true } }, 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; } else { text = L.drawLocal.draw.handlers.polygon.tooltip.end; subtext = this._getMeasurementString(); } return { text: text, subtext: subtext }; }, _getMeasurementString: function () { var area = this._area; if (!area) { return null; } return L.GeometryUtil.readableArea(area, this.options.metric); }, _shapeIsValid: function () { return this._markers.length >= 3; }, _vertexAdded: function () { // Check to see if we should show the area if (this.options.allowIntersection || !this.options.showArea) { return; } var latLngs = this._poly.getLatLngs(); this._area = L.GeometryUtil.geodesicArea(latLngs); }, _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 = {}; L.Draw.SimpleShape = L.Draw.Feature.extend({ options: { repeatMode: false }, initialize: function (map, options) { this._endLabelText = L.drawLocal.draw.handlers.simpleshape.tooltip.end; L.Draw.Feature.prototype.initialize.call(this, map, options); }, 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); } }, 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); L.DomEvent.off(document, 'mouseup', this._onMouseUp, this); // 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; }, _onMouseDown: function (e) { this._isDrawing = true; this._startLatLng = e.latlng; L.DomEvent .on(document, 'mouseup', this._onMouseUp, this) .preventDefault(e.originalEvent); }, _onMouseMove: function (e) { var latlng = e.latlng; this._tooltip.updatePosition(latlng); if (this._isDrawing) { this._tooltip.updateContent({ text: this._endLabelText }); this._drawShape(latlng); } }, _onMouseUp: function () { if (this._shape) { this._fireCreatedEvent(); } this.disable(); if (this.options.repeatMode) { this.enable(); } } }); L.Draw.Rectangle = L.Draw.SimpleShape.extend({ statics: { TYPE: 'rectangle' }, options: { shapeOptions: { stroke: true, color: '#f06eaa', weight: 4, opacity: 0.5, fill: true, fillColor: null, //same as color by default fillOpacity: 0.2, clickable: true } }, 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); }, _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); } }); L.Draw.Circle = L.Draw.SimpleShape.extend({ statics: { TYPE: 'circle' }, options: { shapeOptions: { stroke: true, color: '#f06eaa', 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 meaurement system or imperial }, 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) { if (!this._shape) { this._shape = new L.Circle(this._startLatLng, this._startLatLng.distanceTo(latlng), this.options.shapeOptions); this._map.addLayer(this._shape); } else { this._shape.setRadius(this._startLatLng.distanceTo(latlng)); } }, _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); this._tooltip.updateContent({ text: this._endLabelText, subtext: showRadius ? 'Radius: ' + L.GeometryUtil.readableDistance(radius, useMetric) : '' }); } } }); 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 }, 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; L.Draw.Feature.prototype.initialize.call(this, map, options); }, addHooks: function () { L.Draw.Feature.prototype.addHooks.call(this); if (this._map) { this._tooltip.updateContent({ text: L.drawLocal.draw.handlers.marker.tooltip.start }); // 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); } }, removeHooks: function () { L.Draw.Feature.prototype.removeHooks.call(this); if (this._map) { if (this._marker) { this._marker.off('click', this._onClick, this); this._map .off('click', this._onClick, this) .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 = new L.Marker(latlng, { icon: this.options.icon, zIndexOffset: this.options.zIndexOffset }); // 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); } }, _onClick: function () { this._fireCreatedEvent(); this.disable(); if (this.options.repeatMode) { this.enable(); } }, _fireCreatedEvent: function () { var marker = new L.Marker(this._marker.getLatLng(), { icon: this.options.icon }); L.Draw.Feature.prototype._fireCreatedEvent.call(this, marker); } }); L.Edit = L.Edit || {}; /* * L.Edit.Poly is an editing handler for polylines and polygons. */ L.Edit.Poly = L.Handler.extend({ options: { icon: new L.DivIcon({ iconSize: new L.Point(8, 8), className: 'leaflet-div-icon leaflet-editing-icon' }) }, initialize: function (poly, options) { this._poly = poly; L.setOptions(this, options); }, addHooks: function () { if (this._poly._map) { if (!this._markerGroup) { this._initMarkers(); } this._poly._map.addLayer(this._markerGroup); } }, removeHooks: function () { if (this._poly._map) { this._poly._map.removeLayer(this._markerGroup); delete this._markerGroup; delete this._markers; } }, updateMarkers: function () { this._markerGroup.clearLayers(); this._initMarkers(); }, _initMarkers: function () { if (!this._markerGroup) { this._markerGroup = new L.LayerGroup(); } this._markers = []; var latlngs = this._poly._latlngs, i, j, len, marker; // TODO refactor holes implementation in Polygon to support it here for (i = 0, len = latlngs.length; i < len; i++) { marker = this._createMarker(latlngs[i], i); marker.on('click', this._onMarkerClick, this); this._markers.push(marker); } var markerLeft, markerRight; for (i = 0, j = len - 1; i < len; j = i++) { if (i === 0 && !(L.Polygon && (this._poly instanceof L.Polygon))) { continue; } markerLeft = this._markers[j]; markerRight = this._markers[i]; this._createMiddleMarker(markerLeft, markerRight); this._updatePrevNext(markerLeft, markerRight); } }, _createMarker: function (latlng, index) { var marker = new L.Marker(latlng, { draggable: true, icon: this.options.icon }); marker._origLatLng = latlng; marker._index = index; marker.on('drag', this._onMarkerDrag, this); marker.on('dragend', this._fireEdit, this); this._markerGroup.addLayer(marker); return marker; }, _removeMarker: function (marker) { var i = marker._index; this._markerGroup.removeLayer(marker); this._markers.splice(i, 1); this._poly.spliceLatLngs(i, 1); this._updateIndexes(i, -1); marker .off('drag', this._onMarkerDrag, this) .off('dragend', this._fireEdit, this) .off('click', this._onMarkerClick, this); }, _fireEdit: function () { this._poly.edited = true; this._poly.fire('edit'); }, _onMarkerDrag: function (e) { var marker = e.target; L.extend(marker._origLatLng, marker._latlng); if (marker._middleLeft) { marker._middleLeft.setLatLng(this._getMiddleLatLng(marker._prev, marker)); } if (marker._middleRight) { marker._middleRight.setLatLng(this._getMiddleLatLng(marker, marker._next)); } this._poly.redraw(); }, _onMarkerClick: function (e) { var minPoints = L.Polygon && (this._poly instanceof L.Polygon) ? 4 : 3, marker = e.target; // If removing this point would create an invalid polyline/polygon don't remove if (this._poly._latlngs.length < minPoints) { return; } // remove the marker this._removeMarker(marker); // update prev/next links of adjacent markers this._updatePrevNext(marker._prev, marker._next); // remove ghost markers near the removed marker if (marker._middleLeft) { this._markerGroup.removeLayer(marker._middleLeft); } if (marker._middleRight) { this._markerGroup.removeLayer(marker._middleRight); } // create a ghost marker in place of the removed one if (marker._prev && marker._next) { this._createMiddleMarker(marker._prev, marker._next); } else if (!marker._prev) { marker._next._middleLeft = null; } else if (!marker._next) { marker._prev._middleRight = null; } this._fireEdit(); }, _updateIndexes: function (index, delta) { this._markerGroup.eachLayer(function (marker) { if (marker._index > index) { marker._index += delta; } }); }, _createMiddleMarker: function (marker1, marker2) { var latlng = this._getMiddleLatLng(marker1, marker2), marker = this._createMarker(latlng), onClick, onDragStart, onDragEnd; marker.setOpacity(0.6); marker1._middleRight = marker2._middleLeft = marker; onDragStart = function () { var i = marker2._index; marker._index = i; marker .off('click', onClick, this) .on('click', this._onMarkerClick, this); latlng.lat = marker.getLatLng().lat; latlng.lng = marker.getLatLng().lng; this._poly.spliceLatLngs(i, 0, latlng); this._markers.splice(i, 0, marker); marker.setOpacity(1); this._updateIndexes(i, 1); marker2._index++; this._updatePrevNext(marker1, marker); this._updatePrevNext(marker, marker2); this._poly.fire('editstart'); }; onDragEnd = function () { marker.off('dragstart', onDragStart, this); marker.off('dragend', onDragEnd, this); this._createMiddleMarker(marker1, marker); this._createMiddleMarker(marker, marker2); }; onClick = function () { onDragStart.call(this); onDragEnd.call(this); this._fireEdit(); }; marker .on('click', onClick, this) .on('dragstart', onDragStart, this) .on('dragend', onDragEnd, this); this._markerGroup.addLayer(marker); }, _updatePrevNext: function (marker1, marker2) { if (marker1) { marker1._next = marker2; } if (marker2) { marker2._prev = marker1; } }, _getMiddleLatLng: function (marker1, marker2) { var map = this._poly._map, p1 = map.project(marker1.getLatLng()), p2 = map.project(marker2.getLatLng()); return map.unproject(p1._add(p2)._divideBy(2)); } }); L.Polyline.addInitHook(function () { // Check to see if handler has already been initialized. This is to support versions of Leaflet that still have L.Handler.PolyEdit if (this.editing) { return; } if (L.Edit.Poly) { this.editing = new L.Edit.Poly(this); if (this.options.editable) { this.editing.enable(); } } this.on('add', function () { if (this.editing && this.editing.enabled()) { this.editing.addHooks(); } }); this.on('remove', function () { if (this.editing && this.editing.enabled()) { this.editing.removeHooks(); } }); }); L.Edit = L.Edit || {}; L.Edit.SimpleShape = L.Handler.extend({ options: { moveIcon: new L.DivIcon({ iconSize: new L.Point(8, 8), className: 'leaflet-div-icon leaflet-editing-icon leaflet-edit-move' }), resizeIcon: new L.DivIcon({ iconSize: new L.Point(8, 8), className: 'leaflet-div-icon leaflet-editing-icon leaflet-edit-resize' }) }, initialize: function (shape, options) { this._shape = shape; L.Util.setOptions(this, options); }, addHooks: function () { if (this._shape._map) { this._map = this._shape._map; if (!this._markerGroup) { this._initMarkers(); } this._map.addLayer(this._markerGroup); } }, removeHooks: function () { if (this._shape._map) { this._unbindMarker(this._moveMarker); for (var i = 0, l = this._resizeMarkers.length; i < l; i++) { this._unbindMarker(this._resizeMarkers[i]); } this._resizeMarkers = null; this._map.removeLayer(this._markerGroup); delete this._markerGroup; } this._map = null; }, updateMarkers: function () { this._markerGroup.clearLayers(); this._initMarkers(); }, _initMarkers: function () { if (!this._markerGroup) { this._markerGroup = new L.LayerGroup(); } // Create center marker this._createMoveMarker(); // Create edge marker this._createResizeMarker(); }, _createMoveMarker: function () { // Children override }, _createResizeMarker: function () { // Children override }, _createMarker: function (latlng, icon) { var marker = new L.Marker(latlng, { draggable: true, icon: icon, zIndexOffset: 10 }); this._bindMarker(marker); this._markerGroup.addLayer(marker); return marker; }, _bindMarker: function (marker) { marker .on('dragstart', this._onMarkerDragStart, this) .on('drag', this._onMarkerDrag, this) .on('dragend', this._onMarkerDragEnd, this); }, _unbindMarker: function (marker) { marker .off('dragstart', this._onMarkerDragStart, this) .off('drag', this._onMarkerDrag, this) .off('dragend', this._onMarkerDragEnd, this); }, _onMarkerDragStart: function (e) { var marker = e.target; marker.setOpacity(0); this._shape.fire('editstart'); }, _fireEdit: function () { this._shape.edited = true; this._shape.fire('edit'); }, _onMarkerDrag: function (e) { var marker = e.target, latlng = marker.getLatLng(); if (marker === this._moveMarker) { this._move(latlng); } else { this._resize(latlng); } this._shape.redraw(); }, _onMarkerDragEnd: function (e) { var marker = e.target; marker.setOpacity(1); this._fireEdit(); }, _move: function () { // Children override }, _resize: function () { // Children override } }); L.Edit = L.Edit || {}; L.Edit.Rectangle = L.Edit.SimpleShape.extend({ _createMoveMarker: function () { var bounds = this._shape.getBounds(), center = bounds.getCenter(); this._moveMarker = this._createMarker(center, this.options.moveIcon); }, _createResizeMarker: function () { var corners = this._getCorners(); this._resizeMarkers = []; for (var i = 0, l = corners.length; i < l; i++) { this._resizeMarkers.push(this._createMarker(corners[i], this.options.resizeIcon)); // Monkey in the corner index as we will need to know this for dragging this._resizeMarkers[i]._cornerIndex = i; } }, _onMarkerDragStart: function (e) { L.Edit.SimpleShape.prototype._onMarkerDragStart.call(this, e); // Save a reference to the opposite point var corners = this._getCorners(), marker = e.target, currentCornerIndex = marker._cornerIndex; this._oppositeCorner = corners[(currentCornerIndex + 2) % 4]; this._toggleCornerMarkers(0, currentCornerIndex); }, _onMarkerDragEnd: function (e) { var marker = e.target, bounds, center; // Reset move marker position to the center if (marker === this._moveMarker) { bounds = this._shape.getBounds(); center = bounds.getCenter(); marker.setLatLng(center); } this._toggleCornerMarkers(1); this._repositionCornerMarkers(); L.Edit.SimpleShape.prototype._onMarkerDragEnd.call(this, e); }, _move: function (newCenter) { var latlngs = this._shape.getLatLngs(), bounds = this._shape.getBounds(), center = bounds.getCenter(), offset, newLatLngs = []; // Offset the latlngs to the new center for (var i = 0, l = latlngs.length; i < l; i++) { offset = [latlngs[i].lat - center.lat, latlngs[i].lng - center.lng]; newLatLngs.push([newCenter.lat + offset[0], newCenter.lng + offset[1]]); } this._shape.setLatLngs(newLatLngs); // Reposition the resize markers this._repositionCornerMarkers(); }, _resize: function (latlng) { var bounds; // Update the shape based on the current position of this corner and the opposite point this._shape.setBounds(L.latLngBounds(latlng, this._oppositeCorner)); // Reposition the move marker bounds = this._shape.getBounds(); this._moveMarker.setLatLng(bounds.getCenter()); }, _getCorners: function () { var bounds = this._shape.getBounds(), nw = bounds.getNorthWest(), ne = bounds.getNorthEast(), se = bounds.getSouthEast(), sw = bounds.getSouthWest(); return [nw, ne, se, sw]; }, _toggleCornerMarkers: function (opacity) { for (var i = 0, l = this._resizeMarkers.length; i < l; i++) { this._resizeMarkers[i].setOpacity(opacity); } }, _repositionCornerMarkers: function () { var corners = this._getCorners(); for (var i = 0, l = this._resizeMarkers.length; i < l; i++) { this._resizeMarkers[i].setLatLng(corners[i]); } } }); L.Rectangle.addInitHook(function () { if (L.Edit.Rectangle) { this.editing = new L.Edit.Rectangle(this); if (this.options.editable) { this.editing.enable(); } } }); L.Edit = L.Edit || {}; L.Edit.Circle = L.Edit.SimpleShape.extend({ _createMoveMarker: function () { var center = this._shape.getLatLng(); this._moveMarker = this._createMarker(center, this.options.moveIcon); }, _createResizeMarker: function () { var center = this._shape.getLatLng(), resizemarkerPoint = this._getResizeMarkerPoint(center); this._resizeMarkers = []; this._resizeMarkers.push(this._createMarker(resizemarkerPoint, this.options.resizeIcon)); }, _getResizeMarkerPoint: function (latlng) { // From L.shape.getBounds() var delta = this._shape._radius * Math.cos(Math.PI / 4), point = this._map.project(latlng); return this._map.unproject([point.x + delta, point.y - delta]); }, _move: function (latlng) { var resizemarkerPoint = this._getResizeMarkerPoint(latlng); // Move the resize marker this._resizeMarkers[0].setLatLng(resizemarkerPoint); // Move the circle this._shape.setLatLng(latlng); }, _resize: function (latlng) { var moveLatLng = this._moveMarker.getLatLng(), radius = moveLatLng.distanceTo(latlng); this._shape.setRadius(radius); } }); L.Circle.addInitHook(function () { if (L.Edit.Circle) { this.editing = new L.Edit.Circle(this); if (this.options.editable) { this.editing.enable(); } } this.on('add', function () { if (this.editing && this.editing.enabled()) { this.editing.addHooks(); } }); this.on('remove', function () { if (this.editing && this.editing.enabled()) { this.editing.removeHooks(); } }); }); /* * L.LatLngUtil contains different utility functions for LatLngs. */ L.LatLngUtil = { // Clones a LatLngs[], returns [][] cloneLatLngs: function (latlngs) { var clone = []; for (var i = 0, l = latlngs.length; i < l; i++) { clone.push(this.cloneLatLng(latlngs[i])); } return clone; }, cloneLatLng: function (latlng) { return L.latLng(latlng.lat, latlng.lng); } }; L.GeometryUtil = L.extend(L.GeometryUtil || {}, { // Ported from the OpenLayers implementation. See https://github.com/openlayers/openlayers/blob/master/lib/OpenLayers/Geometry/LinearRing.js#L270 geodesicArea: function (latLngs) { var pointsCount = latLngs.length, area = 0.0, d2r = L.LatLng.DEG_TO_RAD, p1, p2; if (pointsCount > 2) { for (var i = 0; i < pointsCount; i++) { p1 = latLngs[i]; p2 = latLngs[(i + 1) % pointsCount]; area += ((p2.lng - p1.lng) * d2r) * (2 + Math.sin(p1.lat * d2r) + Math.sin(p2.lat * d2r)); } area = area * 6378137.0 * 6378137.0 / 2.0; } return Math.abs(area); }, readableArea: function (area, isMetric) { var areaStr; if (isMetric) { if (area >= 10000) { areaStr = (area * 0.0001).toFixed(2) + ' ha'; } else { areaStr = area.toFixed(2) + ' m²'; } } else { area *= 0.836127; // Square yards in 1 meter if (area >= 3097600) { //3097600 square yards in 1 square mile areaStr = (area / 3097600).toFixed(2) + ' mi²'; } else if (area >= 4840) {//48040 square yards in 1 acre areaStr = (area / 4840).toFixed(2) + ' acres'; } else { areaStr = Math.ceil(area) + ' yd²'; } } return areaStr; }, readableDistance: function (distance, isMetric) { var 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; } }); L.Util.extend(L.LineUtil, { // Checks to see if two line segments intersect. Does not handle degenerate cases. // http://compgeom.cs.uiuc.edu/~jeffe/teaching/373/notes/x06-sweepline.pdf segmentsIntersect: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2, /*Point*/ p3) { return this._checkCounterclockwise(p, p2, p3) !== this._checkCounterclockwise(p1, p2, p3) && this._checkCounterclockwise(p, p1, p2) !== this._checkCounterclockwise(p, p1, p3); }, // check to see if points are in counterclockwise order _checkCounterclockwise: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) { return (p2.y - p.y) * (p1.x - p.x) > (p1.y - p.y) * (p2.x - p.x); } }); L.Polyline.include({ // Check to see if this polyline has any linesegments that intersect. // NOTE: does not support detecting intersection for degenerate cases. intersects: function () { var points = this._originalPoints, len = points ? points.length : 0, i, p, p1; if (this._tooFewPointsForIntersection()) { return false; } for (i = len - 1; i >= 3; i--) { p = points[i - 1]; p1 = points[i]; if (this._lineSegmentsIntersectsRange(p, p1, i - 2)) { return true; } } return false; }, // Check for intersection if new latlng was added to this polyline. // NOTE: does not support detecting intersection for degenerate cases. newLatLngIntersects: function (latlng, skipFirst) { // Cannot check a polyline for intersecting lats/lngs when not added to the map if (!this._map) { return false; } return this.newPointIntersects(this._map.latLngToLayerPoint(latlng), skipFirst); }, // Check for intersection if new point was added to this polyline. // newPoint must be a layer point. // NOTE: does not support detecting intersection for degenerate cases. newPointIntersects: function (newPoint, skipFirst) { var points = this._originalPoints, len = points ? points.length : 0, lastPoint = points ? points[len - 1] : null, // The previous previous line segment. Previous line segment doesn't need testing. maxIndex = len - 2; if (this._tooFewPointsForIntersection(1)) { return false; } return this._lineSegmentsIntersectsRange(lastPoint, newPoint, maxIndex, skipFirst ? 1 : 0); }, // Polylines with 2 sides can only intersect in cases where points are collinear (we don't support detecting these). // Cannot have intersection when < 3 line segments (< 4 points) _tooFewPointsForIntersection: function (extraPoints) { var points = this._originalPoints, len = points ? points.length : 0; // Increment length by extraPoints if present len += extraPoints || 0; return !this._originalPoints || len <= 3; }, // Checks a line segment intersections with any line segments before its predecessor. // Don't need to check the predecessor as will never intersect. _lineSegmentsIntersectsRange: function (p, p1, maxIndex, minIndex) { var points = this._originalPoints, p2, p3; minIndex = minIndex || 0; // Check all previous line segments (beside the immediately previous) for intersections for (var j = maxIndex; j > minIndex; j--) { p2 = points[j - 1]; p3 = points[j]; if (L.LineUtil.segmentsIntersect(p, p1, p2, p3)) { return true; } } return false; } }); L.Polygon.include({ // Checks a polygon for any intersecting line segments. Ignores holes. intersects: function () { var polylineIntersects, points = this._originalPoints, len, firstPoint, lastPoint, maxIndex; if (this._tooFewPointsForIntersection()) { return false; } polylineIntersects = L.Polyline.prototype.intersects.call(this); // If already found an intersection don't need to check for any more. if (polylineIntersects) { return true; } len = points.length; firstPoint = points[0]; lastPoint = points[len - 1]; maxIndex = len - 2; // Check the line segment between last and first point. Don't need to check the first line segment (minIndex = 1) return this._lineSegmentsIntersectsRange(lastPoint, firstPoint, maxIndex, 1); } }); L.Control.Draw = L.Control.extend({ options: { position: 'topleft', draw: {}, edit: false }, initialize: function (options) { if (L.version < '0.7') { throw new Error('Leaflet.draw 0.2.3+ requires Leaflet 0.7.0+. Download latest from https://github.com/Leaflet/Leaflet/'); } L.Control.prototype.initialize.call(this, options); var id, toolbar; this._toolbars = {}; // Initialize toolbars if (L.DrawToolbar && this.options.draw) { toolbar = new L.DrawToolbar(this.options.draw); id = L.stamp(toolbar); this._toolbars[id] = toolbar; // Listen for when toolbar is enabled this._toolbars[id].on('enable', this._toolbarEnabled, this); } if (L.EditToolbar && this.options.edit) { toolbar = new L.EditToolbar(this.options.edit); id = L.stamp(toolbar); this._toolbars[id] = toolbar; // Listen for when toolbar is enabled this._toolbars[id].on('enable', this._toolbarEnabled, this); } }, onAdd: function (map) { var container = L.DomUtil.create('div', 'leaflet-draw'), addedTopClass = false, topClassName = 'leaflet-draw-toolbar-top', toolbarContainer; for (var toolbarId in this._toolbars) { if (this._toolbars.hasOwnProperty(toolbarId)) { toolbarContainer = this._toolbars[toolbarId].addToolbar(map); if (toolbarContainer) { // Add class to the first toolbar to remove the margin if (!addedTopClass) { if (!L.DomUtil.hasClass(toolbarContainer, topClassName)) { L.DomUtil.addClass(toolbarContainer.childNodes[0], topClassName); } addedTopClass = true; } container.appendChild(toolbarContainer); } } } return container; }, onRemove: function () { for (var toolbarId in this._toolbars) { if (this._toolbars.hasOwnProperty(toolbarId)) { this._toolbars[toolbarId].removeToolbar(); } } }, setDrawingOptions: function (options) { for (var toolbarId in this._toolbars) { if (this._toolbars[toolbarId] instanceof L.DrawToolbar) { this._toolbars[toolbarId].setOptions(options); } } }, _toolbarEnabled: function (e) { var id = '' + L.stamp(e.target); for (var toolbarId in this._toolbars) { if (this._toolbars.hasOwnProperty(toolbarId) && toolbarId !== id) { this._toolbars[toolbarId].disable(); } } } }); L.Map.mergeOptions({ drawControlTooltips: true, drawControl: false }); L.Map.addInitHook(function () { if (this.options.drawControl) { this.drawControl = new L.Control.Draw(); this.addControl(this.drawControl); } }); L.Toolbar = L.Class.extend({ includes: [L.Mixin.Events], initialize: function (options) { L.setOptions(this, options); this._modes = {}; this._actionButtons = []; this._activeMode = null; }, enabled: function () { return this._activeMode !== null; }, disable: function () { if (!this.enabled()) { return; } this._activeMode.handler.disable(); }, addToolbar: function (map) { var container = L.DomUtil.create('div', 'leaflet-draw-section'), buttonIndex = 0, buttonClassPrefix = this._toolbarClass || '', modeHandlers = this.getModeHandlers(map), i; this._toolbarContainer = L.DomUtil.create('div', 'leaflet-draw-toolbar leaflet-bar'); this._map = map; for (i = 0; i < modeHandlers.length; i++) { if (modeHandlers[i].enabled) { this._initModeHandler( modeHandlers[i].handler, this._toolbarContainer, buttonIndex++, buttonClassPrefix, modeHandlers[i].title ); } } // if no buttons were added, do not add the toolbar if (!buttonIndex) { return; } // Save button index of the last button, -1 as we would have ++ after the last button this._lastButtonIndex = --buttonIndex; // Create empty actions part of the toolbar this._actionsContainer = L.DomUtil.create('ul', 'leaflet-draw-actions'); // Add draw and cancel containers to the control container container.appendChild(this._toolbarContainer); container.appendChild(this._actionsContainer); return container; }, removeToolbar: function () { // Dispose each handler for (var handlerId in this._modes) { if (this._modes.hasOwnProperty(handlerId)) { // Unbind handler button this._disposeButton( this._modes[handlerId].button, this._modes[handlerId].handler.enable, this._modes[handlerId].handler ); // Make sure is disabled this._modes[handlerId].handler.disable(); // Unbind handler this._modes[handlerId].handler .off('enabled', this._handlerActivated, this) .off('disabled', this._handlerDeactivated, this); } } this._modes = {}; // Dispose the actions toolbar for (var i = 0, l = this._actionButtons.length; i < l; i++) { this._disposeButton( this._actionButtons[i].button, this._actionButtons[i].callback, this ); } this._actionButtons = []; this._actionsContainer = null; }, _initModeHandler: function (handler, container, buttonIndex, classNamePredix, buttonTitle) { var type = handler.type; this._modes[type] = {}; this._modes[type].handler = handler; this._modes[type].button = this._createButton({ title: buttonTitle, className: classNamePredix + '-' + type, container: container, callback: this._modes[type].handler.enable, context: this._modes[type].handler }); this._modes[type].buttonIndex = buttonIndex; this._modes[type].handler .on('enabled', this._handlerActivated, this) .on('disabled', this._handlerDeactivated, this); }, _createButton: function (options) { var link = L.DomUtil.create('a', options.className || '', options.container); link.href = '#'; if (options.text) { link.innerHTML = options.text; } if (options.title) { link.title = options.title; } L.DomEvent .on(link, 'click', L.DomEvent.stopPropagation) .on(link, 'mousedown', L.DomEvent.stopPropagation) .on(link, 'dblclick', L.DomEvent.stopPropagation) .on(link, 'click', L.DomEvent.preventDefault) .on(link, 'click', options.callback, options.context); return link; }, _disposeButton: function (button, callback) { L.DomEvent .off(button, 'click', L.DomEvent.stopPropagation) .off(button, 'mousedown', L.DomEvent.stopPropagation) .off(button, 'dblclick', L.DomEvent.stopPropagation) .off(button, 'click', L.DomEvent.preventDefault) .off(button, 'click', callback); }, _handlerActivated: function (e) { // Disable active mode (if present) this.disable(); // Cache new active feature this._activeMode = this._modes[e.handler]; L.DomUtil.addClass(this._activeMode.button, 'leaflet-draw-toolbar-button-enabled'); this._showActionsToolbar(); this.fire('enable'); }, _handlerDeactivated: function () { this._hideActionsToolbar(); L.DomUtil.removeClass(this._activeMode.button, 'leaflet-draw-toolbar-button-enabled'); this._activeMode = null; this.fire('disable'); }, _createActions: function (handler) { var container = this._actionsContainer, buttons = this.getActions(handler), l = buttons.length, li, di, dl, button; // Dispose the actions toolbar (todo: dispose only not used buttons) for (di = 0, dl = this._actionButtons.length; di < dl; di++) { this._disposeButton(this._actionButtons[di].button, this._actionButtons[di].callback); } this._actionButtons = []; // Remove all old buttons while (container.firstChild) { container.removeChild(container.firstChild); } for (var i = 0; i < l; i++) { if ('enabled' in buttons[i] && !buttons[i].enabled) { continue; } li = L.DomUtil.create('li', '', container); button = this._createButton({ title: buttons[i].title, text: buttons[i].text, container: li, callback: buttons[i].callback, context: buttons[i].context }); this._actionButtons.push({ button: button, callback: buttons[i].callback }); } }, _showActionsToolbar: function () { var buttonIndex = this._activeMode.buttonIndex, lastButtonIndex = this._lastButtonIndex, toolbarPosition = this._activeMode.button.offsetTop - 1; // Recreate action buttons on every click this._createActions(this._activeMode.handler); // Correctly position the cancel button this._actionsContainer.style.top = toolbarPosition + 'px'; if (buttonIndex === 0) { L.DomUtil.addClass(this._toolbarContainer, 'leaflet-draw-toolbar-notop'); L.DomUtil.addClass(this._actionsContainer, 'leaflet-draw-actions-top'); } if (buttonIndex === lastButtonIndex) { L.DomUtil.addClass(this._toolbarContainer, 'leaflet-draw-toolbar-nobottom'); L.DomUtil.addClass(this._actionsContainer, 'leaflet-draw-actions-bottom'); } this._actionsContainer.style.display = 'block'; }, _hideActionsToolbar: function () { this._actionsContainer.style.display = 'none'; L.DomUtil.removeClass(this._toolbarContainer, 'leaflet-draw-toolbar-notop'); L.DomUtil.removeClass(this._toolbarContainer, 'leaflet-draw-toolbar-nobottom'); L.DomUtil.removeClass(this._actionsContainer, 'leaflet-draw-actions-top'); L.DomUtil.removeClass(this._actionsContainer, 'leaflet-draw-actions-bottom'); } }); L.Tooltip = L.Class.extend({ initialize: function (map) { this._map = map; this._popupPane = map._panes.popupPane; this._container = map.options.drawControlTooltips ? L.DomUtil.create('div', 'leaflet-draw-tooltip', this._popupPane) : null; this._singleLineLabel = false; }, dispose: function () { if (this._container) { this._popupPane.removeChild(this._container); this._container = null; } }, updateContent: function (labelText) { if (!this._container) { return this; } labelText.subtext = labelText.subtext || ''; // update the vertical position (only if changed) if (labelText.subtext.length === 0 && !this._singleLineLabel) { L.DomUtil.addClass(this._container, 'leaflet-draw-tooltip-single'); this._singleLineLabel = true; } else if (labelText.subtext.length > 0 && this._singleLineLabel) { L.DomUtil.removeClass(this._container, 'leaflet-draw-tooltip-single'); this._singleLineLabel = false; } this._container.innerHTML = (labelText.subtext.length > 0 ? '' + labelText.subtext + '' + '
' : '') + '' + labelText.text + ''; return this; }, updatePosition: function (latlng) { var pos = this._map.latLngToLayerPoint(latlng), tooltipContainer = this._container; if (this._container) { tooltipContainer.style.visibility = 'inherit'; L.DomUtil.setPosition(tooltipContainer, pos); } return this; }, showAsError: function () { if (this._container) { L.DomUtil.addClass(this._container, 'leaflet-error-draw-tooltip'); } return this; }, removeError: function () { if (this._container) { L.DomUtil.removeClass(this._container, 'leaflet-error-draw-tooltip'); } return this; } }); L.DrawToolbar = L.Toolbar.extend({ options: { polyline: {}, polygon: {}, rectangle: {}, circle: {}, marker: {} }, initialize: function (options) { // Ensure that the options are merged correctly since L.extend is only shallow for (var type in this.options) { if (this.options.hasOwnProperty(type)) { if (options[type]) { options[type] = L.extend({}, this.options[type], options[type]); } } } this._toolbarClass = 'leaflet-draw-draw'; L.Toolbar.prototype.initialize.call(this, options); }, getModeHandlers: function (map) { return [ { enabled: this.options.polyline, handler: new L.Draw.Polyline(map, this.options.polyline), title: L.drawLocal.draw.toolbar.buttons.polyline }, { enabled: this.options.polygon, handler: new L.Draw.Polygon(map, this.options.polygon), title: L.drawLocal.draw.toolbar.buttons.polygon }, { enabled: this.options.rectangle, handler: new L.Draw.Rectangle(map, this.options.rectangle), title: L.drawLocal.draw.toolbar.buttons.rectangle }, { enabled: this.options.circle, handler: new L.Draw.Circle(map, this.options.circle), title: L.drawLocal.draw.toolbar.buttons.circle }, { enabled: this.options.marker, handler: new L.Draw.Marker(map, this.options.marker), title: L.drawLocal.draw.toolbar.buttons.marker } ]; }, // Get the actions part of the toolbar getActions: function (handler) { return [ { enabled: handler.deleteLastVertex, title: L.drawLocal.draw.toolbar.undo.title, text: L.drawLocal.draw.toolbar.undo.text, callback: handler.deleteLastVertex, context: handler }, { title: L.drawLocal.draw.toolbar.actions.title, text: L.drawLocal.draw.toolbar.actions.text, callback: this.disable, context: this } ]; }, setOptions: function (options) { L.setOptions(this, options); for (var type in this._modes) { if (this._modes.hasOwnProperty(type) && options.hasOwnProperty(type)) { this._modes[type].handler.setOptions(options[type]); } } } }); /*L.Map.mergeOptions({ editControl: true });*/ L.EditToolbar = L.Toolbar.extend({ options: { edit: { selectedPathOptions: { color: '#fe57a1', /* Hot pink all the things! */ opacity: 0.6, dashArray: '10, 10', fill: true, fillColor: '#fe57a1', fillOpacity: 0.1 } }, remove: {}, featureGroup: null /* REQUIRED! TODO: perhaps if not set then all layers on the map are selectable? */ }, initialize: function (options) { // Need to set this manually since null is an acceptable value here if (options.edit) { if (typeof options.edit.selectedPathOptions === 'undefined') { options.edit.selectedPathOptions = this.options.edit.selectedPathOptions; } options.edit = L.extend({}, this.options.edit, options.edit); } if (options.remove) { options.remove = L.extend({}, this.options.remove, options.remove); } this._toolbarClass = 'leaflet-draw-edit'; L.Toolbar.prototype.initialize.call(this, options); this._selectedFeatureCount = 0; }, getModeHandlers: function (map) { var featureGroup = this.options.featureGroup; return [ { enabled: this.options.edit, handler: new L.EditToolbar.Edit(map, { featureGroup: featureGroup, selectedPathOptions: this.options.edit.selectedPathOptions }), title: L.drawLocal.edit.toolbar.buttons.edit }, { enabled: this.options.remove, handler: new L.EditToolbar.Delete(map, { featureGroup: featureGroup }), title: L.drawLocal.edit.toolbar.buttons.remove } ]; }, getActions: function () { return [ { title: L.drawLocal.edit.toolbar.actions.save.title, text: L.drawLocal.edit.toolbar.actions.save.text, callback: this._save, context: this }, { title: L.drawLocal.edit.toolbar.actions.cancel.title, text: L.drawLocal.edit.toolbar.actions.cancel.text, callback: this.disable, context: this } ]; }, addToolbar: function (map) { var container = L.Toolbar.prototype.addToolbar.call(this, map); this._checkDisabled(); this.options.featureGroup.on('layeradd layerremove', this._checkDisabled, this); return container; }, removeToolbar: function () { this.options.featureGroup.off('layeradd layerremove', this._checkDisabled, this); L.Toolbar.prototype.removeToolbar.call(this); }, disable: function () { if (!this.enabled()) { return; } this._activeMode.handler.revertLayers(); L.Toolbar.prototype.disable.call(this); }, _save: function () { this._activeMode.handler.save(); this._activeMode.handler.disable(); }, _checkDisabled: function () { var featureGroup = this.options.featureGroup, hasLayers = featureGroup.getLayers().length !== 0, button; if (this.options.edit) { button = this._modes[L.EditToolbar.Edit.TYPE].button; if (hasLayers) { L.DomUtil.removeClass(button, 'leaflet-disabled'); } else { L.DomUtil.addClass(button, 'leaflet-disabled'); } button.setAttribute( 'title', hasLayers ? L.drawLocal.edit.toolbar.buttons.edit : L.drawLocal.edit.toolbar.buttons.editDisabled ); } if (this.options.remove) { button = this._modes[L.EditToolbar.Delete.TYPE].button; if (hasLayers) { L.DomUtil.removeClass(button, 'leaflet-disabled'); } else { L.DomUtil.addClass(button, 'leaflet-disabled'); } button.setAttribute( 'title', hasLayers ? L.drawLocal.edit.toolbar.buttons.remove : L.drawLocal.edit.toolbar.buttons.removeDisabled ); } } }); L.EditToolbar.Edit = L.Handler.extend({ statics: { TYPE: 'edit' }, includes: L.Mixin.Events, initialize: function (map, options) { L.Handler.prototype.initialize.call(this, map); // Set options to the default unless already set this._selectedPathOptions = options.selectedPathOptions; // Store the selectable layer group for ease of access this._featureGroup = options.featureGroup; if (!(this._featureGroup instanceof L.FeatureGroup)) { throw new Error('options.featureGroup must be a L.FeatureGroup'); } this._uneditedLayerProps = {}; // Save the type so super can fire, need to do this as cannot do this.TYPE :( this.type = L.EditToolbar.Edit.TYPE; }, enable: function () { if (this._enabled || !this._hasAvailableLayers()) { return; } this.fire('enabled', {handler: this.type}); //this disable other handlers this._map.fire('draw:editstart', { handler: this.type }); //allow drawLayer to be updated before beginning edition. L.Handler.prototype.enable.call(this); this._featureGroup .on('layeradd', this._enableLayerEdit, this) .on('layerremove', this._disableLayerEdit, this); }, disable: function () { if (!this._enabled) { return; } this._featureGroup .off('layeradd', this._enableLayerEdit, this) .off('layerremove', this._disableLayerEdit, this); L.Handler.prototype.disable.call(this); this._map.fire('draw:editstop', { handler: this.type }); this.fire('disabled', {handler: this.type}); }, addHooks: function () { var map = this._map; if (map) { map.getContainer().focus(); this._featureGroup.eachLayer(this._enableLayerEdit, this); this._tooltip = new L.Tooltip(this._map); this._tooltip.updateContent({ text: L.drawLocal.edit.handlers.edit.tooltip.text, subtext: L.drawLocal.edit.handlers.edit.tooltip.subtext }); this._map.on('mousemove', this._onMouseMove, this); } }, removeHooks: function () { if (this._map) { // Clean up selected layers. this._featureGroup.eachLayer(this._disableLayerEdit, this); // Clear the backups of the original layers this._uneditedLayerProps = {}; this._tooltip.dispose(); this._tooltip = null; this._map.off('mousemove', this._onMouseMove, this); } }, revertLayers: function () { this._featureGroup.eachLayer(function (layer) { this._revertLayer(layer); }, this); }, save: function () { var editedLayers = new L.LayerGroup(); this._featureGroup.eachLayer(function (layer) { if (layer.edited) { editedLayers.addLayer(layer); layer.edited = false; } }); this._map.fire('draw:edited', {layers: editedLayers}); }, _backupLayer: function (layer) { var id = L.Util.stamp(layer); if (!this._uneditedLayerProps[id]) { // Polyline, Polygon or Rectangle if (layer instanceof L.Polyline || layer instanceof L.Polygon || layer instanceof L.Rectangle) { this._uneditedLayerProps[id] = { latlngs: L.LatLngUtil.cloneLatLngs(layer.getLatLngs()) }; } else if (layer instanceof L.Circle) { this._uneditedLayerProps[id] = { latlng: L.LatLngUtil.cloneLatLng(layer.getLatLng()), radius: layer.getRadius() }; } else if (layer instanceof L.Marker) { // Marker this._uneditedLayerProps[id] = { latlng: L.LatLngUtil.cloneLatLng(layer.getLatLng()) }; } } }, _revertLayer: function (layer) { var id = L.Util.stamp(layer); layer.edited = false; if (this._uneditedLayerProps.hasOwnProperty(id)) { // Polyline, Polygon or Rectangle if (layer instanceof L.Polyline || layer instanceof L.Polygon || layer instanceof L.Rectangle) { layer.setLatLngs(this._uneditedLayerProps[id].latlngs); } else if (layer instanceof L.Circle) { layer.setLatLng(this._uneditedLayerProps[id].latlng); layer.setRadius(this._uneditedLayerProps[id].radius); } else if (layer instanceof L.Marker) { // Marker layer.setLatLng(this._uneditedLayerProps[id].latlng); } } }, _toggleMarkerHighlight: function (marker) { if (!marker._icon) { return; } // This is quite naughty, but I don't see another way of doing it. (short of setting a new icon) var icon = marker._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'; }, _enableLayerEdit: function (e) { var layer = e.layer || e.target || e, isMarker = layer instanceof L.Marker, pathOptions; // 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 Leafler.markercluster there // is a chance that a marker doesn't. if (isMarker && !layer._icon) { return; } // Back up this layer (if haven't before) this._backupLayer(layer); // Update layer style so appears editable if (this._selectedPathOptions) { pathOptions = L.Util.extend({}, this._selectedPathOptions); if (isMarker) { this._toggleMarkerHighlight(layer); } else { layer.options.previousOptions = L.Util.extend({ dashArray: null }, layer.options); // Make sure that Polylines are not filled if (!(layer instanceof L.Circle) && !(layer instanceof L.Polygon) && !(layer instanceof L.Rectangle)) { pathOptions.fill = false; } layer.setStyle(pathOptions); } } if (isMarker) { layer.dragging.enable(); layer.on('dragend', this._onMarkerDragEnd); } else { layer.editing.enable(); } }, _disableLayerEdit: function (e) { var layer = e.layer || e.target || e; layer.edited = false; // Reset layer styles to that of before select if (this._selectedPathOptions) { if (layer instanceof L.Marker) { this._toggleMarkerHighlight(layer); } else { // reset the layer style to what is was before being selected layer.setStyle(layer.options.previousOptions); // remove the cached options for the layer object delete layer.options.previousOptions; } } if (layer instanceof L.Marker) { layer.dragging.disable(); layer.off('dragend', this._onMarkerDragEnd, this); } else { layer.editing.disable(); } }, _onMarkerDragEnd: function (e) { var layer = e.target; layer.edited = true; }, _onMouseMove: function (e) { this._tooltip.updatePosition(e.latlng); }, _hasAvailableLayers: function () { return this._featureGroup.getLayers().length !== 0; } }); L.EditToolbar.Delete = L.Handler.extend({ statics: { TYPE: 'remove' // not delete as delete is reserved in js }, includes: L.Mixin.Events, initialize: function (map, options) { L.Handler.prototype.initialize.call(this, map); L.Util.setOptions(this, options); // Store the selectable layer group for ease of access this._deletableLayers = this.options.featureGroup; if (!(this._deletableLayers instanceof L.FeatureGroup)) { throw new Error('options.featureGroup must be a L.FeatureGroup'); } // Save the type so super can fire, need to do this as cannot do this.TYPE :( this.type = L.EditToolbar.Delete.TYPE; }, enable: function () { if (this._enabled || !this._hasAvailableLayers()) { return; } this.fire('enabled', { handler: this.type}); this._map.fire('draw:deletestart', { handler: this.type }); L.Handler.prototype.enable.call(this); this._deletableLayers .on('layeradd', this._enableLayerDelete, this) .on('layerremove', this._disableLayerDelete, this); }, disable: function () { if (!this._enabled) { return; } this._deletableLayers .off('layeradd', this._enableLayerDelete, this) .off('layerremove', this._disableLayerDelete, this); L.Handler.prototype.disable.call(this); this._map.fire('draw:deletestop', { handler: this.type }); this.fire('disabled', { handler: this.type}); }, addHooks: function () { var map = this._map; if (map) { map.getContainer().focus(); this._deletableLayers.eachLayer(this._enableLayerDelete, this); this._deletedLayers = new L.layerGroup(); this._tooltip = new L.Tooltip(this._map); this._tooltip.updateContent({ text: L.drawLocal.edit.handlers.remove.tooltip.text }); this._map.on('mousemove', this._onMouseMove, this); } }, removeHooks: function () { if (this._map) { this._deletableLayers.eachLayer(this._disableLayerDelete, this); this._deletedLayers = null; this._tooltip.dispose(); this._tooltip = null; this._map.off('mousemove', this._onMouseMove, this); } }, revertLayers: function () { // Iterate of the deleted layers and add them back into the featureGroup this._deletedLayers.eachLayer(function (layer) { this._deletableLayers.addLayer(layer); }, this); }, save: function () { this._map.fire('draw:deleted', { layers: this._deletedLayers }); }, _enableLayerDelete: function (e) { var layer = e.layer || e.target || e; layer.on('click', this._removeLayer, this); }, _disableLayerDelete: function (e) { var layer = e.layer || e.target || e; layer.off('click', this._removeLayer, this); // Remove from the deleted layers so we can't accidently revert if the user presses cancel this._deletedLayers.removeLayer(layer); }, _removeLayer: function (e) { var layer = e.layer || e.target || e; this._deletableLayers.removeLayer(layer); this._deletedLayers.addLayer(layer); }, _onMouseMove: function (e) { this._tooltip.updatePosition(e.latlng); }, _hasAvailableLayers: function () { return this._deletableLayers.getLayers().length !== 0; } }); }(window, document));