Consolidate note-drawing into Measure; add edit/delete
Measure already draws distance lines and area polygons by hand — a second click-to-draw UI in the notes tool (Point/Area mode, from the previous commit) duplicated that. Replaced it with a "Save as note" button on the Measure panel that saves whatever shape is currently drawn there; NotesControl goes back to being just a one-click point pin. Both share a single NoteSaveForm (notes.js) for the note-text + target-layer-file UI instead of two copies of it. gpx.js: the "lines" source now also renders a note-flagged LineString (dashed, popup-on-click) alongside the Polygon fill/outline from before — the plain track-line layer excludes category:"Note" features so a saved distance note doesn't also get the track hover-summary/ profile-panel treatment. Notes are now individually editable/deletable from their own popup (logged-in only): Edit swaps the popup content for an inline textarea, Delete confirms and removes. Both target the feature by a new client-generated properties.id rather than array position (new GpxStore#update_feature/#delete_feature, PUT/DELETE /api/gpx/notes), since neither array index nor the note's own text (the thing being edited) is a stable identifier. Verified live: Measure's Save button (disabled/enabled state, saves the exact on-screen shape), the popup's Edit/Delete buttons and inline edit form. Full test suite: 115 runs, 0 failures (new coverage for update_feature/delete_feature and the PUT/DELETE routes).
This commit is contained in:
1 parent
db58468187
commit
aafb4cb4a0
9 files changed
+499
-239
No files matched your search
+43
-9
@@ -1,4 +1,5 @@
|
||||
import { RULER_ICON } from "/js/icons.js";
|
||||
import { NoteSaveForm } from "/js/notes.js";
|
||||
|
||||
const SOURCE_ID = "measure";
|
||||
const LINE_LAYER_ID = "measure-line";
|
||||
@@ -29,24 +30,38 @@ export class MeasureControl {
|
||||
this._container.innerHTML = `
|
||||
<button type="button" class="measure-ctrl__button" title="Measure" aria-label="Measure">${RULER_ICON}</button>
|
||||
<div class="measure-panel" hidden>
|
||||
<button type="button" class="measure-panel__mode active" data-mode="distance">Distance</button>
|
||||
<button type="button" class="measure-panel__mode" data-mode="area">Area</button>
|
||||
<span class="measure-panel__result"></span>
|
||||
<button type="button" class="measure-panel__clear" title="Clear" aria-label="Clear">×</button>
|
||||
<div class="measure-panel__controls">
|
||||
<button type="button" class="measure-panel__mode active" data-mode="distance">Distance</button>
|
||||
<button type="button" class="measure-panel__mode" data-mode="area">Area</button>
|
||||
<span class="measure-panel__result"></span>
|
||||
<button type="button" class="measure-panel__save" title="Save as a note" aria-label="Save as a note" disabled>📍</button>
|
||||
<button type="button" class="measure-panel__clear" title="Clear" aria-label="Clear">×</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this._button = this._container.querySelector(".measure-ctrl__button");
|
||||
this._panel = this._container.querySelector(".measure-panel");
|
||||
this._controls = this._container.querySelector(".measure-panel__controls");
|
||||
this._result = this._container.querySelector(".measure-panel__result");
|
||||
this._saveButton = this._container.querySelector(".measure-panel__save");
|
||||
|
||||
this._noteForm = new NoteSaveForm(this._panel, {
|
||||
onSaved: () => {
|
||||
this._controls.hidden = false;
|
||||
this._clear();
|
||||
},
|
||||
onCancel: () => (this._controls.hidden = false),
|
||||
});
|
||||
|
||||
this._button.addEventListener("click", () => this._setActive(!this._active));
|
||||
this._panel.querySelectorAll(".measure-panel__mode").forEach((button) => {
|
||||
button.addEventListener("click", () => this._setMode(button.dataset.mode));
|
||||
});
|
||||
this._saveButton.addEventListener("click", () => this._saveAsNote());
|
||||
this._panel.querySelector(".measure-panel__clear").addEventListener("click", () => this._clear());
|
||||
|
||||
this._onClick = (event) => {
|
||||
if (!this._active) return;
|
||||
if (!this._active || this._noteForm.isOpen) return;
|
||||
this._points.push([event.lngLat.lng, event.lngLat.lat]);
|
||||
this._render();
|
||||
};
|
||||
@@ -79,10 +94,30 @@ export class MeasureControl {
|
||||
if (active) {
|
||||
this._addLayers();
|
||||
} else {
|
||||
this._noteForm.close();
|
||||
this._controls.hidden = false;
|
||||
this._clear();
|
||||
}
|
||||
}
|
||||
|
||||
// The same shape _render() draws — a distance line (2+ points) or a
|
||||
// closed area polygon (3+ points), or null while there isn't yet enough
|
||||
// to measure — extracted so "Save as note" saves exactly what's on
|
||||
// screen rather than recomputing it slightly differently.
|
||||
_currentShapeGeometry() {
|
||||
if (this._mode === "area") {
|
||||
return this._points.length >= 3 ? { type: "Polygon", coordinates: [[...this._points, this._points[0]]] } : null;
|
||||
}
|
||||
return this._points.length >= 2 ? { type: "LineString", coordinates: this._points } : null;
|
||||
}
|
||||
|
||||
_saveAsNote() {
|
||||
const geometry = this._currentShapeGeometry();
|
||||
if (!geometry) return;
|
||||
this._controls.hidden = true;
|
||||
this._noteForm.open(geometry);
|
||||
}
|
||||
|
||||
_setMode(mode) {
|
||||
this._mode = mode;
|
||||
this._panel.querySelectorAll(".measure-panel__mode").forEach((button) => {
|
||||
@@ -165,10 +200,9 @@ export class MeasureControl {
|
||||
properties: { role: "vertex" },
|
||||
geometry: { type: "Point", coordinates: coord },
|
||||
}));
|
||||
const shapeFeature =
|
||||
this._mode === "area"
|
||||
? this._points.length >= 3 && { type: "Feature", geometry: { type: "Polygon", coordinates: [[...this._points, this._points[0]]] } }
|
||||
: this._points.length >= 2 && { type: "Feature", geometry: { type: "LineString", coordinates: this._points } };
|
||||
const geometry = this._currentShapeGeometry();
|
||||
const shapeFeature = geometry && { type: "Feature", geometry };
|
||||
this._saveButton.disabled = !geometry;
|
||||
// Area mode has no equivalent yet — only asked for distance's own
|
||||
// per-leg labels, and a polygon's edges read differently (perimeter
|
||||
// segments, not a path) if this ever extends there.
|
||||
|
||||
Reference in new issue
Block a user