qwc2 2026.3.18 → 2026.3.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +2 -23
  2. package/components/AppMenu.js +29 -30
  3. package/components/AttributeTableWidget.js +24 -4
  4. package/components/IdentifyViewer.js +9 -4
  5. package/components/SearchBox.js +2 -1
  6. package/components/map3d/Map3D.js +31 -2
  7. package/components/map3d/utils/MiscUtils3D.js +11 -11
  8. package/components/style/AppMenu.css +58 -27
  9. package/components/style/AttributeTableWidget.css +5 -5
  10. package/components/widgets/Input.js +2 -1
  11. package/components/widgets/style/ListInput.css +1 -0
  12. package/package.json +5 -2
  13. package/plugins/Bookmark.js +3 -2
  14. package/plugins/Editing.js +4 -0
  15. package/plugins/LayerTree.js +21 -1
  16. package/plugins/MapExport.js +4 -6
  17. package/plugins/Measure.js +4 -2
  18. package/plugins/Redlining.js +1 -1
  19. package/plugins/SensorThingsTool.js +3022 -0
  20. package/plugins/TopBar.js +15 -11
  21. package/plugins/View3D.js +8 -0
  22. package/plugins/map/MeasurementSupport.js +7 -4
  23. package/plugins/map3d/HideObjects3D.js +4 -2
  24. package/plugins/map3d/MeasureObjects3D.js +8 -6
  25. package/plugins/map3d/Settings3D.js +20 -2
  26. package/plugins/map3d/TopBar3D.js +11 -4
  27. package/plugins/style/Portal.css +0 -5
  28. package/plugins/style/SensorThingsTool.css +193 -0
  29. package/reducers/layers.js +17 -0
  30. package/static/translations/bg-BG.json +96 -0
  31. package/static/translations/ca-ES.json +96 -0
  32. package/static/translations/cs-CZ.json +96 -0
  33. package/static/translations/de-CH.json +96 -0
  34. package/static/translations/de-DE.json +96 -0
  35. package/static/translations/en-US.json +96 -0
  36. package/static/translations/es-ES.json +96 -0
  37. package/static/translations/fi-FI.json +96 -0
  38. package/static/translations/fr-FR.json +96 -0
  39. package/static/translations/hu-HU.json +96 -0
  40. package/static/translations/it-IT.json +96 -0
  41. package/static/translations/ja-JP.json +96 -0
  42. package/static/translations/nl-NL.json +96 -0
  43. package/static/translations/no-NO.json +96 -0
  44. package/static/translations/pl-PL.json +96 -0
  45. package/static/translations/pt-BR.json +96 -0
  46. package/static/translations/pt-PT.json +96 -0
  47. package/static/translations/ro-RO.json +96 -0
  48. package/static/translations/ru-RU.json +96 -0
  49. package/static/translations/sv-SE.json +96 -0
  50. package/static/translations/tr-TR.json +96 -0
  51. package/static/translations/tsconfig.json +78 -0
  52. package/static/translations/uk-UA.json +96 -0
  53. package/utils/LayerUtils.js +24 -11
  54. package/utils/QuickHull2D.js +135 -0
  55. package/utils/ServiceLayerUtils.js +4 -1
  56. package/utils/expr_grammar/grammar.js +15 -15
  57. package/utils/expr_grammar/grammar.ne +1 -1
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ![Logo](https://raw.githubusercontent.com/qgis/qwc2/refs/heads/master/static/assets/img/qwc-logo.svg) QGIS Web Client
1
+ ![Logo](https://raw.githubusercontent.com/qgis/qwc2/refs/heads/master/static/assets/img/qwc-logo.svg) QGIS Web Client · [qwc.app](https://qwc.app)
2
2
  =================
3
3
 
4
4
  ## Introduction
@@ -10,28 +10,7 @@ The `qwc2` NPM package can be used as a dependency to build a custom QWC applica
10
10
 
11
11
  ### Main Features
12
12
 
13
- - Modular and easily configurable and extensible
14
- - Responsive, separately configurable for desktop and mobile devices
15
- - Theme switcher
16
- - Search with configurable search providers
17
- - Layer tree
18
- * Toggle layers and groups
19
- * Change layer order and opacity
20
- * Import external WMS/WFS/WMTS/GeoJSON/KML layers
21
- * Compare layers
22
- - Feature info
23
- - Printing using QGIS print layouts
24
- - Share permalinks
25
- - Bookmarks
26
- - Measuring tools
27
- - Height profile
28
- - Redlining
29
- - Editing and attribute table
30
- - Attribute table
31
- - Export map (raster images, DXF)
32
- - Time manager for temporal layers
33
- - Themeable with color schemes
34
- - [Additional plugins!](https://qwc-services.github.io/master/references/qwc2_plugins/)
13
+ See [qwc.app/features](https://qwc.app/features).
35
14
 
36
15
  ## Quick start
37
16
 
@@ -72,8 +72,8 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
72
72
  _this.props.setCurrentTask(null);
73
73
  }
74
74
  _this.props.onMenuToggled(!_this.state.menuVisible);
75
- if (_this.props.menuCompact) {
76
- _this.props.setMenuMargin(!_this.state.menuVisible ? MiscUtils.convertEmToPx(3.75) : 0, 0);
75
+ if (_this.props.menuDisplayMode !== "normal") {
76
+ _this.props.setMenuMargin(!_this.state.menuVisible ? MiscUtils.convertEmToPx(3.5) : 0, 0);
77
77
  }
78
78
  _this.setState(function (state) {
79
79
  return {
@@ -84,7 +84,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
84
84
  });
85
85
  });
86
86
  _defineProperty(_this, "checkCloseMenu", function (ev) {
87
- if (_this.menuEl && !_this.menuEl.contains(ev.target) && !_this.props.keepMenuOpen) {
87
+ if (_this.menuEl && !_this.menuEl.contains(ev.target) && _this.props.menuDisplayMode === "normal") {
88
88
  _this.toggleMenu();
89
89
  MiscUtils.killEvent(ev);
90
90
  }
@@ -98,7 +98,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
98
98
  });
99
99
  });
100
100
  _defineProperty(_this, "onMenuitemClicked", function (item) {
101
- if (!_this.props.keepMenuOpen && _this.state.menuVisible) {
101
+ if (_this.props.menuDisplayMode === "normal" && _this.state.menuVisible) {
102
102
  _this.toggleMenu();
103
103
  }
104
104
  if (item.url) {
@@ -111,6 +111,10 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
111
111
  _defineProperty(_this, "renderMenuItems", function (items, level, filter) {
112
112
  var submenu = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
113
113
  return (items || []).map(function (item) {
114
+ var trargs = item.trargs || [];
115
+ var label = item.title ? LocaleUtils.tr.apply(LocaleUtils, [item.title].concat(_toConsumableArray(trargs))) : LocaleUtils.tr.apply(LocaleUtils, ["appmenu.items." + item.key + (item.mode || "")].concat(_toConsumableArray(trargs)));
116
+ var comment = item.comment ? LocaleUtils.tr.apply(LocaleUtils, ["appmenu.items." + item.key + (item.mode || "") + "_comment"].concat(_toConsumableArray(trargs))) : "";
117
+ var labelclass = _this.props.menuDisplayMode === "icononly" ? "appmenu-menu-item-tooltip" : "appmenu-menu-item-label";
114
118
  if (item.subitems) {
115
119
  var _item$key;
116
120
  var expanded = filter || _this.state.submenusVisible[level] === item.key;
@@ -123,7 +127,6 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
123
127
  "appmenu-submenu": true,
124
128
  "appmenu-submenu-expanded": expanded
125
129
  });
126
- var label = item.title ? LocaleUtils.tr(item.title) : LocaleUtils.tr("appmenu.items." + item.key);
127
130
  return [/*#__PURE__*/React.createElement("div", {
128
131
  className: className,
129
132
  key: (_item$key = item.key) !== null && _item$key !== void 0 ? _item$key : item.title,
@@ -137,14 +140,12 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
137
140
  tabIndex: 0
138
141
  }, /*#__PURE__*/React.createElement(Icon, {
139
142
  icon: item.icon,
140
- size: "xlarge",
141
- title: _this.props.menuIconOnly ? label : null
142
- }), !_this.props.menuIconOnly ? label : null), subitems];
143
+ size: "xlarge"
144
+ }), /*#__PURE__*/React.createElement("span", {
145
+ className: labelclass
146
+ }, label)), subitems];
143
147
  } else {
144
- var trargs = item.trargs || [];
145
- var _label = item.title ? LocaleUtils.tr.apply(LocaleUtils, [item.title].concat(_toConsumableArray(trargs))) : LocaleUtils.tr.apply(LocaleUtils, ["appmenu.items." + item.key + (item.mode || "")].concat(_toConsumableArray(trargs)));
146
- var comment = item.comment ? LocaleUtils.tr.apply(LocaleUtils, ["appmenu.items." + item.key + (item.mode || "") + "_comment"].concat(_toConsumableArray(trargs))) : "";
147
- if (!filter || removeDiacritics(_label.toLowerCase()).match(filter) || comment && removeDiacritics(comment.toLowerCase()).match(filter)) {
148
+ if (!filter || removeDiacritics(label.toLowerCase()).match(filter) || comment && removeDiacritics(comment.toLowerCase()).match(filter)) {
148
149
  var _className = classnames({
149
150
  "appmenu-menu-item": true,
150
151
  "appmenu-menu-item-nested": submenu
@@ -162,13 +163,10 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
162
163
  tabIndex: 0
163
164
  }, /*#__PURE__*/React.createElement(Icon, {
164
165
  icon: item.icon,
165
- size: "xlarge",
166
- title: _this.props.menuIconOnly ? _label : null
167
- }), !_this.props.menuIconOnly ? /*#__PURE__*/React.createElement("span", {
168
- className: "appmenu-menu-item-label"
169
- }, _label, comment ? /*#__PURE__*/React.createElement("div", {
170
- className: "appmenu-menu-item-comment"
171
- }, comment) : null) : null);
166
+ size: "xlarge"
167
+ }), /*#__PURE__*/React.createElement("span", {
168
+ className: labelclass
169
+ }, label));
172
170
  }
173
171
  return null;
174
172
  }
@@ -215,7 +213,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
215
213
  MiscUtils.killEvent(ev);
216
214
  } else if (ev.key === 'Escape') {
217
215
  var _this$menuBtn, _this$menuBtn$focus;
218
- if (!_this.props.menuCompact) {
216
+ if (_this.props.menuDisplayMode === "normal") {
219
217
  _this.toggleMenu();
220
218
  }
221
219
  (_this$menuBtn = _this.menuBtn) === null || _this$menuBtn === void 0 || (_this$menuBtn$focus = _this$menuBtn.focus) === null || _this$menuBtn$focus === void 0 || _this$menuBtn$focus.call(_this$menuBtn);
@@ -237,7 +235,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
237
235
  return _createClass(AppMenu, [{
238
236
  key: "componentDidMount",
239
237
  value: function componentDidMount() {
240
- if (this.props.showOnStartup) {
238
+ if (this.props.showOnStartup || this.props.menuDisplayMode !== "normal") {
241
239
  this.toggleMenu();
242
240
  }
243
241
  this.addKeyBindings(this.props.menuItems);
@@ -249,7 +247,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
249
247
  key: "componentDidUpdate",
250
248
  value: function componentDidUpdate(prevProps, prevState) {
251
249
  var _this2 = this;
252
- if (this.state.menuVisible && !prevState.menuVisible && !this.props.menuCompact) {
250
+ if (this.state.menuVisible && !prevState.menuVisible) {
253
251
  // Need to wait until slide in transition is over
254
252
  setTimeout(function () {
255
253
  var _this2$filterfield, _this2$filterfield$fo;
@@ -259,7 +257,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
259
257
  setTimeout(function () {
260
258
  return document.addEventListener('click', _this2.checkCloseMenu);
261
259
  }, 0);
262
- } else if (prevState.menuVisible && !this.state.menuVisible && !this.props.menuCompact) {
260
+ } else if (prevState.menuVisible && !this.state.menuVisible) {
263
261
  document.removeEventListener('click', this.checkCloseMenu);
264
262
  }
265
263
  }
@@ -281,13 +279,13 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
281
279
  _this$props$buttonCon;
282
280
  var isMobile = ConfigUtils.isMobile();
283
281
  var visible = !this.props.currentTaskBlocked && this.state.menuVisible;
284
- var showLabel = !this.props.menuCompact && !isMobile;
282
+ var showLabel = this.props.menuDisplayMode === "normal" && !isMobile;
285
283
  var className = classnames({
286
284
  "AppMenu": true,
287
285
  "appmenu-blocked": this.props.currentTaskBlocked,
288
286
  "appmenu-visible": visible,
289
- "appmenu-compact": this.props.menuCompact,
290
- "appmenu-icononly": this.props.menuIconOnly,
287
+ "appmenu-compact": this.props.menuDisplayMode === "compact",
288
+ "appmenu-icononly": this.props.menuDisplayMode === "icononly",
291
289
  "appmenu-nolabel": !showLabel
292
290
  });
293
291
  var filter = this.state.filter ? new RegExp(removeDiacritics(this.state.filter).replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"), "i") : null;
@@ -313,6 +311,8 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
313
311
  }))]), /*#__PURE__*/React.createElement("div", {
314
312
  className: "appmenu-menu-container",
315
313
  tabIndex: -1
314
+ }, /*#__PURE__*/React.createElement("div", {
315
+ className: "appmenu-menu-aligner"
316
316
  }, /*#__PURE__*/React.createElement("div", {
317
317
  className: "appmenu-menu",
318
318
  inert: !visible,
@@ -321,7 +321,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
321
321
  _this3.menuEl = el;
322
322
  MiscUtils.setupKillTouchEvents(el);
323
323
  }
324
- }, this.props.showFilterField ? /*#__PURE__*/React.createElement("div", {
324
+ }, this.props.showFilterField && this.props.menuDisplayMode !== "icononly" ? /*#__PURE__*/React.createElement("div", {
325
325
  className: "appmenu-menu-item appmenu-menu-item-filter",
326
326
  onFocus: this.focusFilterField,
327
327
  onKeyDown: this.keyNav,
@@ -352,7 +352,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
352
352
  });
353
353
  },
354
354
  role: "suffix"
355
- }))) : null, this.renderMenuItems(this.props.menuItems, 0, filter))));
355
+ }))) : null, this.renderMenuItems(this.props.menuItems, 0, filter)))));
356
356
  }
