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
@@ -445,6 +445,7 @@ Twenty layers and map sources. **Eighteen have a keyless path.** Some offer addi
|
|||||||
| 🚌 **Transit** | Live buses, trams, metros, trains and ferries with delayed playback between reports, selected-vehicle trails, and mode-coloured DETECT labels — Boston, Austin, Minneapolis, Helsinki, the Netherlands, Norway, South East Queensland, and nine in Ontario: Toronto (surface vehicles; the subway publishes no positions), Mississauga, Hamilton, Durham Region, York Region, Guelph, Burlington, Barrie and Kingston | Operator GTFS-Realtime feeds | 🟢 |
|
| 🚌 **Transit** | Live buses, trams, metros, trains and ferries with delayed playback between reports, selected-vehicle trails, and mode-coloured DETECT labels — Boston, Austin, Minneapolis, Helsinki, the Netherlands, Norway, South East Queensland, and nine in Ontario: Toronto (surface vehicles; the subway publishes no positions), Mississauga, Hamilton, Durham Region, York Region, Guelph, Burlington, Barrie and Kingston | Operator GTFS-Realtime feeds | 🟢 |
|
||||||
| 🚲 **Bikeshare** | Live station availability | GBFS | 🟢 |
|
| 🚲 **Bikeshare** | Live station availability | GBFS | 🟢 |
|
||||||
| 🧭 **Directions** | Click A and B on the globe for a street-following drive, walk or cycle route draped on the terrain with turn-by-turn steps — then FLY the camera along it. No key, no geocoder, no mic | OSRM on FOSSGIS servers (OpenStreetMap) | 🟢 |
|
| 🧭 **Directions** | Click A and B on the globe for a street-following drive, walk or cycle route draped on the terrain with turn-by-turn steps — then FLY the camera along it. No key, no geocoder, no mic | OSRM on FOSSGIS servers (OpenStreetMap) | 🟢 |
|
||||||
|
| ⌖ **OSM Places** | Voice place search ("hospitals in Kathmandu") from OpenStreetMap, drawn as numbered points that follow-up questions can filter. Needs your own Overpass server (`OVERPASS_UPSTREAMS`); off otherwise |
|
||||||
| 🔥 **Active Fires** | Live NASA FIRMS detections, trailing 24h | NASA FIRMS | 🟡 |
|
| 🔥 **Active Fires** | Live NASA FIRMS detections, trailing 24h | NASA FIRMS | 🟡 |
|
||||||
| 🚀 **Space Missions** | Rolling 30-day launches with payload, stage, and recovery detail | Launch Library 2 | 🟢 (🔴 optional paid token raises the allowance) |
|
| 🚀 **Space Missions** | Rolling 30-day launches with payload, stage, and recovery detail | Launch Library 2 | 🟢 (🔴 optional paid token raises the allowance) |
|
||||||
| 🎖️ **Mapped Installations** | Viewport-bounded military-site context from community mapping — incomplete by nature, and labeled that way | OpenStreetMap | 🟢 |
|
| 🎖️ **Mapped Installations** | Viewport-bounded military-site context from community mapping — incomplete by nature, and labeled that way | OpenStreetMap | 🟢 |
|
||||||
@@ -454,7 +455,7 @@ Twenty layers and map sources. **Eighteen have a keyless path.** Some offer addi
|
|||||||
|
|
||||||
**Weather, keyless.** **Wind** animates 10 m forecast flow from NOAA GFS or ECMWF IFS, with an optional color field and a reading at map center. **Rain radar**, **Satellite clouds** and **Lightning density** are observations on one history timeline in the **WEATHER** panel (step back, **Play**, **Latest**); **Cyclone advisories** draw NHC and CPHC positions, forecast tracks and cones. On Google 3D Tiles the observed layers float above the city as translucent shells, sharpened around your view.
|
**Weather, keyless.** **Wind** animates 10 m forecast flow from NOAA GFS or ECMWF IFS, with an optional color field and a reading at map center. **Rain radar**, **Satellite clouds** and **Lightning density** are observations on one history timeline in the **WEATHER** panel (step back, **Play**, **Latest**); **Cyclone advisories** draw NHC and CPHC positions, forecast tracks and cones. On Google 3D Tiles the observed layers float above the city as translucent shells, sharpened around your view.
|
||||||
|
|
||||||
In the app, **Data Layers** groups them as Movement, Cameras, Infrastructure, Events, Weather and Audio; Directions sits in the separate **Utilities** panel with Draw and Import.
|
In the app, **Data Layers** groups them as Movement, Cameras, Infrastructure, Events, Weather and Audio; Directions and OSM Places sit in the separate **Utilities** panel with Draw and Import.
|
||||||
|
|
||||||
**The basemap ladder — what each tier buys you:**
|
**The basemap ladder — what each tier buys you:**
|
||||||
|
|
||||||
|
|||||||
@@ -3270,5 +3270,39 @@
|
|||||||
"Searching imagery catalog": "جارٍ البحث في فهرس الصور",
|
"Searching imagery catalog": "جارٍ البحث في فهرس الصور",
|
||||||
"Loading imagery": "جارٍ تحميل الصور",
|
"Loading imagery": "جارٍ تحميل الصور",
|
||||||
"Searching OpenStreetMap": "جارٍ البحث في OpenStreetMap",
|
"Searching OpenStreetMap": "جارٍ البحث في OpenStreetMap",
|
||||||
"voice area|Area": "منطقة"
|
"voice area|Area": "منطقة",
|
||||||
|
"buffer sized to the place": "منطقة تقريبية بحجم المكان",
|
||||||
|
"enclosing mapped area": "المنطقة المرسومة المحيطة",
|
||||||
|
"the map was cleared while looking it up": "مُسحت الخريطة أثناء البحث",
|
||||||
|
"OpenStreetMap coverage varies by place": "تختلف تغطية OpenStreetMap من مكان لآخر",
|
||||||
|
"the catalog covers the last 30 days": "يغطي الفهرس آخر 30 يومًا",
|
||||||
|
"there are more": "هناك المزيد",
|
||||||
|
"{radius} m around the landmark": "{radius} م حول المعلم",
|
||||||
|
"{count} parts": {
|
||||||
|
"few": "الأجزاء: {count}",
|
||||||
|
"many": "الأجزاء: {count}",
|
||||||
|
"one": "الأجزاء: {count}",
|
||||||
|
"other": "الأجزاء: {count}",
|
||||||
|
"two": "الأجزاء: {count}",
|
||||||
|
"zero": "الأجزاء: {count}"
|
||||||
|
},
|
||||||
|
"{drawn} of {total} parts drawn": {
|
||||||
|
"few": "الأجزاء المرسومة: {drawn} من {total}",
|
||||||
|
"many": "الأجزاء المرسومة: {drawn} من {total}",
|
||||||
|
"one": "الأجزاء المرسومة: {drawn} من {total}",
|
||||||
|
"other": "الأجزاء المرسومة: {drawn} من {total}",
|
||||||
|
"two": "الأجزاء المرسومة: {drawn} من {total}",
|
||||||
|
"zero": "الأجزاء المرسومة: {drawn} من {total}"
|
||||||
|
},
|
||||||
|
"{count} duplicate mappings merged": {
|
||||||
|
"few": "التكرارات المدمجة: {count}",
|
||||||
|
"many": "التكرارات المدمجة: {count}",
|
||||||
|
"one": "التكرارات المدمجة: {count}",
|
||||||
|
"other": "التكرارات المدمجة: {count}",
|
||||||
|
"two": "التكرارات المدمجة: {count}",
|
||||||
|
"zero": "التكرارات المدمجة: {count}"
|
||||||
|
},
|
||||||
|
"listed the first {limit}": "عُرض أول {limit}",
|
||||||
|
"counted over the bounding box of {area}, not its outline": "عُدّ ضمن المستطيل المحيط ({area})، لا ضمن الحدود",
|
||||||
|
"Search cancelled": "أُلغي البحث"
|
||||||
}
|
}
|
||||||
@@ -2982,5 +2982,27 @@
|
|||||||
"Searching imagery catalog": "Bildkatalog wird durchsucht",
|
"Searching imagery catalog": "Bildkatalog wird durchsucht",
|
||||||
"Loading imagery": "Bilddaten werden geladen",
|
"Loading imagery": "Bilddaten werden geladen",
|
||||||
"Searching OpenStreetMap": "OpenStreetMap wird durchsucht",
|
"Searching OpenStreetMap": "OpenStreetMap wird durchsucht",
|
||||||
"voice area|Area": "Gebiet"
|
"voice area|Area": "Gebiet",
|
||||||
|
"buffer sized to the place": "Puffer in Größe des Ortes",
|
||||||
|
"enclosing mapped area": "umschließendes kartiertes Gebiet",
|
||||||
|
"the map was cleared while looking it up": "die Karte wurde während der Suche geleert",
|
||||||
|
"OpenStreetMap coverage varies by place": "Die Abdeckung von OpenStreetMap ist je nach Ort unterschiedlich",
|
||||||
|
"the catalog covers the last 30 days": "der Katalog umfasst die letzten 30 Tage",
|
||||||
|
"there are more": "es gibt weitere",
|
||||||
|
"{radius} m around the landmark": "{radius} m um das Wahrzeichen",
|
||||||
|
"{count} parts": {
|
||||||
|
"one": "Teile: {count}",
|
||||||
|
"other": "Teile: {count}"
|
||||||
|
},
|
||||||
|
"{drawn} of {total} parts drawn": {
|
||||||
|
"one": "Gezeichnete Teile: {drawn} von {total}",
|
||||||
|
"other": "Gezeichnete Teile: {drawn} von {total}"
|
||||||
|
},
|
||||||
|
"{count} duplicate mappings merged": {
|
||||||
|
"one": "Zusammengeführte Doppelkartierungen: {count}",
|
||||||
|
"other": "Zusammengeführte Doppelkartierungen: {count}"
|
||||||
|
},
|
||||||
|
"listed the first {limit}": "die ersten {limit} aufgelistet",
|
||||||
|
"counted over the bounding box of {area}, not its outline": "über das umgebende Rechteck gezählt ({area}), nicht über den Umriss",
|
||||||
|
"Search cancelled": "Suche abgebrochen"
|
||||||
}
|
}
|
||||||
@@ -3054,5 +3054,30 @@
|
|||||||
"Searching imagery catalog": "Buscando en el catálogo de imágenes",
|
"Searching imagery catalog": "Buscando en el catálogo de imágenes",
|
||||||
"Loading imagery": "Cargando imágenes",
|
"Loading imagery": "Cargando imágenes",
|
||||||
"Searching OpenStreetMap": "Buscando en OpenStreetMap",
|
"Searching OpenStreetMap": "Buscando en OpenStreetMap",
|
||||||
"voice area|Area": "Área"
|
"voice area|Area": "Área",
|
||||||
|
"buffer sized to the place": "zona aproximada del tamaño del lugar",
|
||||||
|
"enclosing mapped area": "zona cartografiada que lo rodea",
|
||||||
|
"the map was cleared while looking it up": "el mapa se borró durante la búsqueda",
|
||||||
|
"OpenStreetMap coverage varies by place": "La cobertura de OpenStreetMap varía según el lugar",
|
||||||
|
"the catalog covers the last 30 days": "el catálogo cubre los últimos 30 días",
|
||||||
|
"there are more": "hay más",
|
||||||
|
"{radius} m around the landmark": "{radius} m alrededor del lugar",
|
||||||
|
"{count} parts": {
|
||||||
|
"many": "Partes: {count}",
|
||||||
|
"one": "Partes: {count}",
|
||||||
|
"other": "Partes: {count}"
|
||||||
|
},
|
||||||
|
"{drawn} of {total} parts drawn": {
|
||||||
|
"many": "Partes dibujadas: {drawn} de {total}",
|
||||||
|
"one": "Partes dibujadas: {drawn} de {total}",
|
||||||
|
"other": "Partes dibujadas: {drawn} de {total}"
|
||||||
|
},
|
||||||
|
"{count} duplicate mappings merged": {
|
||||||
|
"many": "Duplicados fusionados: {count}",
|
||||||
|
"one": "Duplicados fusionados: {count}",
|
||||||
|
"other": "Duplicados fusionados: {count}"
|
||||||
|
},
|
||||||
|
"listed the first {limit}": "se listaron los primeros {limit}",
|
||||||
|
"counted over the bounding box of {area}, not its outline": "contado en el rectángulo que lo rodea ({area}), no en su contorno",
|
||||||
|
"Search cancelled": "Búsqueda cancelada"
|
||||||
}
|
}
|
||||||
@@ -3054,5 +3054,30 @@
|
|||||||
"Searching imagery catalog": "Recherche dans le catalogue d'images",
|
"Searching imagery catalog": "Recherche dans le catalogue d'images",
|
||||||
"Loading imagery": "Chargement des images",
|
"Loading imagery": "Chargement des images",
|
||||||
"Searching OpenStreetMap": "Recherche dans OpenStreetMap",
|
"Searching OpenStreetMap": "Recherche dans OpenStreetMap",
|
||||||
"voice area|Area": "Zone"
|
"voice area|Area": "Zone",
|
||||||
|
"buffer sized to the place": "zone tampon à la taille du lieu",
|
||||||
|
"enclosing mapped area": "zone cartographiée englobante",
|
||||||
|
"the map was cleared while looking it up": "la carte a été effacée pendant la recherche",
|
||||||
|
"OpenStreetMap coverage varies by place": "La couverture OpenStreetMap varie selon les lieux",
|
||||||
|
"the catalog covers the last 30 days": "le catalogue couvre les 30 derniers jours",
|
||||||
|
"there are more": "il y en a d'autres",
|
||||||
|
"{radius} m around the landmark": "{radius} m autour du lieu",
|
||||||
|
"{count} parts": {
|
||||||
|
"many": "Parties : {count}",
|
||||||
|
"one": "Parties : {count}",
|
||||||
|
"other": "Parties : {count}"
|
||||||
|
},
|
||||||
|
"{drawn} of {total} parts drawn": {
|
||||||
|
"many": "Parties tracées : {drawn} sur {total}",
|
||||||
|
"one": "Parties tracées : {drawn} sur {total}",
|
||||||
|
"other": "Parties tracées : {drawn} sur {total}"
|
||||||
|
},
|
||||||
|
"{count} duplicate mappings merged": {
|
||||||
|
"many": "Doublons fusionnés : {count}",
|
||||||
|
"one": "Doublons fusionnés : {count}",
|
||||||
|
"other": "Doublons fusionnés : {count}"
|
||||||
|
},
|
||||||
|
"listed the first {limit}": "les {limit} premiers sont listés",
|
||||||
|
"counted over the bounding box of {area}, not its outline": "compté sur le rectangle englobant ({area}), pas sur le contour",
|
||||||
|
"Search cancelled": "Recherche annulée"
|
||||||
}
|
}
|
||||||
@@ -3054,5 +3054,30 @@
|
|||||||
"Searching imagery catalog": "Ricerca nel catalogo immagini",
|
"Searching imagery catalog": "Ricerca nel catalogo immagini",
|
||||||
"Loading imagery": "Caricamento immagini",
|
"Loading imagery": "Caricamento immagini",
|
||||||
"Searching OpenStreetMap": "Ricerca in OpenStreetMap",
|
"Searching OpenStreetMap": "Ricerca in OpenStreetMap",
|
||||||
"voice area|Area": "Area"
|
"voice area|Area": "Area",
|
||||||
|
"buffer sized to the place": "zona tampone delle dimensioni del luogo",
|
||||||
|
"enclosing mapped area": "area mappata circostante",
|
||||||
|
"the map was cleared while looking it up": "la mappa è stata cancellata durante la ricerca",
|
||||||
|
"OpenStreetMap coverage varies by place": "La copertura di OpenStreetMap varia da luogo a luogo",
|
||||||
|
"the catalog covers the last 30 days": "il catalogo copre gli ultimi 30 giorni",
|
||||||
|
"there are more": "ce ne sono altri",
|
||||||
|
"{radius} m around the landmark": "{radius} m attorno al luogo",
|
||||||
|
"{count} parts": {
|
||||||
|
"many": "Parti: {count}",
|
||||||
|
"one": "Parti: {count}",
|
||||||
|
"other": "Parti: {count}"
|
||||||
|
},
|
||||||
|
"{drawn} of {total} parts drawn": {
|
||||||
|
"many": "Parti disegnate: {drawn} su {total}",
|
||||||
|
"one": "Parti disegnate: {drawn} su {total}",
|
||||||
|
"other": "Parti disegnate: {drawn} su {total}"
|
||||||
|
},
|
||||||
|
"{count} duplicate mappings merged": {
|
||||||
|
"many": "Duplicati uniti: {count}",
|
||||||
|
"one": "Duplicati uniti: {count}",
|
||||||
|
"other": "Duplicati uniti: {count}"
|
||||||
|
},
|
||||||
|
"listed the first {limit}": "elencati i primi {limit}",
|
||||||
|
"counted over the bounding box of {area}, not its outline": "contato sul riquadro di delimitazione ({area}), non sul contorno",
|
||||||
|
"Search cancelled": "Ricerca annullata"
|
||||||
}
|
}
|
||||||
@@ -2910,5 +2910,24 @@
|
|||||||
"Searching imagery catalog": "画像カタログを検索中",
|
"Searching imagery catalog": "画像カタログを検索中",
|
||||||
"Loading imagery": "画像を読み込み中",
|
"Loading imagery": "画像を読み込み中",
|
||||||
"Searching OpenStreetMap": "OpenStreetMapを検索中",
|
"Searching OpenStreetMap": "OpenStreetMapを検索中",
|
||||||
"voice area|Area": "エリア"
|
"voice area|Area": "エリア",
|
||||||
|
"buffer sized to the place": "場所の大きさに合わせたバッファ",
|
||||||
|
"enclosing mapped area": "周囲の地図上のエリア",
|
||||||
|
"the map was cleared while looking it up": "検索中に地図がクリアされました",
|
||||||
|
"OpenStreetMap coverage varies by place": "OpenStreetMap の網羅度は場所によって異なります",
|
||||||
|
"the catalog covers the last 30 days": "カタログは過去 30 日分です",
|
||||||
|
"there are more": "ほかにもあります",
|
||||||
|
"{radius} m around the landmark": "ランドマークの周囲 {radius} m",
|
||||||
|
"{count} parts": {
|
||||||
|
"other": "パーツ数: {count}"
|
||||||
|
},
|
||||||
|
"{drawn} of {total} parts drawn": {
|
||||||
|
"other": "描画したパーツ: {drawn}/{total}"
|
||||||
|
},
|
||||||
|
"{count} duplicate mappings merged": {
|
||||||
|
"other": "統合した重複: {count}"
|
||||||
|
},
|
||||||
|
"listed the first {limit}": "最初の {limit} 件を表示",
|
||||||
|
"counted over the bounding box of {area}, not its outline": "外接矩形 ({area}) で数えた結果で、輪郭ではありません",
|
||||||
|
"Search cancelled": "検索をキャンセルしました"
|
||||||
}
|
}
|
||||||
@@ -3054,5 +3054,30 @@
|
|||||||
"Searching imagery catalog": "Buscando no catálogo de imagens",
|
"Searching imagery catalog": "Buscando no catálogo de imagens",
|
||||||
"Loading imagery": "Carregando imagens",
|
"Loading imagery": "Carregando imagens",
|
||||||
"Searching OpenStreetMap": "Buscando no OpenStreetMap",
|
"Searching OpenStreetMap": "Buscando no OpenStreetMap",
|
||||||
"voice area|Area": "Área"
|
"voice area|Area": "Área",
|
||||||
|
"buffer sized to the place": "zona de margem do tamanho do local",
|
||||||
|
"enclosing mapped area": "área mapeada envolvente",
|
||||||
|
"the map was cleared while looking it up": "o mapa foi limpo durante a busca",
|
||||||
|
"OpenStreetMap coverage varies by place": "A cobertura do OpenStreetMap varia de lugar para lugar",
|
||||||
|
"the catalog covers the last 30 days": "o catálogo cobre os últimos 30 dias",
|
||||||
|
"there are more": "há mais",
|
||||||
|
"{radius} m around the landmark": "{radius} m ao redor do local",
|
||||||
|
"{count} parts": {
|
||||||
|
"many": "Partes: {count}",
|
||||||
|
"one": "Partes: {count}",
|
||||||
|
"other": "Partes: {count}"
|
||||||
|
},
|
||||||
|
"{drawn} of {total} parts drawn": {
|
||||||
|
"many": "Partes desenhadas: {drawn} de {total}",
|
||||||
|
"one": "Partes desenhadas: {drawn} de {total}",
|
||||||
|
"other": "Partes desenhadas: {drawn} de {total}"
|
||||||
|
},
|
||||||
|
"{count} duplicate mappings merged": {
|
||||||
|
"many": "Duplicatas mescladas: {count}",
|
||||||
|
"one": "Duplicatas mescladas: {count}",
|
||||||
|
"other": "Duplicatas mescladas: {count}"
|
||||||
|
},
|
||||||
|
"listed the first {limit}": "listados os primeiros {limit}",
|
||||||
|
"counted over the bounding box of {area}, not its outline": "contado na caixa delimitadora ({area}), não no contorno",
|
||||||
|
"Search cancelled": "Busca cancelada"
|
||||||
}
|
}
|
||||||
@@ -3126,5 +3126,33 @@
|
|||||||
"Searching imagery catalog": "Поиск в каталоге снимков",
|
"Searching imagery catalog": "Поиск в каталоге снимков",
|
||||||
"Loading imagery": "Загрузка снимков",
|
"Loading imagery": "Загрузка снимков",
|
||||||
"Searching OpenStreetMap": "Поиск в OpenStreetMap",
|
"Searching OpenStreetMap": "Поиск в OpenStreetMap",
|
||||||
"voice area|Area": "Область"
|
"voice area|Area": "Область",
|
||||||
|
"buffer sized to the place": "буферная зона по размеру места",
|
||||||
|
"enclosing mapped area": "охватывающая область на карте",
|
||||||
|
"the map was cleared while looking it up": "карта была очищена во время поиска",
|
||||||
|
"OpenStreetMap coverage varies by place": "Покрытие OpenStreetMap различается в разных местах",
|
||||||
|
"the catalog covers the last 30 days": "каталог охватывает последние 30 дней",
|
||||||
|
"there are more": "есть ещё",
|
||||||
|
"{radius} m around the landmark": "{radius} м вокруг объекта",
|
||||||
|
"{count} parts": {
|
||||||
|
"few": "Частей: {count}",
|
||||||
|
"many": "Частей: {count}",
|
||||||
|
"one": "Частей: {count}",
|
||||||
|
"other": "Частей: {count}"
|
||||||
|
},
|
||||||
|
"{drawn} of {total} parts drawn": {
|
||||||
|
"few": "Нарисовано частей: {drawn} из {total}",
|
||||||
|
"many": "Нарисовано частей: {drawn} из {total}",
|
||||||
|
"one": "Нарисовано частей: {drawn} из {total}",
|
||||||
|
"other": "Нарисовано частей: {drawn} из {total}"
|
||||||
|
},
|
||||||
|
"{count} duplicate mappings merged": {
|
||||||
|
"few": "Объединено дублей: {count}",
|
||||||
|
"many": "Объединено дублей: {count}",
|
||||||
|
"one": "Объединено дублей: {count}",
|
||||||
|
"other": "Объединено дублей: {count}"
|
||||||
|
},
|
||||||
|
"listed the first {limit}": "показаны первые {limit}",
|
||||||
|
"counted over the bounding box of {area}, not its outline": "подсчитано по ограничивающему прямоугольнику ({area}), а не по контуру",
|
||||||
|
"Search cancelled": "Поиск отменён"
|
||||||
}
|
}
|
||||||
@@ -3126,5 +3126,33 @@
|
|||||||
"Searching imagery catalog": "Пошук у каталозі знімків",
|
"Searching imagery catalog": "Пошук у каталозі знімків",
|
||||||
"Loading imagery": "Завантаження знімків",
|
"Loading imagery": "Завантаження знімків",
|
||||||
"Searching OpenStreetMap": "Пошук в OpenStreetMap",
|
"Searching OpenStreetMap": "Пошук в OpenStreetMap",
|
||||||
"voice area|Area": "Область"
|
"voice area|Area": "Область",
|
||||||
|
"buffer sized to the place": "буферна зона за розміром місця",
|
||||||
|
"enclosing mapped area": "охопна область на карті",
|
||||||
|
"the map was cleared while looking it up": "карту було очищено під час пошуку",
|
||||||
|
"OpenStreetMap coverage varies by place": "Покриття OpenStreetMap відрізняється залежно від місця",
|
||||||
|
"the catalog covers the last 30 days": "каталог охоплює останні 30 днів",
|
||||||
|
"there are more": "є ще",
|
||||||
|
"{radius} m around the landmark": "{radius} м навколо об’єкта",
|
||||||
|
"{count} parts": {
|
||||||
|
"few": "Частин: {count}",
|
||||||
|
"many": "Частин: {count}",
|
||||||
|
"one": "Частин: {count}",
|
||||||
|
"other": "Частин: {count}"
|
||||||
|
},
|
||||||
|
"{drawn} of {total} parts drawn": {
|
||||||
|
"few": "Намальовано частин: {drawn} з {total}",
|
||||||
|
"many": "Намальовано частин: {drawn} з {total}",
|
||||||
|
"one": "Намальовано частин: {drawn} з {total}",
|
||||||
|
"other": "Намальовано частин: {drawn} з {total}"
|
||||||
|
},
|
||||||
|
"{count} duplicate mappings merged": {
|
||||||
|
"few": "Об’єднано дублікатів: {count}",
|
||||||
|
"many": "Об’єднано дублікатів: {count}",
|
||||||
|
"one": "Об’єднано дублікатів: {count}",
|
||||||
|
"other": "Об’єднано дублікатів: {count}"
|
||||||
|
},
|
||||||
|
"listed the first {limit}": "показано перші {limit}",
|
||||||
|
"counted over the bounding box of {area}, not its outline": "підраховано за обмежувальним прямокутником ({area}), а не за контуром",
|
||||||
|
"Search cancelled": "Пошук скасовано"
|
||||||
}
|
}
|
||||||
@@ -2910,5 +2910,24 @@
|
|||||||
"Searching imagery catalog": "正在搜索影像目录",
|
"Searching imagery catalog": "正在搜索影像目录",
|
||||||
"Loading imagery": "正在加载影像",
|
"Loading imagery": "正在加载影像",
|
||||||
"Searching OpenStreetMap": "正在搜索 OpenStreetMap",
|
"Searching OpenStreetMap": "正在搜索 OpenStreetMap",
|
||||||
"voice area|Area": "区域"
|
"voice area|Area": "区域",
|
||||||
|
"buffer sized to the place": "按地点大小设置的缓冲区",
|
||||||
|
"enclosing mapped area": "包含该地点的地图区域",
|
||||||
|
"the map was cleared while looking it up": "查找期间地图已清空",
|
||||||
|
"OpenStreetMap coverage varies by place": "OpenStreetMap 的覆盖程度因地而异",
|
||||||
|
"the catalog covers the last 30 days": "目录涵盖最近 30 天",
|
||||||
|
"there are more": "还有更多",
|
||||||
|
"{radius} m around the landmark": "地标周围 {radius} 米",
|
||||||
|
"{count} parts": {
|
||||||
|
"other": "部分数:{count}"
|
||||||
|
},
|
||||||
|
"{drawn} of {total} parts drawn": {
|
||||||
|
"other": "已绘制部分:{drawn}/{total}"
|
||||||
|
},
|
||||||
|
"{count} duplicate mappings merged": {
|
||||||
|
"other": "已合并重复项:{count}"
|
||||||
|
},
|
||||||
|
"listed the first {limit}": "已列出前 {limit} 个",
|
||||||
|
"counted over the bounding box of {area}, not its outline": "按外接矩形({area})计数,而非其轮廓",
|
||||||
|
"Search cancelled": "搜索已取消"
|
||||||
}
|
}
|
||||||
@@ -2910,5 +2910,24 @@
|
|||||||
"Searching imagery catalog": "正在搜索影像目錄",
|
"Searching imagery catalog": "正在搜索影像目錄",
|
||||||
"Loading imagery": "正在加載影像",
|
"Loading imagery": "正在加載影像",
|
||||||
"Searching OpenStreetMap": "正在搜索 OpenStreetMap",
|
"Searching OpenStreetMap": "正在搜索 OpenStreetMap",
|
||||||
"voice area|Area": "區域"
|
"voice area|Area": "區域",
|
||||||
|
"buffer sized to the place": "按地點大小設置的緩衝區",
|
||||||
|
"enclosing mapped area": "包含該地點的地圖區域",
|
||||||
|
"the map was cleared while looking it up": "查找期間地圖已清空",
|
||||||
|
"OpenStreetMap coverage varies by place": "OpenStreetMap 的覆蓋程度因地而異",
|
||||||
|
"the catalog covers the last 30 days": "目錄涵蓋最近 30 天",
|
||||||
|
"there are more": "還有更多",
|
||||||
|
"{radius} m around the landmark": "地標周圍 {radius} 米",
|
||||||
|
"{count} parts": {
|
||||||
|
"other": "部分數:{count}"
|
||||||
|
},
|
||||||
|
"{drawn} of {total} parts drawn": {
|
||||||
|
"other": "已繪製部分:{drawn}/{total}"
|
||||||
|
},
|
||||||
|
"{count} duplicate mappings merged": {
|
||||||
|
"other": "已合併重複項:{count}"
|
||||||
|
},
|
||||||
|
"listed the first {limit}": "已列出前 {limit} 個",
|
||||||
|
"counted over the bounding box of {area}, not its outline": "按外接矩形({area})計數,而非其輪廓",
|
||||||
|
"Search cancelled": "搜索已取消"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { msg } from '../../i18n/marker.js';
|
import { msg } from '../../i18n/marker.js';
|
||||||
import { OSM_PLACES_LAYER_ID } from './model.js';
|
import { OSM_PLACES_LAYER_ID } from './model.js';
|
||||||
|
import { createNominatimStatus } from '../../sources/nominatimStatus.js';
|
||||||
export * from './model.js';
|
export * from './model.js';
|
||||||
|
|
||||||
const OVERLAY_SOURCE_ID = OSM_PLACES_LAYER_ID;
|
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 search runs only on an operator-configured Overpass, so without one
|
||||||
* the layer stays empty. Session-only: never in share links or stored layer
|
* the layer stays empty. Session-only: never in share links or stored layer
|
||||||
* state.
|
* 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');
|
if (!overlayHost) throw new TypeError('OSM Places requires an overlay host');
|
||||||
let viewer = null;
|
let viewer = null;
|
||||||
let dataSource = null;
|
let dataSource = null;
|
||||||
@@ -25,6 +30,8 @@ export function createOsmPlacesLayer({ overlayHost } = {}) {
|
|||||||
let records = [];
|
let records = [];
|
||||||
let meta = null;
|
let meta = null;
|
||||||
let lastUpdate = null;
|
let lastUpdate = null;
|
||||||
|
/** true / false from the server's probe, null while unknown. */
|
||||||
|
let configured = null;
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
if (!dataSource) return;
|
if (!dataSource) return;
|
||||||
@@ -42,7 +49,9 @@ export function createOsmPlacesLayer({ overlayHost } = {}) {
|
|||||||
outlineColor: Cesium.Color.BLACK.withAlpha(0.8),
|
outlineColor: Cesium.Color.BLACK.withAlpha(0.8),
|
||||||
outlineWidth: 2,
|
outlineWidth: 2,
|
||||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
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)
|
if (record.name && overlay.length < LABEL_LIMIT)
|
||||||
@@ -79,8 +88,24 @@ export function createOsmPlacesLayer({ overlayHost } = {}) {
|
|||||||
source: 'OpenStreetMap',
|
source: 'OpenStreetMap',
|
||||||
updateInterval: 0,
|
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) {
|
init(nextViewer) {
|
||||||
viewer = nextViewer;
|
viewer = nextViewer;
|
||||||
|
Promise.resolve(overpassConfigured?.())
|
||||||
|
.then((answer) => {
|
||||||
|
if (typeof answer === 'boolean') configured = answer;
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
dataSource = new Cesium.CustomDataSource(OSM_PLACES_LAYER_ID);
|
dataSource = new Cesium.CustomDataSource(OSM_PLACES_LAYER_ID);
|
||||||
dataSource.show = false;
|
dataSource.show = false;
|
||||||
viewer.dataSources.add(dataSource);
|
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)',
|
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. */
|
/** Stop catalog/preview work owned by a cancelled voice action. */
|
||||||
cancelPendingSearch() {
|
cancelPendingSearch() {
|
||||||
|
// Without a note the emptied strip would read as "no imagery".
|
||||||
|
if (_searching) _notice = msg('Search cancelled');
|
||||||
cancelSearch();
|
cancelSearch();
|
||||||
clearPending();
|
clearPending();
|
||||||
syncRender();
|
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);
|
assert.equal(f.snap().toolActive, false);
|
||||||
await tool.destroy();
|
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');
|
||||||
|
});
|
||||||
@@ -37,6 +37,7 @@ const ESRI_NOTE = msg('Imagery on Esri · Google 3D returns when cleared');
|
|||||||
const LAYER_NOTICES = new Set([
|
const LAYER_NOTICES = new Set([
|
||||||
ESRI_NOTE,
|
ESRI_NOTE,
|
||||||
msg('Box and images cleared'),
|
msg('Box and images cleared'),
|
||||||
|
msg('Search cancelled'),
|
||||||
msg('Esri map unavailable · no swipe'),
|
msg('Esri map unavailable · no swipe'),
|
||||||
msg('Swipe needs a globe map'),
|
msg('Swipe needs a globe map'),
|
||||||
msg('Imagery stays on Esri · CLEAR to use Google 3D'),
|
msg('Imagery stays on Esri · CLEAR to use Google 3D'),
|
||||||
|
|||||||
@@ -268,6 +268,8 @@ export async function findImagery(
|
|||||||
: args.source === 'any'
|
: args.source === 'any'
|
||||||
? { hls: true, viirs: true }
|
? { hls: true, viirs: true }
|
||||||
: { hls: true };
|
: { hls: true };
|
||||||
|
// The operator's own settings, put back if the box is refused.
|
||||||
|
const before = layer.getSnapshot();
|
||||||
layer.setSources(sources);
|
layer.setSources(sources);
|
||||||
layer.setMode?.('image');
|
layer.setMode?.('image');
|
||||||
|
|
||||||
@@ -279,6 +281,8 @@ export async function findImagery(
|
|||||||
if (!accepted) {
|
if (!accepted) {
|
||||||
const reason =
|
const reason =
|
||||||
layer.getSnapshot().boxError || 'That area cannot be searched.';
|
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.`, {
|
return fail('BOX_REFUSED', `${reason}. Zoom in or pick a smaller area.`, {
|
||||||
...(areaName ? { area: areaName } : {}),
|
...(areaName ? { area: areaName } : {}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ function fakeLayer({
|
|||||||
pins: { a: { key: null } },
|
pins: { a: { key: null } },
|
||||||
shown: { a: null },
|
shown: { a: null },
|
||||||
notes: [],
|
notes: [],
|
||||||
|
sources: { hls: false, viirs: true },
|
||||||
|
mode: 'basemap',
|
||||||
};
|
};
|
||||||
const listeners = new Set();
|
const listeners = new Set();
|
||||||
const calls = [];
|
const calls = [];
|
||||||
@@ -92,9 +94,11 @@ function fakeLayer({
|
|||||||
},
|
},
|
||||||
setSources(sources) {
|
setSources(sources) {
|
||||||
calls.push(['setSources', sources]);
|
calls.push(['setSources', sources]);
|
||||||
|
set({ sources: { ...sources } });
|
||||||
},
|
},
|
||||||
setMode(mode) {
|
setMode(mode) {
|
||||||
calls.push(['setMode', mode]);
|
calls.push(['setMode', mode]);
|
||||||
|
set({ mode });
|
||||||
},
|
},
|
||||||
setAssignment(slot, key) {
|
setAssignment(slot, key) {
|
||||||
calls.push(['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]);
|
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 () => {
|
test('a superseded search is cancelled before anything is pinned', async () => {
|
||||||
const layer = fakeLayer();
|
const layer = fakeLayer();
|
||||||
let current = true;
|
let current = true;
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ export async function osmQuery(
|
|||||||
ok: true,
|
ok: true,
|
||||||
action: 'osm_query',
|
action: 'osm_query',
|
||||||
kind: preset.label,
|
kind: preset.label,
|
||||||
|
kindSingular: singularLabel(preset.label),
|
||||||
count,
|
count,
|
||||||
complete: false,
|
complete: false,
|
||||||
countScope: 'bounding-box',
|
countScope: 'bounding-box',
|
||||||
@@ -290,6 +291,7 @@ export async function osmQuery(
|
|||||||
ok: true,
|
ok: true,
|
||||||
action: 'osm_query',
|
action: 'osm_query',
|
||||||
kind: preset.label,
|
kind: preset.label,
|
||||||
|
kindSingular: singularLabel(preset.label),
|
||||||
count,
|
count,
|
||||||
complete,
|
complete,
|
||||||
area: areaName,
|
area: areaName,
|
||||||
|
|||||||
+80
-12
@@ -293,6 +293,14 @@ const FIXED_NOTES = new Map(
|
|||||||
msg(
|
msg(
|
||||||
'the count is a lower bound because at least one layer reached its retained limit.',
|
'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]),
|
].map((text) => [text, text]),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -302,7 +310,58 @@ const LAYER_STATUS_NOTES = Object.freeze({
|
|||||||
error: (label) => t('{label} error', { label }),
|
error: (label) => t('{label} error', { label }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const countOf = (text) => Number(String(text).replace(/,/g, ''));
|
||||||
|
|
||||||
const NOTE_PATTERNS = [
|
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$/,
|
/^counted the first ([\d,]+) loaded records$/,
|
||||||
([, returned]) =>
|
([, 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) {
|
function shownNote(text) {
|
||||||
if (FIXED_NOTES.has(text)) return t(FIXED_NOTES.get(text));
|
if (FIXED_NOTES.has(text)) return t(FIXED_NOTES.get(text));
|
||||||
for (const [pattern, show] of NOTE_PATTERNS) {
|
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)}` : ''}`,
|
title: `${result.name ? spokenLabel(result.name, 48) : t('Area', { context: 'voice area' })}${result.country ? `, ${spokenLabel(result.country, 24)}` : ''}`,
|
||||||
lines: [
|
lines: [
|
||||||
Number.isFinite(km2) && km2 > 0 ? `${formatNumber(km2)} km²` : null,
|
Number.isFinite(km2) && km2 > 0 ? `${formatNumber(km2)} km²` : null,
|
||||||
display.basis,
|
shownLine(display.basis),
|
||||||
display.parts,
|
shownLine(display.parts),
|
||||||
display.drawnParts,
|
shownLine(display.drawnParts),
|
||||||
result.notDrawn
|
result.notDrawn
|
||||||
? t('Not drawn: {parts}', { parts: result.notDrawn })
|
? t('Not drawn: {parts}', { parts: shownNote(result.notDrawn) })
|
||||||
: null,
|
: null,
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
chips: [
|
chips: [
|
||||||
...(result.approximate ? [{ label: t('approximate') }] : []),
|
...(result.approximate ? [{ label: t('approximate') }] : []),
|
||||||
...(result.partial ? [{ label: shownFeedState('partial') }] : []),
|
...(result.partial ? [{ label: shownFeedState('partial') }] : []),
|
||||||
],
|
],
|
||||||
notes: notesOf(display.caveat),
|
notes: notesOf(display.caveat).map(shownNote),
|
||||||
sources: display.source ? [{ label: display.source }] : [],
|
sources: display.source ? [{ label: display.source }] : [],
|
||||||
},
|
},
|
||||||
referents: [],
|
referents: [],
|
||||||
@@ -509,7 +573,7 @@ function imageryCard(result) {
|
|||||||
total: formatNumber(Number(result.total) || 0),
|
total: formatNumber(Number(result.total) || 0),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
capitalized(display.window),
|
capitalized(shownLine(display.window)),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
tn(
|
tn(
|
||||||
@@ -518,7 +582,7 @@ function imageryCard(result) {
|
|||||||
'{n} acquisitions searched',
|
'{n} acquisitions searched',
|
||||||
{ n: formatNumber(Number(result.total) || 0) },
|
{ n: formatNumber(Number(result.total) || 0) },
|
||||||
),
|
),
|
||||||
capitalized(display.window),
|
capitalized(shownLine(display.window)),
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
display: {
|
display: {
|
||||||
@@ -528,7 +592,7 @@ function imageryCard(result) {
|
|||||||
lines: lines.filter(Boolean),
|
lines: lines.filter(Boolean),
|
||||||
chips:
|
chips:
|
||||||
shown && !shown.onMap ? [{ label: shownFeedState('loading') }] : [],
|
shown && !shown.onMap ? [{ label: shownFeedState('loading') }] : [],
|
||||||
notes: notesOf(display.caveat),
|
notes: notesOf(display.caveat).map(shownNote),
|
||||||
},
|
},
|
||||||
referents: [],
|
referents: [],
|
||||||
};
|
};
|
||||||
@@ -538,8 +602,12 @@ function osmCard(result) {
|
|||||||
if (!result?.ok) return null;
|
if (!result?.ok) return null;
|
||||||
const display = result.display || {};
|
const display = result.display || {};
|
||||||
const count = Number(result.count);
|
const count = Number(result.count);
|
||||||
// The kind is the preset's English noun, the model's data.
|
// The kind is the preset's English noun, the model's data; one place takes
|
||||||
const kind = spokenLabel(result.kind || 'places', 32);
|
// its singular ("1 hospital").
|
||||||
|
const kind = spokenLabel(
|
||||||
|
(count === 1 && result.kindSingular) || result.kind || 'places',
|
||||||
|
32,
|
||||||
|
);
|
||||||
const values = { count: formatNumber(count), kind };
|
const values = { count: formatNumber(count), kind };
|
||||||
const title = !Number.isFinite(count)
|
const title = !Number.isFinite(count)
|
||||||
? t('Unknown number of {kind}', { kind })
|
? t('Unknown number of {kind}', { kind })
|
||||||
@@ -555,9 +623,9 @@ function osmCard(result) {
|
|||||||
result.area
|
result.area
|
||||||
? t('In {area}', { area: spokenLabel(result.area, 48) })
|
? t('In {area}', { area: spokenLabel(result.area, 48) })
|
||||||
: null,
|
: null,
|
||||||
display.merged,
|
shownLine(display.merged),
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
notes: notesOf(display.caveat),
|
notes: notesOf(display.caveat).map(shownNote),
|
||||||
sources: display.source ? [{ label: display.source }] : [],
|
sources: display.source ? [{ label: display.source }] : [],
|
||||||
},
|
},
|
||||||
referents: Array.isArray(result.referents) ? result.referents : [],
|
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');
|
assert.ok(ids.size > 0, 'found the locationId enums');
|
||||||
for (const id of ids) assert.ok(Object.hasOwn(CITY_POIS, id), id);
|
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