UNPKG

leaflet-editable

Version:
1,501 lines (1,306 loc) 69.6 kB
;((factory, window) => { /*globals define, module, require*/ // define an AMD module that relies on 'leaflet' if (typeof define === 'function' && define.amd) { define(['leaflet'], factory) // define a Common JS module that relies on 'leaflet' } else if (typeof exports === 'object') { module.exports = factory(require('leaflet')) } // attach your plugin to the global 'L' variable if (typeof window !== 'undefined' && window.L) { factory(window.L) } })((L) => { // 🍂miniclass CancelableEvent (Event objects) // 🍂method cancel() // Cancel any subsequent action. // 🍂miniclass VertexEvent (Event objects) // 🍂property vertex: VertexMarker // The vertex that fires the event. // 🍂miniclass ShapeEvent (Event objects) // 🍂property shape: Array // The shape (LatLngs array) subject of the action. // 🍂miniclass CancelableVertexEvent (Event objects) // 🍂inherits VertexEvent // 🍂inherits CancelableEvent // 🍂miniclass CancelableShapeEvent (Event objects) // 🍂inherits ShapeEvent // 🍂inherits CancelableEvent // 🍂miniclass LayerEvent (Event objects) // 🍂property layer: object // The Layer (Marker, Polyline…) subject of the action. // 🍂namespace Editable; 🍂class Editable; 🍂aka L.Editable // Main edition handler. By default, it is attached to the map // as `map.editTools` property. // Leaflet.Editable is made to be fully extendable. You have three ways to customize // the behaviour: using options, listening to events, or extending. L.Editable = L.Evented.extend({ statics: { FORWARD: 1, BACKWARD: -1, }, options: { // You can pass them when creating a map using the `editOptions` key. // 🍂option zIndex: int = 1000 // The default zIndex of the editing tools. zIndex: 1000, // 🍂option polygonClass: class = L.Polygon // Class to be used when creating a new Polygon. polygonClass: L.Polygon, // 🍂option polylineClass: class = L.Polyline // Class to be used when creating a new Polyline. polylineClass: L.Polyline, // 🍂option markerClass: class = L.Marker // Class to be used when creating a new Marker. markerClass: L.Marker, // 🍂option circleMarkerClass: class = L.CircleMarker // Class to be used when creating a new CircleMarker. circleMarkerClass: L.CircleMarker, // 🍂option rectangleClass: class = L.Rectangle // Class to be used when creating a new Rectangle. rectangleClass: L.Rectangle, // 🍂option circleClass: class = L.Circle // Class to be used when creating a new Circle. circleClass: L.Circle, // 🍂option drawingCSSClass: string = 'leaflet-editable-drawing' // CSS class to be added to the map container while drawing. drawingCSSClass: 'leaflet-editable-drawing', // 🍂option drawingCursor: const = 'crosshair' // Cursor mode set to the map while drawing. drawingCursor: 'crosshair', // 🍂option editLayer: Layer = new L.LayerGroup() // Layer used to store edit tools (vertex, line guide…). editLayer: undefined, // 🍂option featuresLayer: Layer = new L.LayerGroup() // Default layer used to store drawn features (Marker, Polyline…). featuresLayer: undefined, // 🍂option polylineEditorClass: class = PolylineEditor // Class to be used as Polyline editor. polylineEditorClass: undefined, // 🍂option polygonEditorClass: class = PolygonEditor // Class to be used as Polygon editor. polygonEditorClass: undefined, // 🍂option markerEditorClass: class = MarkerEditor // Class to be used as Marker editor. markerEditorClass: undefined, // 🍂option circleMarkerEditorClass: class = CircleMarkerEditor // Class to be used as CircleMarker editor. circleMarkerEditorClass: undefined, // 🍂option rectangleEditorClass: class = RectangleEditor // Class to be used as Rectangle editor. rectangleEditorClass: undefined, // 🍂option circleEditorClass: class = CircleEditor // Class to be used as Circle editor. circleEditorClass: undefined, // 🍂option lineGuideOptions: hash = {} // Options to be passed to the line guides. lineGuideOptions: {}, // 🍂option skipMiddleMarkers: boolean = false // Set this to true if you don't want middle markers. skipMiddleMarkers: false, }, initialize: function (map, options) { L.setOptions(this, options) this._lastZIndex = this.options.zIndex this.map = map this.editLayer = this.createEditLayer() this.featuresLayer = this.createFeaturesLayer() this.forwardLineGuide = this.createLineGuide() this.backwardLineGuide = this.createLineGuide() }, fireAndForward: function (type, e) { e = e || {} e.editTools = this this.fire(type, e) this.map.fire(type, e) }, createLineGuide: function () { const options = L.extend( { dashArray: '5,10', weight: 1, interactive: false }, this.options.lineGuideOptions ) return L.polyline([], options) }, createVertexIcon: (options) => L.Browser.mobile && L.Browser.touch ? new L.Editable.TouchVertexIcon(options) : new L.Editable.VertexIcon(options), createEditLayer: function () { return this.options.editLayer || new L.LayerGroup().addTo(this.map) }, createFeaturesLayer: function () { return this.options.featuresLayer || new L.LayerGroup().addTo(this.map) }, moveForwardLineGuide: function (latlng) { if (this.forwardLineGuide._latlngs.length) { this.forwardLineGuide._latlngs[1] = latlng this.forwardLineGuide._bounds.extend(latlng) this.forwardLineGuide.redraw() } }, moveBackwardLineGuide: function (latlng) { if (this.backwardLineGuide._latlngs.length) { this.backwardLineGuide._latlngs[1] = latlng this.backwardLineGuide._bounds.extend(latlng) this.backwardLineGuide.redraw() } }, anchorForwardLineGuide: function (latlng) { this.forwardLineGuide._latlngs[0] = latlng this.forwardLineGuide._bounds.extend(latlng) this.forwardLineGuide.redraw() }, anchorBackwardLineGuide: function (latlng) { this.backwardLineGuide._latlngs[0] = latlng this.backwardLineGuide._bounds.extend(latlng) this.backwardLineGuide.redraw() }, attachForwardLineGuide: function () { this.editLayer.addLayer(this.forwardLineGuide) }, attachBackwardLineGuide: function () { this.editLayer.addLayer(this.backwardLineGuide) }, detachForwardLineGuide: function () { this.forwardLineGuide.setLatLngs([]) this.editLayer.removeLayer(this.forwardLineGuide) }, detachBackwardLineGuide: function () { this.backwardLineGuide.setLatLngs([]) this.editLayer.removeLayer(this.backwardLineGuide) }, blockEvents: function () { // Hack: force map not to listen to other layers events while drawing. if (!this._oldTargets) { this._oldTargets = this.map._targets this.map._targets = {} } }, unblockEvents: function () { if (this._oldTargets) { // Reset, but keep targets created while drawing. this.map._targets = L.extend(this.map._targets, this._oldTargets) delete this._oldTargets } }, registerForDrawing: function (editor) { if (this._drawingEditor) this.unregisterForDrawing(this._drawingEditor) this.blockEvents() editor.reset() // Make sure editor tools still receive events. this._drawingEditor = editor this.map.on('mousemove touchmove', editor.onDrawingMouseMove, editor) this.map.on('mousedown', this.onMousedown, this) this.map.on('mouseup', this.onMouseup, this) L.DomUtil.addClass(this.map._container, this.options.drawingCSSClass) this.defaultMapCursor = this.map._container.style.cursor this.map._container.style.cursor = this.options.drawingCursor }, unregisterForDrawing: function (editor) { this.unblockEvents() L.DomUtil.removeClass(this.map._container, this.options.drawingCSSClass) this.map._container.style.cursor = this.defaultMapCursor editor = editor || this._drawingEditor if (!editor) return this.map.off('mousemove touchmove', editor.onDrawingMouseMove, editor) this.map.off('mousedown', this.onMousedown, this) this.map.off('mouseup', this.onMouseup, this) if (editor !== this._drawingEditor) return delete this._drawingEditor if (editor._drawing) editor.cancelDrawing() }, onMousedown: function (e) { if (e.originalEvent.which != 1) return this._mouseDown = e this._drawingEditor.onDrawingMouseDown(e) }, onMouseup: function (e) { if (this._mouseDown) { const editor = this._drawingEditor const mouseDown = this._mouseDown this._mouseDown = null editor.onDrawingMouseUp(e) if (this._drawingEditor !== editor) return // onDrawingMouseUp may call unregisterFromDrawing. const origin = L.point( mouseDown.originalEvent.clientX, mouseDown.originalEvent.clientY ) const distance = L.point( e.originalEvent.clientX, e.originalEvent.clientY ).distanceTo(origin) if (Math.abs(distance) < 9 * (window.devicePixelRatio || 1)) this._drawingEditor.onDrawingClick(e) } }, // 🍂section Public methods // You will generally access them by the `map.editTools` // instance: // // `map.editTools.startPolyline();` // 🍂method drawing(): boolean // Return true if any drawing action is ongoing. drawing: function () { return this._drawingEditor?.drawing() }, // 🍂method stopDrawing() // When you need to stop any ongoing drawing, without needing to know which editor is active. stopDrawing: function () { this.unregisterForDrawing() }, // 🍂method commitDrawing() // When you need to commit any ongoing drawing, without needing to know which editor is active. commitDrawing: function (e) { if (!this._drawingEditor) return this._drawingEditor.commitDrawing(e) }, connectCreatedToMap: function (layer) { return this.featuresLayer.addLayer(layer) }, // 🍂method startPolyline(latlng: L.LatLng, options: hash): L.Polyline // Start drawing a Polyline. If `latlng` is given, a first point will be added. In any case, continuing on user click. // If `options` is given, it will be passed to the Polyline class constructor. startPolyline: function (latlng, options) { const line = this.createPolyline([], options) line.enableEdit(this.map).newShape(latlng) return line }, // 🍂method startPolygon(latlng: L.LatLng, options: hash): L.Polygon // Start drawing a Polygon. If `latlng` is given, a first point will be added. In any case, continuing on user click. // If `options` is given, it will be passed to the Polygon class constructor. startPolygon: function (latlng, options) { const polygon = this.createPolygon([], options) polygon.enableEdit(this.map).newShape(latlng) return polygon }, // 🍂method startMarker(latlng: L.LatLng, options: hash): L.Marker // Start adding a Marker. If `latlng` is given, the Marker will be shown first at this point. // In any case, it will follow the user mouse, and will have a final `latlng` on next click (or touch). // If `options` is given, it will be passed to the Marker class constructor. startMarker: function (latlng, options) { latlng = latlng || this.map.getCenter().clone() const marker = this.createMarker(latlng, options) marker.enableEdit(this.map).startDrawing() return marker }, // 🍂method startCircleMarker(latlng: L.LatLng, options: hash): L.CircleMarker // Start adding a CircleMarker. If `latlng` is given, the CircleMarker will be shown first at this point. // In any case, it will follow the user mouse, and will have a final `latlng` on next click (or touch). // If `options` is given, it will be passed to the CircleMarker class constructor. startCircleMarker: function (latlng, options) { latlng = latlng || this.map.getCenter().clone() const marker = this.createCircleMarker(latlng, options) marker.enableEdit(this.map).startDrawing() return marker }, // 🍂method startRectangle(latlng: L.LatLng, options: hash): L.Rectangle // Start drawing a Rectangle. If `latlng` is given, the Rectangle anchor will be added. In any case, continuing on user drag. // If `options` is given, it will be passed to the Rectangle class constructor. startRectangle: function (latlng, options) { const corner = latlng || L.latLng([0, 0]) const bounds = new L.LatLngBounds(corner, corner) const rectangle = this.createRectangle(bounds, options) rectangle.enableEdit(this.map).startDrawing() return rectangle }, // 🍂method startCircle(latlng: L.LatLng, options: hash): L.Circle // Start drawing a Circle. If `latlng` is given, the Circle anchor will be added. In any case, continuing on user drag. // If `options` is given, it will be passed to the Circle class constructor. startCircle: function (latlng, options) { latlng = latlng || this.map.getCenter().clone() const circle = this.createCircle(latlng, options) circle.enableEdit(this.map).startDrawing() return circle }, startHole: (editor, latlng) => { editor.newHole(latlng) }, createLayer: function (klass, latlngs, options) { options = L.Util.extend({ editOptions: { editTools: this } }, options) const layer = new klass(latlngs, options) // 🍂namespace Editable // 🍂event editable:created: LayerEvent // Fired when a new feature (Marker, Polyline…) is created. this.fireAndForward('editable:created', { layer: layer }) return layer }, createPolyline: function (latlngs, options) { return this.createLayer( options?.polylineClass || this.options.polylineClass, latlngs, options ) }, createPolygon: function (latlngs, options) { return this.createLayer( options?.polygonClass || this.options.polygonClass, latlngs, options ) }, createMarker: function (latlng, options) { return this.createLayer( options?.markerClass || this.options.markerClass, latlng, options ) }, createCircleMarker: function (latlng, options) { return this.createLayer( options?.circleMarkerClass || this.options.circleMarkerClass, latlng, options ) }, createRectangle: function (bounds, options) { return this.createLayer( options?.rectangleClass || this.options.rectangleClass, bounds, options ) }, createCircle: function (latlng, options) { return this.createLayer( options?.circleClass || this.options.circleClass, latlng, options ) }, }) L.extend(L.Editable, { makeCancellable: (e) => { e.cancel = () => { e._cancelled = true } }, }) // 🍂namespace Map; 🍂class Map // Leaflet.Editable add options and events to the `L.Map` object. // See `Editable` events for the list of events fired on the Map. // 🍂example // // ```js // var map = L.map('map', { // editable: true, // editOptions: { // … // } // }); // ``` // 🍂section Editable Map Options L.Map.mergeOptions({ // 🍂namespace Map // 🍂section Map Options // 🍂option editToolsClass: class = L.Editable // Class to be used as vertex, for path editing. editToolsClass: L.Editable, // 🍂option editable: boolean = false // Whether to create a L.Editable instance at map init. editable: false, // 🍂option editOptions: hash = {} // Options to pass to L.Editable when instantiating. editOptions: {}, }) L.Map.addInitHook(function () { this.whenReady(function () { if (this.options.editable) { this.editTools = new this.options.editToolsClass(this, this.options.editOptions) } }) }) L.Editable.VertexIcon = L.DivIcon.extend({ options: { iconSize: new L.Point(8, 8), }, }) L.Editable.TouchVertexIcon = L.Editable.VertexIcon.extend({ options: { iconSize: new L.Point(20, 20), }, }) // 🍂namespace Editable; 🍂class VertexMarker; Handler for dragging path vertices. L.Editable.VertexMarker = L.Marker.extend({ options: { draggable: true, className: 'leaflet-div-icon leaflet-vertex-icon', }, // 🍂section Public methods // The marker used to handle path vertex. You will usually interact with a `VertexMarker` // instance when listening for events like `editable:vertex:ctrlclick`. initialize: function (latlng, latlngs, editor, options) { // We don't use this._latlng, because on drag Leaflet replace it while // we want to keep reference. this.latlng = latlng this.latlngs = latlngs this.editor = editor L.Marker.prototype.initialize.call(this, latlng, options) this.options.icon = this.editor.tools.createVertexIcon({ className: this.options.className, }) this.latlng.__vertex = this this.connect() this.setZIndexOffset(editor.tools._lastZIndex + 1) }, connect: function () { this.editor.editLayer.addLayer(this) }, onAdd: function (map) { L.Marker.prototype.onAdd.call(this, map) this.on('drag', this.onDrag) this.on('dragstart', this.onDragStart) this.on('dragend', this.onDragEnd) this.on('mouseup', this.onMouseup) this.on('click', this.onClick) this.on('contextmenu', this.onContextMenu) this.on('mousedown touchstart', this.onMouseDown) this.on('mouseover', this.onMouseOver) this.on('mouseout', this.onMouseOut) this.addMiddleMarkers() }, onRemove: function (map) { if (this.middleMarker) this.middleMarker.delete() delete this.latlng.__vertex this.off('drag', this.onDrag) this.off('dragstart', this.onDragStart) this.off('dragend', this.onDragEnd) this.off('mouseup', this.onMouseup) this.off('click', this.onClick) this.off('contextmenu', this.onContextMenu) this.off('mousedown touchstart', this.onMouseDown) this.off('mouseover', this.onMouseOver) this.off('mouseout', this.onMouseOut) L.Marker.prototype.onRemove.call(this, map) }, onDrag: function (e) { e.vertex = this this.editor.onVertexMarkerDrag(e) const iconPos = L.DomUtil.getPosition(this._icon) const latlng = this._map.layerPointToLatLng(iconPos) this.latlng.update(latlng) this._latlng = this.latlng // Push back to Leaflet our reference. this.editor.refresh() if (this.middleMarker) this.middleMarker.updateLatLng() const next = this.getNext() if (next?.middleMarker) next.middleMarker.updateLatLng() }, onDragStart: function (e) { e.vertex = this this.editor.onVertexMarkerDragStart(e) }, onDragEnd: function (e) { e.vertex = this this.editor.onVertexMarkerDragEnd(e) }, onClick: function (e) { e.vertex = this this.editor.onVertexMarkerClick(e) }, onMouseup: function (e) { L.DomEvent.stop(e) e.vertex = this this.editor.map.fire('mouseup', e) }, onContextMenu: function (e) { e.vertex = this this.editor.onVertexMarkerContextMenu(e) }, onMouseDown: function (e) { e.vertex = this this.editor.onVertexMarkerMouseDown(e) }, onMouseOver: function (e) { e.vertex = this this.editor.onVertexMarkerMouseOver(e) }, onMouseOut: function (e) { e.vertex = this this.editor.onVertexMarkerMouseOut(e) }, // 🍂method delete() // Delete a vertex and the related LatLng. delete: function () { const next = this.getNext() // Compute before changing latlng this.latlngs.splice(this.getIndex(), 1) this.editor.editLayer.removeLayer(this) this.editor.onVertexDeleted({ latlng: this.latlng, vertex: this }) if (!this.latlngs.length) this.editor.deleteShape(this.latlngs) if (next) next.resetMiddleMarker() this.editor.refresh() }, // 🍂method getIndex(): int // Get the index of the current vertex among others of the same LatLngs group. getIndex: function () { return this.latlngs.indexOf(this.latlng) }, // 🍂method getLastIndex(): int // Get last vertex index of the LatLngs group of the current vertex. getLastIndex: function () { return this.latlngs.length - 1 }, // 🍂method getPrevious(): VertexMarker // Get the previous VertexMarker in the same LatLngs group. getPrevious: function () { if (this.latlngs.length < 2) return const index = this.getIndex() let previousIndex = index - 1 if (index === 0 && this.editor.CLOSED) previousIndex = this.getLastIndex() const previous = this.latlngs[previousIndex] if (previous) return previous.__vertex }, // 🍂method getNext(): VertexMarker // Get the next VertexMarker in the same LatLngs group. getNext: function () { if (this.latlngs.length < 2) return const index = this.getIndex() let nextIndex = index + 1 if (index === this.getLastIndex() && this.editor.CLOSED) nextIndex = 0 const next = this.latlngs[nextIndex] if (next) return next.__vertex }, addMiddleMarker: function (previous) { if (!this.editor.hasMiddleMarkers()) return previous = previous || this.getPrevious() if (previous && !this.middleMarker) this.middleMarker = this.editor.addMiddleMarker( previous, this, this.latlngs, this.editor ) }, addMiddleMarkers: function () { if (!this.editor.hasMiddleMarkers()) return const previous = this.getPrevious() if (previous) this.addMiddleMarker(previous) const next = this.getNext() if (next) next.resetMiddleMarker() }, resetMiddleMarker: function () { if (this.middleMarker) this.middleMarker.delete() this.addMiddleMarker() }, // 🍂method split() // Split the vertex LatLngs group at its index, if possible. split: function () { if (!this.editor.splitShape) return // Only for PolylineEditor this.editor.splitShape(this.latlngs, this.getIndex()) }, // 🍂method continue() // Continue the vertex LatLngs from this vertex. Only active for first and last vertices of a Polyline. continue: function () { if (!this.editor.continueBackward) return // Only for PolylineEditor const index = this.getIndex() if (index === 0) this.editor.continueBackward(this.latlngs) else if (index === this.getLastIndex()) this.editor.continueForward(this.latlngs) }, }) L.Editable.mergeOptions({ // 🍂namespace Editable // 🍂option vertexMarkerClass: class = VertexMarker // Class to be used as vertex, for path editing. vertexMarkerClass: L.Editable.VertexMarker, }) L.Editable.MiddleMarker = L.Marker.extend({ options: { opacity: 0.5, className: 'leaflet-div-icon leaflet-middle-icon', draggable: true, }, initialize: function (left, right, latlngs, editor, options) { this.left = left this.right = right this.editor = editor this.latlngs = latlngs L.Marker.prototype.initialize.call(this, this.computeLatLng(), options) this._opacity = this.options.opacity this.options.icon = this.editor.tools.createVertexIcon({ className: this.options.className, }) this.editor.editLayer.addLayer(this) this.setVisibility() }, setVisibility: function () { const leftPoint = this._map.latLngToContainerPoint(this.left.latlng) const rightPoint = this._map.latLngToContainerPoint(this.right.latlng) const size = L.point(this.options.icon.options.iconSize) if (leftPoint.distanceTo(rightPoint) < size.x * 3) this.hide() else this.show() }, show: function () { this.setOpacity(this._opacity) }, hide: function () { this.setOpacity(0) }, updateLatLng: function () { this.setLatLng(this.computeLatLng()) this.setVisibility() }, computeLatLng: function () { const leftPoint = this.editor.map.latLngToContainerPoint(this.left.latlng) const rightPoint = this.editor.map.latLngToContainerPoint(this.right.latlng) const y = (leftPoint.y + rightPoint.y) / 2 const x = (leftPoint.x + rightPoint.x) / 2 return this.editor.map.containerPointToLatLng([x, y]) }, onAdd: function (map) { L.Marker.prototype.onAdd.call(this, map) L.DomEvent.on(this._icon, 'mousedown touchstart', this.onMouseDown, this) map.on('zoomend', this.setVisibility, this) }, onRemove: function (map) { delete this.right.middleMarker L.DomEvent.off(this._icon, 'mousedown touchstart', this.onMouseDown, this) map.off('zoomend', this.setVisibility, this) L.Marker.prototype.onRemove.call(this, map) }, onMouseDown: function (e) { const iconPos = L.DomUtil.getPosition(this._icon) const latlng = this.editor.map.layerPointToLatLng(iconPos) e = { originalEvent: e, latlng: latlng, } if (this.options.opacity === 0) return L.Editable.makeCancellable(e) this.editor.onMiddleMarkerMouseDown(e) if (e._cancelled) return this.latlngs.splice(this.index(), 0, e.latlng) this.editor.refresh() const icon = this._icon const marker = this.editor.addVertexMarker(e.latlng, this.latlngs) this.editor.onNewVertex(marker) /* Hack to workaround browser not firing touchend when element is no more on DOM */ const parent = marker._icon.parentNode parent.removeChild(marker._icon) marker._icon = icon parent.appendChild(marker._icon) marker._initIcon() marker._initInteraction() marker.setOpacity(1) /* End hack */ // Transfer ongoing dragging to real marker L.Draggable._dragging = false marker.dragging._draggable._onDown(e.originalEvent) this.delete() }, delete: function () { this.editor.editLayer.removeLayer(this) }, index: function () { return this.latlngs.indexOf(this.right.latlng) }, }) L.Editable.mergeOptions({ // 🍂namespace Editable // 🍂option middleMarkerClass: class = VertexMarker // Class to be used as middle vertex, pulled by the user to create a new point in the middle of a path. middleMarkerClass: L.Editable.MiddleMarker, }) // 🍂namespace Editable; 🍂class BaseEditor; 🍂aka L.Editable.BaseEditor // When editing a feature (Marker, Polyline…), an editor is attached to it. This // editor basically knows how to handle the edition. L.Editable.BaseEditor = L.Handler.extend({ initialize: function (map, feature, options) { L.setOptions(this, options) this.map = map this.feature = feature this.feature.editor = this this.editLayer = new L.LayerGroup() this.tools = this.options.editTools || map.editTools }, // 🍂method enable(): this // Set up the drawing tools for the feature to be editable. addHooks: function () { if (this.isConnected()) this.onFeatureAdd() else this.feature.once('add', this.onFeatureAdd, this) this.onEnable() this.feature.on(this._getEvents(), this) }, // 🍂method disable(): this // Remove the drawing tools for the feature. removeHooks: function () { this.feature.off(this._getEvents(), this) if (this.feature.dragging) this.feature.dragging.disable() this.editLayer.clearLayers() this.tools.editLayer.removeLayer(this.editLayer) this.onDisable() if (this._drawing) this.cancelDrawing() }, // 🍂method drawing(): boolean // Return true if any drawing action is ongoing with this editor. drawing: function () { return !!this._drawing }, reset: () => {}, onFeatureAdd: function () { this.tools.editLayer.addLayer(this.editLayer) if (this.feature.dragging) this.feature.dragging.enable() }, hasMiddleMarkers: function () { return !this.options.skipMiddleMarkers && !this.tools.options.skipMiddleMarkers }, fireAndForward: function (type, e) { e = e || {} e.layer = this.feature this.feature.fire(type, e) this.tools.fireAndForward(type, e) }, onEnable: function () { // 🍂namespace Editable // 🍂event editable:enable: Event // Fired when an existing feature is ready to be edited. this.fireAndForward('editable:enable') }, onDisable: function () { // 🍂namespace Editable // 🍂event editable:disable: Event // Fired when an existing feature is not ready anymore to be edited. this.fireAndForward('editable:disable') }, onEditing: function () { // 🍂namespace Editable // 🍂event editable:editing: Event // Fired as soon as any change is made to the feature geometry. this.fireAndForward('editable:editing') }, onEdited: function () { // 🍂namespace Editable // 🍂event editable:edited: Event // Fired after any change is made to the feature geometry. this.fireAndForward('editable:edited') }, onStartDrawing: function () { // 🍂namespace Editable // 🍂section Drawing events // 🍂event editable:drawing:start: Event // Fired when a feature is to be drawn. this.fireAndForward('editable:drawing:start') }, onEndDrawing: function () { // 🍂namespace Editable // 🍂section Drawing events // 🍂event editable:drawing:end: Event // Fired when a feature is not drawn anymore. this.fireAndForward('editable:drawing:end') }, onCancelDrawing: function () { // 🍂namespace Editable // 🍂section Drawing events // 🍂event editable:drawing:cancel: Event // Fired when user cancel drawing while a feature is being drawn. this.fireAndForward('editable:drawing:cancel') }, onCommitDrawing: function (e) { // 🍂namespace Editable // 🍂section Drawing events // 🍂event editable:drawing:commit: Event // Fired when user finish drawing a feature. this.fireAndForward('editable:drawing:commit', e) this.onEdited() }, onDrawingMouseDown: function (e) { // 🍂namespace Editable // 🍂section Drawing events // 🍂event editable:drawing:mousedown: Event // Fired when user `mousedown` while drawing. this.fireAndForward('editable:drawing:mousedown', e) }, onDrawingMouseUp: function (e) { // 🍂namespace Editable // 🍂section Drawing events // 🍂event editable:drawing:mouseup: Event // Fired when user `mouseup` while drawing. this.fireAndForward('editable:drawing:mouseup', e) }, startDrawing: function () { if (!this._drawing) this._drawing = L.Editable.FORWARD this.tools.registerForDrawing(this) this.onStartDrawing() }, commitDrawing: function (e) { this.onCommitDrawing(e) this.endDrawing() }, cancelDrawing: function () { // If called during a vertex drag, the vertex will be removed before // the mouseup fires on it. This is a workaround. Maybe better fix is // To have L.Draggable reset it's status on disable (Leaflet side). L.Draggable._dragging = false this.onCancelDrawing() this.endDrawing() }, endDrawing: function () { this._drawing = false this.tools.unregisterForDrawing(this) this.onEndDrawing() }, onDrawingClick: function (e) { if (!this.drawing()) return L.Editable.makeCancellable(e) // 🍂namespace Editable // 🍂section Drawing events // 🍂event editable:drawing:click: CancelableEvent // Fired when user `click` while drawing, before any internal action is being processed. this.fireAndForward('editable:drawing:click', e) if (e._cancelled) return if (!this.isConnected()) this.connect(e) this.processDrawingClick(e) }, isConnected: function () { return this.map.hasLayer(this.feature) }, connect: function () { this.tools.connectCreatedToMap(this.feature) this.tools.editLayer.addLayer(this.editLayer) }, onMove: function (e) { // 🍂namespace Editable // 🍂section Drawing events // 🍂event editable:drawing:move: Event // Fired when `move` mouse while drawing, while dragging a marker, and while dragging a vertex. this.fireAndForward('editable:drawing:move', e) }, onDrawingMouseMove: function (e) { this.onMove(e) }, _getEvents: function () { return { dragstart: this.onDragStart, drag: this.onDrag, dragend: this.onDragEnd, remove: this.disable, } }, onDragStart: function (e) { this.onEditing() // 🍂namespace Editable // 🍂event editable:dragstart: Event // Fired before a path feature is dragged. this.fireAndForward('editable:dragstart', e) }, onDrag: function (e) { this.onMove(e) // 🍂namespace Editable // 🍂event editable:drag: Event // Fired when a path feature is being dragged. this.fireAndForward('editable:drag', e) }, onDragEnd: function (e) { // 🍂namespace Editable // 🍂event editable:dragend: Event // Fired after a path feature has been dragged. this.fireAndForward('editable:dragend', e) this.onEdited() }, }) // 🍂namespace Editable; 🍂class MarkerEditor; 🍂aka L.Editable.MarkerEditor // 🍂inherits BaseEditor // Editor for Marker. L.Editable.MarkerEditor = L.Editable.BaseEditor.extend({ onDrawingMouseMove: function (e) { L.Editable.BaseEditor.prototype.onDrawingMouseMove.call(this, e) if (this._drawing) this.feature.setLatLng(e.latlng) }, processDrawingClick: function (e) { // 🍂namespace Editable // 🍂section Drawing events // 🍂event editable:drawing:clicked: Event // Fired when user `click` while drawing, after all internal actions. this.fireAndForward('editable:drawing:clicked', e) this.commitDrawing(e) }, connect: function (e) { // On touch, the latlng has not been updated because there is // no mousemove. if (e) this.feature._latlng = e.latlng L.Editable.BaseEditor.prototype.connect.call(this, e) }, }) // 🍂namespace Editable; 🍂class CircleMarkerEditor; 🍂aka L.Editable.CircleMarkerEditor // 🍂inherits BaseEditor // Editor for CircleMarker. L.Editable.CircleMarkerEditor = L.Editable.BaseEditor.extend({ onDrawingMouseMove: function (e) { L.Editable.BaseEditor.prototype.onDrawingMouseMove.call(this, e) if (this._drawing) this.feature.setLatLng(e.latlng) }, processDrawingClick: function (e) { // 🍂namespace Editable // 🍂section Drawing events // 🍂event editable:drawing:clicked: Event // Fired when user `click` while drawing, after all internal actions. this.fireAndForward('editable:drawing:clicked', e) this.commitDrawing(e) }, connect: function (e) { // On touch, the latlng has not been updated because there is // no mousemove. if (e) this.feature._latlng = e.latlng L.Editable.BaseEditor.prototype.connect.call(this, e) }, }) // 🍂namespace Editable; 🍂class PathEditor; 🍂aka L.Editable.PathEditor // 🍂inherits BaseEditor // Base class for all path editors. L.Editable.PathEditor = L.Editable.BaseEditor.extend({ CLOSED: false, MIN_VERTEX: 2, addHooks: function () { L.Editable.BaseEditor.prototype.addHooks.call(this) if (this.feature) { this.initVertexMarkers() this.map.on('moveend', this.onMoveEnd, this) } return this }, removeHooks: function () { L.Editable.BaseEditor.prototype.removeHooks.call(this) if (this.feature) { this.map.off('moveend', this.onMoveEnd, this) } }, onMoveEnd: function () { this.initVertexMarkers() }, initVertexMarkers: function (latlngs) { if (!this.enabled()) return latlngs = latlngs || this.getLatLngs() if (isFlat(latlngs)) { this.addVertexMarkers(latlngs) } else { for (const member of latlngs) { this.initVertexMarkers(member) } } }, getLatLngs: function () { return this.feature.getLatLngs() }, // 🍂method reset() // Rebuild edit elements (Vertex, MiddleMarker, etc.). reset: function () { this.editLayer.clearLayers() this.initVertexMarkers() }, addVertexMarker: function (latlng, latlngs) { if (latlng.__vertex) { latlng.__vertex.connect() return latlng.__vertex } return new this.tools.options.vertexMarkerClass(latlng, latlngs, this) }, onNewVertex: function (vertex) { // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:new: VertexEvent // Fired when a new vertex is created. this.fireAndForward('editable:vertex:new', { latlng: vertex.latlng, vertex: vertex, }) }, addVertexMarkers: function (latlngs) { const bounds = this.map.getBounds() for (const latlng of latlngs) { if (!bounds.contains(latlng)) continue this.addVertexMarker(latlng, latlngs) } }, refreshVertexMarkers: function (latlngs) { latlngs = latlngs || this.getDefaultLatLngs() for (const latlng of latlngs) { latlng.__vertex.update() } }, addMiddleMarker: function (left, right, latlngs) { return new this.tools.options.middleMarkerClass(left, right, latlngs, this) }, onVertexMarkerClick: function (e) { L.Editable.makeCancellable(e) // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:click: CancelableVertexEvent // Fired when a `click` is issued on a vertex, before any internal action is being processed. this.fireAndForward('editable:vertex:click', e) if (e._cancelled) return if (this.tools.drawing() && this.tools._drawingEditor !== this) return const index = e.vertex.getIndex() let commit if (e.originalEvent.ctrlKey) { this.onVertexMarkerCtrlClick(e) } else if (e.originalEvent.altKey) { this.onVertexMarkerAltClick(e) } else if (e.originalEvent.shiftKey) { this.onVertexMarkerShiftClick(e) } else if (e.originalEvent.metaKey) { this.onVertexMarkerMetaKeyClick(e) } else if ( index === e.vertex.getLastIndex() && this._drawing === L.Editable.FORWARD ) { if (index >= this.MIN_VERTEX - 1) commit = true } else if ( index === 0 && this._drawing === L.Editable.BACKWARD && this._drawnLatLngs.length >= this.MIN_VERTEX ) { commit = true } else if ( index === 0 && this._drawing === L.Editable.FORWARD && this._drawnLatLngs.length >= this.MIN_VERTEX && this.CLOSED ) { commit = true // Allow to close on first point also for polygons } else { this.onVertexRawMarkerClick(e) } // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:clicked: VertexEvent // Fired when a `click` is issued on a vertex, after all internal actions. this.fireAndForward('editable:vertex:clicked', e) if (commit) this.commitDrawing(e) }, onVertexRawMarkerClick: function (e) { // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:rawclick: CancelableVertexEvent // Fired when a `click` is issued on a vertex without any special key and without being in drawing mode. this.fireAndForward('editable:vertex:rawclick', e) if (e._cancelled) return if (!this.vertexCanBeDeleted(e.vertex)) return e.vertex.delete() }, vertexCanBeDeleted: function (vertex) { return vertex.latlngs.length > this.MIN_VERTEX }, onVertexDeleted: function (e) { // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:deleted: VertexEvent // Fired after a vertex has been deleted by user. this.fireAndForward('editable:vertex:deleted', e) this.onEdited() }, onVertexMarkerCtrlClick: function (e) { // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:ctrlclick: VertexEvent // Fired when a `click` with `ctrlKey` is issued on a vertex. this.fireAndForward('editable:vertex:ctrlclick', e) }, onVertexMarkerShiftClick: function (e) { // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:shiftclick: VertexEvent // Fired when a `click` with `shiftKey` is issued on a vertex. this.fireAndForward('editable:vertex:shiftclick', e) }, onVertexMarkerMetaKeyClick: function (e) { // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:metakeyclick: VertexEvent // Fired when a `click` with `metaKey` is issued on a vertex. this.fireAndForward('editable:vertex:metakeyclick', e) }, onVertexMarkerAltClick: function (e) { // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:altclick: VertexEvent // Fired when a `click` with `altKey` is issued on a vertex. this.fireAndForward('editable:vertex:altclick', e) }, onVertexMarkerContextMenu: function (e) { // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:contextmenu: VertexEvent // Fired when a `contextmenu` is issued on a vertex. this.fireAndForward('editable:vertex:contextmenu', e) }, onVertexMarkerMouseDown: function (e) { // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:mousedown: VertexEvent // Fired when user `mousedown` a vertex. this.fireAndForward('editable:vertex:mousedown', e) }, onVertexMarkerMouseOver: function (e) { // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:mouseover: VertexEvent // Fired when a user's mouse enters the vertex this.fireAndForward('editable:vertex:mouseover', e) }, onVertexMarkerMouseOut: function (e) { // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:mouseout: VertexEvent // Fired when a user's mouse leaves the vertex this.fireAndForward('editable:vertex:mouseout', e) }, onMiddleMarkerMouseDown: function (e) { // 🍂namespace Editable // 🍂section MiddleMarker events // 🍂event editable:middlemarker:mousedown: VertexEvent // Fired when user `mousedown` a middle marker. this.fireAndForward('editable:middlemarker:mousedown', e) }, onVertexMarkerDrag: function (e) { this.onMove(e) if (this.feature._bounds) this.extendBounds(e) // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:drag: VertexEvent // Fired when a vertex is dragged by user. this.fireAndForward('editable:vertex:drag', e) }, onVertexMarkerDragStart: function (e) { // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:dragstart: VertexEvent // Fired before a vertex is dragged by user. this.fireAndForward('editable:vertex:dragstart', e) }, onVertexMarkerDragEnd: function (e) { // 🍂namespace Editable // 🍂section Vertex events // 🍂event editable:vertex:dragend: VertexEvent // Fired after a vertex is dragged by user. this.fireAndForward('editable:vertex:dragend', e) this.onEdited() }, setDrawnLatLngs: function (latlngs) { this._drawnLatLngs = latlngs || this.getDefaultLatLngs() }, startDrawing: function () { if (!this._drawnLatLngs) this.setDrawnLatLngs() L.Editable.BaseEditor.prototype.startDrawing.call(this) }, startDrawingForward: function () { this.startDrawing() }, endDrawing: function () { this.tools.detachForwardLineGuide() this.tools.detachBackwardLineGuide() if (this._drawnLatLngs && this._drawnLatLngs.length < this.MIN_VERTEX) this.deleteShape(this._drawnLatLngs) L.Editable.BaseEditor.prototype.endDrawing.call(this) delete this._drawnLatLngs }, addLatLng: function (latlng) { if (this._drawing === L.Editable.FORWARD) this._drawnLatLngs.push(latlng) else this._drawnLatLngs.unshift(latlng) this.feature._bounds.extend(latlng) const vertex = this.addVertexMarker(latlng, this._drawnLatLngs) this.onNewVertex(vertex) this.refresh() }, newPointForward: function (latlng) { this.addLatLng(latlng) this.tools.attachForwardLineGuide() this.tools.anchorForwardLineGuide(latlng) }, newPointBackward: function (latlng) { this.addLatLng(latlng) this.tools.anchorBackwardLineGuide(latlng) }, // 🍂namespace PathEditor // 🍂method push() // Programmatically add a point while drawing. push: function (latlng) { if (!latlng) return console.error( 'L.Editable.PathEditor.push expect a valid latlng as parameter' ) if (this._drawing === L.Editable.FORWARD) this.newPointForward(latlng) else this.newPointBackward(latlng) }, removeLatLng: function (latlng) { latlng.__vertex.delete() this.refresh() }, // 🍂method pop(): L.LatLng or null // Programmatically remove last point (if any) while drawing. pop: function () { if (this._drawnLatLngs.length <= 1) return let latlng if (this._drawing === L.Editable.FORWARD) { latlng = this._drawnLatLngs[this._drawnLatLngs.length - 1] } else { latlng = this._drawnLatLngs[0] } this.removeLatLng(latlng) if (this._drawing === L.Editable.FORWARD) { this.tools.anchorForwardLineGuide( this._drawnLatLngs[this._drawnLatLngs.length - 1] ) } else { this.tools.anchorForwardLineGuide(this._drawnLatLngs[0]) } return latlng }, processDrawingClick: function (e) { if (e.vertex && e.vertex.editor === this) return if (this._drawing === L.Editable.FORWARD) this.newPointForward(e.latlng) else this.newPointBackward(e.latlng) this.fireAndForward('editable:drawing:clicked', e) }, onDrawingMouseMove: function (e) { L.Editable.BaseEditor.prototype.onDrawingMouseMove.call(this, e) if (this._drawing) { this.tools.moveForwardLineGuide(e.latlng) this.tools.moveBackwardLineGuide(e.latlng) } }, refresh: function () { this.feature.redraw() this.onEditing() }, // 🍂namespace PathEditor // 🍂method newShape(latlng?: L.LatLng) // Add a new shape (Polyline, Polygon) in a multi, and setup up drawing tools to draw it; // if optional `latlng` is given, start a path at this point. newShape: function (latlng) { const shape = this.addNewEmptyShape() if (!shape) return this.setDrawnLatLngs(shape[0] || shape) // Polygon or polyline this.startDrawingForward() // 🍂namespace Editable // 🍂section Shape events // 🍂event editable:shape:new: ShapeEvent // Fired when a new shape is created in a multi (Polygon or Polyline). this.fireAndForward('editable:shape:new', { shape: shape }) if (latlng) this.newPointForward(latlng) }, deleteShape: function (shape, latlngs) { const e = { shape: shape } L.Editable.makeCancellable(e) // 🍂namespace Editable // 🍂section Shape events // 🍂event editable:shape:delete: CancelableShapeEvent // Fired before a new shape is deleted in a multi (Polygon or Polyline). this.fireAndForward('editable:shape:delete', e) if (e._cancelled) return shape = this._deleteShape(shape, latlngs) if (this.ensureNotFlat) this.ensureNotFlat() // Polygon. this.feature.setLatLngs(this.getLatLngs()) // Force bounds reset. this.refresh() this.reset() // 🍂namespace Editable // 🍂section Shape events // 🍂event editable:shape:deleted: ShapeEvent // Fired after a new shape is deleted in a multi (Polygon or Polyline). this.fireAndForward('editable:shape:deleted', { shape: shape }) this.onEdited() return shape }, _deleteShape: function (shape, latlngs) { latlngs = latlngs || this.getLatLngs() if (!latlngs.length) return const inplaceDelete = (latlngs, shape) => { // Called when deleting a flat latlngs return latlngs.splice(0, Number.MAX_VALUE) } const spliceDelete = (latlngs, shape) => {