From e244ca09f8a15cc17852c6be876fdb948086935d Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 6 Feb 2017 16:22:59 -0500 Subject: [PATCH 1/4] Add a map.screenshot() function. This returns either a data url or an HTMLCanvasElement. You do not need to preserve drawing buffers for this to work -- that doesn't alway work in Chrome in any case, so WebGL layers are rerendered as part of the screenshot process. This needs testing. --- src/map.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/map.js b/src/map.js index 8889d3c15e..00805dcece 100644 --- a/src/map.js +++ b/src/map.js @@ -1545,6 +1545,68 @@ 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}: data URL with the result. + */ + this.screenshot = function (layers, type, encoderOptions) { + // 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'; + 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') { + 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 From 3860594ec4dcf2eb2807df8d17714dd6c0a38e96 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 6 Feb 2017 16:51:52 -0500 Subject: [PATCH 2/4] Use map.screenshot instead of special testing code. Attach a data element to the map's node so that the map is reachable. If a second map is attached to the same node, the first map is destroyed via map.exit(). Improve robustness of testing the blog-lines example. Make sure foreign libraries have a chance to load. --- src/map.js | 11 ++++-- tests/cases/map.js | 56 +++++++++++++++++++++++++++++++ tests/example-cases/blog-lines.js | 26 ++++++++++++-- tests/image-test.js | 11 +----- 4 files changed, 90 insertions(+), 14 deletions(-) diff --git a/src/map.js b/src/map.js index 00805dcece..fe78a64b3c 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(); @@ -1560,7 +1566,8 @@ var map = function (arg) { * 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}: data URL with the result. + * @returns {string|HTMLCanvasElement}: data URL with the result or the + * HTMLCanvasElement with the result. */ this.screenshot = function (layers, type, encoderOptions) { // ensure layers is a list of all the layres we want to include diff --git a/tests/cases/map.js b/tests/cases/map.js index 906dfa74b3..91b556ebce 100644 --- a/tests/cases/map.js +++ b/tests/cases/map.js @@ -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 () { 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 = $('