diff --git a/build/docma-config.json b/build/docma-config.json
index 3c92e3cd811..2df269a2ec2 100644
--- a/build/docma-config.json
+++ b/build/docma-config.json
@@ -235,6 +235,7 @@
"web/client/plugins/BackgroundSelector.jsx",
"web/client/plugins/BurgerMenu.jsx",
"web/client/plugins/CRSSelector.jsx",
+ "web/client/plugins/CameraPosition.jsx",
"web/client/plugins/Context.jsx",
"web/client/plugins/ContextCreator.jsx",
"web/client/plugins/ContextExport.jsx",
@@ -346,4 +347,4 @@
]
}
]
-}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 5eb40f65465..ebcab8ebccf 100644
--- a/package.json
+++ b/package.json
@@ -113,6 +113,7 @@
"@mapbox/geojsonhint": "3.3.0",
"@mapbox/togeojson": "0.16.2",
"@mapstore/patcher": "https://github.com/geosolutions-it/Patcher/tarball/master",
+ "@math.gl/geoid": "^4.1.0",
"@turf/along": "6.5.0",
"@turf/area": "6.5.0",
"@turf/bbox": "4.1.0",
diff --git a/web/client/components/mapcontrols/mouseposition/CRSSelector.jsx b/web/client/components/mapcontrols/mouseposition/CRSSelector.jsx
index c2ee37eee46..39d79e4df04 100644
--- a/web/client/components/mapcontrols/mouseposition/CRSSelector.jsx
+++ b/web/client/components/mapcontrols/mouseposition/CRSSelector.jsx
@@ -7,88 +7,124 @@
*/
import PropTypes from 'prop-types';
-import React from 'react';
+import React, { useRef } from 'react';
import { ControlLabel, FormControl, FormGroup } from 'react-bootstrap';
import ReactDOM from 'react-dom';
-import {filterCRSList, getAvailableCRS} from '../../../utils/CoordinatesUtils';
+import { filterCRSList, getAvailableCRS } from '../../../utils/CoordinatesUtils';
+import FlexBox from '../../layout/FlexBox';
-class CRSSelector extends React.Component {
- static propTypes = {
- id: PropTypes.string,
- label: PropTypes.oneOfType([PropTypes.func, PropTypes.string, PropTypes.object]),
- availableCRS: PropTypes.object,
- filterAllowedCRS: PropTypes.array,
- projectionDefs: PropTypes.array,
- additionalCRS: PropTypes.object,
- crs: PropTypes.string,
- enabled: PropTypes.bool,
- onCRSChange: PropTypes.func,
- useRawInput: PropTypes.bool
- };
+/**
+ * CRSSelector allows to select a crs from a combobox or using a raw input.
+ * @memberof components.mousePosition
+ * @class
+ * @prop {string} id the id of the component
+ * @prop {string|object|function} label the label shown next to the combobox (if editCRS is true)
+ * @prop {object} availableCRS list of available crs to be used in the combobox
+ * @prop {string[]} filterAllowedCRS list of allowed crs in the combobox list
+ * @prop {object[]} projectionDefs list of additional project definitions
+ * @prop {object} additionalCRS additional crs to be added to the list
+ * @prop {string} crs the current selected crs
+ * @prop {boolean} enabled if true shows the component
+ * @prop {function} onCRSChange callback when a new crs is selected
+ * @prop {boolean} useRawInput if true shows a raw input instead of a combobox
+ */
- static defaultProps = {
- id: "mapstore-crsselector",
- availableCRS: getAvailableCRS(),
- crs: null,
- onCRSChange: function() {},
- enabled: false,
- useRawInput: false
- };
+const CRSSelector = (props) => {
+ const {
+ id,
+ label,
+ availableCRS,
+ filterAllowedCRS,
+ projectionDefs,
+ additionalCRS,
+ crs,
+ enabled,
+ onCRSChange,
+ useRawInput
+ } = props;
- render() {
- var val;
- var label;
- var list = [];
- let availableCRS = {};
- if (Object.keys(this.props.availableCRS).length) {
- availableCRS = filterCRSList(this.props.availableCRS, this.props.filterAllowedCRS, this.props.additionalCRS, this.props.projectionDefs );
- }
- for (let crs in availableCRS) {
- if (availableCRS.hasOwnProperty(crs)) {
- val = crs;
- label = availableCRS[crs].label;
- list.push();
- }
- }
+ const formRef = useRef(null);
- if (this.props.enabled && this.props.useRawInput) {
- return (
- );
- } else if (this.props.enabled && !this.props.useRawInput) {
- return (
-
- {this.props.label}
-
- {list}
-
- );
+ const launchNewCRSAction = (ev) => {
+ if (useRawInput) {
+ onCRSChange(ev.target.value);
+ } else {
+ const element = ReactDOM.findDOMNode(formRef.current);
+ const selectNode = element.getElementsByTagName('select').item(0);
+ onCRSChange(selectNode.value);
}
+ };
+
+ if (!enabled) {
return null;
}
- launchNewCRSAction = (ev) => {
- if (this.props.useRawInput) {
- this.props.onCRSChange(ev.target.value);
- } else {
- let element = ReactDOM.findDOMNode(this);
- let selectNode = element.getElementsByTagName('select').item(0);
- this.props.onCRSChange(selectNode.value);
- }
- };
-}
+ const filteredCRS = Object.keys(availableCRS).length
+ ? filterCRSList(availableCRS, filterAllowedCRS, additionalCRS, projectionDefs)
+ : {};
+
+ const options = Object.entries(filteredCRS).map(([crsKey, crsValue]) => (
+
+ ));
+
+ if (useRawInput) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {label}
+
+
+ {options}
+
+
+ );
+};
+
+CRSSelector.propTypes = {
+ id: PropTypes.string,
+ label: PropTypes.oneOfType([PropTypes.func, PropTypes.string, PropTypes.object]),
+ availableCRS: PropTypes.object,
+ filterAllowedCRS: PropTypes.array,
+ projectionDefs: PropTypes.array,
+ additionalCRS: PropTypes.object,
+ crs: PropTypes.string,
+ enabled: PropTypes.bool,
+ onCRSChange: PropTypes.func,
+ useRawInput: PropTypes.bool
+};
+
+CRSSelector.defaultProps = {
+ id: "mapstore-crsselector",
+ availableCRS: getAvailableCRS(),
+ crs: null,
+ onCRSChange: function() {},
+ enabled: false,
+ useRawInput: false
+};
export default CRSSelector;
diff --git a/web/client/components/mapcontrols/mouseposition/HeightTypeSelector.jsx b/web/client/components/mapcontrols/mouseposition/HeightTypeSelector.jsx
new file mode 100644
index 00000000000..679fc647687
--- /dev/null
+++ b/web/client/components/mapcontrols/mouseposition/HeightTypeSelector.jsx
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2025, GeoSolutions Sas.
+ * 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 PropTypes from "prop-types";
+import React from "react";
+import { ControlLabel, FormControl, FormGroup } from "react-bootstrap";
+import FlexBox from "../../layout/FlexBox";
+import { getMessageById } from "../../../utils/LocaleUtils";
+
+/**
+ * HeightTypeSelector allows to select a height type from a combobox.
+ * @memberof components.mousePosition
+ * @class
+ * @prop {string} id the id of the component
+ * @prop {string|object|function} label the label shown next to the combobox (if editHeight is true)
+ * @prop {string[]} filterAllowedHeight list of allowed height type in the combobox list. Accepted values are "Ellipsoidal" and "MSL"
+ * @prop {string} heightType the current selected height type
+ * @prop {boolean} enabled if true shows the component
+ * @prop {function} onHeightTypeChange callback when a new height type is selected
+ */
+
+const HeightTypeSelector = (props, context) => {
+ const {
+ id,
+ label,
+ availableHeightTypes,
+ heightType,
+ enabled,
+ onHeightTypeChange
+ } = props;
+
+ if (!enabled) {
+ return null;
+ }
+
+ const options = availableHeightTypes.map(({ value, labelId }) => (
+
+ ));
+
+ return (
+
+
+ {label}
+
+ onHeightTypeChange(e.target.value)}
+ bsSize="small"
+ style={{ borderRadius: 4 }}
+ >
+ {options}
+
+
+ );
+};
+
+HeightTypeSelector.propTypes = {
+ id: PropTypes.string,
+ label: PropTypes.oneOfType([PropTypes.func, PropTypes.string, PropTypes.object]),
+ filterAllowedHeight: PropTypes.array,
+ heightType: PropTypes.string,
+ enabled: PropTypes.bool,
+ onHeightTypeChange: PropTypes.func
+};
+
+HeightTypeSelector.contextTypes = {
+ messages: PropTypes.object
+};
+
+HeightTypeSelector.defaultProps = {
+ id: "mapstore-heightselector",
+ heightType: null,
+ onHeightTypeChange: function() {},
+ enabled: false
+};
+
+export default HeightTypeSelector;
diff --git a/web/client/components/mapcontrols/mouseposition/MousePosition.jsx b/web/client/components/mapcontrols/mouseposition/MousePosition.jsx
index 6854073ad54..ff8eef060e3 100644
--- a/web/client/components/mapcontrols/mouseposition/MousePosition.jsx
+++ b/web/client/components/mapcontrols/mouseposition/MousePosition.jsx
@@ -9,16 +9,21 @@ import PropTypes from 'prop-types';
import React from 'react';
import proj4js from 'proj4';
-import { Glyphicon, Label } from 'react-bootstrap';
+import { Glyphicon } from 'react-bootstrap';
import CopyToClipboard from 'react-copy-to-clipboard';
import { reproject, getUnits } from '../../../utils/CoordinatesUtils';
import MousePositionLabelDMS from './MousePositionLabelDMS';
import MousePositionLabelYX from './MousePositionLabelYX';
import CRSSelector from './CRSSelector';
+import HeightTypeSelector from './HeightTypeSelector';
import Message from '../../I18N/Message';
import { isNumber } from 'lodash';
import Button from '../../misc/Button';
+
+import FlexBox from '../../layout/FlexBox';
+import Text from '../../layout/Text';
+
import './mousePosition.css';
/**
* MousePosition is a component that shows the coordinate of the mouse position in a selected crs.
@@ -27,115 +32,168 @@ import './mousePosition.css';
* @prop {boolean} showElevation shows elevation in addition to planar coordinates (requires a WMS layer with useElevation: true to be configured in the map)
* @prop {function} elevationTemplate custom template to show the elevation if showElevation is true (default template shows the elevation number with no formatting)
* @prop {string[]} filterAllowedCRS list of allowed crs in the combobox list
+ * @prop {string[]} filterAllowedHeight list of allowed height type in the combobox list. Accepted values are "Ellipsoidal" and "MSL"
* @prop {object[]} projectionDefs list of additional project definitions
* @prop {object} additionalCRS additional crs to be added to the list
*/
-class MousePosition extends React.Component {
- static propTypes = {
- id: PropTypes.string,
- mousePosition: PropTypes.object,
- crs: PropTypes.string,
- enabled: PropTypes.bool,
- showCRS: PropTypes.bool,
- editCRS: PropTypes.bool,
- filterAllowedCRS: PropTypes.array,
- projectionDefs: PropTypes.array,
- additionalCRS: PropTypes.object,
- degreesTemplate: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
- projectedTemplate: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
- crsTemplate: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
- elevationTemplate: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
- style: PropTypes.object,
- copyToClipboardEnabled: PropTypes.bool,
- glyphicon: PropTypes.string,
- btnSize: PropTypes.oneOf(["large", "medium", "small", "xsmall"]),
- onCopy: PropTypes.func,
- onCRSChange: PropTypes.func,
- toggle: PropTypes.object,
- showLabels: PropTypes.bool,
- showToggle: PropTypes.bool,
- showElevation: PropTypes.bool
- };
-
- static defaultProps = {
- id: "mapstore-mouseposition",
- mousePosition: null,
- crs: "EPSG:4326",
- enabled: true,
- showCRS: false,
- editCRS: false,
- degreesTemplate: MousePositionLabelDMS,
- projectedTemplate: MousePositionLabelYX,
- crsTemplate: crs => {crs},
- elevationTemplate: elevation => isNumber(elevation) ? {elevation} m : ,
- style: {},
- copyToClipboardEnabled: false,
- glyphicon: "paste",
- btnSize: "xsmall",
- onCopy: () => {},
- onCRSChange: function() {},
- toggle:
,
- showLabels: false,
- showToggle: false,
- showElevation: false
- };
+const MousePosition = (props) => {
+ const {
+ id,
+ mousePosition,
+ crs,
+ heightType,
+ enabled,
+ showCRS,
+ editCRS,
+ editHeight,
+ degreesTemplate,
+ projectedTemplate,
+ crsTemplate,
+ elevationTemplate,
+ style,
+ copyToClipboardEnabled,
+ glyphicon,
+ btnSize,
+ onCopy,
+ onCRSChange,
+ onHeightTypeChange,
+ toggle,
+ showLabels,
+ showToggle,
+ showElevation,
+ crsId,
+ heightId,
+ projectionDefs,
+ filterAllowedCRS,
+ additionalCRS,
+ availableHeightTypes,
+ additionalHeight
+ } = props;
- getPosition = () => {
- let {x, y, z} = this.props.mousePosition ? this.props.mousePosition : [null, null];
- if (!x && !y) {
+ const getPosition = () => {
+ let {x, y, z} = mousePosition ? mousePosition : [null, null];
+ if (!x && !y && !z) {
// if we repoject null coordinates we can end up with -0.00 instead of 0.00
- ({x, y} = {x: 0, y: 0, z});
- } else if (proj4js.defs(this.props.mousePosition.crs) !== proj4js.defs(this.props.crs)) {
- ({x, y} = reproject([x, y], this.props.mousePosition.crs, this.props.crs));
+ return {x: 0, y: 0, z};
+ } else if (proj4js.defs(mousePosition.crs) !== proj4js.defs(crs)) {
+ const reprojected = reproject([x, y], mousePosition.crs, crs);
+ return {x: reprojected.x, y: reprojected.y, z};
}
- let units = getUnits(this.props.crs);
+ let units = getUnits(crs);
if (units === "degrees") {
return {lat: y, lng: x, z};
}
return {x, y, z};
};
- getTemplateComponent = () => {
- return getUnits(this.props.crs) === "degrees" ? this.props.degreesTemplate : this.props.projectedTemplate;
+ const getTemplateComponent = () => {
+ return getUnits(crs) === "degrees" ? degreesTemplate : projectedTemplate;
};
- render() {
- let Template = this.props.mousePosition ? this.getTemplateComponent() : null;
- if (this.props.enabled) {
- const position = this.getPosition();
- return (
-
-
- {this.props.showLabels ? : null}
- {Template ? :
-
-
-
- }
-
- {this.props.copyToClipboardEnabled &&
-
-
-
- }
- {this.props.showElevation ?
- {this.props.showLabels ? : null}
- {this.props.elevationTemplate(position.z)}
- : null}
- {this.props.showCRS ? this.props.crsTemplate(this.props.crs) : null}
- {this.props.editCRS ?
- : null}
- crs={this.props.crs} enabled onCRSChange={this.props.onCRSChange}/> : null}
- {this.props.showToggle ? this.props.toggle : null}
-
- );
- }
- return this.props.showToggle ? {this.props.toggle}
: null;
+ if (!enabled) {
+ return showToggle ? {toggle}
: null;
}
-}
+
+ const Template = mousePosition ? getTemplateComponent() : null;
+ const position = mousePosition ? getPosition() : null;
+
+ return (
+
+
+ {showLabels ? : null}
+
+ {Template ? : '...'}
+ {showElevation && position ? elevationTemplate(position.z) : null}
+ {showCRS ? crsTemplate(crs) : null}
+ {copyToClipboardEnabled && position ?
+
+
+
+ : null}
+
+ {editCRS ?
+ : null}
+ crs={crs} enabled onCRSChange={onCRSChange}
+ /> : null}
+ {editHeight ?
+ : null}
+ heightType={heightType} enabled onHeightTypeChange={onHeightTypeChange}
+ /> : null}
+ {showToggle ? toggle : null}
+
+
+ );
+};
+
+MousePosition.propTypes = {
+ crsId: PropTypes.string,
+ heightId: PropTypes.string,
+ id: PropTypes.string,
+ mousePosition: PropTypes.object,
+ crs: PropTypes.string,
+ heightType: PropTypes.string,
+ enabled: PropTypes.bool,
+ showCRS: PropTypes.bool,
+ editCRS: PropTypes.bool,
+ editHeight: PropTypes.bool,
+ filterAllowedCRS: PropTypes.array,
+ projectionDefs: PropTypes.array,
+ filterAllowedHeight: PropTypes.array,
+ additionalCRS: PropTypes.object,
+ degreesTemplate: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
+ projectedTemplate: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
+ crsTemplate: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
+ elevationTemplate: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
+ style: PropTypes.object,
+ copyToClipboardEnabled: PropTypes.bool,
+ glyphicon: PropTypes.string,
+ btnSize: PropTypes.oneOf(["large", "medium", "small", "xsmall"]),
+ onCopy: PropTypes.func,
+ onCRSChange: PropTypes.func,
+ onHeightTypeChange: PropTypes.func,
+ toggle: PropTypes.object,
+ showLabels: PropTypes.bool,
+ showToggle: PropTypes.bool,
+ showElevation: PropTypes.bool
+};
+
+MousePosition.defaultProps = {
+ crsId: "mapstore-crsselector",
+ id: "mapstore-mouseposition",
+ heightId: "mapstore-heightselector",
+ mousePosition: null,
+ crs: "EPSG:4326",
+ heightType: "Ellipsoidal",
+ enabled: true,
+ showCRS: false,
+ editCRS: false,
+ editHeight: false,
+ degreesTemplate: MousePositionLabelDMS,
+ projectedTemplate: MousePositionLabelYX,
+ crsTemplate: crs => {crs},
+ elevationTemplate: elevation => isNumber(elevation) ? `Alt: ${elevation} m` : '',
+ style: {},
+ copyToClipboardEnabled: false,
+ glyphicon: "paste",
+ btnSize: "xsmall",
+ onCopy: () => {},
+ onCRSChange: function() {},
+ onHeightTypeChange: function() {},
+ toggle: ,
+ showLabels: false,
+ showToggle: false,
+ showElevation: false
+};
export default MousePosition;
diff --git a/web/client/components/mapcontrols/mouseposition/MousePositionLabelDD.jsx b/web/client/components/mapcontrols/mouseposition/MousePositionLabelDD.jsx
index 1ac7c7a97e6..1aab1fba287 100644
--- a/web/client/components/mapcontrols/mouseposition/MousePositionLabelDD.jsx
+++ b/web/client/components/mapcontrols/mouseposition/MousePositionLabelDD.jsx
@@ -1,4 +1,3 @@
-
/**
* Copyright 2015, GeoSolutions Sas.
* All rights reserved.
@@ -9,31 +8,32 @@
import PropTypes from 'prop-types';
import React from 'react';
-import {Label} from 'react-bootstrap';
import NumberFormat from '../../I18N/Number';
-class MousePositionLabelDD extends React.Component {
- static propTypes = {
- position: PropTypes.shape({
- lng: PropTypes.number,
- lat: PropTypes.number
- })
- };
+const MousePositionLabelDD = ({ position }) => {
+ const integerFormat = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 6, minimumFractionDigits: 6};
+ const lngDFormat = {style: "decimal", minimumIntegerDigits: 3, maximumFractionDigits: 6, minimumFractionDigits: 6};
+
+ return (
+ <>
+
+ {"Lat: "}
+
+
+
+ {"Lng: "}
+
+
+ >
+ );
+};
- render() {
- let integerFormat = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 6, minimumFractionDigits: 6};
- let lngDFormat = {style: "decimal", minimumIntegerDigits: 3, maximumFractionDigits: 6, minimumFractionDigits: 6};
- return (
-
-
-
);
- }
-}
+MousePositionLabelDD.propTypes = {
+ position: PropTypes.shape({
+ lng: PropTypes.number,
+ lat: PropTypes.number
+ })
+};
export default MousePositionLabelDD;
diff --git a/web/client/components/mapcontrols/mouseposition/MousePositionLabelDM.jsx b/web/client/components/mapcontrols/mouseposition/MousePositionLabelDM.jsx
index 345300ce84e..d9e572878a7 100644
--- a/web/client/components/mapcontrols/mouseposition/MousePositionLabelDM.jsx
+++ b/web/client/components/mapcontrols/mouseposition/MousePositionLabelDM.jsx
@@ -9,22 +9,14 @@
import { isNil } from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
-import {Label} from 'react-bootstrap';
import { roundCoord } from '../../../utils/CoordinatesUtils';
import NumberFormat from '../../I18N/Number';
-class MousePositionLabelDM extends React.Component {
- static propTypes = {
- position: PropTypes.shape({
- lng: PropTypes.number,
- lat: PropTypes.number
- })
- };
-
- getPositionValues = (mPos) => {
- let {lng, lat} = mPos ? mPos : [null, null];
- let [latM, lngM] = [lat % 1 * 60, lng % 1 * 60];
+const MousePositionLabelDM = ({ position }) => {
+ const getPositionValues = (mPos) => {
+ const {lng, lat} = mPos ? mPos : [null, null];
+ const [latM, lngM] = [lat % 1 * 60, lng % 1 * 60];
return {
lat,
latM: Math.abs(latM),
@@ -33,23 +25,26 @@ class MousePositionLabelDM extends React.Component {
};
};
- render() {
- let pos = this.getPositionValues(this.props.position);
- let integerFormat = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 0};
- let decimalFormat = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 3, minimumFractionDigits: 3};
- let lngDFormat = {style: "decimal", minimumIntegerDigits: 3, maximumFractionDigits: 0};
- return (
-
-
-
);
- }
-}
+ const pos = getPositionValues(position);
+ const integerFormat = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 0};
+ const decimalFormat = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 3, minimumFractionDigits: 3};
+ const lngDFormat = {style: "decimal", minimumIntegerDigits: 3, maximumFractionDigits: 0};
+
+ return (
+ <>
+ Lat: {"° "} {"' "}
+
+ Lng: {"° "} {"' "}
+
+ >
+ );
+};
+
+MousePositionLabelDM.propTypes = {
+ position: PropTypes.shape({
+ lng: PropTypes.number,
+ lat: PropTypes.number
+ })
+};
export default MousePositionLabelDM;
diff --git a/web/client/components/mapcontrols/mouseposition/MousePositionLabelDMS.jsx b/web/client/components/mapcontrols/mouseposition/MousePositionLabelDMS.jsx
index df0f86920e8..9010d3e43f6 100644
--- a/web/client/components/mapcontrols/mouseposition/MousePositionLabelDMS.jsx
+++ b/web/client/components/mapcontrols/mouseposition/MousePositionLabelDMS.jsx
@@ -8,55 +8,68 @@
import PropTypes from 'prop-types';
import React from 'react';
-import {Label} from 'react-bootstrap';
-
import { roundCoord } from '../../../utils/CoordinatesUtils';
import NumberFormat from '../../I18N/Number';
-class MousePositionLabelDMS extends React.Component {
- static propTypes = {
- position: PropTypes.shape({
- lng: PropTypes.number,
- lat: PropTypes.number
- })
- };
-
- getPositionValues = (mPos) => {
- let {lng, lat} = mPos ? mPos : [null, null];
- let [latM, lngM] = [lat % 1 * 60, lng % 1 * 60];
- let [latS, lngS] = [latM % 1 * 60, lngM % 1 * 60];
+const MousePositionLabelDMS = ({ position }) => {
+ const getPositionValues = (mPos) => {
+ const {lng, lat} = mPos || {};
+ // If lat or lng are not valid numbers, use 0
+ const validLat = (lat !== undefined && lat !== null && !isNaN(lat)) ? lat : 0;
+ const validLng = (lng !== undefined && lng !== null && !isNaN(lng)) ? lng : 0;
+ const latM = validLat % 1 * 60;
+ const lngM = validLng % 1 * 60;
+ const latS = latM % 1 * 60;
+ const lngS = lngM % 1 * 60;
return {
- lat: Math.trunc(lat),
+ lat: Math.trunc(validLat),
latM: Math.abs(latM),
latS: Math.abs(latS),
- lng: Math.trunc(lng),
+ lng: Math.trunc(validLng),
lngM: Math.abs(lngM),
lngS: Math.abs(lngS)
};
};
- render() {
- const {lng, lat} = this.props.position || {};
- let pos = this.getPositionValues(this.props.position);
- let lgnSign = lng < 0 ? "-" : "";
- let latSign = lat < 0 ? "-" : "";
- let integerFormat = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 0};
- let decimalFormat = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 2, minimumFractionDigits: 2};
- let lngDFormat = {style: "decimal", minimumIntegerDigits: 3, maximumFractionDigits: 0};
- return (
-
-
-
);
- }
-}
+ const {lng, lat} = position || {};
+ const pos = getPositionValues(position);
+ const lgnSign = lng < 0 ? "-" : "";
+ const latSign = lat < 0 ? "-" : "";
+ const integerFormat = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 0};
+ const decimalFormat = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 2, minimumFractionDigits: 2};
+ const lngDFormat = {style: "decimal", minimumIntegerDigits: 3, maximumFractionDigits: 0};
+
+ return (
+ <>
+
+ {"Lat: "}
+ {latSign ? `${latSign} ` : null}
+
+ {"° "}
+
+ {"' "}
+
+ {"''"}
+
+
+ {"Lng: "}
+ {lgnSign ? `${lgnSign} ` : null}
+
+ {"° "}
+
+ {"' "}
+
+ {"''"}
+
+ >
+ );
+};
+
+MousePositionLabelDMS.propTypes = {
+ position: PropTypes.shape({
+ lng: PropTypes.number,
+ lat: PropTypes.number
+ })
+};
export default MousePositionLabelDMS;
diff --git a/web/client/components/mapcontrols/mouseposition/MousePositionLabelDMSNW.jsx b/web/client/components/mapcontrols/mouseposition/MousePositionLabelDMSNW.jsx
index 2b4ec1f73a2..90df6de0b7b 100644
--- a/web/client/components/mapcontrols/mouseposition/MousePositionLabelDMSNW.jsx
+++ b/web/client/components/mapcontrols/mouseposition/MousePositionLabelDMSNW.jsx
@@ -8,23 +8,16 @@
import PropTypes from 'prop-types';
import React from 'react';
-import {Label} from 'react-bootstrap';
import { roundCoord } from '../../../utils/CoordinatesUtils';
import NumberFormat from '../../I18N/Number';
-class MousePositionLabelDMSNW extends React.Component {
- static propTypes = {
- position: PropTypes.shape({
- lng: PropTypes.number,
- lat: PropTypes.number
- })
- };
-
- getPositionValues = (mPos) => {
- let {lng, lat} = mPos ? mPos : [null, null];
- let [latM, lngM] = [lat % 1 * 60, lng % 1 * 60];
- let [latS, lngS] = [latM % 1 * 60, lngM % 1 * 60];
+const MousePositionLabelDMSNW = ({ position }) => {
+ // Helper function to calculate position values
+ const getPositionValues = (mPos) => {
+ const {lng, lat} = mPos ? mPos : [null, null];
+ const [latM, lngM] = [lat % 1 * 60, lng % 1 * 60];
+ const [latS, lngS] = [latM % 1 * 60, lngM % 1 * 60];
return {
lat,
latM: Math.abs(latM),
@@ -35,23 +28,26 @@ class MousePositionLabelDMSNW extends React.Component {
};
};
- render() {
- let pos = this.getPositionValues(this.props.position);
- let integerFormat = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 0};
- let decimalFormat = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 2, minimumFractionDigits: 2};
- let lngDFormat = {style: "decimal", minimumIntegerDigits: 3, maximumFractionDigits: 0};
- return (
-
-
-
);
- }
-}
+ const pos = getPositionValues(position);
+ const integerFormat = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 0};
+ const decimalFormat = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 2, minimumFractionDigits: 2};
+ const lngDFormat = {style: "decimal", minimumIntegerDigits: 3, maximumFractionDigits: 0};
+
+ return (
+ <>
+ Lat: {"° "} {"' "} {"''"} {pos.lat > 0 ? "N" : "S"}
+
+ Lng: {"° "} {"' "} {"''"} {pos.lng > 0 ? "E" : "W"}
+
+ >
+ );
+};
+
+MousePositionLabelDMSNW.propTypes = {
+ position: PropTypes.shape({
+ lng: PropTypes.number,
+ lat: PropTypes.number
+ })
+};
export default MousePositionLabelDMSNW;
diff --git a/web/client/components/mapcontrols/mouseposition/MousePositionLabelYX.jsx b/web/client/components/mapcontrols/mouseposition/MousePositionLabelYX.jsx
index eae86eed039..7e7b57b5f7f 100644
--- a/web/client/components/mapcontrols/mouseposition/MousePositionLabelYX.jsx
+++ b/web/client/components/mapcontrols/mouseposition/MousePositionLabelYX.jsx
@@ -1,4 +1,3 @@
-
/**
* Copyright 2015, GeoSolutions Sas.
* All rights reserved.
@@ -9,29 +8,30 @@
import PropTypes from 'prop-types';
import React from 'react';
-import {Label} from 'react-bootstrap';
-
import NumberFormat from '../../I18N/Number';
-class MousePositionLabelYX extends React.Component {
- static propTypes = {
- position: PropTypes.shape({
- x: PropTypes.number,
- y: PropTypes.number
- })
- };
+const MousePositionLabelYX = ({ position }) => {
+ const format = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 2, minimumFractionDigits: 2};
+
+ return (
+ <>
+
+ {"X: "}
+
+
+
+ {"Y: "}
+
+
+ >
+ );
+};
- render() {
- let format = {style: "decimal", minimumIntegerDigits: 2, maximumFractionDigits: 2, minimumFractionDigits: 2};
- return (
-
-
-
);
- }
-}
+MousePositionLabelYX.propTypes = {
+ position: PropTypes.shape({
+ x: PropTypes.number,
+ y: PropTypes.number
+ })
+};
export default MousePositionLabelYX;
diff --git a/web/client/components/mapcontrols/mouseposition/__tests__/CRSSelector-test.js b/web/client/components/mapcontrols/mouseposition/__tests__/CRSSelector-test.js
index f9f14f63ac8..32fb0591740 100644
--- a/web/client/components/mapcontrols/mouseposition/__tests__/CRSSelector-test.js
+++ b/web/client/components/mapcontrols/mouseposition/__tests__/CRSSelector-test.js
@@ -33,10 +33,8 @@ describe('CRSSelector', () => {
it('checks default', () => {
- const cmp = ReactDOM.render(, document.getElementById("container"));
- expect(cmp).toExist();
-
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ ReactDOM.render(, document.getElementById("container"));
+ const cmpDom = document.getElementById("container");
expect(cmpDom).toExist();
const select = cmpDom.getElementsByTagName("select").item(0);
@@ -47,8 +45,8 @@ describe('CRSSelector', () => {
it('checks if a change of the combo fires the proper action', () => {
let newCRS;
- const cmp = ReactDOM.render( {newCRS = crs; }}/>, document.getElementById("container"));
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ ReactDOM.render( {newCRS = crs; }}/>, document.getElementById("container"));
+ const cmpDom = document.getElementById("container");
const select = cmpDom.getElementsByTagName("select").item(0);
select.value = "EPSG:4326";
diff --git a/web/client/components/mapcontrols/mouseposition/__tests__/MousePosition-test.js b/web/client/components/mapcontrols/mouseposition/__tests__/MousePosition-test.js
index c8665d47031..edab44829bd 100644
--- a/web/client/components/mapcontrols/mouseposition/__tests__/MousePosition-test.js
+++ b/web/client/components/mapcontrols/mouseposition/__tests__/MousePosition-test.js
@@ -25,12 +25,12 @@ describe('MousePosition', () => {
});
it('checks enabled', () => {
- const cmp = ReactDOM.render(, document.getElementById("container"));
+ ReactDOM.render(, document.getElementById("container"));
+ const cmp = document.getElementById("container");
expect(cmp).toExist();
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = cmp.querySelector('#mouse-position');
expect(cmpDom).toExist();
- expect(cmpDom.id).toExist();
// checking that the copy to clipboard button don't exists
const buttons = cmpDom.getElementsByTagName('button');
@@ -39,56 +39,64 @@ describe('MousePosition', () => {
});
it('checks disabled', () => {
- const cmp = ReactDOM.render(, document.getElementById("container"));
+ ReactDOM.render(, document.getElementById("container"));
+ const cmp = document.getElementById("container");
expect(cmp).toExist();
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = cmp.querySelector('#mouse-position');
expect(cmpDom).toNotExist();
});
it('checks no position', () => {
- const cmp = ReactDOM.render(, document.getElementById("container"));
+ ReactDOM.render(, document.getElementById("container"));
+ const cmp = document.getElementById("container");
expect(cmp).toExist();
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = cmp.querySelector('#mouse-position');
expect(cmpDom).toExist();
expect(cmpDom.innerText.indexOf('...') !== -1).toBe(true);
});
it('checks no elevation', () => {
- const cmp = ReactDOM.render(, document.getElementById("container"));
+ ReactDOM.render(, document.getElementById("container"));
+ const cmp = document.getElementById("container");
expect(cmp).toExist();
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = cmp.querySelector('#mouse-position');
expect(cmpDom).toExist();
expect(cmpDom.getElementsByClassName('mapstore-mouse-elevation').length).toBe(0);
});
it('checks elevation enabled', () => {
- const cmp = ReactDOM.render(, document.getElementById("container"));
+ ReactDOM.render(, document.getElementById("container"));
+ const cmp = document.getElementById("container");
expect(cmp).toExist();
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = cmp.querySelector('#mouse-position');
expect(cmpDom).toExist();
- expect(cmpDom.getElementsByClassName('mapstore-mouse-elevation').length).toBe(1);
- expect(cmpDom.innerHTML).toContain('13');
+
+ expect(cmpDom.innerHTML).toContain('Alt:');
+ expect(cmpDom.innerHTML).toContain('13 m');
});
it('checks default templates degrees', () => {
- const cmp = ReactDOM.render(, document.getElementById("container"));
+ ReactDOM.render(, document.getElementById("container"));
+ const cmp = document.getElementById("container");
expect(cmp).toExist();
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = cmp.querySelector('#mouse-position');
expect(cmpDom).toExist();
+
expect(cmpDom.innerHTML).toContain('Lat:');
expect(cmpDom.innerHTML).toContain('Lng:');
});
it('checks default templates meters', () => {
- const cmp = ReactDOM.render(, document.getElementById("container"));
+ ReactDOM.render(, document.getElementById("container"));
+ const cmp = document.getElementById("container");
expect(cmp).toExist();
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = cmp.querySelector('#mouse-position');
expect(cmpDom).toExist();
expect(cmpDom.innerHTML).toContain('Y:');
expect(cmpDom.innerHTML).toContain('X:');
@@ -105,10 +113,11 @@ describe('MousePosition', () => {
}
}
- const cmp = ReactDOM.render(, document.getElementById("container"));
+ ReactDOM.render(, document.getElementById("container"));
+ const cmp = document.getElementById("container");
expect(cmp).toExist();
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = cmp.querySelector('#mouse-position');
expect(cmpDom).toExist();
expect(cmpDom.innerHTML).toContain('11');
expect(cmpDom.innerHTML).toContain('12');
@@ -116,25 +125,27 @@ describe('MousePosition', () => {
it('checks custom elevation template', () => {
const elevationTemplate = (z) => Z: {z}
;
-
- const cmp = ReactDOM.render(, document.getElementById("container"));
+ ReactDOM.render(, document.getElementById("container"));
+ const cmp = document.getElementById("container");
expect(cmp).toExist();
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = cmp.querySelector('#mouse-position');
expect(cmpDom).toExist();
expect(cmpDom.innerHTML).toContain('Z:');
expect(cmpDom.innerHTML).toContain('13');
});
it('checks copy to clipboard enabled', () => {
- const cmp = ReactDOM.render(, document.getElementById("container"));
+ const cmp = document.getElementById("container");
expect(cmp).toExist();
// checking if the component exists
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = cmp.querySelector('#mouse-position');
expect(cmpDom).toExist();
expect(cmpDom.id).toExist();
@@ -153,14 +164,15 @@ describe('MousePosition', () => {
let spy = expect.spyOn(actions, "onCopy");
// instaciating mouse position plugin
- const cmp = ReactDOM.render(, document.getElementById("container"));
// getting the copy to clipboard button
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = document.getElementById("container");
const button = cmpDom.getElementsByTagName('button')[0];
// if propmt for ctrl+c we accept
@@ -180,14 +192,15 @@ describe('MousePosition', () => {
let spy = expect.spyOn(actions, "onCopy");
// instaciating mouse position plugin
- const cmp = ReactDOM.render(, document.getElementById("container"));
// getting the copy to clipboard button
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = document.getElementById("container");
const button = cmpDom.getElementsByTagName('button')[0];
// if propmt for ctrl+c we accept
diff --git a/web/client/components/mapcontrols/mouseposition/__tests__/MousePositionLabelDM-test.js b/web/client/components/mapcontrols/mouseposition/__tests__/MousePositionLabelDM-test.js
index fdf3a32b613..ed55a8a1215 100644
--- a/web/client/components/mapcontrols/mouseposition/__tests__/MousePositionLabelDM-test.js
+++ b/web/client/components/mapcontrols/mouseposition/__tests__/MousePositionLabelDM-test.js
@@ -10,7 +10,6 @@ import expect from 'expect';
import React from 'react';
import ReactDOM from 'react-dom';
import MousePositionLabelDM from '../MousePositionLabelDM';
-import ReactTestUtils from 'react-dom/test-utils';
import { IntlProvider } from 'react-intl';
describe('MousePositionLabelDM', () => {
@@ -26,64 +25,66 @@ describe('MousePositionLabelDM', () => {
});
it('checks default', () => {
+ ReactDOM.render(
+ ,
+ document.getElementById("container")
+ );
+ const cmpDom = document.getElementById("container");
+ expect(cmpDom).toExist();
+ expect(cmpDom.textContent).toBe("Lat: ° ' Lng: ° ' ");
+ const mainSpans = cmpDom.querySelectorAll(':scope > span');
- const cmp = ReactDOM.render(
-
- , document.getElementById("container"));
- expect(cmp).toExist();
+ expect(mainSpans.length).toBe(2);
+ expect(mainSpans[0].textContent).toBe("Lat: ° ' ");
+ expect(mainSpans[1].textContent).toBe("Lng: ° ' ");
+
+ expect(mainSpans[0].textContent).toContain("Lat:");
+ expect(mainSpans[0].textContent).toContain("°");
+ expect(mainSpans[0].textContent).toContain("'");
+
+ expect(mainSpans[1].textContent).toContain("Lng:");
+ expect(mainSpans[1].textContent).toContain("°");
+ expect(mainSpans[1].textContent).toContain("'");
- const cmpDom = ReactDOM.findDOMNode(cmp);
- expect(cmpDom).toExist();
- expect(cmpDom.textContent).toBe("Lat: ° ' Lng: ° ' ");
-
- let spans = ReactTestUtils.scryRenderedDOMComponentsWithTag(cmp, "span");
- expect(spans.length).toBe(11);
- expect(spans[1].textContent).toBe("Lat: ");
- expect(spans[2].textContent).toBe("");
- expect(spans[3].textContent).toBe("° ");
- expect(spans[4].textContent).toBe("");
- expect(spans[5].textContent).toBe("\' ");
-
- expect(spans[6].textContent).toBe("Lng: ");
- expect(spans[7].textContent).toBe("");
- expect(spans[8].textContent).toBe("° ");
- expect(spans[9].textContent).toBe("");
- expect(spans[10].textContent).toBe("\' ");
});
it('a position with defaults', () => {
- const cmp = ReactDOM.render(
+ ReactDOM.render(
, document.getElementById("container"));
- expect(cmp).toExist();
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = document.getElementById("container");
expect(cmpDom).toExist();
- expect(cmpDom.textContent).toBe("Lat: 13° 32.000' Lng: 028° 18.000' ");
+ const mainSpans = cmpDom.querySelectorAll(':scope > span');
+
+ expect(mainSpans.length).toBe(2);
+ expect(mainSpans[0].textContent).toBe("Lat: 13° 32.000 ' ");
+ expect(mainSpans[1].textContent).toBe("Lng: 028° 18.000 ' ");
+
});
it('position with no rounding but flooring of latD and lngD', () => {
- const cmp = ReactDOM.render(
+ ReactDOM.render(
, document.getElementById("container"));
- expect(cmp).toExist();
-
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = document.getElementById("container");
expect(cmpDom).toExist();
- // it should be 010° 28' 30.05'' instead of 010° 29' 00''
- expect(cmpDom.textContent).toBe("Lat: 43° 42.436' Lng: 010° 28.501' ");
+ const mainSpans = cmpDom.querySelectorAll(':scope > span');
+ expect(mainSpans.length).toBe(2);
+ expect(mainSpans[0].textContent).toBe("Lat: 43° 42.436 ' ");
+ expect(mainSpans[1].textContent).toBe("Lng: 010° 28.501 ' ");
});
});
diff --git a/web/client/components/mapcontrols/mouseposition/__tests__/MousePositionLabelDMS-test.js b/web/client/components/mapcontrols/mouseposition/__tests__/MousePositionLabelDMS-test.js
index 5b350ab9a45..5ababc2efca 100644
--- a/web/client/components/mapcontrols/mouseposition/__tests__/MousePositionLabelDMS-test.js
+++ b/web/client/components/mapcontrols/mouseposition/__tests__/MousePositionLabelDMS-test.js
@@ -10,7 +10,6 @@ import expect from 'expect';
import React from 'react';
import ReactDOM from 'react-dom';
import MousePositionLabelDMS from '../MousePositionLabelDMS';
-import ReactTestUtils from 'react-dom/test-utils';
import { IntlProvider } from 'react-intl';
describe('MousePositionLabelDMS', () => {
@@ -26,113 +25,107 @@ describe('MousePositionLabelDMS', () => {
});
it('checks default', () => {
- const cmp = ReactDOM.render(
+ ReactDOM.render(
, document.getElementById("container"));
- expect(cmp).toExist();
-
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = document.getElementById("container");
expect(cmpDom).toExist();
- expect(cmpDom.textContent).toBe("Lat: ° ' '' Lng: ° ' ''");
-
- let spans = ReactTestUtils.scryRenderedDOMComponentsWithTag(cmp, "span");
- expect(spans.length).toBe(16);
- expect(spans[1].textContent).toBe("Lat: ");
- expect(spans[2].textContent).toBe("");
- expect(spans[3].textContent).toBe("° ");
- expect(spans[4].textContent).toBe("");
- expect(spans[5].textContent).toBe("\' ");
- expect(spans[6].textContent).toBe("");
- expect(spans[7].textContent).toBe("\'\'");
-
- expect(spans[8].className).toBe("mouseposition-separator");
-
- expect(spans[9].textContent).toBe(" Lng: ");
- expect(spans[10].textContent).toBe("");
- expect(spans[11].textContent).toBe("° ");
- expect(spans[12].textContent).toBe("");
- expect(spans[13].textContent).toBe("\' ");
- expect(spans[14].textContent).toBe("");
- expect(spans[15].textContent).toBe("\'\'");
+ let spans = cmpDom.querySelectorAll(':scope > span');
+ expect(spans.length).toBe(2);
+ expect(spans[0].textContent).toBe("Lat: 0° 0' 0''");
+ expect(spans[1].textContent).toBe("Lng: 0° 0' 0''");
});
it('a position with defaults', () => {
- const cmp = ReactDOM.render(
+ ReactDOM.render(
, document.getElementById("container"));
- expect(cmp).toExist();
-
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = document.getElementById("container");
expect(cmpDom).toExist();
- expect(cmpDom.textContent).toBe("Lat: 13° 31' 60.00'' Lng: 028° 18' 00.00''");
+
+ let spans = cmpDom.querySelectorAll(':scope > span');
+ expect(spans.length).toBe(2);
+ expect(spans[0].textContent).toBe("Lat: 13° 31' 60.00''");
+ expect(spans[1].textContent).toBe("Lng: 028° 18' 00.00''");
});
it('position with no rounding but trunc of latD and lngD', () => {
-
- const cmp = ReactDOM.render(
+ ReactDOM.render(
, document.getElementById("container"));
- expect(cmp).toExist();
-
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = document.getElementById("container");
expect(cmpDom).toExist();
- // it should be 010° 28' 30.05'' instead of 010° 29' 00''
- expect(cmpDom.textContent).toBe("Lat: 43° 42' 26.16'' Lng: 010° 28' 30.05''");
+ let spans = cmpDom.querySelectorAll(':scope > span');
+ expect(spans.length).toBe(2);
+ expect(spans[0].textContent).toBe("Lat: 43° 42' 26.16''");
+ expect(spans[1].textContent).toBe("Lng: 010° 28' 30.05''");
});
it('position with negative lat and lng correctly truncated ladD e lngD', () => {
- const cmp = ReactDOM.render(
+ ReactDOM.render(
, document.getElementById("container"));
- expect(cmp).toExist();
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = document.getElementById("container");
expect(cmpDom).toExist();
- // it should be Lat: -00° 00' 21.60'' Lng: -000° 00' 21.60'' instead of Lat: -01° 00' 21.60'' Lng: -001° 00' 21.60''
- expect(cmpDom.textContent).toBe("Lat: -00° 00' 21.60'' Lng: -000° 00' 21.60''");
+
+ let spans = cmpDom.querySelectorAll(':scope > span');
+ expect(spans.length).toBe(2);
+ // it should be Lat: - 00° 00' 21.60'' Lng: - 000° 00' 21.60'' instead of Lat: -01° 00' 21.60'' Lng: -001° 00' 21.60''
+ expect(spans[0].textContent).toBe("Lat: - 00° 00' 21.60''");
+ expect(spans[1].textContent).toBe("Lng: - 000° 00' 21.60''");
});
+
it('test sign changes when crossing greenwich meridian and equator parallel and latD lngD are 0', () => {
- const cmp = ReactDOM.render(
+ ReactDOM.render(
, document.getElementById("container"));
- expect(cmp).toExist();
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = document.getElementById("container");
expect(cmpDom).toExist();
- // it should be Lat: -00° 00' 21.60'' Lng: -000° 00' 21.60''
- expect(cmpDom.textContent).toBe("Lat: -00° 00' 21.60'' Lng: -000° 00' 21.60''");
+ let spans = cmpDom.querySelectorAll(':scope > span');
+ expect(spans.length).toBe(2);
+
+ expect(spans[0].textContent).toBe("Lat: - 00° 00' 21.60''");
+ expect(spans[1].textContent).toBe("Lng: - 000° 00' 21.60''");
+
+ // it should be Lat: - 00° 00' 21.60'' Lng: - 000° 00' 21.60''
- const cmpPositive = ReactDOM.render(
+ ReactDOM.render(
, document.getElementById("container"));
- expect(cmpPositive).toExist();
- const cmpDomPositive = ReactDOM.findDOMNode(cmpPositive);
+ const cmpDomPositive = document.getElementById("container");
expect(cmpDomPositive).toExist();
+ spans = cmpDomPositive.querySelectorAll(':scope > span');
+ expect(spans.length).toBe(2);
+
+ expect(spans[0].textContent).toBe("Lat: 00° 00' 21.60''");
+ expect(spans[1].textContent).toBe("Lng: 000° 00' 21.60''");
+
// it should be Lat: 00° 00' 21.60'' Lng: 000° 00' 21.60'' instead of Lat: -00° 00' 21.60'' Lng: -000° 00' 21.60''
- expect(cmpDomPositive.textContent).toBe("Lat: 00° 00' 21.60'' Lng: 000° 00' 21.60''");
});
});
diff --git a/web/client/components/mapcontrols/mouseposition/__tests__/MousePositionLabelDMSNW-test.js b/web/client/components/mapcontrols/mouseposition/__tests__/MousePositionLabelDMSNW-test.js
index 76d8b29ea77..7aac988dcae 100644
--- a/web/client/components/mapcontrols/mouseposition/__tests__/MousePositionLabelDMSNW-test.js
+++ b/web/client/components/mapcontrols/mouseposition/__tests__/MousePositionLabelDMSNW-test.js
@@ -10,7 +10,6 @@ import expect from 'expect';
import React from 'react';
import ReactDOM from 'react-dom';
import MousePositionLabelDMSNW from '../MousePositionLabelDMSNW';
-import ReactTestUtils from 'react-dom/test-utils';
import { IntlProvider } from 'react-intl';
describe('MousePositionLabelDMSNW', () => {
@@ -26,67 +25,63 @@ describe('MousePositionLabelDMSNW', () => {
});
it('checks default', () => {
-
- const cmp = ReactDOM.render(
+ ReactDOM.render(
, document.getElementById("container"));
- expect(cmp).toExist();
-
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = document.getElementById("container");
expect(cmpDom).toExist();
-
- expect(cmpDom.textContent).toBe("° ' '' S ° ' '' W");
-
-
- let spans = ReactTestUtils.scryRenderedDOMComponentsWithTag(cmp, "span");
- expect(spans.length).toBe(13);
- expect(spans[1].innerText).toBe("");
- expect(spans[2].innerText).toBe("° ");
- expect(spans[3].innerText).toBe("");
- expect(spans[4].innerText).toBe("\' ");
- expect(spans[5].innerText).toBe("");
- expect(spans[6].innerText).toBe("\'\' S ");
-
- expect(spans[7].innerText).toBe("");
- expect(spans[8].innerText).toBe("° ");
- expect(spans[9].innerText).toBe("");
- expect(spans[10].innerText).toBe("\' ");
- expect(spans[11].innerText).toBe("");
- expect(spans[12].innerText).toBe("\'\' W");
+ const mainSpans = cmpDom.querySelectorAll(':scope > span');
+ expect(mainSpans.length).toBe(2);
+
+ expect(mainSpans[0].textContent).toBe("Lat: ° ' '' S");
+ expect(mainSpans[1].textContent).toBe("Lng: ° ' '' W");
+
+ expect(mainSpans[0].textContent).toContain("Lat:");
+ expect(mainSpans[0].textContent).toContain("°");
+ expect(mainSpans[0].textContent).toContain("'");
+ expect(mainSpans[0].textContent).toContain("''");
+ expect(mainSpans[0].textContent).toContain("S");
+
+ expect(mainSpans[1].textContent).toContain("Lng:");
+ expect(mainSpans[1].textContent).toContain("°");
+ expect(mainSpans[1].textContent).toContain("'");
+ expect(mainSpans[1].textContent).toContain("''");
+ expect(mainSpans[1].textContent).toContain("W");
});
it('a position with defaults', () => {
-
- const cmp = ReactDOM.render(
+ ReactDOM.render(
, document.getElementById("container"));
- expect(cmp).toExist();
-
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = document.getElementById("container");
expect(cmpDom).toExist();
- expect(cmpDom.textContent).toBe("13° 31' 60.00'' N 028° 18' 00.00'' E");
+ const mainSpans = cmpDom.querySelectorAll(':scope > span');
+ expect(mainSpans.length).toBe(2);
+
+ expect(mainSpans[0].textContent).toBe("Lat: 13° 31 ' 60.00 '' N");
+ expect(mainSpans[1].textContent).toBe("Lng: 028° 18 ' 00.00 '' E");
});
it('position with no rounding but flooring of latD and lngD', () => {
-
- const cmp = ReactDOM.render(
+ ReactDOM.render(
, document.getElementById("container"));
- expect(cmp).toExist();
-
- const cmpDom = ReactDOM.findDOMNode(cmp);
+ const cmpDom = document.getElementById("container");
expect(cmpDom).toExist();
+ const mainSpans = cmpDom.querySelectorAll(':scope > span');
+ expect(mainSpans.length).toBe(2);
+ expect(mainSpans[0].textContent).toBe("Lat: 43° 42 ' 26.16 '' N");
+ expect(mainSpans[1].textContent).toBe("Lng: 010° 28 ' 30.05 '' E");
// it should be 010° 28' 30.05'' instead of 010° 29' 00''
- expect(cmpDom.textContent).toBe("43° 42' 26.16'' N 010° 28' 30.05'' E");
});
});
diff --git a/web/client/components/mapcontrols/mouseposition/mousePosition.css b/web/client/components/mapcontrols/mouseposition/mousePosition.css
index 36ba2204457..eb5ecf078cb 100644
--- a/web/client/components/mapcontrols/mouseposition/mousePosition.css
+++ b/web/client/components/mapcontrols/mouseposition/mousePosition.css
@@ -105,7 +105,7 @@
}
#mapstore-mouseposition .form-group select {
- width: 90px;
+ width: 90px !important;
font-size: 11px;
padding: 2px;
-}
+}
\ No newline at end of file
diff --git a/web/client/components/mapcontrols/mouseposition/templates.js b/web/client/components/mapcontrols/mouseposition/templates.js
new file mode 100644
index 00000000000..6e8ca281d40
--- /dev/null
+++ b/web/client/components/mapcontrols/mouseposition/templates.js
@@ -0,0 +1,35 @@
+/**
+ * Copyright 2025, GeoSolutions Sas.
+ * 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 MousePositionLabelDMS from './MousePositionLabelDMS';
+import MousePositionLabelYX from './MousePositionLabelYX';
+import MousePositionLabelDD from './MousePositionLabelDD';
+import MousePositionLabelDM from './MousePositionLabelDM';
+import MousePositionLabelDMSNW from './MousePositionLabelDMSNW';
+
+const templates = {
+ MousePositionLabelDMS,
+ MousePositionLabelYX,
+ MousePositionLabelDD,
+ MousePositionLabelDM,
+ MousePositionLabelDMSNW
+};
+
+/**
+ * Gets a template component by its name
+ * @param {string} templateName - Name of the template to retrieve
+ * @returns {React.Component} The template component
+ * @throws {Error} When template is not found
+ */
+export const getTemplate = (templateName) => {
+ if (!templates[templateName]) {
+ throw new Error(`Template "${templateName}" not found in registry`);
+ }
+ return templates[templateName];
+};
+
+export default templates;
diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json
index 318fc989144..49cfea769a3 100644
--- a/web/client/configs/localConfig.json
+++ b/web/client/configs/localConfig.json
@@ -576,6 +576,9 @@
"additionalCRS": {}
}
},
+ {
+ "name": "CameraPosition"
+ },
{
"name": "CRSSelector",
"cfg": {
diff --git a/web/client/configs/pluginsConfig.json b/web/client/configs/pluginsConfig.json
index aca03bc9149..28b81920d0a 100644
--- a/web/client/configs/pluginsConfig.json
+++ b/web/client/configs/pluginsConfig.json
@@ -328,6 +328,13 @@
"additionalCRS": {}
}
},
+ {
+ "name": "CameraPosition",
+ "glyph": "camera",
+ "title": "plugins.CameraPosition.title",
+ "description": "plugins.CameraPosition.description",
+ "dependencies": ["MapFooter"]
+ },
{
"name": "CRSSelector",
"glyph": "crs",
diff --git a/web/client/plugins/CameraPosition.jsx b/web/client/plugins/CameraPosition.jsx
new file mode 100644
index 00000000000..4be8c9faff5
--- /dev/null
+++ b/web/client/plugins/CameraPosition.jsx
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2025, GeoSolutions Sas.
+ * 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, { useEffect, useState } from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { Tooltip } from 'react-bootstrap';
+import PropTypes from 'prop-types';
+import axios from 'axios';
+
+import { createPlugin } from '../utils/PluginsUtils';
+import ToggleButton from '../components/buttons/ToggleButton';
+import Message from '../components/I18N/Message';
+import MousePositionComponent from '../components/mapcontrols/mouseposition/MousePosition';
+import { getTemplate } from '../components/mapcontrols/mouseposition/templates';
+import { mapSelector, projectionDefsSelector } from '../selectors/map';
+import { getCameraPositionCrs, getCameraPositionHeightType, getShowCameraPosition } from './CameraPosition/selectors/cameraPosition';
+import { showCameraPosition, hideCameraPosition, changeCameraPositionCrs, changeCameraPositionHeightType } from './CameraPosition/actions/cameraPosition';
+import cameraPosition from './CameraPosition/reducers/cameraPosition';
+import './CameraPosition/cameraPosition.css';
+
+const selector = createSelector([
+ (state) => state,
+ mapSelector,
+ (state) => getShowCameraPosition(state),
+ (state) => getCameraPositionCrs(state),
+ (state) => getCameraPositionHeightType(state)
+], (state, map, showCameraPositionEnabled, crs, heightType) => ({
+ showCameraPosition: showCameraPositionEnabled,
+ cameraPosition: map?.viewerOptions?.cameraPosition,
+ projectionDefs: projectionDefsSelector(state),
+ crs: crs,
+ heightType: heightType
+}));
+
+const CameraPositionButton = connect((state) => ({
+ pressed: getShowCameraPosition(state),
+ active: getShowCameraPosition(state),
+ tooltip: ,
+ tooltipPlace: 'left',
+ pressedStyle: "success active",
+ defaultStyle: "primary",
+ glyphicon: "camera",
+ btnConfig: { className: 'square-button-md' },
+ style: {
+ height: '25px',
+ width: '25px'
+ }
+}), { showCameraPosition, hideCameraPosition }, (stateProps, dispatchProps) => {
+ return { ...stateProps, onClick: () => {
+ if (stateProps.pressed) {
+ dispatchProps.hideCameraPosition();
+ } else {
+ dispatchProps.showCameraPosition();
+ }
+ } };
+})(ToggleButton);
+
+let geoidCache = {};
+
+const getGeoidByUrl = (url) => {
+ return import('@math.gl/geoid')
+ .then(({ parsePGM }) => {
+ if (geoidCache[url]) {
+ return Promise.resolve(geoidCache[url]);
+ }
+ return axios.get(url, {
+ responseType: 'arraybuffer'
+ })
+ .then(({ data }) => {
+ geoidCache[url] = parsePGM(new Uint8Array(data), {
+ cubic: false
+ });
+ return geoidCache[url];
+ });
+ });
+};
+
+const CameraPosition = ({
+ availableHeightTypes = [
+ { value: "Ellipsoidal", labelId: "plugins.CameraPosition.ellipsoidal" }
+ ],
+ editHeight = true,
+ showElevation = true,
+ additionalCRS = {},
+ editCRS = true,
+ filterAllowedCRS = ["EPSG:4326", "EPSG:3857"],
+ showLabels = true,
+ showToggle = true,
+ ...props
+}) => {
+ const { degreesTemplate = 'MousePositionLabelDMS', projectedTemplate = 'MousePositionLabelYX', ...other } = props;
+ const { cameraPosition: cameraPositionData = {}, showCameraPosition: showCameraPositionEnabled, heightType } = props;
+ const [mousePosition, setMousePosition] = useState(null);
+
+ const geoidUrl = availableHeightTypes.find((entry) => entry.value === heightType)?.geoidUrl;
+
+ useEffect(() => {
+ if (!cameraPositionData || Object.keys(cameraPositionData).length === 0) {
+ return;
+ }
+
+ if (geoidUrl) {
+ getGeoidByUrl(geoidUrl)
+ .then(geoid => {
+ const heightMSL = cameraPositionData.height - geoid.getHeight(cameraPositionData.latitude, cameraPositionData.longitude);
+ setMousePosition({
+ x: cameraPositionData.longitude,
+ y: cameraPositionData.latitude,
+ z: Number(heightMSL.toFixed(2)),
+ crs: "EPSG:4326"
+ });
+ })
+ .catch(() => {
+ setMousePosition({
+ x: cameraPositionData.longitude,
+ y: cameraPositionData.latitude,
+ z: Number(cameraPositionData.height?.toFixed(2) || 0),
+ crs: "EPSG:4326"
+ });
+ });
+ } else {
+ setMousePosition({
+ x: cameraPositionData.longitude,
+ y: cameraPositionData.latitude,
+ z: Number(cameraPositionData.height?.toFixed(2) || 0),
+ crs: "EPSG:4326"
+ });
+ }
+ }, [
+ cameraPositionData?.longitude,
+ cameraPositionData?.latitude,
+ cameraPositionData?.height,
+ geoidUrl
+ ]);
+
+ return (
+ }
+ mousePosition={mousePosition}
+ availableHeightTypes={availableHeightTypes}
+ {...other}
+ />
+ );
+};
+
+CameraPosition.propTypes = {
+ degreesTemplate: PropTypes.string,
+ projectedTemplate: PropTypes.string,
+ editHeight: PropTypes.bool,
+ showElevation: PropTypes.bool,
+ additionalCRS: PropTypes.object,
+ editCRS: PropTypes.bool,
+ filterAllowedCRS: PropTypes.array,
+ showLabels: PropTypes.bool,
+ showToggle: PropTypes.bool,
+ heightType: PropTypes.string
+};
+
+/**
+ * CameraPosition Plugin is a plugin that shows the coordinate of the camera position in a selected crs along with the height above ellipsoid or mean sea level.
+ * it gets displayed into the mapFooter plugin
+ * @name CameraPosition
+ * @memberof plugins
+ * @class
+ * @prop {string} cfg.editCRS if true shows a combobox to select the crs of the camera position.
+ * @prop {string} cfg.showLabels if true shows the labels of the coordinates.
+ * @prop {string} cfg.showToggle if true shows a toggle button to enable/disable the plugin.
+ * @prop {boolean} cfg.showElevation shows elevation in Ellipsoidal or MSL in 3D map.
+ * @prop {function} cfg.elevationTemplate custom template to show the elevation if showElevation is true (default template shows the elevation number with no formatting)
+ * @prop {object[]} projectionDefs list of additional project definitions
+ * @prop {string[]} cfg.filterAllowedCRS list of allowed crs in the combobox list to used as filter for the one of retrieved proj4.defs()
+ * @prop {string[]} cfg.allowedHeightTypes list of allowed height type in the combobox list. Accepted values are "Ellipsoidal" and "MSL"
+ * @prop {object} cfg.additionalCRS additional crs added to the list. The label param is used after in the combobox.
+ * @example
+ * // If you want to add some crs you need to provide a definition and adding it in the additionalCRS property
+ * // Put the following lines at the first level of the localconfig
+ * {
+ * "projectionDefs": [{
+ * "code": "EPSG:3003",
+ * "def": "+proj=tmerc +lat_0=0 +lon_0=9 +k=0.9996 +x_0=1500000 +y_0=0 +ellps=intl+towgs84=-104.1,-49.1,-9.9,0.971,-2.917,0.714,-11.68 +units=m +no_defs",
+ * "extent": [1241482.0019, 973563.1609, 1830078.9331, 5215189.0853],
+ * "worldExtent": [6.6500, 8.8000, 12.0000, 47.0500]
+ * }]
+ * }
+ * @example
+ * // And configure the mouse position plugin as below:
+ * {
+ * "cfg": {
+ * "additionalCRS": {
+ * "EPSG:3003": { "label": "EPSG:3003" }
+ * },
+ * "filterAllowedCRS": ["EPSG:4326", "EPSG:3857"]
+ * }
+ * }
+ * @example
+ * // to show elevation and (optionally) use a custom template configure the plugin this way:
+ * {
+ * "cfg": {
+ * ...
+ * "showElevation": true,
+ * "elevationTemplate": "{(function(elevation) {return 'myelev: ' + (elevation || 0);})}",
+ * ...
+ * }
+ * }
+ * @example
+ * // to add MSL height type with geoid model configure the plugin this way:
+ * {
+ * "cfg": {
+ * ...
+ * "availableHeightTypes": [
+ * { "value": "Ellipsoidal", "labelId":"plugins.CameraPosition.ellipsoidal" },
+ * { "value": "MSL", "labelId": "plugins.CameraPosition.msl" , "geoidUrl": "http://localhost:port/static/egm96-15.pgm"}
+ * ],
+ * ...
+ * }
+ * }
+ * @example
+ * // to show the crs and height type labels configure the plugin this way:
+ * {
+ * "cfg": {
+ * ...
+ * "showLabels": true,
+ * ...
+ * }
+ * }
+ * @example
+ * // to show the toggle button configure the plugin this way:
+ * {
+ * "cfg": {
+ * ...
+ * "showToggle": true,
+ * ...
+ * }
+ * }
+ * @example
+ * // to show the crs selector configure the plugin this way:
+ * {
+ * "cfg": {
+ * ...
+ * "editCRS": true,
+ * ...
+ * }
+ * }
+*/
+
+export default createPlugin('CameraPosition', {
+ component: connect(selector, {
+ onCRSChange: changeCameraPositionCrs,
+ onHeightTypeChange: changeCameraPositionHeightType
+ })(CameraPosition),
+ containers: {
+ MapFooter: {
+ name: 'cameraPosition',
+ position: 3,
+ target: 'right-footer',
+ priority: 1
+ }
+ },
+ reducers: {
+ cameraPosition
+ },
+ options: {
+ disablePluginIf: "{state('mapType') !== 'cesium'}"
+ }
+});
diff --git a/web/client/plugins/CameraPosition/actions/__tests__/cameraPosition-test.js b/web/client/plugins/CameraPosition/actions/__tests__/cameraPosition-test.js
new file mode 100644
index 00000000000..23e9add3047
--- /dev/null
+++ b/web/client/plugins/CameraPosition/actions/__tests__/cameraPosition-test.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2025, GeoSolutions Sas.
+ * 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 expect from "expect";
+
+import {
+ SHOW_CAMERA_POSITION,
+ HIDE_CAMERA_POSITION,
+ CHANGE_CAMERA_POSITION_CRS,
+ CHANGE_CAMERA_POSITION_HEIGHT_TYPE,
+ showCameraPosition,
+ hideCameraPosition,
+ changeCameraPositionCrs,
+ changeCameraPositionHeightType
+} from "../cameraPosition";
+
+describe("cameraPosition actions", () => {
+ it("show camera position", () => {
+ const result = showCameraPosition();
+ expect(result.type).toBe(SHOW_CAMERA_POSITION);
+ });
+
+ it("hide camera position", () => {
+ const result = hideCameraPosition();
+ expect(result.type).toBe(HIDE_CAMERA_POSITION);
+ });
+
+ it("change camera position crs", () => {
+ const crs = "EPSG:3857";
+ const result = changeCameraPositionCrs(crs);
+ expect(result.type).toBe(CHANGE_CAMERA_POSITION_CRS);
+ expect(result.crs).toBe(crs);
+ });
+
+ it("change camera position height type", () => {
+ const heightType = "MSL";
+ const result = changeCameraPositionHeightType(heightType);
+ expect(result.type).toBe(CHANGE_CAMERA_POSITION_HEIGHT_TYPE);
+ expect(result.heightType).toBe(heightType);
+ });
+});
diff --git a/web/client/plugins/CameraPosition/actions/cameraPosition.js b/web/client/plugins/CameraPosition/actions/cameraPosition.js
new file mode 100644
index 00000000000..da84e7b32b1
--- /dev/null
+++ b/web/client/plugins/CameraPosition/actions/cameraPosition.js
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2025, GeoSolutions Sas.
+ * 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.
+ */
+
+export const CHANGE_CAMERA_POSITION_CRS = 'CHANGE_CAMERA_POSITION_CRS';
+export const CHANGE_CAMERA_POSITION_HEIGHT_TYPE = 'CHANGE_CAMERA_POSITION_HEIGHT_TYPE';
+export const SHOW_CAMERA_POSITION = 'SHOW_CAMERA_POSITION';
+export const HIDE_CAMERA_POSITION = 'HIDE_CAMERA_POSITION';
+
+/**
+ * Change the camera position coordinate reference system
+ * @param {string} crs the coordinate reference system code (e.g. 'EPSG:4326')
+ * @return {object} of type `CHANGE_CAMERA_POSITION_CRS` with crs
+ */
+export function changeCameraPositionCrs(crs) {
+ return {
+ type: CHANGE_CAMERA_POSITION_CRS,
+ crs
+ };
+}
+
+/**
+ * Change the camera position height type
+ * @param {string} heightType the height type (e.g. 'Ellipsoidal' or 'MSL')
+ * @return {object} of type `CHANGE_CAMERA_POSITION_HEIGHT_TYPE` with heightType
+ */
+export function changeCameraPositionHeightType(heightType) {
+ return {
+ type: CHANGE_CAMERA_POSITION_HEIGHT_TYPE,
+ heightType
+ };
+}
+
+/**
+ * Show the camera position component
+ * @return {object} of type `SHOW_CAMERA_POSITION`
+ */
+export function showCameraPosition() {
+ return {
+ type: SHOW_CAMERA_POSITION
+ };
+}
+
+/**
+ * Hide the camera position component
+ * @return {object} of type `HIDE_CAMERA_POSITION`
+ */
+export function hideCameraPosition() {
+ return {
+ type: HIDE_CAMERA_POSITION
+ };
+}
diff --git a/web/client/plugins/CameraPosition/cameraPosition.css b/web/client/plugins/CameraPosition/cameraPosition.css
new file mode 100644
index 00000000000..a674b114021
--- /dev/null
+++ b/web/client/plugins/CameraPosition/cameraPosition.css
@@ -0,0 +1,22 @@
+#mapstore-crsselector-cameraposition{
+ border-radius: 4px;
+ font-size: 11px;
+ padding: 2px;
+ width: 90px !important;
+ height: 25px !important;
+ border-width: 1px !important;
+ border-style: solid !important;
+ line-height: 30px;
+ background-color: var(--ms-main-variant-color,#e6e6e6);
+}
+#mapstore-heightselector-cameraposition{
+ border-radius: 4px;
+ font-size: 11px;
+ padding: 2px;
+ width: 85px !important;
+ height: 25px !important;
+ border-width: 1px !important;
+ border-style: solid !important;
+ line-height: 30px;
+ background-color: var(--ms-main-variant-color,#e6e6e6);
+}
\ No newline at end of file
diff --git a/web/client/plugins/CameraPosition/reducers/__tests__/cameraPosition-test.js b/web/client/plugins/CameraPosition/reducers/__tests__/cameraPosition-test.js
new file mode 100644
index 00000000000..4e2519ef3c9
--- /dev/null
+++ b/web/client/plugins/CameraPosition/reducers/__tests__/cameraPosition-test.js
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2025, GeoSolutions Sas.
+ * 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 expect from 'expect';
+
+import cameraPosition from '../cameraPosition';
+
+import {
+ changeCameraPositionCrs,
+ changeCameraPositionHeightType,
+ showCameraPosition,
+ hideCameraPosition
+} from '../../actions/cameraPosition';
+
+describe('cameraPosition reducer', () => {
+ it('initial state', () => {
+ const state = cameraPosition(undefined, {});
+ expect(state).toEqual({
+ showCameraPosition: false,
+ crs: 'EPSG:4326',
+ heightType: 'Ellipsoidal'
+ });
+ });
+
+ it('show camera position', () => {
+ const state = cameraPosition({}, showCameraPosition());
+ expect(state.showCameraPosition).toBe(true);
+ });
+
+ it('hide camera position', () => {
+ const state = cameraPosition({ showCameraPosition: true }, hideCameraPosition());
+ expect(state.showCameraPosition).toBe(false);
+ });
+
+ it('change camera position CRS', () => {
+ const initialState = {
+ showCameraPosition: false,
+ crs: 'EPSG:4326',
+ heightType: 'Ellipsoidal'
+ };
+ const state = cameraPosition(initialState, changeCameraPositionCrs('EPSG:3857'));
+ expect(state.crs).toBe('EPSG:3857');
+ expect(state.showCameraPosition).toBe(false);
+ expect(state.heightType).toBe('Ellipsoidal');
+ });
+
+ it('change camera position CRS with different values', () => {
+ const initialState = {
+ showCameraPosition: true,
+ crs: 'EPSG:4326',
+ heightType: 'MSL'
+ };
+ const testCRS = ['EPSG:3857', 'EPSG:3003', 'EPSG:32633'];
+ testCRS.forEach(crs => {
+ const state = cameraPosition(initialState, changeCameraPositionCrs(crs));
+ expect(state.crs).toBe(crs);
+ expect(state.showCameraPosition).toBe(true);
+ expect(state.heightType).toBe('MSL');
+ });
+ });
+
+ it('change camera position height type', () => {
+ const initialState = {
+ showCameraPosition: false,
+ crs: 'EPSG:4326',
+ heightType: 'Ellipsoidal'
+ };
+ const state = cameraPosition(initialState, changeCameraPositionHeightType('MSL'));
+ expect(state.heightType).toBe('MSL');
+ expect(state.crs).toBe('EPSG:4326');
+ expect(state.showCameraPosition).toBe(false);
+ });
+
+ it('change camera position height type with different values', () => {
+ const initialState = {
+ showCameraPosition: true,
+ crs: 'EPSG:3857',
+ heightType: 'Ellipsoidal'
+ };
+ const heightTypes = ['Ellipsoidal', 'MSL'];
+ heightTypes.forEach(heightType => {
+ const state = cameraPosition(initialState, changeCameraPositionHeightType(heightType));
+ expect(state.heightType).toBe(heightType);
+ expect(state.crs).toBe('EPSG:3857');
+ expect(state.showCameraPosition).toBe(true);
+ });
+ });
+
+ it('multiple actions in sequence', () => {
+ let state = cameraPosition(undefined, {});
+ expect(state.showCameraPosition).toBe(false);
+ expect(state.crs).toBe('EPSG:4326');
+ expect(state.heightType).toBe('Ellipsoidal');
+
+ state = cameraPosition(state, showCameraPosition());
+ expect(state.showCameraPosition).toBe(true);
+
+ state = cameraPosition(state, changeCameraPositionCrs('EPSG:3857'));
+ expect(state.crs).toBe('EPSG:3857');
+ expect(state.showCameraPosition).toBe(true);
+
+ state = cameraPosition(state, changeCameraPositionHeightType('MSL'));
+ expect(state.heightType).toBe('MSL');
+ expect(state.crs).toBe('EPSG:3857');
+ expect(state.showCameraPosition).toBe(true);
+
+ state = cameraPosition(state, hideCameraPosition());
+ expect(state.showCameraPosition).toBe(false);
+ expect(state.crs).toBe('EPSG:3857');
+ expect(state.heightType).toBe('MSL');
+ });
+});
diff --git a/web/client/plugins/CameraPosition/reducers/cameraPosition.js b/web/client/plugins/CameraPosition/reducers/cameraPosition.js
new file mode 100644
index 00000000000..d2aa6259f2d
--- /dev/null
+++ b/web/client/plugins/CameraPosition/reducers/cameraPosition.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2025, GeoSolutions Sas.
+ * 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 {
+ CHANGE_CAMERA_POSITION_CRS,
+ CHANGE_CAMERA_POSITION_HEIGHT_TYPE,
+ SHOW_CAMERA_POSITION,
+ HIDE_CAMERA_POSITION
+} from '../actions/cameraPosition';
+
+function cameraPosition(state = {
+ showCameraPosition: false,
+ crs: 'EPSG:4326',
+ heightType: 'Ellipsoidal'
+}, action) {
+ switch (action.type) {
+ case SHOW_CAMERA_POSITION:
+ return {
+ ...state,
+ showCameraPosition: true
+ };
+ case HIDE_CAMERA_POSITION:
+ return {
+ ...state,
+ showCameraPosition: false
+ };
+ case CHANGE_CAMERA_POSITION_CRS:
+ return {
+ ...state,
+ crs: action.crs
+ };
+ case CHANGE_CAMERA_POSITION_HEIGHT_TYPE:
+ return {
+ ...state,
+ heightType: action.heightType
+ };
+ default:
+ return state;
+ }
+}
+
+export default cameraPosition;
diff --git a/web/client/plugins/CameraPosition/selectors/__tests__/cameraPosition-test.js b/web/client/plugins/CameraPosition/selectors/__tests__/cameraPosition-test.js
new file mode 100644
index 00000000000..68fba69a98a
--- /dev/null
+++ b/web/client/plugins/CameraPosition/selectors/__tests__/cameraPosition-test.js
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2025, GeoSolutions Sas.
+ * 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 expect from 'expect';
+
+import {
+ getShowCameraPosition,
+ getCameraPositionCrs,
+ getCameraPositionHeightType
+} from '../cameraPosition';
+
+describe('cameraPosition selectors', () => {
+ it('getShowCameraPosition', () => {
+ const state = {
+ cameraPosition: {
+ showCameraPosition: true,
+ crs: 'EPSG:4326',
+ heightType: 'Ellipsoidal'
+ }
+ };
+ expect(getShowCameraPosition(state)).toBe(true);
+ });
+
+ it('getShowCameraPosition when false', () => {
+ const state = {
+ cameraPosition: {
+ showCameraPosition: false,
+ crs: 'EPSG:4326',
+ heightType: 'Ellipsoidal'
+ }
+ };
+ expect(getShowCameraPosition(state)).toBe(false);
+ });
+
+ it('getShowCameraPosition with undefined state', () => {
+ const state = {};
+ expect(getShowCameraPosition(state)).toBe(undefined);
+ });
+
+ it('getCameraPositionCrs', () => {
+ const state = {
+ cameraPosition: {
+ showCameraPosition: true,
+ crs: 'EPSG:4326',
+ heightType: 'Ellipsoidal'
+ }
+ };
+ expect(getCameraPositionCrs(state)).toBe('EPSG:4326');
+ });
+
+ it('getCameraPositionCrs with different CRS', () => {
+ const state = {
+ cameraPosition: {
+ showCameraPosition: true,
+ crs: 'EPSG:3857',
+ heightType: 'Ellipsoidal'
+ }
+ };
+ expect(getCameraPositionCrs(state)).toBe('EPSG:3857');
+ });
+
+ it('getCameraPositionCrs with undefined state', () => {
+ const state = {};
+ expect(getCameraPositionCrs(state)).toBe(undefined);
+ });
+
+ it('getCameraPositionHeightType', () => {
+ const state = {
+ cameraPosition: {
+ showCameraPosition: true,
+ crs: 'EPSG:4326',
+ heightType: 'Ellipsoidal'
+ }
+ };
+ expect(getCameraPositionHeightType(state)).toBe('Ellipsoidal');
+ });
+
+ it('getCameraPositionHeightType with MSL', () => {
+ const state = {
+ cameraPosition: {
+ showCameraPosition: true,
+ crs: 'EPSG:4326',
+ heightType: 'MSL'
+ }
+ };
+ expect(getCameraPositionHeightType(state)).toBe('MSL');
+ });
+
+ it('getCameraPositionHeightType with undefined state', () => {
+ const state = {};
+ expect(getCameraPositionHeightType(state)).toBe(undefined);
+ });
+
+ it('all selectors work with complete state', () => {
+ const state = {
+ cameraPosition: {
+ showCameraPosition: true,
+ crs: 'EPSG:3857',
+ heightType: 'MSL'
+ }
+ };
+ expect(getShowCameraPosition(state)).toBe(true);
+ expect(getCameraPositionCrs(state)).toBe('EPSG:3857');
+ expect(getCameraPositionHeightType(state)).toBe('MSL');
+ });
+});
diff --git a/web/client/plugins/CameraPosition/selectors/cameraPosition.js b/web/client/plugins/CameraPosition/selectors/cameraPosition.js
new file mode 100644
index 00000000000..f1ba4871f6d
--- /dev/null
+++ b/web/client/plugins/CameraPosition/selectors/cameraPosition.js
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2025, GeoSolutions Sas.
+ * 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.
+ */
+
+
+export const getShowCameraPosition = (state) => state?.cameraPosition?.showCameraPosition;
+export const getCameraPositionCrs = (state) => state?.cameraPosition?.crs;
+export const getCameraPositionHeightType = (state) => state?.cameraPosition?.heightType;
diff --git a/web/client/plugins/MousePosition.jsx b/web/client/plugins/MousePosition.jsx
index fa887706410..6c668c2cb9e 100644
--- a/web/client/plugins/MousePosition.jsx
+++ b/web/client/plugins/MousePosition.jsx
@@ -18,6 +18,7 @@ import { changeMousePositionCrs } from '../actions/mousePosition';
import ToggleButton from '../components/buttons/ToggleButton';
import Message from '../components/I18N/Message';
import MousePositionComponent from '../components/mapcontrols/mouseposition/MousePosition';
+import { getTemplate } from '../components/mapcontrols/mouseposition/templates';
import mousePositionReducers from '../reducers/mousePosition';
import { isMouseMoveCoordinatesActiveSelector, mapSelector, projectionDefsSelector } from '../selectors/map';
@@ -69,31 +70,23 @@ const MousePositionButton = connect((state) => ({
return {...stateProps, onClick: () => stateProps.active ? dispatchProps.unRegisterEventListener('mousemove', 'mouseposition') : dispatchProps.registerEventListener('mousemove', 'mouseposition')};
})(ToggleButton);
+const MousePosition = (props) => {
+ const { degreesTemplate = 'MousePositionLabelDMS', projectedTemplate = 'MousePositionLabelYX', ...other } = props;
-class MousePosition extends React.Component {
- static propTypes = {
- degreesTemplate: PropTypes.string,
- projectedTemplate: PropTypes.string
- };
-
- static defaultProps = {
- degreesTemplate: 'MousePositionLabelDMS',
- projectedTemplate: 'MousePositionLabelYX'
- };
+ return (
+ }
+ {...other}
+ />
+ );
+};
- getTemplate = (template) => {
- return require('../components/mapcontrols/mouseposition/' + template + ".jsx").default;
- };
- render() {
- const { degreesTemplate, projectedTemplate, ...other} = this.props;
- return (
- } {...other}/>
- );
- }
-}
+MousePosition.propTypes = {
+ degreesTemplate: PropTypes.string,
+ projectedTemplate: PropTypes.string
+};
/**
* MousePosition Plugin is a plugin that shows the coordinate of the mouse position in a selected crs.
diff --git a/web/client/plugins/__tests__/CameraPosition-test.jsx b/web/client/plugins/__tests__/CameraPosition-test.jsx
new file mode 100644
index 00000000000..f384b771a48
--- /dev/null
+++ b/web/client/plugins/__tests__/CameraPosition-test.jsx
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2025, GeoSolutions Sas.
+ * 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 ReactDOM from 'react-dom';
+import expect from 'expect';
+
+import CameraPositionPlugin from '../CameraPosition';
+import { getPluginForTest } from './pluginsTestUtils';
+
+const CAMERA_POSITION_ID = 'mapstore-cameraposition';
+
+describe('CameraPosition Plugin', () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ it('renders CameraPosition plugin with default props', () => {
+ const { Plugin } = getPluginForTest(CameraPositionPlugin, {
+ maptype: {
+ mapType: 'cesium'
+ },
+ map: {
+ present: {
+ viewerOptions: {
+ cameraPosition: {
+ longitude: 10.5,
+ latitude: 43.5,
+ height: 100
+ }
+ }
+ }
+ },
+ cameraPosition: {
+ showCameraPosition: true,
+ crs: 'EPSG:4326',
+ heightType: 'Ellipsoidal'
+ }
+ });
+ ReactDOM.render(
+ ,
+ document.getElementById("container")
+ );
+ const component = document.getElementById(CAMERA_POSITION_ID);
+ expect(component).toExist();
+ });
+
+
+ it('renders with custom availableHeightTypes', () => {
+ const { Plugin } = getPluginForTest(CameraPositionPlugin, {
+ map: { present: { viewerOptions: {} } },
+ cameraPosition: { showCameraPosition: true, crs: 'EPSG:4326', heightType: 'Ellipsoidal' }
+ });
+ const availableHeightTypes = [ { value: "Ellipsoidal", labelId: "ellipsoidal" }, { value: "MSL", labelId: "msl", geoidUrl: "http://localhost/geoid.pgm" }];
+ ReactDOM.render(
+ ,
+ document.getElementById("container")
+ );
+ const component = document.getElementById(CAMERA_POSITION_ID);
+ expect(component).toExist();
+ });
+
+ it('renders with custom template configurations', () => {
+ const { Plugin } = getPluginForTest(CameraPositionPlugin, {
+ map: { present: { viewerOptions: {} } },
+ cameraPosition: { showCameraPosition: true, crs: 'EPSG:4326', heightType: 'Ellipsoidal' }
+ });
+ ReactDOM.render(
+ ,
+ document.getElementById("container")
+ );
+ const component = document.getElementById(CAMERA_POSITION_ID);
+ expect(component).toExist();
+ });
+});
diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js
index 792ea2c5336..0537cb5dc19 100644
--- a/web/client/product/plugins.js
+++ b/web/client/product/plugins.js
@@ -27,6 +27,7 @@ import Itinerary from "../plugins/Itinerary";
import SecurityPopup from "../plugins/SecurityPopup";
import Isochrone from "../plugins/Isochrone";
import MapFooter from '../plugins/MapFooter';
+import CameraPosition from '../plugins/CameraPosition';
import {toModulePlugin} from "../utils/ModulePluginsUtils";
@@ -58,6 +59,7 @@ export const plugins = {
ItineraryPlugin: Itinerary,
IsochronePlugin: Isochrone,
MapFooterPlugin: MapFooter,
+ CameraPositionPlugin: CameraPosition,
// ### DYNAMIC PLUGINS ### //
// product plugins
diff --git a/web/client/translations/data.ca-ES.json b/web/client/translations/data.ca-ES.json
index 1fabdbb75ae..8c34e14e0ef 100644
--- a/web/client/translations/data.ca-ES.json
+++ b/web/client/translations/data.ca-ES.json
@@ -295,6 +295,7 @@
"mousePositionCoordinates": "Coordenades del cursor",
"mouseCoordinates": "Coordenades:",
"mousePositionCRS": "CRS:",
+ "mousePositionHeight": "Height:",
"mousePositionElevation": "Elevaci\u00f3.:",
"mousePositionNoElevation": "N/a",
"elevationLoading": "Carregant ...",
@@ -3378,6 +3379,12 @@
"description": "Mostra les coordenades del cursor del ratol\u00ed apuntat",
"title": "Posici\u00f3 del ratol\u00ed"
},
+ "CameraPosition": {
+ "description": "Mostra la posici\u00f3 actual de la c\u00e0mera en coordenades i altitud",
+ "title": "Posici\u00f3 de la c\u00e0mera",
+ "ellipsoidal": "Ellipsoidal",
+ "msl": "MSL"
+ },
"Print": {
"description": "Permet imprimir el mapa actual amb llegenda",
"title": "Eina d'impressi\u00f3"
diff --git a/web/client/translations/data.da-DK.json b/web/client/translations/data.da-DK.json
index 8598b33af0f..b0bca9acde7 100644
--- a/web/client/translations/data.da-DK.json
+++ b/web/client/translations/data.da-DK.json
@@ -239,6 +239,7 @@
"mousePositionCoordinates": "Cursor coordinates",
"mouseCoordinates": "Coordinates:",
"mousePositionCRS": "CRS:",
+ "mousePositionHeight": "Height:",
"mousePositionElevation": "Elev.:",
"mousePositionNoElevation": "N/A",
"elevationLoading": "Loading...",
@@ -3071,6 +3072,12 @@
"description": "Shows the coordinates of the pointed by the mouse cursor",
"title": "Mouse Position"
},
+ "CameraPosition": {
+ "description": "Shows the current camera position (only in 3D mode)",
+ "title": "Camera Position",
+ "ellipsoidal": "Ellipsoidal",
+ "msl": "MSL"
+ },
"Print": {
"description": "Allows to print the current map with legend",
"title": "Print Tool"
diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json
index f0eacc478cc..676fc5fc9e1 100644
--- a/web/client/translations/data.de-DE.json
+++ b/web/client/translations/data.de-DE.json
@@ -305,6 +305,7 @@
"mousePositionCoordinates": "Koordinatenanzeige",
"mouseCoordinates": "Koordinaten:",
"mousePositionCRS": "KBS:",
+ "mousePositionHeight": "Höhe:",
"mousePositionElevation": "Höhe:",
"mousePositionNoElevation": "N/v",
"elevationLoading": "Wird geladen...",
@@ -3544,6 +3545,12 @@
"description": "Zeigt die Koordinaten der Mauszeiger Position an",
"title": "Mausposition"
},
+ "CameraPosition": {
+ "description": "Zeigt die aktuelle Kameraposition in der 3D-Ansicht an",
+ "title": "Kameraposition",
+ "ellipsoidal": "Ellipsoidal",
+ "msl": "MSL"
+ },
"Print": {
"description": "Ermöglicht das Drucken der aktuellen Karte mit Legende",
"title": "Druckwerkzeug"
diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json
index ffcb832dde5..d51a0d82dab 100644
--- a/web/client/translations/data.en-US.json
+++ b/web/client/translations/data.en-US.json
@@ -305,6 +305,7 @@
"mousePositionCoordinates": "Cursor coordinates",
"mouseCoordinates": "Coordinates:",
"mousePositionCRS": "CRS:",
+ "mousePositionHeight": "Height:",
"mousePositionElevation": "Elev.:",
"mousePositionNoElevation": "N/A",
"elevationLoading": "Loading...",
@@ -3515,6 +3516,12 @@
"description": "Shows the coordinates of the pointed by the mouse cursor",
"title": "Mouse Position"
},
+ "CameraPosition": {
+ "description": "Shows the current camera position in 3D mode",
+ "title": "Camera Position",
+ "ellipsoidal": "Ellipsoidal",
+ "msl": "MSL"
+ },
"Print": {
"description": "Allows to print the current map with legend",
"title": "Print Tool"
diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json
index 25d68813466..9b844235d4b 100644
--- a/web/client/translations/data.es-ES.json
+++ b/web/client/translations/data.es-ES.json
@@ -305,6 +305,7 @@
"mousePositionCoordinates": "Indicador de coordenadas",
"mouseCoordinates": "Coordenadas:",
"mousePositionCRS": "CRS:",
+ "mousePositionHeight": "Height:",
"mousePositionElevation": "Elev.:",
"mousePositionNoElevation": "N/P",
"elevationLoading": "Cargando...",
@@ -3505,6 +3506,12 @@
"description": "Muestra las coordenadas del puntero con el cursor del mouse.",
"title": "Posición del mouse"
},
+ "CameraPosition": {
+ "description": "Muestra la posición de la cámara en coordenadas geográficas (longitud, latitud, altitud) o en coordenadas proyectadas (X, Y, Z)",
+ "title": "Posición de la cámara",
+ "ellipsoidal": "Ellipsoidal",
+ "msl": "MSL"
+ },
"Print": {
"description": "Permite imprimir el mapa actual con leyenda",
"title": "Herramienta de impresión"
diff --git a/web/client/translations/data.fi-FI.json b/web/client/translations/data.fi-FI.json
index b4cb56573c3..7a8599a2a64 100644
--- a/web/client/translations/data.fi-FI.json
+++ b/web/client/translations/data.fi-FI.json
@@ -136,6 +136,7 @@
"mousePositionCoordinates": "Koordinaattien osoitin",
"mouseCoordinates": "Koordinaatit:",
"mousePositionCRS": "CRS:",
+ "mousePositionHeight": "Korkeus:",
"mousePositionElevation": "Kork.:",
"mousePositionNoElevation": "N/A",
"elevationLoading": "Ladataan...",
@@ -2315,6 +2316,12 @@
"description": "Näyttää koordinaatit kursorin osoittamassa kohdassa",
"title": "Kursorin koordinaatit"
},
+ "CameraPosition": {
+ "description": "Näyttää nykyisen kameran sijainnin",
+ "title": "Kameran sijainti",
+ "ellipsoidal": "Ellipsoidal",
+ "msl": "MSL"
+ },
"Print": {
"description": "Mahdollistaa nykyisen kartan ja selitteen tulostamisen",
"title": "Tulostustyökalu"
diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json
index 55e04af442c..3690d31bb7a 100644
--- a/web/client/translations/data.fr-FR.json
+++ b/web/client/translations/data.fr-FR.json
@@ -305,6 +305,7 @@
"mousePositionCoordinates": "Coordonnées du curseur",
"mouseCoordinates": "Coordonnées:",
"mousePositionCRS": "CRS:",
+ "mousePositionHeight": "Hauteur:",
"mousePositionElevation": "Élév.:",
"mousePositionNoElevation": "N/A",
"elevationLoading": "Chargement...",
@@ -3506,6 +3507,12 @@
"description": "Affiche les coordonnées du pointé par le curseur de la souris",
"title": "Position de la souris"
},
+ "CameraPosition": {
+ "description": "Affiche la position actuelle de la caméra (longitude, latitude, altitude) dans le mode 3D",
+ "title": "Position de la caméra",
+ "ellipsoidal": "Ellipsoidal",
+ "msl": "MSL"
+ },
"Print": {
"description": "Permet d'imprimer la carte actuelle avec légende",
"title": "Outil d'impression"
diff --git a/web/client/translations/data.hr-HR.json b/web/client/translations/data.hr-HR.json
index 3b7487768e7..3555274ff3d 100644
--- a/web/client/translations/data.hr-HR.json
+++ b/web/client/translations/data.hr-HR.json
@@ -135,6 +135,7 @@
"mousePositionCoordinates": "Indikator koordinata",
"mouseCoordinates": "Koordinate:",
"mousePositionCRS": "CRS:",
+ "mousePositionHeight": "Visina:",
"mousePositionElevation": "n/m:",
"mousePositionNoElevation": "N/A",
"elevationLoading": "Učitavanje...",
diff --git a/web/client/translations/data.is-IS.json b/web/client/translations/data.is-IS.json
index 95ee1f33ba6..fd34c38aa6e 100644
--- a/web/client/translations/data.is-IS.json
+++ b/web/client/translations/data.is-IS.json
@@ -243,6 +243,7 @@
"mousePositionCoordinates": "Cursor coordinates",
"mouseCoordinates": "Coordinates:",
"mousePositionCRS": "CRS:",
+ "mousePositionHeight": "Height:",
"mousePositionElevation": "Elev.:",
"mousePositionNoElevation": "N/A",
"elevationLoading": "Loading...",
@@ -3113,6 +3114,12 @@
"description": "Shows the coordinates of the pointed by the mouse cursor",
"title": "Mouse Position"
},
+ "CameraPosition": {
+ "description": "Shows the current camera position (coordinates and zoom level)",
+ "title": "Camera Position",
+ "ellipsoidal": "Ellipsoidal",
+ "msl": "MSL"
+ },
"Print": {
"description": "Allows to print the current map with legend",
"title": "Print Tool"
diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json
index c407deab75b..7821bee04fa 100644
--- a/web/client/translations/data.it-IT.json
+++ b/web/client/translations/data.it-IT.json
@@ -305,6 +305,7 @@
"mousePositionCoordinates": "Indicatore di coordinate",
"mouseCoordinates": "Coordinate:",
"mousePositionCRS": "CRS:",
+ "mousePositionHeight": "Altezza:",
"mousePositionElevation": "Elev.:",
"mousePositionNoElevation": "Non disp.",
"elevationLoading": "Attendi...",
@@ -3507,6 +3508,12 @@
"description": "Mostra le coordinate della punta del cursore del mouse",
"title": "Posizione del mouse"
},
+ "CameraPosition": {
+ "description": "Mostra la posizione della telecamera (longitudine, latitudine, altezza) nella visualizzazione 3D",
+ "title": "Posizione della telecamera",
+ "ellipsoidal": "Ellipsoidal",
+ "msl": "MSL"
+ },
"Print": {
"description": "Permette di stampare la mappa corrente con la legenda",
"title": "Strumento di stampa"
diff --git a/web/client/translations/data.nl-NL.json b/web/client/translations/data.nl-NL.json
index b6ebd53b6d1..7235ac50c74 100644
--- a/web/client/translations/data.nl-NL.json
+++ b/web/client/translations/data.nl-NL.json
@@ -305,6 +305,7 @@
"mousePositionCoordinates": "Coördinaten",
"mouseCoordinates": "Coördinaten:",
"mousePositionCRS": "CRS:",
+ "mousePositionHeight": "Hoogte:",
"mousePositionElevation": "Elev.:",
"mousePositionNoElevation": "n.v.t.",
"elevationLoading": "Bezig met laden...",
@@ -3471,6 +3472,12 @@
"description": "Toont de coördinaten van de muisaanwijzer",
"title": "Muispositie"
},
+ "CameraPosition": {
+ "description": "Toont de huidige positie van de camera in 3D-modus",
+ "title": "Camera positie",
+ "ellipsoidal": "Ellipsoidal",
+ "msl": "MSL"
+ },
"Print": {
"description": "Staat toe om de huidige kaart met legende af te drukken",
"title": "Afdruktool"
diff --git a/web/client/translations/data.pt-PT.json b/web/client/translations/data.pt-PT.json
index b029cdf14d7..90095af4a23 100644
--- a/web/client/translations/data.pt-PT.json
+++ b/web/client/translations/data.pt-PT.json
@@ -137,6 +137,7 @@
"mousePositionCoordinates": "Indicador coordenadas",
"mouseCoordinates": "Coordenadas:",
"mousePositionCRS": "CRS:",
+ "mousePositionHeight": "Altura:",
"mousePositionElevation": "Elev.:",
"mousePositionNoElevation": "N/A",
"elevationLoading": "Inicializar...",
diff --git a/web/client/translations/data.sk-SK.json b/web/client/translations/data.sk-SK.json
index 2aeec5e0e68..fe55b43587a 100644
--- a/web/client/translations/data.sk-SK.json
+++ b/web/client/translations/data.sk-SK.json
@@ -189,6 +189,7 @@
"mousePositionCoordinates": "Súradnice kurzoru",
"mouseCoordinates": "Súradnice:",
"mousePositionCRS": "CRS:",
+ "mousePositionHeight": "Výška:",
"mousePositionElevation": "Elev.:",
"mousePositionNoElevation": "N/A",
"elevationLoading": "Načítanie...",
@@ -2817,6 +2818,12 @@
"description": "Zobrazuje súradnice, na ktoré ukazuje kurzor myši",
"title": "Pozícia myši"
},
+ "CameraPosition": {
+ "description": "Zobrazuje aktuálne súradnice kamery (len v 3D režime)",
+ "title": "Pozícia kamery",
+ "ellipsoidal": "Ellipsoidal",
+ "msl": "MSL"
+ },
"Print": {
"description": "Allows to print the current map with legend",
"title": "Print Tool"
diff --git a/web/client/translations/data.sv-SE.json b/web/client/translations/data.sv-SE.json
index d8b80ee1c53..fd4cc8d0be0 100644
--- a/web/client/translations/data.sv-SE.json
+++ b/web/client/translations/data.sv-SE.json
@@ -305,6 +305,7 @@
"mousePositionCoordinates": "Markörens koordinater",
"mouseCoordinates": "Koordinater:",
"mousePositionCRS": "Kartreferensystem:",
+ "mousePositionHeight": "Höjd:",
"mousePositionElevation": "Höjd:",
"mousePositionNoElevation": "Ej tillgängligt",
"elevationLoading": "Läser in ...",
@@ -3478,6 +3479,12 @@
"description": "Visar koordinaterna för spetsen med muspekaren",
"title": "Musposition"
},
+ "CameraPosition": {
+ "description": "Visar kamerans position i 3D-läge",
+ "title": "Kameraposition",
+ "ellipsoidal": "Ellipsoidal",
+ "msl": "MSL"
+ },
"Print": {
"description": "Gör det möjligt att skriva ut den aktuella kartan med förklaring",
"title": "Utskriftsverktyg"
diff --git a/web/client/translations/data.vi-VN.json b/web/client/translations/data.vi-VN.json
index 4fe57d6edfe..bff0e1ff641 100644
--- a/web/client/translations/data.vi-VN.json
+++ b/web/client/translations/data.vi-VN.json
@@ -910,6 +910,7 @@
"menu": "Menu",
"mouseCoordinates": "Tọa độ:",
"mousePositionCRS": "CRS:",
+ "mousePositionHeight": "Chiều cao:",
"mousePositionCoordinates": "Chỉ báo tọa độ",
"mousePositionElevation": "Độ cao:",
"mousePositionNoElevation": "Không có",
diff --git a/web/client/translations/data.zh-ZH.json b/web/client/translations/data.zh-ZH.json
index 59abadb6fa0..35b3d2e3b2c 100644
--- a/web/client/translations/data.zh-ZH.json
+++ b/web/client/translations/data.zh-ZH.json
@@ -137,6 +137,7 @@
"mousePositionCoordinates": "坐标指示器",
"mouseCoordinates": "坐标:",
"mousePositionCRS": "CRS:",
+ "mousePositionHeight": "高度:",
"mousePositionElevation": "Elev.:",
"mousePositionNoElevation": "N/A",
"elevationLoading": "Loading...",