terriajs
Version:
Geospatial data visualization platform.
264 lines (225 loc) • 9.61 kB
JSX
;
import { formatDateTime } from '../../../BottomDock/Timeline/DateFormats';
import createReactClass from 'create-react-class';
import Description from '../../../Preview/Description';
import DOMPurify from 'dompurify/dist/purify';
import FeatureInfoPanel from '../../../FeatureInfo/FeatureInfoPanel';
import Legend from '../../../Workbench/Controls/Legend';
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';
const PrintView = createReactClass({
displayName: 'PrintView',
propTypes: {
terria: PropTypes.object,
viewState: PropTypes.object,
window: PropTypes.object,
readyCallback: PropTypes.func
},
getInitialState() {
return {
mapImageDataUrl: undefined,
ready: false,
printingStarted: false
};
},
componentDidMount() {
return this.props.terria.currentViewer.captureScreenshot().then(mapImageDataUrl => {
// We need to periodically check whether all images are loaded.
// We can theoretically do that either with a setInterval on the original TerriaJS window,
// or on the print view window. But:
// * Chrome (as of v66.0.3359.139 anyway) seems to aggressively suspend setInterval calls in background
// tabs, so only a setInterval on the print view window works reliably.
// * Internet Explorer 11 does not seem to allow a cross-window setInterval call, so only a setInterval
// on the original TerriaJS window works reliably.
// So, we'll do both.
const printWindow = this.props.window;
const mainWindow = window;
const printWindowIntervalId = printWindow.setInterval(this.checkForImagesReady, 200);
const mainWindowIntervalId = mainWindow.setInterval(this.checkForImagesReady, 200);
this._stopCheckingForImages = () => {
printWindow.clearInterval(printWindowIntervalId);
mainWindow.clearInterval(mainWindowIntervalId);
this._stopCheckingForImages = undefined;
};
this.setState({
mapImageDataUrl: mapImageDataUrl
});
});
},
componentWillUnmount() {
this.stopCheckingForImages();
},
componentDidUpdate() {
if (this.state.ready && !this.state.printingStarted) {
if (this.props.readyCallback) {
this.props.readyCallback(this.props.window);
}
this.setState({
printingStarted: true
});
}
},
stopCheckingForImages() {
if (this._stopCheckingForImages) {
this._stopCheckingForImages();
}
},
checkForImagesReady() {
if (this.state.ready) {
return;
}
const imageTags = this.props.window.document.getElementsByTagName('img');
if (imageTags.length === 0) {
return;
}
let allImagesReady = true;
for (let i = 0; allImagesReady && i < imageTags.length; ++i) {
allImagesReady = imageTags[i].complete;
}
if (allImagesReady) {
this.stopCheckingForImages();
this.setState({
ready: allImagesReady
});
}
},
render() {
if (!this.state.mapImageDataUrl) {
return <div>Creating print view...</div>;
}
return (
<div>
<p>
<img className="map-image" src={this.state.mapImageDataUrl} alt="Map snapshot" />
</p>
<h1>Legends</h1>
{this.props.terria.nowViewing.items.map(this.renderLegend)}
{this.renderFeatureInfo()}
<h1>Dataset Details</h1>
{this.props.terria.nowViewing.items.map(this.renderDetails)}
<h1>Map Credits</h1>
<ul>
{this.props.terria.currentViewer.getAllAttribution().map(this.renderAttribution)}
</ul>
<If condition={this.props.terria.configParameters.printDisclaimer}>
<h1>Print Disclaimer</h1>
<p>{this.props.terria.configParameters.printDisclaimer.text}</p>
</If>
</div>
);
},
renderAttribution(attribution) {
// For reasons I don't entirely understanding, using parseCustomHtmlToReact instead
// of dangerouslySetInnerHTML here doesn't work in IE11 or Edge. All elements after
// the first attribution end up just completely missing from the DOM.
const html = { __html: DOMPurify.sanitize(attribution) };
return (<li key={attribution} dangerouslySetInnerHTML={html}></li>);
},
renderLegend(catalogItem) {
if (!catalogItem.isMappable) {
return null;
}
return (
<div key={catalogItem.uniqueId} className="layer-legends">
<div className="layer-title">{catalogItem.name}</div>
{catalogItem.discreteTime && <div className="layer-time">Time: {formatDateTime(catalogItem.discreteTime)}</div>}
<Legend item={catalogItem} />
</div>
);
},
renderDetails(catalogItem) {
if (!catalogItem.isMappable) {
return null;
}
const nowViewingItem = catalogItem.nowViewingCatalogItem || catalogItem;
return (
<div key={catalogItem.uniqueId} className="layer-details">
<h2>{catalogItem.name}</h2>
<Description item={nowViewingItem} printView={true} />
</div>
);
},
renderFeatureInfo() {
if (!this.props.viewState.featureInfoPanelIsVisible || !this.props.terria.pickedFeatures ||
!this.props.terria.pickedFeatures.features || this.props.terria.pickedFeatures.features.length === 0) {
return null;
}
return (
<div className="feature-info">
<h1>Feature Information</h1>
<FeatureInfoPanel terria={this.props.terria} viewState={this.props.viewState} printView={true} />
</div>
);
}
});
PrintView.Styles = `
.tjs-_base__list-reset {
list-style: none;
padding-left: 0;
margin: 0;
}
.background {
width: 100%;
fill: rgba(255, 255, 255, 1.0);
}
.map-image {
max-width: 95vw;
max-height: 95vh;
}
.layer-legends {
display: inline;
float: left;
padding-left: 20px;
padding-right: 20px;
}
.layer-title {
font-weight: bold;
}
h1, h2, h3 {
clear: both;
}
.tjs-_form__input {
width: 80%;
}
`;
/**
* Creates a new printable view.
*
* @param {Terria} options.terria The Terria instance.
* @param {ViewState} options.viewState The terria ViewState instance.
* @param {Window} [options.printWindow] The window in which to create the print view. This is usually a new window created with
* `window.open()` or an iframe's `contentWindow`. If undefined, a new window (tab) will be created.
* @param {Function} [options.readyCallback] A function that is called when the print view is ready to be used. The function is
* given the print view window as its only parameter.
* @param {Function} [options.closeCallback] A function that is called when the print view is closed. The function is given
* the print view window as its only parameter.
*/
PrintView.create = function(options) {
const { terria, viewState, printWindow = window.open(), readyCallback, closeCallback } = options;
if (closeCallback) {
printWindow.addEventListener('unload', () => {
closeCallback(printWindow);
});
}
// Open and immediately close the document. This works around a problem in Firefox that is
// captured here: https://bugzilla.mozilla.org/show_bug.cgi?id=667227.
// Essentially, when we first create an iframe, it has no document loaded and asynchronously
// starts a load of "about:blank". If we access the document object and start manipulating it
// before that async load completes, a new document will be automatically created. But then
// when the async load completes, the original, automatically-created document gets unloaded
// and the new "about:blank" gets swapped in. End result: everything we add to the DOM before
// the async load complete gets lost and Firefox ends up printing a blank page.
// Explicitly opening and then closing a new document _seems_ to avoid this.
printWindow.document.open();
printWindow.document.close();
printWindow.document.head.innerHTML = `
<meta charset="UTF-8">
<title>${terria.appName} Print View</title>
<style>${PrintView.Styles}</style>
`;
printWindow.document.body.innerHTML = '<div id="print"></div>';
const printView = <PrintView terria={terria} viewState={viewState} window={printWindow} readyCallback={readyCallback} />;
ReactDOM.render(printView, printWindow.document.getElementById('print'));
};
module.exports = PrintView;