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 ?