leaflet-partially-editable-polyline
v0.2.0
Published
A Leaflet v2 plugin for editing only a local portion of a large polyline.
Maintainers
Readme
leaflet-partially-editable-polyline
A Leaflet v2 plugin for editing only a local portion of a large polyline.
PartiallyEditablePolyline extends Leaflet's Polyline and provides a lightweight editing interface that creates editor markers only around the currently selected point. This makes it possible to edit a small portion of a large polyline without creating markers for every point.
Status: Early development
This project is still under development. The public API may evolve as the implementation is reviewed and used.
Requirements
- Leaflet v2 (tested with 2.0.0-alpha.1)
- Modern browser with ES module support
This library currently targets Leaflet v2 and is not intended to support older Leaflet versions.
The source imports Leaflet with the bare module specifier "leaflet", so the application needs an import map or a bundler that resolves it. The application and the library must use the same Leaflet module instance; for example, a LatLng passed to startEditing() must be an instance of that module's LatLng class.
Features
- Edit only a local portion of a large polyline
- Existing points can be dragged to new positions
- Midpoint markers can be dragged to insert new points
- Existing points can be deleted using
contextmenu - Only points within the current editing range receive editor markers
- Editing markers are rebuilt when the editing target changes
- External
setLatLngs()calls are supported and rebuild the editing state - Editing operations are reported through dedicated events
- The library does not manage application-specific metadata such as elevation or timestamps
Scope
This project is intentionally kept small and focused.
The library currently provides the editing interaction and geometry management required by the application that motivated it.
In particular, the following are outside the responsibility of this library:
- Application-specific data structure or synchronization
- Elevation calculation or interpolation
- Timestamp management
- Persistence
- Undo/redo
The application using the library remains the source of truth for such data.
Installation
From npm
Install the library and Leaflet:
npm install leaflet-partially-editable-polyline leafletThen import the plugin and its stylesheet:
import { PartiallyEditablePolyline } from "leaflet-partially-editable-polyline";
import "leaflet-partially-editable-polyline/css";The package has no build step and distributes its ESM source directly.
From the source files
Without npm, import the module directly from the source files:
import { PartiallyEditablePolyline } from "./src/leaflet-partially-editable-polyline.js";Without a bundler
With no bundler and no npm install, the browser needs to resolve the bare specifier "leaflet" used by the source (see Requirements). An import map does this:
<script type="importmap">
{
"imports": {
"leaflet": "https://unpkg.com/[email protected]/dist/leaflet.js"
}
}
</script>This is how the demos under examples/ load both Leaflet and the library.
Usage
Once the library is available, here is how to use it.
Styling the editor markers
The editor markers are styled with plain CSS, using these class names:
.leaflet-partially-editable-polyline-point {
width: 10px;
height: 10px;
background: #fff;
border: 2px solid #3388ff;
border-radius: 50%;
box-sizing: border-box;
}
.leaflet-partially-editable-polyline-new-point {
width: 8px;
height: 8px;
background: #3388ff;
border: 2px solid #fff;
border-radius: 50%;
box-sizing: border-box;
opacity: 0.5;
}Add these rules (or your own version of them) to your application's stylesheet. These class names are part of the library's public API and only change with a breaking change.
The library also ships a default stylesheet (see Installation for importing it when installed from npm).
The stylesheet can also be linked or copied directly from the package if needed.
Without the stylesheet or equivalent application styles, editing still works, but the markers have no visible style.
Creating the polyline
Create a polyline in the same way as a normal Leaflet Polyline:
const editor = new PartiallyEditablePolyline(latlngs, {
editablePointRadius: 100,
});
editor.addTo(map);The polyline itself remains a Leaflet layer. The library does not start editing by itself; the application decides when editing starts. Editing can be started explicitly, once the polyline has been added to a map:
editor.startEditing(latlng);or by responding to a polyline click (the library does not register a click handler on its own):
editor.on("click", (event) => {
editor.startEditing(event.latlng);
});When the user clicks elsewhere on the map, an application can end the current editing session:
map.on("click", () => {
editor.endEditing();
});API
new PartiallyEditablePolyline(latlngs, options)
Creates a polyline in the same way as a normal Leaflet Polyline.
latlngs is the initial geometry; see setLatLngs(latlngs) for the accepted formats and the normalization applied to it.
options is passed through to Polyline, together with the following options.
editablePointRadius
The library is designed for large polylines. When editing starts, the nearest point to the supplied LatLng is selected, and only a local range of points around that point receives editing markers. editablePointRadius is the number of points to include on either side of the selected point; with the default of 100, up to 201 points may get markers.
The range is recalculated after an insertion or a deletion, around the inserted point or, after a deletion, around the point that took the deleted point's position (the last point if the last one was deleted). Moving a point does not change the range. In all cases, the range only determines which points receive editor markers; the application continues to own the complete geometry.
The value must be a non-negative integer, or Infinity to include all points (0 shows a marker for the selected point only, with no midpoint markers). Any other value makes startEditing() throw a RangeError, without firing editingerror.
This option sets the default for every editing session, and can be overridden for a single session by passing the same option to startEditing() (see startEditing(latlng, options)).
Default: 100
Example:
const editor = new PartiallyEditablePolyline(latlngs, {
editablePointRadius: 50,
});pointIcon
Leaflet icon used for existing editable points.
newPointIcon
Leaflet icon used for midpoint markers.
The default icons are provided by the library.
startEditing(latlng, options)
Starts an editing session around the point nearest to the supplied Leaflet LatLng.
editor.startEditing(latlng);The argument must be a Leaflet LatLng (an instance of the same Leaflet module's LatLng class). Anything else throws a TypeError; editingerror is not fired in this case.
The method does not accept a point index or a Leaflet event object. If the caller has a Leaflet event, pass its latlng property explicitly:
editor.startEditing(event.latlng);The second argument is optional. It is an options object that can override the editable range for this editing session:
editor.startEditing(latlng, { editablePointRadius: 20 });editablePointRadius replaces the value given to the constructor for this editing session only, including the ranges recalculated after an insertion or a deletion. The next session uses the constructor's value again unless it is overridden as well. An invalid value throws a RangeError; editingerror is not fired in this case.
If editing has been disabled with disableEditing(), startEditing() throws EditingDisabledError and fires the editingerror event.
If the polyline has not been added to a map, or has no points, the method does nothing and fires no event.
If an editing session is already active, it is ended first (editingend is fired) and then the new session starts (editingstart is fired).
EditingDisabledError is exported from the same module as PartiallyEditablePolyline.
endEditing()
Ends the current editing session.
If no editing session is active, this method does nothing and does not fire editingend.
editor.endEditing();getEditablePointRange()
Returns the inclusive point-index range for the existing points that are currently editable:
const range = editor.getEditablePointRange();
// { startIndex, endIndex }, or nullThe indices refer to the complete flat polyline. The method returns null
when editing is not active or there are no editable existing points. A new
object is returned on every call. The range may change after an insertion or
deletion as the library rebuilds the local editing window.
enableEditing()
Enables editing.
editor.enableEditing();disableEditing()
Disables editing.
If an editing session is active, it is ended immediately.
Subsequent calls to startEditing() throw EditingDisabledError.
editor.disableEditing();setLatLngs(latlngs)
Replaces the whole geometry. This is the only supported way to change the geometry from outside the library: when the application's own data changes, pass the new coordinates to this method.
PartiallyEditablePolyline retains the Polyline#setLatLngs() method, but this library supports flat geometry only.
When called by the application:
- An active editing session is ended.
- The coordinates are copied and normalized to latitude and longitude (see Elevation and
LatLng.alt), and the Polyline geometry is replaced with the copy. - The library's internal editing state is rebuilt from the new geometry.
The same normalization is applied to the coordinates passed to the constructor.
Each element may be a LatLng, a [lat, lng] array, or a { lat, lng } object. Nested coordinate arrays are not supported and cause an error to be thrown.
addLatLng()
Not supported. addLatLng() always throws an error.
The polyline held by the library is an editing copy of the application's geometry (see Design), and changing the copy independently of the application's data is not supported. Update the application's own data and call setLatLngs() instead.
Events
The library emits the following editing events.
editingstart
Fired when an editing session starts.
Payload:
{
index
}index is the global index of the selected point in the complete flat polyline.
The editable markers have already been built when this event is fired, so
getEditablePointRange() returns the new session's range in the handler.
editingend
Fired when an active editing session ends.
This happens when the session is ended by endEditing(), disableEditing() or setLatLngs(), when the layer is removed from the map, when startEditing() is called during an active session, or when the last remaining point is deleted.
No additional payload is provided.
pointchange
Fired once when an existing point has been moved and the drag operation has completed.
Payload:
{
index,
previousLatLng,
latlng
}index is the global index of the changed point.
The operation corresponds conceptually to:
latlngs[index] = latlng;The event is fired after the library's internal geometry has been updated.
previousLatLng and latlng are independent LatLng snapshots and are not references to the library's internal objects.
pointinsert
Fired once when a midpoint marker has been dragged to insert a new point.
Payload:
{
index,
latlng
}The operation corresponds conceptually to:
latlngs.splice(index, 0, latlng);The event is fired after the insertion has been applied.
The editable range and markers have also been rebuilt, so
getEditablePointRange() returns the post-insertion range in the handler.
The event represents the completed insertion. The midpoint drag does not generate an additional pointchange event.
pointdelete
Fired when an existing point is deleted.
Payload:
{
index,
latlng
}index is the global index of the point immediately before deletion.
The operation corresponds conceptually to:
latlngs.splice(index, 1);latlng is a snapshot of the deleted point.
The editable range and markers have already been rebuilt when this event is
fired, so getEditablePointRange() returns the post-deletion range in the
handler. When the last point is deleted it returns null, and editingend
follows the pointdelete event.
There is no lower limit on the number of points. When the last remaining point is deleted, pointdelete is followed by editingend.
editingerror
Fired when an editing operation cannot be started.
For example, attempting to call startEditing() while editing is disabled produces:
{
error
}where error is an EditingDisabledError.
The error is also thrown by startEditing().
Event Ordering and Indices
Editing events describe completed operations and are emitted after the corresponding internal geometry/state update.
Indices are global indices in the complete flat polyline, not indices relative to the currently editable range.
Consumers should process events in emission order.
For example, after a pointinsert event, the geometry has already been updated before any subsequent event is interpreted.
This means that subsequent indices refer to the geometry state resulting from the preceding event, just as they would after a JavaScript array operation such as splice().
Elevation and LatLng.alt
The library does not manage elevation. The geometry held by the library is an editing copy that contains latitude and longitude only: alt values passed to the constructor or to setLatLngs() are discarded, and getLatLngs() and the latlng / previousLatLng values in events never contain alt.
Applications should keep elevation and other point data themselves and update it using the index in editing events.
Interaction
Existing point markers are draggable. While a marker is being dragged, the other editor markers are hidden and dashed helper lines to the adjacent points are shown; the polyline itself is updated when the drag ends.
Midpoint markers between editable points can be dragged to insert a new point.
Existing points can be deleted through Leaflet's contextmenu interaction, such as the standard desktop right-click interaction. The contextmenu interaction on a midpoint marker does nothing.
The library does not define application-level map interaction. For example, an application may use a polyline click to start editing and a map click to end editing.
Polyline and editor marker pointer events are configured so that their interaction does not unintentionally bubble into the map's general pointer handling.
Data Model
The library operates on a single flat sequence of Leaflet LatLng objects:
[
LatLng,
LatLng,
LatLng,
// ...
]Each LatLng represents one point of the polyline and carries latitude and longitude only. The library treats this sequence as geometry and does not attach any application-specific meaning to individual points.
Applications that need to associate additional information with points or maintain application-specific data structures should manage that information separately and use the editing events to keep it in sync with the edited geometry.
Design
PartiallyEditablePolyline is implemented as a subclass of Leaflet's Polyline.
The polyline is an editing aid rather than the original data. It holds an editing copy of the geometry (latitude and longitude only), and the application remains the source of truth: editing results are reported through events, and the application applies them to its own data. When the application's data changes, it replaces the geometry with setLatLngs(); operations that would change the copy independently of the application's data, such as addLatLng(), are not supported.
The library deliberately keeps the editing state small:
- The Leaflet geometry of the polyline is an editing copy of the complete geometry, not shared with the application's data.
- The library keeps its own internal records of the points as its editing state.
- Editor markers are created only for the current local editing range.
- The application remains responsible for the semantic meaning and persistence of the data.
This separation allows the library to provide local editing without taking ownership of application-specific data structures.
Limitations
The current implementation has several intentional limitations:
- Leaflet v2 only
- Flat
LatLng[]geometry only LatLng.altis discardedaddLatLng()is not supported- Directly modifying the array returned by
getLatLngs(), or theLatLngobjects in it, is not supported - No build step; the package distributes the source ESM directly
- No compatibility layer for older Leaflet versions
- Editing (moving, inserting, and deleting points) is mouse- and touch-only; there is no keyboard-only way to perform these operations
The project is still under development, so the public API may evolve.
Acknowledgements
This project was inspired by and developed with reference to Leaflet.js Editable Polylines plugin by tkrajina.
If you are using Leaflet v1, that plugin may be a suitable alternative. This project is specifically designed for Leaflet v2
License
This project is released under the Zero-Clause BSD License (0BSD).
See LICENSE for the full license text.
