diff --git a/web/client/actions/snapshot.js b/web/client/actions/snapshot.js index ab5cc9800b..eec1e098fd 100644 --- a/web/client/actions/snapshot.js +++ b/web/client/actions/snapshot.js @@ -14,10 +14,11 @@ const SNAPSHOT_ADD_QUEUE = 'SNAPSHOT_ADD_QUEUE'; const SNAPSHOT_REMOVE_QUEUE = 'SNAPSHOT_REMOVE_QUEUE'; const SAVE_IMAGE = 'SAVE_IMAGE'; -function changeSnapshotState(state) { +function changeSnapshotState(state, tainted) { return { type: CHANGE_SNAPSHOT_STATE, - state: state + state: state, + tainted }; } function onSnapshotError(error) { diff --git a/web/client/components/map/leaflet/snapshot/GrabMap.jsx b/web/client/components/map/leaflet/snapshot/GrabMap.jsx index c325e5dc14..3d4fab97a5 100644 --- a/web/client/components/map/leaflet/snapshot/GrabMap.jsx +++ b/web/client/components/map/leaflet/snapshot/GrabMap.jsx @@ -10,7 +10,7 @@ const ConfigUtils = require('../../../../utils/ConfigUtils'); const ProxyUtils = require('../../../../utils/ProxyUtils'); const {isEqual} = require('lodash'); const html2canvas = require('html2canvas'); - +require("./snapshotMapStyle.css"); /** * GrabMap for Leaflet uses HTML2CANVAS to generate the image for the existing * leaflet map. @@ -63,10 +63,7 @@ let GrabLMap = React.createClass({ let mapIsLoading = this.mapIsLoading(this.props.layers); if (!mapIsLoading && this.props.active) { this.props.onStatusChange("SHOTING"); - this.previousTimeout = setTimeout(() => { - this.doSnapshot(this.props); - }, - this.props.timeout); + this.triggerShooting(this.props.timeout); } }, componentWillReceiveProps(nextProps) { @@ -92,13 +89,7 @@ let GrabLMap = React.createClass({ componentDidUpdate(prevProps) { let mapIsLoading = this.mapIsLoading(this.props.layers); let mapChanged = this.mapChanged(prevProps); - if ( this.props.active && !mapIsLoading && mapChanged ) { - this.previousTimeout = setTimeout(() => { - this.doSnapshot(this.props); - }, - this.props.timeout); - } - if (!mapIsLoading && this.props.active && (mapChanged || this.props.snapstate.state === "SHOTING") ) { + if ( this.props.active && !mapIsLoading && (mapChanged || this.props.snapstate.state === "SHOTING") ) { this.triggerShooting(this.props.timeout); } @@ -130,30 +121,45 @@ let GrabLMap = React.createClass({ return layers.some((layer) => { return layer.visibility && layer.loading; }); }, triggerShooting(delay) { + if (this.previousTimeout) { + clearTimeout(this.previousTimeout); + } this.previousTimeout = setTimeout(() => { this.doSnapshot(this.props); }, delay); }, doSnapshot(props) { + // get map style shifted + var leftString = window.getComputedStyle(this.mapDiv).getPropertyValue("left"); + var left = 0; + if (leftString) { + left = parseInt( leftString.replace('px', ''), 10); + } + const tilePane = this.mapDiv.getElementsByClassName("leaflet-tile-pane"); if (tilePane && tilePane.length > 0) { let layers = [].slice.call(tilePane[0].getElementsByClassName("leaflet-layer"), 0); layers.sort(function compare(a, b) { return Number.parseFloat(a.style.zIndex) - Number.parseFloat(b.style.zIndex); }); - let canvas = this.refs.canvas; - let context = canvas.getContext("2d"); + let canvas = this.getCanvas(); + let context = canvas && canvas.getContext("2d"); + if (!context) { + return; + } context.clearRect(0, 0, canvas.width, canvas.height); let queue = layers.map((l) => { + let newCanvas = this.refs.canvas.cloneNode(); + newCanvas.width = newCanvas.width + left; return html2canvas(l, { // you have to provide a canvas to avoid html2canvas to crop the image - canvas: this.refs.canvas.cloneNode(), + canvas: newCanvas, logging: false, proxy: this.proxy, - allowTaint: props.allowTaint, + allowTaint: props && props.allowTaint, // TODO: improve to useCORS if every source has CORS enabled - useCORS: props.allowTaint + useCORS: props && props.allowTaint }); }, this); queue = [this.refs.canvas, ...queue]; @@ -172,12 +178,12 @@ let GrabLMap = React.createClass({ }else { cx.globalAlpha = 1; } - cx.drawImage(canv, 0, 0); + cx.drawImage(canv, -1 * left, 0); return pCanv; }); - this.props.onStatusChange("READY"); - this.props.onSnapshotReady(canvas); + this.props.onStatusChange("READY", this.isTainted(canvas)); + this.props.onSnapshotReady(canvas, null, null, null, this.isTainted(canvas)); }); } @@ -186,8 +192,8 @@ let GrabLMap = React.createClass({ * Check if the canvas is tainted, so if it is allowed to export images * from it. */ - isTainted() { - let canvas = this.refs.canvas; + isTainted(can) { + let canvas = can || this.refs.canvas; let ctx = canvas.getContext("2d"); try { // try to generate a small image diff --git a/web/client/components/map/leaflet/snapshot/__tests__/GrabMap-test.jsx b/web/client/components/map/leaflet/snapshot/__tests__/GrabMap-test.jsx index 1ac048af13..22552d08df 100644 --- a/web/client/components/map/leaflet/snapshot/__tests__/GrabMap-test.jsx +++ b/web/client/components/map/leaflet/snapshot/__tests__/GrabMap-test.jsx @@ -42,6 +42,18 @@ describe("test the Leaflet GrabMap component", () => { const tb = ReactDOM.render( { expect(tb.isTainted()).toBe(false); done(); }} layers={[{loading: false, visibility: true}, {loading: false}]}/>, document.getElementById("snap")); expect(tb).toExist(); }); + it('snapshot creation with opacity', (done) => { + const tb = ReactDOM.render( { expect(tb.isTainted()).toBe(false); done(); }} layers={[{loading: false, opacity: 0.5, visibility: true}, {loading: false}]}/>, document.getElementById("snap")); + expect(tb).toExist(); + }); + it('snapshot creation with only one layer', (done) => { + const tb = ReactDOM.render( { expect(tb.isTainted()).toBe(false); done(); }} layers={[{loading: false}]}/>, document.getElementById("snap")); + expect(tb).toExist(); + }); + it('snapshot creation with only one layer', (done) => { + const tb = ReactDOM.render( { expect(tb.isTainted()).toBe(false); done(); }} layers={[{loading: false}]}/>, document.getElementById("snap")); + expect(tb).toExist(); + }); /* it('snapshot update', (done) => { const tb = ReactDOM.render( { expect(tb.isTainted()).toBe(false); done(); }} layers={[{loading: false, visibility: true}, {loading: false}]}/>, document.getElementById("snap")); diff --git a/web/client/components/map/leaflet/snapshot/snapshotMapStyle.css b/web/client/components/map/leaflet/snapshot/snapshotMapStyle.css new file mode 100644 index 0000000000..beec9a2f2c --- /dev/null +++ b/web/client/components/map/leaflet/snapshot/snapshotMapStyle.css @@ -0,0 +1,10 @@ + .snapshot_hidden_map{ + position:absolute; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + visibility: hidden; + overflow: hidden; + z-index:-1000; + } diff --git a/web/client/components/map/openlayers/snapshot/GrabMap.jsx b/web/client/components/map/openlayers/snapshot/GrabMap.jsx index 3303d7a12c..7562f625ed 100644 --- a/web/client/components/map/openlayers/snapshot/GrabMap.jsx +++ b/web/client/components/map/openlayers/snapshot/GrabMap.jsx @@ -104,7 +104,13 @@ let GrabOlMap = React.createClass({ if (this.toLoad === 0) { let map = (this.refs.snapMap) ? this.refs.snapMap.map : null; if (map) { - map.once('postcompose', (e) => this.createSnapshot(e.context.canvas)); + map.once('postrender', (e) => setTimeout( () => { + let canvas = e.map && e.map.getTargetElement() && e.map.getTargetElement().getElementsByTagName("canvas")[0]; + if (canvas) { + this.createSnapshot(canvas); + } + }, 500)); + // map.once('postcompose', (e) => setTimeout( () => this.createSnapshot(e.context.canvas), 100)); } } }, @@ -117,7 +123,25 @@ let GrabOlMap = React.createClass({ this.toLoad++; }, createSnapshot(canvas) { - this.props.onSnapshotReady(canvas); + this.props.onSnapshotReady(canvas, null, null, null, this.isTainted(canvas)); + }, + /** + * Check if the canvas is tainted, so if it is allowed to export images + * from it. + */ + isTainted(canvas) { + if (canvas) { + let ctx = canvas.getContext("2d"); + try { + // try to generate a small image + ctx.getImageData(0, 0, 1, 1); + return false; + } catch(err) { + // check the error code for tainted resources + return (err.code === 18); + } + } + } }); diff --git a/web/client/components/map/openlayers/snapshot/Preview.jsx b/web/client/components/map/openlayers/snapshot/Preview.jsx index ce094372ae..1b12ae50ee 100644 --- a/web/client/components/map/openlayers/snapshot/Preview.jsx +++ b/web/client/components/map/openlayers/snapshot/Preview.jsx @@ -44,7 +44,7 @@ let GrabLMap = React.createClass({ canvas: , drawCanvas: true, mapId: "map", - timeout: 1000 + timeout: 0 }; }, componentDidMount() { @@ -61,38 +61,33 @@ let GrabLMap = React.createClass({ let mapIsLoading = this.mapIsLoading(this.props.layers); if (!mapIsLoading && this.props.active) { this.props.onStatusChange("SHOTING"); - this.previousTimeout = setTimeout(() => { - this.doSnapshot(); - }, - this.props.timeout); + this.triggerShooting(this.props.timeout); } }, componentWillReceiveProps(nextProps) { - let mapIsLoading = this.mapIsLoading(nextProps.layers); - let mapChanged = this.mapChanged(nextProps); if (this.previousTimeout) { clearTimeout(this.previousTimeout); } - if ( nextProps.active && !mapIsLoading && mapChanged ) { - this.props.onStatusChange("SHOTING"); - this.previousTimeout = setTimeout(() => { - this.doSnapshot(); - }, - nextProps.timeout); - } else { - if (!nextProps.active) { - this.props.onStatusChange("DISABLED"); - if (this.props.snapstate.error) { - this.props.onSnapshotError(null); - } + if (!nextProps.active) { + this.props.onStatusChange("DISABLED"); + if (this.props.snapstate.error) { + this.props.onSnapshotError(null); } } - if (!mapIsLoading && nextProps.active && (mapChanged || nextProps.snapstate.state === "SHOTING") ) { - this.triggerShooting(nextProps.timeout); - } }, shouldComponentUpdate(nextProps) { - return this.mapChanged(nextProps) && this.props.snapstate !== nextProps.snapstate; + return this.mapChanged(nextProps) || this.props.snapstate !== nextProps.snapstate; + }, + componentDidUpdate(prevProps) { + let mapIsLoading = this.mapIsLoading(this.props.layers); + let mapChanged = this.mapChanged(prevProps); + if (!mapIsLoading && this.props.active && (mapChanged || this.props.snapstate.state === "SHOTING") ) { + if (this.props.snapstate.state !== "SHOTING") { + this.props.onStatusChange("SHOTING"); + } + this.triggerShooting(this.props.timeout); + } + }, componentWillUnmount() { if (this.previousTimeout) { @@ -109,7 +104,8 @@ let GrabLMap = React.createClass({ height={this.props.config && this.props.config.size ? this.props.config.size.height : "100%"} style={{ maxWidth: "400px", - maxHeight: "400px" + maxHeight: "400px", + visibility: this.props.active ? "block" : "none" }} ref="canvas" /> ); @@ -121,6 +117,9 @@ let GrabLMap = React.createClass({ return layers.some((layer) => { return layer.visibility && layer.loading; }); }, triggerShooting(delay) { + if (this.previousTimeout) { + clearTimeout(this.previousTimeout); + } this.previousTimeout = setTimeout(() => { this.doSnapshot(); }, @@ -128,18 +127,19 @@ let GrabLMap = React.createClass({ }, doSnapshot() { var div = document.getElementById(this.props.mapId); - let sourceCanvas = div.getElementsByTagName("canvas")[0]; - if (sourceCanvas) { - let canvas = this.refs.canvas; + let sourceCanvas = div && div.getElementsByTagName("canvas")[0]; + if (sourceCanvas && this.getCanvas()) { + let canvas = this.getCanvas(); let context = canvas.getContext("2d"); context.clearRect(0, 0, canvas.width, canvas.height); context.drawImage(sourceCanvas, 0, 0); - this.props.onStatusChange("READY"); this.props.onSnapshotReady(sourceCanvas); + this.props.onStatusChange("READY", this.isTainted(sourceCanvas)); + } }, - isTainted() { - let canvas = this.refs.canvas; + isTainted(canv) { + let canvas = canv || this.refs.canvas; let ctx = canvas.getContext("2d"); try { ctx.getImageData(0, 0, 1, 1); diff --git a/web/client/components/map/openlayers/snapshot/__tests__/GrabMap-test.jsx b/web/client/components/map/openlayers/snapshot/__tests__/GrabMap-test.jsx index 7bf435e36a..75d3a32312 100644 --- a/web/client/components/map/openlayers/snapshot/__tests__/GrabMap-test.jsx +++ b/web/client/components/map/openlayers/snapshot/__tests__/GrabMap-test.jsx @@ -56,6 +56,9 @@ describe("the OL GrabMap component", () => { const tb = ReactDOM.render( { done(); }}/>, document.getElementById("snap")); expect(tb).toExist(); tb.setProps({active: true}); + // emulate map load + tb.layerLoading(); + tb.layerLoad(); // force snapshot creation tb.createSnapshot(); }); diff --git a/web/client/components/mapcontrols/Snapshot/SnapshotPanel.jsx b/web/client/components/mapcontrols/Snapshot/SnapshotPanel.jsx index 6a03ffa27e..eaafcb993c 100644 --- a/web/client/components/mapcontrols/Snapshot/SnapshotPanel.jsx +++ b/web/client/components/mapcontrols/Snapshot/SnapshotPanel.jsx @@ -7,7 +7,7 @@ */ const React = require('react'); -const {Button, Col, Grid, Row, Image, Glyphicon, Table, Panel} = require('react-bootstrap'); +const {Button, Col, Grid, Row, Image, Glyphicon, Table, Panel, Alert} = require('react-bootstrap'); const {DateFormat} = require('../../I18N/I18N'); require("./css/snapshot.css"); @@ -187,6 +187,11 @@ let SnapshotPanel = React.createClass({ } return panel; }, + renderTaintedMessage() { + if (this.props.snapshot && this.props.snapshot && this.props.snapshot.tainted) { + return ; + } + }, render() { let bingOrGoogle = this.isBingOrGoogle(); let snapshotReady = this.isSnapshotReady(); @@ -211,6 +216,7 @@ let SnapshotPanel = React.createClass({ { this.renderButton(!bingOrGoogle && snapshotReady)} + { this.renderTaintedMessage()} {this.renderSnapshotQueue()} diff --git a/web/client/components/mapcontrols/Snapshot/SnapshotQueue.jsx b/web/client/components/mapcontrols/Snapshot/SnapshotQueue.jsx index a1bf27cfc1..a2f2a10c89 100644 --- a/web/client/components/mapcontrols/Snapshot/SnapshotQueue.jsx +++ b/web/client/components/mapcontrols/Snapshot/SnapshotQueue.jsx @@ -21,6 +21,7 @@ let SnapshotQueue = React.createClass({ queue: React.PropTypes.array, browser: React.PropTypes.string, onRemoveSnapshot: React.PropTypes.func, + onSnapshotError: React.PropTypes.func, downloadImg: React.PropTypes.func, mapType: React.PropTypes.string @@ -39,6 +40,7 @@ let SnapshotQueue = React.createClass({ getDefaultProps() { return { onRemoveSnapshot: () => {}, + onSnapshotError: () => {}, mapType: 'leaflet' }; }, @@ -68,12 +70,17 @@ let SnapshotQueue = React.createClass({ return