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 2 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
71 changes: 70 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,69 @@ 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.
* @returns {string|HTMLCanvasElement}: data URL with the result or the
* HTMLCanvasElement with the result.
*/
this.screenshot = function (layers, type, encoderOptions) {
Copy link
Member

Choose a reason for hiding this comment

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

I like the API

// 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');
// start with a white background
context.fillStyle = 'white';
Copy link
Member

Choose a reason for hiding this comment

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

@manthey do we not need to set the transparency? I believe the later drawImage call will update the final value to the combined value of all opacities.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The drawImage properly uses the opacity of individual pixels in the canvas, so layers composite properly. But... I did forget that we adjust the opacity of the containing div of the layer when you set the layer opacity, so I'll need to take that into account.

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 () {
if (layer.renderer().api() === 'vgl') {
Copy link
Member

Choose a reason for hiding this comment

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

Would be nice to have it not check for vgl but I understand why we have to do this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I original thought that if we preserved the drawing buffers, this wouldn't be necessary, but they aren't preserved when the browser tab is switched in any case, so this ends up being necessary.

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
56 changes: 56 additions & 0 deletions tests/cases/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,62 @@ 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);
// 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