diff --git a/components/map3d/HeightProfile3D.jsx b/components/map3d/HeightProfile3D.jsx
new file mode 100644
index 000000000..4a1c5be2b
--- /dev/null
+++ b/components/map3d/HeightProfile3D.jsx
@@ -0,0 +1,328 @@
+/**
+ * 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 {Line} from "react-chartjs-2";
+import ReactDOM from 'react-dom';
+
+import Shape from '@giro3d/giro3d/entities/Shape.js';
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ Title,
+ Tooltip,
+ Filler
+} from 'chart.js';
+import FileSaver from 'file-saver';
+import PropTypes from 'prop-types';
+import {Vector3} from 'three';
+
+import LocaleUtils from '../../utils/LocaleUtils';
+import MeasureUtils from '../../utils/MeasureUtils';
+import MiscUtils from '../../utils/MiscUtils';
+import ResizeableWindow from '../ResizeableWindow';
+
+import '../../plugins/style/HeightProfile.css';
+
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ Title,
+ Tooltip,
+ Filler
+);
+
+class HeightProfilePrintDialog extends React.PureComponent {
+ static propTypes = {
+ children: PropTypes.func,
+ onClose: PropTypes.func,
+ sceneContext: PropTypes.object,
+ templatePath: PropTypes.string
+ };
+ constructor(props) {
+ super(props);
+ this.externalWindow = null;
+ this.chart = null;
+ this.portalEl = null;
+ this.imageEl = null;
+ }
+ state = {
+ initialized: false,
+ imageUrl: ''
+ };
+ componentDidMount() {
+ const templatePath = MiscUtils.resolveAssetsPath(this.props.templatePath);
+ this.externalWindow = window.open(templatePath, LocaleUtils.tr("heightprofile.title"), "toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes");
+ this.externalWindow.addEventListener('load', this.setWindowContent, false);
+ this.externalWindow.addEventListener('resize', this.windowResized, false);
+ window.addEventListener('beforeunload', this.closePrintWindow);
+ this.props.sceneContext.scene.view.controls.addEventListener('change', this.scheduleRefreshImage);
+ }
+ componentDidUpdate(prevProps, prevState) {
+ if ((this.state.initialized && !prevState.initialized)) {
+ this.refreshImage();
+ }
+ }
+ componentWillUnmount() {
+ this.closePrintWindow();
+ window.removeEventListener('beforeunload', this.closePrintWindow);
+ this.props.sceneContext.scene.view.controls.removeEventListener('change', this.scheduleRefreshImage);
+ }
+ closePrintWindow = () => {
+ this.externalWindow.close();
+ };
+ setWindowContent = () => {
+ this.externalWindow.addEventListener('beforeunload', this.props.onClose, false);
+ const container = this.externalWindow.document.getElementById("heightprofilecontainer");
+ if (container) {
+ const printBtn = this.externalWindow.document.createElement('div');
+ printBtn.id = "print";
+ printBtn.style.marginBottom = "1em";
+ printBtn.innerHTML = '' +
+ '';
+ container.appendChild(printBtn);
+
+ this.imageEl = this.externalWindow.document.createElement('div');
+ this.imageEl.innerHTML = LocaleUtils.tr("heightprofile.loadingimage");
+ container.appendChild(this.imageEl);
+
+ this.portalEl = this.externalWindow.document.createElement('div');
+ this.portalEl.id = 'profile';
+ container.appendChild(this.portalEl);
+
+ this.setState({initialized: true});
+ this.externalWindow.document.body.style.overflowX = 'hidden';
+ } else {
+ this.externalWindow.document.body.innerHTML = "Broken template. An element with id=heightprofilecontainer must exist.";
+ }
+ };
+ scheduleRefreshImage = () => {
+ clearTimeout(this.refreshTimeout);
+ this.refreshTimeout = setTimeout(this.refreshImage, 500);
+ };
+ refreshImage = () => {
+ const src = this.props.sceneContext.scene.renderer.domElement.toDataURL('image/png');
+ const width = this.props.sceneContext.scene.renderer.domElement.offsetWidth;
+ this.imageEl.innerHTML = ``;
+ };
+ windowResized = () => {
+ if (this.chart) {
+ this.chart.resize();
+ }
+ };
+ render() {
+ if (!this.state.initialized) {
+ return null;
+ }
+ return ReactDOM.createPortal(this.props.children(el => {this.chart = el;}, false), this.portalEl);
+ }
+}
+
+
+export default class HeightProfile extends React.Component {
+ static propTypes = {
+ data: PropTypes.array,
+ /** The height of the height profile widget in pixels. */
+ height: PropTypes.number,
+ /** The precision of displayed and exported values (0: no decimals, 1: 1 decimal position, etc). */
+ heightProfilePrecision: PropTypes.number,
+ sceneContext: PropTypes.object,
+ /** Template location for the height profile print functionality */
+ templatePath: PropTypes.string
+ };
+ static defaultProps = {
+ heightProfilePrecision: 0,
+ height: 150,
+ templatePath: ":/templates/heightprofileprint.html"
+ };
+ state = {
+ printdialog: false,
+ visible: true
+ };
+ constructor(props) {
+ super(props);
+ this.chart = null;
+ this.profilePrintWindow = null;
+ }
+ componentDidMount() {
+ this.marker = new Shape({
+ showVertexLabels: true,
+ showLine: false,
+ showVertices: true,
+ vertexLabelFormatter: ({position}) => MeasureUtils.formatMeasurement(position.z, false, 'm')
+ });
+ this.marker.visible = false;
+ this.props.sceneContext.scene.add(this.marker);
+ }
+ componentWillUnmount() {
+ this.props.sceneContext.scene.remove(this.marker);
+ }
+ componentDidUpdate(prevProps) {
+ if (this.props.data !== prevProps.data) {
+ this.setState({visible: true});
+ }
+ }
+ onClose = () => {
+ this.setState({visible: false, printdialog: false});
+ this.marker.visible = false;
+ };
+ render() {
+ if (!this.state.visible) {
+ return null;
+ }
+ const extraControls = [
+ {icon: 'export', callback: this.exportProfile, title: LocaleUtils.tr("heightprofile.export")},
+ {icon: 'print', active: this.state.printdialog, callback: () => this.setState(state => ({printdialog: !state.printdialog})), title: LocaleUtils.tr("heightprofile.print")}
+ ];
+ return [(
+
+ {this.renderHeightProfile((el) => { this.chart = el; }, true)}
+
+ ),
+ this.state.printdialog ? (
+ this.setState({printdialog: false})} sceneContext={this.props.sceneContext} templatePath={this.props.templatePath}>
+ {this.renderHeightProfile}
+
+ ) : null];
+ }
+ renderHeightProfile = (saveRef, interactive) => {
+ const distanceStr = LocaleUtils.tr("heightprofile.distance");
+ const heightStr = LocaleUtils.tr("heightprofile.height");
+ const aslStr = LocaleUtils.tr("heightprofile.asl");
+
+ const data = {
+ labels: this.props.data.map(entry => entry[3]),
+ datasets: [
+ {
+ data: this.props.data.map(entry => entry[2]),
+ fill: true,
+ backgroundColor: "rgba(255,0,0,0.5)",
+ borderColor: "rgb(255,0,0)",
+ borderWidth: 2,
+ pointRadius: 0,
+ order: 1
+ }
+ ]
+ };
+ // Approx 10 ticks
+ const totLength = this.props.data[this.props.data.length - 1][3];
+ const maxHeight = Math.max(...this.props.data.map(x => x[2]));
+ const stepSizeFact = Math.pow(10, Math.ceil(Math.log10(totLength / 10)));
+ const stepSize = Math.round(totLength / (stepSizeFact)) * stepSizeFact / 10;
+ const prec = this.props.heightProfilePrecision;
+ const options = {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: {
+ duration: 0
+ },
+ plugins: {
+ legend: {
+ display: false
+ },
+ tooltip: {
+ enabled: interactive,
+ intersect: false,
+ displayColors: false,
+ bodyFont: {weight: 'bold'},
+ callbacks: {
+ title: (ctx) => (distanceStr + ": " + MeasureUtils.formatMeasurement(ctx[0].parsed.x, false, 'metric')),
+ label: (ctx) => (heightStr + ": " + MeasureUtils.formatMeasurement(ctx.parsed.y, false, 'm') + " " + aslStr)
+ }
+ }
+ },
+ scales: {
+ x: {
+ type: 'linear',
+ ticks: {
+ stepSize: stepSize,
+ font: {size: 10},
+ callback: (value) => value
+ },
+ title: {
+ display: true,
+ text: distanceStr + " [m]",
+ padding: 0
+ },
+ max: Math.ceil(totLength)
+ },
+ y: {
+ ticks: {
+ font: {size: 10},
+ callback: (value) => value.toFixed(prec)
+ },
+ title: {
+ display: true,
+ text: heightStr + " [m " + aslStr + "]"
+ },
+ max: Math.ceil(maxHeight)
+ }
+ },
+ onHover: interactive ? (evt, activeEls, chart) => {
+ const chartArea = chart.chartArea;
+ const chartX = Math.min(Math.max(evt.x - chartArea.left), chartArea.width);
+ this.updateMarker(chartX / chartArea.width * totLength);
+ } : undefined
+ };
+
+ return (
+
+
+
+ );
+ };
+ resizeChart = () => {
+ if (this.chart) {
+ this.chart.resize();
+ }
+ };
+ updateMarker = (dist) => {
+ const data = this.props.data;
+ const i = data.findIndex(x => x[3] >= dist);
+ if (i === 0) {
+ this.marker.setPoints([new Vector3(...data[0])]);
+ } else {
+ const lambda = (dist - data[i - 1][3]) / (data[i][3] - data[i - 1][3]);
+ const p = new Vector3(
+ data[i - 1][0] + lambda * (data[i][0] - data[i][0]),
+ data[i - 1][1] + lambda * (data[i][1] - data[i][1]),
+ data[i - 1][2] + lambda * (data[i][2] - data[i][2])
+ );
+ this.marker.setPoints([p]);
+ }
+ this.marker.visible = true;
+ };
+ hideMarker = () => {
+ this.marker.visible = false;
+ };
+ exportProfile = () => {
+ let csv = "";
+ csv += "index" + "\t" + "distance" + "\t" + "elevation" + "\n";
+ this.props.data.forEach((entry, idx) => {
+ const sample = {x: entry[3], y: entry[2]};
+ const prec = this.props.heightProfilePrecision;
+ const distance = Math.round(sample.x * Math.pow(10, prec)) / Math.pow(10, prec);
+ const height = Math.round(sample.y * Math.pow(10, prec)) / Math.pow(10, prec);
+ csv += String(idx).replace('"', '""') + "\t"
+ + String(distance) + "\t"
+ + String(height) + "\n";
+ });
+ FileSaver.saveAs(new Blob([csv], {type: "text/plain;charset=utf-8"}), "heightprofile.csv");
+ };
+}
diff --git a/components/map3d/Map3D.jsx b/components/map3d/Map3D.jsx
index daa49044b..e49145f33 100644
--- a/components/map3d/Map3D.jsx
+++ b/components/map3d/Map3D.jsx
@@ -32,6 +32,7 @@ import Icon from '../Icon';
import BottomBar3D from './BottomBar3D';
import LayerTree3D from './LayerTree3D';
import Map3DLight from './Map3DLight';
+import Measure3D from './Measure3D';
import OverviewMap3D from './OverviewMap3D';
import TopBar3D from './TopBar3D';
import LayerRegistry from './layers/index';
@@ -337,6 +338,7 @@ class Map3D extends React.Component {
+
) : null}
@@ -360,7 +362,8 @@ class Map3D extends React.Component {
target: this.container,
crs: projection,
renderer: {
- clearColor: 0x000000
+ clearColor: 0x000000,
+ preserveDrawingBuffer: true
}
});
diff --git a/components/map3d/Measure3D.jsx b/components/map3d/Measure3D.jsx
new file mode 100644
index 000000000..2c83e6676
--- /dev/null
+++ b/components/map3d/Measure3D.jsx
@@ -0,0 +1,383 @@
+/**
+ * Copyright 2024 Sourcepole AG
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React from 'react';
+
+import Coordinates from '@giro3d/giro3d/core/geographic/Coordinates';
+import ColorLayer from '@giro3d/giro3d/core/layer/ColorLayer';
+import Shape from '@giro3d/giro3d/entities/Shape';
+import DrawTool, {conditions} from "@giro3d/giro3d/interactions/DrawTool.js";
+import VectorSource from '@giro3d/giro3d/sources/VectorSource';
+import ol from 'openlayers';
+import pointInPolygon from 'point-in-polygon';
+import PropTypes from 'prop-types';
+import {CurvePath, LineCurve, Vector2, Vector3} from 'three';
+
+import ConfigUtils from '../../utils/ConfigUtils';
+import CoordinatesUtils from '../../utils/CoordinatesUtils';
+import LocaleUtils from '../../utils/LocaleUtils';
+import MeasureUtils from '../../utils/MeasureUtils';
+import TaskBar from '../TaskBar';
+import ButtonBar from '../widgets/ButtonBar';
+import CopyButton from '../widgets/CopyButton';
+import HeightProfile3D from './HeightProfile3D';
+
+import '../../plugins/style/Measure.css';
+
+
+export default class Measure3D extends React.Component {
+ static propTypes = {
+ maxSampleCount: PropTypes.number,
+ minMeasureLength: PropTypes.number,
+ sceneContext: PropTypes.object
+ };
+ static defaultProps = {
+ maxSampleCount: 500,
+ minMeasureLength: 5
+ };
+ state = {
+ mode: null,
+ result: null,
+ lenUnit: 'metric',
+ areaUnit: 'metric',
+ elevUnit: 'absolute'
+ };
+ constructor(props) {
+ super(props);
+ this.measureTool = null;
+ this.measurementObjects = [];
+ }
+ componentDidUpdate(prevProps, prevState) {
+ if (this.state.mode !== prevState.mode) {
+ this.clearResult();
+ this.restart();
+ }
+ }
+ onShow = (mode) => {
+ this.setState({mode: mode ?? 'Point'});
+ this.abortController = new AbortController();
+ this.measureTool = new DrawTool({
+ instance: this.props.sceneContext.scene
+ });
+ this.drawLayer = new ColorLayer({
+ source: new VectorSource({
+ data: [],
+ format: new ol.format.GeoJSON(),
+ style: this.featureStyleFunction
+ })
+ });
+ this.props.sceneContext.map.addLayer(this.drawLayer);
+ };
+ onHide = () => {
+ this.clearResult();
+ this.setState({mode: null});
+ this.abortController.abort();
+ this.abortController = null;
+ this.measureTool.dispose();
+ this.measureTool = null;
+ this.props.sceneContext.map.removeLayer(this.drawLayer, {dispose: true});
+ };
+ renderModeSwitcher = () => {
+ const buttons = [
+ {key: "Point", label: LocaleUtils.tr("measureComponent.pointLabel")},
+ {key: "LineString", label: LocaleUtils.tr("measureComponent.lengthLabel")},
+ {key: "Polygon", label: LocaleUtils.tr("measureComponent.areaLabel")}
+ ];
+ return (
+ this.setState({mode, result: null})} />
+ );
+ };
+ renderResult = () => {
+ if (!this.state.result) {
+ return null;
+ }
+ let text = "";
+ let unitSelector = null;
+
+ if (this.state.mode === "Point") {
+ text = CoordinatesUtils.getFormattedCoordinate(this.state.result.pos.slice(0, 2), this.props.sceneContext.mapCrs);
+ if (this.state.result.ground >= 0) {
+ const prec = ConfigUtils.getConfigProp("measurementPrecision");
+ text += ", " + (this.state.elevUnit === 'ground' ? this.state.result.ground : this.state.result.pos[2]).toFixed(prec);
+ unitSelector = (
+
+ );
+ }
+ } else if (this.state.mode === "LineString") {
+ text = MeasureUtils.formatMeasurement(this.state.result.length, false, this.state.lenUnit);
+ unitSelector = (
+
+ );
+ } else if (this.state.mode === "Polygon") {
+ text = MeasureUtils.formatMeasurement(this.state.result, true, this.state.areaUnit);
+ unitSelector = (
+
+ );
+ }
+ return (
+
+
+ {unitSelector}
+
+
+ );
+ };
+ render() {
+ return [
+ (
+
+ {() => ({
+ body: (
+
+ {this.renderModeSwitcher()}
+ {this.renderResult()}
+
+ )
+ })}
+
+ ),
+ this.state.result?.profile ? (
+
+ ) : null
+ ];
+ }
+ featureStyleFunction = () => {
+ return [
+ new ol.style.Style({
+ fill: new ol.style.Fill({
+ color: [41, 120, 180, 0.5]
+ })
+ }),
+ new ol.style.Style({
+ stroke: new ol.style.Stroke({
+ color: [255, 255, 255],
+ width: 4
+ })
+ }),
+ new ol.style.Style({
+ stroke: new ol.style.Stroke({
+ color: [41, 120, 180],
+ width: 1.5
+ })
+ })
+ ];
+ };
+ clearResult = () => {
+ this.drawLayer.source.clear();
+ this.measurementObjects.forEach(object => {
+ this.props.sceneContext.scene.remove(object);
+ });
+ this.setState({result: null});
+ };
+ restart = () => {
+ if (this.abortController) {
+ this.abortController.abort();
+ }
+ this.abortController = new AbortController();
+ const options = {
+ signal: this.abortController.signal,
+ endCondition: conditions.doubleClick
+ };
+ if (this.state.mode === 'Point') {
+ this.measureTool.createPoint(options).then(this.measurePoint);
+ } else if (this.state.mode === 'LineString') {
+ this.measureTool.createLineString(options).then(this.measureLine);
+ } else if (this.state.mode === 'Polygon') {
+ this.measureTool.createPolygon(options).then(this.measureArea);
+ } else if (this.state.mode === 'Height') {
+ this.measureTool.createVerticalMeasure(options).then(this.measureHeight);
+ }
+ };
+ measurePoint = (point) => {
+ if (point === null) {
+ this.restart();
+ return;
+ }
+ this.clearResult();
+ const pos = point.points[0];
+
+ // Measure point above terrain
+ const elevation = this.getElevation([pos.x, pos.y]);
+ if (pos.z - elevation > 0) {
+ const elevationStr = MeasureUtils.formatMeasurement(pos.z - elevation, false, 'm');
+ // Add line
+ const line = new Shape({
+ showLineLabel: true,
+ showLine: true,
+ showVertices: true,
+ lineLabelFormatter: () => elevationStr
+ });
+ line.setPoints([new Vector3(pos.x, pos.y, elevation), pos]);
+ this.props.sceneContext.scene.add(line);
+ this.measurementObjects.push(line);
+ this.props.sceneContext.scene.remove(point);
+ } else {
+ this.measurementObjects.push(point);
+ }
+
+ this.setState({result: {pos: [pos.x, pos.y, pos.z], ground: pos.z - elevation}});
+
+ // Setup for next measurement
+ this.restart();
+ };
+ measureLine = (lineString) => {
+ if (lineString === null) {
+ this.restart();
+ return;
+ }
+ this.clearResult();
+ const features = (new ol.format.GeoJSON()).readFeatures(lineString.toGeoJSON(), {
+ dataProjection: "EPSG:4326",
+ featureProjection: this.props.sceneContext.mapCrs
+ });
+ this.drawLayer.source.addFeatures(features);
+ this.props.sceneContext.scene.remove(lineString);
+
+ // Compute 2d length and nSamples spaced points
+ const path = new CurvePath();
+ let len2d = 0;
+ for (let i = 0; i < lineString.points.length - 1; i++) {
+ const v0 = lineString.points[i];
+ const v1 = lineString.points[i + 1];
+
+ const line = new LineCurve(
+ new Vector2(v0.x, v0.y),
+ new Vector2(v1.x, v1.y),
+ );
+ path.add(line);
+ len2d += Math.sqrt((v1.x - v0.x) * (v1.x - v0.x) + (v1.y - v0.y) * (v1.y - v0.y));
+ }
+ const nSamples = Math.min(this.props.maxSampleCount, Math.round(len2d / this.props.minMeasureLength));
+
+ const points = path.getSpacedPoints(nSamples - 1);
+ const line3d = new Array(nSamples);
+ line3d[0] = [points[0].x, points[0].y, this.getElevation([points[0].x, points[0].y]), 0];
+ let len3d = 0;
+ for (let i = 1; i < nSamples; ++i) {
+ line3d[i] = [points[i].x, points[i].y, this.getElevation([points[i].x, points[i].y]), 0];
+ const dx = line3d[i][0] - line3d[i - 1][0];
+ const dy = line3d[i][1] - line3d[i - 1][1];
+ const dz = line3d[i][2] - line3d[i - 1][2];
+ len3d += Math.sqrt(dx * dx + dy * dy + dz * dz);
+ line3d[i][3] = len3d; // Also store incremental length for height profie
+ }
+ this.setState({result: {length: len3d, profile: line3d}});
+
+ // Setup for next measurement
+ this.restart();
+ };
+ measureArea = (polygon) => {
+ if (polygon === null) {
+ this.restart();
+ return;
+ }
+ this.clearResult();
+
+ const features = (new ol.format.GeoJSON()).readFeatures(polygon.toGeoJSON(), {
+ dataProjection: "EPSG:4326",
+ featureProjection: this.props.sceneContext.mapCrs
+ });
+ this.drawLayer.source.addFeatures(features);
+ this.props.sceneContext.scene.remove(polygon);
+
+ // Compute boundingbox of polygon, divide boundingbox into quads,
+ // compute quad area on terrain for each quad in polygon
+ const bbox = [
+ polygon.points[0].x, polygon.points[0].y,
+ polygon.points[0].x, polygon.points[0].y
+ ];
+ const coordinates = polygon.points.map(v => {
+ bbox[0] = Math.min(bbox[0], v.x);
+ bbox[1] = Math.min(bbox[1], v.y);
+ bbox[2] = Math.max(bbox[2], v.x);
+ bbox[3] = Math.max(bbox[3], v.y);
+ return [v.x, v.y];
+ });
+ const quadSize = this.props.minMeasureLength;
+ const numX = Math.min(this.props.maxSampleCount, Math.round((bbox[2] - bbox[0]) / quadSize));
+ const numY = Math.min(this.props.maxSampleCount, Math.round((bbox[3] - bbox[1]) / quadSize));
+ const deltaX = (bbox[2] - bbox[0]) / numX;
+ const deltaY = (bbox[3] - bbox[1]) / numY;
+ let area = 0;
+ const elevationCache = new Array(numX * numY);
+ for (let iX = 0; iX < numX - 1; ++iX) {
+ for (let iY = 0; iY < numY - 1; ++iY) {
+ // If quad center lies in polygon, consider it
+ const p = [bbox[0] + iX * deltaX, bbox[1] + iY * deltaY];
+ const c = [p[0] + 0.5 * deltaX, p[1] + 0.5 * deltaY];
+ if (!pointInPolygon(c, coordinates)) {
+ continue;
+ }
+ // Get elevations
+ const z1 = elevationCache[iY * numX + iX] ?? (elevationCache[iY * numX + iX] = this.getElevation(p));
+ const z2 = elevationCache[iY * numX + iX + 1] ?? (elevationCache[iY * numX + iX + 1] = this.getElevation([p[0] + deltaX, p[1]]));
+ const z3 = elevationCache[(iY + 1) * numX + iX + 1] ?? (elevationCache[(iY + 1) * numX + iX + 1] = this.getElevation([p[0] + deltaX, p[1] + deltaY]));
+ const z4 = elevationCache[(iY + 1) * numX + iX] ?? (elevationCache[(iY + 1) * numX + iX] = this.getElevation([p[0], p[1] + deltaY]));
+ // Divide quad along diagonal with smaller elevation difference
+ const dz1 = Math.abs(z3 - z1);
+ const dz2 = Math.abs(z4 - z2);
+ if (dz1 < dz2) {
+ const area1 = this.triangleArea([-deltaX, 0, z1 - z2], [0, deltaY, z3 - z2]);
+ const area2 = this.triangleArea([0, -deltaY, z1 - z4], [deltaX, 0, z3 - z4]);
+ area += area1 + area2;
+ } else {
+ const area1 = this.triangleArea([deltaX, 0, z2 - z1], [0, deltaY, z4 - z1]);
+ const area2 = this.triangleArea([-deltaX, 0, z4 - z3], [0, -deltaY, z1 - z3]);
+ area += area1 + area2;
+ }
+ }
+ }
+ this.setState({result: area});
+
+ // Setup for next measurement
+ this.restart();
+ };
+ measureHeight = (lineString) => {
+ if (lineString === null) {
+ this.restart();
+ return;
+ }
+ this.clearResult();
+ this.measurementObjects.push(lineString);
+
+ // Setup for next measurement
+ this.restart();
+ };
+ getElevation = (point) => {
+ const coordinates = new Coordinates(this.props.sceneContext.mapCrs, point[0], point[1], 0);
+ const result = this.props.sceneContext.map.getElevation({coordinates});
+ if (result.samples.length > 0) {
+ result.samples.sort((a, b) => a.resolution - b.resolution);
+ return result.samples[0].elevation;
+ }
+ return null;
+ };
+ triangleArea = (u, v) => {
+ const cross = [u[1] * v[2] - u[2] * v[1], u[0] * v[2] - u[2] * v[0], u[0] * v[1] - u[1] * v[0]];
+ return 0.5 * Math.sqrt(cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]);
+ };
+}
diff --git a/package.json b/package.json
index 1f77ef841..10cd4dcdc 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,12 @@
{
"name": "qwc2",
- "version": "2024.12.20-master",
+ "version": "2025.01.02-master",
"description": "QGIS Web Client 2 core",
"author": "Sourcepole AG",
"license": "BSD-2-Clause",
"repository": "git@github.com:qgis/qwc2.git",
"dependencies": {
- "@giro3d/giro3d": "^0.40.0",
+ "@giro3d/giro3d": "^0.41.0",
"@ladjs/country-language": "^1.0.3",
"@loaders.gl/core": "^4.3.3",
"@loaders.gl/shapefile": "^4.3.3",