diff --git a/package.json b/package.json index cb5f57e09..6b58579f2 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@loaders.gl/core": "^4.3.3", "@loaders.gl/shapefile": "^4.3.3", "@loaders.gl/zip": "^4.3.3", + "@panoramax/web-viewer": "^3.2.2-develop-34e6d6f5", "@reduxjs/toolkit": "^2.4.0", "@turf/buffer": "^6.5.0", "@turf/helpers": "^6.5.0", diff --git a/plugins/Panoramax.jsx b/plugins/Panoramax.jsx new file mode 100644 index 000000000..a7bdc7716 --- /dev/null +++ b/plugins/Panoramax.jsx @@ -0,0 +1,258 @@ +/** + * Copyright 2024 Sourcepole AG + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import {connect} from 'react-redux'; + +import * as PanoViewer from '@panoramax/web-viewer'; +import axios from 'axios'; +import PropTypes from 'prop-types'; +import ConfigUtils from 'qwc2/utils/ConfigUtils'; +import CoordinatesUtils from 'qwc2/utils/CoordinatesUtils'; +import MapUtils from 'qwc2/utils/MapUtils'; +import ResourceRegistry from 'qwc2/utils/ResourceRegistry'; + +import {addLayer, addLayerFeatures, removeLayer, LayerRole} from '../actions/layers'; +import {setCurrentTask} from '../actions/task'; +import MapSelection from '../components/MapSelection'; +import ResizeableWindow from '../components/ResizeableWindow'; +import LocaleUtils from '../utils/LocaleUtils'; + +import './style/Panoramax.css'; +import '@panoramax/web-viewer/build/index.css'; + +class Panoramax extends React.Component { + static propTypes = { + active: PropTypes.bool, + addLayer: PropTypes.func, + addLayerFeatures: PropTypes.func, + geometry: PropTypes.shape({ + initialWidth: PropTypes.number, + initialHeight: PropTypes.number, + initialX: PropTypes.number, + initialY: PropTypes.number, + initiallyDocked: PropTypes.bool, + side: PropTypes.string + }), + loadSequencesTiles: PropTypes.bool, + panoramaxInstance: PropTypes.string, + removeLayer: PropTypes.func, + setCurrentTask: PropTypes.func, + theme: PropTypes.object, + tileMode: PropTypes.string, + wmsUrl: PropTypes.string + }; + static defaultProps = { + geometry: { + initialWidth: 640, + initialHeight: 640, + initialX: 0, + initialY: 0, + initiallyDocked: false, + side: 'left' + + }, + loadSequencesTiles: true, + panoramaxInstance: 'api.panoramax.xyz', + tileMode: 'mvt' + }; + state = { + lon: null, + lat: null, + queryImage: null, + selectionActive: false, + selectionGeom: null, + yaw: null, + currentTooltip: '' + }; + constructor(props) { + super(props); + this.viewerRef = React.createRef(); + } + componentDidUpdate(prevProps, prevState) { + if (!prevProps.active && this.props.active) { + this.setState({selectionActive: true}); + if (this.props.loadSequencesTiles) { + if (this.props.tileMode === "wms" && this.props.wmsUrl) { + this.addRecordingsWMS(); + } else { + this.addRecordingsMVT(); + } + } + } else if ( this.state.selectionGeom && + this.state.selectionGeom !== prevState.selectionGeom) { + this.queryPoint(this.state.selectionGeom); + } else if (this.state.queryImage && !this.viewer && this.state.selectionGeom) { + this.initializeViewer(this.state.queryImage); + } else if (this.viewer && this.state.queryImage !== prevState.queryImage) { + this.viewer.select(null, this.state.queryImage, true); + } + } + componentWillUnmount() { + this.onClose(); + } + onClose = () => { + this.props.setCurrentTask(null); + this.props.removeLayer('panoramax-recordings'); + this.props.removeLayer('panoramaxselection'); + this.setState({selectionGeom: null, queryImage: null, lon: null, lat: null, selectionActive: null, yaw: null, currentTooltip: ''}); + if (this.viewer) { + this.viewer.stopSequence(); + this.viewer.destroy(); + delete this.viewer; + } + ResourceRegistry.removeResource('selected'); + }; + render() { + if (!this.props.active) { + return null; + } + const { selectionGeom, queryImage } = this.state; + return ( + <> + {selectionGeom && ( + +
+ {!queryImage && !this.viewer ? ( +
+

{LocaleUtils.tr("panoramax.notfound")}

+
+ ) : ( +
+ )} +
+ + )} + this.setState({ selectionGeom: geom })} + styleOptions={{ fillColor: [0, 0, 0, 0], strokeColor: [0, 0, 0, 0] }} + /> + + ); + } + initializeViewer = (image) => { + const viewerElement = this.viewerRef.current; + if (viewerElement) { + this.viewer = new PanoViewer.Viewer( + viewerElement, + `https://${this.props.panoramaxInstance}/api`, + { + map: false, + selectedPicture: image + } + ); + this.viewer.addEventListener('psv:picture-loading', (event) => { + this.setState( + { + lon: event.detail.lon, + lat: event.detail.lat + }, + () => this.handlePanoramaxEvent() + ); + }); + this.viewer.addEventListener('psv:view-rotated', (event) => { + this.setState( + { yaw: event.detail.x }, + () => this.handlePanoramaxEvent() + ); + }); + this.viewer.addEventListener('psv:picture-loaded', (event) => { + this.setState( + { yaw: event.detail.x }, + () => this.handlePanoramaxEvent() + ); + }); + } + }; + handlePanoramaxEvent = () => { + ResourceRegistry.addResource('selected', `${ConfigUtils.getAssetsPath()}/img/panoramax-cursor.svg`); + const layer = { + id: "panoramaxselection", + role: LayerRole.SELECTION + }; + const feature = { + geometry: { + type: 'Point', + coordinates: [this.state.lon, this.state.lat] + }, + crs: 'EPSG:4326', + styleName: 'image', + styleOptions: { + img: 'selected', + rotation: MapUtils.degreesToRadians(this.state.yaw), + anchor: [0.5, 0.5] + } + }; + this.props.addLayerFeatures(layer, [feature], true); + }; + addRecordingsMVT = () => { + const resolutions = MapUtils.getResolutionsForScales(this.props.theme.scales, this.props.theme.mapCrs); + const layer = { + id: 'panoramax-recordings', + type: 'mvt', + projection: this.props.theme.mapCrs, + tileGridConfig: { + origin: [0, 0], + resolutions: resolutions + }, + style: `https://${this.props.panoramaxInstance}/api/map/style.json`, + role: LayerRole.USERLAYER + }; + this.props.addLayer(layer); + }; + addRecordingsWMS = () => { + const layer = { + id: 'panoramax-recordings', + type: 'wms', + projection: this.props.theme.mapCrs, + url: this.props.wmsUrl, + role: LayerRole.USERLAYER + }; + this.props.addLayer(layer); + }; + queryPoint = (props) => { + const [centerX, centerY] = CoordinatesUtils.reproject(props.coordinates, this.props.theme.mapCrs, 'EPSG:4326'); + const offset = 0.001; + const bbox = `${centerX - offset},${centerY - offset},${centerX + offset},${centerY + offset}`; + axios.get(`https://${this.props.panoramaxInstance}/api/search?bbox=${bbox}`) + .then(response => { + + this.setState({ queryImage: response.data.features[0].id }); + }) + .catch(() => { + this.setState({ queryImage: null }); + }); + }; +} + +export default connect((state) => ({ + active: state.task.id === "Panoramax", + click: state.map.click, + mapScale: MapUtils.computeForZoom(state.map.scales, state.map.zoom), + theme: state.theme.current +}), { + addLayer: addLayer, + addLayerFeatures: addLayerFeatures, + removeLayer: removeLayer, + setCurrentTask: setCurrentTask +})(Panoramax); diff --git a/plugins/style/Panoramax.css b/plugins/style/Panoramax.css new file mode 100644 index 000000000..1a2156e68 --- /dev/null +++ b/plugins/style/Panoramax.css @@ -0,0 +1,15 @@ +div.panoramax-body { + height: calc(100% + 0.5em); + position: relative; + display: flex; + flex-direction: column; + margin: -0.25em; +} + +div.panoramax-body .gvs-psv-tour-arrows{ + all: unset; +} + +div.panoramax-body .gvs-btn:focus{ + outline: none; +} diff --git a/translations/ca-ES.json b/translations/ca-ES.json index 6423afc1f..57669c880 100644 --- a/translations/ca-ES.json +++ b/translations/ca-ES.json @@ -43,6 +43,7 @@ "MeasureLineString": "Mesurar una línia", "MeasurePolygon": "Mesurar un polígon", "NewsPopup": "", + "Panoramax": "", "Portal": "", "PrintScreen3D": "", "Redlining": "Línia de demarcació", @@ -373,6 +374,10 @@ "width": "", "windowtitle": "" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "" }, diff --git a/translations/cs-CZ.json b/translations/cs-CZ.json index 1708b9436..c0711cb11 100644 --- a/translations/cs-CZ.json +++ b/translations/cs-CZ.json @@ -43,6 +43,7 @@ "MeasureLineString": "Změřit úsek", "MeasurePolygon": "Měřit mnohoúhelník", "NewsPopup": "", + "Panoramax": "", "Portal": "", "PrintScreen3D": "", "Redlining": "Připomínkování", @@ -373,6 +374,10 @@ "width": "", "windowtitle": "" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "" }, diff --git a/translations/de-CH.json b/translations/de-CH.json index 39ea53545..9117741a5 100644 --- a/translations/de-CH.json +++ b/translations/de-CH.json @@ -43,6 +43,7 @@ "MeasureLineString": "Messen Linie", "MeasurePolygon": "Messen Polygon", "NewsPopup": "Aktuelles", + "Panoramax": "", "Portal": "Portal", "PrintScreen3D": "Raster Export", "Redlining": "Zeichnen", @@ -373,6 +374,10 @@ "width": "Breite", "windowtitle": "Numerische Eingabe" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "Abfragen..." }, diff --git a/translations/de-DE.json b/translations/de-DE.json index 34f481d9b..9c64025cf 100644 --- a/translations/de-DE.json +++ b/translations/de-DE.json @@ -43,6 +43,7 @@ "MeasureLineString": "Messen Linie", "MeasurePolygon": "Messen Polygon", "NewsPopup": "Aktuelles", + "Panoramax": "", "Portal": "Portal", "PrintScreen3D": "Raster Export", "Redlining": "Zeichnen", @@ -373,6 +374,10 @@ "width": "Breite", "windowtitle": "Numerische Eingabe" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "Abfragen..." }, diff --git a/translations/en-US.json b/translations/en-US.json index 60a2769d7..1d1e758e8 100644 --- a/translations/en-US.json +++ b/translations/en-US.json @@ -43,6 +43,7 @@ "MeasureLineString": "Measure a line", "MeasurePolygon": "Measure a polygon", "NewsPopup": "News", + "Panoramax": "Panoramax", "Portal": "Portal", "PrintScreen3D": "Raster Export", "Redlining": "Redlining", @@ -373,6 +374,10 @@ "width": "Width", "windowtitle": "Numeric input" }, + "panoramax": { + "notfound": "There is no image available for this location.", + "title": "Panoramax Viewer" + }, "pickfeature": { "querying": "Querying..." }, diff --git a/translations/es-ES.json b/translations/es-ES.json index 5e8cb4157..ec6a35722 100644 --- a/translations/es-ES.json +++ b/translations/es-ES.json @@ -43,6 +43,7 @@ "MeasureLineString": "Medir una línea", "MeasurePolygon": "Medir un polígono", "NewsPopup": "", + "Panoramax": "", "Portal": "", "PrintScreen3D": "", "Redlining": "Línea de demarcación", @@ -373,6 +374,10 @@ "width": "", "windowtitle": "" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "" }, diff --git a/translations/fi-FI.json b/translations/fi-FI.json index ce5cf0ebe..e955f36d5 100644 --- a/translations/fi-FI.json +++ b/translations/fi-FI.json @@ -43,6 +43,7 @@ "MeasureLineString": "Mittaa viiva", "MeasurePolygon": "Mittaa monikulmion", "NewsPopup": "", + "Panoramax": "", "Portal": "", "PrintScreen3D": "", "Redlining": "Omat merkinnät", @@ -373,6 +374,10 @@ "width": "", "windowtitle": "" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "" }, diff --git a/translations/fr-FR.json b/translations/fr-FR.json index c73963268..b4dc5b0a5 100644 --- a/translations/fr-FR.json +++ b/translations/fr-FR.json @@ -43,6 +43,7 @@ "MeasureLineString": "Mesurer une ligne", "MeasurePolygon": "Mesurer un polygone", "NewsPopup": "Actualités", + "Panoramax": "Panoramax", "Portal": "Portail", "PrintScreen3D": "Export raster", "Redlining": "Dessiner", @@ -373,6 +374,10 @@ "width": "Largeur", "windowtitle": "Entrée numérique" }, + "panoramax": { + "notfound": "Il n'y a pas d'image disponible pour cet emplacement.", + "title": "Visionneuse Panoramax" + }, "pickfeature": { "querying": "Interrogation" }, diff --git a/translations/hu-HU.json b/translations/hu-HU.json index 95e630861..786b2a5da 100644 --- a/translations/hu-HU.json +++ b/translations/hu-HU.json @@ -43,6 +43,7 @@ "MeasureLineString": "Mérési vonal", "MeasurePolygon": "Mérési sokszög", "NewsPopup": "", + "Panoramax": "", "Portal": "", "PrintScreen3D": "", "Redlining": "Vázlat", @@ -373,6 +374,10 @@ "width": "", "windowtitle": "" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "" }, diff --git a/translations/it-IT.json b/translations/it-IT.json index c56dc6497..2f4b253ed 100644 --- a/translations/it-IT.json +++ b/translations/it-IT.json @@ -43,6 +43,7 @@ "MeasureLineString": "Misura una linea", "MeasurePolygon": "Misura un poligono", "NewsPopup": "Attualità", + "Panoramax": "", "Portal": "Portale", "PrintScreen3D": "Esporta raster", "Redlining": "Strumenti di disegno", @@ -373,6 +374,10 @@ "width": "Larghezza", "windowtitle": "Formulario numerico" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "Query..." }, diff --git a/translations/ja-JP.json b/translations/ja-JP.json index 68e6f0885..6e5892b94 100644 --- a/translations/ja-JP.json +++ b/translations/ja-JP.json @@ -43,6 +43,7 @@ "MeasureLineString": "線を計測", "MeasurePolygon": "ポリゴンを計測", "NewsPopup": "ニュース", + "Panoramax": "", "Portal": "ポータル", "PrintScreen3D": "ラスタ・エクスポート", "Redlining": "赤線引き", @@ -373,6 +374,10 @@ "width": "幅", "windowtitle": "数値入力" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "検索中..." }, diff --git a/translations/no-NO.json b/translations/no-NO.json index 83aa5f10c..7efb7e8b0 100644 --- a/translations/no-NO.json +++ b/translations/no-NO.json @@ -43,6 +43,7 @@ "MeasureLineString": "Måle en linje", "MeasurePolygon": "Måle en polygon", "NewsPopup": "", + "Panoramax": "", "Portal": "", "PrintScreen3D": "", "Redlining": "Redlining", @@ -373,6 +374,10 @@ "width": "", "windowtitle": "" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "" }, diff --git a/translations/pl-PL.json b/translations/pl-PL.json index 9f91c73b3..7aeab24d2 100644 --- a/translations/pl-PL.json +++ b/translations/pl-PL.json @@ -43,6 +43,7 @@ "MeasureLineString": "Zmierzyć linię", "MeasurePolygon": "Zmierzyć wielokąt", "NewsPopup": "", + "Panoramax": "", "Portal": "", "PrintScreen3D": "", "Redlining": "Redlining", @@ -373,6 +374,10 @@ "width": "", "windowtitle": "" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "" }, diff --git a/translations/pt-BR.json b/translations/pt-BR.json index 6649c54d6..36d93dc8a 100644 --- a/translations/pt-BR.json +++ b/translations/pt-BR.json @@ -43,6 +43,7 @@ "MeasureLineString": "Medir uma linha", "MeasurePolygon": "Medir um polígono", "NewsPopup": "", + "Panoramax": "", "Portal": "", "PrintScreen3D": "", "Redlining": "Linha de demarcação", @@ -373,6 +374,10 @@ "width": "Largura", "windowtitle": "Título da janela" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "" }, diff --git a/translations/pt-PT.json b/translations/pt-PT.json index 22cb13047..a8868f908 100644 --- a/translations/pt-PT.json +++ b/translations/pt-PT.json @@ -43,6 +43,7 @@ "MeasureLineString": "Medir Linha", "MeasurePolygon": "Medir Polígono", "NewsPopup": "", + "Panoramax": "", "Portal": "", "PrintScreen3D": "", "Redlining": "Marcação", @@ -373,6 +374,10 @@ "width": "Largura", "windowtitle": "Entrada Numérica" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "" }, diff --git a/translations/ro-RO.json b/translations/ro-RO.json index f1081e62b..605d792f0 100644 --- a/translations/ro-RO.json +++ b/translations/ro-RO.json @@ -43,6 +43,7 @@ "MeasureLineString": "Măsoară o linie", "MeasurePolygon": "Măsoară un poligon", "NewsPopup": "", + "Panoramax": "", "Portal": "", "PrintScreen3D": "", "Redlining": "Evidențiere", @@ -373,6 +374,10 @@ "width": "Lățimea", "windowtitle": "Valori numerice" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "" }, diff --git a/translations/ru-RU.json b/translations/ru-RU.json index 984d3c91a..c4b69053d 100644 --- a/translations/ru-RU.json +++ b/translations/ru-RU.json @@ -43,6 +43,7 @@ "MeasureLineString": "измерить линию", "MeasurePolygon": "измерить многоугольник", "NewsPopup": "", + "Panoramax": "", "Portal": "", "PrintScreen3D": "", "Redlining": "Обводка", @@ -373,6 +374,10 @@ "width": "", "windowtitle": "" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "" }, diff --git a/translations/sv-SE.json b/translations/sv-SE.json index b9ac692fa..df527e155 100644 --- a/translations/sv-SE.json +++ b/translations/sv-SE.json @@ -43,6 +43,7 @@ "MeasureLineString": "Mäta en linje", "MeasurePolygon": "Mäta en polygon", "NewsPopup": "", + "Panoramax": "", "Portal": "", "PrintScreen3D": "", "Redlining": "Rita", @@ -373,6 +374,10 @@ "width": "", "windowtitle": "" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "" }, diff --git a/translations/tr-TR.json b/translations/tr-TR.json index 2794b2a16..5a9b67571 100644 --- a/translations/tr-TR.json +++ b/translations/tr-TR.json @@ -43,6 +43,7 @@ "MeasureLineString": "Çizgi Ölç", "MeasurePolygon": "Alan Ölç", "NewsPopup": "", + "Panoramax": "", "Portal": "Portal", "PrintScreen3D": "Görüntüyü ver", "Redlining": "Serbest Çizim", @@ -373,6 +374,10 @@ "width": "Genişlik", "windowtitle": "Sayısal giriş" }, + "panoramax": { + "notfound": "", + "title": "" + }, "pickfeature": { "querying": "Sorgulanıyor..." }, diff --git a/translations/tsconfig.json b/translations/tsconfig.json index 8c9f590b7..766398759 100644 --- a/translations/tsconfig.json +++ b/translations/tsconfig.json @@ -40,6 +40,7 @@ "appmenu.items.MeasureLineString", "appmenu.items.MeasurePolygon", "appmenu.items.NewsPopup", + "appmenu.items.Panoramax", "appmenu.items.Portal", "appmenu.items.PrintScreen3D", "appmenu.items.Redlining", @@ -318,6 +319,8 @@ "numericinput.side", "numericinput.width", "numericinput.windowtitle", + "panoramax.notfound", + "panoramax.title", "pickfeature.querying", "portal.filter", "portal.menulabel", diff --git a/utils/FeatureStyles.js b/utils/FeatureStyles.js index dfba34227..3617a2369 100644 --- a/utils/FeatureStyles.js +++ b/utils/FeatureStyles.js @@ -368,7 +368,7 @@ export default { image: new ol.style.Icon({ src: ResourceRegistry.getResource(options.img), rotation: options.rotation, - anchor: [0.5, 1], + anchor: options.anchor, imgSize: options.size, rotateWithView: true })