qwc2
Version:
QGIS Web Client
526 lines (524 loc) • 25.2 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 ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
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 2025 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 axios from 'axios';
import isEqual from 'lodash.isequal';
import ol from 'openlayers';
import PropTypes from 'prop-types';
import { zoomToExtent } from '../actions/map';
import { setCurrentTask } from '../actions/task';
import Icon from '../components/Icon';
import OverviewMapButton from '../components/OverviewMapButton';
import ResizeableWindow from '../components/ResizeableWindow';
import OlLayer from '../components/map/OlLayer';
import InputContainer from '../components/widgets/InputContainer';
import ConfigUtils from '../utils/ConfigUtils';
import CoordinatesUtils from '../utils/CoordinatesUtils';
import LocaleUtils from '../utils/LocaleUtils';
import MapUtils from '../utils/MapUtils';
import MiscUtils from '../utils/MiscUtils';
import './style/ObliqueView.css';
/**
* Display oblique satellite imagery.
*
* Requires `obliqueImageryServiceUrl` in `config.json` to point to a `qwc-oblique-imagery-service`.
*
* You can configure oblique imagery datasets in the `obliqueDatasets` entry in a QWC theme configuration as follows:
* ```
* {
* ...
* "obliqueDatasets": [{
* {
* "name": "<dataset_name>",
* "default": <false|true>,
* "backgroundLayer": "<background_layer_name>",
* "backgroundOpacity": <0-255>,
* "title": "<dataset_title>",
* "titleMsgId": "<dataset_title_msgid>"
* },
* ...
* ]
* }
* ```
* where `dataset_name` is the the name of a dataset configured in the `qwc-oblique-imagery-service`.
*/
var ObliqueView = /*#__PURE__*/function (_React$Component) {
function ObliqueView(props) {
var _this;
_classCallCheck(this, ObliqueView);
_this = _callSuper(this, ObliqueView, [props]);
_defineProperty(_this, "onClose", function () {
_this.setState(ObliqueView.defaultState);
});
_defineProperty(_this, "renderScaleChooser", function () {
return /*#__PURE__*/React.createElement("div", {
className: "obliqueview-scalechooser"
}, /*#__PURE__*/React.createElement("span", null, LocaleUtils.tr("bottombar.scale_label"), ":\xA0"), /*#__PURE__*/React.createElement(InputContainer, null, /*#__PURE__*/React.createElement("span", {
role: "prefix"
}, " 1 : "), /*#__PURE__*/React.createElement("select", {
onChange: function onChange(ev) {
return _this.setState({
currentZoom: parseInt(ev.target.value, 10)
});
},
role: "input",
value: _this.state.currentZoom
}, _this.props.scales.map(function (item, index) {
return /*#__PURE__*/React.createElement("option", {
key: index,
value: index
}, LocaleUtils.toLocaleFixed(item, 0));
}))));
});
_defineProperty(_this, "changeZoom", function (delta) {
_this.setState(function (state) {
return {
currentZoom: Math.max(0, Math.min(state.currentZoom + delta, _this.props.scales.length - 1))
};
});
});
_defineProperty(_this, "queryDatasetConfig", function () {
var obliqueImageryServiceUrl = ConfigUtils.getConfigProp('obliqueImageryServiceUrl');
if (_this.state.selectedDataset && obliqueImageryServiceUrl) {
var reqUrl = obliqueImageryServiceUrl.replace(/\/$/, '') + "/".concat(_this.state.selectedDataset, "/config");
axios.get(reqUrl).then(function (response) {
var datasetConfig = response.data;
var direction = 'n' in datasetConfig.image_centers ? 'n' : Object.keys(datasetConfig.image_centers)[0];
_this.setState({
datasetConfig: datasetConfig,
currentDirection: direction
});
})["catch"](function () {
/* eslint-disable-next-line */
console.warn("Failed to load dataset config");
});
} else {
_this.obliqueImageryLayer.setSource(null);
_this.closestImage = null;
}
});
_defineProperty(_this, "setupLayer", function () {
var datasetConfig = _this.state.datasetConfig;
var projection = new ol.proj.Projection({
code: datasetConfig.crs,
extent: datasetConfig.extent,
units: "m"
});
var targetScale = _this.props.initialScale;
var zoom = _this.props.scales.reduce(function (best, v, i) {
return Math.abs(v - targetScale) < Math.abs(_this.props.scales[best] - targetScale) ? i : best;
}, 0);
_this.map.setView(new ol.View({
projection: projection,
center: ol.extent.getCenter(datasetConfig.extent),
rotation: _this.getRotation() / 180 * Math.PI,
zoom: zoom,
resolutions: MapUtils.getResolutionsForScales(_this.props.scales, datasetConfig.crs),
constrainResolution: true
// showFullExtent: true
}));
_this.setState({
currentZoom: zoom
});
_this.obliqueImageryLayer.setSource(new ol.source.XYZ({
projection: projection,
tileGrid: new ol.tilegrid.TileGrid({
extent: datasetConfig.extent,
resolutions: datasetConfig.resolutions,
tileSize: datasetConfig.tileSize,
origin: datasetConfig.origin
}),
url: datasetConfig.url,
crossOrigin: "anonymous",
tileLoadFunction: function tileLoadFunction(tile, src) {
var _this$closestImage;
if (((_this$closestImage = _this.closestImage) !== null && _this$closestImage !== void 0 ? _this$closestImage : null) !== null) {
src += "?img=" + _this.closestImage;
}
tile.getImage().src = src.replace('{direction}', _this.state.currentDirection);
}
}));
_this.searchClosestImage();
});
_defineProperty(_this, "searchClosestImage", function () {
var _this$state$datasetCo;
var best = null;
var imageCenters = (_this$state$datasetCo = _this.state.datasetConfig) === null || _this$state$datasetCo === void 0 || (_this$state$datasetCo = _this$state$datasetCo.image_centers) === null || _this$state$datasetCo === void 0 ? void 0 : _this$state$datasetCo[_this.state.currentDirection];
if (imageCenters) {
var center = _this.map.getView().getCenter();
var dsqr = function dsqr(p, q) {
return (p[0] - q[0]) * (p[0] - q[0]) + (p[1] - q[1]) * (p[1] - q[1]);
};
best = 0;
var bestDist = dsqr(center, imageCenters[0]);
for (var i = 1; i < imageCenters.length; ++i) {
var dist = dsqr(center, imageCenters[i]);
if (dist < bestDist) {
bestDist = dist;
best = i;
}
}
}
if (best !== _this.closestImage) {
if (_this.obliqueImageryLayer) {
_this.obliqueImageryLayer.getSource().refresh();
}
_this.closestImage = best;
}
});
_defineProperty(_this, "getRotation", function () {
return {
n: 0,
w: 90,
e: -90,
s: 180
}[_this.state.currentDirection];
});
_defineProperty(_this, "sync2DExtent", function () {
if (!_this.state.datasetConfig) {
return;
}
_this.setState(function (state) {
var center = CoordinatesUtils.reproject(_this.props.map.center, _this.props.map.projection, state.datasetConfig.crs);
var resolution = MapUtils.computeForZoom(_this.props.map.resolutions, _this.props.map.zoom);
_this.map.getView().setCenter(center);
_this.map.getView().setResolution(resolution);
return {
currentCenter: center,
currentZoom: _this.map.getView().getZoom()
};
});
});
_defineProperty(_this, "trackFocus", function (ev) {
var _this$map, _this$map$getTargetEl, _mapEl$contains, _mapObliqueEl$contain;
var mapEl = document.getElementById("map");
var mapObliqueEl = (_this$map = _this.map) === null || _this$map === void 0 || (_this$map$getTargetEl = _this$map.getTargetElement) === null || _this$map$getTargetEl === void 0 ? void 0 : _this$map$getTargetEl.call(_this$map);
if (mapEl !== null && mapEl !== void 0 && (_mapEl$contains = mapEl.contains) !== null && _mapEl$contains !== void 0 && _mapEl$contains.call(mapEl, document.activeElement)) {
_this.focusedMap = "map";
} else if (mapObliqueEl !== null && mapObliqueEl !== void 0 && (_mapObliqueEl$contain = mapObliqueEl.contains) !== null && _mapObliqueEl$contain !== void 0 && _mapObliqueEl$contain.call(mapObliqueEl, document.activeElement)) {
_this.focusedMap = "mapOblique";
} else {
_this.focusedMap = null;
}
});
var controls = ol.control.defaults({
zoom: false,
attribution: false,
rotate: false
});
var interactions = ol.interaction.defaults({
onFocusOnly: false
});
_this.map = new ol.Map({
controls: controls,
interactions: interactions
});
_this.map.on('rotateend', _this.searchClosestImage);
_this.map.on('moveend', function () {
_this.searchClosestImage();
_this.setState(function (state) {
var newZoom = _this.map.getView().getZoom();
var newCenter = _this.map.getView().getCenter();
if (newZoom !== state.currentZoom || !isEqual(newCenter, state.currentCenter)) {
return {
currentZoom: newZoom,
currentCenter: newCenter
};
}
return null;
});
});
_this.obliqueImageryLayer = new ol.layer.Tile();
_this.map.addLayer(_this.obliqueImageryLayer);
_this.closestImage = null;
_this.state = ObliqueView.defaultState;
_this.focusedMap = null;
return _this;
}
_inherits(ObliqueView, _React$Component);
return _createClass(ObliqueView, [{
key: "componentDidMount",
value: function componentDidMount() {
window.addEventListener('focus', this.trackFocus, true);
}
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
window.removeEventListener('focus', this.trackFocus);
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps, prevState) {
if (this.props.active && !prevProps.active) {
this.setState({
active: true
});
this.props.setCurrentTask(null);
}
if (this.state.active && this.props.theme && (this.props.theme !== prevProps.theme || !prevState.active)) {
var _datasets$find$name, _datasets$find, _datasets$;
var datasets = this.props.theme.obliqueDatasets || [];
var defaultDataset = (_datasets$find$name = (_datasets$find = datasets.find(function (entry) {
return entry["default"];
})) === null || _datasets$find === void 0 ? void 0 : _datasets$find.name) !== null && _datasets$find$name !== void 0 ? _datasets$find$name : (_datasets$ = datasets[0]) === null || _datasets$ === void 0 ? void 0 : _datasets$.name;
this.setState({
selectedDataset: defaultDataset,
datasetConfig: null,
currentDirection: null
});
}
if (this.state.selectedDataset !== prevState.selectedDataset) {
this.queryDatasetConfig();
}
if (this.state.datasetConfig && this.state.datasetConfig !== prevState.datasetConfig) {
this.setupLayer();
}
if (this.state.datasetConfig && this.state.currentDirection !== prevState.currentDirection) {
var _this$obliqueImageryL, _this$obliqueImageryL2, _this$obliqueImageryL3, _this$map$getView, _this$map$getView$set;
(_this$obliqueImageryL = this.obliqueImageryLayer) === null || _this$obliqueImageryL === void 0 || (_this$obliqueImageryL2 = _this$obliqueImageryL.getSource) === null || _this$obliqueImageryL2 === void 0 || (_this$obliqueImageryL2 = _this$obliqueImageryL2.call(_this$obliqueImageryL)) === null || _this$obliqueImageryL2 === void 0 || (_this$obliqueImageryL3 = _this$obliqueImageryL2.refresh) === null || _this$obliqueImageryL3 === void 0 || _this$obliqueImageryL3.call(_this$obliqueImageryL2);
(_this$map$getView = this.map.getView()) === null || _this$map$getView === void 0 || (_this$map$getView$set = _this$map$getView.setRotation) === null || _this$map$getView$set === void 0 || _this$map$getView$set.call(_this$map$getView, this.getRotation() / 180 * Math.PI);
}
if (this.state.datasetConfig && this.state.currentZoom !== prevState.currentZoom) {
var _this$map$getView2, _this$map$getView2$se;
(_this$map$getView2 = this.map.getView()) === null || _this$map$getView2 === void 0 || (_this$map$getView2$se = _this$map$getView2.setZoom) === null || _this$map$getView2$se === void 0 || _this$map$getView2$se.call(_this$map$getView2, this.state.currentZoom);
}
if (this.state.viewsLocked && this.state.datasetConfig) {
if (this.focusedMap === "map" && this.props.map.bbox !== prevProps.map.bbox) {
this.sync2DExtent();
} else if (this.focusedMap === "mapOblique" && (this.state.currentCenter !== prevState.currentCenter || this.state.currentZoom !== prevState.currentZoom)) {
this.props.zoomToExtent(this.map.getView().calculateExtent(), this.state.datasetConfig.crs);
}
}
}
}, {
key: "render",
value: function render() {
var _this2 = this,
_obliqueConfig$backgr,
_this$state$selectedD;
if (!this.state.active) {
return null;
}
var rot = this.getRotation();
var extraControls = [{
icon: "sync",
callback: this.sync2DExtent,
title: LocaleUtils.tr("common.sync2dview")
}, {
icon: "lock",
callback: function callback() {
return _this2.setState(function (state) {
return {
viewsLocked: !state.viewsLocked
};
});
},
title: LocaleUtils.tr("common.lock2dview"),
active: this.state.viewsLocked
}];
var obliqueConfig = this.props.theme.obliqueDatasets.find(function (entry) {
return entry.name === _this2.state.selectedDataset;
});
var basemap = this.props.themes.backgroundLayers.find(function (entry) {
return entry.name === (obliqueConfig === null || obliqueConfig === void 0 ? void 0 : obliqueConfig.backgroundLayer);
});
return /*#__PURE__*/React.createElement(ResizeableWindow, {
dockable: this.props.geometry.side,
extraControls: extraControls,
icon: "oblique",
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,
splitTopAndBottomBar: true,
title: LocaleUtils.tr("obliqueview.title")
}, /*#__PURE__*/React.createElement("div", {
className: "obliqueview-body"
}, /*#__PURE__*/React.createElement("div", {
className: "obliqueview-map",
ref: function ref(el) {
return _this2.map.setTarget(el);
},
tabIndex: 0
}), !this.state.selectedDataset && /*#__PURE__*/React.createElement("div", {
className: "obliqueview-empty-overlay"
}, LocaleUtils.tr("obliqueview.nodataset")), /*#__PURE__*/React.createElement("div", {
className: "obliqueview-nav-rotate",
style: {
transform: "rotate(".concat(rot, "deg)")
}
}, /*#__PURE__*/React.createElement("span", null), /*#__PURE__*/React.createElement("span", {
className: "obliqueview-nav-dir",
onClick: function onClick() {
return _this2.setState({
currentDirection: "n"
});
},
onKeyDown: MiscUtils.checkKeyActivate,
style: {
transform: "rotate(".concat(-rot, "deg)")
},
tabIndex: 0
}, "N"), /*#__PURE__*/React.createElement("span", null), /*#__PURE__*/React.createElement("span", {
className: "obliqueview-nav-dir",
onClick: function onClick() {
return _this2.setState({
currentDirection: "w"
});
},
onKeyDown: MiscUtils.checkKeyActivate,
style: {
transform: "rotate(".concat(-rot, "deg)")
},
tabIndex: 0
}, "W"), /*#__PURE__*/React.createElement("span", null), /*#__PURE__*/React.createElement("span", {
className: "obliqueview-nav-dir",
onClick: function onClick() {
return _this2.setState({
currentDirection: "e"
});
},
onKeyDown: MiscUtils.checkKeyActivate,
style: {
transform: "rotate(".concat(-rot, "deg)")
},
tabIndex: 0
}, "E"), /*#__PURE__*/React.createElement("span", null), /*#__PURE__*/React.createElement("span", {
className: "obliqueview-nav-dir",
onClick: function onClick() {
return _this2.setState({
currentDirection: "s"
});
},
onKeyDown: MiscUtils.checkKeyActivate,
style: {
transform: "rotate(".concat(-rot, "deg)")
},
tabIndex: 0
}, "S"), /*#__PURE__*/React.createElement("span", null)), /*#__PURE__*/React.createElement("div", {
className: "obliqueview-nav-zoom"
}, /*#__PURE__*/React.createElement(Icon, {
icon: "plus",
onClick: function onClick() {
return _this2.changeZoom(+1);
}
}), /*#__PURE__*/React.createElement(Icon, {
icon: "minus",
onClick: function onClick() {
return _this2.changeZoom(-1);
}
})), basemap && this.state.datasetConfig ? /*#__PURE__*/React.createElement(OlLayer, {
map: this.map,
options: _objectSpread(_objectSpread({}, basemap), {}, {
opacity: (_obliqueConfig$backgr = obliqueConfig.backgroundOpacity) !== null && _obliqueConfig$backgr !== void 0 ? _obliqueConfig$backgr : 127
}),
projection: this.state.datasetConfig.crs,
zIndex: -1
}) : null, /*#__PURE__*/React.createElement("div", {
className: "obliqueview-bottombar"
}, /*#__PURE__*/React.createElement("select", {
onChange: function onChange(ev) {
return _this2.setState({
selectedDataset: ev.target.value
});
},
value: (_this$state$selectedD = this.state.selectedDataset) !== null && _this$state$selectedD !== void 0 ? _this$state$selectedD : ""
}, (this.props.theme.obliqueDatasets || []).map(function (entry) {
var _entry$title;
return /*#__PURE__*/React.createElement("option", {
key: entry.name,
value: entry.name
}, LocaleUtils.trWithFallback(entry.titleMsgId, (_entry$title = entry.title) !== null && _entry$title !== void 0 ? _entry$title : entry.name));
})), /*#__PURE__*/React.createElement("span", {
className: "obliqueview-bottombar-spacer"
}), this.renderScaleChooser(), /*#__PURE__*/React.createElement("span", {
className: "obliqueview-bottombar-spacer"
}), basemap && this.state.datasetConfig ? /*#__PURE__*/React.createElement(OverviewMapButton, {
center: this.state.currentCenter,
coneRotation: this.getRotation() / 180 * Math.PI,
layer: basemap,
projection: this.state.datasetConfig.crs,
resolution: MapUtils.computeForZoom(this.state.datasetConfig.resolutions, this.state.currentZoom) * 0.25
}) : null)));
}
}]);
}(React.Component);
_defineProperty(ObliqueView, "propTypes", {
active: 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 initial map scale. */
initialScale: PropTypes.number,
map: PropTypes.object,
projection: PropTypes.string,
/** A list of allowed map scales, in decreasing order. */
scales: PropTypes.arrayOf(PropTypes.number),
setCurrentTask: PropTypes.func,
theme: PropTypes.object,
themes: PropTypes.object,
zoomToExtent: PropTypes.func
});
_defineProperty(ObliqueView, "defaultProps", {
geometry: {
initialWidth: 480,
initialHeight: 640,
initialX: 0,
initialY: 0,
initiallyDocked: true,
side: 'left'
},
initialScale: 1000,
scales: [20000, 10000, 5000, 2500, 1000, 500, 250]
});
_defineProperty(ObliqueView, "defaultState", {
active: false,
selectedDataset: null,
datasetConfig: null,
currentDirection: null,
currentZoom: 0,
currentCenter: null,
viewsLocked: false
});
export default connect(function (state) {
return {
active: state.task.id === "ObliqueView",
map: state.map,
theme: state.theme.current,
themes: state.theme.themes
};
}, {
setCurrentTask: setCurrentTask,
zoomToExtent: zoomToExtent
})(ObliqueView);