mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +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
@@ -268,6 +268,8 @@ export async function findImagery(
|
||||
: args.source === 'any'
|
||||
? { hls: true, viirs: true }
|
||||
: { hls: true };
|
||||
// The operator's own settings, put back if the box is refused.
|
||||
const before = layer.getSnapshot();
|
||||
layer.setSources(sources);
|
||||
layer.setMode?.('image');
|
||||
|
||||
@@ -279,6 +281,8 @@ export async function findImagery(
|
||||
if (!accepted) {
|
||||
const reason =
|
||||
layer.getSnapshot().boxError || 'That area cannot be searched.';
|
||||
if (before?.sources) layer.setSources(before.sources);
|
||||
if (before?.mode) layer.setMode?.(before.mode);
|
||||
return fail('BOX_REFUSED', `${reason}. Zoom in or pick a smaller area.`, {
|
||||
...(areaName ? { area: areaName } : {}),
|
||||
});
|
||||
|
||||
@@ -47,6 +47,8 @@ function fakeLayer({
|
||||
pins: { a: { key: null } },
|
||||
shown: { a: null },
|
||||
notes: [],
|
||||
sources: { hls: false, viirs: true },
|
||||
mode: 'basemap',
|
||||
};
|
||||
const listeners = new Set();
|
||||
const calls = [];
|
||||
@@ -92,9 +94,11 @@ function fakeLayer({
|
||||
},
|
||||
setSources(sources) {
|
||||
calls.push(['setSources', sources]);
|
||||
set({ sources: { ...sources } });
|
||||
},
|
||||
setMode(mode) {
|
||||
calls.push(['setMode', mode]);
|
||||
set({ mode });
|
||||
},
|
||||
setAssignment(slot, key) {
|
||||
calls.push(['setAssignment', slot, key]);
|
||||
@@ -271,6 +275,15 @@ test('a box the layer refuses is refused with its reason, and the layer is turne
|
||||
assert.deepEqual(toggles, [true]);
|
||||
});
|
||||
|
||||
test('a refused box leaves the operator panel sources and mode as they were', async () => {
|
||||
const layer = fakeLayer({ refuse: 'Box is 1,200 km wide · limit 1,000 km' });
|
||||
const result = await findImagery(deps(layer).value, {});
|
||||
assert.equal(result.code, 'BOX_REFUSED');
|
||||
const snap = layer.getSnapshot();
|
||||
assert.deepEqual(snap.sources, { hls: false, viirs: true });
|
||||
assert.equal(snap.mode, 'basemap');
|
||||
});
|
||||
|
||||
test('a superseded search is cancelled before anything is pinned', async () => {
|
||||
const layer = fakeLayer();
|
||||
let current = true;
|
||||
|
||||
@@ -211,6 +211,7 @@ export async function osmQuery(
|
||||
ok: true,
|
||||
action: 'osm_query',
|
||||
kind: preset.label,
|
||||
kindSingular: singularLabel(preset.label),
|
||||
count,
|
||||
complete: false,
|
||||
countScope: 'bounding-box',
|
||||
@@ -290,6 +291,7 @@ export async function osmQuery(
|
||||
ok: true,
|
||||
action: 'osm_query',
|
||||
kind: preset.label,
|
||||
kindSingular: singularLabel(preset.label),
|
||||
count,
|
||||
complete,
|
||||
area: areaName,
|
||||
|
||||
+80
-12
@@ -293,6 +293,14 @@ const FIXED_NOTES = new Map(
|
||||
msg(
|
||||
'the count is a lower bound because at least one layer reached its retained limit.',
|
||||
),
|
||||
// Area, imagery and OSM card lines (areaActions, imageryActions,
|
||||
// osmActions); the model gets the English.
|
||||
msg('buffer sized to the place'),
|
||||
msg('enclosing mapped area'),
|
||||
msg('the map was cleared while looking it up'),
|
||||
msg('OpenStreetMap coverage varies by place'),
|
||||
msg('the catalog covers the last 30 days'),
|
||||
msg('there are more'),
|
||||
].map((text) => [text, text]),
|
||||
);
|
||||
|
||||
@@ -302,7 +310,58 @@ const LAYER_STATUS_NOTES = Object.freeze({
|
||||
error: (label) => t('{label} error', { label }),
|
||||
});
|
||||
|
||||
const countOf = (text) => Number(String(text).replace(/,/g, ''));
|
||||
|
||||
const NOTE_PATTERNS = [
|
||||
[
|
||||
/^([\d,]+) m around the landmark$/,
|
||||
([, radius]) =>
|
||||
t('{radius} m around the landmark', {
|
||||
radius: formatNumber(countOf(radius)),
|
||||
}),
|
||||
],
|
||||
[
|
||||
/^([\d,]+) parts$/,
|
||||
([, n]) =>
|
||||
tn(countOf(n), '{count} part', '{count} parts', {
|
||||
count: formatNumber(countOf(n)),
|
||||
}),
|
||||
],
|
||||
[
|
||||
/^([\d,]+) of ([\d,]+) parts drawn$/,
|
||||
([, drawn, total]) =>
|
||||
tn(
|
||||
countOf(total),
|
||||
'{drawn} of {total} part drawn',
|
||||
'{drawn} of {total} parts drawn',
|
||||
{
|
||||
drawn: formatNumber(countOf(drawn)),
|
||||
total: formatNumber(countOf(total)),
|
||||
},
|
||||
),
|
||||
],
|
||||
[
|
||||
/^([\d,]+) duplicate mappings merged$/,
|
||||
([, n]) =>
|
||||
tn(
|
||||
countOf(n),
|
||||
'{count} duplicate mapping merged',
|
||||
'{count} duplicate mappings merged',
|
||||
{ count: formatNumber(countOf(n)) },
|
||||
),
|
||||
],
|
||||
[
|
||||
/^listed the first ([\d,]+)$/,
|
||||
([, limit]) =>
|
||||
t('listed the first {limit}', { limit: formatNumber(countOf(limit)) }),
|
||||
],
|
||||
[
|
||||
/^counted over the bounding box of (.+), not its outline$/,
|
||||
([, area]) =>
|
||||
t('counted over the bounding box of {area}, not its outline', {
|
||||
area: spokenLabel(area, 48),
|
||||
}),
|
||||
],
|
||||
[
|
||||
/^counted the first ([\d,]+) loaded records$/,
|
||||
([, returned]) =>
|
||||
@@ -373,6 +432,11 @@ const NOTE_PATTERNS = [
|
||||
],
|
||||
];
|
||||
|
||||
/** A card line from a producer's English text, or null when absent. */
|
||||
function shownLine(text) {
|
||||
return text ? shownNote(String(text)) : null;
|
||||
}
|
||||
|
||||
function shownNote(text) {
|
||||
if (FIXED_NOTES.has(text)) return t(FIXED_NOTES.get(text));
|
||||
for (const [pattern, show] of NOTE_PATTERNS) {
|
||||
@@ -465,18 +529,18 @@ function areaCard(result, args = {}) {
|
||||
title: `${result.name ? spokenLabel(result.name, 48) : t('Area', { context: 'voice area' })}${result.country ? `, ${spokenLabel(result.country, 24)}` : ''}`,
|
||||
lines: [
|
||||
Number.isFinite(km2) && km2 > 0 ? `${formatNumber(km2)} km²` : null,
|
||||
display.basis,
|
||||
display.parts,
|
||||
display.drawnParts,
|
||||
shownLine(display.basis),
|
||||
shownLine(display.parts),
|
||||
shownLine(display.drawnParts),
|
||||
result.notDrawn
|
||||
? t('Not drawn: {parts}', { parts: result.notDrawn })
|
||||
? t('Not drawn: {parts}', { parts: shownNote(result.notDrawn) })
|
||||
: null,
|
||||
].filter(Boolean),
|
||||
chips: [
|
||||
...(result.approximate ? [{ label: t('approximate') }] : []),
|
||||
...(result.partial ? [{ label: shownFeedState('partial') }] : []),
|
||||
],
|
||||
notes: notesOf(display.caveat),
|
||||
notes: notesOf(display.caveat).map(shownNote),
|
||||
sources: display.source ? [{ label: display.source }] : [],
|
||||
},
|
||||
referents: [],
|
||||
@@ -509,7 +573,7 @@ function imageryCard(result) {
|
||||
total: formatNumber(Number(result.total) || 0),
|
||||
},
|
||||
),
|
||||
capitalized(display.window),
|
||||
capitalized(shownLine(display.window)),
|
||||
]
|
||||
: [
|
||||
tn(
|
||||
@@ -518,7 +582,7 @@ function imageryCard(result) {
|
||||
'{n} acquisitions searched',
|
||||
{ n: formatNumber(Number(result.total) || 0) },
|
||||
),
|
||||
capitalized(display.window),
|
||||
capitalized(shownLine(display.window)),
|
||||
];
|
||||
return {
|
||||
display: {
|
||||
@@ -528,7 +592,7 @@ function imageryCard(result) {
|
||||
lines: lines.filter(Boolean),
|
||||
chips:
|
||||
shown && !shown.onMap ? [{ label: shownFeedState('loading') }] : [],
|
||||
notes: notesOf(display.caveat),
|
||||
notes: notesOf(display.caveat).map(shownNote),
|
||||
},
|
||||
referents: [],
|
||||
};
|
||||
@@ -538,8 +602,12 @@ function osmCard(result) {
|
||||
if (!result?.ok) return null;
|
||||
const display = result.display || {};
|
||||
const count = Number(result.count);
|
||||
// The kind is the preset's English noun, the model's data.
|
||||
const kind = spokenLabel(result.kind || 'places', 32);
|
||||
// The kind is the preset's English noun, the model's data; one place takes
|
||||
// its singular ("1 hospital").
|
||||
const kind = spokenLabel(
|
||||
(count === 1 && result.kindSingular) || result.kind || 'places',
|
||||
32,
|
||||
);
|
||||
const values = { count: formatNumber(count), kind };
|
||||
const title = !Number.isFinite(count)
|
||||
? t('Unknown number of {kind}', { kind })
|
||||
@@ -555,9 +623,9 @@ function osmCard(result) {
|
||||
result.area
|
||||
? t('In {area}', { area: spokenLabel(result.area, 48) })
|
||||
: null,
|
||||
display.merged,
|
||||
shownLine(display.merged),
|
||||
].filter(Boolean),
|
||||
notes: notesOf(display.caveat),
|
||||
notes: notesOf(display.caveat).map(shownNote),
|
||||
sources: display.source ? [{ label: display.source }] : [],
|
||||
},
|
||||
referents: Array.isArray(result.referents) ? result.referents : [],
|
||||
|
||||
@@ -280,3 +280,66 @@ test('every locationId the voice tools accept is a preset city', () => {
|
||||
assert.ok(ids.size > 0, 'found the locationId enums');
|
||||
for (const id of ids) assert.ok(Object.hasOwn(CITY_POIS, id), id);
|
||||
});
|
||||
|
||||
test('area, imagery and OSM card lines are shown in the UI language', () => {
|
||||
const area = {
|
||||
ok: true,
|
||||
name: 'Ferry Building',
|
||||
approximate: true,
|
||||
display: {
|
||||
basis: '250 m around the landmark',
|
||||
parts: '3 parts',
|
||||
drawnParts: '2 of 3 parts drawn',
|
||||
source: 'approximate outline',
|
||||
},
|
||||
notDrawn: 'the map was cleared while looking it up',
|
||||
};
|
||||
const areaLines = inLocale('de', () => presentResult('resolve_area', area).display.lines);
|
||||
assert.ok(areaLines.includes('250 m um das Wahrzeichen'), areaLines.join(' | '));
|
||||
assert.ok(areaLines.includes('Teile: 3'));
|
||||
assert.ok(areaLines.includes('Gezeichnete Teile: 2 von 3'));
|
||||
assert.ok(areaLines.some((line) => line.includes('die Karte wurde während der Suche geleert')));
|
||||
const osm = {
|
||||
ok: true,
|
||||
kind: 'hospitals',
|
||||
kindSingular: 'hospital',
|
||||
count: 1,
|
||||
complete: false,
|
||||
area: 'Kathmandu',
|
||||
display: {
|
||||
source: 'OpenStreetMap',
|
||||
merged: '2 duplicate mappings merged',
|
||||
caveat: 'listed the first 500; there are more',
|
||||
},
|
||||
};
|
||||
const osmCard = inLocale('de', () => presentResult('osm_query', osm).display);
|
||||
assert.match(osmCard.title, /\bhospital\b/);
|
||||
assert.doesNotMatch(osmCard.title, /hospitals/, 'one place takes the singular');
|
||||
assert.equal(presentResult('osm_query', { ...osm, complete: true }).display.title, '1 hospital');
|
||||
assert.ok(osmCard.lines.includes('Zusammengeführte Doppelkartierungen: 2'));
|
||||
assert.deepEqual(osmCard.notes, ['die ersten 500 aufgelistet', 'es gibt weitere']);
|
||||
const imagery = {
|
||||
ok: true,
|
||||
total: 4,
|
||||
count: 0,
|
||||
display: { window: 'the catalog covers the last 30 days' },
|
||||
};
|
||||
const imageryLines = inLocale('de', () => presentResult('find_imagery', imagery).display.lines);
|
||||
assert.ok(imageryLines.includes('Der Katalog umfasst die letzten 30 Tage'), imageryLines.join(' | '));
|
||||
});
|
||||
|
||||
test('same-named clarification choices show their region code', () => {
|
||||
const asked = {
|
||||
ok: false,
|
||||
needsClarification: true,
|
||||
code: 'AMBIGUOUS_AREA',
|
||||
candidates: [
|
||||
{ candidateId: 'ne:state:united-states-of-america-washington-wa', name: 'Washington', level: 'admin1', region: 'WA', country: 'United States of America' },
|
||||
{ candidateId: 'ne:state:united-states-of-america-washington-dc', name: 'Washington', level: 'admin1', region: 'DC', country: 'United States of America' },
|
||||
],
|
||||
};
|
||||
const lines = presentResult('resolve_area', asked, { query: 'Washington' }).display.lines;
|
||||
assert.match(lines[0], /\bWA\b/);
|
||||
assert.match(lines[1], /\bDC\b/);
|
||||
assert.notEqual(lines[0], lines[1]);
|
||||
});
|
||||
Reference in new issue
Block a user