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 (
+
+ );
+ };
+ 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)} |
+ |
+
+ );
+ }
+ }
+ };
+ getSearchMarkerLabel = () => {
+ // TODO
+ return "";
+ };
+ getAttributionLabel = () => {
+ // TODO
+ return "";
+ };
+}
+
+export default connect((state) => ({
+}), {
+ setCurrentTask: setCurrentTask
+})(MapExport3D);
diff --git a/components/map3d/PrintScreen3D.jsx b/components/map3d/PrintScreen3D.jsx
deleted file mode 100644
index 0a9d2c99e..000000000
--- a/components/map3d/PrintScreen3D.jsx
+++ /dev/null
@@ -1,135 +0,0 @@
-/**
- * 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 FileSaver from 'file-saver';
-import PropTypes from 'prop-types';
-import utif from "utif";
-
-import {setCurrentTask} from '../../actions/task';
-import LocaleUtils from '../../utils/LocaleUtils';
-import TaskBar from '../TaskBar';
-
-import './style/PrintScreen3D.css';
-
-
-class PrintScreen3D extends React.Component {
- static propTypes = {
- sceneContext: PropTypes.object,
- setCurrentTask: PropTypes.func
- };
- state = {
- selectedFormat: 'image/jpeg',
- x: 0,
- y: 0,
- width: 0,
- height: 0
- };
- formatChanged = (ev) => {
- this.setState({selectedFormat: ev.target.value});
- };
- renderBody = () => {
- const formatMap = {
- "image/jpeg": "JPEG",
- "image/png": "PNG",
- "image/tiff": "TIFF"
- };
- return (
-
-
{LocaleUtils.tr("printscreen3d.selectinfo")}
-
- {LocaleUtils.tr("printscreen3d.format")}
-
-
-
- );
- };
- 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() {
- 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 onMouseMove = (event) => {
- this.setState((state) => ({
- width: Math.round(Math.max(0, Math.round(event.clientX - rect.left) - state.x)),
- height: Math.round(Math.max(0, Math.round(event.clientY - rect.top) - state.y))
- }));
- };
- ev.view.addEventListener('mousemove', onMouseMove);
- ev.view.addEventListener('mouseup', () => {
- ev.view.removeEventListener('mousemove', onMouseMove);
- this.bboxSelected();
- this.setState({x: 0, y: 0, width: 0, height: 0});
- }, {once: true});
- }
- };
- bboxSelected = () => {
- 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 === "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(/.*\//, ''));
- } else {
- canvas.toBlob((blob) => {
- FileSaver.saveAs(blob, "export." + this.state.selectedFormat.replace(/.*\//, ''));
- }, this.state.selectedFormat);
- }
- };
- }
- };
-}
-
-export default connect((state) => ({
-}), {
- setCurrentTask: setCurrentTask
-})(PrintScreen3D);
diff --git a/components/map3d/TopBar3D.jsx b/components/map3d/TopBar3D.jsx
index 5a817260c..f74508fcc 100644
--- a/components/map3d/TopBar3D.jsx
+++ b/components/map3d/TopBar3D.jsx
@@ -35,7 +35,7 @@ class TopBar3D extends React.Component {
{key: "Measure3D", icon: "measure"},
{key: "Compare3D", icon: "compare"},
{key: "DateTime3D", icon: "clock"},
- {key: "PrintScreen3D", icon: "rasterexport"}
+ {key: "MapExport3D", icon: "rasterexport"}
];
return (
diff --git a/components/map3d/style/PrintScreen3D.css b/components/map3d/style/MapExport3D.css
similarity index 66%
rename from components/map3d/style/PrintScreen3D.css
rename to components/map3d/style/MapExport3D.css
index b57963b79..329c8efc7 100644
--- a/components/map3d/style/PrintScreen3D.css
+++ b/components/map3d/style/MapExport3D.css
@@ -1,18 +1,15 @@
-div.printscreen3d-event-container {
+div.mapexport3d-event-container {
z-index: 2;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
-
}
-div.printscreen3d-frame {
+div.mapexport3d-frame {
position: absolute;
- background: rgba(255, 255, 255, 0.5);
box-shadow: rgba(0, 0, 0, 0.5) 0 0 0 30000px;
- padding: 0.25em;
white-space: nowrap;
display: flex;
align-items: center;
@@ -22,11 +19,7 @@ div.printscreen3d-frame {
user-select: none;
}
-div.printscreen3d-body {
- display: flex;
- align-items: center;
-}
-
-div.printscreen3d-body > div:first-child {
- margin-right: 1em;
+span.mapexport3d-frame-label {
+ padding: 0.25em;
+ background: rgba(255, 255, 255, 0.5);
}
\ No newline at end of file