mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge pull request #1036 from rudycelekli/contrib/gods-015-imagery-box-missing-edge-20261011-reviewed
fix(recent-imagery): reject absent box edges instead of inventing zero degrees
This commit is contained in:
4 files changed
+75
-6
No files matched your search
@@ -36,6 +36,9 @@
|
|||||||
|
|
||||||
- Director timeline seeks cross the dateline along the short longitude arc.
|
- Director timeline seeks cross the dateline along the short longitude arc.
|
||||||
|
|
||||||
|
- Recent Imagery’s exported `setBox` and `searchHls` reject missing, blank or
|
||||||
|
boolean programmatic box edges instead of interpreting them as zero coordinates.
|
||||||
|
|
||||||
- Treat explicitly current, complete empty vessel coverage as a successful update,
|
- Treat explicitly current, complete empty vessel coverage as a successful update,
|
||||||
clearing obsolete contacts while retaining stale or incomplete snapshots.
|
clearing obsolete contacts while retaining stale or incomplete snapshots.
|
||||||
|
|
||||||
|
|||||||
@@ -4453,6 +4453,11 @@ are omitted rather than framing the wrong part of the globe.
|
|||||||
|
|
||||||
### Recent Imagery (September 2026)
|
### Recent Imagery (September 2026)
|
||||||
|
|
||||||
|
Programmatic boxes passed to the exported `setBox` and `searchHls` operations
|
||||||
|
reject missing, blank or boolean edges instead of interpreting them as zero
|
||||||
|
coordinates. Valid finite numeric edges remain accepted; the app’s drag tool,
|
||||||
|
pins and share links already supply complete boxes.
|
||||||
|
|
||||||
- The layer sits in the Data Layers panel's Cameras group beside Cameras (CCTV); Mapped ALPR Cameras moved to the Infrastructure group to make room.
|
- The layer sits in the Data Layers panel's Cameras group beside Cameras (CCTV); Mapped ALPR Cameras moved to the Infrastructure group to make room.
|
||||||
- DATA ▸ **Recent Imagery** (`recent-imagery`, `src/layers/recentImagery/`) lists the last 30 UTC days of imagery for a selected box and drapes the days you pick: HLS S30 (Sentinel-2) and L30 (Landsat 8/9) at 30 m, and the VIIRS NOAA-21 daily overview at 250 m. Keyless and browser-direct: NASA CMR for which days have HLS granules in the box (`catalog.js`, both collections concurrently, paged with `CMR-Search-After` up to 2,000 records each, `truncated` against CMR `hits`), GIBS WMTS for tiles, Worldview Snapshots for thumbnails and exports. Worldview's `Data-Present` header is the only availability signal: GIBS answers an empty tile with HTTP 200, and the JPEG overview would drape black, so a day confirmed empty never drapes.
|
- DATA ▸ **Recent Imagery** (`recent-imagery`, `src/layers/recentImagery/`) lists the last 30 UTC days of imagery for a selected box and drapes the days you pick: HLS S30 (Sentinel-2) and L30 (Landsat 8/9) at 30 m, and the VIIRS NOAA-21 daily overview at 250 m. Keyless and browser-direct: NASA CMR for which days have HLS granules in the box (`catalog.js`, both collections concurrently, paged with `CMR-Search-After` up to 2,000 records each, `truncated` against CMR `hits`), GIBS WMTS for tiles, Worldview Snapshots for thumbnails and exports. Worldview's `Data-Present` header is the only availability signal: GIBS answers an empty tile with HTTP 200, and the JPEG overview would drape black, so a day confirmed empty never drapes.
|
||||||
- **Box.** SELECT BOX (`src/ui/imageryBoxTool.js`) claims the pointer like Draw, borrows the stock click actions and freezes the camera only mid-drag; USE VIEW takes the camera rectangle; `boxFromPinAt` grows a 10 km box. Boxes must be ≤ 1,000 km a side, on one side of the dateline and inside the Web-Mercator limit (`validateBox`). A refusal (`boxError`, including the tool's own via `reportBoxRefusal`) stays at the top of the panel until a box succeeds; a view over the cap is refused with its size, never shrunk. An oversized box or view (`Box is … km wide · limit 1,000 km`, `View is … wide · …`) sets the hint to `Zoom in or draw a smaller box` and enables ZOOM IN: `zoomToFit()` flies top-down (1.2 s) to the refused box's centre, or the ground under the canvas centre for a view, at the height whose view is about 400 km wide for the canvas aspect and the camera's vertical FOV (`fitViewHeightM`, clamped 5–400 km), and returns that height.
|
- **Box.** SELECT BOX (`src/ui/imageryBoxTool.js`) claims the pointer like Draw, borrows the stock click actions and freezes the camera only mid-drag; USE VIEW takes the camera rectangle; `boxFromPinAt` grows a 10 km box. Boxes must be ≤ 1,000 km a side, on one side of the dateline and inside the Web-Mercator limit (`validateBox`). A refusal (`boxError`, including the tool's own via `reportBoxRefusal`) stays at the top of the panel until a box succeeds; a view over the cap is refused with its size, never shrunk. An oversized box or view (`Box is … km wide · limit 1,000 km`, `View is … wide · …`) sets the hint to `Zoom in or draw a smaller box` and enables ZOOM IN: `zoomToFit()` flies top-down (1.2 s) to the refused box's centre, or the ground under the canvas centre for a view, at the height whose view is about 400 km wide for the canvas aspect and the camera's vertical FOV (`fitViewHeightM`, clamped 5–400 km), and returns that height.
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
boxCentre,
|
||||||
|
boxFromRectangle,
|
||||||
|
dequantizeBox,
|
||||||
|
validateBox,
|
||||||
|
} from './model.js';
|
||||||
|
import { searchHls } from './catalog.js';
|
||||||
|
|
||||||
|
test('missing imagery edges do not turn into invented zero coordinates', () => {
|
||||||
|
for (const value of [null, undefined, '', ' ', false, true]) {
|
||||||
|
const box = { west: value, south: 0, east: 1, north: 1 };
|
||||||
|
assert.equal(validateBox(box).ok, false);
|
||||||
|
assert.equal(validateBox(box).reason, 'invalid');
|
||||||
|
assert.equal(boxCentre(box), null);
|
||||||
|
}
|
||||||
|
assert.equal(validateBox({ west: 0, south: 0, east: 1, north: 1 }).ok, true);
|
||||||
|
assert.equal(
|
||||||
|
validateBox({ west: '0', south: '0', east: '1', north: '1' }).ok,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every edge and every box entry point refuses a missing edge', async () => {
|
||||||
|
const blanks = [null, undefined, '', ' ', false, [], {}];
|
||||||
|
for (const edge of ['west', 'south', 'east', 'north']) {
|
||||||
|
for (const value of blanks) {
|
||||||
|
const box = { west: 1, south: 1, east: 2, north: 2, [edge]: value };
|
||||||
|
assert.equal(validateBox(box).ok, false, `${edge}=${String(value)}`);
|
||||||
|
// Share-link integers and Cesium radians are converted per edge.
|
||||||
|
assert.equal(dequantizeBox(box), null, `share ${edge}`);
|
||||||
|
assert.equal(boxFromRectangle(box), null, `radians ${edge}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let fetched = false;
|
||||||
|
await assert.rejects(
|
||||||
|
searchHls({
|
||||||
|
box: { west: ' ', south: 1, east: 2, north: 2 },
|
||||||
|
fetchImpl: async () => {
|
||||||
|
fetched = true;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
TypeError,
|
||||||
|
);
|
||||||
|
assert.equal(fetched, false);
|
||||||
|
assert.ok(dequantizeBox({ west: 0, south: 0, east: 100000, north: 100000 }));
|
||||||
|
});
|
||||||
@@ -75,6 +75,14 @@ const pad2 = (value) => String(value).padStart(2, '0');
|
|||||||
* east are not swapped: west > east crosses the dateline and `validateBox`
|
* east are not swapped: west > east crosses the dateline and `validateBox`
|
||||||
* rejects it. Null when any edge is nonfinite.
|
* rejects it. Null when any edge is nonfinite.
|
||||||
*/
|
*/
|
||||||
|
/** A box edge as a number, or null when it is missing, blank or not numeric. */
|
||||||
|
function edgeNumber(value) {
|
||||||
|
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
|
||||||
|
if (typeof value !== 'string' || !value.trim()) return null;
|
||||||
|
const number = Number(value);
|
||||||
|
return Number.isFinite(number) ? number : null;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeBox(box) {
|
function normalizeBox(box) {
|
||||||
if (!box || typeof box !== 'object') return null;
|
if (!box || typeof box !== 'object') return null;
|
||||||
const [west, south, east, north] = [
|
const [west, south, east, north] = [
|
||||||
@@ -82,8 +90,8 @@ function normalizeBox(box) {
|
|||||||
box.south,
|
box.south,
|
||||||
box.east,
|
box.east,
|
||||||
box.north,
|
box.north,
|
||||||
].map(Number);
|
].map(edgeNumber);
|
||||||
if (![west, south, east, north].every(Number.isFinite)) return null;
|
if ([west, south, east, north].some((edge) => edge == null)) return null;
|
||||||
return {
|
return {
|
||||||
west,
|
west,
|
||||||
south: Math.min(south, north),
|
south: Math.min(south, north),
|
||||||
@@ -211,11 +219,16 @@ export function boxFromPin(lon, lat, sideKm = PIN_BOX_SIDE_KM) {
|
|||||||
|
|
||||||
function mapBox(box, fn) {
|
function mapBox(box, fn) {
|
||||||
if (!box || typeof box !== 'object') return null;
|
if (!box || typeof box !== 'object') return null;
|
||||||
|
// Converting first would turn a blank edge into 0 before normalizeBox sees it.
|
||||||
|
const edge = (value) => {
|
||||||
|
const number = edgeNumber(value);
|
||||||
|
return number == null ? null : fn(number);
|
||||||
|
};
|
||||||
return normalizeBox({
|
return normalizeBox({
|
||||||
west: fn(box.west),
|
west: edge(box.west),
|
||||||
south: fn(box.south),
|
south: edge(box.south),
|
||||||
east: fn(box.east),
|
east: edge(box.east),
|
||||||
north: fn(box.north),
|
north: edge(box.north),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user