mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abfc85e03d | ||
|
|
fbec1488ac |
No files matched your search
@@ -0,0 +1,6 @@
|
||||
- When the globe stops drawing after a render error or a lost WebGL context,
|
||||
an alert dialog says so instead of leaving a frozen or black globe behind
|
||||
working controls. Retry restarts drawing (Reload after a lost context), and
|
||||
Reset view reopens the app without the view restored from the address bar.
|
||||
Cesium's "Rendering has stopped" panel no longer appears in the app
|
||||
(German, @gekh, #1579).
|
||||
@@ -1579,6 +1579,33 @@ future driver fix restores the effect with no code change; iOS/iPadOS platform
|
||||
detection is the backstop for when no probe context can be created. Applied
|
||||
during viewer construction, before any tile builds a draw command.
|
||||
|
||||
## Render failure notice
|
||||
|
||||
When the globe stops drawing, the scene chrome says so instead of leaving a
|
||||
black globe behind controls that still respond. `installRenderFailureNotice()`
|
||||
(`src/ui/renderFailure.js`) listens for Cesium's `renderError`, after which
|
||||
Cesium stops its render loop, and for the canvas' `webglcontextlost`. It logs
|
||||
the error and publishes a notice to the status store, which
|
||||
`RenderFailure.vue` renders as an alert dialog that takes focus. The
|
||||
application scene creates its viewer with `showRenderLoopErrors: false`, so
|
||||
Cesium's own "Rendering has stopped" panel, which offers no way back, no longer
|
||||
appears; `applicationViewerOptions()` and `createApplicationViewer()` keep
|
||||
Cesium's panel by default for other callers.
|
||||
|
||||
- **Retry** after a render error restarts the render loop. The notice stays
|
||||
until a frame starts and ends without a render error (Cesium raises
|
||||
`postRender` after a failed frame too), and a loop that fails again keeps it.
|
||||
- **Reload** after a lost context reloads the page: Cesium cannot draw into a
|
||||
context it has lost, and the address keeps the view.
|
||||
- **Reset view** opens the app's root address (`appRootUrl()`), without the
|
||||
hash or `/share/<code>` path the view was restored from; the query, such as
|
||||
`?lang=`, is kept.
|
||||
|
||||
An inline conversation panel never reloads or navigates its host's page, so it
|
||||
offers Retry after a render error only, and nothing after a lost context, where
|
||||
the panel runtime's own notice applies. The listener does not
|
||||
`preventDefault()` the context loss or restore the context; it only reports it.
|
||||
|
||||
## Map Source control ownership
|
||||
|
||||
Map Source controls own chip listeners, source-state subscriptions and selection
|
||||
|
||||
@@ -545,6 +545,8 @@
|
||||
"src/ui/frameRateMonitor.js",
|
||||
"src/ui/frameRateMonitor.test.mjs",
|
||||
"src/ui/frameRateView.test.mjs",
|
||||
"src/ui/renderFailure.test.mjs",
|
||||
"src/ui/renderFailureView.test.mjs",
|
||||
"src/layers/cctv/lifecycle.js",
|
||||
"src/layers/cctv/controls.js",
|
||||
"src/layers/cctv/geometry.js",
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
"src/ui/kit/UiBadge.vue",
|
||||
"src/ui/stores/globeActions.js",
|
||||
"src/ui/views/panels/GlobeActions.vue",
|
||||
"src/ui/views/panels/RenderFailure.vue",
|
||||
"src/ui/views/panels/GlobeCompass.vue",
|
||||
"src/ui/stores/recording.js",
|
||||
"src/ui/styleParameters.js",
|
||||
@@ -2802,6 +2803,7 @@
|
||||
"src/ui/displayControls.js",
|
||||
"src/ui/effects.js",
|
||||
"src/ui/frameRateMonitor.js",
|
||||
"src/ui/renderFailure.js",
|
||||
"src/ui/layerPanel.js",
|
||||
"src/ui/weatherPanel.js",
|
||||
"src/ui/railCards.js",
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
governorRequestRender,
|
||||
} from '../renderGovernor.js';
|
||||
import { describeError } from './errors.js';
|
||||
import { isEmbeddedInline } from './embed.js';
|
||||
import { installRenderFailureNotice } from '../ui/renderFailure.js';
|
||||
import { onLocaleChange, t } from '../i18n/index.js';
|
||||
|
||||
/** Construct the application globe using the caller's local configuration. */
|
||||
@@ -52,11 +54,17 @@ export async function createApplicationScene({
|
||||
const viewer = createApplicationViewer({
|
||||
container: 'cesiumContainer',
|
||||
creditContainer,
|
||||
// The render failure notice below replaces Cesium's dead-end panel.
|
||||
showRenderLoopErrors: false,
|
||||
});
|
||||
defer(() => {
|
||||
uninstallRenderGovernor(viewer);
|
||||
if (!viewer.isDestroyed()) viewer.destroy();
|
||||
});
|
||||
// An inline panel must not reload or navigate its host's page.
|
||||
defer(
|
||||
installRenderFailureNotice(viewer, { allowReload: !isEmbeddedInline() }),
|
||||
);
|
||||
defer(installTrackpadPinchZoom(viewer));
|
||||
registerDataCredits(viewer, credits);
|
||||
configureCreditKeyboardAccess(document);
|
||||
|
||||
+16
-4
@@ -103,8 +103,15 @@ export function installTrackpadPinchZoom(
|
||||
};
|
||||
}
|
||||
|
||||
/** Cesium.Viewer options of the standard globe viewer. */
|
||||
export function applicationViewerOptions(creditContainer) {
|
||||
/**
|
||||
* Cesium.Viewer options of the standard globe viewer. `showRenderLoopErrors`
|
||||
* false leaves render errors to the caller's own notice instead of Cesium's
|
||||
* panel.
|
||||
*/
|
||||
export function applicationViewerOptions(
|
||||
creditContainer,
|
||||
{ showRenderLoopErrors = true } = {},
|
||||
) {
|
||||
return {
|
||||
timeline: false,
|
||||
animation: false,
|
||||
@@ -124,16 +131,21 @@ export function applicationViewerOptions(creditContainer) {
|
||||
creditContainer,
|
||||
msaaSamples: 4,
|
||||
contextOptions: { webgl: { preserveDrawingBuffer: true } },
|
||||
showRenderLoopErrors,
|
||||
};
|
||||
}
|
||||
|
||||
/** Create the standard globe viewer in caller-owned, visible containers. */
|
||||
export function createApplicationViewer({ container, creditContainer }) {
|
||||
export function createApplicationViewer({
|
||||
container,
|
||||
creditContainer,
|
||||
showRenderLoopErrors,
|
||||
}) {
|
||||
if (!container || !creditContainer)
|
||||
throw new TypeError('Viewer and credit containers are required');
|
||||
const viewer = new Cesium.Viewer(
|
||||
container,
|
||||
applicationViewerOptions(creditContainer),
|
||||
applicationViewerOptions(creditContainer, { showRenderLoopErrors }),
|
||||
);
|
||||
try {
|
||||
viewer.targetFrameRate = 60;
|
||||
|
||||
@@ -3404,5 +3404,12 @@
|
||||
"FAA prohibited and restricted special-use airspace only, not the UAS Facility Map or UAS flight restrictions. Informational only — not a substitute for official UAS flight-planning tools (B4UFLY / LAANC).": "المجال الجوي الخاص المحظور والمقيّد لدى FAA فقط، ولا يشمل UAS Facility Map أو قيود طيران UAS. للعلم فقط — ولا يغني عن الأدوات الرسمية لتخطيط رحلات UAS (B4UFLY / LAANC).",
|
||||
"Prohibited": "محظور",
|
||||
"Restricted": "مقيّد",
|
||||
"Prohibited & Restricted Airspace": "المجال الجوي المحظور والمقيّد"
|
||||
"Prohibited & Restricted Airspace": "المجال الجوي المحظور والمقيّد",
|
||||
"The globe stopped drawing": "توقفت الكرة الأرضية عن الرسم",
|
||||
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "استعاد المتصفح رسومات الكرة الأرضية ثلاثية الأبعاد. لا تزال الطبقات وعناصر التحكم تعمل، لكن الخريطة لن تُرسم حتى تُعاد تحميل الصفحة.",
|
||||
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "توقف العرض بعد خطأ. لا تزال الطبقات وعناصر التحكم تعمل، لكن الخريطة لا تتحدث.",
|
||||
"Reset view reloads the app without the view restored from the address bar.": "تعيد «إعادة ضبط العرض» تحميل التطبيق دون العرض المستعاد من شريط العنوان.",
|
||||
"Reload": "إعادة التحميل",
|
||||
"Retry": "إعادة المحاولة",
|
||||
"Reset view": "إعادة ضبط العرض"
|
||||
}
|
||||
@@ -3104,5 +3104,12 @@
|
||||
"The provider rejected the API key. Check it in Provider Settings.": "Der Anbieter hat den API-Schlüssel abgelehnt. Den Schlüssel in den Anbieter-Einstellungen prüfen.",
|
||||
"Too many chat requests. Try again in {seconds} s.": "Zu viele Chat-Anfragen. In {seconds} s erneut versuchen.",
|
||||
"The provider refused access for this key. Check the key's project, region or permissions.": "Der Anbieter hat den Zugriff für diesen Schlüssel verweigert. Projekt, Region oder Berechtigungen des Schlüssels prüfen.",
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "Das Anbieterkonto hat kein Guthaben mehr. Die Abrechnung prüfen oder in den Einstellungen einen anderen KI-Anbieter wählen."
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "Das Anbieterkonto hat kein Guthaben mehr. Die Abrechnung prüfen oder in den Einstellungen einen anderen KI-Anbieter wählen.",
|
||||
"The globe stopped drawing": "Der Globus zeichnet nicht mehr",
|
||||
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Der Browser hat dem Globus die 3D-Grafik entzogen. Ebenen und Bedienelemente funktionieren weiter, aber die Karte kann erst nach dem Neuladen der Seite wieder zeichnen.",
|
||||
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Das Rendern wurde nach einem Fehler angehalten. Ebenen und Bedienelemente funktionieren weiter, aber die Karte wird nicht aktualisiert.",
|
||||
"Reset view reloads the app without the view restored from the address bar.": "Ansicht zurücksetzen lädt die App ohne die aus der Adressleiste wiederhergestellte Ansicht neu.",
|
||||
"Reload": "Neu laden",
|
||||
"Retry": "Erneut versuchen",
|
||||
"Reset view": "Ansicht zurücksetzen"
|
||||
}
|
||||
@@ -3179,5 +3179,12 @@
|
||||
"The provider rejected the API key. Check it in Provider Settings.": "El proveedor rechazó la clave de API. Revísala en Ajustes de proveedores.",
|
||||
"Too many chat requests. Try again in {seconds} s.": "Demasiadas solicitudes de chat. Vuelve a intentarlo en {seconds} s.",
|
||||
"The provider refused access for this key. Check the key's project, region or permissions.": "El proveedor denegó el acceso a esta clave. Revisa el proyecto, la región o los permisos de la clave.",
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "La cuenta del proveedor no tiene crédito. Revisa la facturación o elige otro proveedor de IA en Ajustes."
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "La cuenta del proveedor no tiene crédito. Revisa la facturación o elige otro proveedor de IA en Ajustes.",
|
||||
"The globe stopped drawing": "El globo dejó de dibujarse",
|
||||
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "El navegador retiró los gráficos 3D del globo. Las capas y los controles siguen funcionando, pero el mapa no se dibujará hasta que se recargue la página.",
|
||||
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "El renderizado se detuvo tras un error. Las capas y los controles siguen funcionando, pero el mapa no se actualiza.",
|
||||
"Reset view reloads the app without the view restored from the address bar.": "Restablecer la vista recarga la app sin la vista restaurada desde la barra de direcciones.",
|
||||
"Reload": "Recargar",
|
||||
"Retry": "Reintentar",
|
||||
"Reset view": "Restablecer la vista"
|
||||
}
|
||||
@@ -3179,5 +3179,12 @@
|
||||
"The provider rejected the API key. Check it in Provider Settings.": "Le fournisseur a refusé la clé API. Vérifiez-la dans les paramètres des fournisseurs.",
|
||||
"Too many chat requests. Try again in {seconds} s.": "Trop de requêtes de discussion. Réessayez dans {seconds} s.",
|
||||
"The provider refused access for this key. Check the key's project, region or permissions.": "Le fournisseur a refusé l'accès pour cette clé. Vérifiez le projet, la région ou les autorisations de la clé.",
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "Le compte du fournisseur n'a plus de crédit. Vérifiez la facturation ou choisissez un autre fournisseur d'IA dans les paramètres."
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "Le compte du fournisseur n'a plus de crédit. Vérifiez la facturation ou choisissez un autre fournisseur d'IA dans les paramètres.",
|
||||
"The globe stopped drawing": "Le globe ne s’affiche plus",
|
||||
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Le navigateur a retiré au globe ses graphismes 3D. Les calques et les commandes fonctionnent toujours, mais la carte ne pourra s’afficher qu’après le rechargement de la page.",
|
||||
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Le rendu s’est arrêté après une erreur. Les calques et les commandes fonctionnent toujours, mais la carte ne se met plus à jour.",
|
||||
"Reset view reloads the app without the view restored from the address bar.": "Réinitialiser la vue recharge l’app sans la vue restaurée depuis la barre d’adresse.",
|
||||
"Reload": "Recharger",
|
||||
"Retry": "Réessayer",
|
||||
"Reset view": "Réinitialiser la vue"
|
||||
}
|
||||
@@ -3179,5 +3179,12 @@
|
||||
"The provider rejected the API key. Check it in Provider Settings.": "Il provider ha rifiutato la chiave API. Controllala in Impostazioni provider.",
|
||||
"Too many chat requests. Try again in {seconds} s.": "Troppe richieste di chat. Riprova tra {seconds} s.",
|
||||
"The provider refused access for this key. Check the key's project, region or permissions.": "Il provider ha negato l'accesso per questa chiave. Controlla progetto, regione o autorizzazioni della chiave.",
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "L'account del provider ha esaurito il credito. Controlla la fatturazione o scegli un altro provider IA in Impostazioni."
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "L'account del provider ha esaurito il credito. Controlla la fatturazione o scegli un altro provider IA in Impostazioni.",
|
||||
"The globe stopped drawing": "Il globo ha smesso di disegnare",
|
||||
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Il browser ha ritirato la grafica 3D del globo. Livelli e controlli funzionano ancora, ma la mappa non può essere disegnata finché la pagina non viene ricaricata.",
|
||||
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Il rendering si è fermato dopo un errore. Livelli e controlli funzionano ancora, ma la mappa non si aggiorna.",
|
||||
"Reset view reloads the app without the view restored from the address bar.": "Ripristina vista ricarica l’app senza la vista ripristinata dalla barra degli indirizzi.",
|
||||
"Reload": "Ricarica",
|
||||
"Retry": "Riprova",
|
||||
"Reset view": "Ripristina vista"
|
||||
}
|
||||
@@ -3029,5 +3029,12 @@
|
||||
"The provider rejected the API key. Check it in Provider Settings.": "プロバイダーがAPIキーを拒否しました。プロバイダー設定で確認してください。",
|
||||
"Too many chat requests. Try again in {seconds} s.": "チャットのリクエストが多すぎます。{seconds}秒後にもう一度お試しください。",
|
||||
"The provider refused access for this key. Check the key's project, region or permissions.": "プロバイダーがこのキーでのアクセスを拒否しました。キーのプロジェクト、リージョン、権限を確認してください。",
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "プロバイダーアカウントのクレジットが不足しています。課金状況を確認するか、設定で別のAIプロバイダーを選んでください。"
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "プロバイダーアカウントのクレジットが不足しています。課金状況を確認するか、設定で別のAIプロバイダーを選んでください。",
|
||||
"The globe stopped drawing": "地球儀の描画が止まりました",
|
||||
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "ブラウザーが地球儀の 3D グラフィックスを解放しました。レイヤーと操作は引き続き使えますが、ページを再読み込みするまで地図は描画されません。",
|
||||
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "エラーのため描画が停止しました。レイヤーと操作は引き続き使えますが、地図は更新されません。",
|
||||
"Reset view reloads the app without the view restored from the address bar.": "「ビューをリセット」は、アドレスバーから復元したビューを使わずにアプリを再読み込みします。",
|
||||
"Reload": "再読み込み",
|
||||
"Retry": "再試行",
|
||||
"Reset view": "ビューをリセット"
|
||||
}
|
||||
@@ -3179,5 +3179,12 @@
|
||||
"The provider rejected the API key. Check it in Provider Settings.": "O provedor recusou a chave de API. Verifique-a em \"Configurações de provedores\".",
|
||||
"Too many chat requests. Try again in {seconds} s.": "Chat: limite de requisições atingido. Tente novamente em {seconds} s.",
|
||||
"The provider refused access for this key. Check the key's project, region or permissions.": "O provedor negou o acesso para esta chave. Verifique o projeto, a região ou as permissões da chave.",
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "A conta do provedor está sem crédito. Verifique o faturamento ou escolha outro provedor de IA em \"Configurações\"."
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "A conta do provedor está sem crédito. Verifique o faturamento ou escolha outro provedor de IA em \"Configurações\".",
|
||||
"The globe stopped drawing": "O globo parou de desenhar",
|
||||
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "O navegador retirou os gráficos 3D do globo. As camadas e os controles continuam funcionando, mas o mapa só volta a ser desenhado depois de recarregar a página.",
|
||||
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "A renderização parou após um erro. As camadas e os controles continuam funcionando, mas o mapa não está sendo atualizado.",
|
||||
"Reset view reloads the app without the view restored from the address bar.": "Redefinir vista recarrega o app sem a vista restaurada da barra de endereços.",
|
||||
"Reload": "Recarregar",
|
||||
"Retry": "Tentar novamente",
|
||||
"Reset view": "Redefinir vista"
|
||||
}
|
||||
@@ -3254,5 +3254,12 @@
|
||||
"The provider rejected the API key. Check it in Provider Settings.": "Провайдер отклонил ключ API. Проверьте его в «Настройках провайдеров».",
|
||||
"Too many chat requests. Try again in {seconds} s.": "Слишком много запросов в чате. Повторите попытку через {seconds} с.",
|
||||
"The provider refused access for this key. Check the key's project, region or permissions.": "Провайдер отказал этому ключу в доступе. Проверьте проект, регион или разрешения ключа.",
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "На вашем счёте у провайдера закончились средства. Проверьте оплату или выберите другого провайдера ИИ в настройках."
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "На вашем счёте у провайдера закончились средства. Проверьте оплату или выберите другого провайдера ИИ в настройках.",
|
||||
"The globe stopped drawing": "Глобус перестал отрисовываться",
|
||||
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Браузер отключил 3D-графику глобуса. Слои и элементы управления работают, но карта не отрисуется, пока страница не будет перезагружена.",
|
||||
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Отрисовка остановилась из-за ошибки. Слои и элементы управления работают, но карта не обновляется.",
|
||||
"Reset view reloads the app without the view restored from the address bar.": "«Сбросить вид» перезагружает приложение без вида, восстановленного из адресной строки.",
|
||||
"Reload": "Перезагрузить",
|
||||
"Retry": "Повторить",
|
||||
"Reset view": "Сбросить вид"
|
||||
}
|
||||
@@ -3254,5 +3254,12 @@
|
||||
"The provider rejected the API key. Check it in Provider Settings.": "Постачальник відхилив ключ API. Перевірте його в «Налаштуваннях постачальників».",
|
||||
"Too many chat requests. Try again in {seconds} s.": "Забагато запитів до чату. Спробуйте ще раз через {seconds} с.",
|
||||
"The provider refused access for this key. Check the key's project, region or permissions.": "Постачальник відмовив цьому ключу в доступі. Перевірте проєкт, регіон або дозволи ключа.",
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "На вашому рахунку в постачальника закінчилися кошти. Перевірте оплату або виберіть іншого постачальника ШІ в налаштуваннях."
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "На вашому рахунку в постачальника закінчилися кошти. Перевірте оплату або виберіть іншого постачальника ШІ в налаштуваннях.",
|
||||
"The globe stopped drawing": "Глобус перестав відмальовуватися",
|
||||
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "Браузер вимкнув 3D-графіку глобуса. Шари та елементи керування працюють, але карта не відмалюється, доки сторінку не буде перезавантажено.",
|
||||
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "Відмальовування зупинилося через помилку. Шари та елементи керування працюють, але карта не оновлюється.",
|
||||
"Reset view reloads the app without the view restored from the address bar.": "«Скинути вигляд» перезавантажує застосунок без вигляду, відновленого з адресного рядка.",
|
||||
"Reload": "Перезавантажити",
|
||||
"Retry": "Повторити",
|
||||
"Reset view": "Скинути вигляд"
|
||||
}
|
||||
@@ -3029,5 +3029,12 @@
|
||||
"The provider rejected the API key. Check it in Provider Settings.": "服务商拒绝了 API 密钥。请在服务商设置中检查。",
|
||||
"Too many chat requests. Try again in {seconds} s.": "聊天请求过多。请在 {seconds} 秒后重试。",
|
||||
"The provider refused access for this key. Check the key's project, region or permissions.": "服务商拒绝了此密钥的访问请求。请检查该密钥所属的项目、区域或权限。",
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "服务商账户余额不足。请检查账户的计费信息,或在设置中选择其他 AI 服务商。"
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "服务商账户余额不足。请检查账户的计费信息,或在设置中选择其他 AI 服务商。",
|
||||
"The globe stopped drawing": "地球已停止绘制",
|
||||
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "浏览器收回了地球的 3D 图形。图层和控件仍可使用,但在重新加载页面之前地图无法绘制。",
|
||||
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "渲染因错误而停止。图层和控件仍可使用,但地图不再更新。",
|
||||
"Reset view reloads the app without the view restored from the address bar.": "“重置视图”会重新加载应用,不使用从地址栏恢复的视图。",
|
||||
"Reload": "重新加载",
|
||||
"Retry": "重试",
|
||||
"Reset view": "重置视图"
|
||||
}
|
||||
@@ -3029,5 +3029,12 @@
|
||||
"The provider rejected the API key. Check it in Provider Settings.": "服務商拒絕了 API 密鑰。請在服務商設置中檢查。",
|
||||
"Too many chat requests. Try again in {seconds} s.": "聊天請求過多。請在 {seconds} 秒後重試。",
|
||||
"The provider refused access for this key. Check the key's project, region or permissions.": "服務商拒絕了此密鑰的訪問請求。請檢查該密鑰所屬的項目、區域或權限。",
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "服務商賬戶餘額不足。請檢查賬戶的計費信息,或在設置中選擇其他 AI 服務商。"
|
||||
"The provider account is out of credit. Check its billing, or choose another AI Provider in Settings.": "服務商賬戶餘額不足。請檢查賬戶的計費信息,或在設置中選擇其他 AI 服務商。",
|
||||
"The globe stopped drawing": "地球已停止繪製",
|
||||
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.": "瀏覽器收回了地球的 3D 圖形。圖層和控件仍可使用,但在重新加載頁面之前地圖無法繪製。",
|
||||
"Rendering stopped after an error. Layers and controls still work, but the map is not updating.": "渲染因錯誤而停止。圖層和控件仍可使用,但地圖不再更新。",
|
||||
"Reset view reloads the app without the view restored from the address bar.": "「重置視圖」會重新加載應用,不使用從地址欄恢復的視圖。",
|
||||
"Reload": "重新加載",
|
||||
"Retry": "重試",
|
||||
"Reset view": "重置視圖"
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { useStatusStore } from './stores/status.js';
|
||||
import { applicationPinia } from './vueRuntime.js';
|
||||
import { describeError } from '../app/errors.js';
|
||||
import { appRootUrl } from '../share/shareLinkPath.js';
|
||||
|
||||
/**
|
||||
* Tell the user when the globe stops drawing, rather than leave a black globe
|
||||
* behind controls that still respond. Cesium stops its render loop after a
|
||||
* render error, and a page whose WebGL context was lost never draws again.
|
||||
*
|
||||
* The notice is published to the status store, which the scene chrome
|
||||
* renders. Retry restarts the render loop after an error, and the notice stays
|
||||
* until a frame renders without one; after a context loss it reloads the page,
|
||||
* whose address keeps the view. Reset opens the app's root address, without
|
||||
* the view restored from the address bar. Where the page must not reload
|
||||
* itself (`allowReload: false`), Reset is left out, and so is Retry after a
|
||||
* context loss.
|
||||
*
|
||||
* Returns a function that removes the listeners and the notice.
|
||||
*/
|
||||
export function installRenderFailureNotice(
|
||||
viewer,
|
||||
{
|
||||
store = useStatusStore(applicationPinia()),
|
||||
locationRef = globalThis.location,
|
||||
allowReload = true,
|
||||
logError = (error) =>
|
||||
console.error('[Render] The globe stopped drawing:', error),
|
||||
} = {},
|
||||
) {
|
||||
const scene = viewer?.scene;
|
||||
if (!scene?.renderError) return () => {};
|
||||
const canvas = viewer.canvas ?? scene.canvas;
|
||||
let notice = null;
|
||||
let shown = 0;
|
||||
let contextLost = false;
|
||||
let frameFailed = false;
|
||||
let removeFrameListeners = null;
|
||||
|
||||
function retry() {
|
||||
if (contextLost) {
|
||||
locationRef.reload();
|
||||
return;
|
||||
}
|
||||
viewer.useDefaultRenderLoop = true;
|
||||
scene.requestRender?.();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
locationRef.assign(appRootUrl(locationRef.href));
|
||||
}
|
||||
|
||||
function stopWatchingFrames() {
|
||||
removeFrameListeners?.();
|
||||
removeFrameListeners = null;
|
||||
}
|
||||
|
||||
function clear() {
|
||||
stopWatchingFrames();
|
||||
if (store.renderFailure === notice) store.publishRenderFailure(null);
|
||||
notice = null;
|
||||
}
|
||||
|
||||
// Cesium raises postRender after a failed frame too, so only a frame that
|
||||
// starts and ends without a render error clears the notice.
|
||||
function watchFrames() {
|
||||
if (removeFrameListeners || contextLost) return;
|
||||
const removeStart = scene.preUpdate.addEventListener(() => {
|
||||
frameFailed = false;
|
||||
});
|
||||
const removeEnd = scene.postRender.addEventListener(() => {
|
||||
if (!frameFailed) clear();
|
||||
});
|
||||
removeFrameListeners = () => {
|
||||
removeStart();
|
||||
removeEnd();
|
||||
};
|
||||
}
|
||||
|
||||
function present(detail) {
|
||||
const reason = contextLost ? 'context-lost' : 'render-error';
|
||||
// A loop restarted into the same error keeps the notice as it was.
|
||||
if (notice?.reason === reason && notice.detail === detail) return;
|
||||
notice = {
|
||||
reason,
|
||||
detail,
|
||||
shown: notice ? notice.shown : ++shown,
|
||||
retry: contextLost && !allowReload ? null : retry,
|
||||
reset: allowReload ? reset : null,
|
||||
};
|
||||
store.publishRenderFailure(notice);
|
||||
watchFrames();
|
||||
}
|
||||
|
||||
const onRenderError = (_scene, error) => {
|
||||
frameFailed = true;
|
||||
logError(error);
|
||||
present(describeError(error));
|
||||
};
|
||||
const onContextLost = () => {
|
||||
contextLost = true;
|
||||
// No frame can clear this one; only a reload draws again.
|
||||
stopWatchingFrames();
|
||||
present('');
|
||||
};
|
||||
|
||||
const removeRenderError = scene.renderError.addEventListener(onRenderError);
|
||||
canvas?.addEventListener?.('webglcontextlost', onContextLost);
|
||||
return () => {
|
||||
removeRenderError();
|
||||
canvas?.removeEventListener?.('webglcontextlost', onContextLost);
|
||||
clear();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { installRenderFailureNotice } from './renderFailure.js';
|
||||
|
||||
function event() {
|
||||
const listeners = new Set();
|
||||
return {
|
||||
listeners,
|
||||
addEventListener(listener) {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
raise(...args) {
|
||||
for (const listener of [...listeners]) listener(...args);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fixture(t, options = {}) {
|
||||
const canvasListeners = new Map();
|
||||
const scene = {
|
||||
renderError: event(),
|
||||
preUpdate: event(),
|
||||
postRender: event(),
|
||||
requestRender() {
|
||||
this.requests = (this.requests ?? 0) + 1;
|
||||
},
|
||||
};
|
||||
const viewer = {
|
||||
scene,
|
||||
useDefaultRenderLoop: true,
|
||||
canvas: {
|
||||
addEventListener: (name, listener) => canvasListeners.set(name, listener),
|
||||
removeEventListener: (name, listener) => {
|
||||
if (canvasListeners.get(name) === listener)
|
||||
canvasListeners.delete(name);
|
||||
},
|
||||
},
|
||||
};
|
||||
const store = {
|
||||
renderFailure: null,
|
||||
published: [],
|
||||
publishRenderFailure(next) {
|
||||
this.renderFailure = next;
|
||||
this.published.push(next);
|
||||
},
|
||||
};
|
||||
const visits = [];
|
||||
const locationRef = {
|
||||
href: 'https://gev.example/share/AQc?lang=ru#lat=48.8584&lon=2.2945',
|
||||
reload: () => visits.push('reload'),
|
||||
assign: (url) => visits.push(String(url)),
|
||||
};
|
||||
const logged = [];
|
||||
const uninstall = installRenderFailureNotice(viewer, {
|
||||
store,
|
||||
locationRef,
|
||||
logError: (error) => logged.push(error),
|
||||
...options,
|
||||
});
|
||||
t.after(uninstall);
|
||||
// One Cesium frame: preUpdate, an optional render error, then postRender,
|
||||
// which Cesium raises after a failed frame too.
|
||||
const frame = (error) => {
|
||||
scene.preUpdate.raise(scene);
|
||||
if (error) {
|
||||
viewer.useDefaultRenderLoop = false;
|
||||
scene.renderError.raise(scene, error);
|
||||
}
|
||||
scene.postRender.raise(scene);
|
||||
};
|
||||
return {
|
||||
viewer,
|
||||
scene,
|
||||
store,
|
||||
visits,
|
||||
logged,
|
||||
uninstall,
|
||||
frame,
|
||||
loseContext: () => canvasListeners.get('webglcontextlost')?.({}),
|
||||
canvasListeners,
|
||||
};
|
||||
}
|
||||
|
||||
test('a render error shows a notice that survives the failed frame', (t) => {
|
||||
const f = fixture(t);
|
||||
assert.equal(f.store.renderFailure, null);
|
||||
const error = new Error('Expected width to be greater than 0');
|
||||
f.frame(error);
|
||||
const notice = f.store.renderFailure;
|
||||
assert.equal(notice.reason, 'render-error');
|
||||
assert.equal(notice.detail, 'Expected width to be greater than 0');
|
||||
assert.equal(notice.shown, 1);
|
||||
assert.equal(typeof notice.retry, 'function');
|
||||
assert.equal(typeof notice.reset, 'function');
|
||||
assert.deepEqual(f.logged, [error]);
|
||||
});
|
||||
|
||||
test('Retry restarts the render loop and a clean frame clears the notice', (t) => {
|
||||
const f = fixture(t);
|
||||
f.frame(new Error('boom'));
|
||||
f.store.renderFailure.retry();
|
||||
assert.equal(f.viewer.useDefaultRenderLoop, true);
|
||||
assert.equal(f.scene.requests, 1);
|
||||
assert.ok(f.store.renderFailure, 'the notice waits for a frame');
|
||||
f.frame();
|
||||
assert.equal(f.store.renderFailure, null);
|
||||
// Clearing stops the per-frame listeners.
|
||||
assert.equal(f.scene.preUpdate.listeners.size, 0);
|
||||
assert.equal(f.scene.postRender.listeners.size, 0);
|
||||
});
|
||||
|
||||
test('a loop restarted into the same error keeps one notice', (t) => {
|
||||
const f = fixture(t);
|
||||
f.frame(new Error('boom'));
|
||||
const first = f.store.renderFailure;
|
||||
f.store.renderFailure.retry();
|
||||
f.frame(new Error('boom'));
|
||||
assert.equal(f.store.renderFailure, first);
|
||||
assert.equal(f.store.published.length, 1);
|
||||
f.frame(new Error('other'));
|
||||
assert.equal(f.store.renderFailure.detail, 'other');
|
||||
assert.equal(f.store.renderFailure.shown, first.shown, 'no new appearance');
|
||||
});
|
||||
|
||||
test('a new failure after recovery is a new appearance', (t) => {
|
||||
const f = fixture(t);
|
||||
f.frame(new Error('boom'));
|
||||
f.store.renderFailure.retry();
|
||||
f.frame();
|
||||
f.frame(new Error('boom'));
|
||||
assert.equal(f.store.renderFailure.shown, 2);
|
||||
});
|
||||
|
||||
test('Reset opens the app root without the restored view', (t) => {
|
||||
const f = fixture(t);
|
||||
f.frame(new Error('boom'));
|
||||
f.store.renderFailure.reset();
|
||||
assert.deepEqual(f.visits, ['https://gev.example/?lang=ru']);
|
||||
});
|
||||
|
||||
test('a lost context offers a reload and no frame clears it', (t) => {
|
||||
const f = fixture(t);
|
||||
f.loseContext();
|
||||
const notice = f.store.renderFailure;
|
||||
assert.equal(notice.reason, 'context-lost');
|
||||
assert.equal(notice.detail, '');
|
||||
// Cesium's next frame then fails on the dead drawing buffer.
|
||||
f.frame(new Error('Expected width to be greater than 0'));
|
||||
assert.equal(f.store.renderFailure.reason, 'context-lost');
|
||||
f.frame();
|
||||
assert.ok(f.store.renderFailure, 'a lost context never draws again');
|
||||
f.store.renderFailure.retry();
|
||||
assert.deepEqual(f.visits, ['reload']);
|
||||
});
|
||||
|
||||
test('a page that must not reload offers neither reload nor reset', (t) => {
|
||||
const f = fixture(t, { allowReload: false });
|
||||
f.frame(new Error('boom'));
|
||||
assert.equal(typeof f.store.renderFailure.retry, 'function');
|
||||
assert.equal(f.store.renderFailure.reset, null);
|
||||
f.loseContext();
|
||||
assert.equal(f.store.renderFailure.retry, null);
|
||||
assert.equal(f.store.renderFailure.reset, null);
|
||||
});
|
||||
|
||||
test('uninstall removes the listeners and the notice', (t) => {
|
||||
const f = fixture(t);
|
||||
f.frame(new Error('boom'));
|
||||
f.uninstall();
|
||||
assert.equal(f.store.renderFailure, null);
|
||||
assert.equal(f.scene.renderError.listeners.size, 0);
|
||||
assert.equal(f.scene.postRender.listeners.size, 0);
|
||||
assert.equal(f.canvasListeners.size, 0);
|
||||
f.frame(new Error('later'));
|
||||
assert.equal(f.store.renderFailure, null);
|
||||
});
|
||||
|
||||
test('a viewer without a scene installs nothing', () => {
|
||||
const uninstall = installRenderFailureNotice(null, {
|
||||
store: { publishRenderFailure: () => assert.fail('nothing to publish') },
|
||||
});
|
||||
uninstall();
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
// src/ui/renderFailureView.test.mjs — the scene chrome renders the render
|
||||
// failure notice installRenderFailureNotice() publishes, with its actions.
|
||||
import '../testSupport/registerDom.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createApp, nextTick } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { useStatusStore } from './stores/status.js';
|
||||
import SceneChromeView from './views/SceneChromeView.vue';
|
||||
|
||||
function mount(t) {
|
||||
document.body.replaceChildren();
|
||||
const pinia = createPinia();
|
||||
const app = createApp(SceneChromeView).use(pinia);
|
||||
app.mount(document.body.appendChild(document.createElement('div')));
|
||||
t.after(() => app.unmount());
|
||||
return useStatusStore(pinia);
|
||||
}
|
||||
|
||||
const notice = () => document.getElementById('render-failure');
|
||||
const buttons = () =>
|
||||
[...(notice()?.querySelectorAll('button') ?? [])].map((button) =>
|
||||
button.textContent.trim(),
|
||||
);
|
||||
|
||||
test('a render failure shows an alert dialog that moves focus to Retry', async (t) => {
|
||||
const store = mount(t);
|
||||
assert.equal(notice(), null, 'no notice while the globe draws');
|
||||
const calls = [];
|
||||
store.publishRenderFailure({
|
||||
reason: 'render-error',
|
||||
detail: 'Expected width to be greater than 0',
|
||||
shown: 1,
|
||||
retry: () => calls.push('retry'),
|
||||
reset: () => calls.push('reset'),
|
||||
});
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
const element = notice();
|
||||
assert.equal(element.getAttribute('role'), 'alertdialog');
|
||||
assert.equal(
|
||||
document.getElementById(element.getAttribute('aria-labelledby'))
|
||||
.textContent,
|
||||
'The globe stopped drawing',
|
||||
);
|
||||
assert.match(
|
||||
document.getElementById(element.getAttribute('aria-describedby'))
|
||||
.textContent,
|
||||
/^Rendering stopped after an error\./,
|
||||
);
|
||||
assert.equal(
|
||||
element.querySelector('.render-failure-detail').textContent,
|
||||
'Expected width to be greater than 0',
|
||||
);
|
||||
assert.deepEqual(buttons(), ['Retry', 'Reset view']);
|
||||
assert.equal(document.activeElement?.textContent.trim(), 'Retry');
|
||||
for (const button of element.querySelectorAll('button')) button.click();
|
||||
assert.deepEqual(calls, ['retry', 'reset']);
|
||||
store.publishRenderFailure(null);
|
||||
await nextTick();
|
||||
assert.equal(notice(), null);
|
||||
});
|
||||
|
||||
test('a lost context offers Reload, and a panel that cannot reload offers nothing', async (t) => {
|
||||
const store = mount(t);
|
||||
store.publishRenderFailure({
|
||||
reason: 'context-lost',
|
||||
detail: '',
|
||||
shown: 1,
|
||||
retry: () => {},
|
||||
reset: () => {},
|
||||
});
|
||||
await nextTick();
|
||||
assert.match(
|
||||
document.getElementById('render-failure-message').textContent,
|
||||
/3D graphics/,
|
||||
);
|
||||
assert.equal(notice().querySelector('.render-failure-detail'), null);
|
||||
assert.deepEqual(buttons(), ['Reload', 'Reset view']);
|
||||
store.publishRenderFailure({
|
||||
reason: 'context-lost',
|
||||
detail: '',
|
||||
shown: 1,
|
||||
retry: null,
|
||||
reset: null,
|
||||
});
|
||||
await nextTick();
|
||||
assert.deepEqual(buttons(), []);
|
||||
assert.equal(notice().querySelector('.render-failure-hint'), null);
|
||||
});
|
||||
+18
-1
@@ -7,7 +7,9 @@ import { markRaw, shallowRef } from 'vue';
|
||||
* road network chip, CctvControls the camera grid chip; the toast shows
|
||||
* what any owner's createToast() (src/ui/toast.js) publishes, and the title
|
||||
* bar the FPS readout createFrameRateMonitor() publishes. Each label keeps its
|
||||
* last text while hidden, so the next change flaps from what was shown.
|
||||
* last text while hidden, so the next change flaps from what was shown. The
|
||||
* render failure notice is what installRenderFailureNotice()
|
||||
* (src/ui/renderFailure.js) publishes while the globe cannot draw.
|
||||
*/
|
||||
export const useStatusStore = defineStore('gev-status', () => {
|
||||
/**
|
||||
@@ -32,6 +34,14 @@ export const useStatusStore = defineStore('gev-status', () => {
|
||||
*/
|
||||
const fps = shallowRef(null);
|
||||
|
||||
/**
|
||||
* The globe's render failure, `{reason, detail, shown, retry, reset}`; null
|
||||
* while the globe draws. `reason` is 'render-error' or 'context-lost',
|
||||
* `shown` changes each time the notice appears, and `retry` and `reset` are
|
||||
* null where that action is not offered.
|
||||
*/
|
||||
const renderFailure = shallowRef(null);
|
||||
|
||||
/** @param {?object} next A fresh presentation of the loading status. */
|
||||
function publishLoading(next) {
|
||||
loading.value = next && markRaw(next);
|
||||
@@ -57,6 +67,11 @@ export const useStatusStore = defineStore('gev-status', () => {
|
||||
fps.value = next && markRaw(next);
|
||||
}
|
||||
|
||||
/** @param {?object} next A fresh presentation of the render failure. */
|
||||
function publishRenderFailure(next) {
|
||||
renderFailure.value = next && markRaw(next);
|
||||
}
|
||||
|
||||
return {
|
||||
fps,
|
||||
publishFps,
|
||||
@@ -68,5 +83,7 @@ export const useStatusStore = defineStore('gev-status', () => {
|
||||
publishTraffic,
|
||||
cctv,
|
||||
publishCctv,
|
||||
renderFailure,
|
||||
publishRenderFailure,
|
||||
};
|
||||
});
|
||||
@@ -387,6 +387,69 @@ html:not([lang='en']) #global-loading-detail {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Render failure notice ───────────────────── */
|
||||
/* Above the loading screen: a globe that fails while it loads says so too. */
|
||||
#render-failure {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1001;
|
||||
box-sizing: border-box;
|
||||
width: min(420px, calc(100vw - 32px));
|
||||
max-height: calc(100vh - 32px);
|
||||
overflow-y: auto;
|
||||
padding: 20px 22px;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid #ff7272;
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
box-shadow: 0 0 24px rgba(255, 88, 88, 0.18);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#render-failure-title {
|
||||
margin: 0 0 8px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
color: #ff7272;
|
||||
}
|
||||
|
||||
#render-failure p {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
#render-failure .render-failure-detail {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
#render-failure .render-failure-hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
#render-failure .render-failure-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
#render-failure .gev-action-button {
|
||||
font-size: 11px;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
/* ── Toast Notification ──────────────────────── */
|
||||
#toast {
|
||||
position: fixed;
|
||||
|
||||
@@ -18,6 +18,7 @@ import ScreenAnnotations from './panels/ScreenAnnotations.vue';
|
||||
import AwarenessDirection from './panels/AwarenessDirection.vue';
|
||||
import UiButton from '../kit/UiButton.vue';
|
||||
import GlobeActions from './panels/GlobeActions.vue';
|
||||
import RenderFailure from './panels/RenderFailure.vue';
|
||||
|
||||
// The ACTIVE STYLE corner shows the style VisualSettings publishes.
|
||||
const { style } = storeToRefs(useDisplayStore());
|
||||
@@ -60,6 +61,7 @@ const vToastText = {
|
||||
|
||||
<template>
|
||||
<div id="cesiumContainer"></div>
|
||||
<RenderFailure />
|
||||
<div id="world-overlay-root" aria-hidden="true">
|
||||
<canvas id="world-overlay-canvas"></canvas>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { msg, t } from '../../../i18n/index.js';
|
||||
import { useStatusStore } from '../../stores/status.js';
|
||||
import UiButton from '../../kit/UiButton.vue';
|
||||
|
||||
// The notice installRenderFailureNotice() (src/ui/renderFailure.js) publishes
|
||||
// while the globe cannot draw, with the actions it offers.
|
||||
const { renderFailure: failure } = storeToRefs(useStatusStore());
|
||||
const panel = ref(null);
|
||||
const MESSAGES = {
|
||||
'context-lost': msg(
|
||||
"The browser took back the globe's 3D graphics. Layers and controls still work, but the map cannot draw until the page reloads.",
|
||||
),
|
||||
'render-error': msg(
|
||||
'Rendering stopped after an error. Layers and controls still work, but the map is not updating.',
|
||||
),
|
||||
};
|
||||
// Focus moves to the first action each time the notice appears, so a
|
||||
// keyboard user can act on it at once.
|
||||
watch(
|
||||
() => failure.value?.shown,
|
||||
async (shown) => {
|
||||
if (!shown) return;
|
||||
await nextTick();
|
||||
panel.value?.querySelector('button')?.focus();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="failure" id="render-failure" ref="panel" role="alertdialog" aria-labelledby="render-failure-title" aria-describedby="render-failure-message">
|
||||
<h2 id="render-failure-title">{{ t('The globe stopped drawing') }}</h2>
|
||||
<p id="render-failure-message">{{ t(MESSAGES[failure.reason] ?? MESSAGES['render-error']) }}</p>
|
||||
<p v-if="failure.detail" class="render-failure-detail" translate="no">{{ failure.detail }}</p>
|
||||
<p v-if="failure.reset" class="render-failure-hint">{{ t('Reset view reloads the app without the view restored from the address bar.') }}</p>
|
||||
<div v-if="failure.retry || failure.reset" class="render-failure-actions">
|
||||
<UiButton v-if="failure.retry" class="gev-action-button render-failure-retry" :onClick="failure.retry">{{ failure.reason === 'context-lost' ? t('Reload') : t('Retry') }}</UiButton>
|
||||
<UiButton v-if="failure.reset" class="gev-action-button render-failure-reset" :onClick="failure.reset">{{ t('Reset view') }}</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user