diff --git a/components/PrintFrame.jsx b/components/PrintFrame.jsx deleted file mode 100644 index e2213692d..000000000 --- a/components/PrintFrame.jsx +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Copyright 2016-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 isEqual from 'lodash.isequal'; -import PropTypes from 'prop-types'; - -import MapUtils from '../utils/MapUtils'; - -import './style/PrintFrame.css'; - -export default class PrintFrame extends React.Component { - static propTypes = { - bboxSelected: PropTypes.func, - dpi: PropTypes.number, - fixedFrame: PropTypes.shape({ - width: PropTypes.number, // in meters - height: PropTypes.number // in meters - }), - map: PropTypes.object.isRequired - }; - static defaultProps = { - bboxSelected: () => {}, - dpi: 96 - }; - state = { - x: 0, y: 0, width: 0, height: 0, moving: false - }; - componentDidMount() { - this.recomputeBox(this.props, {}); - } - componentDidUpdate(prevProps) { - if ( - this.props.map.center !== prevProps.map.center || - this.props.map.bbox !== prevProps.map.bbox || - this.props.dpi !== prevProps.dpi || - !isEqual(this.props.fixedFrame, prevProps.fixedFrame) - ) { - this.recomputeBox(); - } - if (!this.props.fixedFrame && prevProps.fixedFrame) { - this.setState({x: 0, y: 0, width: 0, height: 0, moving: false}); - } - } - recomputeBox = () => { - if (this.props.fixedFrame) { - const getPixelFromCoordinate = MapUtils.getHook(MapUtils.GET_PIXEL_FROM_COORDINATES_HOOK); - let newState = {x: 0, y: 0, width: 0, height: 0, moving: false}; - const cosa = Math.cos(-this.props.map.bbox.rotation); - const sina = Math.sin(-this.props.map.bbox.rotation); - const center = this.props.map.center; - const {width, height} = MapUtils.transformExtent(this.props.map.projection, center, this.props.fixedFrame.width, this.props.fixedFrame.height); - const mapp1 = [ - center[0] - 0.5 * width * cosa - 0.5 * height * sina, - center[1] + 0.5 * width * sina - 0.5 * height * cosa - ]; - const mapp2 = [ - center[0] + 0.5 * width * cosa + 0.5 * height * sina, - center[1] - 0.5 * width * sina + 0.5 * height * cosa - ]; - const pixp1 = getPixelFromCoordinate(mapp1); - const pixp2 = getPixelFromCoordinate(mapp2); - newState = { - x: Math.min(pixp1[0], pixp2[0]), - y: Math.min(pixp1[1], pixp2[1]), - width: Math.abs(pixp2[0] - pixp1[0]), - height: Math.abs(pixp2[1] - pixp1[1]) - }; - this.setState(newState, this.notifyBBox); - } else { - this.notifyBBox(); - } - }; - startSelection = (ev) => { - if (ev.button === 1) { - document.addEventListener('mouseup', () => { ev.target.style.pointerEvents = ''; }, {once: true}); - // Move behind - ev.target.style.pointerEvents = 'none'; - MapUtils.getHook(MapUtils.GET_MAP).getViewport().dispatchEvent(new MouseEvent('pointerdown', ev)); - return; - } - const x = Math.round(ev.clientX); - const y = Math.round(ev.clientY); - this.setState({ - x: x, - y: y, - width: 0, - height: 0, - moving: true - }); - }; - updateSelection = (ev) => { - if (this.state.moving) { - this.setState((state) => { - const x = Math.round(ev.clientX); - const y = Math.round(ev.clientY); - const width = Math.round(Math.max(0, x - state.x)); - const height = Math.round(Math.max(0, y - state.y)); - return { - width: width, - height: height - }; - }); - } - }; - endSelection = () => { - if (this.state.moving) { - this.setState({moving: false}); - this.notifyBBox(); - } - }; - notifyBBox = () => { - const getCoordinateFromPixel = MapUtils.getHook(MapUtils.GET_COORDINATES_FROM_PIXEL_HOOK); - const p1 = getCoordinateFromPixel([this.state.x, this.state.y]); - const p2 = getCoordinateFromPixel([this.state.x + this.state.width, this.state.y + this.state.height]); - const bbox = [ - Math.min(p1[0], p2[0]), - Math.min(p1[1], p2[1]), - Math.max(p1[0], p2[0]), - Math.max(p1[1], p2[1]) - ]; - if (bbox[0] !== bbox[2] && bbox[1] !== bbox[3]) { - const dpiScale = this.props.dpi / 96; - this.props.bboxSelected(bbox, this.props.map.projection, [this.state.width * dpiScale, this.state.height * dpiScale]); - } else { - this.props.bboxSelected(null, this.props.map.projection, [0, 0]); - } - }; - render() { - const boxStyle = { - left: this.state.x + 'px', - top: this.state.y + 'px', - width: this.state.width + 'px', - height: this.state.height + 'px', - lineHeight: this.state.height + 'px' - }; - if (this.props.fixedFrame) { - return ( -
- ); - } else { - return ( -
this.endSelection(ev.changedTouches[0])} - onTouchMove={(ev) => {this.updateSelection(ev.changedTouches[0]); ev.preventDefault();}} - onTouchStart={(ev) => this.startSelection(ev.changedTouches[0])} - > -
- {this.state.width + " x " + this.state.height} -
-
- ); - } - } -} diff --git a/components/PrintSelection.jsx b/components/PrintSelection.jsx new file mode 100644 index 000000000..6dd4f31a9 --- /dev/null +++ b/components/PrintSelection.jsx @@ -0,0 +1,616 @@ +/** + * Copyright 2024 Stadtwerke München GmbH + * 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 isEqual from 'lodash.isequal'; +import ol from 'openlayers'; +import PropTypes from 'prop-types'; + +import FeatureStyles from '../utils/FeatureStyles'; +import MapUtils from '../utils/MapUtils'; + + +export default class PrintSelection extends React.Component { + static propTypes = { + allowRotation: PropTypes.bool, + allowScaling: PropTypes.bool, + allowTranslation: PropTypes.bool, + center: PropTypes.arrayOf(PropTypes.number), + fixedFrame: PropTypes.shape({ + width: PropTypes.number, // in meters + height: PropTypes.number // in meters + }), + geometryChanged: PropTypes.func, + printSeriesChanged: PropTypes.func, + printSeriesEnabled: PropTypes.bool, + printSeriesGridSize: PropTypes.number, + printSeriesOverlap: PropTypes.number, + printSeriesSelected: PropTypes.arrayOf(PropTypes.string), + rotation: PropTypes.number, + scale: PropTypes.number + }; + static defaultProps = { + allowRotation: true, + allowScaling: true, + allowTranslation: true, + fixedFrame: null, + geometryChanged: () => {}, + printSeriesChanged: () => {}, + printSeriesEnabled: false, + printSeriesGridSize: 2, + printSeriesOverlap: 0, + printSeriesSelected: [], + rotation: 0, + scale: 1000 + }; + constructor(props) { + super(props); + + this.map = MapUtils.getHook(MapUtils.GET_MAP); + + // create a layer to draw on + this.source = new ol.source.Vector(); + this.selectionLayer = new ol.layer.Vector({ + source: this.source, + zIndex: 1000000, + style: this.layerStyle + }); + + // create a geometry for the background feature + const extent = this.map.getView().getProjection().getExtent() ?? [ + Number.MIN_SAFE_INTEGER, + Number.MIN_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER + ]; + const geometry = ol.geom.polygonFromExtent(extent); + + this.backgroundFeature = new ol.Feature(geometry); + this.source.addFeature(this.backgroundFeature); + + this.printSeriesFeature = new ol.Feature(geometry); + this.source.addFeature(this.printSeriesFeature); + + this.feature = null; + this.initialWidth = null; + this.initialHeight = null; + this.seriesGeometries = []; + + this.translateInteraction = null; + this.scaleRotateInteraction = null; + this.selectPrintSeriesInteraction = null; + this.drawInteraction = null; + + this.isInteracting = false; + } + componentDidUpdate(prevProps) { + if ( + !isEqual(prevProps.fixedFrame, this.props.fixedFrame) || + !isEqual(prevProps.center, this.props.center) || + prevProps.rotation !== this.props.rotation || + prevProps.scale !== this.props.scale + ) { + if (!this.isInteracting) { + this.recomputeFeature(); + } + } + if ( + prevProps.printSeriesEnabled !== this.props.printSeriesEnabled || + prevProps.printSeriesOverlap !== this.props.printSeriesOverlap || + prevProps.printSeriesSelected !== this.props.printSeriesSelected + ) { + this.geometryChanged(); + } + } + recomputeFeature() { + // delete the old feature + if (this.feature !== null) { + // remove old feature + this.source.removeFeature(this.feature); + this.feature = null; + this.initialWidth = null; + this.initialHeight = null; + this.seriesGeometries = []; + } + // render the current feature if given a fixed frame + if (this.props.fixedFrame !== null) { + // calculate actual width and height + const {width, height} = MapUtils.transformExtent( + this.map.getView().getProjection(), + this.props.center, + this.props.fixedFrame.width, + this.props.fixedFrame.height + ); + // add rectangle + const x1 = this.props.center[0] + 0.5 * width; + const x2 = this.props.center[0] - 0.5 * width; + const y1 = this.props.center[1] + 0.5 * height; + const y2 = this.props.center[1] - 0.5 * height; + const geometry = new ol.geom.Polygon([ + [ + [x1, y1], + [x1, y2], + [x2, y2], + [x2, y1], + [x1, y1] + ] + ]); + // rotate and scale rectangle + if (this.props.rotation) { + geometry.rotate(this.props.rotation * Math.PI / 180, this.props.center); + } + if (this.props.scale) { + geometry.scale(this.props.scale / 1000, undefined, this.props.center); + } + // add feature to layer + this.feature = new ol.Feature(geometry); + this.feature.on('change', this.geometryChanged); + this.source.addFeature(this.feature); + // store initial width and height for future updates + this.initialWidth = width; + this.initialHeight = height; + // update geometry to new extent + this.geometryChanged(); + } + } + componentDidMount() { + this.map.addLayer(this.selectionLayer); + this.addInteractions(); + this.recomputeFeature(); + } + componentWillUnmount() { + this.map.removeLayer(this.selectionLayer); + this.removeInteractions(); + } + addInteractions() { + // move the selection + const translateCondition = (ev) => { + return ol.events.condition.primaryAction(ev) + && this.props.fixedFrame + && this.props.allowTranslation; + }; + this.translateInteraction = new ol.interaction.Translate({ + condition: translateCondition, + // add condition to filter for correct cursor selection + filter: feature => this.props.fixedFrame && this.props.allowTranslation && feature === this.feature, + layers: [this.selectionLayer] + }); + this.translateInteraction.on('translatestart', () => { + this.isInteracting = true; + }); + this.translateInteraction.on('translateend', () => { + this.isInteracting = false; + }); + + // scale and rotate the selection + const modifyCondition = (ev) => { + return ol.events.condition.primaryAction(ev) + && this.props.fixedFrame + && (this.props.allowScaling || this.props.allowRotation); + }; + this.scaleRotateInteraction = new ol.interaction.Modify({ + source: this.source, + condition: modifyCondition, + deleteCondition: ol.events.condition.never, + insertVertexCondition: ol.events.condition.never, + pixelTolerance: 20, + style: this.scaleRotateStyle + }); + this.scaleRotateInteraction.on('modifystart', (ev) => { + this.isInteracting = true; + this.map.getViewport().style.cursor = 'grabbing'; + ev.features.forEach((feature) => { + feature.set( + 'modifyGeometry', + {geometry: feature.getGeometry().clone()}, + true, + ); + }); + }); + this.scaleRotateInteraction.on('modifyend', (ev) => { + this.isInteracting = false; + this.map.getViewport().style.cursor = ''; + ev.features.forEach((feature) => { + const modifyGeometry = feature.get('modifyGeometry'); + if (modifyGeometry) { + feature.setGeometry(modifyGeometry.geometry); + feature.unset('modifyGeometry', true); + } + }); + }); + + // select frames for printing a series + this.selectPrintSeriesInteraction = new ol.interaction.Select({ + filter: feature => feature === this.printSeriesFeature, + layers: [this.selectionLayer], + condition: ol.events.condition.click, + addCondition: ol.events.condition.always, + removeCondition: ol.events.condition.always, + style: null + }); + this.selectPrintSeriesInteraction.on('select', (ev) => { + const coordinate = ev.mapBrowserEvent.coordinate; + const intersecting = this.seriesGeometries.find((entry) => !isEqual(entry.index, [0, 0]) && entry.geometry.intersectsCoordinate(coordinate)); + + if (intersecting) { + let selected = this.props.printSeriesSelected; + if (selected.includes(intersecting.index.join(','))) { + selected = selected.filter(index => index !== intersecting.index.join(',')); + } else { + selected = [...selected, intersecting.index.join(',')]; + } + this.props.printSeriesChanged(selected); + } + }); + + // select a new area when no frame is given (only added when no fixed frame is given) + const drawCondition = (ev) => { + return ol.events.condition.primaryAction(ev) + && !this.props.fixedFrame; + }; + this.drawInteraction = new ol.interaction.Draw({ + source: this.source, + type: 'Circle', + style: FeatureStyles.printInteraction(), + geometryFunction: ol.interaction.createBox(), + condition: ol.events.condition.never, + freehandCondition: drawCondition + }); + this.drawInteraction.on('drawstart', (ev) => { + this.isInteracting = true; + this.feature = ev.feature; + this.feature.on('change', this.geometryChanged); + }); + this.drawInteraction.on('drawend', () => { + this.isInteracting = false; + this.geometryChanged(); + }); + + // register interactions + this.map.addInteraction(this.translateInteraction); + this.map.addInteraction(this.scaleRotateInteraction); + this.map.addInteraction(this.selectPrintSeriesInteraction); + this.map.addInteraction(this.drawInteraction); + } + removeInteractions() { + if (this.translateInteraction !== null) { + this.map.removeInteraction(this.translateInteraction); + this.translateInteraction = null; + } + if (this.scaleRotateInteraction !== null) { + this.map.removeInteraction(this.scaleRotateInteraction); + this.scaleRotateInteraction = null; + } + if (this.selectPrintSeriesInteraction !== null) { + this.map.removeInteraction(this.selectPrintSeriesInteraction); + this.selectPrintSeriesInteraction = null; + } + if (this.drawInteraction !== null) { + this.map.removeInteraction(this.drawInteraction); + this.drawInteraction = null; + } + } + getGeometry = () => { + const modifyGeometry = this.feature.get('modifyGeometry'); + return modifyGeometry ? modifyGeometry.geometry : this.feature.getGeometry(); + }; + getBackgroundGeometry = (feature) => { + const background = feature.getGeometry().clone(); + + if (this.feature !== null) { + const geom = this.getGeometry().clone(); + + // ignore degenerate geometries + if (geom.getArea() === 0) { + return background; + } + + // make the current selection transparent + background.appendLinearRing(geom.getLinearRing(0)); + + // add the origin to the selected tiles + const selected = ['0,0', ...this.props.printSeriesSelected]; + + // make the selected series transparent + this.seriesGeometries.filter(this.isPrintSeriesSelected).forEach((entry) => { + // add the inner part of the tile + const clonedGeom = entry.geometry.clone(); + clonedGeom.scale(1 - 2 * this.props.printSeriesOverlap); + background.appendLinearRing(clonedGeom.getLinearRing(0)); + + // clone the original geometry and rotate it + const clonedGeomBase = entry.geometry.clone(); + const center = ol.extent.getCenter(clonedGeomBase.getExtent()); + clonedGeomBase.rotate(entry.rotation, center); + const extent = clonedGeomBase.getExtent(); + + // create the geometries for the overlapping borders + const clonedGeomLeft = clonedGeomBase.clone(); + const centerLeft = [extent[0], 0.5 * (extent[1] + extent[3])]; + clonedGeomLeft.scale(this.props.printSeriesOverlap, 1 - 2 * this.props.printSeriesOverlap, centerLeft); + clonedGeomLeft.rotate(-entry.rotation, center); + + const clonedGeomRight = clonedGeomBase.clone(); + const centerRight = [extent[2], 0.5 * (extent[1] + extent[3])]; + clonedGeomRight.scale(this.props.printSeriesOverlap, 1 - 2 * this.props.printSeriesOverlap, centerRight); + clonedGeomRight.rotate(-entry.rotation, center); + + const clonedGeomBottom = clonedGeomBase.clone(); + const centerBottom = [0.5 * (extent[0] + extent[2]), extent[1]]; + clonedGeomBottom.scale(1 - 2 * this.props.printSeriesOverlap, this.props.printSeriesOverlap, centerBottom); + clonedGeomBottom.rotate(-entry.rotation, center); + + const clonedGeomTop = clonedGeomBase.clone(); + const centerTop = [0.5 * (extent[0] + extent[2]), extent[3]]; + clonedGeomTop.scale(1 - 2 * this.props.printSeriesOverlap, this.props.printSeriesOverlap, centerTop); + clonedGeomTop.rotate(-entry.rotation, center); + + // create the geometries for the overlapping corners + const clonedGeomBottomLeft = clonedGeomBase.clone(); + const bottomLeft = [extent[0], extent[1]]; + clonedGeomBottomLeft.scale(this.props.printSeriesOverlap, this.props.printSeriesOverlap, bottomLeft); + clonedGeomBottomLeft.rotate(-entry.rotation, center); + + const clonedGeomBottomRight = clonedGeomBase.clone(); + const bottomRight = [extent[2], extent[1]]; + clonedGeomBottomRight.scale(this.props.printSeriesOverlap, this.props.printSeriesOverlap, bottomRight); + clonedGeomBottomRight.rotate(-entry.rotation, center); + + const clonedGeomTopLeft = clonedGeomBase.clone(); + const topLeft = [extent[0], extent[3]]; + clonedGeomTopLeft.scale(this.props.printSeriesOverlap, this.props.printSeriesOverlap, topLeft); + clonedGeomTopLeft.rotate(-entry.rotation, center); + + const clonedGeomTopRight = clonedGeomBase.clone(); + const topRight = [extent[2], extent[3]]; + clonedGeomTopRight.scale(this.props.printSeriesOverlap, this.props.printSeriesOverlap, topRight); + clonedGeomTopRight.rotate(-entry.rotation, center); + + // calculate the neighbours of the current tile + const topNeighbour = [entry.index[0] - 1, entry.index[1]].join(','); + const bottomNeighbour = [entry.index[0] + 1, entry.index[1]].join(','); + const leftNeighbour = [entry.index[0], entry.index[1] - 1].join(','); + const rightNeighbour = [entry.index[0], entry.index[1] + 1].join(','); + + const topLeftNeighbour = [entry.index[0] - 1, entry.index[1] - 1].join(','); + const topRightNeighbour = [entry.index[0] - 1, entry.index[1] + 1].join(','); + const bottomLeftNeighbour = [entry.index[0] + 1, entry.index[1] - 1].join(','); + const bottomRightNeighbour = [entry.index[0] + 1, entry.index[1] + 1].join(','); + + // Each tile is responsible to draw its border facing away from the origin. + + // left border + if (!selected.includes(leftNeighbour) || entry.index[1] <= 0) { + background.appendLinearRing(clonedGeomLeft.getLinearRing(0)); + } + // right border + if (!selected.includes(rightNeighbour) || entry.index[1] >= 0) { + background.appendLinearRing(clonedGeomRight.getLinearRing(0)); + } + // top border + if (!selected.includes(topNeighbour) || entry.index[0] <= 0) { + background.appendLinearRing(clonedGeomTop.getLinearRing(0)); + } + // bottom border + if (!selected.includes(bottomNeighbour) || entry.index[0] >= 0) { + background.appendLinearRing(clonedGeomBottom.getLinearRing(0)); + } + + // The corners are drawn by the tile facing away from the origin, and in counter-clockwise order. + + // top-left corner + if ((entry.index[0] <= 0 && entry.index[1] <= 0) + || (entry.index[0] <= 0 && !selected.includes(leftNeighbour)) + || (entry.index[1] <= 0 && !selected.includes(topNeighbour) && !selected.includes(topLeftNeighbour)) + || (!selected.includes(leftNeighbour) && !selected.includes(topNeighbour) && !selected.includes(topLeftNeighbour)) + ) { + background.appendLinearRing(clonedGeomTopLeft.getLinearRing(0)); + } + // top-right corner + if ((entry.index[0] <= 0 && entry.index[1] >= 0) + || (entry.index[0] <= 0 && !selected.includes(rightNeighbour)) + || (entry.index[1] >= 0 && !selected.includes(topNeighbour) && !selected.includes(topRightNeighbour)) + || (!selected.includes(rightNeighbour) && !selected.includes(topNeighbour) && !selected.includes(topRightNeighbour)) + ) { + background.appendLinearRing(clonedGeomTopRight.getLinearRing(0)); + } + // bottom-left corner + if ((entry.index[0] >= 0 && entry.index[1] <= 0) + || (entry.index[0] >= 0 && !selected.includes(leftNeighbour)) + || (entry.index[1] <= 0 && !selected.includes(bottomNeighbour) && !selected.includes(bottomLeftNeighbour)) + || (!selected.includes(leftNeighbour) && !selected.includes(bottomNeighbour) && !selected.includes(bottomLeftNeighbour)) + ) { + background.appendLinearRing(clonedGeomBottomLeft.getLinearRing(0)); + } + // bottom-right corner + if ((entry.index[0] >= 0 && entry.index[1] >= 0) + || (entry.index[0] >= 0 && !selected.includes(rightNeighbour)) + || (entry.index[1] >= 0 && !selected.includes(bottomNeighbour) && !selected.includes(bottomRightNeighbour)) + || (!selected.includes(rightNeighbour) && !selected.includes(bottomNeighbour) && !selected.includes(bottomRightNeighbour)) + ) { + background.appendLinearRing(clonedGeomBottomRight.getLinearRing(0)); + } + }); + } + + return background; + }; + calculateSeriesGeometries = () => { + const featureGeometry = this.getGeometry(); + const coordinates = featureGeometry.getCoordinates()[0]; + const dx1 = coordinates[1][0] - coordinates[2][0]; + const dy1 = coordinates[1][1] - coordinates[2][1]; + const dx2 = coordinates[2][0] - coordinates[3][0]; + const dy2 = coordinates[2][1] - coordinates[3][1]; + const rotation = -Math.atan2(dy1, dx1); + const gridSize = this.props.printSeriesEnabled ? this.props.printSeriesGridSize : 0; + + const geometries = []; + for (let i = -gridSize; i <= gridSize; i++) { + for (let j = -gridSize; j <= gridSize; j++) { + const index = [i, j]; + const geometry = featureGeometry.clone(); + geometry.translate( + (1 - this.props.printSeriesOverlap) * (j * dx1 + i * dx2), + (1 - this.props.printSeriesOverlap) * (j * dy1 + i * dy2) + ); + geometries.push({ index, geometry, rotation }); + } + } + + return geometries; + }; + layerStyle = (feature) => { + // background geometry with own styling + if (feature === this.backgroundFeature) { + return FeatureStyles.printInteractionBackground({ + geometryFunction: this.getBackgroundGeometry + }); + } + + // draw series geometries with own styling + if (feature === this.printSeriesFeature && this.props.printSeriesEnabled) { + const styles = []; + const size = Math.min(this.props.fixedFrame.width, this.props.fixedFrame.height); + const radius = Math.min(this.props.scale * size / this.map.getView().getResolution() / 100_000, 2); + this.seriesGeometries.forEach((entry) => { + // ignore the center geometry + if (!isEqual(entry.index, [0, 0])) { + styles.push(FeatureStyles.printInteractionSeries({ + geometryFunction: entry.geometry + })); + styles.push(...FeatureStyles.printInteractionSeriesIcon({ + geometryFunction: new ol.geom.Point(ol.extent.getCenter(entry.geometry.getExtent())), + rotation: entry.rotation, + radius: radius, + img: this.isPrintSeriesSelected(entry) ? 'minus' : 'plus' + })); + } + }); + return styles; + } + + // main feature + if (feature === this.feature) { + const styles = [ + FeatureStyles.printInteraction({ + geometryFunction: this.getGeometry + }) + ]; + const coordinates = this.getGeometry().getCoordinates()[0]; + if (coordinates && this.props.fixedFrame) { + if (this.props.allowScaling) { + // vertices to scale the selection + styles.push(FeatureStyles.printInteractionVertex({ + geometryFunction: new ol.geom.MultiPoint(coordinates.slice(2)) + })); + } + if (this.props.allowScaling || this.props.allowRotation) { + // vertices to scale or rotate the selection + styles.push(FeatureStyles.printInteractionVertex({ + geometryFunction: new ol.geom.MultiPoint(coordinates.slice(1, 2)), + fill: this.props.allowRotation + })); + } + } + return styles; + } + + return null; + }; + scaleRotateStyle = (feature) => { + feature.get('features').forEach((modifyFeature) => { + const modifyGeometry = modifyFeature.get('modifyGeometry'); + if (modifyGeometry) { + const point = feature.getGeometry().getCoordinates(); + // rotate only with vertex on bottom-right + const isRotationVertex = isEqual(point, modifyFeature.getGeometry().getCoordinates()[0][1]); + + if (!modifyGeometry.point) { + // save the initial geometry and vertex position + modifyGeometry.point = point; + modifyGeometry.initialGeometry = modifyGeometry.geometry; + } + + const center = ol.extent.getCenter(modifyGeometry.initialGeometry.getExtent()); + const [rotation, scale] = this.calculateRotationScale(modifyGeometry.point, point, center); + + const geometry = modifyGeometry.initialGeometry.clone(); + if (this.props.allowRotation && isRotationVertex) { + geometry.rotate(rotation, center); + } else if (this.props.allowScaling) { + geometry.scale(scale, undefined, center); + } + modifyGeometry.geometry = geometry; + } + }); + + return null; + }; + isPrintSeriesSelected = (entry) => { + return this.props.printSeriesSelected.includes(entry.index.join(',')); + }; + calculateExtents = () => { + this.seriesGeometries = this.calculateSeriesGeometries(); + this.selectionLayer.changed(); + + return this.seriesGeometries + .filter(entry => isEqual(entry.index, [0, 0]) || this.isPrintSeriesSelected(entry)) + .map(entry => { + const clonedGeom = entry.geometry.clone(); + const center = ol.extent.getCenter(clonedGeom.getExtent()); + clonedGeom.rotate(- this.props.rotation * Math.PI / 180, center); + return clonedGeom.getExtent(); + }); + }; + geometryChanged = () => { + const geometry = this.getGeometry(); + const extent = geometry.getExtent(); + const point = geometry.getCoordinates()[0][0]; + const center = ol.extent.getCenter(extent); + + // Update series geometries and obtain extents + const extents = this.calculateExtents(); + + let rotation = 0; + let scale = 0; + + if (this.initialWidth !== null && this.initialHeight !== null) { + const initialPoint = [ + center[0] + 0.5 * this.initialWidth, + center[1] + 0.5 * this.initialHeight + ]; + + const [calcRotation, calcScale] = this.calculateRotationScale(initialPoint, point, center); + + const degree = (360 + (calcRotation * 180) / Math.PI) % 360; + + rotation = Math.round(degree * 10) / 10 % 360; + scale = Math.round(1000 * calcScale); + } + + this.props.geometryChanged(center, extents, rotation, scale); + }; + calculateRotationScale = (p1, p2, center) => { + let dx = p1[0] - center[0]; + let dy = p1[1] - center[1]; + const initialAngle = Math.atan2(dy, dx); + const initialRadius = Math.sqrt(dx * dx + dy * dy); + + dx = p2[0] - center[0]; + dy = p2[1] - center[1]; + const currentAngle = Math.atan2(dy, dx); + const currentRadius = Math.sqrt(dx * dx + dy * dy); + + return [currentAngle - initialAngle, currentRadius / initialRadius]; + }; + render() { + return null; + } +} diff --git a/components/style/PrintFrame.css b/components/style/PrintFrame.css deleted file mode 100644 index f9f291d0c..000000000 --- a/components/style/PrintFrame.css +++ /dev/null @@ -1,24 +0,0 @@ -#PrintFrame { - position: absolute; - box-shadow: rgba(0, 0, 0, 0.5) 0 0 0 30000px; - z-index: 2; - pointer-events: none; - text-align: center; - font-size: small; - overflow: hidden; -} - -#PrintFrame span.size-box { - background: rgba(255, 255, 255, 0.5); - padding: 0.25em; - white-space: nowrap; -} - -#PrintFrameEventLayer { - z-index: 3; - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; -} diff --git a/doc/plugins.md b/doc/plugins.md index e969f48b8..b28881253 100644 --- a/doc/plugins.md +++ b/doc/plugins.md @@ -9,7 +9,6 @@ Plugin reference * [BottomBar](#bottombar) * [CookiePopup](#cookiepopup) * [Cyclomedia](#cyclomedia) -* [DxfExport](#dxfexport) * [Editing](#editing) * [FeatureForm](#featureform) * [FeatureSearch](#featuresearch) @@ -35,7 +34,6 @@ Plugin reference * [Portal](#portal) * [Print](#print) * [ProcessNotifications](#processnotifications) -* [RasterExport](#rasterexport) * [Redlining](#redlining) * [Reports](#reports) * [Routing](#routing) @@ -263,20 +261,6 @@ Cyclomedia integration for QWC2. | maxMapScale | `number` | The maximum map scale above which the recordings WFS won't be displayed. | `5000` | | projection | `string` | The projection to use for Cyclomedia. | `'EPSG:3857'` | -DxfExport ----------------------------------------------------------------- -Allows exporting a selected extent of the map as DXF. - -Uses the DXF format support of QGIS Server. - -Deprecated. Use the MapExport plugin instead. - -| Property | Type | Description | Default value | -|----------|------|-------------|---------------| -| formatOptions | `string` | Optional format options to pass to QGIS Server via FORMAT_OPTIONS. | `undefined` | -| layerOptions | `[{`
`  label: string,`
`  layers: string,`
`}]` | Optional choice of layer sets to pass to QGIS Server via LAYERS. | `undefined` | -| serviceUrl | `string` | Optional URL invoked on export instead of the default QGIS Server URL. | `undefined` | - Editing ---------------------------------------------------------------- Allows editing geometries and attributes of datasets. @@ -532,6 +516,7 @@ Allows exporting a selected portion of the map to a variety of formats. | defaultScaleFactor | `number` | The factor to apply to the map scale to determine the initial export map scale (if `allowedScales` is not `false`). | `1` | | dpis | `[number]` | List of dpis at which to export the map. If empty, the default server dpi is used. | `undefined` | | exportExternalLayers | `bool` | Whether to include external layers in the image. Requires QGIS Server 3.x! | `true` | +| fileNameTemplate | `string` | Template for the name of the generated files when downloading. | `'{theme}_{timestamp}'` | | formatConfiguration | `{`
`  format: [{`
`  name: string,`
`  extraQuery: string,`
`  formatOptions: string,`
`  baseLayer: string,`
`}],`
`}` | Custom export configuration per format.
If more than one configuration per format is provided, a selection combo will be displayed.
`extraQuery` will be appended to the query string (replacing any existing parameters).
`formatOptions` will be passed as FORMAT_OPTIONS.
`baseLayer` will be appended to the LAYERS instead of the background layer. | `undefined` | | pageSizes | `[{`
`  name: string,`
`  width: number,`
`  height: number,`
`}]` | List of image sizes to offer, in addition to the free-hand selection. The width and height are in millimeters. | `[]` | | side | `string` | The side of the application on which to display the sidebar. | `'right'` | @@ -663,7 +648,9 @@ Uses the print layouts defined in the QGIS project. | allowGeoPdfExport | `bool` | Whether to allow GeoPDF export. Requires QGIS Server 3.32 or newer. | `undefined` | | defaultDpi | `number` | The default print dpi. | `300` | | defaultScaleFactor | `number` | The factor to apply to the map scale to determine the initial print map scale. | `0.5` | +| displayPrintSeries | `bool` | Show an option to print a series of extents. | `false` | | displayRotation | `bool` | Whether to display the map rotation control. | `true` | +| fileNameTemplate | `string` | Template for the name of the generated files when downloading. | `'{theme}_{timestamp}'` | | formats | `[string]` | Export layout format mimetypes. If empty, supported formats are listed. If format is not supported by QGIS Server, print will fail | `undefined` | | gridInitiallyEnabled | `bool` | Whether the grid is enabled by default. | `false` | | hideAutopopulatedFields | `bool` | Whether to hide form fields which contain autopopulated values (i.e. search result label). | `undefined` | @@ -678,23 +665,6 @@ Adds support for displaying notifications of background processes. Only useful for third-party plugins which use this functionality. -RasterExport ----------------------------------------------------------------- -Allows exporting a selected portion of the map to an image ("screenshot"). - -Deprecated. Use the MapExport plugin instead. - -| Property | Type | Description | Default value | -|----------|------|-------------|---------------| -| allowedFormats | `[string]` | Whitelist of allowed export format mimetypes. If empty, supported formats are listed. | `undefined` | -| allowedScales | `[number]` | List of scales at which to export the map. | `undefined` | -| defaultFormat | `string` | Default export format mimetype. If empty, first available format is used. | `undefined` | -| defaultScaleFactor | `number` | The factor to apply to the map scale to determine the initial export map scale. | `0.5` | -| dpis | `[number]` | List of dpis at which to export the map. If empty, the default server dpi is used. | `undefined` | -| exportExternalLayers | `bool` | Whether to include external layers in the image. Requires QGIS Server 3.x! | `true` | -| pageSizes | `[{`
`  name: string,`
`  width: number,`
`  height: number,`
`}]` | List of image sizes to offer, in addition to the free-hand selection. The width and height are in millimeters. | `[`
`  {name: '15 x 15 cm', width: 150, height: 150},`
`  {name: '30 x 30 cm', width: 300, height: 300}`
`]` | -| side | `string` | The side of the application on which to display the sidebar. | `'right'` | - Redlining ---------------------------------------------------------------- Allows drawing figures and text labels on the map. diff --git a/libs/openlayers.js b/libs/openlayers.js index 4e4ac6c7b..997abc501 100644 --- a/libs/openlayers.js +++ b/libs/openlayers.js @@ -41,7 +41,7 @@ import OlGeomMultiPoint from 'ol/geom/MultiPoint'; import OlGeomMultiPolygon from 'ol/geom/MultiPolygon'; import OlGeomPoint from 'ol/geom/Point'; import OlGeomPolygon from 'ol/geom/Polygon'; -import {fromCircle as olPolygonFromCircle} from 'ol/geom/Polygon'; +import {fromCircle as olPolygonFromCircle, fromExtent as olPolygonFromExtent} from 'ol/geom/Polygon'; import {defaults as olInteractionDefaults} from 'ol/interaction'; import OlInteractionDoubleClickZoom from 'ol/interaction/DoubleClickZoom'; import OlInteractionDragBox from 'ol/interaction/DragBox'; @@ -127,7 +127,8 @@ export default { MultiPolygon: OlGeomMultiPolygon, Point: OlGeomPoint, Polygon: OlGeomPolygon, - polygonFromCircle: olPolygonFromCircle + polygonFromCircle: olPolygonFromCircle, + polygonFromExtent: olPolygonFromExtent }, Graticule: OlGraticule, interaction: { diff --git a/package.json b/package.json index 320d75df8..cbb05718e 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "painterro": "^1.2.87", "path-browserify": "^1.0.1", "pdfjs-dist": "^4.6.82", + "pdf-lib": "^1.17.1", "point-in-polygon": "^1.1.0", "polygon-intersect-test": "^1.0.1", "proj4": "^2.11.0", diff --git a/plugins/API.jsx b/plugins/API.jsx index 24a944d4c..7a9eac308 100644 --- a/plugins/API.jsx +++ b/plugins/API.jsx @@ -42,7 +42,7 @@ import MessageBar from '../components/MessageBar'; import NumericInputWindow from '../components/NumericInputWindow'; import PickFeature from '../components/PickFeature'; import PluginsContainer from '../components/PluginsContainer'; -import PrintFrame from '../components/PrintFrame'; +import PrintSelection from '../components/PrintSelection'; import QtDesignerForm from '../components/QtDesignerForm'; import ResizeableWindow from '../components/ResizeableWindow'; import Search from '../components/Search'; @@ -193,7 +193,7 @@ class API extends React.Component { window.qwc2.components.PickFeature = PickFeature; window.qwc2.components.PluginsContainer = PluginsContainer; window.qwc2.components.PopupMenu = PopupMenu; - window.qwc2.components.PrintFrame = PrintFrame; + window.qwc2.components.PrintSelection = PrintSelection; window.qwc2.components.QtDesignerForm = QtDesignerForm; window.qwc2.components.ResizeableWindow = ResizeableWindow; window.qwc2.components.SearchBox = SearchBox; diff --git a/plugins/DxfExport.jsx b/plugins/DxfExport.jsx deleted file mode 100644 index da261bdbf..000000000 --- a/plugins/DxfExport.jsx +++ /dev/null @@ -1,194 +0,0 @@ -/** - * Copyright 2016-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 {LayerRole} from '../actions/layers'; -import {setCurrentTask} from '../actions/task'; -import PrintFrame from '../components/PrintFrame'; -import TaskBar from '../components/TaskBar'; -import InputContainer from '../components/widgets/InputContainer'; -import CoordinatesUtils from '../utils/CoordinatesUtils'; -import LocaleUtils from '../utils/LocaleUtils'; -import MiscUtils from '../utils/MiscUtils'; - -import './style/DxfExport.css'; - - -/** - * Allows exporting a selected extent of the map as DXF. - * - * Uses the DXF format support of QGIS Server. - * - * Deprecated. Use the MapExport plugin instead. - */ -class DxfExport extends React.Component { - static propTypes = { - /** Optional format options to pass to QGIS Server via FORMAT_OPTIONS. */ - formatOptions: PropTypes.string, - /** Optional choice of layer sets to pass to QGIS Server via LAYERS. */ - layerOptions: PropTypes.arrayOf(PropTypes.shape({ - label: PropTypes.string, - layers: PropTypes.string - })), - layers: PropTypes.array, - map: PropTypes.object, - /** Optional URL invoked on export instead of the default QGIS Server URL. */ - serviceUrl: PropTypes.string, - setCurrentTask: PropTypes.func, - theme: PropTypes.object - }; - state = { - selectedLayers: "" - }; - constructor(props) { - super(props); - this.state.selectedLayers = !isEmpty(props.layerOptions) ? props.layerOptions[0].layers : ""; - - /* eslint-disable-next-line */ - console.warn("The DxfExport plugin is deprecated. Use the MapExport plugin instead."); - } - renderBody = () => { - const themeLayers = this.props.layers.filter(layer => layer.role === LayerRole.THEME); - if (!this.props.theme || isEmpty(themeLayers)) { - return null; - } - const themeSubLayers = themeLayers.map(layer => layer.params.LAYERS).reverse().join(","); - const action = this.props.serviceUrl || this.props.theme.url; - const formatOptions = this.props.formatOptions - ? - : null; - const basename = this.props.serviceUrl ? this.props.serviceUrl.replace(/\/$/, '').replace(/^.*\//, '') : this.props.theme.name.split("/").pop(); - - const dimensionValues = this.props.layers.reduce((res, layer) => { - if (layer.role === LayerRole.THEME) { - Object.entries(layer.dimensionValues || {}).forEach(([key, value]) => { - if (value !== undefined) { - res[key] = value; - } - }); - } - return res; - }, {}); - const extraOptions = Object.fromEntries((this.props.theme.extraDxfParameters || "").split("&").map(entry => entry.split("="))); - const paramValue = (key, deflt) => { - if (key in extraOptions) { - const value = extraOptions[key]; - delete extraOptions[key]; - return value; - } - return deflt; - }; - - return ( - -
{ this.form = form; }}> -
{LocaleUtils.tr("dxfexport.selectinfo")}
-
- - {LocaleUtils.tr("dxfexport.symbologyscale")}:  - -  1 :  - - - - {!isEmpty(this.props.layerOptions) ? ( - - {LocaleUtils.tr("dxfexport.layers")}:  - - - ) : ( - - )} -
- - - - - - - { this.extentInput = input; }} type="hidden" value="" /> - {Object.entries(dimensionValues).map(([key, value]) => ( - - ))} - - {Object.entries(extraOptions).map(([key, value]) => ())} - {formatOptions} -
-
- ); - }; - render() { - return ( - - {() => ({ - body: this.renderBody(), - extra: () - })} - - ); - } - export = (ev = null) => { - if (ev) { - ev.preventDefault(); - } - const formData = formDataEntries(new FormData(this.form)); - 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 action = this.props.serviceUrl || this.props.theme.url; - axios.post(action, data, config).then(response => { - const contentType = response.headers["content-type"]; - FileSaver.saveAs(new Blob([response.data], {type: contentType}), this.props.theme.name + '.dxf'); - }).catch(e => { - if (e.response) { - /* eslint-disable-next-line */ - console.log(new TextDecoder().decode(e.response.data)); - } - /* eslint-disable-next-line */ - alert('Export failed'); - }); - }; - bboxSelected = (bbox, crs) => { - if (!bbox) { - return; - } - const version = this.props.theme.version; - const extent = (CoordinatesUtils.getAxisOrder(crs).substr(0, 2) === 'ne' && version === '1.3.0') ? - bbox[1] + "," + bbox[0] + "," + bbox[3] + "," + bbox[2] : - bbox.join(','); - this.extentInput.value = extent; - this.export(); - this.props.setCurrentTask(null); - }; -} - -const selector = (state) => ({ - theme: state.theme.current, - map: state.map, - layers: state.layers.flat -}); - -export default connect(selector, { - setCurrentTask: setCurrentTask -})(DxfExport); diff --git a/plugins/MapExport.jsx b/plugins/MapExport.jsx index 6c76272b8..a05adeedc 100644 --- a/plugins/MapExport.jsx +++ b/plugins/MapExport.jsx @@ -10,15 +10,17 @@ import React from 'react'; import {connect} from 'react-redux'; import axios from 'axios'; +import dayjs from 'dayjs'; import FileSaver from 'file-saver'; import formDataEntries from 'formdata-json'; import isEmpty from 'lodash.isempty'; import PropTypes from 'prop-types'; +import {setIdentifyEnabled} from '../actions/identify'; import {LayerRole} from '../actions/layers'; -import {setCurrentTask} from '../actions/task'; +import {setSnappingConfig} from '../actions/map'; import Icon from '../components/Icon'; -import PrintFrame from '../components/PrintFrame'; +import PrintSelection from '../components/PrintSelection'; import SideBar from '../components/SideBar'; import InputContainer from '../components/widgets/InputContainer'; import Spinner from '../components/widgets/Spinner'; @@ -50,6 +52,8 @@ class MapExport extends React.Component { dpis: PropTypes.arrayOf(PropTypes.number), /** Whether to include external layers in the image. Requires QGIS Server 3.x! */ exportExternalLayers: PropTypes.bool, + /** Template for the name of the generated files when downloading. */ + fileNameTemplate: PropTypes.string, /** Custom export configuration per format. * If more than one configuration per format is provided, a selection combo will be displayed. * `extraQuery` will be appended to the query string (replacing any existing parameters). @@ -71,7 +75,8 @@ class MapExport extends React.Component { width: PropTypes.number, height: PropTypes.number })), - setCurrentTask: PropTypes.func, + setIdentifyEnabled: PropTypes.func, + setSnappingConfig: PropTypes.func, /** The side of the application on which to display the sidebar. */ side: PropTypes.string, theme: PropTypes.object @@ -79,6 +84,7 @@ class MapExport extends React.Component { static defaultProps = { defaultScaleFactor: 1, exportExternalLayers: true, + fileNameTemplate: '{theme}_{timestamp}', side: 'right', pageSizes: [] }; @@ -88,50 +94,21 @@ class MapExport extends React.Component { this.state.dpi = (props.dpis || [])[0] || 96; } state = { - extent: '', - width: 0, - height: 0, + extents: [], exporting: false, availableFormats: [], selectedFormat: null, selectedFormatConfiguration: '', - scale: '', + scale: null, pageSize: null, dpi: 96 }; componentDidUpdate(prevProps, prevState) { - if ( - this.props.map.center !== prevProps.map.center || - this.props.map.bbox !== prevProps.map.bbox || - this.state.pageSize !== prevState.pageSize || - this.state.scale !== prevState.scale || - this.state.dpi !== prevState.dpi - ) { - if (this.state.pageSize !== null) { - this.setState((state) => { - const scale = this.getExportScale(state); - const center = this.props.map.center; - const mapCrs = this.props.map.projection; - const pageSize = this.props.pageSizes[state.pageSize]; - const widthm = scale * pageSize.width / 1000; - const heightm = scale * pageSize.height / 1000; - const {width, height} = MapUtils.transformExtent(mapCrs, center, widthm, heightm); - let extent = [center[0] - 0.5 * width, center[1] - 0.5 * height, center[0] + 0.5 * width, center[1] + 0.5 * height]; - extent = (CoordinatesUtils.getAxisOrder(mapCrs).substr(0, 2) === 'ne' && this.props.theme.version === '1.3.0') ? - extent[1] + "," + extent[0] + "," + extent[3] + "," + extent[2] : - extent.join(','); - return { - width: Math.round(pageSize.width / 1000 * 39.3701 * state.dpi), - height: Math.round(pageSize.height / 1000 * 39.3701 * state.dpi), - extent: extent - }; - }); - } else if (prevState.pageSize !== null) { - this.setState({width: '', height: '', extent: ''}); - } + if (this.state.pageSize === null && prevState.pageSize !== null) { + this.setState({extents: []}); } } - formatChanged = (ev) => { + changeFormat = (ev) => { const selectedFormat = ev.target.value; const selectedFormatConfiguration = ((this.props.formatConfiguration?.[selectedFormat] || [])[0] || {}).name; this.setState({ @@ -139,8 +116,11 @@ class MapExport extends React.Component { selectedFormatConfiguration: selectedFormatConfiguration }); }; - dpiChanged = (ev) => { - this.setState({dpi: parseInt(ev.target.value, 10)}); + changeScale = (ev) => { + this.setState({scale: parseInt(ev.target.value, 10) || 0}); + }; + changeResolution = (ev) => { + this.setState({dpi: parseInt(ev.target.value, 10) || 0}); }; renderBody = () => { if (!this.props.theme || !this.state.selectedFormat) { @@ -162,27 +142,35 @@ class MapExport extends React.Component { let scaleChooser = null; if (!isEmpty(this.props.allowedScales)) { scaleChooser = ( - + {this.props.allowedScales.map(scale => ())} ); } else if (this.props.allowedScales !== false) { scaleChooser = ( - this.setState({scale: ev.target.value})} role="input" type="number" value={this.state.scale} /> + ); } - const filename = this.props.theme.id.split("/").pop() + "." + this.state.selectedFormat.split(";")[0].split("/").pop(); const action = this.props.theme.url; const exportExternalLayers = this.state.selectedFormat !== "application/dxf" && this.props.exportExternalLayers && ConfigUtils.getConfigProp("qgisServerVersion") >= 3; - const mapScale = MapUtils.computeForZoom(this.props.map.scales, this.props.map.zoom); - let scaleFactor = 1; - if (this.state.pageSize === null && this.props.allowedScales !== false) { - scaleFactor = mapScale / this.state.scale; - } const selectedFormatConfiguration = formatConfiguration.find(entry => entry.name === this.state.selectedFormatConfiguration) || {}; const exportParams = LayerUtils.collectPrintParams(this.props.layers, this.props.theme, this.state.scale, this.props.map.projection, exportExternalLayers, !!selectedFormatConfiguration.baseLayer); const highlightParams = VectorLayerUtils.createPrintHighlighParams(this.props.layers, this.props.map.projection, this.state.scale, this.state.dpi); + const version = this.props.theme.version; + const crs = this.props.map.projection; + const extent = this.state.extents.at(0) ?? [0, 0, 0, 0]; + const formattedExtent = (CoordinatesUtils.getAxisOrder(crs).substring(0, 2) === 'ne' && version === '1.3.0') ? + extent[1] + "," + extent[0] + "," + extent[3] + "," + extent[2] : + extent.join(','); + + const getPixelFromCoordinate = MapUtils.getHook(MapUtils.GET_PIXEL_FROM_COORDINATES_HOOK); + const p1 = getPixelFromCoordinate(extent.slice(0, 2)); + const p2 = getPixelFromCoordinate(extent.slice(2, 4)); + const width = Math.abs(p1[0] - p2[0]) * this.state.dpi / 96; + const height = Math.abs(p1[1] - p2[1]) * this.state.dpi / 96; + return (
{ this.form = el; }}> @@ -191,7 +179,7 @@ class MapExport extends React.Component { {LocaleUtils.tr("mapexport.format")} - {this.state.availableFormats.map(format => { return (); })} @@ -214,7 +202,7 @@ class MapExport extends React.Component { {LocaleUtils.tr("mapexport.size")} - this.setState({pageSize: ev.target.value || null})} value={this.state.pageSize || ""}> {this.props.pageSizes.map((entry, idx) => ( @@ -223,7 +211,7 @@ class MapExport extends React.Component { ) : null} - {scaleChooser ? ( + {scaleChooser && this.state.pageSize !== null ? ( {LocaleUtils.tr("mapexport.scale")} @@ -238,7 +226,7 @@ class MapExport extends React.Component { {LocaleUtils.tr("mapexport.resolution")} - {this.props.dpis.map(dpi => { return (); })} @@ -255,10 +243,9 @@ class MapExport extends React.Component { - - - - + + + {Object.keys(this.props.theme.watermark || {}).map(key => { return (); })} @@ -272,7 +259,7 @@ class MapExport extends React.Component {
-
); }; - renderFrame = () => { + renderPrintSelection = () => { if (this.state.pageSize !== null) { - const px2m = 1 / (this.state.dpi * 39.3701) * this.getExportScale(this.state); + const pageSize = this.props.pageSizes[this.state.pageSize]; const frame = { - width: this.state.width * px2m, - height: this.state.height * px2m + width: pageSize.width, + height: pageSize.height }; - return (); + return (); } else { - return (); + return (); } }; render() { @@ -304,7 +293,7 @@ class MapExport extends React.Component { {() => ({ body: this.state.minimized ? null : this.renderBody(), extra: [ - this.renderFrame() + this.renderPrintSelection() ] })} @@ -336,33 +325,23 @@ class MapExport extends React.Component { selectedFormat: selectedFormat, selectedFormatConfiguration: selectedFormatConfiguration }); + this.props.setIdentifyEnabled(false); + this.props.setSnappingConfig(false, false); }; onHide = () => { this.setState({ - extent: '', - width: '', - height: '' + extents: [], + width: 0, + height: 0, + scale: null, + pageSize: null }); + this.props.setIdentifyEnabled(true); }; - getExportScale = (state) => { - if (this.props.allowedScales === false) { - return Math.round(MapUtils.computeForZoom(this.props.map.scales, this.props.map.zoom)); - } else { - return state.scale; - } - }; - bboxSelected = (bbox, crs, pixelsize) => { - const version = this.props.theme.version; - let extent = ''; - if (bbox) { - extent = (CoordinatesUtils.getAxisOrder(crs).substr(0, 2) === 'ne' && version === '1.3.0') ? - bbox[1] + "," + bbox[0] + "," + bbox[3] + "," + bbox[2] : - bbox.join(','); - } + geometryChanged = (center, extents, rotation, scale) => { this.setState({ - extent: extent, - width: pixelsize[0], - height: pixelsize[1] + extents: extents, + scale: scale }); }; export = (ev) => { @@ -385,6 +364,14 @@ class MapExport extends React.Component { const format = this.state.selectedFormat.split(";")[0]; const formatConfiguration = (this.props.formatConfiguration?.[format] || []).find(entry => entry.name === this.state.selectedFormatConfiguration); + const ext = format.split("/").pop(); + const timestamp = dayjs(new Date()).format("YYYYMMDD_HHmmss"); + const fileName = this.props.fileNameTemplate + .replace("{theme}", this.props.theme.id) + .replace("{timestamp}", timestamp) + "." + ext; + + params.filename = fileName; + if (formatConfiguration) { const keyCaseMap = Object.keys(params).reduce((res, key) => ({...res, [key.toLowerCase()]: key}), {}); (formatConfiguration.extraQuery || "").split(/[?&]/).filter(Boolean).forEach(entry => { @@ -411,8 +398,8 @@ class MapExport extends React.Component { axios.post(this.props.theme.url, data, config).then(response => { this.setState({exporting: false}); const contentType = response.headers["content-type"]; - const ext = this.state.selectedFormat.split(";")[0].split("/").pop(); - FileSaver.saveAs(new Blob([response.data], {type: contentType}), this.props.theme.id + '.' + ext); + + FileSaver.saveAs(new Blob([response.data], {type: contentType}), fileName); }).catch(e => { this.setState({exporting: false}); if (e.response) { @@ -432,5 +419,6 @@ const selector = (state) => ({ }); export default connect(selector, { - setCurrentTask: setCurrentTask + setIdentifyEnabled: setIdentifyEnabled, + setSnappingConfig: setSnappingConfig })(MapExport); diff --git a/plugins/Print.jsx b/plugins/Print.jsx index 9c492cc64..44be72520 100644 --- a/plugins/Print.jsx +++ b/plugins/Print.jsx @@ -10,16 +10,20 @@ import React from 'react'; import {connect} from 'react-redux'; import axios from 'axios'; +import dayjs from 'dayjs'; import FileSaver from 'file-saver'; import formDataEntries from 'formdata-json'; +import JSZip from 'jszip'; import isEmpty from 'lodash.isempty'; +import {PDFDocument} from 'pdf-lib'; import PropTypes from 'prop-types'; +import {setIdentifyEnabled} from '../actions/identify'; import {LayerRole, addLayerFeatures, clearLayer} from '../actions/layers'; -import {changeRotation, panTo} from '../actions/map'; +import {setSnappingConfig} from '../actions/map'; import Icon from '../components/Icon'; import PickFeature from '../components/PickFeature'; -import PrintFrame from '../components/PrintFrame'; +import PrintSelection from '../components/PrintSelection'; import ResizeableWindow from '../components/ResizeableWindow'; import SideBar from '../components/SideBar'; import InputContainer from '../components/widgets/InputContainer'; @@ -46,15 +50,18 @@ class Print extends React.Component { addLayerFeatures: PropTypes.func, /** Whether to allow GeoPDF export. Requires QGIS Server 3.32 or newer. */ allowGeoPdfExport: PropTypes.bool, - changeRotation: PropTypes.func, clearLayer: PropTypes.func, /** The default print dpi. */ defaultDpi: PropTypes.number, /** The factor to apply to the map scale to determine the initial print map scale. */ defaultScaleFactor: PropTypes.number, - /** Whether to display the map rotation control. */ + /** Show an option to print a series of extents. */ + displayPrintSeries: PropTypes.bool, + /** Whether to display the printing rotation control. */ displayRotation: PropTypes.bool, - /** Export layout format mimetypes. If empty, supported formats are listed. If format is not supported by QGIS Server, print will fail */ + /** Template for the name of the generated files when downloading. */ + fileNameTemplate: PropTypes.string, + /** Export layout format mimetypes. If format is not supported by QGIS Server, print will fail. */ formats: PropTypes.arrayOf(PropTypes.string), /** Whether the grid is enabled by default. */ gridInitiallyEnabled: PropTypes.bool, @@ -64,50 +71,59 @@ class Print extends React.Component { inlinePrintOutput: PropTypes.bool, layers: PropTypes.array, map: PropTypes.object, - panTo: PropTypes.func, /** Whether to print external layers. Requires QGIS Server 3.x! */ printExternalLayers: PropTypes.bool, /** Scale factor to apply to line widths, font sizes, ... of redlining drawings passed to GetPrint. */ scaleFactor: PropTypes.number, + setIdentifyEnabled: PropTypes.func, + setSnappingConfig: PropTypes.func, /** The side of the application on which to display the sidebar. */ side: PropTypes.string, theme: PropTypes.object }; static defaultProps = { - printExternalLayers: true, - inlinePrintOutput: false, - scaleFactor: 1.9, // Experimentally determined... defaultDpi: 300, defaultScaleFactor: 0.5, + displayPrintSeries: false, displayRotation: true, + fileNameTemplate: '{theme}_{timestamp}', gridInitiallyEnabled: false, + formats: ['application/pdf', 'image/jpeg', 'image/png', 'image/svg'], + inlinePrintOutput: false, + printExternalLayers: true, + scaleFactor: 1.9, // Experimentally determined... side: 'right' }; state = { + center: null, + extents: [], layout: null, - scale: null, + rotation: 0, + scale: 0, dpi: 300, - initialRotation: 0, grid: false, legend: false, - rotationNull: false, minimized: false, printOutputVisible: false, outputLoaded: false, printing: false, atlasFeatures: [], geoPdf: false, - availableFormats: [], selectedFormat: "", printOutputData: undefined, pdfData: null, - pdfDataUrl: null + pdfDataUrl: null, + downloadMode: "onepdf", + printSeriesEnabled: false, + printSeriesOverlap: 0, + printSeriesSelected: [] }; constructor(props) { super(props); this.printForm = null; this.state.grid = props.gridInitiallyEnabled; - this.fixedMapCenter = null; + this.state.dpi = props.defaultDpi; + this.state.selectedFormat = props.formats[0]; } componentDidUpdate(prevProps, prevState) { if (prevProps.theme !== this.props.theme) { @@ -120,7 +136,6 @@ class Print extends React.Component { } else { this.setState({layout: null, atlasFeatures: []}); } - this.fixedMapCenter = null; } if (this.state.atlasFeatures !== prevState.atlasFeatures) { if (!isEmpty(this.state.atlasFeatures)) { @@ -134,11 +149,12 @@ class Print extends React.Component { this.props.clearLayer("print-pick-selection"); } } + if (this.state.printSeriesEnabled && this.state.selectedFormat !== 'application/pdf') { + this.setState({ selectedFormat: 'application/pdf' }); + } } onShow = () => { - const defaultFormats = ['application/pdf', 'image/jpeg', 'image/png', 'image/svg']; - const availableFormats = !isEmpty(this.props.formats) ? this.props.formats : defaultFormats; - const selectedFormat = availableFormats[0]; + // setup initial extent let scale = Math.round(MapUtils.computeForZoom(this.props.map.scales, this.props.map.zoom) * this.props.defaultScaleFactor); if (this.props.theme.printScales && this.props.theme.printScales.length > 0) { let closestVal = Math.abs(scale - this.props.theme.printScales[0]); @@ -152,17 +168,23 @@ class Print extends React.Component { } scale = this.props.theme.printScales[closestIdx]; } - this.setState({ - scale: scale, - initialRotation: this.props.map.bbox.rotation, - dpi: this.props.defaultDpi, - availableFormats: availableFormats, - selectedFormat: selectedFormat - }); + const bounds = this.props.map.bbox.bounds; + const center = this.state.center || [0, 0]; + const resetCenter = (center[0] < bounds[0]) || (center[0] > bounds[2]) || (center[1] < bounds[1]) || (center[1] > bounds[3]); + const resetScale = (this.state.scale / scale < 0.01) || (this.state.scale / scale > 10); + if (resetCenter || resetScale) { + this.setState({ + center: null, + rotation: 0, + scale: scale + }); + } + this.props.setIdentifyEnabled(false); + this.props.setSnappingConfig(false, false); }; onHide = () => { - this.props.changeRotation(this.state.initialRotation); - this.setState({minimized: false, scale: null, atlasFeatures: []}); + this.setState({minimized: false, printSeriesEnabled: false, atlasFeatures: []}); + this.props.setIdentifyEnabled(true); }; renderBody = () => { if (!this.state.layout) { @@ -174,29 +196,23 @@ class Print extends React.Component { } const formvisibility = 'hidden'; - const printDpi = parseInt(this.state.dpi, 10); + const printDpi = parseInt(this.state.dpi, 10) || 0; const mapCrs = this.props.map.projection; const version = this.props.theme.version; const mapName = this.state.layout.map.name; const printParams = LayerUtils.collectPrintParams(this.props.layers, this.props.theme, this.state.scale, mapCrs, this.props.printExternalLayers); - let extent = this.computeCurrentExtent(); - extent = (CoordinatesUtils.getAxisOrder(mapCrs).substr(0, 2) === 'ne' && version === '1.3.0') ? - extent[1] + "," + extent[0] + "," + extent[3] + "," + extent[2] : - extent.join(','); + let formattedExtent = this.formatExtent(this.state.extents.at(0) ?? [0, 0, 0, 0]); if (!isEmpty(this.state.atlasFeatures)) { - extent = ""; - } - let rotation = ""; - if (!this.state.rotationNull) { - rotation = this.props.map.bbox ? Math.round((this.props.map.bbox.rotation / Math.PI * 180) * 10) / 10 : 0; + formattedExtent = ""; } let scaleChooser = (); if (this.props.theme.printScales && this.props.theme.printScales.length > 0) { scaleChooser = ( ); @@ -208,7 +224,8 @@ class Print extends React.Component { resolutionChooser = ( ); + + ); } else { resolutionInput = (); } @@ -216,17 +233,40 @@ class Print extends React.Component { resolutionChooser = (); } + let rotationInput = null; + if (this.props.displayRotation) { + rotationInput = (); + } + let gridIntervalX = null; let gridIntervalY = null; const printGrid = this.props.theme.printGrid; if (printGrid && printGrid.length > 0 && this.state.scale && this.state.grid) { let cur = 0; - for (; cur < printGrid.length - 1 && this.state.scale < printGrid[cur].s; ++cur); + while (cur < printGrid.length - 1 && this.state.scale < printGrid[cur].s) { + cur += 1; + } gridIntervalX = (); gridIntervalY = (); } const printLegend = this.state.layout.legendLayout; + let overlapChooser = null; + if (this.props.displayPrintSeries) { + overlapChooser = (); + } + + let downloadModeChooser = null; + if (this.props.displayPrintSeries && !this.props.inlinePrintOutput) { + downloadModeChooser = ( + + ); + } + const labels = this.state.layout && this.state.layout.labels ? this.state.layout.labels : []; const highlightParams = VectorLayerUtils.createPrintHighlighParams(this.props.layers, mapCrs, this.state.scale, printDpi, this.props.scaleFactor); @@ -253,7 +293,7 @@ class Print extends React.Component { "image/png": "PNG", "image/svg": "SVG" }; - const selectedFormat = this.state.selectedFormat; + const pdfFormatSelected = this.state.selectedFormat === "application/pdf"; return (
@@ -274,12 +314,12 @@ class Print extends React.Component { - {this.state.availableFormats.length > 1 ? ( + {this.props.formats.length > 1 ? ( {LocaleUtils.tr("print.format")} - + {this.props.formats.map(format => { return (); })} @@ -328,11 +368,16 @@ class Print extends React.Component { ) : null} - {this.props.displayRotation === true ? ( + {rotationInput ? ( {LocaleUtils.tr("print.rotation")} - + + {rotationInput} + + this.setState({rotation: 0})} title={LocaleUtils.tr("map.resetrotation")} /> + + ) : null} @@ -352,6 +397,33 @@ class Print extends React.Component { ) : null} + {this.props.displayPrintSeries ? ( + + {LocaleUtils.tr("print.series")} + + this.setState({printSeriesEnabled: newstate})} /> + + + ) : null} + {overlapChooser ? ( + + {LocaleUtils.tr("print.overlap")} + + + {overlapChooser} + {this.state.printSeriesOverlap} % + + + + ) : null} + {downloadModeChooser ? ( + + {LocaleUtils.tr("print.download")} + + {downloadModeChooser} + + + ) : null} {(labels || []).map(label => { // Omit labels which start with __ if (label.startsWith("__")) { @@ -364,7 +436,7 @@ class Print extends React.Component { }; return this.renderPrintLabelField(label, opts); })} - {selectedFormat === "application/pdf" && this.props.allowGeoPdfExport ? ( + {pdfFormatSelected && this.props.allowGeoPdfExport ? ( GeoPDF @@ -375,11 +447,11 @@ class Print extends React.Component {
- + - + {Object.entries(printParams).map(([key, value]) => ())} @@ -396,7 +468,7 @@ class Print extends React.Component { - {selectedFormat === "application/pdf" && this.props.allowGeoPdfExport ? () : null} + {pdfFormatSelected && this.props.allowGeoPdfExport ? () : null} {gridIntervalX} {gridIntervalY} {resolutionInput} @@ -443,7 +515,7 @@ class Print extends React.Component { if (opts.rows || opts.cols) { style.resize = 'none'; } - if (opts.rows) { + if (opts.cols) { style.width = 'initial'; } return ( @@ -473,16 +545,52 @@ class Print extends React.Component { } }).join(" | "); }; - renderPrintFrame = () => { - let printFrame = null; + renderPrintSelection = () => { + let printSelection = null; if (this.state.layout && isEmpty(this.state.atlasFeatures)) { const frame = { - width: this.state.scale * this.state.layout.map.width / 1000, - height: this.state.scale * this.state.layout.map.height / 1000 + width: this.state.layout.map.width, + height: this.state.layout.map.height }; - printFrame = (); + printSelection = (); } - return printFrame; + return printSelection; + }; + formatExtent = (extent) => { + const mapCrs = this.props.map.projection; + const version = this.props.theme.version; + + if (CoordinatesUtils.getAxisOrder(mapCrs).substring(0, 2) === 'ne' && version === '1.3.0') { + return extent[1] + "," + extent[0] + "," + extent[3] + "," + extent[2]; + } + + return extent.join(','); + }; + geometryChanged = (center, extents, rotation, scale) => { + this.setState({ + center: center, + extents: extents, + rotation: rotation, + scale: scale + }); + }; + printSeriesChanged = (selected) => { + this.setState({ + printSeriesSelected: selected + }); }; renderPrintOutputWindow = () => { const extraControls = [{ @@ -508,7 +616,7 @@ class Print extends React.Component { ); }; savePrintOutput = () => { - FileSaver.saveAs(this.state.pdfData, this.props.theme.id + '.pdf'); + FileSaver.saveAs(this.state.pdfData.content, this.state.pdfData.fileName); }; render() { const minMaxTooltip = this.state.minimized ? LocaleUtils.tr("print.maximize") : LocaleUtils.tr("print.minimize"); @@ -521,7 +629,7 @@ class Print extends React.Component { {() => ({ body: this.state.minimized ? null : this.renderBody(), extra: [ - this.renderPrintFrame() + this.renderPrintSelection() ] })} @@ -562,81 +670,141 @@ class Print extends React.Component { changeLayout = (ev) => { const layout = this.props.theme.print.find(item => item.name === ev.target.value); this.setState({layout: layout, atlasFeature: null}); - this.fixedMapCenter = null; }; changeScale = (ev) => { - this.setState({scale: ev.target.value}); + this.setState({scale: parseInt(ev.target.value, 10) || 0}); }; changeResolution = (ev) => { this.setState({dpi: ev.target.value}); }; changeRotation = (ev) => { if (!ev.target.value) { - this.setState({rotationNull: true}); + this.setState({rotation: 0}); } else { - this.setState({rotationNull: false}); - let angle = parseFloat(ev.target.value) || 0; - while (angle < 0) { - angle += 360; - } - while (angle >= 360) { - angle -= 360; - } - this.props.changeRotation(angle / 180 * Math.PI); + const angle = parseFloat(ev.target.value) || 0; + this.setState({rotation: (angle % 360 + 360) % 360}); } }; + changeSeriesOverlap = (ev) => { + this.setState({printSeriesOverlap: parseInt(ev.target.value, 10) || 0}); + }; + changeDownloadMode = (ev) => { + this.setState({downloadMode: ev.target.value}); + }; formatChanged = (ev) => { this.setState({selectedFormat: ev.target.value}); }; - computeCurrentExtent = () => { - if (!this.props.map || !this.state.layout || !this.state.scale) { - return [0, 0, 0, 0]; - } - const center = this.props.map.center; - const widthm = this.state.scale * this.state.layout.map.width / 1000; - const heightm = this.state.scale * this.state.layout.map.height / 1000; - const {width, height} = MapUtils.transformExtent(this.props.map.projection, center, widthm, heightm); - const x1 = center[0] - 0.5 * width; - const x2 = center[0] + 0.5 * width; - const y1 = center[1] - 0.5 * height; - const y2 = center[1] + 0.5 * height; - return [x1, y1, x2, y2]; - }; print = (ev) => { + ev.preventDefault(); + this.setState({ printing: true }); if (this.props.inlinePrintOutput) { - this.setState({printOutputVisible: true, outputLoaded: false}); + this.setState({ printOutputVisible: true, outputLoaded: false }); } - ev.preventDefault(); - this.setState({printing: true}); + const formData = formDataEntries(new FormData(this.printForm)); + let pages = [formData]; + + if (this.state.printSeriesEnabled) { + pages = this.state.extents.map((extent, index) => { + const fd = structuredClone(formData); + fd.name = (index + 1).toString().padStart(2, '0'); + fd[this.state.layout.map.name + ':extent'] = this.formatExtent(extent); + return fd; + }); + } + + const timestamp = dayjs(new Date()).format("YYYYMMDD_HHmmss"); + const fileName = this.props.fileNameTemplate + .replace("{theme}", this.props.theme.id) + .replace("{timestamp}", timestamp); + + // batch print all pages + this.batchPrint(pages, fileName) + .catch((e) => { + this.setState({ outputLoaded: true, printOutputVisible: false }); + if (e.response) { + /* eslint-disable-next-line */ + console.warn(new TextDecoder().decode(e.response.data)); + } + /* eslint-disable-next-line */ + alert('Print failed'); + }).finally(() => { + this.setState({ printing: false }); + }); + }; + async batchPrint(pages, fileName) { + // Print pages on server + const promises = pages.map((formData) => this.printRequest(formData)); + // Collect printing results + const docs = await Promise.all(promises); + // Convert into downloadable files + const files = await this.collectFiles(docs, fileName); + // Download or display files + if (this.props.inlinePrintOutput && files.length === 1) { + const file = files.pop(); + const fileURL = URL.createObjectURL(file.content); + this.setState({ pdfData: file, pdfDataUrl: fileURL, outputLoaded: true }); + } else { + for (const file of files) { + FileSaver.saveAs(file.content, file.fileName); + } + } + } + async printRequest(formData) { const data = Object.entries(formData).map((pair) => pair.map(entry => encodeURIComponent(entry).replace(/%20/g, '+')).join("=") - ).join("&"); + ).join('&'); const config = { headers: {'Content-Type': 'application/x-www-form-urlencoded' }, - responseType: "arraybuffer" + responseType: 'arraybuffer' }; - axios.post(this.props.theme.printUrl, data, config).then(response => { - this.setState({printing: false}); - const contentType = response.headers["content-type"]; - const file = new Blob([response.data], { type: contentType }); - if (this.props.inlinePrintOutput) { - const fileURL = URL.createObjectURL(file); - this.setState({ pdfData: file, pdfDataUrl: fileURL, outputLoaded: true }); - } else { - const ext = this.state.selectedFormat.split(";")[0].split("/").pop(); - FileSaver.saveAs(file, this.props.theme.id + '.' + ext); - } - }).catch(e => { - this.setState({printing: false, outputLoaded: true, printOutputVisible: false}); - if (e.response) { - /* eslint-disable-next-line */ - console.warn(new TextDecoder().decode(e.response.data)); - } - /* eslint-disable-next-line */ - alert('Print failed'); + const response = await axios.post(this.props.theme.printUrl, data, config); + const contentType = response.headers['content-type']; + return { + name: formData.name, + data: response.data, + contentType: contentType + }; + } + async collectFiles(docs, fileName) { + if (docs.length > 1 && this.state.downloadMode === 'onepdf') { + const data = await this.collectOnePdf(docs); + const content = new Blob([data], { type: 'application/pdf' }); + return [{ content, fileName: fileName + '.pdf' }]; + } + if (docs.length > 1 && this.state.downloadMode === 'onezip') { + const data = await this.collectOneZip(docs, fileName); + const content = new Blob([data], { type: 'application/zip' }); + return [{ content, fileName: fileName + '.zip' }]; + } + return docs.map((doc) => { + const content = new Blob([doc.data], { type: doc.contentType }); + const ext = this.state.selectedFormat.split(";")[0].split("/").pop(); + const appendix = doc.name ? '_' + doc.name : ''; + return { content, fileName: fileName + appendix + '.' + ext }; }); - }; + } + async collectOnePdf(docs) { + const mergedDoc = await PDFDocument.create(); + for (const doc of docs) { + const pdfBytes = await PDFDocument.load(doc.data); + const copiedPages = await mergedDoc.copyPages(pdfBytes, pdfBytes.getPageIndices()); + for (const page of copiedPages) { + mergedDoc.addPage(page); + } + } + return await mergedDoc.save(); + } + async collectOneZip(docs, fileName) { + const mergedDoc = new JSZip(); + for (const doc of docs) { + const file = new Blob([doc.data], { type: doc.contentType }); + const ext = this.state.selectedFormat.split(";")[0].split("/").pop(); + const appendix = doc.name ? '_' + doc.name : ''; + mergedDoc.file(fileName + appendix + '.' + ext, file); + } + return await mergedDoc.generateAsync({ type: 'arraybuffer' }); + } } const selector = (state) => ({ @@ -649,6 +817,6 @@ const selector = (state) => ({ export default connect(selector, { addLayerFeatures: addLayerFeatures, clearLayer: clearLayer, - changeRotation: changeRotation, - panTo: panTo + setIdentifyEnabled: setIdentifyEnabled, + setSnappingConfig: setSnappingConfig })(Print); diff --git a/plugins/RasterExport.jsx b/plugins/RasterExport.jsx deleted file mode 100644 index d81bbe16f..000000000 --- a/plugins/RasterExport.jsx +++ /dev/null @@ -1,377 +0,0 @@ -/** - * Copyright 2017-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 {LayerRole} from '../actions/layers'; -import {setCurrentTask} from '../actions/task'; -import Icon from '../components/Icon'; -import PrintFrame from '../components/PrintFrame'; -import SideBar from '../components/SideBar'; -import InputContainer from '../components/widgets/InputContainer'; -import Spinner from '../components/widgets/Spinner'; -import ConfigUtils from '../utils/ConfigUtils'; -import CoordinatesUtils from '../utils/CoordinatesUtils'; -import LayerUtils from '../utils/LayerUtils'; -import LocaleUtils from '../utils/LocaleUtils'; -import MapUtils from '../utils/MapUtils'; -import MiscUtils from '../utils/MiscUtils'; -import VectorLayerUtils from '../utils/VectorLayerUtils'; - -import './style/RasterExport.css'; - - -/** - * Allows exporting a selected portion of the map to an image ("screenshot"). - * - * Deprecated. Use the MapExport plugin instead. - */ -class RasterExport extends React.Component { - static propTypes = { - /** Whitelist of allowed export format mimetypes. If empty, supported formats are listed. */ - allowedFormats: PropTypes.arrayOf(PropTypes.string), - /** List of scales at which to export the map. */ - allowedScales: PropTypes.arrayOf(PropTypes.number), - /** Default export format mimetype. If empty, first available format is used. */ - defaultFormat: PropTypes.string, - /** The factor to apply to the map scale to determine the initial export map scale. */ - defaultScaleFactor: PropTypes.number, - /** List of dpis at which to export the map. If empty, the default server dpi is used. */ - dpis: PropTypes.arrayOf(PropTypes.number), - /** Whether to include external layers in the image. Requires QGIS Server 3.x! */ - exportExternalLayers: PropTypes.bool, - layers: PropTypes.array, - map: PropTypes.object, - /** List of image sizes to offer, in addition to the free-hand selection. The width and height are in millimeters. */ - pageSizes: PropTypes.arrayOf(PropTypes.shape({ - name: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number - })), - setCurrentTask: PropTypes.func, - /** The side of the application on which to display the sidebar. */ - side: PropTypes.string, - theme: PropTypes.object - }; - static defaultProps = { - defaultScaleFactor: 0.5, - exportExternalLayers: true, - side: 'right', - pageSizes: [ - {name: '15 x 15 cm', width: 150, height: 150}, - {name: '30 x 30 cm', width: 300, height: 300} - ] - }; - constructor(props) { - super(props); - this.form = null; - this.state.dpi = props.dpis[0] || 96; - - /* eslint-disable-next-line */ - console.warn("The RasterExport plugin is deprecated. Use the MapExport plugin instead."); - } - state = { - extent: '', - width: 0, - height: 0, - exporting: false, - availableFormats: [], - selectedFormat: null, - scale: '', - pageSize: null, - dpi: 96 - }; - componentDidUpdate(prevProps, prevState) { - if ( - this.props.map.center !== prevProps.map.center || - this.state.pageSize !== prevState.pageSize || - this.state.scale !== prevState.scale || - this.state.dpi !== prevState.dpi - ) { - if (this.state.pageSize !== null) { - this.setState((state) => { - const center = this.props.map.center; - const mapCrs = this.props.map.projection; - const pageSize = this.props.pageSizes[state.pageSize]; - const widthm = state.scale * pageSize.width / 1000; - const heightm = state.scale * pageSize.height / 1000; - const {width, height} = MapUtils.transformExtent(mapCrs, center, widthm, heightm); - let extent = [center[0] - 0.5 * width, center[1] - 0.5 * height, center[0] + 0.5 * width, center[1] + 0.5 * height]; - extent = (CoordinatesUtils.getAxisOrder(mapCrs).substr(0, 2) === 'ne' && this.props.theme.version === '1.3.0') ? - extent[1] + "," + extent[0] + "," + extent[3] + "," + extent[2] : - extent.join(','); - return { - width: Math.round(pageSize.width / 1000 * 39.3701 * state.dpi), - height: Math.round(pageSize.height / 1000 * 39.3701 * state.dpi), - extent: extent - }; - }); - } else if (prevState.pageSize !== null) { - this.setState({width: '', height: '', extent: ''}); - } - } - } - formatChanged = (ev) => { - this.setState({selectedFormat: ev.target.value}); - }; - dpiChanged = (ev) => { - this.setState({dpi: parseInt(ev.target.value, 10)}); - }; - renderBody = () => { - if (!this.props.theme || !this.state.selectedFormat) { - return null; - } - const formatMap = { - "image/jpeg": "JPEG", - "image/png": "PNG", - "image/png; mode=16bit": "PNG 16bit", - "image/png; mode=8bit": "PNG 8bit", - "image/png; mode=1bit": "PNG 1bit", - "image/geotiff": "GeoTIFF", - "image/tiff": "GeoTIFF" - }; - - let scaleChooser = null; - if (!isEmpty(this.props.allowedScales)) { - scaleChooser = ( - ); - } else { - scaleChooser = ( - this.setState({scale: ev.target.value})} role="input" type="number" value={this.state.scale} /> - ); - } - const filename = this.props.theme.name.split("/").pop() + "." + this.state.selectedFormat.split(";")[0].split("/").pop(); - const action = this.props.theme.url; - const exportExternalLayers = this.props.exportExternalLayers && ConfigUtils.getConfigProp("qgisServerVersion") >= 3; - - const exportParams = LayerUtils.collectPrintParams(this.props.layers, this.props.theme, this.state.scale, this.props.map.projection, exportExternalLayers); - - // Local vector layer features - const mapCrs = this.props.map.projection; - const highlightParams = VectorLayerUtils.createPrintHighlighParams(this.props.layers, mapCrs, this.state.scale, this.state.dpi); - const dimensionValues = this.props.layers.reduce((res, layer) => { - if (layer.role === LayerRole.THEME) { - Object.entries(layer.dimensionValues || {}).forEach(([key, value]) => { - if (value !== undefined) { - res[key] = value; - } - }); - } - return res; - }, {}); - - return ( -
- { this.form = el; }}> - - - - - - - {this.props.pageSizes ? ( - - - - - ) : null} - {this.state.pageSize !== null ? ( - - - - - ) : null} - {this.props.dpis ? ( - - - - - ) : null} - -
{LocaleUtils.tr("rasterexport.format")} - -
{LocaleUtils.tr("rasterexport.size")} - -
{LocaleUtils.tr("rasterexport.scale")} - - 1 :  - {scaleChooser} - -
{LocaleUtils.tr("rasterexport.resolution")} - -
- - - - {Object.entries(exportParams).map(([key, value]) => ())} - - - - - - - - - {Object.keys(this.props.theme.watermark || {}).map(key => { - return (); - })} - - - - - - - - - {Object.entries(dimensionValues).map(([key, value]) => ( - - ))} - -
- -
- -
- ); - }; - renderFrame = () => { - if (this.state.pageSize !== null) { - const px2m = 1 / (this.state.dpi * 39.3701) * this.state.scale; - const frame = { - width: this.state.width * px2m, - height: this.state.height * px2m - }; - return (); - } else { - return (); - } - }; - render() { - const minMaxTooltip = this.state.minimized ? LocaleUtils.tr("print.maximize") : LocaleUtils.tr("print.minimize"); - const extraTitlebarContent = ( this.setState((state) => ({minimized: !state.minimized}))} title={minMaxTooltip}/>); - return ( - - {() => ({ - body: this.state.minimized ? null : this.renderBody(), - extra: [ - this.renderFrame() - ] - })} - - ); - } - onShow = () => { - let scale = Math.round(MapUtils.computeForZoom(this.props.map.scales, this.props.map.zoom) * this.props.defaultScaleFactor); - if (!isEmpty(this.props.allowedScales)) { - let closestVal = Math.abs(scale - this.props.allowedScales[0]); - let closestIdx = 0; - for (let i = 1; i < this.props.allowedScales.length; ++i) { - const currVal = Math.abs(scale - this.props.allowedScales[i]); - if (currVal < closestVal) { - closestVal = currVal; - closestIdx = i; - } - } - scale = this.props.allowedScales[closestIdx]; - } - let availableFormats = this.props.theme.availableFormats; - if (!isEmpty(this.props.allowedFormats)) { - availableFormats = availableFormats.filter(fmt => this.props.allowedFormats.includes(fmt)); - } - const selectedFormat = this.props.defaultFormat && availableFormats.includes(this.props.defaultFormat) ? this.props.defaultFormat : availableFormats[0]; - this.setState({scale: scale, availableFormats: availableFormats, selectedFormat: selectedFormat}); - }; - onHide = () => { - this.setState({ - extent: '', - width: '', - height: '' - }); - }; - bboxSelected = (bbox, crs, pixelsize) => { - const version = this.props.theme.version; - let extent = ''; - if (bbox) { - extent = (CoordinatesUtils.getAxisOrder(crs).substr(0, 2) === 'ne' && version === '1.3.0') ? - bbox[1] + "," + bbox[0] + "," + bbox[3] + "," + bbox[2] : - bbox.join(','); - } - this.setState((state) => ({ - extent: extent, - width: Math.round(pixelsize[0] * parseInt(state.dpi || 96, 10) / 96), - height: Math.round(pixelsize[1] * parseInt(state.dpi || 96, 10) / 96) - })); - }; - export = (ev) => { - ev.preventDefault(); - this.setState({exporting: true}); - const formData = formDataEntries(new FormData(this.form)); - 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" - }; - axios.post(this.props.theme.url, data, config).then(response => { - this.setState({exporting: false}); - const contentType = response.headers["content-type"]; - FileSaver.saveAs(new Blob([response.data], {type: contentType}), this.props.theme.name + '.pdf'); - }).catch(e => { - this.setState({exporting: false}); - if (e.response) { - /* eslint-disable-next-line */ - console.log(new TextDecoder().decode(e.response.data)); - } - /* eslint-disable-next-line */ - alert('Export failed'); - }); - }; -} - -const selector = (state) => ({ - theme: state.theme.current, - map: state.map, - layers: state.layers.flat -}); - -export default connect(selector, { - setCurrentTask: setCurrentTask -})(RasterExport); diff --git a/plugins/style/DxfExport.css b/plugins/style/DxfExport.css deleted file mode 100644 index 3584b4369..000000000 --- a/plugins/style/DxfExport.css +++ /dev/null @@ -1,23 +0,0 @@ -.DxfExport div.help-text { - font-style: italic; - padding-bottom: 0.5em; -} - -.DxfExport div.export-settings { - font-size: small; -} - -.DxfExport div.export-settings > span { - margin: 0 0.25em; -} - -.DxfExport div.export-settings select { - border: 1px solid var(--border-color); - background-color: var(--input-bg-color); - display: inline-flex; - align-items: center; -} - -.DxfExport div.input-container input { - width: 8ch; -} diff --git a/plugins/style/RasterExport.css b/plugins/style/RasterExport.css deleted file mode 100644 index 04c3e3910..000000000 --- a/plugins/style/RasterExport.css +++ /dev/null @@ -1,55 +0,0 @@ -#RasterExport div.rasterexport-body { - padding: 0.25em; -} - -#RasterExport .rasterexport-minimize-maximize { - margin-left: 1em; - padding: 0.25em; -} - -#RasterExport table.options-table { - width: 100%; -} - -#RasterExport table.options-table td { - padding: 0.125em 0; -} - -#RasterExport table.options-table td:first-child { - max-width: 10em; - overflow: hidden; - text-overflow: ellipsis; - padding-right: 0.25em; -} - -#RasterExport table.options-table td:nth-child(2) { - width: 99%; -} - -#RasterExport table.options-table td:nth-child(2) > * { - width: 100%; -} - -#RasterExport table.options-table textarea { - resize: vertical; -} - -#RasterExport div.button-bar { - margin-top: 0.5em; -} - -#RasterExport div.button-bar > button { - width: 100%; -} - -#RasterExport span.rasterexport-wait { - display: flex; - align-items: center; -} - -#RasterExport span.rasterexport-wait div.spinner { - width: 1.25em; - height: 1.25em; - margin-right: 0.5em; - flex: 0 0 auto; -} \ No newline at end of file diff --git a/translations/ca-ES.json b/translations/ca-ES.json index ba6cc6a95..2ce29b3ff 100644 --- a/translations/ca-ES.json +++ b/translations/ca-ES.json @@ -18,14 +18,12 @@ "MapExport": "Exportar mapa", "MapFilter": "", "Print": "Imprimir", - "RasterExport": "Exportar trama", "Reports": "", "Settings": "Opcions", "Share": "Compartir link", "ThemeSwitcher": "Tema", "AttributeTable": "Taula d'atributs", "Cyclomedia": "", - "DxfExport": "Exportar DXF", "FeatureForm": "Formulari d'element", "FeatureSearch": "", "GeometryDigitizer": "", @@ -113,11 +111,6 @@ "scalehint": "", "title": "" }, - "dxfexport": { - "layers": "Capes", - "selectinfo": "Arrossega un rectangle al voltant de la zona per exportar...", - "symbologyscale": "Escala de la simbologia:" - }, "editing": { "add": "Afegir", "attrtable": "Taula", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "Format", "grid": "Graella", "layout": "Diseny", @@ -359,26 +356,19 @@ "nolayouts": "El tema seleccionat no admet la impresió", "notheme": "No hi ha tema seleccionat", "output": "Sortida d'impressió", + "overlap": "", "pickatlasfeature": "", "resolution": "Resolució", "rotation": "Rotació", "save": "", "scale": "Escala", + "series": "", "submit": "Imprimir", "wait": "Esperi si us plau..." }, "qtdesignerform": { "loading": "Carregant formulari..." }, - "rasterexport": { - "format": "Format:", - "resolution": "Resolució:", - "scale": "", - "size": "", - "submit": "", - "usersize": "", - "wait": "" - }, "redlining": { "border": "Límit", "buffer": "Buffer", diff --git a/translations/cs-CZ.json b/translations/cs-CZ.json index 3d8345faa..d5a0a80ad 100644 --- a/translations/cs-CZ.json +++ b/translations/cs-CZ.json @@ -18,14 +18,12 @@ "MapExport": "Export mapu", "MapFilter": "", "Print": "Tisk", - "RasterExport": "Export rastru", "Reports": "", "Settings": "Nastavení", "Share": "Sdílet odkaz", "ThemeSwitcher": "Téma", "AttributeTable": "Atributy", "Cyclomedia": "Cyclomedia", - "DxfExport": "DXF Export", "FeatureForm": "Editační formulář", "FeatureSearch": "", "GeometryDigitizer": "", @@ -113,11 +111,6 @@ "scalehint": "", "title": "Cyclomedia" }, - "dxfexport": { - "layers": "Vrstvy", - "selectinfo": "Vyberte oblast pro export...", - "symbologyscale": "Meřítko symbolů:" - }, "editing": { "add": "Přidat", "attrtable": "", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "Formát", "grid": "Mřížka:", "layout": "Rovržení:", @@ -359,26 +356,19 @@ "nolayouts": "Vybrané téma nepodporuje tisk", "notheme": "Není vybráno téma", "output": "", + "overlap": "", "pickatlasfeature": "", "resolution": "Rozlišení", "rotation": "Orientace", "save": "", "scale": "Měřítko", + "series": "", "submit": "Tisk", "wait": "Čekejte..." }, "qtdesignerform": { "loading": "" }, - "rasterexport": { - "format": "Formát:", - "resolution": "Rozlišení:", - "scale": "Měřítko", - "size": "Velikost", - "submit": "Export", - "usersize": "Vybrat na mapě...", - "wait": "Čekejte..." - }, "redlining": { "border": "Okraj", "buffer": "", diff --git a/translations/de-CH.json b/translations/de-CH.json index 612b4caee..4f1db5468 100644 --- a/translations/de-CH.json +++ b/translations/de-CH.json @@ -18,14 +18,12 @@ "MapExport": "Karte exportieren", "MapFilter": "Kartenfilter", "Print": "Drucken", - "RasterExport": "Raster-Export", "Reports": "Berichte", "Settings": "Einstellungen", "Share": "Teilen", "ThemeSwitcher": "Themen", "AttributeTable": "Attributtabelle", "Cyclomedia": "Cyclomedia", - "DxfExport": "DXF-Export", "FeatureForm": "Objektformular", "FeatureSearch": "Objektsuche", "GeometryDigitizer": "Geometriedigitalisierung", @@ -113,11 +111,6 @@ "scalehint": "Die Aufnahmen sind nur ab Massstab 1:{0} auf der Karte sichtbar.", "title": "Cyclomedia Viewer" }, - "dxfexport": { - "layers": "Ebenen", - "selectinfo": "Rechteck um die zu exportierende Region aufziehen", - "symbologyscale": "Darstellungsmassstab" - }, "editing": { "add": "Hinzufügen", "attrtable": "Tabelle", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "Atlasobjekt", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "Format", "grid": "Gitter", "layout": "Format", @@ -359,26 +356,19 @@ "nolayouts": "Das gewählte Thema stellt keine Druck-Vorlagen zur Verfügung.", "notheme": "Kein Thema selektiert", "output": "Druckausgabe", + "overlap": "", "pickatlasfeature": "In Ebene {0} auswählen...", "resolution": "Auflösung", "rotation": "Rotation", "save": "Speichern", "scale": "Massstab", + "series": "", "submit": "Erzeugen", "wait": "Bitte warten..." }, "qtdesignerform": { "loading": "Formular wird geladen..." }, - "rasterexport": { - "format": "Format", - "resolution": "Auflösung", - "scale": "Massstab", - "size": "Dimensionen", - "submit": "Exportieren", - "usersize": "Auf Karte auswählen...", - "wait": "Bitte warten..." - }, "redlining": { "border": "Rand", "buffer": "Puffern", diff --git a/translations/de-DE.json b/translations/de-DE.json index 2efb63566..97eedcfcb 100644 --- a/translations/de-DE.json +++ b/translations/de-DE.json @@ -18,14 +18,12 @@ "MapExport": "Karte exportieren", "MapFilter": "Kartenfilter", "Print": "Drucken", - "RasterExport": "Raster-Export", "Reports": "Berichte", "Settings": "Einstellungen", "Share": "Teilen", "ThemeSwitcher": "Themen", "AttributeTable": "Attributtabelle", "Cyclomedia": "Cyclomedia", - "DxfExport": "DXF-Export", "FeatureForm": "Objektformular", "FeatureSearch": "Objektsuche", "GeometryDigitizer": "Geometriedigitalisierung", @@ -113,11 +111,6 @@ "scalehint": "Die Aufnahmen sind nur ab Maßstab 1:{0} auf der Karte sichtbar.", "title": "Cyclomedia Viewer" }, - "dxfexport": { - "layers": "Ebenen", - "selectinfo": "Rechteck um die zu exportierende Region aufziehen", - "symbologyscale": "Darstellungsmaßstab" - }, "editing": { "add": "Hinzufügen", "attrtable": "Tabelle", @@ -350,35 +343,32 @@ }, "print": { "atlasfeature": "Atlasobjekt", + "download": "Herunterladen", + "download_as_onepdf": "als ein PDF-Dokument", + "download_as_onezip": "als ein ZIP-Archiv", + "download_as_single": "als einzelne Dateien", "format": "Format", "grid": "Gitter", - "layout": "Format", + "layout": "Layout", "legend": "Legende", "maximize": "Maximieren", "minimize": "Minimieren", "nolayouts": "Das gewählte Thema stellt keine Druck-Vorlagen zur Verfügung.", "notheme": "Kein Thema selektiert", "output": "Druckausgabe", + "overlap": "Überlappung", "pickatlasfeature": "In Ebene {0} auswählen...", "resolution": "Auflösung", "rotation": "Rotation", "save": "Speichern", "scale": "Maßstab", + "series": "Seriendruck", "submit": "Erzeugen", "wait": "Bitte warten..." }, "qtdesignerform": { "loading": "Formular wird geladen..." }, - "rasterexport": { - "format": "Format", - "resolution": "Auflösung", - "scale": "Maßstab", - "size": "Dimensionen", - "submit": "Exportieren", - "usersize": "Auf Karte auswählen...", - "wait": "Bitte warten..." - }, "redlining": { "border": "Rand", "buffer": "Puffern", diff --git a/translations/en-US.json b/translations/en-US.json index 9da5d2b4b..4bb96dd47 100644 --- a/translations/en-US.json +++ b/translations/en-US.json @@ -18,14 +18,12 @@ "MapExport": "Export map", "MapFilter": "Map Filter", "Print": "Print", - "RasterExport": "Raster Export", "Reports": "Reports", "Settings": "Settings", "Share": "Share Link", "ThemeSwitcher": "Theme", "AttributeTable": "Attribute Table", "Cyclomedia": "Cyclomedia", - "DxfExport": "DXF Export", "FeatureForm": "Feature Form", "FeatureSearch": "Feature Search", "GeometryDigitizer": "Geometry digitizer", @@ -113,11 +111,6 @@ "scalehint": "The recordings are only visible on the map below scale 1:{0}.", "title": "Cyclomedia Viewer" }, - "dxfexport": { - "layers": "Layers", - "selectinfo": "Drag a rectangle around the region to export...", - "symbologyscale": "Symbology scale" - }, "editing": { "add": "Add", "attrtable": "Table", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "Atlas feature", + "download": "Download", + "download_as_onepdf": "as one PDF document", + "download_as_onezip": "as one ZIP file", + "download_as_single": "as single files", "format": "Format", "grid": "Grid", "layout": "Layout", @@ -359,26 +356,19 @@ "nolayouts": "The selected theme does not support printing", "notheme": "No theme selected", "output": "Print output", + "overlap": "Overlap", "pickatlasfeature": "Pick in layer {0}...", "resolution": "Resolution", "rotation": "Rotation", "save": "Save", "scale": "Scale", + "series": "Print series", "submit": "Print", "wait": "Please wait..." }, "qtdesignerform": { "loading": "Loading form..." }, - "rasterexport": { - "format": "Format", - "resolution": "Resolution", - "scale": "Scale", - "size": "Size", - "submit": "Export", - "usersize": "Select on map...", - "wait": "Please wait..." - }, "redlining": { "border": "Border", "buffer": "Buffer", diff --git a/translations/es-ES.json b/translations/es-ES.json index 9a8a4da9c..32c0da801 100644 --- a/translations/es-ES.json +++ b/translations/es-ES.json @@ -18,14 +18,12 @@ "MapExport": "Exportar mapa", "MapFilter": "", "Print": "Imprimir", - "RasterExport": "Exportar trama", "Reports": "", "Settings": "Opciones", "Share": "Compartir enlace", "ThemeSwitcher": "Tema", "AttributeTable": "Tabla de Atributos", "Cyclomedia": "", - "DxfExport": "Exportar DXF", "FeatureForm": "Formulario de Elemento", "FeatureSearch": "", "GeometryDigitizer": "", @@ -113,11 +111,6 @@ "scalehint": "", "title": "" }, - "dxfexport": { - "layers": "Capas", - "selectinfo": "Arrastre un rectángulo alrededor de la zona para exportar...", - "symbologyscale": "Escala de la simbología:" - }, "editing": { "add": "Añadir", "attrtable": "Tabla", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "Formato:", "grid": "Grilla:", "layout": "Diseño:", @@ -359,26 +356,19 @@ "nolayouts": "El tema seleccionado no admite su impresión", "notheme": "No hay tema seleccionado", "output": "Salida de impresión", + "overlap": "", "pickatlasfeature": "", "resolution": "Resolución", "rotation": "Rotación", "save": "", "scale": "Escala:", + "series": "", "submit": "Imprimir", "wait": "Por favor espere..." }, "qtdesignerform": { "loading": "Cargando formulario..." }, - "rasterexport": { - "format": "Formato:", - "resolution": "Resolución:", - "scale": "", - "size": "", - "submit": "", - "usersize": "", - "wait": "" - }, "redlining": { "border": "Límite", "buffer": "Bufer", diff --git a/translations/fi-FI.json b/translations/fi-FI.json index d21a20b8d..c4b50ea1e 100644 --- a/translations/fi-FI.json +++ b/translations/fi-FI.json @@ -18,14 +18,12 @@ "MapExport": "Vienti kartta", "MapFilter": "", "Print": "Tulosta", - "RasterExport": "Rasterin vienti", "Reports": "", "Settings": "", "Share": "Jaa linkki", "ThemeSwitcher": "Teema", "AttributeTable": "", "Cyclomedia": "", - "DxfExport": "DXF vienti", "FeatureForm": "", "FeatureSearch": "", "GeometryDigitizer": "", @@ -113,11 +111,6 @@ "scalehint": "", "title": "" }, - "dxfexport": { - "layers": "Karttatasot", - "selectinfo": "Piirrä suorakulmio alueen ympärille vientiä varten", - "symbologyscale": "Symbolien mittakaava" - }, "editing": { "add": "", "attrtable": "", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "Formaatti", "grid": "Ristikko", "layout": "Layout", @@ -359,26 +356,19 @@ "nolayouts": "Valittu teema ei tue tulostamista", "notheme": "Teemaa ei ole valittu", "output": "Tulosta tuotos", + "overlap": "", "pickatlasfeature": "", "resolution": "Resoluutio", "rotation": "Rotaatio", "save": "", "scale": "Mittakaava", + "series": "", "submit": "Tulosta", "wait": "Odota..." }, "qtdesignerform": { "loading": "" }, - "rasterexport": { - "format": "Formaatti:", - "resolution": "Resoluutio:", - "scale": "", - "size": "", - "submit": "", - "usersize": "", - "wait": "" - }, "redlining": { "border": "Reunat", "buffer": "Bufferi", diff --git a/translations/fr-FR.json b/translations/fr-FR.json index e85f4293f..85e126c6e 100644 --- a/translations/fr-FR.json +++ b/translations/fr-FR.json @@ -18,14 +18,12 @@ "MapExport": "Exporter la carte", "MapFilter": "Filtrer la carte", "Print": "Imprimer", - "RasterExport": "Exporter l'image", "Reports": "Rapports", "Settings": "Paramètres", "Share": "Partager", "ThemeSwitcher": "Thèmes", "AttributeTable": "Table d'attributs", "Cyclomedia": "Cyclomedia", - "DxfExport": "Export DXF", "FeatureForm": "Formulaire d'objet", "FeatureSearch": "Recherche objet", "GeometryDigitizer": "Digitalisation des géométries", @@ -113,11 +111,6 @@ "scalehint": "Les enregistrements ne sont visibles que sur la carte sous l'échelle 1 :{0}.", "title": "Visualiseur Cyclomedia" }, - "dxfexport": { - "layers": "Couches", - "selectinfo": "Faites glisser un rectangle autour de la région à exporter..", - "symbologyscale": "Echelle" - }, "editing": { "add": "Ajouter", "attrtable": "Table", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "Objet atlas", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "Format", "grid": "Grille", "layout": "Mise en page", @@ -359,26 +356,19 @@ "nolayouts": "Il n'y a pas de mise en page disponible pour le thème choisi.", "notheme": "Pas de thème sélectionné", "output": "Impression", + "overlap": "", "pickatlasfeature": "Choisir dans la couche {0}", "resolution": "Résolution", "rotation": "Rotation", "save": "Enregistrer", "scale": "Echelle", + "series": "", "submit": "Imprimer", "wait": "Veuillez patienter..." }, "qtdesignerform": { "loading": "Chargement du formulaire..." }, - "rasterexport": { - "format": "Format", - "resolution": "Résolution", - "scale": "Echelle", - "size": "Dimension", - "submit": "Exporter", - "usersize": "Selectionner sur la carte...", - "wait": "Veuillez patienter..." - }, "redlining": { "border": "Bordure", "buffer": "Tampon", diff --git a/translations/hu-HU.json b/translations/hu-HU.json index 2d94fb425..a3eec4725 100644 --- a/translations/hu-HU.json +++ b/translations/hu-HU.json @@ -18,14 +18,12 @@ "MapExport": "Export térkép", "MapFilter": "", "Print": "Nyomtatás", - "RasterExport": "Kép exportálása", "Reports": "", "Settings": "", "Share": "Megosztható link", "ThemeSwitcher": "Térkép", "AttributeTable": "", "Cyclomedia": "", - "DxfExport": "DXF Exportálás", "FeatureForm": "", "FeatureSearch": "", "GeometryDigitizer": "", @@ -113,11 +111,6 @@ "scalehint": "", "title": "" }, - "dxfexport": { - "layers": "", - "selectinfo": "Rajzolj egy négyszöget az exportálni kívánt terület köré...", - "symbologyscale": "Jel méretatarány:" - }, "editing": { "add": "", "attrtable": "", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "Formátum", "grid": "Rács", "layout": "Elrendezés", @@ -359,26 +356,19 @@ "nolayouts": "A kiválasztott térkép nem támogatja a nyomtatást", "notheme": "Nincs térkép kiválasztva", "output": "", + "overlap": "", "pickatlasfeature": "", "resolution": "Felbontás", "rotation": "Forgatás", "save": "", "scale": "Méretarány", + "series": "", "submit": "Nyomtatás", "wait": "" }, "qtdesignerform": { "loading": "" }, - "rasterexport": { - "format": "Formátum:", - "resolution": "Felbontás:", - "scale": "", - "size": "", - "submit": "", - "usersize": "", - "wait": "" - }, "redlining": { "border": "Szegély", "buffer": "", diff --git a/translations/it-IT.json b/translations/it-IT.json index ae5528257..65a2cd672 100644 --- a/translations/it-IT.json +++ b/translations/it-IT.json @@ -18,14 +18,12 @@ "MapExport": "Esporta mappa", "MapFilter": "Filta mappa", "Print": "Stampa", - "RasterExport": "Esporta su immagine", "Reports": "Rapporti", "Settings": "Impostazioni", "Share": "Condividi", "ThemeSwitcher": "Temi", "AttributeTable": "Tabella attributi", "Cyclomedia": "Cyclomedia", - "DxfExport": "Esporta su DXF", "FeatureForm": "Formulario oggetto", "FeatureSearch": "Ricerca oggetto", "GeometryDigitizer": "Digitalizzazione geometrie", @@ -113,11 +111,6 @@ "scalehint": "Le registrazioni sono solo visibili sulla mappa a partire da una scala di 1:{0}.", "title": "Visualizzatore Cyclomedia" }, - "dxfexport": { - "layers": "Livelli", - "selectinfo": "Seleziona l'area da esportare", - "symbologyscale": "Scala per la simbologia" - }, "editing": { "add": "Aggiungi", "attrtable": "Tabella", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "Oggetto atlante", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "Formato", "grid": "Griglia", "layout": "Layout", @@ -359,26 +356,19 @@ "nolayouts": "Nessun layout di stampa", "notheme": "Nessun tema selezionato", "output": "Stampa", + "overlap": "", "pickatlasfeature": "Seleziona nel livello {1}...", "resolution": "Risoluzione", "rotation": "Rotazione", "save": "Salva", "scale": "Scala", + "series": "", "submit": "Stampa", "wait": "Attendere..." }, "qtdesignerform": { "loading": "Caricando formulario..." }, - "rasterexport": { - "format": "Formato", - "resolution": "Risoluzione", - "scale": "Scala", - "size": "Dimensione", - "submit": "Esporta", - "usersize": "Seleziona sulla mappa...", - "wait": "Attendere..." - }, "redlining": { "border": "Bordo", "buffer": "Buffer", diff --git a/translations/no-NO.json b/translations/no-NO.json index 2e56d6098..1093234e9 100644 --- a/translations/no-NO.json +++ b/translations/no-NO.json @@ -18,14 +18,12 @@ "MapExport": "Eksport kart", "MapFilter": "", "Print": "Skriv ut", - "RasterExport": "Eksporter Raster", "Reports": "", "Settings": "", "Share": "Del lenke", "ThemeSwitcher": "Tema", "AttributeTable": "", "Cyclomedia": "", - "DxfExport": "Eksporter DXF", "FeatureForm": "", "FeatureSearch": "", "GeometryDigitizer": "", @@ -113,11 +111,6 @@ "scalehint": "", "title": "" }, - "dxfexport": { - "layers": "", - "selectinfo": "Tegn et rektangel rundt området som skal eksporteres...", - "symbologyscale": "Symbolskala:" - }, "editing": { "add": "", "attrtable": "", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "Format", "grid": "Rutenett", "layout": "Layout", @@ -359,26 +356,19 @@ "nolayouts": "Valgt tema støtter ikke utskrift", "notheme": "Ingen tema valgt", "output": "Utskrift", + "overlap": "", "pickatlasfeature": "", "resolution": "Oppløsning", "rotation": "Rotasjon", "save": "", "scale": "Skala", + "series": "", "submit": "Skriv ut", "wait": "Venter..." }, "qtdesignerform": { "loading": "" }, - "rasterexport": { - "format": "Format:", - "resolution": "Oppløsning:", - "scale": "", - "size": "", - "submit": "", - "usersize": "", - "wait": "" - }, "redlining": { "border": "Kant", "buffer": "Buffer", diff --git a/translations/pl-PL.json b/translations/pl-PL.json index 98e53c1e7..3fa4466fc 100644 --- a/translations/pl-PL.json +++ b/translations/pl-PL.json @@ -18,14 +18,12 @@ "MapExport": "Eksportuj mapę", "MapFilter": "", "Print": "Drukuj", - "RasterExport": "Eksport Rastra", "Reports": "", "Settings": "", "Share": "Udostępnij Link", "ThemeSwitcher": "Motyw", "AttributeTable": "", "Cyclomedia": "", - "DxfExport": "Eksport DXF", "FeatureForm": "", "FeatureSearch": "", "GeometryDigitizer": "", @@ -113,11 +111,6 @@ "scalehint": "", "title": "" }, - "dxfexport": { - "layers": "", - "selectinfo": "Narysuj prostokąt, określając obszar do eksportu...", - "symbologyscale": "Skala symbolizacji:" - }, "editing": { "add": "", "attrtable": "", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "Format", "grid": "Siatka", "layout": "Layout", @@ -359,26 +356,19 @@ "nolayouts": "Wybrany motyw nie umożliwia drukowania", "notheme": "Brak wybranego motywu", "output": "Drukuj output", + "overlap": "", "pickatlasfeature": "", "resolution": "Rozdzielczość", "rotation": "Obrót", "save": "", "scale": "Skala", + "series": "", "submit": "Drukuj", "wait": "Proszę czekać..." }, "qtdesignerform": { "loading": "" }, - "rasterexport": { - "format": "Format:", - "resolution": "Rozdzielczość:", - "scale": "", - "size": "", - "submit": "", - "usersize": "", - "wait": "" - }, "redlining": { "border": "Obramowanie", "buffer": "Buforuj", diff --git a/translations/pt-BR.json b/translations/pt-BR.json index bdfa798ab..6e19fab83 100644 --- a/translations/pt-BR.json +++ b/translations/pt-BR.json @@ -18,14 +18,12 @@ "MapExport": "Exportar mapa", "MapFilter": "", "Print": "Imprimir", - "RasterExport": "Exportar imagem", "Reports": "", "Settings": "Configurações", "Share": "Compartilhar link", "ThemeSwitcher": "Tema", "AttributeTable": "Tabela de atributos", "Cyclomedia": "Cyclomedia", - "DxfExport": "Exportar DXF", "FeatureForm": "Atributos da feição", "FeatureSearch": "Procurar feição", "GeometryDigitizer": "", @@ -113,11 +111,6 @@ "scalehint": "Indicação de escala", "title": "Titulo" }, - "dxfexport": { - "layers": "Camada", - "selectinfo": "Arraste um retângulo ao redor da região para exportar", - "symbologyscale": "Escala de simbologia" - }, "editing": { "add": "Adicionar", "attrtable": "Tabela de atributos", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "Feições do atlas", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "Formato", "grid": "Grade", "layout": "Layout", @@ -359,26 +356,19 @@ "nolayouts": "O tema selecionado não suporta impressão", "notheme": "Nenhum tema selecionado", "output": "Saída de impressão", + "overlap": "", "pickatlasfeature": "", "resolution": "Resolução", "rotation": "Rotação", "save": "", "scale": "Escala", + "series": "", "submit": "Imprimir", "wait": "Aguarde..." }, "qtdesignerform": { "loading": "Carregando" }, - "rasterexport": { - "format": "Formato:", - "resolution": "Resolução:", - "scale": "Escala", - "size": "Tamanho", - "submit": "Enviar", - "usersize": "Medidas do usuário", - "wait": "Aguarde" - }, "redlining": { "border": "Borda", "buffer": "Buffer", diff --git a/translations/pt-PT.json b/translations/pt-PT.json index a74847ba1..acbe3788d 100644 --- a/translations/pt-PT.json +++ b/translations/pt-PT.json @@ -18,14 +18,12 @@ "MapExport": "Exportar Mapa", "MapFilter": "", "Print": "Imprimir", - "RasterExport": "Exportar Raster", "Reports": "", "Settings": "Configurações", "Share": "Partilhar Link", "ThemeSwitcher": "Tema", "AttributeTable": "Tabela de Atributos", "Cyclomedia": "Cyclomedia", - "DxfExport": "Exportar DXF", "FeatureForm": "Formulário de Recurso", "FeatureSearch": "Pesquisa de Recurso", "GeometryDigitizer": "", @@ -113,11 +111,6 @@ "scalehint": "Dica de Escala", "title": "Cyclomedia" }, - "dxfexport": { - "layers": "Camadas", - "selectinfo": "Arraste um retângulo ao redor da região para exportar...", - "symbologyscale": "Escala de Símbolos" - }, "editing": { "add": "Adicionar", "attrtable": "Tabela de Atributos", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "Recurso do Atlas", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "Formato", "grid": "Grelha", "layout": "Esquema", @@ -359,26 +356,19 @@ "nolayouts": "O tema selecionado não suporta a impressão", "notheme": "Nenhum tema selecionado", "output": "Saída", + "overlap": "", "pickatlasfeature": "Escolher Recurso do Atlas", "resolution": "Resolução", "rotation": "Rotação", "save": "", "scale": "Escala", + "series": "", "submit": "Imprimir", "wait": "Por favor, aguarde..." }, "qtdesignerform": { "loading": "A carregar..." }, - "rasterexport": { - "format": "Formato:", - "resolution": "Resolução:", - "scale": "Escala:", - "size": "Tamanho:", - "submit": "Enviar", - "usersize": "Tamanho Personalizado", - "wait": "Por favor, aguarde..." - }, "redlining": { "border": "Fronteira", "buffer": "Tampão", diff --git a/translations/ro-RO.json b/translations/ro-RO.json index 55ce07c07..969c265b9 100644 --- a/translations/ro-RO.json +++ b/translations/ro-RO.json @@ -18,14 +18,12 @@ "MapExport": "", "MapFilter": "", "Print": "Tipărire", - "RasterExport": "Export Raster", "Reports": "", "Settings": "Setări", "Share": "Trimite Link", "ThemeSwitcher": "Hărți tematice", "AttributeTable": "Tabela de atribute", "Cyclomedia": "Cyclomedia", - "DxfExport": "Export DXF", "FeatureForm": "Editare atribute", "FeatureSearch": "", "GeometryDigitizer": "", @@ -113,11 +111,6 @@ "scalehint": "Înregistrările sunt vizibile pe hartă doar la scara 1:{0} sau mai mare", "title": "Informații Cyclomedia" }, - "dxfexport": { - "layers": "Straturi", - "selectinfo": "Încadrați într-un dreptunghi zona de exportat..", - "symbologyscale": "Scara simbolurilor" - }, "editing": { "add": "Adaugă", "attrtable": "Tabel", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "Obiect în atlas", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "", "grid": "Grid", "layout": "Șablon", @@ -359,26 +356,19 @@ "nolayouts": "Această temă nu permite tipărirea", "notheme": "Nicio temă selectată", "output": "Rezultat tipărire", + "overlap": "", "pickatlasfeature": "Selectare din stratul {0}...", "resolution": "Rezoluție", "rotation": "Rotație", "save": "", "scale": "Scară", + "series": "", "submit": "Tipărește", "wait": "Vă rugăm așteptați..." }, "qtdesignerform": { "loading": "Formularul se încarcă" }, - "rasterexport": { - "format": "Format", - "resolution": "Rezoluție", - "scale": "Scara", - "size": "Dimensiuni", - "submit": "Export", - "usersize": "Selectați în hartă...", - "wait": "Vă rugăm așteptați..." - }, "redlining": { "border": "Margine", "buffer": "Zonă tampon", diff --git a/translations/ru-RU.json b/translations/ru-RU.json index 01837e489..eb37f18e9 100644 --- a/translations/ru-RU.json +++ b/translations/ru-RU.json @@ -18,14 +18,12 @@ "MapExport": "экспортировать карту", "MapFilter": "", "Print": "Печать", - "RasterExport": "Экспорт в растровый Формат бумаги", "Reports": "", "Settings": "", "Share": "Поделиться ссылкой", "ThemeSwitcher": "Тема", "AttributeTable": "", "Cyclomedia": "", - "DxfExport": "Экспорт в DXF", "FeatureForm": "", "FeatureSearch": "", "GeometryDigitizer": "", @@ -113,11 +111,6 @@ "scalehint": "", "title": "" }, - "dxfexport": { - "layers": "", - "selectinfo": "Для экспорта обведите регион многоугольником...", - "symbologyscale": "Масштаб символики:" - }, "editing": { "add": "", "attrtable": "", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "Формат бумаги", "grid": "Сетка", "layout": "Формат бумаги", @@ -359,26 +356,19 @@ "nolayouts": "Выбранная тема не поддерживает печать", "notheme": "Тема не выбрана", "output": "", + "overlap": "", "pickatlasfeature": "", "resolution": "Разрешение", "rotation": "Поворот", "save": "", "scale": "Масштаб", + "series": "", "submit": "Печать", "wait": "" }, "qtdesignerform": { "loading": "" }, - "rasterexport": { - "format": "Формат бумаги:", - "resolution": "Разрешение:", - "scale": "", - "size": "", - "submit": "", - "usersize": "", - "wait": "" - }, "redlining": { "border": "Граница", "buffer": "", diff --git a/translations/sv-SE.json b/translations/sv-SE.json index d2e9ec872..09a6962b5 100644 --- a/translations/sv-SE.json +++ b/translations/sv-SE.json @@ -18,14 +18,12 @@ "MapExport": "Exportera karta", "MapFilter": "", "Print": "Skriv ut", - "RasterExport": "Raster Export", "Reports": "", "Settings": "", "Share": "Dela länk", "ThemeSwitcher": "Tema", "AttributeTable": "", "Cyclomedia": "", - "DxfExport": "DXF Export", "FeatureForm": "", "FeatureSearch": "", "GeometryDigitizer": "", @@ -113,11 +111,6 @@ "scalehint": "", "title": "" }, - "dxfexport": { - "layers": "", - "selectinfo": "Rita en rektangel runt området som ska exporteras...", - "symbologyscale": "Symbolskala:" - }, "editing": { "add": "", "attrtable": "", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "Format", "grid": "Rutnät", "layout": "Layout:", @@ -359,26 +356,19 @@ "nolayouts": "Valt tema stöder inte utskrift", "notheme": "Inget tema valt", "output": "Utskrift", + "overlap": "", "pickatlasfeature": "", "resolution": "Upplösning", "rotation": "Rotation", "save": "", "scale": "Skala", + "series": "", "submit": "Skriv ut", "wait": "Vänta..." }, "qtdesignerform": { "loading": "" }, - "rasterexport": { - "format": "Format:", - "resolution": "Upplösning:", - "scale": "", - "size": "", - "submit": "", - "usersize": "", - "wait": "" - }, "redlining": { "border": "Kant", "buffer": "Buffert", diff --git a/translations/tr-TR.json b/translations/tr-TR.json index bf7f2d178..cbd979165 100644 --- a/translations/tr-TR.json +++ b/translations/tr-TR.json @@ -18,14 +18,12 @@ "MapExport": "Dışarıya ver", "MapFilter": "Harita Filtresi", "Print": "Yazdır", - "RasterExport": "Resim Olarak Kaydet", "Reports": "", "Settings": "Ayarlar", "Share": "Bağlantı Paylaş", "ThemeSwitcher": "Tema", "AttributeTable": "Öznitelik Tablosu", "Cyclomedia": "Siklomedya", - "DxfExport": "DXF'e Veri Aktar", "FeatureForm": "Obje Formu", "FeatureSearch": "Obje Arama", "GeometryDigitizer": "Geometri Sayısallaştırıcı", @@ -113,11 +111,6 @@ "scalehint": "The recordings are only visible on the map below scale 1:{0}.", "title": "Cyclomedia Viewer" }, - "dxfexport": { - "layers": "Katmanlar", - "selectinfo": "Export edilecek alana bir dikdörtgen çizin...", - "symbologyscale": "Sembol Ölçeği:" - }, "editing": { "add": "Ekle", "attrtable": "Tablo", @@ -350,6 +343,10 @@ }, "print": { "atlasfeature": "Atlas Objesi", + "download": "", + "download_as_onepdf": "", + "download_as_onezip": "", + "download_as_single": "", "format": "Format", "grid": "Karelaj", "layout": "Yazdırma düzeni", @@ -359,26 +356,19 @@ "nolayouts": "Seçili tema yazdırma işlemini desteklemiyor", "notheme": "Tema seçilmedi", "output": "Yazıcı çıktısı", + "overlap": "", "pickatlasfeature": "Katmandan seçiniz: {0}...", "resolution": "Çözünürlük", "rotation": "Döndürme", "save": "", "scale": "Ölçek:", + "series": "", "submit": "Yazdır", "wait": "Lütfen bekleyiniz..." }, "qtdesignerform": { "loading": "Form yükleniyor..." }, - "rasterexport": { - "format": "Format:", - "resolution": "Çözünürlük:", - "scale": "Ölçek", - "size": "Boyut", - "submit": "Ver", - "usersize": "Haritadan seç...", - "wait": "Lütfen bekleyiniz..." - }, "redlining": { "border": "Sınır", "buffer": "Tampon", diff --git a/translations/tsconfig.json b/translations/tsconfig.json index d68bc0ea7..6816b7c83 100644 --- a/translations/tsconfig.json +++ b/translations/tsconfig.json @@ -22,7 +22,6 @@ "extra_strings": [ "appmenu.items.AttributeTable", "appmenu.items.Cyclomedia", - "appmenu.items.DxfExport", "appmenu.items.Editing", "appmenu.items.FeatureForm", "appmenu.items.FeatureSearch", @@ -42,7 +41,6 @@ "appmenu.items.MeasurePolygon", "appmenu.items.Portal", "appmenu.items.Print", - "appmenu.items.RasterExport", "appmenu.items.Reports", "appmenu.items.MapExport", "appmenu.items.Redlining", @@ -68,7 +66,6 @@ "appmenu.items.MapExport", "appmenu.items.MapFilter", "appmenu.items.Print", - "appmenu.items.RasterExport", "appmenu.items.Reports", "appmenu.items.Settings", "appmenu.items.Share", @@ -126,9 +123,6 @@ "cyclomedia.login", "cyclomedia.scalehint", "cyclomedia.title", - "dxfexport.layers", - "dxfexport.selectinfo", - "dxfexport.symbologyscale", "editing.add", "editing.attrtable", "editing.canceldelete", @@ -302,6 +296,10 @@ "portal.filter", "portal.menulabel", "print.atlasfeature", + "print.download", + "print.download_as_onepdf", + "print.download_as_onezip", + "print.download_as_single", "print.format", "print.grid", "print.layout", @@ -311,21 +309,16 @@ "print.nolayouts", "print.notheme", "print.output", + "print.overlap", "print.pickatlasfeature", "print.resolution", "print.rotation", "print.save", "print.scale", + "print.series", "print.submit", "print.wait", "qtdesignerform.loading", - "rasterexport.format", - "rasterexport.resolution", - "rasterexport.scale", - "rasterexport.size", - "rasterexport.submit", - "rasterexport.usersize", - "rasterexport.wait", "redlining.border", "redlining.buffer", "redlining.buffercompute", diff --git a/utils/FeatureStyles.js b/utils/FeatureStyles.js index 26a6e0869..e32116000 100644 --- a/utils/FeatureStyles.js +++ b/utils/FeatureStyles.js @@ -8,6 +8,8 @@ import ol from 'openlayers'; +import minus from '../icons/minus.svg'; +import plus from '../icons/plus.svg'; import ConfigUtils from './ConfigUtils'; import ResourceRegistry from './ResourceRegistry'; import arrowhead from './img/arrowhead.svg'; @@ -17,6 +19,8 @@ import measurehead from './img/measurehead.svg'; ResourceRegistry.addResource('arrowhead', arrowhead); ResourceRegistry.addResource('measurehead', measurehead); ResourceRegistry.addResource('marker', markerIcon); +ResourceRegistry.addResource('minus', minus); +ResourceRegistry.addResource('plus', plus); const DEFAULT_FEATURE_STYLE = { strokeColor: [0, 0, 255, 1], @@ -60,7 +64,12 @@ const DEFAULT_INTERACTION_STYLE = { measurePointRadius: 6, sketchPointFillColor: "#0099FF", sketchPointStrokeColor: "white", - sketchPointRadius: 6 + sketchPointRadius: 6, + printStrokeColor: '#3399CC', + printStrokeWidth: 3, + printVertexColor: '#FFFFFF', + printVertexRadius: 6, + printBackgroundColor: [0, 0, 0, 0.5] }; export const END_MARKERS = { @@ -278,6 +287,82 @@ export default { }) }); }, + printInteraction: (options) => { + const opts = {...DEFAULT_INTERACTION_STYLE, ...ConfigUtils.getConfigProp("defaultInteractionStyle"), ...options}; + return new ol.style.Style({ + geometry: opts.geometryFunction, + fill: new ol.style.Fill({ + color: [0, 0, 0, 0] + }), + stroke: new ol.style.Stroke({ + color: opts.printStrokeColor, + width: opts.printStrokeWidth + }) + }); + }, + printInteractionVertex: (options) => { + const opts = {...DEFAULT_INTERACTION_STYLE, ...ConfigUtils.getConfigProp("defaultInteractionStyle"), ...options}; + return new ol.style.Style({ + geometry: opts.geometryFunction, + image: new ol.style.Circle({ + radius: opts.printVertexRadius, + fill: new ol.style.Fill({ + color: opts.fill ? opts.printStrokeColor : opts.printVertexColor + }), + stroke: new ol.style.Stroke({ + color: opts.printStrokeColor, + width: opts.printStrokeWidth + }) + }) + }); + }, + printInteractionBackground: (options) => { + const opts = {...DEFAULT_INTERACTION_STYLE, ...ConfigUtils.getConfigProp("defaultInteractionStyle"), ...options}; + return new ol.style.Style({ + geometry: opts.geometryFunction, + fill: new ol.style.Fill({ + color: opts.printBackgroundColor + }) + }); + }, + printInteractionSeries: (options) => { + const opts = {...DEFAULT_INTERACTION_STYLE, ...ConfigUtils.getConfigProp("defaultInteractionStyle"), ...options}; + return new ol.style.Style({ + geometry: opts.geometryFunction, + stroke: new ol.style.Stroke({ + color: opts.printStrokeColor, + width: opts.printStrokeWidth + }) + }); + }, + printInteractionSeriesIcon: (options) => { + const opts = {...DEFAULT_INTERACTION_STYLE, ...ConfigUtils.getConfigProp("defaultInteractionStyle"), ...options}; + return [ + new ol.style.Style({ + geometry: opts.geometryFunction, + image: new ol.style.Circle({ + radius: 20 * opts.radius, + fill: new ol.style.Fill({ + color: opts.printVertexColor + }), + stroke: new ol.style.Stroke({ + color: opts.printStrokeColor, + width: opts.printStrokeWidth + }) + }) + }), + new ol.style.Style({ + geometry: opts.geometryFunction, + image: new ol.style.Icon({ + src: ResourceRegistry.getResource(opts.img), + opacity: 0.5, + rotation: opts.rotation, + scale: opts.radius, + rotateWithView: true + }) + }) + ]; + }, image: (feature, options) => { return new ol.style.Style({ image: new ol.style.Icon({