Skip to content

Commit

Permalink
Improvements to the Snapshot Tool (#1272)
Browse files Browse the repository at this point in the history
- Fix #1267. Now if the canvas is tainted, a warning suggest the user how to have a better result
 - Removed glitch when leaflet try to save snapshot (a box was appearing in top-right corner)
 - Simplified the snapshot creation procedure (triggers)
 - Error management in case of tainted canvas in the save mode (see #1269)
 - Various Fixes with wrong sizes of snapshot
 - Now leaflet snapshot support resized map when the drawer menu do not overlap
 - Increased test coverage
  • Loading branch information
offtherailz authored Nov 15, 2016
1 parent b6518e9 commit 51cb7b2
Show file tree
Hide file tree
Showing 16 changed files with 160 additions and 71 deletions.
5 changes: 3 additions & 2 deletions web/client/actions/snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
50 changes: 28 additions & 22 deletions web/client/components/map/leaflet/snapshot/GrabMap.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}

Expand Down Expand Up @@ -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];
Expand All @@ -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));
});
}

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ describe("test the Leaflet GrabMap component", () => {
const tb = ReactDOM.render(<GrabMap active={true} timeout={0} onSnapshotReady={() => { 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(<GrabMap active={true} timeout={0} onSnapshotReady={() => { 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(<GrabMap active={true} timeout={0} onSnapshotReady={() => { 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(<GrabMap active={true} timeout={0} onSnapshotReady={() => { expect(tb.isTainted()).toBe(false); done(); }} layers={[{loading: false}]}/>, document.getElementById("snap"));
expect(tb).toExist();
});
/*
it('snapshot update', (done) => {
const tb = ReactDOM.render(<GrabMap active={false} timeout={0} onSnapshotReady={() => { expect(tb.isTainted()).toBe(false); done(); }} layers={[{loading: false, visibility: true}, {loading: false}]}/>, document.getElementById("snap"));
Expand Down
10 changes: 10 additions & 0 deletions web/client/components/map/leaflet/snapshot/snapshotMapStyle.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.snapshot_hidden_map{
position:absolute;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
visibility: hidden;
overflow: hidden;
z-index:-1000;
}
28 changes: 26 additions & 2 deletions web/client/components/map/openlayers/snapshot/GrabMap.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
},
Expand All @@ -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);
}
}

}
});

Expand Down
60 changes: 30 additions & 30 deletions web/client/components/map/openlayers/snapshot/Preview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ let GrabLMap = React.createClass({
canvas: <canvas></canvas>,
drawCanvas: true,
mapId: "map",
timeout: 1000
timeout: 0
};
},
componentDidMount() {
Expand All @@ -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) {
Expand All @@ -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" />
);
Expand All @@ -121,25 +117,29 @@ 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();
},
delay);
},
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ describe("the OL GrabMap component", () => {
const tb = ReactDOM.render(<GrabMap config={map} layers={layers} snapstate={{state: "DISABLED"}} active={false} timeout={0} onSnapshotReady={() => { done(); }}/>, document.getElementById("snap"));
expect(tb).toExist();
tb.setProps({active: true});
// emulate map load
tb.layerLoading();
tb.layerLoad();
// force snapshot creation
tb.createSnapshot();
});
Expand Down
8 changes: 7 additions & 1 deletion web/client/components/mapcontrols/Snapshot/SnapshotPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -187,6 +187,11 @@ let SnapshotPanel = React.createClass({
}
return panel;
},
renderTaintedMessage() {
if (this.props.snapshot && this.props.snapshot && this.props.snapshot.tainted) {
return <Alert bsStyle="warning"><Message msgId="snapshot.taintedMessage" /></Alert>;
}
},
render() {
let bingOrGoogle = this.isBingOrGoogle();
let snapshotReady = this.isSnapshotReady();
Expand All @@ -211,6 +216,7 @@ let SnapshotPanel = React.createClass({

<Row key="buttons" htopclassName="pull-right" style={{marginTop: "5px"}}>
{ this.renderButton(!bingOrGoogle && snapshotReady)}
{ this.renderTaintedMessage()}
{this.renderSnapshotQueue()}
</Row>

Expand Down
19 changes: 13 additions & 6 deletions web/client/components/mapcontrols/Snapshot/SnapshotQueue.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -39,6 +40,7 @@ let SnapshotQueue = React.createClass({
getDefaultProps() {
return {
onRemoveSnapshot: () => {},
onSnapshotError: () => {},
mapType: 'leaflet'
};
},
Expand Down Expand Up @@ -68,12 +70,17 @@ let SnapshotQueue = React.createClass({
return <noscript />;
},
saveImage(canvas, config) {
let dataURL = canvas.toDataURL();
this.props.onRemoveSnapshot(config);
setTimeout(() => {
this.props.downloadImg(dataURL);
}, 0);

try {
this.props.onSnapshotError();
let dataURL = canvas.toDataURL();
this.props.onRemoveSnapshot(config);
setTimeout(() => {
this.props.downloadImg(dataURL);
}, 0);
} catch(e) {
this.props.onSnapshotError("Error saving snapshot");
this.props.onRemoveSnapshot(config);
}
}

});
Expand Down
Loading

0 comments on commit 51cb7b2

Please sign in to comment.