/** * @module ol/source/Image */ import ImageWrapper from '../Image.js'; import ImageState from '../ImageState.js'; import {linearFindNearest} from '../array.js'; import Event from '../events/Event.js'; import EventType from '../events/EventType.js'; import { containsExtent, equals, getCenter, getForViewAndSize, getHeight, getWidth, } from '../extent.js'; import {ceil} from '../math.js'; import {equivalent} from '../proj.js'; import ReprojImage from '../reproj/Image.js'; import {fromResolutionLike} from '../resolution.js'; import Source from './Source.js'; import {DECIMALS} from './common.js'; /** * @enum {string} */ export const ImageSourceEventType = { /** * Triggered when an image starts loading. * @event module:ol/source/Image.ImageSourceEvent#imageloadstart * @api */ IMAGELOADSTART: 'imageloadstart', /** * Triggered when an image finishes loading. * @event module:ol/source/Image.ImageSourceEvent#imageloadend * @api */ IMAGELOADEND: 'imageloadend', /** * Triggered if image loading results in an error. * @event module:ol/source/Image.ImageSourceEvent#imageloaderror * @api */ IMAGELOADERROR: 'imageloaderror', }; /** * @typedef {'imageloadend'|'imageloaderror'|'imageloadstart'} ImageSourceEventTypes */ /** * @classdesc * Events emitted by {@link module:ol/source/Image~ImageSource} instances are instances of this * type. */ export class ImageSourceEvent extends Event { /** * @param {string} type Type. * @param {import("../Image.js").default} image The image. */ constructor(type, image) { super(type); /** * The image related to the event. * @type {import("../Image.js").default} * @api */ this.image = image; } } /*** * @template Return * @typedef {import("../Observable").OnSignature & * import("../Observable").OnSignature & * import("../Observable").OnSignature & * import("../Observable").CombinedOnSignature} ImageSourceOnSignature */ /** * @typedef {Object} Options * @property {import("./Source.js").AttributionLike} [attributions] Attributions. * @property {boolean} [interpolate=true] Use interpolated values when resampling. By default, * linear interpolation is used when resampling. Set to false to use the nearest neighbor instead. * @property {import("../Image.js").Loader} [loader] Loader. Can either be a custom loader, or one of the * loaders created with a `createLoader()` function ({@link module:ol/source/wms.createLoader wms}, * {@link module:ol/source/arcgisRest.createLoader arcgisRest}, {@link module:ol/source/mapguide.createLoader mapguide}, * {@link module:ol/source/static.createLoader static}). * @property {import("../proj.js").ProjectionLike} [projection] Projection. * @property {Array} [resolutions] Resolutions. * @property {import("./Source.js").State} [state] State. */ /** * @classdesc * Base class for sources providing a single image. * @fires module:ol/source/Image.ImageSourceEvent * @api */ class ImageSource extends Source { /** * @param {Options} options Single image source options. */ constructor(options) { super({ attributions: options.attributions, projection: options.projection, state: options.state, interpolate: options.interpolate !== undefined ? options.interpolate : true, }); /*** * @type {ImageSourceOnSignature} */ this.on; /*** * @type {ImageSourceOnSignature} */ this.once; /*** * @type {ImageSourceOnSignature} */ this.un; /** * @protected * @type {import("../Image.js").Loader} */ this.loader = options.loader || null; /** * @private * @type {Array|null} */ this.resolutions_ = options.resolutions !== undefined ? options.resolutions : null; /** * @private * @type {import("../reproj/Image.js").default} */ this.reprojectedImage_ = null; /** * @private * @type {number} */ this.reprojectedRevision_ = 0; /** * @protected * @type {import("../Image.js").default} */ this.image = null; /** * @private * @type {import("../extent.js").Extent} */ this.wantedExtent_; /** * @private * @type {number} */ this.wantedResolution_; /** * @private * @type {boolean} */ this.static_ = options.loader ? options.loader.length === 0 : false; /** * @private * @type {import("../proj/Projection.js").default} */ this.wantedProjection_ = null; } /** * @return {Array|null} Resolutions. * @override */ getResolutions() { return this.resolutions_; } /** * @param {Array|null} resolutions Resolutions. */ setResolutions(resolutions) { this.resolutions_ = resolutions; } /** * @protected * @param {number} resolution Resolution. * @return {number} Resolution. */ findNearestResolution(resolution) { const resolutions = this.getResolutions(); if (resolutions) { const idx = linearFindNearest(resolutions, resolution, 0); resolution = resolutions[idx]; } return resolution; } /** * @param {import("../extent.js").Extent} extent Extent. * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @param {import("../proj/Projection.js").default} projection Projection. * @return {import("../Image.js").default} Single image. */ getImage(extent, resolution, pixelRatio, projection) { const sourceProjection = this.getProjection(); if ( !sourceProjection || !projection || equivalent(sourceProjection, projection) ) { if (sourceProjection) { projection = sourceProjection; } return this.getImageInternal(extent, resolution, pixelRatio, projection); } if (this.reprojectedImage_) { if ( this.reprojectedRevision_ == this.getRevision() && equivalent(this.reprojectedImage_.getProjection(), projection) && this.reprojectedImage_.getResolution() == resolution && equals(this.reprojectedImage_.getExtent(), extent) ) { return this.reprojectedImage_; } this.reprojectedImage_.dispose(); this.reprojectedImage_ = null; } this.reprojectedImage_ = new ReprojImage( sourceProjection, projection, extent, resolution, pixelRatio, (extent, resolution, pixelRatio) => this.getImageInternal(extent, resolution, pixelRatio, sourceProjection), this.getInterpolate(), ); this.reprojectedRevision_ = this.getRevision(); return this.reprojectedImage_; } /** * @abstract * @param {import("../extent.js").Extent} extent Extent. * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @param {import("../proj/Projection.js").default} projection Projection. * @return {import("../Image.js").default} Single image. * @protected */ getImageInternal(extent, resolution, pixelRatio, projection) { if (this.loader) { const requestExtent = getRequestExtent(extent, resolution, pixelRatio, 1); const requestResolution = this.findNearestResolution(resolution); if ( this.image && (this.static_ || (this.wantedProjection_ === projection && ((this.wantedExtent_ && containsExtent(this.wantedExtent_, requestExtent)) || containsExtent(this.image.getExtent(), requestExtent)) && ((this.wantedResolution_ && fromResolutionLike(this.wantedResolution_) === requestResolution) || fromResolutionLike(this.image.getResolution()) === requestResolution))) ) { return this.image; } this.wantedProjection_ = projection; this.wantedExtent_ = requestExtent; this.wantedResolution_ = requestResolution; this.image = new ImageWrapper( requestExtent, requestResolution, pixelRatio, this.loader, ); this.image.addEventListener( EventType.CHANGE, this.handleImageChange.bind(this), ); } return this.image; } /** * Handle image change events. * @param {import("../events/Event.js").default} event Event. * @protected */ handleImageChange(event) { const image = /** @type {import("../Image.js").default} */ (event.target); let type; switch (image.getState()) { case ImageState.LOADING: this.loading = true; type = ImageSourceEventType.IMAGELOADSTART; break; case ImageState.LOADED: this.loading = false; type = ImageSourceEventType.IMAGELOADEND; break; case ImageState.ERROR: this.loading = false; type = ImageSourceEventType.IMAGELOADERROR; break; default: return; } if (this.hasListener(type)) { this.dispatchEvent(new ImageSourceEvent(type, image)); } } } /** * Default image load function for image sources that use import("../Image.js").Image image * instances. * @param {import("../Image.js").default} image Image. * @param {string} src Source. */ export function defaultImageLoadFunction(image, src) { /** @type {HTMLImageElement|HTMLVideoElement} */ (image.getImage()).src = src; } /** * Adjusts the extent so it aligns with pixel boundaries. * @param {import("../extent.js").Extent} extent Extent. * @param {number} resolution Reolution. * @param {number} pixelRatio Pixel ratio. * @param {number} ratio Ratio between request size and view size. * @return {import("../extent.js").Extent} Request extent. */ export function getRequestExtent(extent, resolution, pixelRatio, ratio) { const imageResolution = resolution / pixelRatio; const center = getCenter(extent); const viewWidth = ceil(getWidth(extent) / imageResolution, DECIMALS); const viewHeight = ceil(getHeight(extent) / imageResolution, DECIMALS); const marginWidth = ceil(((ratio - 1) * viewWidth) / 2, DECIMALS); const requestWidth = viewWidth + 2 * marginWidth; const marginHeight = ceil(((ratio - 1) * viewHeight) / 2, DECIMALS); const requestHeight = viewHeight + 2 * marginHeight; return getForViewAndSize(center, imageResolution, 0, [ requestWidth, requestHeight, ]); } export default ImageSource;