diff --git a/@here/harp-mapview/lib/image/Image.ts b/@here/harp-mapview/lib/image/Image.ts index ee9c2a9405..b03922cb36 100644 --- a/@here/harp-mapview/lib/image/Image.ts +++ b/@here/harp-mapview/lib/image/Image.ts @@ -4,6 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { LoggerManager } from "@here/harp-utils"; +import * as THREE from "three"; + +import { MipMapGenerator } from "./MipMapGenerator"; + /** Any type supported by WebGLRenderingContext.texImage2D() for texture creation */ export type TexturizableImage = | HTMLImageElement @@ -12,28 +17,100 @@ export type TexturizableImage = | ImageData | ImageBitmap; +const logger = LoggerManager.instance.create("loadImage"); +const mipMapGenerator = new MipMapGenerator(); + /** * `ImageItem` is used to identify an image in the {@link ImageCache}. */ -export interface ImageItem { - /** URL of the image, or unique identifier. */ - url: string; - image?: TexturizableImage; +export class ImageItem { /** Mip maps for image data */ mipMaps?: ImageData[]; - /** Turns to `true` when the data has finished loading. */ - loaded: boolean; /** Turns to `true` if the loading has been cancelled. */ cancelled?: boolean; /** `loadingPromise` is only used during loading/generating the image. */ - loadingPromise?: Promise; -} + private loadingPromise?: Promise; -export namespace ImageItem { /** - * Missing Typedoc + * Create the `ImageItem`. + * + * @param url - URL of the image, or unique identifier. + * @param image - Optional image if already loaded. */ - export function isLoading(imageItem: ImageItem): boolean { - return imageItem.loadingPromise !== undefined; + constructor(readonly url: string, public image?: TexturizableImage) {} + + get loaded(): boolean { + return this.image !== undefined && this.mipMaps !== undefined; + } + + get loading(): boolean { + return this.loadingPromise !== undefined; + } + + /** + * Load an {@link ImageItem}. + * + * @remarks + * If the loading process is already running, it returns the current promise. + * + * @param imageItem - `ImageItem` containing the URL to load image from. + * @returns An {@link ImageItem} if the image has already been loaded, a promise otherwise. + */ + loadImage(): Promise { + if (this.loaded) { + return Promise.resolve(this); + } + + if (this.loading) { + return this.loadingPromise!; + } + + this.loadingPromise = new Promise((resolve, reject) => { + if (this.image) { + const image = this.image; + if (image instanceof HTMLImageElement && !image.complete) { + image.addEventListener("load", this.finalizeImage.bind(this, image, resolve)); + image.addEventListener("error", reject); + } else { + this.finalizeImage(this.image, resolve); + } + return; + } + + logger.debug(`Loading image: ${this.url}`); + if (this.cancelled === true) { + logger.debug(`Cancelled loading image: ${this.url}`); + resolve(undefined); + } else { + new THREE.ImageLoader().load( + this.url, + (image: HTMLImageElement) => { + if (this.cancelled === true) { + logger.debug(`Cancelled loading image: ${this.url}`); + resolve(undefined); + return; + } + + this.finalizeImage(image, resolve); + }, + undefined, + errorEvent => { + logger.error(`... loading image failed: ${this.url} : ${errorEvent}`); + + this.loadingPromise = undefined; + reject(`... loading image failed: ${this.url} : ${errorEvent}`); + } + ); + } + }); + + return this.loadingPromise; + } + + private finalizeImage(image: TexturizableImage, resolve: (item: ImageItem) => void) { + this.image = image; + this.mipMaps = mipMapGenerator.generateTextureAtlasMipMap(this); + this.loadingPromise = undefined; + resolve(this); } } diff --git a/@here/harp-mapview/lib/image/ImageCache.ts b/@here/harp-mapview/lib/image/ImageCache.ts index 70c8bb700e..82775c2c40 100644 --- a/@here/harp-mapview/lib/image/ImageCache.ts +++ b/@here/harp-mapview/lib/image/ImageCache.ts @@ -3,14 +3,7 @@ * Licensed under Apache 2.0, see full license in LICENSE * SPDX-License-Identifier: Apache-2.0 */ -import { LoggerManager } from "@here/harp-utils"; -import * as THREE from "three"; - import { ImageItem, TexturizableImage } from "./Image"; -import { MipMapGenerator } from "./MipMapGenerator"; - -const logger = LoggerManager.instance.create("ImageCache"); -const mipMapGenerator = new MipMapGenerator(); /** * Combines an {@link ImageItem} with a list of owners (which can be any object) that reference it. @@ -90,11 +83,7 @@ export class ImageCache { } imageCacheItem = { - imageItem: { - url, - image, - loaded: false - }, + imageItem: new ImageItem(url, image), owners: [owner] }; @@ -155,74 +144,6 @@ export class ImageCache { return this.m_images.size; } - /** - * Load an {@link ImageItem}. - * - * @remarks - * If the loading process is already running, it returns the current promise. - * - * @param imageItem - `ImageItem` containing the URL to load image from. - * @returns An {@link ImageItem} if the image has already been loaded, a promise otherwise. - */ - loadImage(imageItem: ImageItem): ImageItem | Promise { - const finalizeImage = (image: TexturizableImage, resolve: (item: ImageItem) => void) => { - imageItem.image = image; - imageItem.mipMaps = mipMapGenerator.generateTextureAtlasMipMap(imageItem); - imageItem.loadingPromise = undefined; - imageItem.loaded = true; - resolve(imageItem); - }; - - if (imageItem.loaded) { - return imageItem; - } - - if (imageItem.loadingPromise !== undefined) { - return imageItem.loadingPromise; - } - - imageItem.loadingPromise = new Promise((resolve, reject) => { - if (imageItem.image) { - const image = imageItem.image; - if (image instanceof HTMLImageElement && !image.complete) { - image.addEventListener("load", finalizeImage.bind(this, image, resolve)); - image.addEventListener("error", reject); - } else { - finalizeImage(imageItem.image, resolve); - } - return; - } - - logger.debug(`Loading image: ${imageItem.url}`); - if (imageItem.cancelled === true) { - logger.debug(`Cancelled loading image: ${imageItem.url}`); - resolve(undefined); - } else { - new THREE.ImageLoader().load( - imageItem.url, - (image: HTMLImageElement) => { - if (imageItem.cancelled === true) { - logger.debug(`Cancelled loading image: ${imageItem.url}`); - resolve(undefined); - return; - } - - finalizeImage(image, resolve); - }, - undefined, - errorEvent => { - logger.error(`... loading image failed: ${imageItem.url} : ${errorEvent}`); - - imageItem.loadingPromise = undefined; - reject(`... loading image failed: ${imageItem.url} : ${errorEvent}`); - } - ); - } - }); - - return imageItem.loadingPromise; - } - /** * Find the cached {@link ImageItem} by URL. * @@ -238,7 +159,7 @@ export class ImageCache { * @param imageItem - Item to cancel loading. */ private cancelLoading(imageItem: ImageItem) { - if (imageItem.loadingPromise !== undefined) { + if (imageItem.loading) { // Notify that we are cancelling. imageItem.cancelled = true; } diff --git a/@here/harp-mapview/lib/image/MapViewImageCache.ts b/@here/harp-mapview/lib/image/MapViewImageCache.ts index 54728c1287..752ef70c18 100644 --- a/@here/harp-mapview/lib/image/MapViewImageCache.ts +++ b/@here/harp-mapview/lib/image/MapViewImageCache.ts @@ -67,7 +67,7 @@ export class MapViewImageCache { const url = urlOrImage; const imageItem = this.registerImage(name, url); - return startLoading ? ImageCache.instance.loadImage(imageItem) : imageItem; + return startLoading ? imageItem.loadImage() : imageItem; } const image = urlOrImage; @@ -119,15 +119,6 @@ export class MapViewImageCache { return ImageCache.instance.findImage(url); } - /** - * Load an {@link ImageItem}. Returns a promise or a loaded {@link ImageItem}. - * - * @param imageItem - ImageItem to load. - */ - loadImage(imageItem: ImageItem): ImageItem | Promise { - return ImageCache.instance.loadImage(imageItem); - } - /** * Remove all {@link ImageItem}s from the cache. * diff --git a/@here/harp-mapview/lib/poi/PoiRenderer.ts b/@here/harp-mapview/lib/poi/PoiRenderer.ts index 0797631c76..2971e8bcab 100644 --- a/@here/harp-mapview/lib/poi/PoiRenderer.ts +++ b/@here/harp-mapview/lib/poi/PoiRenderer.ts @@ -10,7 +10,6 @@ import { assert, LoggerManager, Math2D } from "@here/harp-utils"; import * as THREE from "three"; import { ImageItem } from "../image/Image"; -import { MapViewImageCache } from "../image/MapViewImageCache"; import { MipMapGenerator } from "../image/MipMapGenerator"; import { MapView } from "../MapView"; import { ScreenCollisions } from "../ScreenCollisions"; @@ -372,6 +371,41 @@ export class PoiBatchRegistry { } } +// keep track of the missing textures, but only warn once +const missingTextureName: Map = new Map(); + +function findImageItem( + poiInfo: PoiInfo, + mapView: MapView, + imageTexture?: ImageTexture +): ImageItem | undefined { + const imageTextureName = poiInfo.imageTextureName; + + if (imageTexture) { + const imageDefinition = imageTexture.image; + const imageItem = mapView.imageCache.findImageByName(imageDefinition); + + if (!imageItem) { + logger.error(`init: No imageItem found with name '${imageDefinition}'`); + poiInfo.isValid = false; + } + return imageItem; + } + + // No image texture found. Either this is a user image or it's missing. + const imageItem = mapView.userImageCache.findImageByName(imageTextureName); + + if (!imageItem) { + // Warn about a missing texture, but only once. + if (missingTextureName.get(imageTextureName) === undefined) { + missingTextureName.set(imageTextureName, true); + logger.error(`preparePoi: No imageTexture with name '${imageTextureName}' found`); + } + poiInfo.isValid = false; + } + return imageItem; +} + /** * @internal * Manage POI rendering. Uses a [[PoiBatchRegistry]] to actually create the geometry that is being @@ -417,9 +451,6 @@ export class PoiRenderer { return screenBox; } - // keep track of the missing textures, but only warn once - private static readonly m_missingTextureName: Map = new Map(); - // the render buffer containing all batches, one batch per texture/material. private readonly m_poiBatchRegistry: PoiBatchRegistry; @@ -537,7 +568,7 @@ export class PoiRenderer { */ private preparePoi(pointLabel: TextElement, env: Env): void { const poiInfo = pointLabel.poiInfo; - if (poiInfo === undefined || !pointLabel.visible) { + if (!poiInfo || !pointLabel.visible) { return; } @@ -559,39 +590,10 @@ export class PoiRenderer { } const imageTextureName = poiInfo.imageTextureName; - const imageTexture = this.mapView.poiManager.getImageTexture(imageTextureName); - let imageItem: ImageItem; - let imageCache: MapViewImageCache; - if (imageTexture) { - const imageDefinition = imageTexture.image; - - const image = this.mapView.imageCache.findImageByName(imageDefinition); - - if (!image) { - logger.error(`init: No imageItem found with name '${imageDefinition}'`); - poiInfo.isValid = false; - return; - } - imageItem = image; - imageCache = this.mapView.imageCache; - } else { - // No image texture found. Either this is a user image or it's missing. - const image = this.mapView.userImageCache.findImageByName(imageTextureName); - - if (!image) { - // Warn about a missing texture, but only once. - if (PoiRenderer.m_missingTextureName.get(imageTextureName) === undefined) { - PoiRenderer.m_missingTextureName.set(imageTextureName, true); - logger.error( - `preparePoi: No imageTexture with name '${imageTextureName}' found` - ); - } - poiInfo.isValid = false; - return; - } - imageItem = image; - imageCache = this.mapView.userImageCache; + const imageItem = findImageItem(poiInfo, this.mapView, imageTexture); + if (!imageItem) { + return; } if (imageItem.loaded) { @@ -599,21 +601,18 @@ export class PoiRenderer { return; } - if (imageItem.loadingPromise) { + if (imageItem.loading) { // already being loaded, will be rendered once available return; } - const result = imageCache.loadImage(imageItem); - assert(result instanceof Promise); - const loadPromise = result as Promise; - loadPromise + imageItem + .loadImage() .then(loadedImageItem => { - if (loadedImageItem === undefined) { - logger.error(`preparePoi: Failed to load imageItem: '${imageItem.url}`); - return; + // Skip setup if image was not loaded (cancelled). + if (loadedImageItem.image) { + this.setupPoiInfo(poiInfo, loadedImageItem, env, imageTexture); } - this.setupPoiInfo(poiInfo, loadedImageItem, env, imageTexture); }) .catch(error => { logger.error(`preparePoi: Failed to load imageItem: '${imageItem.url}`, error); @@ -638,7 +637,7 @@ export class PoiRenderer { ) { assert(poiInfo.uvBox === undefined); - if (imageItem === undefined || imageItem.image === undefined) { + if (!imageItem.image) { logger.error("setupPoiInfo: No imageItem/imageData found"); poiInfo.isValid = false; return; diff --git a/@here/harp-mapview/test/ImageCacheTest.ts b/@here/harp-mapview/test/ImageCacheTest.ts index 16e83682ec..ceac94474b 100644 --- a/@here/harp-mapview/test/ImageCacheTest.ts +++ b/@here/harp-mapview/test/ImageCacheTest.ts @@ -153,7 +153,7 @@ describe("MapViewImageCache", function() { assert.isUndefined(imageItem.image); assert.isFalse(imageItem!.loaded); - const promise = cache.loadImage(imageItem); + const promise = imageItem.loadImage(); assert.isTrue(promise instanceof Promise); if (promise instanceof Promise) { @@ -341,7 +341,7 @@ describe("ImageCache", function() { "test/resources/headshot.png" ); - const promise = cache.loadImage(cache.registerImage(owner, imageUrl)); + const promise = cache.registerImage(owner, imageUrl).loadImage(); const testImage = cache.findImage(imageUrl); assert.exists(testImage); @@ -378,7 +378,7 @@ describe("ImageCache", function() { assert.isUndefined(testImage!.image); assert.isFalse(testImage!.loaded); - const promise = cache.loadImage(cacheItem); + const promise = cacheItem.loadImage(); assert.isTrue(promise instanceof Promise); @@ -411,7 +411,7 @@ describe("ImageCache", function() { assert.equal(testImage!.image!.width, 37); assert.equal(testImage!.image!.height, 32); - const promise = cache.loadImage(testImage); + const promise = testImage.loadImage(); assert.isTrue(promise instanceof Promise); @@ -443,7 +443,7 @@ describe("ImageCache", function() { assert.isUndefined(imageItem.image); assert.isFalse(imageItem!.loaded); - const promise = cache.loadImage(imageItem); + const promise = imageItem.loadImage(); assert.isTrue(promise instanceof Promise); @@ -470,7 +470,7 @@ describe("ImageCache", function() { assert.isDefined(testImage!.image); assert.isFalse(testImage!.loaded); - const result = cache.loadImage(testImage); + const result = testImage.loadImage(); assert.isTrue(result instanceof Promise); const promise = result as Promise; diff --git a/@here/harp-mapview/test/MipMapGeneratorTest.ts b/@here/harp-mapview/test/MipMapGeneratorTest.ts index 2607d5acf6..41271585dc 100644 --- a/@here/harp-mapview/test/MipMapGeneratorTest.ts +++ b/@here/harp-mapview/test/MipMapGeneratorTest.ts @@ -13,6 +13,7 @@ import { getTestResourceUrl } from "@here/harp-test-utils"; import { expect } from "chai"; +import { ImageItem } from "../lib/image/Image"; import { MipMapGenerator } from "../lib/image/MipMapGenerator"; const isNode = typeof window === "undefined"; @@ -59,11 +60,9 @@ describe("MipMapGenerator", function() { } else { it("creates mipmaps from ImageData", function() { const mipMapGenerator = new MipMapGenerator(); - const mipMaps = mipMapGenerator.generateTextureAtlasMipMap({ - url: "/test.png", - image: imageData, - loaded: true - }); + const mipMaps = mipMapGenerator.generateTextureAtlasMipMap( + new ImageItem("/test.png", imageData) + ); expect(mipMaps).to.have.length(7); for (let level = 0; level < mipMaps.length; ++level) { @@ -79,11 +78,9 @@ describe("MipMapGenerator", function() { it("creates mipmaps from ImageBitmap", function() { const mipMapGenerator = new MipMapGenerator(); - const mipMaps = mipMapGenerator.generateTextureAtlasMipMap({ - url: "/test.png", - image: imageBitmap, - loaded: true - }); + const mipMaps = mipMapGenerator.generateTextureAtlasMipMap( + new ImageItem("/test.png", imageBitmap) + ); expect(mipMaps).to.have.length(7); for (let level = 0; level < mipMaps.length; ++level) { const size = Math.pow(2, 6 - level); diff --git a/@here/harp-mapview/test/PoiInfoBuilder.ts b/@here/harp-mapview/test/PoiInfoBuilder.ts index 66d79f8f63..b7c59d88d6 100644 --- a/@here/harp-mapview/test/PoiInfoBuilder.ts +++ b/@here/harp-mapview/test/PoiInfoBuilder.ts @@ -106,15 +106,12 @@ export class PoiInfoBuilder { } withImageItem(): PoiInfoBuilder { - this.m_imageItem = { - url: "dummy", - image: { - height: this.m_height, - width: this.m_width, - data: new Uint8ClampedArray(this.m_height * this.m_width * 4) - }, - loaded: true - }; + this.m_imageItem = new ImageItem("dummy", { + height: this.m_height, + width: this.m_width, + data: new Uint8ClampedArray(this.m_height * this.m_width * 4) + }); + return this; }