Merge origin/main: Google Street View, KartaView and Panoramax on the slim Street Level

Main gained three imagery providers (Google Street View, KartaView,
Panoramax), shared line-coverage, cone and photo-viewer modules, one source
lit at a time and the translated interface. Their shared modules still
carried what this branch removes, so the merge ports them onto it:

- lineCoverage.js is this branch's coverage.js made provider-neutral
  (draped lines near the ground only, the selection drawn over them, key
  and rate-limit blocking); sequenceCones.js is sequences.js the same way.
  Mapillary, KartaView and Panoramax all build on them; Mapillary moves
  back to providers/mapillary/ beside the others.
- No ground casting, mesh sampling, surface modes, FOLLOW, legend, prewarm,
  orbit overview points (Panoramax's density dots included), stale-tile
  swaps or water-filled point budget come back.
- Kept from main because the new providers need them: requiresMapStack and
  attachMapStackController (Street View on Google 3D only), ground clicks,
  openNearest with an optional provider nearestImage, openImage {frame},
  reportError, the view-cone marker (now an SVG glyph), filters /
  coverageLabel / sinceBasis, OPEN STREET VIEW, keyAlternatives and i18n.
- photoViewer.js treats the viewer as expanded when its wrap is full screen
  or the iPhone popover, as EXPAND now is.
- The fixture browser gate keeps its 8 steps and adds one each for
  KartaView and Panoramax; catalogs, docs, boundaries and format scope
  follow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-19 13:51:45 +04:00
commit 6390a3346f
809 files changed
+81825 -6035

No files matched your search

+73 -3
View File
@@ -5,7 +5,7 @@ import { createSelection } from './selection.js';
import { createPickRouter } from './pickRouter.js';
/** The selection installed on a stand-in viewer, with what its clicks reach. */
function harness({ selected = false } = {}) {
function harness({ selected = false, services = {}, providers = [] } = {}) {
const saved = globalThis.document;
globalThis.document = new EventTarget();
// The document's keydown listeners, so a test can hand them any target.
@@ -26,6 +26,7 @@ function harness({ selected = false } = {}) {
};
const sequences = { selected, cleared: 0, selects: [] };
const opened = [];
const groundClicks = [];
/** What the pointer is over: a pick id, or nothing. */
const scene = { under: null };
const canvas = Object.assign(new EventTarget(), {
@@ -40,7 +41,7 @@ function harness({ selected = false } = {}) {
viewer,
enabled: true,
clickHandler: null,
services: { picking: null, input: null },
services: { picking: null, input: null, ...services },
providers: new Map([
[
'mapillary',
@@ -56,6 +57,7 @@ function harness({ selected = false } = {}) {
},
},
],
...providers,
]),
};
const parts = {
@@ -63,6 +65,7 @@ function harness({ selected = false } = {}) {
positionId: 'sl:pos',
}),
hasSelectedSequence: () => sequences.selected,
openAtGround: (position, picked) => groundClicks.push({ position, picked }),
clearSequences() {
sequences.cleared++;
sequences.selected = false;
@@ -76,8 +79,9 @@ function harness({ selected = false } = {}) {
state,
sequences,
opened,
groundClicks,
scene,
click: () => action('LEFT_CLICK')({ position: { x: 10, y: 10 } }),
click: (x = 10) => action('LEFT_CLICK')({ position: { x, y: 10 } }),
/** A keydown reaching the document from `target`; the event as handled. */
key(key, target, { defaultPrevented = false } = {}) {
const event = {
@@ -112,6 +116,11 @@ test('a click on a line selects its sequence, on a cone opens its image, elsewhe
h.click();
}
assert.deepEqual([h.sequences.selects, h.opened], [['abc'], ['42']]);
// The marker is ours; nothing and another layer's pick go to the ground.
assert.deepEqual(
h.groundClicks.map(({ picked }) => picked?.id),
[undefined, 'cctv:7'],
);
} finally {
h.restore();
}
@@ -170,3 +179,64 @@ test('Esc in a text field or rich-text editor keeps the selection (M61)', () =>
h.restore();
}
});
test('a click on nothing of ours goes to the ground handler with the pick, which decides', () => {
const h = harness();
try {
h.click(5);
h.scene.under = 'flight:abc';
h.click(6);
assert.deepEqual(
h.groundClicks.map(({ position, picked }) => [position.x, picked?.id]),
[
[5, undefined],
[6, 'flight:abc'],
],
);
} finally {
h.restore();
}
});
test('a click on a provider line hands it the ground under the click, measured only if asked', () => {
const handled = [];
const groundPicks = [];
const h = harness({
providers: [
[
'kartaview',
{
def: { id: 'kartaview', pickPrefix: 'kv:' },
instance: {
handlePick: (id, click) => handled.push({ id, click }),
},
},
],
],
services: {
scenePick: {
pickGroundPosition: (viewer, position) => {
groundPicks.push(position);
return Cesium.Cartesian3.fromDegrees(-83.0458, 42.3314, 190);
},
},
},
});
try {
h.scene.under = 'kv:line:14/1/2/3';
h.click(40);
assert.equal(handled.length, 1);
assert.equal(handled[0].id, 'kv:line:14/1/2/3');
assert.equal(
groundPicks.length,
0,
'no depth read until the provider asks',
);
const point = handled[0].click.groundPoint();
assert.ok(Math.abs(point.lon + 83.0458) < 1e-9);
assert.ok(Math.abs(point.lat - 42.3314) < 1e-9);
assert.deepEqual(groundPicks, [{ x: 40, y: 10 }]);
} finally {
h.restore();
}
});