mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix(voice): localize the area, imagery and OSM cards and tidy OSM Places
From review: - Card lines the actions write in English (area basis, part counts, merged duplicates, OSM caveats, the imagery window) are shown in the UI language through the analyst card's note path; the model still gets the English. One OSM place reads "1 hospital". - Same-named clarification choices show their region code (WA, DC). - OSM Places shows as unavailable once the server says no Overpass is configured, and its points are depth-tested past 2.5 km so they never show through the globe. - find_imagery puts the operator's sources and mode back when a box is refused, and a voice search cancelled mid-flight leaves "Search cancelled" instead of an empty strip. - The README lists OSM Places with the other Utilities.
This commit is contained in:
1 parent
34647758fc
commit
40d8839589
22 files changed
+535
-27
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { msg } from '../../i18n/marker.js';
|
||||
import { OSM_PLACES_LAYER_ID } from './model.js';
|
||||
import { createNominatimStatus } from '../../sources/nominatimStatus.js';
|
||||
export * from './model.js';
|
||||
|
||||
const OVERLAY_SOURCE_ID = OSM_PLACES_LAYER_ID;
|
||||
@@ -15,9 +16,13 @@ const COLOR = '#7fe3ff';
|
||||
* The search runs only on an operator-configured Overpass, so without one
|
||||
* the layer stays empty. Session-only: never in share links or stored layer
|
||||
* state.
|
||||
* @param {{overlayHost: object}} options
|
||||
* @param {{overlayHost: object, overpassConfigured?: () => Promise<boolean|null>}} options
|
||||
*/
|
||||
export function createOsmPlacesLayer({ overlayHost } = {}) {
|
||||
export function createOsmPlacesLayer({
|
||||
overlayHost,
|
||||
// The same once-per-page capability probe as Nominatim's, on Overpass.
|
||||
overpassConfigured = createNominatimStatus({ url: '/api/overpass/status' }),
|
||||
} = {}) {
|
||||
if (!overlayHost) throw new TypeError('OSM Places requires an overlay host');
|
||||
let viewer = null;
|
||||
let dataSource = null;
|
||||
@@ -25,6 +30,8 @@ export function createOsmPlacesLayer({ overlayHost } = {}) {
|
||||
let records = [];
|
||||
let meta = null;
|
||||
let lastUpdate = null;
|
||||
/** true / false from the server's probe, null while unknown. */
|
||||
let configured = null;
|
||||
|
||||
function render() {
|
||||
if (!dataSource) return;
|
||||
@@ -42,7 +49,9 @@ export function createOsmPlacesLayer({ overlayHost } = {}) {
|
||||
outlineColor: Cesium.Color.BLACK.withAlpha(0.8),
|
||||
outlineWidth: 2,
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||
// On top only up close (as bikeshare stations): beyond that the
|
||||
// depth test keeps far-side points from showing through the globe.
|
||||
disableDepthTestDistance: 2500,
|
||||
},
|
||||
});
|
||||
if (record.name && overlay.length < LABEL_LIMIT)
|
||||
@@ -79,8 +88,24 @@ export function createOsmPlacesLayer({ overlayHost } = {}) {
|
||||
source: 'OpenStreetMap',
|
||||
updateInterval: 0,
|
||||
|
||||
/** Unavailable only once the server says no Overpass is configured. */
|
||||
getSourceAvailability() {
|
||||
return configured === false
|
||||
? {
|
||||
available: false,
|
||||
source: OSM_PLACES_LAYER_ID,
|
||||
reason: 'Needs an operator-configured Overpass server',
|
||||
}
|
||||
: { available: true, source: OSM_PLACES_LAYER_ID, reason: null };
|
||||
},
|
||||
|
||||
init(nextViewer) {
|
||||
viewer = nextViewer;
|
||||
Promise.resolve(overpassConfigured?.())
|
||||
.then((answer) => {
|
||||
if (typeof answer === 'boolean') configured = answer;
|
||||
})
|
||||
.catch(() => {});
|
||||
dataSource = new Cesium.CustomDataSource(OSM_PLACES_LAYER_ID);
|
||||
dataSource.show = false;
|
||||
viewer.dataSources.add(dataSource);
|
||||
|
||||
@@ -100,3 +100,53 @@ test('destroy and re-init expose a fresh empty session', () => {
|
||||
statusMessage: 'Voice place search (needs an Overpass server)',
|
||||
});
|
||||
});
|
||||
|
||||
test('without an operator Overpass the layer shows as unavailable', async () => {
|
||||
const added = [];
|
||||
const viewer = { dataSources: { add: (ds) => added.push(ds), remove() {} } };
|
||||
const off = createOsmPlacesLayer({
|
||||
overlayHost: fakeOverlayHost(),
|
||||
overpassConfigured: async () => false,
|
||||
});
|
||||
assert.equal(
|
||||
off.getSourceAvailability().available,
|
||||
true,
|
||||
'unknown until asked',
|
||||
);
|
||||
off.init(viewer);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(off.getSourceAvailability().available, false);
|
||||
const unknown = createOsmPlacesLayer({
|
||||
overlayHost: fakeOverlayHost(),
|
||||
overpassConfigured: async () => null,
|
||||
});
|
||||
unknown.init(viewer);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(
|
||||
unknown.getSourceAvailability().available,
|
||||
true,
|
||||
'an unknown answer keeps it listed',
|
||||
);
|
||||
});
|
||||
|
||||
test('places are depth-tested past close range, so they never show through the globe', () => {
|
||||
const added = [];
|
||||
const viewer = { dataSources: { add: (ds) => added.push(ds), remove() {} } };
|
||||
const layer = createOsmPlacesLayer({
|
||||
overlayHost: fakeOverlayHost(),
|
||||
overpassConfigured: async () => true,
|
||||
});
|
||||
layer.init(viewer);
|
||||
layer.enable();
|
||||
layer.setResults(RECORDS, {
|
||||
kind: 'hospital',
|
||||
label: 'hospitals',
|
||||
area: 'Kathmandu',
|
||||
});
|
||||
const point = added[0].entities.values[0].point;
|
||||
const distance = point.disableDepthTestDistance.getValue();
|
||||
assert.ok(
|
||||
Number.isFinite(distance) && distance <= 5000,
|
||||
`depth test off only within ${distance} m`,
|
||||
);
|
||||
});
|
||||
@@ -1211,6 +1211,8 @@ export function createRecentImageryLayer({
|
||||
|
||||
/** Stop catalog/preview work owned by a cancelled voice action. */
|
||||
cancelPendingSearch() {
|
||||
// Without a note the emptied strip would read as "no imagery".
|
||||
if (_searching) _notice = msg('Search cancelled');
|
||||
cancelSearch();
|
||||
clearPending();
|
||||
syncRender();
|
||||
|
||||
@@ -1385,3 +1385,13 @@ test('with SELECT BOX armed, Escape clears the preview first and the next Escape
|
||||
assert.equal(f.snap().toolActive, false);
|
||||
await tool.destroy();
|
||||
});
|
||||
|
||||
test('a voice search cancelled mid-flight says so instead of looking empty', async () => {
|
||||
const f = fixture();
|
||||
f.layer.enable();
|
||||
f.layer.setBox(BOX);
|
||||
assert.equal(f.snap().searching, true);
|
||||
f.layer.cancelPendingSearch();
|
||||
assert.equal(f.snap().searching, false);
|
||||
assert.equal(f.snap().notice, 'Search cancelled');
|
||||
});
|
||||
Reference in new issue
Block a user