qwc2
Version:
QGIS Web Client
448 lines (446 loc) • 25.7 kB
JavaScript
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);