Skip to content

Commit

Permalink
Canvas source type (#3765)
Browse files Browse the repository at this point in the history
Adds a new source type, `canvas`, which reads the contents of an HTML canvas and applies them to the map given a set of coordinates (similar to the behavior of image and video sources).
  • Loading branch information
Lauren Budorick authored Jan 20, 2017
1 parent c31cf05 commit 5082fec
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 7 deletions.
1 change: 1 addition & 0 deletions documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ toc:
- GeoJSONSource
- VideoSource
- ImageSource
- CanvasSource
- name: Options
description: |
These shared option can be supplied as arguments to various methods
Expand Down
126 changes: 126 additions & 0 deletions js/source/canvas_source.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
'use strict';

const ImageSource = require('./image_source');
const window = require('../util/window');

/**
* A data source containing the contents of an HTML canvas.
* (See the [Style Specification](https://www.mapbox.com/mapbox-gl-style-spec/#sources-canvas) for detailed documentation of options.)
* @interface CanvasSource
* @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 {

constructor(id, options, dispatcher, eventedParent) {
super(id, options, dispatcher, eventedParent);
this.options = options;
this.animate = options.hasOwnProperty('animate') ? options.animate : true;
}

load() {
this.canvas = this.canvas || window.document.getElementById(this.options.canvas);
this.width = this.canvas.width;
this.height = this.canvas.height;
if (this._hasInvalidDimensions(this.canvas)) return this.fire('error', new Error('Canvas dimensions cannot be less than or equal to zero.'));

let loopID;

this.play = function() {
loopID = this.map.style.animationLoop.set(Infinity);
this.map._rerender();
};

this.pause = function() {
this.map.style.animationLoop.cancel(loopID);
};

this._finishLoading();
}

/**
* Returns the HTML `canvas` element.
*
* @returns {HTMLCanvasElement} The HTML `canvas` element.
*/
getCanvas() {
return this.canvas;
}

onAdd(map) {
if (this.map) return;
this.map = map;
this.load();
if (this.canvas) {
if (this.animate) this.play();
this.setCoordinates(this.coordinates);
}
}

/**
* Sets the canvas's coordinates and re-renders the map.
*
* @method setCoordinates
* @param {Array<Array<number>>} 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 (!this.tile) return; // not enough data for current position
this._prepareImage(this.map.painter.gl, this.canvas, resize);
}

serialize() {
return {
type: 'canvas',
canvas: this.canvas,
coordinates: this.coordinates
};
}

_hasInvalidDimensions() {
for (const x of [this.canvas.width, this.canvas.height]) {
if (isNaN(x) || x <= 0) return true;
}
return false;
}
}

module.exports = CanvasSource;
6 changes: 4 additions & 2 deletions js/source/image_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class ImageSource extends Evented {
this._prepareImage(this.map.painter.gl, this.image);
}

_prepareImage(gl, image) {
_prepareImage(gl, image, resize) {
if (this.tile.state !== 'loaded') {
this.tile.state = 'loaded';
this.tile.texture = gl.createTexture();
Expand All @@ -157,7 +157,9 @@ class ImageSource extends Evented {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
} else if (image instanceof window.HTMLVideoElement) {
} else if (resize) {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
} else if (image instanceof window.HTMLVideoElement || image instanceof window.ImageData || image instanceof window.HTMLCanvasElement) {
gl.bindTexture(gl.TEXTURE_2D, this.tile.texture);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, image);
}
Expand Down
3 changes: 2 additions & 1 deletion js/source/source.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const sourceTypes = {
'raster': require('../source/raster_tile_source'),
'geojson': require('../source/geojson_source'),
'video': require('../source/video_source'),
'image': require('../source/image_source')
'image': require('../source/image_source'),
'canvas': require('../source/canvas_source')
};

/*
Expand Down
3 changes: 2 additions & 1 deletion js/source/video_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const ImageSource = require('./image_source');
* @see [Add a video](https://www.mapbox.com/mapbox-gl-js/example/video-on-a-map/)
*/
class VideoSource extends ImageSource {

constructor(id, options, dispatcher, eventedParent) {
super(id, options, dispatcher, eventedParent);
this.roundZoom = true;
Expand Down Expand Up @@ -102,7 +103,7 @@ class VideoSource extends ImageSource {
* They do not have to represent a rectangle.
* @returns {VideoSource} this
*/
// setCoordiates inherited from ImageSource
// setCoordinates inherited from ImageSource

prepare() {
if (!this.tile || this.video.readyState < 2) return; // not enough data for current position
Expand Down
2 changes: 1 addition & 1 deletion js/style/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ class Style extends Evented {
throw new Error(`The type property must be defined, but the only the following properties were given: ${Object.keys(source)}.`);
}

const builtIns = ['vector', 'raster', 'geojson', 'video', 'image'];
const builtIns = ['vector', 'raster', 'geojson', 'video', 'image', 'canvas'];
const shouldValidate = builtIns.indexOf(source.type) >= 0;
if (shouldValidate && this._validate(validateStyle.source, `sources.${id}`, source, null, options)) return;

Expand Down
6 changes: 6 additions & 0 deletions js/util/window.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ function restore() {
return originalGetContext.call(this, type, attributes);
};

window.useFakeHTMLCanvasGetContext = function() {
window.HTMLCanvasElement.prototype.getContext = sinon.stub().returns('2d');
};

window.useFakeXMLHttpRequest = function() {
sinon.xhr.supportsCORS = true;
window.server = sinon.fakeServer.create();
Expand All @@ -53,6 +57,8 @@ function restore() {

window.restore = restore;

window.ImageData = window.ImageData || sinon.stub().returns(false);

return window;
}

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
"geojson-rewind": "^0.1.0",
"geojson-vt": "^2.4.0",
"grid-index": "^1.0.0",
"mapbox-gl-function": "mapbox/mapbox-gl-function#4f829622413f336080d3c710244c251421c0ec30",
"mapbox-gl-style-spec": "mapbox/mapbox-gl-style-spec#e85407a377510acb647161de6be6357ab4f606dd",
"mapbox-gl-function": "mapbox/mapbox-gl-function#41c6724e2bbd7bd1eb5991451bbf118b7d02b525",
"mapbox-gl-style-spec": "mapbox/mapbox-gl-style-spec#d11f6d2775bf5b22534b3b2fb3410755b2473cdf",
"mapbox-gl-supported": "^1.2.0",
"package-json-versionify": "^1.0.2",
"pbf": "^1.3.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"ignored":{
"native": "https://github.com/mapbox/mapbox-gl-native/issues/4860",
"js":"https://github.com/mapbox/mapbox-gl-js/issues/3682#issuecomment-264348200"
},
"skipped": {
"js": true
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
"ignored": {
"js": "https://github.com/mapbox/mapbox-gl-js/issues/3682",
"native": "https://github.com/mapbox/mapbox-gl-native/issues/4860"
},
"skipped": {
"js": true
}
}
},
Expand Down
107 changes: 107 additions & 0 deletions test/js/source/canvas_source.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
'use strict';

const test = require('mapbox-gl-js-test').test;
const CanvasSource = require('../../../js/source/canvas_source');
const Transform = require('../../../js/geo/transform');
const Evented = require('../../../js/util/evented');
const util = require('../../../js/util/util');
const window = require('../../../js/util/window');

function createSource(options) {
window.useFakeHTMLCanvasGetContext();

const c = window.document.createElement('canvas');
c.width = 20;
c.height = 20;

options = util.extend({
canvas: 'id',
coordinates: [[0, 0], [1, 0], [1, 1], [0, 1]]
}, options);

const source = new CanvasSource('id', options, { send: function() {} }, options.eventedParent);

source.canvas = c;

return source;
}

class StubMap extends Evented {
constructor() {
super();
this.transform = new Transform();
this.style = { animationLoop: { set: function() {} } };
}

_rerender() {
this.fire('rerender');
}
}

test('CanvasSource', (t) => {
t.afterEach((callback) => {
window.restore();
callback();
});

t.test('constructor', (t) => {
const source = createSource();

source.on('source.load', () => {
t.equal(source.minzoom, 0);
t.equal(source.maxzoom, 22);
t.equal(source.tileSize, 512);
t.equal(source.animate, true);
t.equal(typeof source.play, 'function');
t.end();
});

source.onAdd(new StubMap());
});

t.test('rerenders if animated', (t) => {
const source = createSource();
const map = new StubMap();

map.on('rerender', () => {
t.ok(true, 'fires rerender event');
t.end();
});

source.onAdd(map);
});

t.test('can be static', (t) => {
const source = createSource({
animate: false
});
const map = new StubMap();

map.on('rerender', () => {
t.notOk(true, 'shouldn\'t rerender here');
t.end();
});

source.on('source.load', () => {
t.ok(true, 'fires load event without rerendering');
t.end();
});

source.onAdd(map);
});

t.end();
});

test('CanvasSource#serialize', (t) => {
const source = createSource();

const serialized = source.serialize();
t.equal(serialized.type, 'canvas');
t.ok(serialized.canvas);
t.deepEqual(serialized.coordinates, [[0, 0], [1, 0], [1, 1], [0, 1]]);

window.restore();

t.end();
});

0 comments on commit 5082fec

Please sign in to comment.