221 lines
7.1 KiB
TypeScript
221 lines
7.1 KiB
TypeScript
import {ImageSource} from './image_source';
|
|
|
|
import {Texture} from '../render/texture';
|
|
import {Event, ErrorEvent} from '../util/evented';
|
|
import {ValidationError} from '@maplibre/maplibre-gl-style-spec';
|
|
|
|
import type {Map} from '../ui/map';
|
|
import type {Dispatcher} from '../util/dispatcher';
|
|
import type {Evented} from '../util/evented';
|
|
|
|
/**
|
|
* Options to add a canvas source type to the map.
|
|
*/
|
|
export type CanvasSourceSpecification = {
|
|
/**
|
|
* Source type. Must be `"canvas"`.
|
|
*/
|
|
type: 'canvas';
|
|
/**
|
|
* Four geographical coordinates denoting where to place the corners of the canvas, specified in `[longitude, latitude]` pairs.
|
|
*/
|
|
coordinates: [[number, number], [number, number], [number, number], [number, number]];
|
|
/**
|
|
* Whether the canvas source is animated. If the canvas is static (i.e. pixels do not need to be re-read on every frame), `animate` should be set to `false` to improve performance.
|
|
* @defaultValue true
|
|
*/
|
|
animate?: boolean;
|
|
/**
|
|
* Canvas source from which to read pixels. Can be a string representing the ID of the canvas element, or the `HTMLCanvasElement` itself.
|
|
*/
|
|
canvas?: string | HTMLCanvasElement;
|
|
};
|
|
|
|
/**
|
|
* A data source containing the contents of an HTML canvas. See {@link CanvasSourceSpecification} for detailed documentation of options.
|
|
*
|
|
* @group Sources
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* // add to map
|
|
* map.addSource('some id', {
|
|
* type: 'canvas',
|
|
* canvas: 'idOfMyHTMLCanvas',
|
|
* animate: true,
|
|
* coordinates: [
|
|
* [-76.54, 39.18],
|
|
* [-76.52, 39.18],
|
|
* [-76.52, 39.17],
|
|
* [-76.54, 39.17]
|
|
* ]
|
|
* });
|
|
*
|
|
* // update
|
|
* let mySource = map.getSource('some id');
|
|
* mySource.setCoordinates([
|
|
* [-76.54335737228394, 39.18579907229748],
|
|
* [-76.52803659439087, 39.1838364847587],
|
|
* [-76.5295386314392, 39.17683392507606],
|
|
* [-76.54520273208618, 39.17876344106642]
|
|
* ]);
|
|
*
|
|
* map.removeSource('some id'); // remove
|
|
* ```
|
|
*/
|
|
export class CanvasSource extends ImageSource {
|
|
options: CanvasSourceSpecification;
|
|
animate: boolean;
|
|
canvas: HTMLCanvasElement;
|
|
width: number;
|
|
height: number;
|
|
/**
|
|
* Enables animation. The image will be copied from the canvas to the map on each frame.
|
|
*/
|
|
play: () => void;
|
|
/**
|
|
* Disables animation. The map will display a static copy of the canvas image.
|
|
*/
|
|
pause: () => void;
|
|
_playing: boolean;
|
|
|
|
/** @internal */
|
|
constructor(id: string, options: CanvasSourceSpecification, dispatcher: Dispatcher, eventedParent: Evented) {
|
|
super(id, options, dispatcher, eventedParent);
|
|
|
|
// We build in some validation here, since canvas sources aren't included in the style spec:
|
|
if (!options.coordinates) {
|
|
this.fire(new ErrorEvent(new ValidationError(`sources.${id}`, null, 'missing required property "coordinates"')));
|
|
} else if (!Array.isArray(options.coordinates) || options.coordinates.length !== 4 ||
|
|
options.coordinates.some(c => !Array.isArray(c) || c.length !== 2 || c.some(l => typeof l !== 'number'))) {
|
|
this.fire(new ErrorEvent(new ValidationError(`sources.${id}`, null, '"coordinates" property must be an array of 4 longitude/latitude array pairs')));
|
|
}
|
|
|
|
if (options.animate && typeof options.animate !== 'boolean') {
|
|
this.fire(new ErrorEvent(new ValidationError(`sources.${id}`, null, 'optional "animate" property must be a boolean value')));
|
|
}
|
|
|
|
if (!options.canvas) {
|
|
this.fire(new ErrorEvent(new ValidationError(`sources.${id}`, null, 'missing required property "canvas"')));
|
|
} else if (typeof options.canvas !== 'string' && !(options.canvas instanceof HTMLCanvasElement)) {
|
|
this.fire(new ErrorEvent(new ValidationError(`sources.${id}`, null, '"canvas" must be either a string representing the ID of the canvas element from which to read, or an HTMLCanvasElement instance')));
|
|
}
|
|
|
|
this.options = options;
|
|
this.animate = options.animate !== undefined ? options.animate : true;
|
|
}
|
|
|
|
async load() {
|
|
this._loaded = true;
|
|
if (!this.canvas) {
|
|
this.canvas = (this.options.canvas instanceof HTMLCanvasElement) ?
|
|
this.options.canvas :
|
|
document.getElementById(this.options.canvas) as HTMLCanvasElement;
|
|
// cast to HTMLCanvasElement in else of ternary
|
|
// should we do a safety check and throw if it's not actually HTMLCanvasElement?
|
|
}
|
|
this.width = this.canvas.width;
|
|
this.height = this.canvas.height;
|
|
|
|
if (this._hasInvalidDimensions()) {
|
|
this.fire(new ErrorEvent(new Error('Canvas dimensions cannot be less than or equal to zero.')));
|
|
return;
|
|
}
|
|
|
|
this.play = function() {
|
|
this._playing = true;
|
|
this.map.triggerRepaint();
|
|
};
|
|
|
|
this.pause = function() {
|
|
if (this._playing) {
|
|
this.prepare();
|
|
this._playing = false;
|
|
}
|
|
};
|
|
|
|
this._finishLoading();
|
|
}
|
|
|
|
/**
|
|
* Returns the HTML `canvas` element.
|
|
*
|
|
* @returns The HTML `canvas` element.
|
|
*/
|
|
getCanvas(): HTMLCanvasElement {
|
|
return this.canvas;
|
|
}
|
|
|
|
onAdd(map: Map) {
|
|
this.map = map;
|
|
this.load();
|
|
if (this.canvas) {
|
|
if (this.animate) this.play();
|
|
}
|
|
}
|
|
|
|
onRemove() {
|
|
this.pause();
|
|
}
|
|
|
|
prepare() {
|
|
let resize = false;
|
|
if (this.canvas.width !== this.width) {
|
|
this.width = this.canvas.width;
|
|
resize = true;
|
|
}
|
|
if (this.canvas.height !== this.height) {
|
|
this.height = this.canvas.height;
|
|
resize = true;
|
|
}
|
|
|
|
if (this._hasInvalidDimensions()) return;
|
|
|
|
if (Object.keys(this.tiles).length === 0) return; // not enough data for current position
|
|
|
|
const context = this.map.painter.context;
|
|
const gl = context.gl;
|
|
|
|
if (!this.texture) {
|
|
this.texture = new Texture(context, this.canvas, gl.RGBA, {premultiply: true});
|
|
this.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
|
|
} else if (resize || this._playing) {
|
|
this.texture.update(this.canvas, {premultiply: true});
|
|
}
|
|
|
|
let newTilesLoaded = false;
|
|
for (const w in this.tiles) {
|
|
const tile = this.tiles[w];
|
|
if (tile.state !== 'loaded') {
|
|
tile.state = 'loaded';
|
|
tile.texture = this.texture;
|
|
newTilesLoaded = true;
|
|
}
|
|
}
|
|
|
|
if (newTilesLoaded) {
|
|
this.fire(new Event('data', {dataType: 'source', sourceDataType: 'idle', sourceId: this.id}));
|
|
}
|
|
}
|
|
|
|
serialize(): CanvasSourceSpecification {
|
|
return {
|
|
type: 'canvas',
|
|
animate: this.animate,
|
|
canvas: this.options.canvas,
|
|
coordinates: this.coordinates
|
|
};
|
|
}
|
|
|
|
hasTransition() {
|
|
return this._playing;
|
|
}
|
|
|
|
_hasInvalidDimensions() {
|
|
for (const x of [this.canvas.width, this.canvas.height]) {
|
|
if (isNaN(x) || x <= 0) return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|