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

Add a map.screenshot() function. #665

Merged
merged 4 commits into from
Feb 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
84 changes: 83 additions & 1 deletion src/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand All @@ -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();
Expand Down Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions tests/cases/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
26 changes: 24 additions & 2 deletions tests/example-cases/blog-lines.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
11 changes: 1 addition & 10 deletions tests/image-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ module.exports.prepareImageTest = function () {
};

module.exports.prepareIframeTest = function () {
window.contextPreserveDrawingBuffer = true;
$('#map').remove();
var map = $('<iframe id="map"/>').css({
width: '800px', height: '600px', border: 0});
Expand Down Expand Up @@ -88,15 +87,7 @@ module.exports.imageTest = function (name, elemSelector, threshold, doneFunc, id
var readyFunc = function () {
var result;
if (!elemSelector) {
result = $('<canvas>')[0];
result.width = $('canvas')[0].width;
result.height = $('canvas')[0].height;
var context = result.getContext('2d');
context.fillStyle = 'white';
context.fillRect(0, 0, result.width, result.height);
$('canvas').each(function () {
context.drawImage($(this)[0], 0, 0);
});
result = $('#map').data('data-geojs-map').screenshot(null, 'canvas');
} else {
var innerScreenX = window.mozInnerScreenX !== undefined ?
window.mozInnerScreenX :
Expand Down