leaflet-editable
Version:
Make geometries editable in Leaflet
1,501 lines (1,306 loc) • 69.6 kB
JavaScript
;((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) => {