@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
163 lines (139 loc) • 5 kB
text/typescript
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
import { CanvasTexture, MathUtils, Texture } from 'three';
import type CoordinateSystem from '../core/geographic/CoordinateSystem';
import type Extent from '../core/geographic/Extent';
import type { GridExtent } from '../core/geographic/Extent';
import type { GetImageOptions, ImageResponse, ImageSourceEvents } from './ImageSource';
import EmptyTexture from '../renderer/EmptyTexture';
import Fetcher from '../utils/Fetcher';
import ImageSource, { ImageResult } from './ImageSource';
/**
* Options for the {@link StaticImageSource} constructor.
*/
export interface StaticImageSourceOptions {
/**
* The source of the image. It can be:
* - a URL to a remote PNG, JPEG or WebP file,
* - an `<canvas>` or `<image>` element,
* - a THREE.js [`Texture`](https://threejs.org/docs/index.html?q=texture#api/en/textures/Texture).
*/
source: string | HTMLImageElement | HTMLCanvasElement | Texture;
/**
* The extent of the image.
*/
extent: Extent;
/**
* Should the texture be flipped vertically ? This parameter only applies if
* {@link StaticImageSourceOptions.source | source} is a texture.
*/
flipY?: boolean;
}
export interface StaticImageSourceEvents extends ImageSourceEvents {
/**
* Raised when the remote image has been loaded.
*/
loaded: unknown;
/**
* Raised when the remote image failed to load.
*/
error: {
error: Error;
};
}
/**
* An {@link ImageSource} that displays a single, static image.
*
* The image must be either a PNG, JPG or WebP file, whose dimensions are not greater
* than the maximal texture size allowed by WebGL on this browser.
*/
export default class StaticImageSource extends ImageSource<StaticImageSourceEvents> {
public readonly isStaticImageSource = true as const;
public override readonly type = 'StaticImageSource' as const;
private readonly _extent: Extent;
private readonly _source: string | HTMLImageElement | HTMLCanvasElement | Texture;
private readonly _id = MathUtils.generateUUID();
private _promise: Promise<ImageResult> | undefined;
/**
* Create a {@link StaticImageSource}.
* @param options - The options.
*/
public constructor(options: StaticImageSourceOptions) {
super({
colorSpace: 'srgb',
flipY: typeof options.source === 'string' ? false : (options.flipY ?? true),
is8bit: true,
});
if (options.source == null) {
throw new Error('invalid source');
}
if (options.extent == null) {
throw new Error('invalid extent');
}
this._extent = options.extent;
this._source = options.source;
}
public getExtent(): Extent {
return this._extent;
}
public getCrs(): CoordinateSystem {
return this._extent.crs;
}
private async fetchTexture(url: string): Promise<Texture> {
// We directly flip the texture during decoding, which is why we don't need to flip it in the layer itself.
return Fetcher.texture(url, { flipY: true, priority: this.priority })
.then(texture => {
this.dispatchEvent({ type: 'loaded' });
return texture;
})
.catch(error => {
console.error(error);
this.dispatchEvent({ type: 'error', error });
return new EmptyTexture();
});
}
private async loadImageOnce(): Promise<ImageResult> {
let texture: Texture;
if (typeof this._source === 'string') {
texture = await this.fetchTexture(this._source);
} else if (this._source instanceof HTMLCanvasElement) {
texture = new CanvasTexture(this._source);
} else if (this._source instanceof HTMLImageElement) {
texture = new Texture(this._source);
} else {
texture = this._source;
}
return new ImageResult({
id: this._id,
texture,
extent: this._extent,
});
}
public override adjustExtentAndPixelSize(
requestExtent: Extent,
requestWidth: number,
requestHeight: number,
): GridExtent | null {
return { extent: requestExtent, width: requestWidth, height: requestHeight };
}
public override update(): void {
this._promise = undefined;
super.update();
}
private async loadImage(): Promise<ImageResult> {
if (this._promise == null) {
this._promise = this.loadImageOnce();
}
return this._promise;
}
public getImages(_options: GetImageOptions): ImageResponse[] {
const response: ImageResponse = {
id: this._id,
request: this.loadImage.bind(this),
};
return [response];
}
}