diff --git a/components/map3d/Map3D.jsx b/components/map3d/Map3D.jsx index a76618519..6538fc2d2 100644 --- a/components/map3d/Map3D.jsx +++ b/components/map3d/Map3D.jsx @@ -35,9 +35,9 @@ import Compare3D from './Compare3D'; import Draw3D from './Draw3D'; import LayerTree3D from './LayerTree3D'; import Map3DLight from './Map3DLight'; +import MapExport3D from './MapExport3D'; import Measure3D from './Measure3D'; import OverviewMap3D from './OverviewMap3D'; -import PrintScreen3D from './PrintScreen3D'; import TopBar3D from './TopBar3D'; import LayerRegistry from './layers/index'; @@ -365,7 +365,7 @@ class Map3D extends React.Component { - + ) : null} diff --git a/components/map3d/MapExport3D.jsx b/components/map3d/MapExport3D.jsx new file mode 100644 index 000000000..2544d4517 --- /dev/null +++ b/components/map3d/MapExport3D.jsx @@ -0,0 +1,329 @@ +/** + * Copyright 2024 Sourcepole AG + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import {connect} from 'react-redux'; + +import axios from 'axios'; +import FileSaver from 'file-saver'; +import formDataEntries from 'formdata-json'; +import isEmpty from 'lodash.isempty'; +import PropTypes from 'prop-types'; +import utif from 'utif'; + +import {setCurrentTask} from '../../actions/task'; +import LocaleUtils from '../../utils/LocaleUtils'; +import MiscUtils from '../../utils/MiscUtils'; +import Icon from '../Icon'; +import SideBar from '../SideBar'; +import Spinner from '../widgets/Spinner'; + +import './../../plugins/style/MapExport.css'; +import './style/MapExport3D.css'; + + +class MapExport3D extends React.Component { + static propTypes = { + hideAutopopulatedFields: PropTypes.bool, + sceneContext: PropTypes.object, + setCurrentTask: PropTypes.func, + theme: PropTypes.object + }; + state = { + minimized: false, + selectedFormat: 'image/jpeg', + layout: "", + x: 0, + y: 0, + width: 0, + height: 0, + exporting: false + }; + onShow = () => { + if (!isEmpty(this.props.theme?.print)) { + const layouts = this.props.theme.print.filter(l => l.map).sort((a, b) => { + return a.name.localeCompare(b.name, undefined, {numeric: true}); + }); + const layout = layouts.find(l => l.default) || layouts[0]; + this.setState({layout: layout}); + } + }; + formatChanged = (ev) => { + this.setState({selectedFormat: ev.target.value}); + }; + layoutChanged = (ev) => { + const layout = this.props.theme.print.find(item => item.name === ev.target.value); + this.setState({layout: layout}); + }; + renderBody = () => { + const formatMap = { + "image/jpeg": "JPEG", + "image/png": "PNG", + "image/tiff": "TIFF", + "application/pdf": "PDF" + }; + const layouts = this.props.theme.print.filter(l => l.map).sort((a, b) => { + return a.name.localeCompare(b.name, undefined, {numeric: true}); + }); + const exportDisabled = this.state.exporting || this.state.width === 0 || ( + this.state.selectedFormat === "application/pdf" && !this.state.layout + ); + const mapName = this.state.layout?.map?.name || ""; + return ( +
+
+ + + + + + + {this.state.selectedFormat === 'application/pdf' ? ( + + + + + ) : null} + {this.state.selectedFormat === 'application/pdf' ? (this.state.layout?.labels || []).map(label => { + // Omit labels which start with __ + if (label.startsWith("__")) { + return null; + } + const opts = { + rows: 1, + name: label.toUpperCase(), + ...this.props.theme.printLabelConfig?.[label] + }; + return this.renderPrintLabelField(label, opts); + }) : null} + +
{LocaleUtils.tr("rasterexport.format")} + +
{LocaleUtils.tr("print.layout")} + +
+ {this.state.selectedFormat === 'application/pdf' ? ( +
+ + + + + + + + + + +
+ ) : null} +
+ +
+
+
+ ); + }; + renderExportFrame = () => { + const boxStyle = { + left: this.state.x + 'px', + top: this.state.y + 'px', + width: this.state.width + 'px', + height: this.state.height + 'px' + }; + return ( +
+
+ + {this.state.width + " x " + this.state.height} + +
+
+ ); + }; + render() { + const minMaxTooltip = this.state.minimized ? LocaleUtils.tr("print.maximize") : LocaleUtils.tr("print.minimize"); + const minMaxIcon = this.state.minimized ? 'chevron-down' : 'chevron-up'; + const extraTitlebarContent = ( + this.setState((state) => ({minimized: !state.minimized}))} title={minMaxTooltip}/> + ); + return ( + + {() => ({ + body: this.renderBody(), + extra: this.renderExportFrame() + })} + + ); + } + startSelection = (ev) => { + if (ev.button === 0) { + const rect = ev.target.getBoundingClientRect(); + this.setState({ + x: Math.round(ev.clientX - rect.left), + y: Math.round(ev.clientY - rect.top), + width: 0, + height: 0 + }); + const constrainRatio = this.state.selectedFormat === "application/pdf" && this.state.layout; + const ratio = constrainRatio ? this.state.layout.map.height / this.state.layout.map.width : null; + const onMouseMove = (event) => { + this.setState((state) => { + const width = Math.round(Math.max(0, Math.round(event.clientX - rect.left) - state.x)); + const height = constrainRatio ? Math.round(width * ratio) : Math.round(Math.max(0, Math.round(event.clientY - rect.top) - state.y)); + return { + width: width, + height: height + }; + }); + }; + ev.view.addEventListener('mousemove', onMouseMove); + ev.view.addEventListener('mouseup', () => { + ev.view.removeEventListener('mousemove', onMouseMove); + }, {once: true}); + } + }; + export = (ev) => { + ev.preventDefault(); + const form = ev.target; + this.setState({exporting: true}); + const {x, y, width, height} = this.state; + if (width > 0 && height > 0) { + const data = this.props.sceneContext.scene.renderer.domElement.toDataURL('image/png'); + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const img = new Image(); + img.src = data; + img.onload = () => { + canvas.width = width; + canvas.height = height; + ctx.drawImage(img, -x, -y); + if (this.state.selectedFormat === "application/pdf") { + canvas.toBlob((blob) => { + blob.arrayBuffer().then(imgBuffer => this.exportToPdf(form, imgBuffer)); + }, "image/png"); + } else if (this.state.selectedFormat === "image/tiff") { + const imageData = ctx.getImageData(0, 0, width, height); + const blob = new Blob([utif.encodeImage(imageData.data, width, height)], { type: "image/tiff" }); + FileSaver.saveAs(blob, "export." + this.state.selectedFormat.replace(/.*\//, '')); + this.setState({exporting: false}); + } else { + canvas.toBlob((blob) => { + FileSaver.saveAs(blob, "export." + this.state.selectedFormat.replace(/.*\//, '')); + this.setState({exporting: false}); + }, this.state.selectedFormat); + } + }; + } + }; + async exportToPdf(form, imgBuffer) { + const formData = { + ...formDataEntries(new FormData(form)), + ...Object.fromEntries((this.props.theme.extraPrintParameters || "").split("&").filter(Boolean).map(entry => entry.split("="))) + }; + const data = Object.entries(formData).map((pair) => + pair.map(entry => encodeURIComponent(entry).replace(/%20/g, '+')).join("=") + ).join("&"); + const config = { + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + responseType: "arraybuffer" + }; + const response = await axios.post(this.props.theme.printUrl, data, config); + if (response) { + const {PDFDocument} = await import('pdf-lib'); + const doc = await PDFDocument.load(response.data); + const page = doc.getPages()[0]; + const pngImage = await doc.embedPng(imgBuffer); + const x = this.state.layout.map.x * 2.8346; + const y = this.state.layout.map.y * 2.8346; + const width = this.state.layout.map.width * 2.8346; + const height = this.state.layout.map.height * 2.8346; + page.drawImage(pngImage, { + x: x, + y: y, + width: width, + height: height + }); + const pdfData = await doc.save(); + const blob = new Blob([pdfData], { type: 'application/pdf' }); + FileSaver.saveAs(blob, this.state.layout.name + ".pdf"); + this.setState({exporting: false}); + } else { + /* eslint-disable-next-line */ + alert('Print failed'); + this.setState({exporting: false}); + } + }; + renderPrintLabelField = (label, opts) => { + let defaultValue = opts.defaultValue || ""; + let autopopulated = false; + if (label === this.props.theme.printLabelForSearchResult) { + defaultValue = this.getSearchMarkerLabel(); + autopopulated = true; + } else if (label === this.props.theme.printLabelForAttribution) { + defaultValue = this.getAttributionLabel(); + autopopulated = true; + } + if (autopopulated && this.props.hideAutopopulatedFields) { + return (); + } else { + if (opts.options) { + return ( + + {MiscUtils.capitalizeFirst(label)} + + + + + ); + } else { + const style = {}; + if (opts.rows || opts.cols) { + style.resize = 'none'; + } + if (opts.cols) { + style.width = 'initial'; + } + return ( + + {MiscUtils.capitalizeFirst(label)} +