Make Measure's vertices draggable and deletable
Drag: mousedown on a vertex circle calls event.preventDefault() (the documented way to opt a gesture out of MapLibre's own drag-to-pan), then a map-wide mousemove/mouseup follows the cursor and updates that point's coordinates live. Delete: clicking a vertex selects it (shown larger/yellow); Delete or Backspace removes it, guarded against eating a keystroke meant for an actual text input on the page. A vertex's own mousedown sets a one-shot flag so the same click doesn't also fall through to the generic map-click handler and add a brand new point on top of it.
This commit is contained in:
1 parent
b5b91d0df5
commit
b0576f3725
2 files changed
+81
-3
No files matched your search
@@ -43,6 +43,8 @@ independence from connectivity.
|
|||||||
* [x] User overlay: offline drawing of custom points with notes (e.g. "turning space for a long vehicle", "narrow gate", "good swimming spot") — one click on the map, pick or name a `.geojson` layer, saved into the same GpxStore the Tracks panel already reads (so visibility/popup come for free)
|
* [x] User overlay: offline drawing of custom points with notes (e.g. "turning space for a long vehicle", "narrow gate", "good swimming spot") — one click on the map, pick or name a `.geojson` layer, saved into the same GpxStore the Tracks panel already reads (so visibility/popup come for free)
|
||||||
** [x] Save a distance/area shape as a note too — Measure already draws these by hand, so a "Save" button there (not a second click-to-draw UI in the notes tool) reuses that instead of duplicating it; both share one `NoteSaveForm` (notes.js). `gpx.js`'s `_addLayers` now renders Polygon and note-flagged LineString features (fill/outline, dashed line), not just points
|
** [x] Save a distance/area shape as a note too — Measure already draws these by hand, so a "Save" button there (not a second click-to-draw UI in the notes tool) reuses that instead of duplicating it; both share one `NoteSaveForm` (notes.js). `gpx.js`'s `_addLayers` now renders Polygon and note-flagged LineString features (fill/outline, dashed line), not just points
|
||||||
** [x] Edit/delete individual notes from their popup (logged-in only) — targets a feature by a client-generated `properties.id` (`GpxStore#update_feature`/`#delete_feature`), not array position
|
** [x] Edit/delete individual notes from their popup (logged-in only) — targets a feature by a client-generated `properties.id` (`GpxStore#update_feature`/`#delete_feature`), not array position
|
||||||
|
** [x] Measure's vertices are draggable (mousedown + move, `event.preventDefault()` to stop the map's own drag-pan) and deletable (click to select — shown in yellow — then Delete/Backspace)
|
||||||
|
** [x] Gated behind login: NotesControl isn't added to the map at all when logged out, Measure's "Save as note" button is hidden (Measure itself stays usable either way)
|
||||||
** [ ] The Unigrid Matrix category filter only re-slices the *points* source (`_applyCategoryFilter`) — toggling the "Note" category off hides point notes but not polygon/line ones sharing that category
|
** [ ] The Unigrid Matrix category filter only re-slices the *points* source (`_applyCategoryFilter`) — toggling the "Note" category off hides point notes but not polygon/line ones sharing that category
|
||||||
* [ ] Offline satellite tile caching (e.g. ESRI) for a selected route area — key for judging surface quality and obstacles when wild camping
|
* [ ] Offline satellite tile caching (e.g. ESRI) for a selected route area — key for judging surface quality and obstacles when wild camping
|
||||||
|
|
||||||
|
|||||||
+79
-3
@@ -21,6 +21,9 @@ export class MeasureControl {
|
|||||||
this._mode = "distance";
|
this._mode = "distance";
|
||||||
this._active = false;
|
this._active = false;
|
||||||
this._points = [];
|
this._points = [];
|
||||||
|
this._selectedIndex = null; // which vertex Delete/Backspace acts on
|
||||||
|
this._draggingIndex = null;
|
||||||
|
this._suppressNextClick = false; // a mousedown on a vertex already handled this click; don't also add a new point
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(map) {
|
onAdd(map) {
|
||||||
@@ -67,11 +70,72 @@ export class MeasureControl {
|
|||||||
|
|
||||||
this._onClick = (event) => {
|
this._onClick = (event) => {
|
||||||
if (!this._active || this._noteForm.isOpen) return;
|
if (!this._active || this._noteForm.isOpen) return;
|
||||||
|
// A mousedown on an existing vertex (below) already selected/started
|
||||||
|
// dragging it — without this, that same click would also land here
|
||||||
|
// and add a brand new point right on top of it.
|
||||||
|
if (this._suppressNextClick) {
|
||||||
|
this._suppressNextClick = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._selectedIndex = null;
|
||||||
this._points.push([event.lngLat.lng, event.lngLat.lat]);
|
this._points.push([event.lngLat.lng, event.lngLat.lat]);
|
||||||
this._render();
|
this._render();
|
||||||
};
|
};
|
||||||
map.on("click", this._onClick);
|
map.on("click", this._onClick);
|
||||||
|
|
||||||
|
// Vertex drag: mousedown on a point starts it, a map-wide mousemove
|
||||||
|
// follows the cursor while dragging, mouseup anywhere ends it — not
|
||||||
|
// scoped to the layer since the cursor is very likely to leave the
|
||||||
|
// (few-pixel) vertex circle mid-drag. preventDefault on the mousedown
|
||||||
|
// is what stops MapLibre's own drag-to-pan from also engaging for the
|
||||||
|
// same gesture (its documented way to opt out per-interaction).
|
||||||
|
this._onVertexMouseDown = (event) => {
|
||||||
|
if (!this._active || this._noteForm.isOpen) return;
|
||||||
|
const index = event.features[0]?.properties?.index;
|
||||||
|
if (index === undefined) return;
|
||||||
|
event.preventDefault();
|
||||||
|
this._draggingIndex = index;
|
||||||
|
this._selectedIndex = index;
|
||||||
|
this._suppressNextClick = true;
|
||||||
|
this._map.getCanvas().style.cursor = "grabbing";
|
||||||
|
this._render();
|
||||||
|
};
|
||||||
|
map.on("mousedown", POINT_LAYER_ID, this._onVertexMouseDown);
|
||||||
|
|
||||||
|
this._onMouseMove = (event) => {
|
||||||
|
if (this._draggingIndex === null) return;
|
||||||
|
this._points[this._draggingIndex] = [event.lngLat.lng, event.lngLat.lat];
|
||||||
|
this._render();
|
||||||
|
};
|
||||||
|
map.on("mousemove", this._onMouseMove);
|
||||||
|
|
||||||
|
this._onMouseUp = () => {
|
||||||
|
if (this._draggingIndex === null) return;
|
||||||
|
this._draggingIndex = null;
|
||||||
|
this._map.getCanvas().style.cursor = this._active ? "crosshair" : "";
|
||||||
|
};
|
||||||
|
map.on("mouseup", this._onMouseUp);
|
||||||
|
|
||||||
|
map.on("mouseenter", POINT_LAYER_ID, () => {
|
||||||
|
if (this._draggingIndex === null) this._map.getCanvas().style.cursor = "grab";
|
||||||
|
});
|
||||||
|
map.on("mouseleave", POINT_LAYER_ID, () => {
|
||||||
|
if (this._draggingIndex === null) this._map.getCanvas().style.cursor = this._active ? "crosshair" : "";
|
||||||
|
});
|
||||||
|
|
||||||
|
this._onKeyDown = (event) => {
|
||||||
|
if (!this._active || this._selectedIndex === null) return;
|
||||||
|
if (event.key !== "Delete" && event.key !== "Backspace") return;
|
||||||
|
// Don't eat Backspace while the note-text form (or anything else on
|
||||||
|
// the page) actually has focus for typing.
|
||||||
|
if (["INPUT", "TEXTAREA"].includes(document.activeElement?.tagName)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
this._points.splice(this._selectedIndex, 1);
|
||||||
|
this._selectedIndex = null;
|
||||||
|
this._render();
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", this._onKeyDown);
|
||||||
|
|
||||||
// setStyle() (switching Online/Offline) tears down all sources/layers —
|
// setStyle() (switching Online/Offline) tears down all sources/layers —
|
||||||
// same re-add-on-reload pattern as radar.js/gpx.js.
|
// same re-add-on-reload pattern as radar.js/gpx.js.
|
||||||
map.on("style.load", () => {
|
map.on("style.load", () => {
|
||||||
@@ -83,6 +147,10 @@ export class MeasureControl {
|
|||||||
|
|
||||||
onRemove() {
|
onRemove() {
|
||||||
this._map.off("click", this._onClick);
|
this._map.off("click", this._onClick);
|
||||||
|
this._map.off("mousedown", POINT_LAYER_ID, this._onVertexMouseDown);
|
||||||
|
this._map.off("mousemove", this._onMouseMove);
|
||||||
|
this._map.off("mouseup", this._onMouseUp);
|
||||||
|
document.removeEventListener("keydown", this._onKeyDown);
|
||||||
this._removeLayers();
|
this._removeLayers();
|
||||||
this._container.remove();
|
this._container.remove();
|
||||||
}
|
}
|
||||||
@@ -129,11 +197,14 @@ export class MeasureControl {
|
|||||||
button.classList.toggle("active", button.dataset.mode === mode);
|
button.classList.toggle("active", button.dataset.mode === mode);
|
||||||
});
|
});
|
||||||
this._points = [];
|
this._points = [];
|
||||||
|
this._selectedIndex = null;
|
||||||
this._render();
|
this._render();
|
||||||
}
|
}
|
||||||
|
|
||||||
_clear() {
|
_clear() {
|
||||||
this._points = [];
|
this._points = [];
|
||||||
|
this._selectedIndex = null;
|
||||||
|
this._draggingIndex = null;
|
||||||
this._render();
|
this._render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +238,12 @@ export class MeasureControl {
|
|||||||
type: "circle",
|
type: "circle",
|
||||||
source: SOURCE_ID,
|
source: SOURCE_ID,
|
||||||
filter: ["==", ["get", "role"], "vertex"],
|
filter: ["==", ["get", "role"], "vertex"],
|
||||||
paint: { "circle-radius": 4, "circle-color": "#fff", "circle-stroke-color": "#1e5fbf", "circle-stroke-width": 2 },
|
paint: {
|
||||||
|
"circle-radius": ["case", ["get", "selected"], 6, 4],
|
||||||
|
"circle-color": ["case", ["get", "selected"], "#ffd43b", "#fff"],
|
||||||
|
"circle-stroke-color": "#1e5fbf",
|
||||||
|
"circle-stroke-width": 2,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
// Distance mode only (see _render) — each segment's own length, not
|
// Distance mode only (see _render) — each segment's own length, not
|
||||||
// just the running total, matching how a typical measure tool
|
// just the running total, matching how a typical measure tool
|
||||||
@@ -200,9 +276,9 @@ export class MeasureControl {
|
|||||||
const source = this._map.getSource(SOURCE_ID);
|
const source = this._map.getSource(SOURCE_ID);
|
||||||
if (!source) return;
|
if (!source) return;
|
||||||
|
|
||||||
const pointFeatures = this._points.map((coord) => ({
|
const pointFeatures = this._points.map((coord, index) => ({
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
properties: { role: "vertex" },
|
properties: { role: "vertex", index, selected: index === this._selectedIndex },
|
||||||
geometry: { type: "Point", coordinates: coord },
|
geometry: { type: "Point", coordinates: coord },
|
||||||
}));
|
}));
|
||||||
const geometry = this._currentShapeGeometry();
|
const geometry = this._currentShapeGeometry();
|
||||||
|
|||||||
Reference in new issue
Block a user