357
357
  }]);
358
358
  }(React.Component);
@@ -362,8 +362,7 @@ _defineProperty(AppMenu, "propTypes", {
362
362
  buttonContents: PropTypes.object,
363
363
  buttonLabel: PropTypes.string,
364
364
  currentTaskBlocked: PropTypes.bool,
365
- keepMenuOpen: PropTypes.bool,
366
- menuCompact: PropTypes.bool,
365
+ menuDisplayMode: PropTypes.string,
367
366
  menuIconOnly: PropTypes.bool,
368
367
  menuItems: PropTypes.array,
369
368
  onMenuToggled: PropTypes.func,
@@ -38,7 +38,7 @@ import { connect } from 'react-redux';
38
38
  import FileSaver from 'file-saver';
39
39
  import isEmpty from 'lodash.isempty';
40
40
  import PropTypes from 'prop-types';
41
- import { LayerRole, addLayerFeatures, removeLayer } from '../actions/layers';
41
+ import { LayerRole, addLayerFeatures, removeLayer, refreshLayer } from '../actions/layers';
42
42
  import { zoomToExtent, zoomToPoint } from '../actions/map';
43
43
  import { setCurrentTask, setCurrentTaskBlocked } from '../actions/task';
44
44
  import EditComboField from '../components/EditComboField';
@@ -388,7 +388,7 @@ var AttributeTableWidget = /*#__PURE__*/function (_React$Component) {
388
388
  };
389
389
  newState[stateField] = val;
390
390
  // Reset page if a reload is triggered (either filter changed with a set filter value, or filter value cleared)
391
- if (newState.filterVal || _this.state.filterVal && !newState.filterVal) {
391
+ if (newState.filterField && (newState.filterVal || _this.state.filterVal && !newState.filterVal)) {
392
392
  newState.currentPage = 0;
393
393
  _this.reload(_this.state.selectedLayer, false, newState);
394
394
  } else {
@@ -485,6 +485,10 @@ var AttributeTableWidget = /*#__PURE__*/function (_React$Component) {
485
485
  return newState;
486
486
  }, function () {
487
487
  if (reload) {
488
+ var mapPrefix = _this.state.curEditConfig.editDataset.split(".")[0];
489
+ _this.props.refreshLayer(function (layer) {
490
+ return layer.wms_name === mapPrefix;
491
+ });
488
492
  _this.reload(_this.state.loadedLayer, true);
489
493
  }
490
494
  });
@@ -566,6 +570,10 @@ var AttributeTableWidget = /*#__PURE__*/function (_React$Component) {
566
570
  alert(result);
567
571
  } else {
568
572
  _this.changedFiles = {};
573
+ var mapPrefix = _this.state.curEditConfig.editDataset.split(".")[0];
574
+ _this.props.refreshLayer(function (layer) {
575
+ return layer.wms_name === mapPrefix;
576
+ });
569
577
  _this.reload(_this.state.loadedLayer, true, {
570
578
  changedFeatureIdx: null,
571
579
  originalFeatureProps: null,
@@ -806,6 +814,13 @@ var AttributeTableWidget = /*#__PURE__*/function (_React$Component) {
806
814
  if (this.state.highlightedFeature !== prevState.highlightedFeature || this.state.features !== prevState.features || this.state.selectedFeatures !== prevState.selectedFeatures) {
807
815
  this.highlightFeatures();
808
816
  }
817
+ if (this.state.loadedLayer !== prevState.loadedLayer && this.props.showDisplayFieldOnly) {
818
+ this.setState(function (state) {
819
+ return {
820
+ filterField: state.curEditConfig.displayField
821
+ };
822
+ });
823
+ }
809
824
  }
810
825
  }, {
811
826
  key: "componentWillUnmount",
@@ -1029,7 +1044,10 @@ var AttributeTableWidget = /*#__PURE__*/function (_React$Component) {
1029
1044
  return _this2.updateFilter("filterField", ev.target.value);
1030
1045
  },
1031
1046
  value: this.state.filterField
1032
- }, showIdColumn ? /*#__PURE__*/React.createElement("option", {
1047
+ }, /*#__PURE__*/React.createElement("option", {
1048
+ disabled: true,
1049
+ value: ""
1050
+ }, LocaleUtils.tr("common.select")), showIdColumn ? /*#__PURE__*/React.createElement("option", {
1033
1051
  value: "<id>"
1034
1052
  }, this.translateFieldName(primaryKey)) : null, fields.map(function (field) {
1035
1053
  return /*#__PURE__*/React.createElement("option", {
@@ -1239,6 +1257,7 @@ _defineProperty(AttributeTableWidget, "propTypes", {
1239
1257
  mapCrs: PropTypes.string,
1240
1258
  mapScales: PropTypes.array,
1241
1259
  readOnly: PropTypes.bool,
1260
+ refreshLayer: PropTypes.func,
1242
1261
  removeLayer: PropTypes.func,
1243
1262
  setCurrentTask: PropTypes.func,
1244
1263
  setCurrentTaskBlocked: PropTypes.func,
@@ -1279,7 +1298,7 @@ _defineProperty(AttributeTableWidget, "defaultState", {
1279
1298
  originalFeatureProps: null,
1280
1299
  pageSize: 50,
1281
1300
  currentPage: 0,
1282
- filterField: "id",
1301
+ filterField: "",
1283
1302
  filterOp: "~",
1284
1303
  filterVal: "",
1285
1304
  sortField: null,
@@ -1302,6 +1321,7 @@ export default connect(function (state) {
1302
1321
  };
1303
1322
  }, {
1304
1323
  addLayerFeatures: addLayerFeatures,
1324
+ refreshLayer: refreshLayer,
1305
1325
  removeLayer: removeLayer,
1306
1326
  setCurrentTask: setCurrentTask,
1307
1327
  setCurrentTaskBlocked: setCurrentTaskBlocked,
@@ -620,9 +620,11 @@ var IdentifyViewer = /*#__PURE__*/function (_React$Component) {
620
620
  var inlineExtaAttribs = false;
621
621
  var featureReports = _this.state.reports[layerid] || [];
622
622
  if (feature.featurereport) {
623
+ var parts = feature.featurereport.split(".");
623
624
  featureReports.push({
624
625
  title: null,
625
- template: feature.featurereport
626
+ template: parts.shift(),
627
+ format: parts.join(".")
626
628
  });
627
629
  }
628
630
  if (feature.type === "text") {
@@ -768,7 +770,7 @@ var IdentifyViewer = /*#__PURE__*/function (_React$Component) {
768
770
  }) : null, /*#__PURE__*/React.createElement("span", null, [_this.props.showLayerTitles ? feature.layertitle : "", feature.displayname].filter(Boolean).join(": ")), zoomToFeatureButton, /*#__PURE__*/React.createElement(Icon, {
769
771
  icon: "info-sign",
770
772
  onClick: function onClick() {
771
- return _this.showLayerInfo(layerid);
773
+ return _this.showLayerInfo(layerid, feature.layerinfo);
772
774
  }
773
775
  }), /*#__PURE__*/React.createElement(Icon, {
774
776
  icon: "trash",
@@ -1122,12 +1124,15 @@ var IdentifyViewer = /*#__PURE__*/function (_React$Component) {
1122
1124
  });
1123
1125
  });
1124
1126
  });
1125
- _defineProperty(_this, "showLayerInfo", function (layer) {
1127
+ _defineProperty(_this, "showLayerInfo", function (layer, infolayer) {
1126
1128
  var _layer$split = layer.split('#'),
1127
1129
  _layer$split2 = _slicedToArray(_layer$split, 2),
1128
1130
  layerUrl = _layer$split2[0],
1129
1131
  layerName = _layer$split2[1];
1130
- var match = LayerUtils.searchLayer(_this.props.layers, 'url', layerUrl, 'name', layerName);
1132
+ if (!infolayer) {
1133
+ infolayer = layerName;
1134
+ }
1135
+ var match = LayerUtils.searchLayer(_this.props.layers, 'url', layerUrl, 'name', infolayer);
1131
1136
  if (match) {
1132
1137
  _this.props.setActiveLayerInfo(match.layer, match.sublayer);
1133
1138
  }
@@ -1025,12 +1025,13 @@ var SearchBox = /*#__PURE__*/function (_React$Component) {
1025
1025
  var text = LocaleUtils.tr("search.existinglayer") + ": " + existingLayerName;
1026
1026
  _this.props.showNotification("existinglayer", text);
1027
1027
  } else {
1028
+ var _existingLayer$role;
1028
1029
  var existingLayer = _this.props.layers.find(function (l) {
1029
1030
  return l.type === layer.type && l.url === layer.url;
1030
1031
  });
1031
1032
  _this.props.addLayer(_objectSpread(_objectSpread({}, layer), {}, {
1032
1033
  srcid: existingLayer === null || existingLayer === void 0 ? void 0 : existingLayer.srcid,
1033
- role: LayerRole.USERLAYER
1034
+ role: (_existingLayer$role = existingLayer === null || existingLayer === void 0 ? void 0 : existingLayer.role) !== null && _existingLayer$role !== void 0 ? _existingLayer$role : LayerRole.USERLAYER
1034
1035
  }));
1035
1036
  }
1036
1037
  if (_this.props.searchOptions.zoomToLayers && layer.bbox) {
@@ -52,7 +52,7 @@ import { fromUrl } from "geotiff";
52
52
  import isEmpty from 'lodash.isempty';
53
53
  import PropTypes from 'prop-types';
54
54
  import * as THREE from 'three';
55
- import { Vector2, CubeTextureLoader, Group, Raycaster, Mesh, Box3, Vector3, Matrix4, EventDispatcher } from 'three';
55
+ import { Vector2, CubeTextureLoader, Group, Raycaster, Mesh, Box3, Vector3, Matrix4, EventDispatcher, Points } from 'three';
56
56
  import { computeBoundsTree, disposeBoundsTree, computeBatchedBoundsTree, disposeBatchedBoundsTree, acceleratedRaycast } from 'three-mesh-bvh';
57
57
  import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
58
58
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader';
@@ -147,6 +147,7 @@ var Map3D = /*#__PURE__*/function (_React$Component2) {
147
147
  updateSceneObject: function updateSceneObject(objectId, options, flags) {},
148
148
  zoomToObject: function zoomToObject(objectId) {},
149
149
  objectIsVisible: function objectIsVisible(objectId) {},
150
+ objectContainsPoints: function objectContainsPoints(object) {},
150
151
  getMap: function getMap() {},
151
152
  computeBoundsTree: function computeBoundsTree(object) {},
152
153
  setViewToExtent: function setViewToExtent(bounds, angle) {},
@@ -489,6 +490,9 @@ var Map3D = /*#__PURE__*/function (_React$Component2) {
489
490
  scene.userData.tilesetName = objectId;
490
491
  scene.userData.featureIdAttr = "id";
491
492
  Tiles3DStyle.applyTileStyle(scene, tiles.userData, _this2.state.sceneContext);
493
+ if (_this2.objectContainsPoints(tiles)) {
494
+ tiles.pointSize = _this2.state.sceneContext.settings.pointSize;
495
+ }
492
496
  _this2.computeBoundsTree(scene);
493
497
  _this2.instance.notifyChange(tiles);
494
498
  });
@@ -782,6 +786,17 @@ var Map3D = /*#__PURE__*/function (_React$Component2) {
782
786
  }
783
787
  return isVisible;
784
788
  });
789
+ _defineProperty(_this2, "objectContainsPoints", function (object) {
790
+ var containsPoints = false;
791
+ if (object !== null && object !== void 0 && object.tiles) {
792
+ object.traverse(function (child) {
793
+ if (child instanceof Points) {
794
+ containsPoints = true;
795
+ }
796
+ });
797
+ }
798
+ return containsPoints;
799
+ });
785
800
  _defineProperty(_this2, "getMap", function () {
786
801
  return _this2.map;
787
802
  });
@@ -999,7 +1014,7 @@ var Map3D = /*#__PURE__*/function (_React$Component2) {
999
1014
  });
1000
1015
 
1001
1016
  // Inspector
1002
- if (["1", "true"].includes((UrlParams.getParam("inspector") || "").toLowerCase())) {
1017
+ if ((process.env.NODE_ENV !== "production" || _this2.props.forceAllowInspector) && ["1", "true"].includes((UrlParams.getParam("inspector") || "").toLowerCase())) {
1003
1018
  var inspectorContainer = document.createElement("div");
1004
1019
  inspectorContainer.className = 'map3d-inspector';
1005
1020
  _this2.container.appendChild(inspectorContainer);
@@ -1368,6 +1383,7 @@ var Map3D = /*#__PURE__*/function (_React$Component2) {
1368
1383
  _this2.state.sceneContext.updateSceneObject = _this2.updateSceneObject;
1369
1384
  _this2.state.sceneContext.zoomToObject = _this2.zoomToObject;
1370
1385
  _this2.state.sceneContext.objectIsVisible = _this2.objectIsVisible;
1386
+ _this2.state.sceneContext.objectContainsPoints = _this2.objectContainsPoints;
1371
1387
  _this2.state.sceneContext.getMap = _this2.getMap;
1372
1388
  _this2.state.computeBoundsTree = _this2.computeBoundsTree;
1373
1389
  _this2.state.sceneContext.getTerrainHeightFromDTM = _this2.getTerrainHeightFromDTM;
@@ -1376,6 +1392,7 @@ var Map3D = /*#__PURE__*/function (_React$Component2) {
1376
1392
  _this2.state.sceneContext.getSetting = _this2.getSetting;
1377
1393
  _this2.state.sceneContext.setSetting = _this2.setSetting;
1378
1394
  _this2.state.sceneContext.settings.fov = props.defaultFov;
1395
+ _this2.state.sceneContext.settings.pointSize = props.defaultPointSize;
1379
1396
  _this2.state.sceneContext.settings.sceneQuality = props.defaultSceneQuality;
1380
1397
  registerPermalinkDataStoreHook("map3d", _this2.store3dState);
1381
1398
  return _this2;
@@ -1454,6 +1471,15 @@ var Map3D = /*#__PURE__*/function (_React$Component2) {
1454
1471
  this.instance.view.camera.fov = this.state.sceneContext.settings.fov;
1455
1472
  this.instance.notifyChange(this.instance.view.camera);
1456
1473
  }
1474
+ if (this.state.sceneContext.settings.pointSize !== prevState.sceneContext.settings.pointSize) {
1475
+ Object.values(this.state.sceneContext.objectTree).map(function (entry) {
1476
+ var obj = _this3.state.sceneContext.getSceneObject(entry.objectId);
1477
+ if (_this3.objectContainsPoints(obj)) {
1478
+ obj.pointSize = _this3.state.sceneContext.settings.pointSize;
1479
+ _this3.instance.notifyChange(obj);
1480
+ }
1481
+ });
1482
+ }
1457
1483
  if (this.state.sceneContext.settings.sceneQuality !== prevState.sceneContext.settings.sceneQuality) {
1458
1484
  var quality = Math.max(20, this.state.sceneContext.settings.sceneQuality);
1459
1485
  this.map.segments = Math.pow(2, Math.floor(quality / 20));
@@ -1500,7 +1526,9 @@ _defineProperty(Map3D, "contextType", MapContainerPortalContext);
1500
1526
  _defineProperty(Map3D, "propTypes", {
1501
1527
  controlsPosition: PropTypes.string,
1502
1528
  defaultFov: PropTypes.number,
1529
+ defaultPointSize: PropTypes.number,
1503
1530
  defaultSceneQuality: PropTypes.number,
1531
+ forceAllowInspector: PropTypes.bool,
1504
1532
  innerRef: PropTypes.func,
1505
1533
  layers: PropTypes.array,
1506
1534
  mouseButtons: PropTypes.object,
@@ -1534,6 +1562,7 @@ _defineProperty(Map3D, "defaultSceneState", {
1534
1562
  snapObjects: [],
1535
1563
  settings: {
1536
1564
  fov: 30,
1565
+ pointSize: 0,
1537
1566
  sceneQuality: 100
1538
1567
  },
1539
1568
  sceneId: null,
@@ -21,10 +21,10 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
21
21
  * LICENSE file in the root directory of this source tree.
22
22
  */
23
23
 
24
- import convexHull from 'quick-hull-2d';
25
24
  import { Box3, BufferGeometry, Matrix3, Matrix4, Mesh, Vector2, Vector3 } from 'three';
26
25
  import { MeshLine, MeshLineMaterial } from 'three.meshline';
27
26
  import { CSS2DObject } from "three/addons/renderers/CSS2DRenderer";
27
+ import convexHull from '../../../utils/QuickHull2D';
28
28
  export function createLabelObject(text, pos, sceneContext, zoffset) {
29
29
  var yoffset = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0;
30
30
  var labelEl = document.createElement("span");
@@ -101,9 +101,10 @@ export function computeOBBXY(mesh) {
101
101
  var zmin = Infinity;
102
102
  var zmax = -Infinity;
103
103
  for (var i = 0; i < n; i++) {
104
- pointsxy[i] = [pos.getX(i), pos.getY(i)];
105
- zmin = Math.min(zmin, pos.getZ(i));
106
- zmax = Math.max(zmax, pos.getZ(i));
104
+ var p = new Vector3(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(mesh.matrixWorld);
105
+ pointsxy[i] = [p.x, p.y];
106
+ zmin = Math.min(zmin, p.z);
107
+ zmax = Math.max(zmax, p.z);
107
108
  }
108
109
 
109
110
  // Compute convex hull
@@ -125,9 +126,9 @@ export function computeOBBXY(mesh) {
125
126
  var _vmin = Infinity;
126
127
  var _vmax = -Infinity;
127
128
  for (var j = 0; j < hull.length; j++) {
128
- var p = _construct(Vector2, _toConsumableArray(hull[j]));
129
- var pu = p.dot(_u);
130
- var pv = p.dot(_v);
129
+ var _p = _construct(Vector2, _toConsumableArray(hull[j]));
130
+ var pu = _p.dot(_u);
131
+ var pv = _p.dot(_v);
131
132
  if (pu < _umin) _umin = pu;
132
133
  if (pu > _umax) _umax = pu;
133
134
  if (pv < _vmin) _vmin = pv;
@@ -153,12 +154,11 @@ export function computeOBBXY(mesh) {
153
154
  umax = _best.umax,
154
155
  vmin = _best.vmin,
155
156
  vmax = _best.vmax;
156
- var center = new Vector3(u.x * (umin + umax) / 2 + v.x * (vmin + vmax) / 2, u.y * (umin + umax) / 2 + v.y * (vmin + vmax) / 2, (zmin + zmax) / 2).applyMatrix4(mesh.matrixWorld);
157
- var normalMatrix = new Matrix3().getNormalMatrix(mesh.matrixWorld);
157
+ var center = new Vector3(u.x * (umin + umax) / 2 + v.x * (vmin + vmax) / 2, u.y * (umin + umax) / 2 + v.y * (vmin + vmax) / 2, (zmin + zmax) / 2);
158
158
  return {
159
159
  center: center,
160
- axes: [new Vector3(u.x, u.y, 0).applyMatrix3(normalMatrix).normalize(), new Vector3(v.x, v.y, 0).applyMatrix3(normalMatrix).normalize(), new Vector3(0, 0, 1)],
161
- halfSizes: new Vector3((umax - umin) / 2 * Math.hypot(u.x * mesh.scale.x, u.y * mesh.scale.y), (vmax - vmin) / 2 * Math.hypot(v.x * mesh.scale.x, v.y * mesh.scale.y), (zmax - zmin) / 2 * mesh.scale.z)
160
+ axes: [new Vector3(u.x, u.y, 0), new Vector3(v.x, v.y, 0), new Vector3(0, 0, 1)],
161
+ halfSizes: new Vector3((umax - umin) / 2, (vmax - vmin) / 2, (zmax - zmin) / 2)
162
162
  };
163
163
  }
164
164
  export var TileMeshHelper = /*#__PURE__*/function () {
@@ -36,10 +36,16 @@ div.AppMenu.appmenu-visible .appmenu-label {
36
36
  color: var(--app-submenu-text-color-hover);
37
37
  }
38
38
 
39
+ div.AppMenu .appmenu-icon {
40
+ width: 3.5em;
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ }
45
+
39
46
  div.AppMenu .appmenu-icon > span.icon {
40
47
  color: var(--app-menu-text-color);
41
48
  padding: 0.25em;
42
- margin: 0 1em;
43
49
  border: 2px solid var(--app-menu-text-color);
44
50
  transition: color 0.25s, border-color 0.25s, background-color 0.25s;
45
51
  }
@@ -58,47 +64,52 @@ div.AppMenu div.appmenu-menu-container {
58
64
  position: absolute;
59
65
  top: 100%;
60
66
  right: 0;
61
- box-shadow: 0px 2px 4px rgba(136, 136, 136, 0.5);
62
- width: 20em;
63
- max-width: 100vw;
67
+ width: 100%;
68
+ overflow-y: auto;
69
+ overflow-x: hidden;
70
+ height: calc(var(--plugins-container-height) - var(--topbar-height) - var(--bottombar-height));
71
+ pointer-events: none;
64
72
  opacity: 0;
65
73
  transform-origin: top;
66
74
  transform: scaleY(0);
67
75
  transition: transform 0.25s, opacity 0.25s;
68
- overflow-y: auto;
69
- max-height: calc(var(--plugins-container-height) - var(--topbar-height) - var(--bottombar-height));
70
- border-radius: 0px 0px 0px var(--border-radius);
71
76
  }
72
77
 
73
- div.AppMenu.appmenu-compact div.appmenu-menu-container {
74
- right: -11.25em;
75
- width: 15em;
76
- height: calc(var(--plugins-container-height) - var(--topbar-height) - var(--bottombar-height));
77
- transition: transform 0.25s, opacity 0.25s, right 0.5s;
78
- background: var(--app-menu-bg-color);
79
- box-shadow: 0px 0px 4px rgba(136, 136, 136, 0.5);
80
- top: 3.5em;
81
- }
82
-
83
- div.AppMenu.appmenu-icononly div.appmenu-menu-container {
84
- right: 0;
85
- width: auto;
78
+ div.AppMenu.appmenu-visible div.appmenu-menu-container {
79
+ transform: scaleY(1);
80
+ opacity: 1;
86
81
  }
87
82
 
88
- div.AppMenu.appmenu-compact div.appmenu-menu-container:hover,
89
- div.AppMenu.appmenu-compact div.appmenu-menu-container:focus-within {
90
- right: 0;
83
+ div.AppMenu div.appmenu-menu-aligner {
84
+ display: flex;
85
+ justify-content: end;
91
86
  }
92
87
 
93
88
  div.AppMenu div.appmenu-menu {
89
+ position: relative;
90
+ pointer-events: initial;
91
+ text-align: left;
94
92
  background-color: var(--app-menu-bg-color);
93
+ box-shadow: 0px 2px 4px rgba(136, 136, 136, 0.5);
95
94
  text-align: left;
96
- font-size: small;
95
+ width: 22.22em;
96
+ font-size: 90%;
97
+ border-radius: 0px 0px 0px var(--border-radius);
97
98
  }
98
99
 
99
- div.AppMenu.appmenu-visible div.appmenu-menu-container {
100
- transform: scaleY(1);
101
- opacity: 1;
100
+ div.AppMenu.appmenu-compact div.appmenu-menu {
101
+ right: -18.33em;
102
+ transition: right 0.5s;
103
+ }
104
+
105
+ div.AppMenu.appmenu-compact div.appmenu-menu:hover,
106
+ div.AppMenu.appmenu-compact div.appmenu-menu:focus-within {
107
+ right: 0;
108
+ }
109
+
110
+ div.AppMenu.appmenu-icononly div.appmenu-menu {
111
+ width: 3.89em;
112
+ right: 0;
102
113
  }
103
114
 
104
115
  div.appmenu-menu-item {
@@ -106,6 +117,7 @@ div.appmenu-menu-item {
106
117
  align-items: center;
107
118
  color: var(--app-menu-text-color);
108
119
  font-weight: bold;
120
+ position: relative;
109
121
  }
110
122
 
111
123
  div.appmenu-menu-item:not(:last-child) {
@@ -126,6 +138,25 @@ div.appmenu-menu-item-comment {
126
138
  font-size: 90%;
127
139
  }
128
140
 
141
+ span.appmenu-menu-item-tooltip {
142
+ position: absolute;
143
+ display: none;
144
+ background-color: var(--tooltip-bg-color);
145
+ border: 1px solid var(--tooltip-border-color);
146
+ color: var(--tooltip-text-color);
147
+ font-size: 75%;
148
+ font-weight: normal;
149
+ padding: 0.5em;
150
+ border-radius: 0.5em;
151
+ white-space: nowrap;
152
+ right: calc(100% + 0.25em);
153
+ z-index: 2;
154
+ }
155
+
156
+ div.appmenu-menu-item:hover span.appmenu-menu-item-tooltip {
157
+ display: inline;
158
+ }
159
+
129
160
  div.appmenu-submenu-active,
130
161
  div.appmenu-submenu-expanded {
131
162
  background-color: var(--app-menu-bg-color-hover);