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
@@ -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 | 🟢 |
|
||||
| 🚲 **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) | 🟢 |
|
||||
| ⌖ **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 | 🟡 |
|
||||
| 🚀 **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 | 🟢 |
|
||||
@@ -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.
|
||||
|
||||
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:**
|
||||
|
||||
|
||||
@@ -3270,5 +3270,39 @@
|
||||
"Searching imagery catalog": "جارٍ البحث في فهرس الصور",
|
||||
"Loading imagery": "جارٍ تحميل الصور",
|
||||
"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",
|
||||
"Loading imagery": "Bilddaten werden geladen",
|
||||
"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",
|
||||
"Loading imagery": "Cargando imágenes",
|
||||
"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",
|
||||
"Loading imagery": "Chargement des images",
|
||||
"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",
|
||||
"Loading imagery": "Caricamento immagini",
|
||||
"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": "画像カタログを検索中",
|
||||
"Loading imagery": "画像を読み込み中",
|
||||
"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",
|
||||
"Loading imagery": "Carregando imagens",
|
||||
"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": "Поиск в каталоге снимков",
|
||||
"Loading imagery": "Загрузка снимков",
|
||||
"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": "Пошук у каталозі знімків",
|
||||
"Loading imagery": "Завантаження знімків",
|
||||
"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": "正在搜索影像目录",
|
||||
"Loading imagery": "正在加载影像",
|
||||
"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": "正在搜索影像目錄",
|
||||
"Loading imagery": "正在加載影像",
|
||||
"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 { 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');
|
||||
});
|
||||
@@ -37,6 +37,7 @@ const ESRI_NOTE = msg('Imagery on Esri · Google 3D returns when cleared');
|
||||
const LAYER_NOTICES = new Set([
|
||||
ESRI_NOTE,
|
||||
msg('Box and images cleared'),
|
||||
msg('Search cancelled'),
|
||||
msg('Esri map unavailable · no swipe'),
|
||||
msg('Swipe needs a globe map'),
|
||||
msg('Imagery stays on Esri · CLEAR to use Google 3D'),
|
||||
|
||||
@@ -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