UNPKG

qwc2

Version:
448 lines (446 loc) 25.7 kB
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; } function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); } function _possibleConstructorReturn(t, e) { if (e && ("object" == _typeof(e) || "function" == typeof e)) return e; if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined"); return _assertThisInitialized(t); } function _assertThisInitialized(e) { if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); return e; } function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); } function _getPrototypeOf(t) { return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) { return t.__proto__ || Object.getPrototypeOf(t); }, _getPrototypeOf(t); } function _inherits(t, e) { if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function"); t.prototype = Object.create(e && e.prototype, { constructor: { value: t, writable: !0, configurable: !0 } }), Object.defineProperty(t, "prototype", { writable: !1 }), e && _setPrototypeOf(t, e); } function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) { return t.__proto__ = e, t; }, _setPrototypeOf(t, e); } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /** * 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 PropTypes from 'prop-types'; import { addLayer, addLayerFeatures, changeLayerProperty, removeLayer, LayerRole } from '../actions/layers'; import { setCurrentTask } from '../actions/task'; import ResizeableWindow from '../components/ResizeableWindow'; import Spinner from '../components/widgets/Spinner'; import CoordinatesUtils from '../utils/CoordinatesUtils'; import LocaleUtils from '../utils/LocaleUtils'; import MapUtils from '../utils/MapUtils'; import ResourceRegistry from '../utils/ResourceRegistry'; import './style/Cyclomedia.css'; var Status = { LOGIN: 0, INITIALIZING: 1, INITIALIZED: 2, ERROR: 3, LOADPOS: 4, HAVEPOS: 5 }; /** * Cyclomedia integration for QWC2. */ var Cyclomedia = /*#__PURE__*/function (_React$Component) { function Cyclomedia(props) { var _this; _classCallCheck(this, Cyclomedia); _this = _callSuper(this, Cyclomedia, [props]); _defineProperty(_this, "state", { status: Status.LOGIN, message: "", username: "", password: "", loginFailed: false }); _defineProperty(_this, "onClose", function () { _this.props.setCurrentTask(null); _this.setState({ status: Status.LOGIN, loginFailed: false }); _this.iframe = null; }); _defineProperty(_this, "setIframeRef", function (iframe) { if (iframe && iframe !== _this.iframe) { _this.iframe = iframe; clearInterval(_this.iframePollIntervall); _this.iframePollIntervall = setInterval(function () { return _this.setupIframe(iframe); }, 500); } }); _defineProperty(_this, "setupIframe", function (iframe) { if (!iframe.getAttribute("content-set")) { if (iframe.contentWindow && iframe.contentWindow.document) { iframe.setAttribute("content-set", true); iframe.contentWindow.document.open(); iframe.contentWindow.document.write(_this.cyclomediaIndexHtml()); iframe.contentWindow.document.close(); _this.iframe = iframe; } } else if (!iframe.getAttribute("callback-registered")) { if (iframe.contentWindow && iframe.contentWindow.registerCallbacks) { iframe.setAttribute("callback-registered", true); iframe.contentWindow.registerCallbacks(_this.apiInitialized, _this.panoramaPositionChanged, _this.measurementChanged); } } else if (!iframe.getAttribute("init-called")) { if (iframe.contentWindow && iframe.contentWindow.StreetSmartApi) { iframe.setAttribute("init-called", true); iframe.contentWindow.initApi(); } } else { clearInterval(_this.iframePollIntervall); } }); _defineProperty(_this, "apiInitialized", function (success) { var message = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; _this.setState({ status: success ? Status.INITIALIZED : Status.LOGIN, message: message, loginFailed: !success }); }); _defineProperty(_this, "panoramaPositionChanged", function (posData) { if (_this.state.status !== Status.HAVEPOS) { _this.setState({ status: Status.HAVEPOS }); } var scale = 50; var angle = posData.hFov / 2.0; var width = Math.sin(angle); var length = Math.sqrt(1.0 - width * width); var size = scale / Math.sqrt(width * length); var coordinates = [[0, 0], [size * width * 2, 0], [size * width, size * length]]; var dimensions = [coordinates[1][0] + 0.5, coordinates[2][1] + 0.5]; var canvas = document.createElement('canvas'); canvas.width = dimensions[0]; canvas.height = dimensions[1]; var context = canvas.getContext('2d'); context.fillStyle = 'rgba(255, 0, 0, 0.5)'; context.strokeStyle = '#FF0000'; context.lineWidth = 1; context.beginPath(); context.moveTo(coordinates[0][0], coordinates[0][1]); coordinates.slice(1).forEach(function (coo) { return context.lineTo(coo[0], coo[1]); }); context.closePath(); context.fill(); ResourceRegistry.addResource("cyclomedia-cone", context.canvas.toDataURL()); var feature = { geometry: { type: 'Point', coordinates: posData.pos }, crs: posData.crs, styleName: 'image', styleOptions: { img: "cyclomedia-cone", rotation: posData.yaw, size: dimensions, anchor: [0.5, 1] } }; var layer = { id: "cyclomedia-cone", role: LayerRole.MARKER }; _this.props.addLayerFeatures(layer, [feature], true); }); _defineProperty(_this, "measurementChanged", function (measurement) { if (_this.props.displayMeasurements) { if (measurement) { var layer = { id: "cyclomedia-measurements", role: LayerRole.MARKER, crs: measurement.crs.properties.name, styleOptions: { strokeColor: 'red', strokeWidth: 4, fillColor: [255, 0, 0, 0.25], strokeDash: [] } }; _this.props.addLayerFeatures(layer, measurement.features, true); } else { _this.props.removeLayer("cyclomedia-measurements"); } } }); _defineProperty(_this, "cyclomediaIndexHtml", function () { var supportedLang = ["de", "en-GB", "en-US", "fi", "fr", "nl", "tr", "pl"]; var lang = LocaleUtils.lang(); if (supportedLang.indexOf(lang) < 0) { lang = lang.slice(0, 2); if (supportedLang.indexOf(lang) < 0) { lang = "en-US"; } } var loginOauth = !!_this.props.clientId && !_this.state.loginFailed; return "\n <!DOCTYPE html>\n <html>\n <head>\n <script type=\"text/javascript\" src=\"https://unpkg.com/react@18.3.1/umd/react.production.min.js\"></script>\n <script type=\"text/javascript\" src=\"https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js\"></script>\n <script type=\"text/javascript\" src=\"https://streetsmart.cyclomedia.com/api/v".concat(_this.props.cyclomediaVersion, "/StreetSmartApi.js\"></script>\n <script type=\"text/javascript\">\n let apiInitialized = false;\n let initCallback = null;\n let posCallback = null;\n let measureCallback = null;\n\n function initApi() {\n StreetSmartApi.init({\n targetElement: document.getElementById(\"streetsmartApi\"),\n username: \"").concat(_this.state.username || undefined, "\",\n password: \"").concat(_this.state.password || undefined, "\",\n apiKey: \"").concat(_this.props.apikey, "\",\n clientId: \"").concat(_this.props.clientId, "\",\n loginOauth: ").concat(loginOauth, ",\n loginRedirectUri: \"").concat(_this.props.loginRedirectUri, "\",\n logoutRedirectUri: \"").concat(_this.props.logoutRedirectUri, "\",\n srs: \"").concat(_this.props.projection, "\",\n locale: \"").concat(lang, "\",\n configurationUrl: 'https://atlas.cyclomedia.com/configuration',\n addressSettings: {\n locale: \"us\",\n database: \"Nokia\"\n }\n }).then(() => {\n apiInitialized = true;\n if (initCallback) {\n initCallback(true);\n }\n }, (e) => {\n apiInitialized = false;\n if (initCallback) {\n initCallback(false, e.message);\n }\n });\n }\n function openImage(posStr, crs) {\n if (!apiInitialized) {\n return;\n }\n StreetSmartApi.open(posStr, {\n viewerType: StreetSmartApi.ViewerType.PANORAMA,\n srs: crs,\n panoramaViewer: {\n closable: false,\n maximizable: true,\n replace: true,\n recordingsVisible: true,\n navbarVisible: true,\n timeTravelVisible: true,\n measureTypeButtonVisible: true,\n measureTypeButtonStart: true,\n measureTypeButtonToggle: true,\n },\n }).then((result) => {\n if (result && result[0]){\n window.panoramaViewer = result[0];\n window.panoramaViewer.on(StreetSmartApi.Events.panoramaViewer.IMAGE_CHANGE, changeView);\n window.panoramaViewer.on(StreetSmartApi.Events.panoramaViewer.VIEW_CHANGE, changeView);\n StreetSmartApi.on(StreetSmartApi.Events.measurement.MEASUREMENT_CHANGED, changeMeasurement);\n StreetSmartApi.on(StreetSmartApi.Events.measurement.MEASUREMENT_STOPPED, stopMeasurement);\n }\n }).catch((reason) => {\n console.log('Failed to create component(s) through API: ' + reason);\n });\n }\n function changeView() {\n if (posCallback) {\n const recording = window.panoramaViewer.getRecording();\n const orientation = window.panoramaViewer.getOrientation();\n const pos = recording.xyz;\n const posData = {\n pos: [pos[0], pos[1]],\n crs: recording.srs,\n yaw: orientation.yaw * Math.PI / 180,\n hFov: orientation.hFov * Math.PI / 180.0\n }\n posCallback(posData);\n }\n }\n function changeMeasurement(e) {\n measureCallback(e.detail.activeMeasurement);\n }\n function stopMeasurement() {\n measureCallback(null);\n }\n function registerCallbacks(_initCallback, _posCallback, _measureCallback) {\n initCallback = _initCallback;\n posCallback = _posCallback;\n measureCallback = _measureCallback;\n }\n </script>\n <style>\n html, body, #streetsmartApi {height: 100%;}\n </style>\n </head>\n <body style=\"margin: 0\">\n <div id=\"streetsmartApi\">\n </div>\n </body>\n </html>\n "); }); _defineProperty(_this, "addRecordingsWFS", function () { var layer = { id: 'cyclomedia-recordings', type: 'wfs', loader: function loader(vectorSource, extent, resolution, projection, success, failure) { var bbox = CoordinatesUtils.reprojectBbox(extent, projection.getCode(), _this.props.mapCrs); var bboxstr = bbox.join(","); var reqUrl = "https://atlasapi.cyclomedia.com/api/recording/wfs?service=WFS&version=1.1.0&request=GetFeature&typename=atlas:Recording&srsname=".concat(_this.props.mapCrs, "&bbox=").concat(bboxstr, "&maxFeatures=10000000"); var xhr = new XMLHttpRequest(); xhr.open('GET', reqUrl); xhr.setRequestHeader("Authorization", "Basic " + btoa(_this.state.username + ":" + _this.state.password)); var onError = function onError() { vectorSource.removeLoadedExtent(extent); failure(); }; xhr.onerror = onError; xhr.onload = function () { if (xhr.status === 200) { var features = vectorSource.getFormat().readFeatures(xhr.responseText, { dataProjection: _this.props.mapCrs, featureProjection: projection.getCode() }); vectorSource.addFeatures(features); success(features); } else { onError(); } }; xhr.send(); }, name: 'atlas:Recording', version: '1.1.0', projection: _this.props.mapCrs, formats: ['text/xml; subtype=gml/3.1.1'], invertAxisOrientation: true, role: LayerRole.SELECTION, color: '#6666FF', visibility: _this.props.mapScale <= _this.props.maxMapScale }; _this.props.addLayer(layer); }); _defineProperty(_this, "queryPoint", function (prevProps) { if (_this.props.click === prevProps.click) { return null; } var cmFeature = _this.props.click.features.find(function (feature) { return feature.layerId === 'cyclomedia-recordings'; }); return cmFeature ? cmFeature.geometry.coordinates : null; }); _this.iframe = null; _this.iframePollIntervall = null; if (props.credentialUserInfoFields && props.userInfos) { _this.state.username = props.userInfos[props.credentialUserInfoFields.username]; _this.state.password = props.userInfos[props.credentialUserInfoFields.password]; } return _this; } _inherits(Cyclomedia, _React$Component); return _createClass(Cyclomedia, [{ key: "componentDidUpdate", value: function componentDidUpdate(prevProps, prevState) { if (!prevProps.active && this.props.active) { this.setState({ status: this.props.clientId ? Status.INITIALIZING : Status.LOGIN, loginFailed: false }); } else if (prevProps.active && !this.props.active || prevProps.theme && !this.props.theme) { this.onClose(); } // Load WFS when loading if (this.state.status === Status.INITIALIZING && prevState.status < Status.INITIALIZING) { this.addRecordingsWFS(); } // Handle map click events if ((this.state.status === Status.INITIALIZED || this.state.status === Status.HAVEPOS) && this.iframe) { var clickPoint = this.queryPoint(prevProps); if (clickPoint) { var posStr = clickPoint[0] + "," + clickPoint[1]; this.iframe.contentWindow.openImage(posStr, this.props.mapCrs); if (this.state.status !== Status.LOADPOS) { this.setState({ status: Status.LOADPOS }); this.props.removeLayer('cyclomedia-cone'); this.props.removeLayer('cyclomedia-measurements'); ResourceRegistry.removeResource("cyclomedia-cone"); } } } if (this.props.active && this.props.mapScale !== prevProps.mapScale) { this.props.changeLayerProperty('cyclomedia-recordings', 'visibility', this.props.mapScale <= this.props.maxMapScale); } if (this.state.status === Status.LOGIN && prevState.status > Status.LOGIN) { this.props.removeLayer('cyclomedia-recordings'); this.props.removeLayer('cyclomedia-cone'); this.props.removeLayer('cyclomedia-measurements'); ResourceRegistry.removeResource("cyclomedia-cone"); } } }, { key: "render", value: function render() { var _this2 = this; if (!this.props.active) { return null; } var overlay = null; if (this.state.status === Status.LOGIN) { overlay = /*#__PURE__*/React.createElement("div", { className: "cyclomedia-body-overlay" }, /*#__PURE__*/React.createElement("div", { className: "cyclomedia-login" }, /*#__PURE__*/React.createElement("table", null, /*#__PURE__*/React.createElement("tbody", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", null, "Username:"), /*#__PURE__*/React.createElement("td", null, /*#__PURE__*/React.createElement("input", { onChange: function onChange(ev) { return _this2.setState({ username: ev.target.value }); }, type: "text", value: this.state.username }))), /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", null, "Password:"), /*#__PURE__*/React.createElement("td", null, /*#__PURE__*/React.createElement("input", { onChange: function onChange(ev) { return _this2.setState({ password: ev.target.value }); }, type: "password", value: this.state.password }))), /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", { colSpan: "2" }, /*#__PURE__*/React.createElement("button", { className: "button", disabled: !this.state.username, onClick: function onClick() { return _this2.setState({ status: Status.INITIALIZING }); }, type: "button" }, LocaleUtils.tr("cyclomedia.login")))), /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", { className: "cyclomedia-login-message", colSpan: "2" }, this.state.message)))))); } else if (this.state.status === Status.INITIALIZING) { overlay = /*#__PURE__*/React.createElement("div", { className: "cyclomedia-body-overlay" }, /*#__PURE__*/React.createElement(Spinner, null), /*#__PURE__*/React.createElement("span", null, LocaleUtils.tr("cyclomedia.initializing"))); } else if (this.state.status === Status.ERROR) { overlay = /*#__PURE__*/React.createElement("div", { className: "cyclomedia-body-overlay" }, /*#__PURE__*/React.createElement("span", null, LocaleUtils.tr("cyclomedia.loaderror"))); } else if (this.state.status === Status.INITIALIZED) { overlay = /*#__PURE__*/React.createElement("div", { className: "cyclomedia-body-overlay" }, /*#__PURE__*/React.createElement("span", null, LocaleUtils.tr("cyclomedia.clickonmap"))); } else if (this.state.status === Status.LOADPOS) { overlay = /*#__PURE__*/React.createElement("div", { className: "cyclomedia-body-overlay" }, /*#__PURE__*/React.createElement(Spinner, null), /*#__PURE__*/React.createElement("span", null, LocaleUtils.tr("cyclomedia.loading"))); } return /*#__PURE__*/React.createElement(ResizeableWindow, { dockable: this.props.geometry.side, icon: "cyclomedia", initialHeight: this.props.geometry.initialHeight, initialWidth: this.props.geometry.initialWidth, initialX: this.props.geometry.initialX, initialY: this.props.geometry.initialY, initiallyDocked: this.props.geometry.initiallyDocked, onClose: this.onClose, splitScreenWhenDocked: true, title: LocaleUtils.tr("cyclomedia.title"), usePortal: false }, /*#__PURE__*/React.createElement("div", { className: "cyclomedia-body" }, this.props.mapScale > this.props.maxMapScale && this.state.status > Status.LOGIN ? /*#__PURE__*/React.createElement("div", { className: "cyclomedia-scale-hint" }, LocaleUtils.tr("cyclomedia.scalehint", this.props.maxMapScale)) : null, this.state.status > Status.LOGIN ? /*#__PURE__*/React.createElement("iframe", { className: "cyclomedia-frame", onLoad: function onLoad(ev) { return _this2.setupIframe(ev.target); }, ref: function ref(el) { return _this2.setIframeRef(el); } }) : null, overlay)); } }]); }(React.Component); _defineProperty(Cyclomedia, "propTypes", { active: PropTypes.bool, addLayer: PropTypes.func, addLayerFeatures: PropTypes.func, /** The Cyclomedia API key */ apikey: PropTypes.string, changeLayerProperty: PropTypes.func, click: PropTypes.object, /** OAuth client ID. */ clientId: PropTypes.string, /** Fields from user_infos which contain username and password which will be pre-inserted into the login form. */ credentialUserInfoFields: PropTypes.shape({ username: PropTypes.string, password: PropTypes.string }), /** The cyclomedia version. */ cyclomediaVersion: PropTypes.string, /** Whether to display Cyclomedia measurement geometries on the map. */ displayMeasurements: PropTypes.bool, /** Default window geometry with size, position and docking status. Positive position values (including '0') are related to top (InitialY) and left (InitialX), negative values (including '-0') to bottom (InitialY) and right (InitialX). */ geometry: PropTypes.shape({ initialWidth: PropTypes.number, initialHeight: PropTypes.number, initialX: PropTypes.number, initialY: PropTypes.number, initiallyDocked: PropTypes.bool, side: PropTypes.string }), /** The relative path to the redirect login handling of oauth. */ loginRedirectUri: PropTypes.string, /** The relative path to the redirect logout handling of oauth. */ logoutRedirectUri: PropTypes.string, mapCrs: PropTypes.string, mapScale: PropTypes.number, /** The maximum map scale above which the recordings WFS won't be displayed. */ maxMapScale: PropTypes.number, /** The projection to use for Cyclomedia. */ projection: PropTypes.string, removeLayer: PropTypes.func, setCurrentTask: PropTypes.func, theme: PropTypes.object, userInfos: PropTypes.object }); _defineProperty(Cyclomedia, "defaultProps", { cyclomediaVersion: '25.7', displayMeasurements: true, geometry: { initialWidth: 480, initialHeight: 640, initialX: 0, initialY: 0, initiallyDocked: false, side: 'left' }, maxMapScale: 5000, projection: 'EPSG:3857' }); export default connect(function (state) { return { active: state.task.id === "Cyclomedia", click: state.map.click, mapCrs: state.map.projection, mapScale: MapUtils.computeForZoom(state.map.scales, state.map.zoom), theme: state.theme.current, userInfos: state.localConfig.user_infos }; }, { addLayer: addLayer, addLayerFeatures: addLayerFeatures, changeLayerProperty: changeLayerProperty, removeLayer: removeLayer, setCurrentTask: setCurrentTask })(Cyclomedia);