// @flow import ImageSource from './image_source'; import window from '../util/window'; import rasterBoundsAttributes from '../data/raster_bounds_attributes'; import SegmentVector from '../data/segment'; import Texture from '../render/texture'; import {ErrorEvent} from '../util/evented'; import ValidationError from '../style-spec/error/validation_error'; import type Map from '../ui/map'; import type Dispatcher from '../util/dispatcher'; import type {Evented} from '../util/evented'; export type CanvasSourceSpecification = {| "type": "canvas", "coordinates": [[number, number], [number, number], [number, number], [number, number]], "animate"?: boolean, "canvas": string | HTMLCanvasElement |}; /** * Options to add a canvas source type to the map. * * @typedef {Object} CanvasSourceOptions * @property {string} type Source type. Must be `"canvas"`. * @property {string|HTMLCanvasElement} canvas Canvas source from which to read pixels. Can be a string representing the ID of the canvas element, or the `HTMLCanvasElement` itself. * @property {Array>} coordinates Four geographical coordinates denoting where to place the corners of the canvas, specified in `[longitude, latitude]` pairs. * @property {boolean} [animate=true] 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. */ /** * A data source containing the contents of an HTML canvas. See {@link CanvasSourceOptions} for detailed documentation of options. * * @example * // 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 * var 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 */ class CanvasSource extends ImageSource { options: CanvasSourceSpecification; animate: boolean; canvas: HTMLCanvasElement; width: number; height: number; play: () => void; pause: () => void; _playing: boolean; /** * @private */ 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 window.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; } /** * Enables animation. The image will be copied from the canvas to the map on each frame. * @method play * @instance * @memberof CanvasSource */ /** * Disables animation. The map will display a static copy of the canvas image. * @method pause * @instance * @memberof CanvasSource */ load() { this._loaded = true; if (!this.canvas) { this.canvas = (this.options.canvas instanceof window.HTMLCanvasElement) ? this.options.canvas : window.document.getElementById(this.options.canvas); } 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 {HTMLCanvasElement} The HTML `canvas` element. */ getCanvas() { return this.canvas; } onAdd(map: Map) { this.map = map; this.load(); if (this.canvas) { if (this.animate) this.play(); } } onRemove() { this.pause(); } /** * Sets the canvas's coordinates and re-renders the map. * * @method setCoordinates * @instance * @memberof CanvasSource * @param {Array>} coordinates Four geographical coordinates, * represented as arrays of longitude and latitude numbers, which define the corners of the canvas. * The coordinates start at the top left corner of the canvas and proceed in clockwise order. * They do not have to represent a rectangle. * @returns {CanvasSource} this */ // setCoordinates inherited from ImageSource 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.boundsBuffer) { this.boundsBuffer = context.createVertexBuffer(this._boundsArray, rasterBoundsAttributes.members); } if (!this.boundsSegments) { this.boundsSegments = SegmentVector.simpleSegment(0, 0, 4, 2); } 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}); } for (const w in this.tiles) { const tile = this.tiles[w]; if (tile.state !== 'loaded') { tile.state = 'loaded'; tile.texture = this.texture; } } } serialize(): Object { 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; } } export default CanvasSource;