From 7ee4fab433b429ef461520601d004b315273ce0c Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 14 May 2026 16:57:03 +0200 Subject: [PATCH 1/3] Added handler for WebGL unavailability in map widget --- .../components/widget/lib/maps/geo-map.ts | 6 +++++ .../widget/lib/maps/leaflet/leaflet-tb.ts | 25 ++++++++++++++++--- .../components/widget/lib/maps/map-layer.ts | 3 +++ .../assets/locale/locale.constant-en_US.json | 3 +++ 4 files changed, 34 insertions(+), 3 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..3361b4ee6a 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 @@ -93,6 +93,12 @@ export class TbGeoMap extends TbMap { tap((layers: L.TB.LayerData[]) => { if (layers.length) { const layer = layers[0]; + layer.layer.once('gl-error', () => { + this.ctx.showErrorToast( + this.ctx.translate.instant('widgets.maps.gl.webgl-not-available'), + 'top', 'center', this.ctx.toastTargetId, true + ); + }); layer.layer.addTo(this.map); this.map.attributionControl.setPrefix(layer.attributionPrefix); if (layers.length > 1) { 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..5ba313f506 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 @@ -1093,6 +1093,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 +1137,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 +1207,17 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { zoom: this._map.getZoom() - 1, attributionControl: false }); - this._glMap = new MapLibreGLMap(options); + try { + this._glMap = new MapLibreGLMap(options); + } catch (e) { + this._glError = true; + this.fire('gl-error', { error: e }); + return; + } + this._glMap.once('webglcontextlost', () => { + this._glError = true; + this.fire('gl-error', {}); + }); this._glMap.once('load', () => { this.fire('load'); }); @@ -1223,7 +1236,7 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { } private _update() { - if (!this._map) { + if (!this._map || this._glError) { return; } this._offset = this._map.containerPointToLayerPoint([0, 0]); @@ -1253,6 +1266,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 +1274,7 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { } private _pinchZoom() { + if (this._glError) { return; } this._glMap.jumpTo({ zoom: this._map.getZoom() - 1, center: this._map.getCenter() @@ -1267,6 +1282,7 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { } private _animateZoom(e: L.ZoomAnimEvent) { + if (this._glError || !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 +1307,7 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { } private _zoomEnd() { + if (this._glError || !this._actualCanvas) { return; } const scale = this._map.getZoomScale(this._map.getZoom()); L.DomUtil.setTransform( this._actualCanvas, @@ -1302,7 +1319,9 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { } private _transitionEnd() { + if (this._glError) { return; } L.Util.requestAnimFrame(() => { + if (this._glError) { 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..08a5eb2143 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 @@ -120,6 +120,9 @@ export abstract class TbMapLayer { let referenceLayerLoaded = false; baseLayer.addTo(layer); referenceLayer.addTo(layer); + baseLayer.once('gl-error', (e: any) => { + layer.fire('gl-error', e); + }); baseLayer.once('load', () => { baseLayerLoaded = true; if (referenceLayerLoaded) { 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 039e7a33e7..182089b4e2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -8249,6 +8249,9 @@ "default-map-zoom-level": "Default map zoom level", "entities-limit": "Limit of entities to load" }, + "gl": { + "webgl-not-available": "The browser tried to use the GPU to draw the map, but the GPU is turned off or blocked." + }, "layer": { "label": "Label", "layer": "Layer", From 4151932a5b0cf8cc2e798dedf072c41ec90e0e9c Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 15 May 2026 12:10:09 +0200 Subject: [PATCH 2/3] Changed position of toast --- .../src/app/modules/home/components/widget/lib/maps/geo-map.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3361b4ee6a..a55d9b6b96 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 @@ -96,7 +96,7 @@ export class TbGeoMap extends TbMap { layer.layer.once('gl-error', () => { this.ctx.showErrorToast( this.ctx.translate.instant('widgets.maps.gl.webgl-not-available'), - 'top', 'center', this.ctx.toastTargetId, true + 'bottom', 'left', this.ctx.toastTargetId, true ); }); layer.layer.addTo(this.map); From 20309f7adff1998a1d781c75bf27136264850ef7 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 25 May 2026 11:57:07 +0200 Subject: [PATCH 3/3] Additional fixes --- .../home/components/widget/lib/maps/geo-map.ts | 17 ++++++++++------- .../widget/lib/maps/leaflet/leaflet-tb.ts | 5 +++-- .../components/widget/lib/maps/map-layer.ts | 4 ++-- .../assets/locale/locale.constant-en_US.json | 4 +--- 4 files changed, 16 insertions(+), 14 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 a55d9b6b96..d7a8261d1d 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,7 +18,7 @@ import { DEFAULT_ZOOM_LEVEL, defaultGeoMapSettings, GeoMapSettings, - latLngPointToBounds, + latLngPointToBounds, MapControlsPosition, MapZoomAction, TbCircleData, TbPolygonCoordinate, @@ -93,12 +93,15 @@ export class TbGeoMap extends TbMap { tap((layers: L.TB.LayerData[]) => { if (layers.length) { const layer = layers[0]; - layer.layer.once('gl-error', () => { - this.ctx.showErrorToast( - this.ctx.translate.instant('widgets.maps.gl.webgl-not-available'), - 'bottom', 'left', this.ctx.toastTargetId, true - ); - }); + layers.forEach(layer => { + layer.layer.once('gl-error', () => { + const toastPosition = this.settings.controlsPosition === MapControlsPosition.bottomleft || this.settings.controlsPosition === MapControlsPosition.bottomright ? 'top' : 'bottom'; + this.ctx.showErrorToast( + this.ctx.translate.instant('widgets.maps.gl.webgl-not-available'), + toastPosition, 'left', this.ctx.toastTargetId, true + ); + }); + }) layer.layer.addTo(this.map); this.map.attributionControl.setPrefix(layer.attributionPrefix); if (layers.length > 1) { 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 5ba313f506..0d3eaf2b08 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 @@ -1207,6 +1207,7 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { zoom: this._map.getZoom() - 1, attributionControl: false }); + this._glError = false; try { this._glMap = new MapLibreGLMap(options); } catch (e) { @@ -1214,9 +1215,9 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer { this.fire('gl-error', { error: e }); return; } - this._glMap.once('webglcontextlost', () => { + this._glMap.once('webglcontextlost', (e) => { this._glError = true; - this.fire('gl-error', {}); + this.fire('gl-error', {error: e}); }); this._glMap.once('load', () => { this.fire('load'); 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 08a5eb2143..be4ae212b8 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 @@ -35,7 +35,7 @@ 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,7 +120,7 @@ export abstract class TbMapLayer { let referenceLayerLoaded = false; baseLayer.addTo(layer); referenceLayer.addTo(layer); - baseLayer.once('gl-error', (e: any) => { + baseLayer.once('gl-error', (e: LeafletEvent) => { layer.fire('gl-error', e); }); baseLayer.once('load', () => { 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 182089b4e2..d73a422773 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -8249,9 +8249,6 @@ "default-map-zoom-level": "Default map zoom level", "entities-limit": "Limit of entities to load" }, - "gl": { - "webgl-not-available": "The browser tried to use the GPU to draw the map, but the GPU is turned off or blocked." - }, "layer": { "label": "Label", "layer": "Layer", @@ -8264,6 +8261,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",