From 156ddd113e84609719e24ec3812d3a8da623bea7 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 18 Jun 2026 16:06:32 +0200 Subject: [PATCH] Added handler for WebGL unavailability in map widget (#15640) * Added handler for WebGL unavailability in map widget * Changed position of toast * Additional fixes * Code improvements --- .../components/widget/lib/maps/geo-map.ts | 20 +++++++++--- .../widget/lib/maps/leaflet/leaflet-tb.ts | 31 +++++++++++++++++-- .../components/widget/lib/maps/map-layer.ts | 11 +++++-- .../shared/models/widget/maps/map.models.ts | 2 ++ .../assets/locale/locale.constant-en_US.json | 1 + 5 files changed, 55 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts index bc1e5f8f89..405be8729e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts @@ -18,13 +18,13 @@ import { DEFAULT_ZOOM_LEVEL, defaultGeoMapSettings, GeoMapSettings, - latLngPointToBounds, + latLngPointToBounds, MapControlsPosition, MapZoomAction, TbCircleData, TbPolygonCoordinate, TbPolygonCoordinates, TbPolygonRawCoordinate, - TbPolygonRawCoordinates + TbPolygonRawCoordinates, WEBGL_ERROR_EVENT } from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; @@ -92,9 +92,19 @@ export class TbGeoMap extends TbMap { return this.loadLayers().pipe( tap((layers: L.TB.LayerData[]) => { if (layers.length) { - const layer = layers[0]; - layer.layer.addTo(this.map); - this.map.attributionControl.setPrefix(layer.attributionPrefix); + const defaultLayer = layers[0]; + layers.forEach(layer => { + layer.layer.once(WEBGL_ERROR_EVENT, () => { + const toastPosition = this.settings.controlsPosition === MapControlsPosition.bottomleft + || this.settings.controlsPosition === MapControlsPosition.bottomright ? 'top' : 'bottom'; + this.ctx.showErrorToast( + this.ctx.translate.instant('widgets.maps.layer.webgl-not-available'), + toastPosition, 'left', this.ctx.toastTargetId, true + ); + }); + }); + defaultLayer.layer.addTo(this.map); + this.map.attributionControl.setPrefix(defaultLayer.attributionPrefix); if (layers.length > 1) { const sidebar = this.getSidebar(); L.TB.layers({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts index c16d5cc67c..72e7e5b324 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts @@ -24,6 +24,7 @@ import { MatIconRegistry } from '@angular/material/icon'; import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; import { catchError, take } from 'rxjs/operators'; import { of } from 'rxjs'; +import { WEBGL_ERROR_EVENT } from '@shared/models/widget/maps/map.models'; L.MarkerCluster = L.MarkerCluster.mergeOptions({ pmIgnore: true }); @@ -1093,6 +1094,7 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { private _actualCanvas: HTMLCanvasElement; private _offset: L.Point; private _zooming: boolean; + private _glError = false; constructor(options: TB.MapLibreGL.LeafletMapLibreGLMapOptions) { super(); @@ -1136,7 +1138,9 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { const paneName = this.getPaneName(); map.getPane(paneName).removeChild(this._container); - this._glMap.remove(); + if (this._glMap) { + this._glMap.remove(); + } this._glMap = null; return this; @@ -1204,7 +1208,18 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { zoom: this._map.getZoom() - 1, attributionControl: false }); - this._glMap = new MapLibreGLMap(options); + this._glError = false; + try { + this._glMap = new MapLibreGLMap(options); + } catch (e) { + this._glError = true; + this.fire(WEBGL_ERROR_EVENT, { error: e }); + return; + } + this._glMap.once('webglcontextlost', (e) => { + this._glError = true; + this.fire(WEBGL_ERROR_EVENT, {error: e}); + }); this._glMap.once('load', () => { this.fire('load'); }); @@ -1222,8 +1237,12 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { } } + private _glReady(): boolean { + return !this._glError && !!this._glMap; + } + private _update() { - if (!this._map) { + if (!this._map || !this._glReady()) { return; } this._offset = this._map.containerPointToLayerPoint([0, 0]); @@ -1253,6 +1272,7 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { private _transformGL(gl: MapLibreGLMap) { const center = this._map.getCenter(); const tr = gl._getTransformForUpdate(); + if (!tr) { return; } tr.setCenter(MapLibreGLLngLat.convert([center.lng, center.lat])); tr.setZoom(this._map.getZoom() - 1); gl.transform.apply(tr); @@ -1260,6 +1280,7 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { } private _pinchZoom() { + if (!this._glReady()) { return; } this._glMap.jumpTo({ zoom: this._map.getZoom() - 1, center: this._map.getCenter() @@ -1267,6 +1288,7 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { } private _animateZoom(e: L.ZoomAnimEvent) { + if (!this._glReady() || !this._actualCanvas) { return; } const scale = this._map.getZoomScale(e.zoom); const padding = this._map.getSize().multiplyBy(this.options.padding * scale); const viewHalf = this.getSize().divideBy(2); @@ -1291,6 +1313,7 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { } private _zoomEnd() { + if (!this._glReady() || !this._actualCanvas) { return; } const scale = this._map.getZoomScale(this._map.getZoom()); L.DomUtil.setTransform( this._actualCanvas, @@ -1302,7 +1325,9 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { } private _transitionEnd() { + if (!this._glReady()) { return; } L.Util.requestAnimFrame(() => { + if (!this._glReady()) { return; } const zoom = this._map.getZoom(); const center = this._map.getCenter(); const offset = this._map.latLngToContainerPoint( diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts index 2953c609ce..4bc2dbf231 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts @@ -28,14 +28,14 @@ import { MapProvider, OpenStreetMapLayerSettings, ReferenceLayerType, - TencentMapLayerSettings + TencentMapLayerSettings, WEBGL_ERROR_EVENT } from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; import { mergeDeep } from '@core/utils'; import { Observable, of, shareReplay, switchMap } from 'rxjs'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; -import L from 'leaflet'; +import L, { LeafletEvent } from 'leaflet'; import { catchError, map } from 'rxjs/operators'; import { ResourcesService } from '@core/services/resources.service'; import { StyleSpecification } from '@maplibre/maplibre-gl-style-spec'; @@ -120,6 +120,13 @@ export abstract class TbMapLayer { let referenceLayerLoaded = false; baseLayer.addTo(layer); referenceLayer.addTo(layer); + // Forwarding is needed only here: when a reference layer is present the base layer is + // wrapped in this featureGroup, and geo-map listens for WEBGL_ERROR_EVENT on the + // returned layer (the group). In the other branches the MapLibreGLLayer is returned + // directly and fires WEBGL_ERROR_EVENT itself, so no forwarding is required. + baseLayer.once(WEBGL_ERROR_EVENT, (e: LeafletEvent) => { + layer.fire(WEBGL_ERROR_EVENT, e); + }); baseLayer.once('load', () => { baseLayerLoaded = true; if (referenceLayerLoaded) { diff --git a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts index 6db3b1481b..e8141e9eb1 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts @@ -1127,6 +1127,8 @@ export interface ImageMapSourceSettings { entityKey?: DataKey; } +export const WEBGL_ERROR_EVENT = 'gl-error'; + export const imageMapSourceSettingsValid = (imageSource: ImageMapSourceSettings): boolean => { if (!imageSource?.sourceType) { return false; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 78308369f5..acfe5c344a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -8538,6 +8538,7 @@ "roadmap": "Roadmap", "satellite": "Satellite", "hybrid": "Hybrid", + "webgl-not-available": "The selected map layer requires graphics acceleration to display. Please enable 'Hardware acceleration' in your browser settings, or switch to a different layer.", "reference": { "reference-layer": "Reference layer", "no-layer": "No layer",