/** * @module ol/Image */ import EventType from './events/EventType.js'; import ImageBase from './ImageBase.js'; import ImageState from './ImageState.js'; import {IMAGE_DECODE} from './has.js'; import {getHeight} from './extent.js'; import {listenOnce, unlistenByKey} from './events.js'; /** * A function that takes an {@link module:ol/Image~ImageWrapper} for the image and a * `{string}` for the src as arguments. It is supposed to make it so the * underlying image {@link module:ol/Image~ImageWrapper#getImage} is assigned the * content specified by the src. If not specified, the default is * * function(image, src) { * image.getImage().src = src; * } * * Providing a custom `imageLoadFunction` can be useful to load images with * post requests or - in general - through XHR requests, where the src of the * image element would be set to a data URI when the content is loaded. * * @typedef {function(ImageWrapper, string): void} LoadFunction * @api */ class ImageWrapper extends ImageBase { /** * @param {import("./extent.js").Extent} extent Extent. * @param {number|undefined} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @param {string} src Image source URI. * @param {?string} crossOrigin Cross origin. * @param {LoadFunction} imageLoadFunction Image load function. * @param {CanvasRenderingContext2D} [context] Canvas context. When provided, the image will be * drawn into the context's canvas, and `getImage()` will return the canvas once the image * has finished loading. */ constructor( extent, resolution, pixelRatio, src, crossOrigin, imageLoadFunction, context ) { super(extent, resolution, pixelRatio, ImageState.IDLE); /** * @private * @type {string} */ this.src_ = src; /** * @private * @type {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} */ this.image_ = new Image(); if (crossOrigin !== null) { this.image_.crossOrigin = crossOrigin; } /** * @private * @type {CanvasRenderingContext2D} */ this.context_ = context; /** * @private * @type {?function():void} */ this.unlisten_ = null; /** * @protected * @type {import("./ImageState.js").default} */ this.state = ImageState.IDLE; /** * @private * @type {LoadFunction} */ this.imageLoadFunction_ = imageLoadFunction; } /** * @return {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} Image. * @api */ getImage() { if ( this.state == ImageState.LOADED && this.context_ && !(this.image_ instanceof HTMLCanvasElement) ) { const canvas = this.context_.canvas; canvas.width = this.image_.width; canvas.height = this.image_.height; this.context_.drawImage(this.image_, 0, 0); this.image_ = this.context_.canvas; } return this.image_; } /** * Tracks loading or read errors. * * @private */ handleImageError_() { this.state = ImageState.ERROR; this.unlistenImage_(); this.changed(); } /** * Tracks successful image load. * * @private */ handleImageLoad_() { if (this.resolution === undefined) { this.resolution = getHeight(this.extent) / this.image_.height; } this.state = ImageState.LOADED; this.unlistenImage_(); this.changed(); } /** * Load the image or retry if loading previously failed. * Loading is taken care of by the tile queue, and calling this method is * only needed for preloading or for reloading in case of an error. * @api */ load() { if (this.state == ImageState.IDLE || this.state == ImageState.ERROR) { this.state = ImageState.LOADING; this.changed(); this.imageLoadFunction_(this, this.src_); this.unlisten_ = listenImage( this.image_, this.handleImageLoad_.bind(this), this.handleImageError_.bind(this) ); } } /** * @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} image Image. */ setImage(image) { this.image_ = image; this.resolution = getHeight(this.extent) / this.image_.height; } /** * Discards event handlers which listen for load completion or errors. * * @private */ unlistenImage_() { if (this.unlisten_) { this.unlisten_(); this.unlisten_ = null; } } } /** * @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} image Image element. * @param {function():any} loadHandler Load callback function. * @param {function():any} errorHandler Error callback function. * @return {function():void} Callback to stop listening. */ export function listenImage(image, loadHandler, errorHandler) { const img = /** @type {HTMLImageElement} */ (image); let listening = true; let decoding = false; let loaded = false; const listenerKeys = [ listenOnce(img, EventType.LOAD, function () { loaded = true; if (!decoding) { loadHandler(); } }), ]; if (img.src && IMAGE_DECODE) { decoding = true; img .decode() .then(function () { if (listening) { loadHandler(); } }) .catch(function (error) { if (listening) { if (loaded) { loadHandler(); } else { errorHandler(); } } }); } else { listenerKeys.push(listenOnce(img, EventType.ERROR, errorHandler)); } return function unlisten() { listening = false; listenerKeys.forEach(unlistenByKey); }; } export default ImageWrapper;