Browse Source

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
pull/15813/head
Maksym Tsymbarov 3 months ago
committed by GitHub
parent
commit
156ddd113e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 20
      ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts
  2. 31
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts
  3. 11
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts
  4. 2
      ui-ngx/src/app/shared/models/widget/maps/map.models.ts
  5. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json

20
ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts

@ -18,13 +18,13 @@ import {
DEFAULT_ZOOM_LEVEL, DEFAULT_ZOOM_LEVEL,
defaultGeoMapSettings, defaultGeoMapSettings,
GeoMapSettings, GeoMapSettings,
latLngPointToBounds, latLngPointToBounds, MapControlsPosition,
MapZoomAction, MapZoomAction,
TbCircleData, TbCircleData,
TbPolygonCoordinate, TbPolygonCoordinate,
TbPolygonCoordinates, TbPolygonCoordinates,
TbPolygonRawCoordinate, TbPolygonRawCoordinate,
TbPolygonRawCoordinates TbPolygonRawCoordinates, WEBGL_ERROR_EVENT
} from '@shared/models/widget/maps/map.models'; } from '@shared/models/widget/maps/map.models';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { DeepPartial } from '@shared/models/common'; import { DeepPartial } from '@shared/models/common';
@ -92,9 +92,19 @@ export class TbGeoMap extends TbMap<GeoMapSettings> {
return this.loadLayers().pipe( return this.loadLayers().pipe(
tap((layers: L.TB.LayerData[]) => { tap((layers: L.TB.LayerData[]) => {
if (layers.length) { if (layers.length) {
const layer = layers[0]; const defaultLayer = layers[0];
layer.layer.addTo(this.map); layers.forEach(layer => {
this.map.attributionControl.setPrefix(layer.attributionPrefix); 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) { if (layers.length > 1) {
const sidebar = this.getSidebar(); const sidebar = this.getSidebar();
L.TB.layers({ L.TB.layers({

31
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 { isSvgIcon, splitIconName } from '@shared/models/icon.models';
import { catchError, take } from 'rxjs/operators'; import { catchError, take } from 'rxjs/operators';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { WEBGL_ERROR_EVENT } from '@shared/models/widget/maps/map.models';
L.MarkerCluster = L.MarkerCluster.mergeOptions({ pmIgnore: true }); L.MarkerCluster = L.MarkerCluster.mergeOptions({ pmIgnore: true });
@ -1093,6 +1094,7 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer {
private _actualCanvas: HTMLCanvasElement; private _actualCanvas: HTMLCanvasElement;
private _offset: L.Point; private _offset: L.Point;
private _zooming: boolean; private _zooming: boolean;
private _glError = false;
constructor(options: TB.MapLibreGL.LeafletMapLibreGLMapOptions) { constructor(options: TB.MapLibreGL.LeafletMapLibreGLMapOptions) {
super(); super();
@ -1136,7 +1138,9 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer {
const paneName = this.getPaneName(); const paneName = this.getPaneName();
map.getPane(paneName).removeChild(this._container); map.getPane(paneName).removeChild(this._container);
this._glMap.remove(); if (this._glMap) {
this._glMap.remove();
}
this._glMap = null; this._glMap = null;
return this; return this;
@ -1204,7 +1208,18 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer {
zoom: this._map.getZoom() - 1, zoom: this._map.getZoom() - 1,
attributionControl: false 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._glMap.once('load', () => {
this.fire('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() { private _update() {
if (!this._map) { if (!this._map || !this._glReady()) {
return; return;
} }
this._offset = this._map.containerPointToLayerPoint([0, 0]); this._offset = this._map.containerPointToLayerPoint([0, 0]);
@ -1253,6 +1272,7 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer {
private _transformGL(gl: MapLibreGLMap) { private _transformGL(gl: MapLibreGLMap) {
const center = this._map.getCenter(); const center = this._map.getCenter();
const tr = gl._getTransformForUpdate(); const tr = gl._getTransformForUpdate();
if (!tr) { return; }
tr.setCenter(MapLibreGLLngLat.convert([center.lng, center.lat])); tr.setCenter(MapLibreGLLngLat.convert([center.lng, center.lat]));
tr.setZoom(this._map.getZoom() - 1); tr.setZoom(this._map.getZoom() - 1);
gl.transform.apply(tr); gl.transform.apply(tr);
@ -1260,6 +1280,7 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer {
} }
private _pinchZoom() { private _pinchZoom() {
if (!this._glReady()) { return; }
this._glMap.jumpTo({ this._glMap.jumpTo({
zoom: this._map.getZoom() - 1, zoom: this._map.getZoom() - 1,
center: this._map.getCenter() center: this._map.getCenter()
@ -1267,6 +1288,7 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer {
} }
private _animateZoom(e: L.ZoomAnimEvent) { private _animateZoom(e: L.ZoomAnimEvent) {
if (!this._glReady() || !this._actualCanvas) { return; }
const scale = this._map.getZoomScale(e.zoom); const scale = this._map.getZoomScale(e.zoom);
const padding = this._map.getSize().multiplyBy(this.options.padding * scale); const padding = this._map.getSize().multiplyBy(this.options.padding * scale);
const viewHalf = this.getSize().divideBy(2); const viewHalf = this.getSize().divideBy(2);
@ -1291,6 +1313,7 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer {
} }
private _zoomEnd() { private _zoomEnd() {
if (!this._glReady() || !this._actualCanvas) { return; }
const scale = this._map.getZoomScale(this._map.getZoom()); const scale = this._map.getZoomScale(this._map.getZoom());
L.DomUtil.setTransform( L.DomUtil.setTransform(
this._actualCanvas, this._actualCanvas,
@ -1302,7 +1325,9 @@ class MapLibreGLLayer extends L.Layer implements TB.MapLibreGL.MapLibreGLLayer {
} }
private _transitionEnd() { private _transitionEnd() {
if (!this._glReady()) { return; }
L.Util.requestAnimFrame(() => { L.Util.requestAnimFrame(() => {
if (!this._glReady()) { return; }
const zoom = this._map.getZoom(); const zoom = this._map.getZoom();
const center = this._map.getCenter(); const center = this._map.getCenter();
const offset = this._map.latLngToContainerPoint( const offset = this._map.latLngToContainerPoint(

11
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts

@ -28,14 +28,14 @@ import {
MapProvider, MapProvider,
OpenStreetMapLayerSettings, OpenStreetMapLayerSettings,
ReferenceLayerType, ReferenceLayerType,
TencentMapLayerSettings TencentMapLayerSettings, WEBGL_ERROR_EVENT
} from '@shared/models/widget/maps/map.models'; } from '@shared/models/widget/maps/map.models';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { DeepPartial } from '@shared/models/common'; import { DeepPartial } from '@shared/models/common';
import { mergeDeep } from '@core/utils'; import { mergeDeep } from '@core/utils';
import { Observable, of, shareReplay, switchMap } from 'rxjs'; import { Observable, of, shareReplay, switchMap } from 'rxjs';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import L from 'leaflet'; import L, { LeafletEvent } from 'leaflet';
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from 'rxjs/operators';
import { ResourcesService } from '@core/services/resources.service'; import { ResourcesService } from '@core/services/resources.service';
import { StyleSpecification } from '@maplibre/maplibre-gl-style-spec'; import { StyleSpecification } from '@maplibre/maplibre-gl-style-spec';
@ -120,6 +120,13 @@ export abstract class TbMapLayer<S extends MapLayerSettings> {
let referenceLayerLoaded = false; let referenceLayerLoaded = false;
baseLayer.addTo(layer); baseLayer.addTo(layer);
referenceLayer.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', () => { baseLayer.once('load', () => {
baseLayerLoaded = true; baseLayerLoaded = true;
if (referenceLayerLoaded) { if (referenceLayerLoaded) {

2
ui-ngx/src/app/shared/models/widget/maps/map.models.ts

@ -1127,6 +1127,8 @@ export interface ImageMapSourceSettings {
entityKey?: DataKey; entityKey?: DataKey;
} }
export const WEBGL_ERROR_EVENT = 'gl-error';
export const imageMapSourceSettingsValid = (imageSource: ImageMapSourceSettings): boolean => { export const imageMapSourceSettingsValid = (imageSource: ImageMapSourceSettings): boolean => {
if (!imageSource?.sourceType) { if (!imageSource?.sourceType) {
return false; return false;

1
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -8538,6 +8538,7 @@
"roadmap": "Roadmap", "roadmap": "Roadmap",
"satellite": "Satellite", "satellite": "Satellite",
"hybrid": "Hybrid", "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": {
"reference-layer": "Reference layer", "reference-layer": "Reference layer",
"no-layer": "No layer", "no-layer": "No layer",

Loading…
Cancel
Save