diff --git a/src/map.js b/src/map.js
index 8889d3c15e..678277bc18 100644
--- a/src/map.js
+++ b/src/map.js
@@ -899,7 +899,11 @@ var map = function (arg) {
throw new Error('Map require DIV node');
}
+ if (m_node.data('data-geojs-map') && $.isFunction(m_node.data('data-geojs-map').exit)) {
+ m_node.data('data-geojs-map').exit();
+ }
m_node.addClass('geojs-map');
+ m_node.data('data-geojs-map', m_this);
return m_this;
};
@@ -923,13 +927,15 @@ var map = function (arg) {
////////////////////////////////////////////////////////////////////////////
this.exit = function () {
var i, layers = m_this.children();
- for (i = 0; i < layers.length; i += 1) {
+ for (i = layers.length - 1; i >= 0; i -= 1) {
layers[i]._exit();
+ m_this.removeChild(layers[i]);
}
if (m_this.interactor()) {
m_this.interactor().destroy();
m_this.interactor(null);
}
+ m_this.node().data('data-geojs-map', null);
m_this.node().off('.geo');
/* make sure the map node has nothing left in it */
m_this.node().empty();
@@ -1545,6 +1551,82 @@ var map = function (arg) {
return m_this;
};
+ /**
+ * Get a screen-shot of all or some of the canvas layers of map. Note that
+ * webGL layers are rerendered, even if
+ * window.contextPreserveDrawingBuffer = true;
+ * is set before creating the map object. Chrome, at least, may not keep the
+ * drawing buffers if the tab loses focus (and returning focus won't
+ * necessarily rerender).
+ *
+ * @param {object|array|undefined} layers: either a layer, a list of
+ * layers, or falsy to get all layers.
+ * @param {string} type: see canvas.toDataURL. Defaults to 'image/png'.
+ * Alternately, 'canvas' to return the canvas element (this can be used
+ * to get the results as a blob, which can be faster for some operations
+ * but is not supported as widely).
+ * @param {Number} encoderOptions: see canvas.toDataURL.
+ * @param {object} opts: additional screenshot options:
+ * background: if false or null, don't prefill the background. If
+ * undefined, use the default (white). Otherwise, a css color or
+ * CanvasRenderingContext2D.fillStyle to fill the initial canvas.
+ * This could match the background of the browser page, for instance.
+ * @returns {string|HTMLCanvasElement}: data URL with the result or the
+ * HTMLCanvasElement with the result.
+ */
+ this.screenshot = function (layers, type, encoderOptions, opts) {
+ opts = opts || {};
+ // ensure layers is a list of all the layres we want to include
+ if (!layers) {
+ layers = m_this.layers();
+ } else if (!Array.isArray(layers)) {
+ layers = [layers];
+ }
+ // filter to only the included layers
+ layers = layers.filter(function (l) { return m_this.layers().indexOf(l) >= 0; });
+ // sort layers by z-index
+ layers = layers.sort(
+ function (a, b) { return (a.zIndex() - b.zIndex()); }
+ );
+ // create a new canvas element
+ var result = document.createElement('canvas');
+ result.width = m_width;
+ result.height = m_height;
+ var context = result.getContext('2d');
+ // optionally start with a white or custom background
+ if (opts.background !== false && opts.background !== null) {
+ context.fillStyle = opts.background !== undefined ? opts.background : 'white';
+ context.fillRect(0, 0, result.width, result.height);
+ }
+ // for each layer, copy all canvases to our new canvas. If we ever support
+ // non-canvases, add them here. It looks like some support could be added
+ // with a library such as rasterizehtml (avialable on npm).
+ layers.forEach(function (layer) {
+ $('canvas', layer.node()).each(function () {
+ var opacity = layer.opacity();
+ if (opacity <= 0) {
+ return;
+ }
+ context.globalAlpha = opacity;
+ if (layer.renderer().api() === 'vgl') {
+ layer.renderer()._renderFrame();
+ }
+ var transform = $(this).css('transform');
+ // if the canvas is being transformed, apply the same transformation
+ if (transform && transform.substr(0, 7) === 'matrix(') {
+ context.setTransform.apply(context, transform.substr(7, transform.length - 8).split(',').map(parseFloat));
+ } else {
+ context.setTransform(1, 0, 0, 1, 0, 0);
+ }
+ context.drawImage($(this)[0], 0, 0);
+ });
+ });
+ if (type !== 'canvas') {
+ result = result.toDataURL(type, encoderOptions);
+ }
+ return result;
+ };
+
/**
* Instead of each function using window.requestAnimationFrame, schedule all
* such frames here. This allows the callbacks to be reordered or removed as
diff --git a/tests/cases/map.js b/tests/cases/map.js
index 906dfa74b3..a8dc491f20 100644
--- a/tests/cases/map.js
+++ b/tests/cases/map.js
@@ -566,6 +566,78 @@ describe('geo.core.map', function () {
expect(wasCalled).toBe(true);
unmockAnimationFrame();
});
+ it('node class and data attribute', function () {
+ var selector = '#map-create-map';
+ var m = create_map();
+ expect($(selector).hasClass('geojs-map')).toBe(true);
+ expect($(selector).data('data-geojs-map')).toBe(m);
+ m.createLayer('feature');
+ expect(m.layers().length).toBe(1);
+ var m2 = geo.map({node: selector});
+ expect($(selector).data('data-geojs-map')).toBe(m2);
+ m2.createLayer('feature');
+ expect(m.layers().length).toBe(0);
+ expect(m2.layers().length).toBe(1);
+ });
+ it('screenshot', function () {
+ var mockAnimationFrame = require('../test-utils').mockAnimationFrame;
+ var stepAnimationFrame = require('../test-utils').stepAnimationFrame;
+ var unmockAnimationFrame = require('../test-utils').unmockAnimationFrame;
+
+ mockAnimationFrame();
+
+ var m = create_map({
+ width: 64, height: 48, zoom: 2, center: {x: 7.5, y: 7.5}});
+ var layer1 = m.createLayer('feature', {renderer: 'canvas'});
+ var l1 = layer1.createFeature('line', {
+ style: {strokeWidth: 5, strokeColor: 'blue'}});
+ l1.data([[{x: 0, y: 0}, {x: 5, y: 0}],
+ [{x: 0, y: 10}, {x: 5, y: 12}, {x: 2, y: 15}],
+ [{x: 10, y: 0}, {x: 15, y: 2}, {x: 12, y: 5}]]);
+ var layer2 = m.createLayer('feature', {renderer: 'canvas'});
+ var l2 = layer2.createFeature('line', {
+ style: {strokeWidth: 5, strokeColor: 'black'}});
+ l2.data([[{x: 10, y: 10}, {x: 15, y: 10}],
+ [{x: 0, y: 10}, {x: 5, y: 12}, {x: 2, y: 15}]]);
+
+ m.draw();
+ stepAnimationFrame(new Date().getTime());
+ var dataUrl, dataUrl2, dataUrl3;
+ dataUrl = m.screenshot();
+ expect(dataUrl.substr(0, 22)).toBe('data:image/png;base64,');
+ dataUrl2 = m.screenshot(null, 'image/jpeg');
+ expect(dataUrl2.substr(0, 23)).toBe('data:image/jpeg;base64,');
+ expect(dataUrl2.length).toBeLessThan(dataUrl.length);
+ dataUrl2 = m.screenshot(layer1);
+ expect(dataUrl2.substr(0, 22)).toBe('data:image/png;base64,');
+ expect(dataUrl2).not.toEqual(dataUrl);
+ dataUrl3 = m.screenshot([layer1]);
+ expect(dataUrl3).toEqual(dataUrl2);
+ // making a layer transparent is as good as not asking for it
+ layer2.opacity(0);
+ dataUrl3 = m.screenshot();
+ expect(dataUrl3).toEqual(dataUrl2);
+ // a partial opacity should get different results than full
+ layer2.opacity(0.5);
+ dataUrl3 = m.screenshot();
+ expect(dataUrl3).not.toEqual(dataUrl);
+ expect(dataUrl3).not.toEqual(dataUrl2);
+ layer2.opacity(1);
+ // we can ask for no or different backgrounds
+ dataUrl2 = m.screenshot(null, undefined, undefined, {background: false});
+ expect(dataUrl2).not.toEqual(dataUrl);
+ dataUrl3 = m.screenshot(null, undefined, undefined, {background: 'red'});
+ expect(dataUrl3).not.toEqual(dataUrl);
+ expect(dataUrl3).not.toEqual(dataUrl2);
+ // asking for layers out of order shouldn't matter
+ dataUrl3 = m.screenshot([layer2, layer1]);
+ expect(dataUrl3).toEqual(dataUrl);
+ layer2.canvas().css('transform', 'translate(10px, 20px) scale(1.2) rotate(5deg)');
+ stepAnimationFrame(new Date().getTime());
+ dataUrl2 = m.screenshot();
+ expect(dataUrl2).not.toEqual(dataUrl);
+ unmockAnimationFrame();
+ });
});
describe('Public non-class methods', function () {
diff --git a/tests/example-cases/blog-lines.js b/tests/example-cases/blog-lines.js
index bf6189c520..b6dcc3f8e2 100644
--- a/tests/example-cases/blog-lines.js
+++ b/tests/example-cases/blog-lines.js
@@ -8,15 +8,37 @@ describe('blog-lines example', function () {
imageTest.prepareIframeTest();
});
+ /* Check if all of the visible mpas in the test window have some content.
+ * This relies on the structure and internal functions of the example.
+ *
+ * @param {function} callback: function to call when the page appears ready.
+ */
+ function ready(callback) {
+ var missing;
+ var cw = $('iframe#map')[0].contentWindow;
+ var base$ = cw.jQuery;
+ if (base$) {
+ var entries = base$('#main_list>span>.entry>a');
+ missing = $.makeArray(entries).some(function (entry) {
+ return cw.elementInViewport(entry) && !base$(entry).children('div').children().length;
+ }) || !entries.length;
+ }
+ if (!base$ || missing) {
+ window.setTimeout(function () { ready(callback); }, 100);
+ } else {
+ callback();
+ }
+ }
+
it('basic', function (done) {
$('#map').attr('src', '/examples/blog-lines/index.html?mode=select');
- imageTest.imageTest('exampleBlogLines', '#map', 0.0015, done, null, 1000, 2);
+ imageTest.imageTest('exampleBlogLines', '#map', 0.0015, done, ready, 500, 2);
}, 10000);
it('round line cap', function (done) {
$('#map')[0].contentWindow.scrollTo(0, 130);
base$ = $('iframe#map')[0].contentWindow.jQuery;
base$('#feature').val('linecap-round').trigger('change');
- imageTest.imageTest('exampleBlogLinesRoundCap', '#map', 0.0015, done, null, 1000, 2, '.mapboxgl-canvas');
+ imageTest.imageTest('exampleBlogLinesRoundCap', '#map', 0.0015, done, ready, 500, 2, '.mapboxgl-canvas');
}, 20000);
it('10,000 lines in geojs', function (done) {
$('#map').attr('src', '/examples/blog-lines/index.html?renderer=vgl&data=roads&lines=10000&x=-73.7593015&y=42.8496799&zoom=13&strokeOpacity=1&strokeWidth=2&antialiasing=2&referenceLines=false');
diff --git a/tests/image-test.js b/tests/image-test.js
index 64905870c5..9b88d5b112 100644
--- a/tests/image-test.js
+++ b/tests/image-test.js
@@ -54,7 +54,6 @@ module.exports.prepareImageTest = function () {
};
module.exports.prepareIframeTest = function () {
- window.contextPreserveDrawingBuffer = true;
$('#map').remove();
var map = $('').css({
width: '800px', height: '600px', border: 0});
@@ -88,15 +87,7 @@ module.exports.imageTest = function (name, elemSelector, threshold, doneFunc, id
var readyFunc = function () {
var result;
if (!elemSelector) {
- result = $('