diff --git a/changelog.d/1446-draw-area-units.md b/changelog.d/1446-draw-area-units.md new file mode 100644 index 000000000..ef82d6628 --- /dev/null +++ b/changelog.d/1446-draw-area-units.md @@ -0,0 +1,3 @@ +- Show a drawn area of 9,999.5 m² or more as 1.0 ha, and of 999,500 m² or + more as 1.00 km². The draw hint picked the unit before rounding, so it read + "10000 m²" and "100.0 ha" (Aditya, @Rohilalala, #1446). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index bfdc06904..015807ec2 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -5438,6 +5438,7 @@ request viewbox and continues searching without that geographic preference. - Drawn-line distance labels select metres or kilometres after rounding to the nearest metre, so a 999.5-metre line displays as `1.0 km` rather than `1000 m`. - **Measurements and centres are computed on unwrapped longitudes** (`unwrapLongitudes` / `wrapLongitude` in `drawMode.js`, used by `ringAreaM2`, `ringCentroid` and the engine's manual-area anchor). A ~50,000 m² rectangle straddling the antimeridian reads its 0.002° width as 359.998° under raw arithmetic: it measured thousands of square kilometres and anchored on the Greenwich meridian, half a world from the shape. Path length was already safe — haversine is periodic in the longitude delta — and a test pins that so a "fix" cannot break it. - **Bounds.** One shape holds at most `MAX_VERTICES` = 512 points; two clicks within 0.5 m are one vertex (that is what makes a double-click finish without leaving a stray point); a vertex that is not a finite on-globe coordinate is refused. A shape that encloses nothing is refused rather than placed: three collinear points are not an area and a line under a metre long is not a line, and the hint says which. +- Drawn-area measures round before choosing m², ha or km², so 9,999.6 m² displays as `1.0 ha`, not `10000 m²`, and 999,960 m² as `1.00 km²`, not `100.0 ha`. - Pointer ownership: while a session is open the tool holds the single pointer claim (`claimPointer('draw')`, `src/data/inputOwnership.js`) and **every** ambient scene selection handler — the shared tracking click gesture behind flights, military and CCTV, plus vessels, satellites, launches, fires, installations, bikeshare, radio, submarine cables, the bundled GeoJSON layers and the CCTV calibration gizmo — returns before picking. A vertex over an aircraft is a vertex, not a selection. - **The claim is a LEASE.** `claimPointer(owner)` returns an opaque token or `null`; `releasePointer(lease)` takes that token. A second claim fails even under the same owner name, because two live instances of one tool — an old one mid-teardown and its replacement — are two owners; releasing by name let the old instance free the claim its successor was holding, and every layer started selecting through the live session's vertices. A superseded lease now releases nothing. - **Cesium's own click actions are borrowed too.** The Viewer binds a selecting LEFT_CLICK and a tracking LEFT_DOUBLE_CLICK on its own handler, outside every layer and therefore outside the shared claim — guarding the layers alone still left a drawn vertex selecting the entity under it. Both are removed for the session and restored, as the same function objects, when it ends or the tool is destroyed. diff --git a/src/annotations/drawMode.js b/src/annotations/drawMode.js index 538c2e5da..93f75dbdc 100644 --- a/src/annotations/drawMode.js +++ b/src/annotations/drawMode.js @@ -215,9 +215,12 @@ export function formatMeasure(session) { } if (session.shape === 'area') { const m2 = ringAreaM2(session.vertices); - if (m2 >= 1e6) return `${(m2 / 1e6).toFixed(2)} km²`; - if (m2 >= 1e4) return `${(m2 / 1e4).toFixed(1)} ha`; - return `${Math.round(m2)} m²`; + // Round before each unit cut, as for a line. 9,999.6 m² was "10000 m²". + const whole = Math.round(m2); + const ha = (m2 / 1e4).toFixed(1); + if (whole < 1e4) return `${whole} m²`; + if (Number(ha) < 100) return `${ha} ha`; + return `${(m2 / 1e6).toFixed(2)} km²`; } return ''; } diff --git a/src/annotations/drawMode.test.mjs b/src/annotations/drawMode.test.mjs index be9894dda..8018b4527 100644 --- a/src/annotations/drawMode.test.mjs +++ b/src/annotations/drawMode.test.mjs @@ -345,3 +345,43 @@ test('a line that rounds up to a kilometre reads 1.0 km, not 1000 m', () => { assert.equal(formatMeasure(line), '1.0 km'); assert.match(drawHint(line), /^1\.0 km · /); }); + +test('an area that rounds up to the next unit reads in that unit', () => { + // A square on the equator whose shoelace area is about `m2`. + const square = (m2) => { + const side = Math.sqrt(m2) / 111320; + const area = createDrawSession('area'); + for (const [lon, lat] of [ + [0, 0], + [side, 0], + [side, side], + [0, side], + ]) + addVertex(area, { lon, lat }); + return area; + }; + const almostHectare = square(9999.6); + const metres = ringAreaM2(almostHectare.vertices); + assert.ok( + metres >= 9999.5 && metres < 1e4, + `area should round to 10000 m², got ${metres}`, + ); + assert.equal(formatMeasure(almostHectare), '1.0 ha'); + assert.match(drawHint(almostHectare), /^1\.0 ha · /); + assert.equal(formatMeasure(square(9999.4)), '9999 m²'); + // Pin the shoelace area too, so a geodesy change fails here and not as a unit. + const almostKm2 = square(999_960); + const nearKm2 = ringAreaM2(almostKm2.vertices); + assert.ok( + nearKm2 >= 999_500 && nearKm2 < 1e6, + `area should round to 100.0 ha, got ${nearKm2}`, + ); + assert.equal(formatMeasure(almostKm2), '1.00 km²'); + const under = square(999_400); + const underM2 = ringAreaM2(under.vertices); + assert.ok( + underM2 >= 998_500 && underM2 < 999_500, + `area should round to 99.9 ha, got ${underM2}`, + ); + assert.equal(formatMeasure(under), '99.9 ha'); +});