Skip to content

Commit

Permalink
Merge pull request #665 from OpenGeoscience/canvas-screenshot
Browse files Browse the repository at this point in the history
Add a map.screenshot() function.
  • Loading branch information
manthey authored Feb 9, 2017
2 parents edebe10 + 8f82e75 commit 6686104
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 13 deletions.
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

0 comments on commit 6686104

Please sign in to comment.