maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
218 lines (190 loc) • 7.17 kB
text/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});
} 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',
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;
}
}