mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
Allow optional Street Level provider composition
This commit is contained in:
1 parent
6be25595b1
commit
940b277460
10 files changed
+130
-11
No files matched your search
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
- Street Level providers are optional in application composition. Omitting a
|
||||||
|
Mapillary source no longer prevents startup; callers can inject a different
|
||||||
|
provider list or an empty list without changing layer identities or share links.
|
||||||
|
|
||||||
- Vector tile sources take their allowed tile origin from the configured
|
- Vector tile sources take their allowed tile origin from the configured
|
||||||
`tileJsonUrl`, so repointing only that URL at a mirror loads tiles from the
|
`tileJsonUrl`, so repointing only that URL at a mirror loads tiles from the
|
||||||
mirror or fails visibly instead of silently using OpenFreeMap. Thanks to
|
mirror or fails visibly instead of silently using OpenFreeMap. Thanks to
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# God's Eye View Current State
|
# God's Eye View Current State
|
||||||
|
|
||||||
|
Application catalog composition accepts an optional `streetLevelProviders` list.
|
||||||
|
An explicit list replaces the default Mapillary provider; an empty list removes
|
||||||
|
all imagery providers. Without that option, Mapillary is registered only when
|
||||||
|
`sources.mapillary` is supplied. An absent source leaves Street Level registered
|
||||||
|
with a truthful no-provider state, preserving layer controls and share tokens.
|
||||||
|
Malformed supplied sources and provider definitions still fail validation.
|
||||||
|
|
||||||
## God's Eye View in conversations — October 2, 2026
|
## God's Eye View in conversations — October 2, 2026
|
||||||
|
|
||||||
Tool answers that can be shown in God's Eye View include a view: camera, layers,
|
Tool answers that can be shown in God's Eye View include a view: camera, layers,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { createWeatherLayer } from '../layers/weather/index.js';
|
|||||||
import { createCyclonesLayer } from '../layers/cyclones/index.js';
|
import { createCyclonesLayer } from '../layers/cyclones/index.js';
|
||||||
import { createWindLayer } from '../layers/wind/index.js';
|
import { createWindLayer } from '../layers/wind/index.js';
|
||||||
import { createLayerCatalog } from './catalog.js';
|
import { createLayerCatalog } from './catalog.js';
|
||||||
import { MAPILLARY_SOURCE_METHODS } from '../layers/streetLevel/providers/mapillary/policy.js';
|
|
||||||
import { LAYER_STATE_REGISTRY } from '../data/layerState.js';
|
import { LAYER_STATE_REGISTRY } from '../data/layerState.js';
|
||||||
import { createMilitaryRegistry } from '../layers/aircraft/classification.js';
|
import { createMilitaryRegistry } from '../layers/aircraft/classification.js';
|
||||||
import { createApplicationFlights } from './layers/flights.js';
|
import { createApplicationFlights } from './layers/flights.js';
|
||||||
@@ -57,7 +56,6 @@ const SOURCE_METHODS = Object.freeze({
|
|||||||
earthquakes: ['getSnapshot'],
|
earthquakes: ['getSnapshot'],
|
||||||
'fire-perimeters': ['getSnapshot'],
|
'fire-perimeters': ['getSnapshot'],
|
||||||
cables: ['fetch'],
|
cables: ['fetch'],
|
||||||
mapillary: MAPILLARY_SOURCE_METHODS,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,6 +84,7 @@ export function createApplicationCatalog({
|
|||||||
vesselOptions,
|
vesselOptions,
|
||||||
resolveAsset,
|
resolveAsset,
|
||||||
nepalBoundaryResolver,
|
nepalBoundaryResolver,
|
||||||
|
streetLevelProviders,
|
||||||
}) {
|
}) {
|
||||||
if (!signal?.addEventListener)
|
if (!signal?.addEventListener)
|
||||||
throw new TypeError('An application lifetime signal is required');
|
throw new TypeError('An application lifetime signal is required');
|
||||||
@@ -154,6 +153,7 @@ export function createApplicationCatalog({
|
|||||||
createApplicationStreetLevel({
|
createApplicationStreetLevel({
|
||||||
surface,
|
surface,
|
||||||
sources: { mapillary: sources.mapillary },
|
sources: { mapillary: sources.mapillary },
|
||||||
|
providers: streetLevelProviders,
|
||||||
}),
|
}),
|
||||||
satellites,
|
satellites,
|
||||||
createApplicationLaunches({ source: sources.launches, satellites }),
|
createApplicationLaunches({ source: sources.launches, satellites }),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { createApplicationCatalog } from './constructCatalog.js';
|
|||||||
import { createStandaloneLayerSources } from '../standalone/layerSources.js';
|
import { createStandaloneLayerSources } from '../standalone/layerSources.js';
|
||||||
import { catalogControlServices } from './catalog.js';
|
import { catalogControlServices } from './catalog.js';
|
||||||
import { LayerLifecycle } from '../data/lifecycle.js';
|
import { LayerLifecycle } from '../data/lifecycle.js';
|
||||||
|
import { fakeStreetLevelProvider } from '../testSupport/streetLevelFakes.mjs';
|
||||||
|
|
||||||
function fixtureSources(ids, calls) {
|
function fixtureSources(ids, calls) {
|
||||||
const sources = createStandaloneLayerSources();
|
const sources = createStandaloneLayerSources();
|
||||||
@@ -140,3 +141,53 @@ function fixtureSurface(signal) {
|
|||||||
eventTarget: null,
|
eventTarget: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('Mapillary can be omitted or replaced without changing catalog membership', (t) => {
|
||||||
|
const lifetime = new AbortController();
|
||||||
|
t.after(() => lifetime.abort());
|
||||||
|
const sources = fixtureSources([], []);
|
||||||
|
delete sources.mapillary;
|
||||||
|
const options = {
|
||||||
|
sources,
|
||||||
|
signal: lifetime.signal,
|
||||||
|
surface: fixtureSurface(lifetime.signal),
|
||||||
|
};
|
||||||
|
const catalog = createApplicationCatalog(options);
|
||||||
|
assert.equal(catalog.layers.length, 30);
|
||||||
|
assert.deepEqual(catalog.get('street-level').providerIds, []);
|
||||||
|
assert.equal(
|
||||||
|
catalogControlServices(catalog).streetLevelLayer,
|
||||||
|
catalog.get('street-level'),
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
catalog.get('street-level').getUIState().coverage.hint,
|
||||||
|
/No street-level imagery providers/,
|
||||||
|
);
|
||||||
|
const provider = fakeStreetLevelProvider({
|
||||||
|
id: 'example',
|
||||||
|
pickPrefix: 'example:',
|
||||||
|
});
|
||||||
|
const replacement = createApplicationCatalog({
|
||||||
|
...options,
|
||||||
|
streetLevelProviders: [provider],
|
||||||
|
});
|
||||||
|
assert.deepEqual(replacement.get('street-level').providerIds, ['example']);
|
||||||
|
assert.deepEqual(replacement.metadata, catalog.metadata);
|
||||||
|
assert.equal(provider.calls.activate, 0, 'construction must not acquire');
|
||||||
|
assert.deepEqual(
|
||||||
|
createApplicationCatalog({
|
||||||
|
...options,
|
||||||
|
sources: fixtureSources([], []),
|
||||||
|
streetLevelProviders: [],
|
||||||
|
}).get('street-level').providerIds,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
createApplicationCatalog({
|
||||||
|
...options,
|
||||||
|
sources: { ...sources, mapillary: {} },
|
||||||
|
}),
|
||||||
|
/Mapillary source/,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -5,10 +5,16 @@ import * as picking from '../../data/pickRegistry.js';
|
|||||||
import * as input from '../../data/inputOwnership.js';
|
import * as input from '../../data/inputOwnership.js';
|
||||||
import * as render from '../../renderGovernor.js';
|
import * as render from '../../renderGovernor.js';
|
||||||
|
|
||||||
/** A new provider registers here; its chip, credit and share bit follow. */
|
/** Inject providers, or use Mapillary only when its source is supplied. */
|
||||||
export function createApplicationStreetLevel({ surface, sources }) {
|
export function createApplicationStreetLevel({
|
||||||
|
surface,
|
||||||
|
sources = {},
|
||||||
|
providers = sources.mapillary == null
|
||||||
|
? []
|
||||||
|
: [createMapillaryProvider({ source: sources.mapillary })],
|
||||||
|
}) {
|
||||||
return createStreetLevelLayer({
|
return createStreetLevelLayer({
|
||||||
providers: [createMapillaryProvider({ source: sources.mapillary })],
|
providers,
|
||||||
services: {
|
services: {
|
||||||
sprites,
|
sprites,
|
||||||
picking,
|
picking,
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createApplicationStreetLevel } from './streetLevel.js';
|
||||||
|
import { fakeStreetLevelProvider } from '../../testSupport/streetLevelFakes.mjs';
|
||||||
|
|
||||||
|
test('explicit providers replace the default source and an empty list removes it', () => {
|
||||||
|
const sources = { mapillary: {} };
|
||||||
|
const provider = fakeStreetLevelProvider({
|
||||||
|
id: 'example',
|
||||||
|
pickPrefix: 'example:',
|
||||||
|
});
|
||||||
|
const layer = createApplicationStreetLevel({
|
||||||
|
sources,
|
||||||
|
providers: [provider],
|
||||||
|
});
|
||||||
|
assert.deepEqual(layer.providerIds, ['example']);
|
||||||
|
assert.deepEqual(
|
||||||
|
createApplicationStreetLevel({ sources, providers: [] }).providerIds,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => createApplicationStreetLevel({ sources }),
|
||||||
|
/Mapillary source/,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => createApplicationStreetLevel({ providers: null }),
|
||||||
|
/array/,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -482,3 +482,17 @@ test('switching a provider on while the layer is off activates nothing (M01)', a
|
|||||||
assert.equal(provider.calls.activate, 1);
|
assert.equal(provider.calls.activate, 1);
|
||||||
assert.equal(viewer.credits.length, 1);
|
assert.equal(viewer.credits.length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('an empty provider composition enables and tears down without acquiring imagery', async (t) => {
|
||||||
|
const { layer, viewer } = await enabledLayer(t, []);
|
||||||
|
assert.deepEqual(layer.providerIds, []);
|
||||||
|
assert.deepEqual(layer.getUIState().providers, []);
|
||||||
|
assert.equal(layer.getUIState().keyRequired, false);
|
||||||
|
assert.match(
|
||||||
|
layer.getUIState().coverage.hint,
|
||||||
|
/No street-level imagery providers/,
|
||||||
|
);
|
||||||
|
layer.disable();
|
||||||
|
layer.enable(viewer);
|
||||||
|
assert.deepEqual(layer.getUIState().providers, []);
|
||||||
|
});
|
||||||
@@ -86,8 +86,8 @@ const ID_GRAMMAR = /^[a-z][a-z0-9-]*$/;
|
|||||||
* @returns {ReadonlyArray<StreetLevelProvider>}
|
* @returns {ReadonlyArray<StreetLevelProvider>}
|
||||||
*/
|
*/
|
||||||
export function validateProviders(providers) {
|
export function validateProviders(providers) {
|
||||||
if (!Array.isArray(providers) || providers.length === 0)
|
if (!Array.isArray(providers))
|
||||||
throw new TypeError('Street Level needs at least one imagery provider');
|
throw new TypeError('Street Level providers must be an array');
|
||||||
const ids = new Set();
|
const ids = new Set();
|
||||||
const prefixes = [];
|
const prefixes = [];
|
||||||
for (const provider of providers) {
|
for (const provider of providers) {
|
||||||
|
|||||||
@@ -26,9 +26,14 @@ test('a valid list is frozen in registration order', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('an empty list, duplicate ids and malformed ids are rejected', () => {
|
test('an empty provider list is valid and has no key requirement', () => {
|
||||||
assert.throws(() => validateProviders([]), /at least one/);
|
assert.deepEqual(validateProviders([]), []);
|
||||||
assert.throws(() => validateProviders(null), /at least one/);
|
assert.ok(Object.isFrozen(validateProviders([])));
|
||||||
|
assert.equal(requiresKeyIdFor([]), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-arrays, duplicate ids and malformed ids are rejected', () => {
|
||||||
|
assert.throws(() => validateProviders(null), /array/);
|
||||||
assert.throws(() => validateProviders([def(), def()]), /Duplicate/);
|
assert.throws(() => validateProviders([def(), def()]), /Duplicate/);
|
||||||
assert.throws(() => validateProviders([def({ id: 'Bad Id' })]), /slug/);
|
assert.throws(() => validateProviders([def({ id: 'Bad Id' })]), /slug/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ export function summarizeCoverage(providers) {
|
|||||||
return {
|
return {
|
||||||
count: active.reduce((sum, p) => sum + (p.count || 0), 0),
|
count: active.reduce((sum, p) => sum + (p.count || 0), 0),
|
||||||
loading: active.some((p) => p.loading),
|
loading: active.some((p) => p.loading),
|
||||||
hint: active.find((p) => p.hint)?.hint || '',
|
hint:
|
||||||
|
providers.length === 0
|
||||||
|
? 'No street-level imagery providers configured'
|
||||||
|
: active.find((p) => p.hint)?.hint || '',
|
||||||
error: active.find((p) => p.error)?.error || null,
|
error: active.find((p) => p.error)?.error || null,
|
||||||
keyRequired:
|
keyRequired:
|
||||||
active.length > 0 && active.every((p) => p.keyRequired === true),
|
active.length > 0 && active.every((p) => p.keyRequired === true),
|
||||||
|
|||||||
Reference in new issue
Block a user