Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Canvas type #3765

Merged
merged 24 commits into from
Jan 20, 2017
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
129 changes: 129 additions & 0 deletions js/source/canvas_source.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
'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;
this.resize = false;
}

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() {
if (this.canvas.width !== this.width) {
this.width = this.canvas.width;
this.resize = true;
}
if (this.canvas.height !== this.height) {
this.height = this.canvas.height;
this.resize = true;
}
if (this._hasInvalidDimensions()) {
return this.fire('error', new Error('Canvas dimensions cannot be less than or equal to zero.'));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe make this a no-op instead of a failure


if (!this.tile) return; // not enough data for current position
this._prepareImage(this.map.painter.gl, this.canvas, this.resize);
this.resize = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe convert this to a local var?

}

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
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();
});
5 changes: 4 additions & 1 deletion test/suite_implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const sinon = require('sinon');
const request = require('request');
const PNG = require('pngjs').PNG;
const Map = require('../js/ui/map');
const Style = require('../js/style/style');
const window = require('../js/util/window');
const browser = require('../js/util/browser');
const rtlTextPlugin = require('../js/source/rtl_text_plugin');
Expand All @@ -30,13 +31,15 @@ module.exports = function(style, options, _callback) {

const map = new Map({
container: container,
style: style,
classes: options.classes,
interactive: false,
attributionControl: false,
preserveDrawingBuffer: true
});

const _style = new Style(style, map, { validate: !options.ignored });
map.setStyle(_style);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use skipped flags to get around validating these tests without adding new code

// Configure the map to never stop the render loop
map.repaint = true;

Expand Down