diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e9cbdba..8ab80bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ # MapTiler SDK Changelog -## NEXT +## LATEST + +### ✨ Features and improvements +None + +### 🐛 Bug Fixes +- Fixes a bug where Webgl would throw a texture error when two maps are rendered on the page due to a race condition loading images. + +### ⚙️ Others +None + + + +## NEXT (3.9.0) ### ✨ Features and improvements - Additions and improvements to ImageViewer diff --git a/src/Map.ts b/src/Map.ts index bf84b52e..730318ca 100644 --- a/src/Map.ts +++ b/src/Map.ts @@ -568,7 +568,7 @@ export class Map extends maplibregl.Map { this.setStyle(MapStyle.STREETS); warning += `Loading default MapTiler Cloud style "${MapStyle.STREETS.getDefaultVariant().getId()}" as a fallback.`; } else { - warning += "Leaving the style as is."; + warning += " Leaving the style as is."; } console.warn(warning); }; @@ -1207,17 +1207,21 @@ export class Map extends maplibregl.Map { return; } - const targetBeforeLayer = this.getLayersOrder()[0]; - if (this.space) { - this.setSpaceFromStyle({ style: styleSpec }); - } else { - this.initSpace({ before: targetBeforeLayer, spec: styleSpec.metadata?.maptiler?.space }); - } + try { + const targetBeforeLayer = this.getLayersOrder()[0]; + if (this.space) { + this.setSpaceFromStyle({ style: styleSpec }); + } else { + this.initSpace({ before: targetBeforeLayer, spec: styleSpec.metadata?.maptiler?.space }); + } - if (this.halo) { - this.setHaloFromStyle({ style: styleSpec }); - } else { - this.initHalo({ before: targetBeforeLayer, spec: styleSpec.metadata?.maptiler?.halo }); + if (this.halo) { + this.setHaloFromStyle({ style: styleSpec }); + } else { + this.initHalo({ before: targetBeforeLayer, spec: styleSpec.metadata?.maptiler?.halo }); + } + } catch (e) { + console.error(e); } }; @@ -1255,7 +1259,9 @@ export class Map extends maplibregl.Map { // we have no way of knowing if the style is loaded or not // which will fail internally if the style is not loaded correctly handleStyleLoad(); - } catch {} + } catch (e) { + console.error(e); + } return this; } diff --git a/src/custom-layers/CubemapLayer/CubemapLayer.ts b/src/custom-layers/CubemapLayer/CubemapLayer.ts index 826c783f..a2daca74 100644 --- a/src/custom-layers/CubemapLayer/CubemapLayer.ts +++ b/src/custom-layers/CubemapLayer/CubemapLayer.ts @@ -276,8 +276,12 @@ class CubemapLayer implements CustomLayerInterface { */ public onRemove(_map: MapSDK, gl: WebGLRenderingContext | WebGL2RenderingContext) { if (this.cubemap) { + if (this.texture) { + gl.deleteTexture(this.texture); + } gl.deleteProgram(this.cubemap.shaderProgram); gl.deleteBuffer(this.cubemap.positionBuffer); + this.texture = undefined; } } diff --git a/src/custom-layers/CubemapLayer/loadCubemapTexture.ts b/src/custom-layers/CubemapLayer/loadCubemapTexture.ts index ae823a3e..eeaaf369 100644 --- a/src/custom-layers/CubemapLayer/loadCubemapTexture.ts +++ b/src/custom-layers/CubemapLayer/loadCubemapTexture.ts @@ -1,7 +1,9 @@ import { CubemapFaceNames, CubemapFaces } from "./types"; +type WebGLCtx = WebGLRenderingContext | WebGL2RenderingContext; + interface LoadCubemapTextureOptions { - gl: WebGLRenderingContext | WebGL2RenderingContext; + gl: WebGLCtx; faces?: CubemapFaces; onReady: (texture: WebGLTexture, images?: HTMLImageElement[]) => void; forceRefresh?: boolean; @@ -9,12 +11,12 @@ interface LoadCubemapTextureOptions { /** * Stores the result of the last successful execution of {@link loadCubemapTexture}. - * @type {WebGLTexture | undefined} + * @type {Map} * @private */ -let memoizedTexture: WebGLTexture | undefined = undefined; +const memoizedTextures = new Map(); -let memoizedImages: HTMLImageElement[] | undefined = undefined; +const memoizedImages = new Map(); /** * Stores the stringified content of the 'faces' object from the last successful execution. * Used for memoization by {@link loadCubemapTexture}. @@ -61,14 +63,15 @@ interface ImageLoadingPromiseReturnValue { * }); */ export function loadCubemapTexture({ gl, faces, onReady, forceRefresh }: LoadCubemapTextureOptions) { - if (memoizedTexture && !forceRefresh && facesKey === JSON.stringify(faces)) { - onReady(memoizedTexture, memoizedImages); + if (memoizedTextures.get(gl) && !forceRefresh && facesKey === JSON.stringify(faces)) { + onReady(memoizedTextures.get(gl)!, memoizedImages.get(gl)!); + return; } facesKey = JSON.stringify(faces); - const texture = memoizedTexture ?? gl.createTexture(); - gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture); + const texture = memoizedTextures.get(gl) ?? gl.createTexture(); + // gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture); if (!faces) { console.warn("[CubemapLayer][loadCubemapTexture]: Faces are null"); @@ -150,8 +153,8 @@ export function loadCubemapTexture({ gl, faces, onReady, forceRefresh }: LoadCub onReady(texture, imageElements); - memoizedImages = imageElements; - memoizedTexture = texture; + memoizedImages.set(gl, imageElements); + memoizedTextures.set(gl, texture); }) .catch((error) => { console.error(`[CubemapLayer][loadCubemapTexture]: Error loading cubemap texture`, error); diff --git a/src/custom-layers/RadialGradientLayer/RadialGradientLayer.ts b/src/custom-layers/RadialGradientLayer/RadialGradientLayer.ts index 68ea854d..e8218cde 100644 --- a/src/custom-layers/RadialGradientLayer/RadialGradientLayer.ts +++ b/src/custom-layers/RadialGradientLayer/RadialGradientLayer.ts @@ -130,6 +130,12 @@ export class RadialGradientLayer implements CustomLayerInterface { this.gradient = defaultConstructorOptions; return; } + const errors = validateHaloSpecification(gradient); + if (errors.length > 0) { + throw new Error(`[RadialGradientLayer]: Invalid Halo specification: + - ${errors.join("\n - ")} + `); + } this.gradient = { ...defaultConstructorOptions, @@ -351,13 +357,11 @@ export class RadialGradientLayer implements CustomLayerInterface { await this.animateOut(); - if (!validateHaloSpecification(gradient)) { - this.gradient.scale = defaultConstructorOptions.scale; - this.gradient.stops = [ - [0, "transparent"], - [1, "transparent"], - ]; - return; + const errors = validateHaloSpecification(gradient); + if (errors.length > 0) { + throw new Error(`[RadialGradientLayer]: Invalid Halo specification: + - ${errors.join("\n - ")} + `); } if (gradient === true) { @@ -386,24 +390,39 @@ export class RadialGradientLayer implements CustomLayerInterface { } } -export function validateHaloSpecification(halo: RadialGradientLayerConstructorOptions | boolean): boolean { +const validKeys = ["scale", "stops"]; + +export function validateHaloSpecification(halo: RadialGradientLayerConstructorOptions | boolean): Array { + const errors: string[] = []; + if (typeof halo === "boolean") { - return true; + return []; + } + + try { + const additionalKeys = Object.keys(halo).filter((key) => !validKeys.includes(key)); + if (additionalKeys.length > 0) { + errors.push(`Properties ${additionalKeys.map((key) => `\`${key}\``).join(", ")} are not supported.`); + } + } catch { + errors.push("Halo specification is not an object."); } if (typeof halo.scale !== "number") { - return false; + errors.push("Halo `scale` property is not a number."); } // this is testing external data so we need to check // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!halo.stops || halo.stops.length === 0) { - return false; + errors.push("Halo `stops` property is not an array."); } - if (halo.stops.some((stop) => typeof stop[0] !== "number" || typeof stop[1] !== "string")) { - return false; + // this is testing external data so we need to check + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (halo.stops?.some((stop) => typeof stop[0] !== "number" || typeof stop[1] !== "string")) { + errors.push("Halo `stops` property is not an array of [number, string]"); } - return true; + return errors; }