Fix overlapping Distance/Area text; label each segment's own length
The measure panel's mode buttons sit inside a maplibregl-ctrl-group
container (for the ruler button's own 29x29 sizing) — its global
"button" rule reaches them too, clipping "Distance"/"Area" into a
fixed 29px square with zero padding instead of sizing to fit the
text. Fixed by upping specificity (".measure-panel .measure-panel__mode")
rather than fighting the cascade with !important.
In distance mode, each segment between two clicked points now shows
its own length at its midpoint, not just the running total in the
panel — a symbol layer keyed off the existing source, filtered apart
from the vertex circles by a new "role" property.
Verified both in a real browser (panel no longer overlaps; two
638m segments rendered and labeled correctly summing to the panel's
1.28km total).
This commit is contained in:
1 parent
12635a48c3
commit
24b6180c03
3 files changed
+83
-6
No files matched your search
@@ -172,6 +172,29 @@ from an unmerged upstream branch and patched for a MapLibre API break
|
|||||||
(it targeted MapLibre ~2.4's public `map.transform`, since removed) —
|
(it targeted MapLibre ~2.4's public `map.transform`, since removed) —
|
||||||
see `docs/third-party-notices.adoc` for the exact patch.
|
see `docs/third-party-notices.adoc` for the exact patch.
|
||||||
|
|
||||||
|
=== Measuring distance and area
|
||||||
|
|
||||||
|
`MeasureControl` (`public/js/measure.js`) opens a small floating
|
||||||
|
panel next to its ruler button (mirroring NPS's own Area/Distance
|
||||||
|
ruler) with Distance/Area mode buttons, a live result, and Clear —
|
||||||
|
its buttons needed `.measure-panel <selector>` (not just the bare
|
||||||
|
class) to outrank `.maplibregl-ctrl-group button`'s own 29×29px/
|
||||||
|
zero-padding rule, which otherwise clips "Distance"/"Area" into
|
||||||
|
overlapping text (the container is a `maplibregl-ctrl-group` for the
|
||||||
|
ruler button's own sizing, and that rule reaches every button nested
|
||||||
|
inside it, panel included). Clicking the map adds a vertex; distance
|
||||||
|
sums haversine segment lengths, area uses the spherical-excess
|
||||||
|
polygon formula (Chamberlain & Duquette — the same one turf.js's
|
||||||
|
`area()` uses) rather than a flat shoelace, since a flat approximation
|
||||||
|
drifts at the scale a park boundary can reach. In distance mode, each
|
||||||
|
individual segment also gets its own label at its midpoint (a `symbol`
|
||||||
|
layer keyed off a `role: "segment-label"` property, filtered apart
|
||||||
|
from the draggable vertex points, which share the same source but
|
||||||
|
`role: "vertex"`) — the running total in the panel doesn't say how
|
||||||
|
long any one leg was on its own. Area mode has no equivalent yet;
|
||||||
|
a polygon's edges are its perimeter, not a path, so the same treatment
|
||||||
|
may not even read the same way there.
|
||||||
|
|
||||||
=== Debug inspector (Alt+click)
|
=== Debug inspector (Alt+click)
|
||||||
|
|
||||||
Alt+click anywhere on the map opens a popup listing every rendered
|
Alt+click anywhere on the map opens a popup listing every rendered
|
||||||
|
|||||||
+14
-2
@@ -537,7 +537,16 @@ html, body {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.measure-panel__mode {
|
/* ".measure-panel " prefix (not just ".measure-panel__mode") to outrank
|
||||||
|
".maplibregl-ctrl-group button" (higher specificity, since these buttons
|
||||||
|
sit inside a maplibregl-ctrl-group container for the ruler icon's own
|
||||||
|
sake) — its fixed 29x29 square + zero padding otherwise wins, clipping
|
||||||
|
"Distance"/"Area" into overlapping text instead of sizing to fit it. */
|
||||||
|
.measure-panel .measure-panel__mode {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -562,7 +571,10 @@ html, body {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.measure-panel__clear {
|
.measure-panel .measure-panel__clear {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
display: inline-block;
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
+46
-4
@@ -4,6 +4,7 @@ const SOURCE_ID = "measure";
|
|||||||
const LINE_LAYER_ID = "measure-line";
|
const LINE_LAYER_ID = "measure-line";
|
||||||
const FILL_LAYER_ID = "measure-fill";
|
const FILL_LAYER_ID = "measure-fill";
|
||||||
const POINT_LAYER_ID = "measure-points";
|
const POINT_LAYER_ID = "measure-points";
|
||||||
|
const SEGMENT_LABEL_LAYER_ID = "measure-segment-labels";
|
||||||
const EARTH_RADIUS_M = 6378137; // WGS84 equatorial radius, matches turf.js's area formula
|
const EARTH_RADIUS_M = 6378137; // WGS84 equatorial radius, matches turf.js's area formula
|
||||||
|
|
||||||
// A distance/area measuring tool, matching the ruler button in NPS's own
|
// A distance/area measuring tool, matching the ruler button in NPS's own
|
||||||
@@ -118,15 +119,31 @@ export class MeasureControl {
|
|||||||
id: POINT_LAYER_ID,
|
id: POINT_LAYER_ID,
|
||||||
type: "circle",
|
type: "circle",
|
||||||
source: SOURCE_ID,
|
source: SOURCE_ID,
|
||||||
filter: ["==", "$type", "Point"],
|
filter: ["all", ["==", "$type", "Point"], ["==", ["get", "role"], "vertex"]],
|
||||||
paint: { "circle-radius": 4, "circle-color": "#fff", "circle-stroke-color": "#1e5fbf", "circle-stroke-width": 2 },
|
paint: { "circle-radius": 4, "circle-color": "#fff", "circle-stroke-color": "#1e5fbf", "circle-stroke-width": 2 },
|
||||||
});
|
});
|
||||||
|
// Distance mode only (see _render) — each segment's own length, not
|
||||||
|
// just the running total, matching how a typical measure tool
|
||||||
|
// (Google Maps, QGIS) labels every leg of a multi-point line.
|
||||||
|
this._map.addLayer({
|
||||||
|
id: SEGMENT_LABEL_LAYER_ID,
|
||||||
|
type: "symbol",
|
||||||
|
source: SOURCE_ID,
|
||||||
|
filter: ["==", ["get", "role"], "segment-label"],
|
||||||
|
layout: {
|
||||||
|
"text-field": ["get", "label"],
|
||||||
|
"text-font": ["Noto Sans Regular"],
|
||||||
|
"text-size": 11,
|
||||||
|
"text-allow-overlap": true,
|
||||||
|
},
|
||||||
|
paint: { "text-color": "#1e5fbf", "text-halo-color": "#fff", "text-halo-width": 1.5 },
|
||||||
|
});
|
||||||
|
|
||||||
this._render();
|
this._render();
|
||||||
}
|
}
|
||||||
|
|
||||||
_removeLayers() {
|
_removeLayers() {
|
||||||
[FILL_LAYER_ID, LINE_LAYER_ID, POINT_LAYER_ID].forEach((id) => {
|
[FILL_LAYER_ID, LINE_LAYER_ID, POINT_LAYER_ID, SEGMENT_LABEL_LAYER_ID].forEach((id) => {
|
||||||
if (this._map.getLayer(id)) this._map.removeLayer(id);
|
if (this._map.getLayer(id)) this._map.removeLayer(id);
|
||||||
});
|
});
|
||||||
if (this._map.getSource(SOURCE_ID)) this._map.removeSource(SOURCE_ID);
|
if (this._map.getSource(SOURCE_ID)) this._map.removeSource(SOURCE_ID);
|
||||||
@@ -136,13 +153,31 @@ 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) => ({ type: "Feature", geometry: { type: "Point", coordinates: coord } }));
|
const pointFeatures = this._points.map((coord) => ({
|
||||||
|
type: "Feature",
|
||||||
|
properties: { role: "vertex" },
|
||||||
|
geometry: { type: "Point", coordinates: coord },
|
||||||
|
}));
|
||||||
const shapeFeature =
|
const shapeFeature =
|
||||||
this._mode === "area"
|
this._mode === "area"
|
||||||
? this._points.length >= 3 && { type: "Feature", geometry: { type: "Polygon", coordinates: [[...this._points, this._points[0]]] } }
|
? 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 } };
|
: this._points.length >= 2 && { type: "Feature", geometry: { type: "LineString", coordinates: this._points } };
|
||||||
|
// 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.
|
||||||
|
const segmentLabels =
|
||||||
|
this._mode === "distance"
|
||||||
|
? this._points.slice(1).map((point, i) => ({
|
||||||
|
type: "Feature",
|
||||||
|
properties: { role: "segment-label", label: formatDistance(haversine(this._points[i], point)) },
|
||||||
|
geometry: { type: "Point", coordinates: midpoint(this._points[i], point) },
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
source.setData({ type: "FeatureCollection", features: shapeFeature ? [shapeFeature, ...pointFeatures] : pointFeatures });
|
source.setData({
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: [...(shapeFeature ? [shapeFeature] : []), ...pointFeatures, ...segmentLabels],
|
||||||
|
});
|
||||||
|
|
||||||
this._result.textContent = this._mode === "area" ? formatArea(polygonArea(this._points)) : formatDistance(lineLength(this._points));
|
this._result.textContent = this._mode === "area" ? formatArea(polygonArea(this._points)) : formatDistance(lineLength(this._points));
|
||||||
}
|
}
|
||||||
@@ -166,6 +201,13 @@ function lineLength(points) {
|
|||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A plain coordinate average, not the geodesic midpoint — this only
|
||||||
|
// positions a text label, and the two differ by nothing worth computing a
|
||||||
|
// great-circle formula for at the segment lengths this tool measures.
|
||||||
|
function midpoint([lng1, lat1], [lng2, lat2]) {
|
||||||
|
return [(lng1 + lng2) / 2, (lat1 + lat2) / 2];
|
||||||
|
}
|
||||||
|
|
||||||
// Spherical polygon area (Chamberlain & Duquette, 2007) — the same formula
|
// Spherical polygon area (Chamberlain & Duquette, 2007) — the same formula
|
||||||
// turf.js's area() uses. A flat shoelace formula would drift noticeably at
|
// turf.js's area() uses. A flat shoelace formula would drift noticeably at
|
||||||
// the scale a park boundary can reach; this accounts for the sphere.
|
// the scale a park boundary can reach; this accounts for the sphere.
|
||||||
|
|||||||
Reference in new issue
Block a user