qwc2 2026.4.29 → 2026.5.6

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.
package/actions/theme.js CHANGED
@@ -53,7 +53,7 @@ export function setThemeLayersList(theme) {
53
53
  };
54
54
  }
55
55
  export function finishThemeSetup(dispatch, theme, themes, layerConfigs, insertPos, permalinkLayers, externalLayerRestorer, visibleBgLayer, initialTheme, initialTask) {
56
- var _theme$config;
56
+ var _theme$config$section, _theme$config, _theme$config2;
57
57
  // Create layer
58
58
  var themeLayer = ThemeUtils.createThemeLayer(theme, themes);
59
59
  var layers = [themeLayer];
@@ -148,7 +148,8 @@ export function finishThemeSetup(dispatch, theme, themes, layerConfigs, insertPo
148
148
  type: SWITCHING_THEME,
149
149
  switching: false
150
150
  });
151
- var task = initialTask || (theme === null || theme === void 0 || (_theme$config = theme.config) === null || _theme$config === void 0 ? void 0 : _theme$config.startupTask) || (initialTheme ? ConfigUtils.getConfigProp("startupTask") : null);
151
+ var section = ConfigUtils.isMobile() ? "mobile" : "desktop";
152
+ var task = initialTask || ((_theme$config$section = theme === null || theme === void 0 || (_theme$config = theme.config) === null || _theme$config === void 0 || (_theme$config = _theme$config[section]) === null || _theme$config === void 0 ? void 0 : _theme$config.startupTask) !== null && _theme$config$section !== void 0 ? _theme$config$section : theme === null || theme === void 0 || (_theme$config2 = theme.config) === null || _theme$config2 === void 0 ? void 0 : _theme$config2.startupTask) || (initialTheme ? ConfigUtils.getConfigProp("startupTask") : null);
152
153
  if (task) {
153
154
  var mapClickAction = ConfigUtils.getPluginConfig(task.key).mapClickAction;
154
155
  dispatch(setCurrentTask(task.key, task.mode, mapClickAction, task.data));
@@ -50,7 +50,9 @@ var AutoEditForm = /*#__PURE__*/function (_React$Component) {
50
50
  }
51
51
  var input = null;
52
52
  var title = field.name + ":";
53
- if (field.type === "boolean" || field.type === "bool") {
53
+ if (constraints.hidden) {
54
+ return null;
55
+ } else if (field.type === "boolean" || field.type === "bool") {
54
56
  if (_this.props.touchFriendly) {
55
57
  var boolvalue = value === "1" || value === "on" || value === "true" || value === true;
56
58
  input = /*#__PURE__*/React.createElement(ToggleSwitch, _extends({
@@ -87,12 +89,11 @@ var AutoEditForm = /*#__PURE__*/function (_React$Component) {
87
89
  values: constraints.values
88
90
  }));
89
91
  } else if (field.type === "number") {
90
- var precision = constraints.step > 0 ? Math.ceil(-Math.log10(constraints.step)) : 6;
92
+ var precision = constraints.step > 0 ? Math.ceil(-Math.log10(constraints.step)) : 0;
91
93
  input = /*#__PURE__*/React.createElement(NumberInput, {
92
94
  decimals: precision,
93
95
  max: constraints.max,
94
96
  min: constraints.min,
95
- mobile: true,
96
97
  name: field.id,
97
98
  onChange: function onChange(nr) {
98
99
  return _this.props.updateField(field.id, nr);
@@ -1155,6 +1155,9 @@ var IdentifyViewer = /*#__PURE__*/function (_React$Component) {
1155
1155
  var _window$qwc3;
1156
1156
  var formatter = _Object$values[_i];
1157
1157
  text = formatter(attrName, text, layer, result);
1158
+ if (/*#__PURE__*/React.isValidElement(text)) {
1159
+ return text;
1160
+ }
1158
1161
  }
1159
1162
  text = _this.props.attributeTransform(attrName, text, layer, result);
1160
1163
  text = MiscUtils.addLinkAnchors(text);
@@ -271,7 +271,8 @@ var PickFeature = /*#__PURE__*/function (_React$Component) {
271
271
  }
272
272
  }, entry.layer + ": " + ((_entry$feature$displa = entry.feature.displayname) !== null && _entry$feature$displa !== void 0 ? _entry$feature$displa : entry.feature.id));
273
273
  }) : /*#__PURE__*/React.createElement("div", {
274
- className: "pick-feature-menu-querying"
274
+ className: "pick-feature-menu-querying",
275
+ key: "spinner"
275
276
  }, /*#__PURE__*/React.createElement(Spinner, null), LocaleUtils.tr("pickfeature.querying")));
276
277
  }
277
278
  return [resultsMenu, /*#__PURE__*/React.createElement(MapSelection, {
@@ -54,6 +54,7 @@ var DateTimeInput = /*#__PURE__*/function (_React$Component) {
54
54
  value: function render() {
55
55
  var _this2 = this;
56
56
  var parts = (this.props.value || "T").split("T");
57
+ parts[1] = (parts[1] || "").replace(/\+[0-9:]+$/, ''); // Strip timezone
57
58
  parts[1] = (parts[1] || "").replace(/\.\d+$/, ''); // Strip milliseconds
58
59
  return /*#__PURE__*/React.createElement(InputContainer, {
59
60
  className: "DateTimeInput"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwc2",
3
- "version": "2026.04.29",
3
+ "version": "2026.05.06",
4
4
  "description": "QGIS Web Client",
5
5
  "author": "Sourcepole AG",
6
6
  "license": "BSD-2-Clause",
package/plugins/API.js CHANGED
@@ -401,7 +401,7 @@ var API = /*#__PURE__*/function (_React$Component) {
401
401
  * * `name`: An identifier
402
402
  * * `fmtFunc`: The formatter function with signature `function(name, value, layer, feature)`
403
403
  *
404
- * The `fmtFunc` should return a string (which may also be a HTML fragment).
404
+ * The `fmtFunc` should return a string (which may also be a HTML fragment) or a React element.
405
405
  */
406
406
  _defineProperty(_this, "addIdentifyAttributeFormatter", function (name, fmtFunc) {
407
407
  window.qwc2.__attributeFormatters[name] = fmtFunc;
@@ -1,4 +1,8 @@
1
1
  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); }
2
+ function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
3
+ function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
4
+ function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
5
+ function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
2
6
  function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
3
7
  function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
4
8
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
@@ -192,6 +196,10 @@ var Cyclomedia = /*#__PURE__*/function (_React$Component) {
192
196
  return "\n <!DOCTYPE html>\n <html>\n <head>\n <script nonce=\"".concat((_window$__CSP_NONCE__ = window.__CSP_NONCE__) !== null && _window$__CSP_NONCE__ !== void 0 ? _window$__CSP_NONCE__ : '', "\" type=\"text/javascript\" src=\"https://unpkg.com/react@18.3.1/umd/react.production.min.js\"></script>\n <script nonce=\"").concat((_window$__CSP_NONCE__2 = window.__CSP_NONCE__) !== null && _window$__CSP_NONCE__2 !== void 0 ? _window$__CSP_NONCE__2 : '', "\" type=\"text/javascript\" src=\"https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js\"></script>\n <script nonce=\"").concat((_window$__CSP_NONCE__3 = window.__CSP_NONCE__) !== null && _window$__CSP_NONCE__3 !== void 0 ? _window$__CSP_NONCE__3 : '', "\" type=\"text/javascript\" src=\"https://streetsmart.cyclomedia.com/api/v").concat(_this.props.cyclomediaVersion, "/StreetSmartApi.js\"></script>\n <script nonce=\"").concat((_window$__CSP_NONCE__4 = window.__CSP_NONCE__) !== null && _window$__CSP_NONCE__4 !== void 0 ? _window$__CSP_NONCE__4 : '', "\" 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 ");
193
197
  });
194
198
  _defineProperty(_this, "addRecordingsWFS", function () {
199
+ // Maximum tile size in map CRS units to avoid exceeding the feature limit per request.
200
+ // ~1000 m in metric CRS, ~0.01° (≈ 1000 m) in geographic CRS, ~3280 ft in imperial CRS.
201
+ var crsUnits = CoordinatesUtils.getUnits(_this.props.mapCrs);
202
+ var MAX_TILE_SIZE = crsUnits === 'degrees' ? 0.01 : crsUnits === 'ft' ? 3280 : 1000;
195
203
  var layer = {
196
204
  id: 'cyclomedia-recordings',
197
205
  type: 'wfs',
@@ -202,38 +210,62 @@ var Cyclomedia = /*#__PURE__*/function (_React$Component) {
202
210
  miny = _CoordinatesUtils$rep2[1],
203
211
  maxx = _CoordinatesUtils$rep2[2],
204
212
  maxy = _CoordinatesUtils$rep2[3];
205
- // Cyclomedia WFS only returns up to 3000 points per request. Split bbox in four to reduce chance of hitting the limit
206
- var midx = 0.5 * (minx + maxx);
207
- var midy = 0.5 * (miny + maxy);
208
- var bboxes = [[minx, miny, midx, midy],
209
- // Bottom left
210
- [midx, miny, maxx, midy],
211
- // Bottom right
212
- [midx, midy, maxx, maxy],
213
- // Top right
214
- [minx, midy, midx, maxy] // Top left
215
- ];
216
- bboxes.forEach(function (bbox) {
217
- var bboxstr = bbox.join(",");
218
- 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");
213
+ var width = maxx - minx;
214
+ var height = maxy - miny;
215
+ // Cyclomedia WFS only returns up to 3000 points per request.
216
+ // Split the bbox into tiles so that no tile exceeds MAX_TILE_SIZE in each dimension.
217
+ // Each tile is expanded by OVERLAP on every side so that features on tile borders
218
+ // are reliably included. Duplicate features are removed afterwards via their id.
219
+ var OVERLAP = MAX_TILE_SIZE * 0.05; // 5 % of tile size
220
+ var cols = Math.max(1, Math.ceil(width / MAX_TILE_SIZE));
221
+ var rows = Math.max(1, Math.ceil(height / MAX_TILE_SIZE));
222
+ var tileW = width / cols;
223
+ var tileH = height / rows;
224
+ var tiles = [];
225
+ for (var row = 0; row < rows; row++) {
226
+ for (var col = 0; col < cols; col++) {
227
+ tiles.push([minx + col * tileW - OVERLAP, miny + row * tileH - OVERLAP, minx + (col + 1) * tileW + OVERLAP, miny + (row + 1) * tileH + OVERLAP]);
228
+ }
229
+ }
230
+ var authHeader = "Basic " + btoa(_this.state.username + ":" + _this.state.password);
231
+ var onError = function onError() {
232
+ vectorSource.removeLoadedExtent(extent);
233
+ failure();
234
+ };
235
+ var completed = 0;
236
+ var hasError = false;
237
+ var allFeatures = [];
238
+ tiles.forEach(function (tile) {
239
+ var bboxstr = tile.join(",");
240
+ 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=10000");
219
241
  var xhr = new XMLHttpRequest();
220
242
  xhr.open('GET', reqUrl);
221
- xhr.setRequestHeader("Authorization", "Basic " + btoa(_this.state.username + ":" + _this.state.password));
222
- var onError = function onError() {
223
- vectorSource.removeLoadedExtent(extent);
224
- failure();
243
+ xhr.setRequestHeader("Authorization", authHeader);
244
+ xhr.onerror = function () {
245
+ if (!hasError) {
246
+ hasError = true;
247
+ onError();
248
+ }
225
249
  };
226
- xhr.onerror = onError;
227
250
  xhr.onload = function () {
251
+ if (hasError) {
252
+ return;
253
+ }
228
254
  if (xhr.status === 200) {
229
255
  var features = vectorSource.getFormat().readFeatures(xhr.responseText, {
230
256
  dataProjection: _this.props.mapCrs,
231
257
  featureProjection: projection.getCode()
232
258
  });
233
- vectorSource.addFeatures(features);
234
- success(features);
259
+ allFeatures.push.apply(allFeatures, _toConsumableArray(features));
235
260
  } else {
261
+ hasError = true;
236
262
  onError();
263
+ return;
264
+ }
265
+ completed++;
266
+ if (completed === tiles.length) {
267
+ vectorSource.addFeatures(allFeatures);
268
+ success(allFeatures);
237
269
  }
238
270
  };
239
271
  xhr.send();
@@ -532,7 +532,7 @@ var Editing = /*#__PURE__*/function (_React$Component) {
532
532
  if (match) {
533
533
  var oldvisibility = match.sublayer.visibility;
534
534
  if (oldvisibility !== visibility && visibility !== null) {
535
- var recurseDirection = !oldvisibility ? "both" : "children";
535
+ var recurseDirection = !oldvisibility ? "parents" : null;
536
536
  _this.props.changeLayerProperty(match.layer.id, "visibility", visibility, match.path, recurseDirection);
537
537
  }
538
538
  return oldvisibility;
package/plugins/Help.js CHANGED
@@ -60,8 +60,13 @@ var Help = /*#__PURE__*/function (_React$Component) {
60
60
  return _createClass(Help, [{
61
61
  key: "componentDidMount",
62
62
  value: function componentDidMount() {
63
+ this.componentDidUpdate({});
64
+ }
65
+ }, {
66
+ key: "componentDidUpdate",
67
+ value: function componentDidUpdate(prevProps) {
63
68
  var _this2 = this;
64
- if (this.props.bodyContentsFragmentUrl) {
69
+ if (this.props.bodyContentsFragmentUrl && this.props.bodyContentsFragmentUrl !== prevProps.bodyContentsFragmentUrl) {
65
70
  axios.get(this.props.bodyContentsFragmentUrl).then(function (response) {
66
71
  _this2.setState({
67
72
  body: response.data.replace('$VERSION$', process.env.BuildDate)
@@ -351,6 +351,7 @@ var MapExport = /*#__PURE__*/function (_React$Component) {
351
351
  params.WIDTH = width;
352
352
  params.HEIGHT = height;
353
353
  params.filename = fileName;
354
+ params.FILTER_GEOM = VectorLayerUtils.geoJSONGeomToWkt(VectorLayerUtils.reprojectGeometry(_this.props.filter.filterGeom, _this.props.map.projection, crs));
354
355
 
355
356
  // Dimension values
356
357
  _this.props.layers.forEach(function (layer) {
@@ -464,15 +465,14 @@ var MapExport = /*#__PURE__*/function (_React$Component) {
464
465
  var _layer$mapFormats;
465
466
  return layer.type === 'wms' && layer.role > LayerRole.BACKGROUND && ((_layer$mapFormats = layer.mapFormats) === null || _layer$mapFormats === void 0 ? void 0 : _layer$mapFormats.includes("application/dxf"));
466
467
  }).reverse().map(function (layer) {
467
- var _layer$params$FILTER, _layer$params$FILTER_;
468
+ var _layer$params$FILTER;
468
469
  return {
469
470
  layer: layer,
470
471
  params: _objectSpread(_objectSpread({}, baseParams), {}, {
471
472
  LAYERS: layer.params.LAYERS,
472
473
  OPACITIES: layer.params.OPACITIES,
473
474
  STYLES: layer.params.STYLES,
474
- FILTER: (_layer$params$FILTER = layer.params.FILTER) !== null && _layer$params$FILTER !== void 0 ? _layer$params$FILTER : '',
475
- FILTER_GEOM: (_layer$params$FILTER_ = layer.params.FILTER_GEOM) !== null && _layer$params$FILTER_ !== void 0 ? _layer$params$FILTER_ : ''
475
+ FILTER: (_layer$params$FILTER = layer.params.FILTER) !== null && _layer$params$FILTER !== void 0 ? _layer$params$FILTER : ''
476
476
  })
477
477
  };
478
478
  });
@@ -596,6 +596,7 @@ _defineProperty(MapExport, "propTypes", {
596
596
  exportExternalLayers: PropTypes.bool,
597
597
  /** Template for the name of the generated files when downloading. Can contain the placeholders `{username}`, `{tenant}`, `{theme}`, `{themeTitle}`, `{timestamp}`. */
598
598
  fileNameTemplate: PropTypes.string,
599
+ filter: PropTypes.object,
599
600
  /** Formats to force as available even if the map capabilities report otherwise. Useful if a serviceUrl is defined in a format configuration. */
600
601
  forceAvailableFormats: PropTypes.array,
601
602
  /** Custom format export configuration. Specify a format mime-type (i.e. `application/dxf`) as key, and an array of one or more configurations as value.
@@ -640,7 +641,8 @@ var selector = function selector(state) {
640
641
  return {
641
642
  theme: state.theme.current,
642
643
  map: state.map,
643
- layers: state.layers.flat
644
+ layers: state.layers.flat,
645
+ filter: state.layers.filter
644
646
  };
645
647
  };
646
648
  export default connect(selector, {
@@ -383,8 +383,10 @@ var MapFilter = /*#__PURE__*/function (_React$Component) {
383
383
  });
384
384
  _defineProperty(_this, "renderPredefinedFilters", function () {
385
385
  var predefinedFilters = _this.collectPredefinedFilters(_this.props.layers);
386
- return Object.values(predefinedFilters).map(function (config) {
387
- var _config$title, _this$state$filters$c;
386
+ return Object.values(predefinedFilters).filter(function (filter) {
387
+ return filter.id in _this.state.filters;
388
+ }).map(function (config) {
389
+ var _config$title;
388
390
  return /*#__PURE__*/React.createElement("div", {
389
391
  className: "map-filter-entry",
390
392
  key: config.id
@@ -393,7 +395,7 @@ var MapFilter = /*#__PURE__*/function (_React$Component) {
393
395
  }, /*#__PURE__*/React.createElement("span", {
394
396
  className: "map-filter-entry-title"
395
397
  }, (_config$title = config.title) !== null && _config$title !== void 0 ? _config$title : LocaleUtils.tr(config.titlemsgid)), /*#__PURE__*/React.createElement(ToggleSwitch, {
396
- active: (_this$state$filters$c = _this.state.filters[config.id]) === null || _this$state$filters$c === void 0 ? void 0 : _this$state$filters$c.active,
398
+ active: _this.state.filters[config.id].active,
397
399
  onChange: function onChange(active) {
398
400
  return _this.toggleFilter(config.id, active);
399
401
  }
@@ -37,7 +37,9 @@ import { setEditContext } from '../../actions/editing';
37
37
  import LocationRecorder from '../../components/LocationRecorder';
38
38
  import MeasureSwitcher from '../../components/MeasureSwitcher';
39
39
  import { BottomToolPortalContext } from '../../components/PluginsContainer';
40
+ import ButtonBar from '../../components/widgets/ButtonBar';
40
41
  import FeatureStyles from "../../utils/FeatureStyles";
42
+ import LocaleUtils from '../../utils/LocaleUtils';
41
43
  import MeasureUtils from '../../utils/MeasureUtils';
42
44
 
43
45
  /**
@@ -49,6 +51,7 @@ var EditingSupport = /*#__PURE__*/function (_React$Component) {
49
51
  _classCallCheck(this, EditingSupport);
50
52
  _this = _callSuper(this, EditingSupport, [props]);
51
53
  _defineProperty(_this, "state", {
54
+ activeEditTool: 'Node',
52
55
  showRecordLocation: false,
53
56
  measurements: {
54
57
  showmeasurements: false,
@@ -56,6 +59,11 @@ var EditingSupport = /*#__PURE__*/function (_React$Component) {
56
59
  areaUnit: 'metric'
57
60
  }
58
61
  });
62
+ _defineProperty(_this, "setEditMode", function (action) {
63
+ _this.setState({
64
+ activeEditTool: action
65
+ }, _this.setEditInteraction);
66
+ });
59
67
  _defineProperty(_this, "changeMeasurementState", function (diff) {
60
68
  _this.setState(function (state) {
61
69
  return {
@@ -95,7 +103,7 @@ var EditingSupport = /*#__PURE__*/function (_React$Component) {
95
103
  }
96
104
  _this.currentLayer = _this.layers[_this.props.editContext.id];
97
105
  });
98
- _defineProperty(_this, "addDrawInteraction", function () {
106
+ _defineProperty(_this, "setDrawInteraction", function () {
99
107
  _this.reset();
100
108
  _this.setCurrentLayer();
101
109
  var geomType = _this.props.editContext.geomType.replace(/Z$/, '');
@@ -126,7 +134,7 @@ var EditingSupport = /*#__PURE__*/function (_React$Component) {
126
134
  showRecordLocation: ["Point", "LineString", "MultiPoint", "MultiLineString"].includes(geomType)
127
135
  });
128
136
  });
129
- _defineProperty(_this, "addEditInteraction", function () {
137
+ _defineProperty(_this, "setEditInteraction", function () {
130
138
  var _this$props$editConte;
131
139
  _this.reset();
132
140
  _this.setCurrentLayer();
@@ -135,26 +143,42 @@ var EditingSupport = /*#__PURE__*/function (_React$Component) {
135
143
  _this.currentFeature.on('change', _this.updateMeasurements);
136
144
  _this.updateMeasurements();
137
145
  _this.currentLayer.getSource().addFeature(_this.currentFeature);
138
- var modifyInteraction = new ol.interaction.Modify({
139
- features: new ol.Collection([_this.currentFeature]),
140
- condition: function condition(event) {
141
- return event.originalEvent.buttons === 1;
142
- },
143
- deleteCondition: function deleteCondition(event) {
144
- // delete vertices on SHIFT + click
145
- if (event.type === "pointerdown" && ol.events.condition.shiftKeyOnly(event)) {
146
- _this.props.map.setIgnoreNextClick(true);
147
- }
148
- return ol.events.condition.shiftKeyOnly(event) && ol.events.condition.singleClick(event);
149
- },
150
- style: FeatureStyles.sketchInteraction()
151
- });
152
- modifyInteraction.on('modifyend', function () {
153
- _this.commitCurrentFeature();
154
- }, _this);
155
- modifyInteraction.setActive(_this.props.editContext.geomType && _this.props.editContext.permissions.updatable && ((_this$props$editConte = _this.props.editContext.editConfig) === null || _this$props$editConte === void 0 || (_this$props$editConte = _this$props$editConte.permissions) === null || _this$props$editConte === void 0 ? void 0 : _this$props$editConte.updatable) === true && !_this.props.editContext.geomReadOnly && !_this.props.editContext.geomNonZeroZ);
156
- _this.props.map.addInteraction(modifyInteraction);
157
- _this.interaction = modifyInteraction;
146
+ if (_this.state.activeEditTool === "Node") {
147
+ var modifyInteraction = new ol.interaction.Modify({
148
+ features: new ol.Collection([_this.currentFeature]),
149
+ condition: function condition(event) {
150
+ return event.originalEvent.buttons === 1;
151
+ },
152
+ deleteCondition: function deleteCondition(event) {
153
+ // delete vertices on SHIFT + click
154
+ if (event.type === "pointerdown" && ol.events.condition.shiftKeyOnly(event)) {
155
+ _this.props.map.setIgnoreNextClick(true);
156
+ }
157
+ return ol.events.condition.shiftKeyOnly(event) && ol.events.condition.singleClick(event);
158
+ },
159
+ style: FeatureStyles.sketchInteraction()
160
+ });
161
+ modifyInteraction.on('modifyend', _this.commitCurrentFeature);
162
+ _this.interaction = modifyInteraction;
163
+ _this.props.map.addInteraction(_this.interaction);
164
+ } else if (_this.state.activeEditTool === "Transform") {
165
+ var transformInteraction = new ol.interaction.Transform({
166
+ stretch: false,
167
+ keepAspectRatio: function keepAspectRatio(ev) {
168
+ return ol.events.condition.shiftKeyOnly(ev);
169
+ },
170
+ layers: [_this.currentLayer],
171
+ translateFeature: true,
172
+ selection: false
173
+ });
174
+ transformInteraction.on('rotateend', _this.commitCurrentFeature);
175
+ transformInteraction.on('translateend', _this.commitCurrentFeature);
176
+ transformInteraction.on('scaleend', _this.commitCurrentFeature);
177
+ _this.interaction = transformInteraction;
178
+ _this.props.map.addInteraction(_this.interaction);
179
+ _this.interaction.setSelection([_this.currentFeature]);
180
+ }
181
+ _this.interaction.setActive(_this.props.editContext.geomType && _this.props.editContext.permissions.updatable && ((_this$props$editConte = _this.props.editContext.editConfig) === null || _this$props$editConte === void 0 || (_this$props$editConte = _this$props$editConte.permissions) === null || _this$props$editConte === void 0 ? void 0 : _this$props$editConte.updatable) === true && !_this.props.editContext.geomReadOnly && !_this.props.editContext.geomNonZeroZ);
158
182
  });
159
183
  _defineProperty(_this, "updateMeasurements", function () {
160
184
  var _this$state$measureme;
@@ -237,16 +261,16 @@ var EditingSupport = /*#__PURE__*/function (_React$Component) {
237
261
  } else if (curContext.action === 'Pick' && curContext.feature) {
238
262
  // If a feature without geometry was picked, enter draw mode, otherwise enter edit mode
239
263
  if (!curContext.feature.geometry && curContext.geomType) {
240
- this.addDrawInteraction();
264
+ this.setDrawInteraction();
241
265
  } else {
242
- this.addEditInteraction();
266
+ this.setEditInteraction();
243
267
  }
244
268
  } else if (curContext.action === 'Draw' && curContext.geomType) {
245
269
  // Usually, draw mode starts without a feature, but draw also can start with a pre-set geometry
246
270
  if (!(curContext.feature || {}).geometry || prevContext.id === curContext.id && prevContext.geomType !== curContext.geomType) {
247
- this.addDrawInteraction();
271
+ this.setDrawInteraction();
248
272
  } else if ((curContext.feature || {}).geometry) {
249
- this.addEditInteraction();
273
+ this.setEditInteraction();
250
274
  }
251
275
  } else {
252
276
  this.reset();
@@ -261,9 +285,28 @@ var EditingSupport = /*#__PURE__*/function (_React$Component) {
261
285
  }, {
262
286
  key: "render",
263
287
  value: function render() {
264
- var _this$props$editConte2;
288
+ var _this$props$editConte2, _this$props$editConte3;
289
+ var toolbar = null;
265
290
  var locationRecorder = null;
266
291
  var measureSwitcher = null;
292
+ if ((_this$props$editConte2 = this.props.editContext.feature) !== null && _this$props$editConte2 !== void 0 && _this$props$editConte2.geometry) {
293
+ var editButtons = [{
294
+ key: "Node",
295
+ tooltip: LocaleUtils.tr("redlining.draw"),
296
+ icon: "nodetool"
297
+ }, {
298
+ key: "Transform",
299
+ tooltip: LocaleUtils.tr("redlining.transform"),
300
+ icon: "transformtool"
301
+ }];
302
+ toolbar = /*#__PURE__*/ReactDOM.createPortal(/*#__PURE__*/React.createElement(ButtonBar, {
303
+ active: this.state.activeEditTool,
304
+ buttons: editButtons,
305
+ key: "ButtonBar",
306
+ onClick: this.setEditMode,
307
+ tooltipPos: "top"
308
+ }), this.context);
309
+ }
267
310
  if (this.state.showRecordLocation && this.props.editContext.geomType) {
268
311
  var geomType = this.props.editContext.geomType.replace(/Z$/, '');
269
312
  locationRecorder = /*#__PURE__*/React.createElement(LocationRecorder, {
@@ -273,16 +316,15 @@ var EditingSupport = /*#__PURE__*/function (_React$Component) {
273
316
  map: this.props.map
274
317
  });
275
318
  }
276
- if (this.props.editContext.action === "Draw" || (_this$props$editConte2 = this.props.editContext.feature) !== null && _this$props$editConte2 !== void 0 && _this$props$editConte2.geometry) {
319
+ if (this.props.editContext.action === "Draw" || (_this$props$editConte3 = this.props.editContext.feature) !== null && _this$props$editConte3 !== void 0 && _this$props$editConte3.geometry) {
277
320
  measureSwitcher = /*#__PURE__*/ReactDOM.createPortal(/*#__PURE__*/React.createElement(MeasureSwitcher, {
278
321
  changeMeasureState: this.changeMeasurementState,
279
322
  geomType: this.props.editContext.geomType,
280
- iconSize: "large",
281
323
  key: "MeasureSwitcher",
282
324
  measureState: this.state.measurements
283
325
  }), this.context);
284
326
  }
285
- return [measureSwitcher, locationRecorder];
327
+ return [toolbar, measureSwitcher, locationRecorder];
286
328
  }
287
329
  }]);
288
330
  }(React.Component);
@@ -150,7 +150,7 @@ export default function layers() {
150
150
  }
151
151
  case ADD_LAYER:
152
152
  {
153
- var _action$layer$srcid, _action$layer$visibil, _action$layer$opacity, _action$options, _action$options2;
153
+ var _action$layer$srcid, _action$layer$visibil, _action$layer$opacity, _ref, _action$options$layer, _action$options, _action$options2, _action$options3;
154
154
  var _newLayers2 = (state.flat || []).concat();
155
155
  var layerId = action.layer.id || uuidv4();
156
156
  var newLayer = _objectSpread(_objectSpread({}, action.layer), {}, {
@@ -161,9 +161,9 @@ export default function layers() {
161
161
  queryable: action.layer.queryable || false,
162
162
  visibility: (_action$layer$visibil = action.layer.visibility) !== null && _action$layer$visibil !== void 0 ? _action$layer$visibil : true,
163
163
  opacity: (_action$layer$opacity = action.layer.opacity) !== null && _action$layer$opacity !== void 0 ? _action$layer$opacity : 255,
164
- layertreehidden: action.layer.layertreehidden || action.layer.role > LayerRole.USERLAYER
164
+ layertreehidden: (_ref = (_action$options$layer = (_action$options = action.options) === null || _action$options === void 0 ? void 0 : _action$options.layertreehidden) !== null && _action$options$layer !== void 0 ? _action$options$layer : action.layer.layertreehidden) !== null && _ref !== void 0 ? _ref : action.layer.role > LayerRole.USERLAYER
165
165
  });
166
- if ((_action$options = action.options) !== null && _action$options !== void 0 && _action$options.beforeLayerName || (_action$options2 = action.options) !== null && _action$options2 !== void 0 && _action$options2.afterLayerName) {
166
+ if ((_action$options2 = action.options) !== null && _action$options2 !== void 0 && _action$options2.beforeLayerName || (_action$options3 = action.options) !== null && _action$options3 !== void 0 && _action$options3.afterLayerName) {
167
167
  _newLayers2 = LayerUtils.insertLayer(_newLayers2, newLayer, "name", action.options.beforeLayerName || action.options.afterLayerName, action.options.afterLayerName ? true : false);
168
168
  } else {
169
169
  var inspos = 0;
@@ -315,6 +315,7 @@ export default function layers() {
315
315
  return layer.id === _layerId;
316
316
  });
317
317
  if (idx === -1) {
318
+ var _action$layer$srcid2;
318
319
  var newFeatures = action.features.map(function (f) {
319
320
  return _objectSpread(_objectSpread({}, f), {}, {
320
321
  id: f.id || (f.properties || {}).id || uuidv4()
@@ -330,7 +331,8 @@ export default function layers() {
330
331
  visibility: action.layer.visibility || true,
331
332
  opacity: action.layer.opacity || 255,
332
333
  layertreehidden: action.layer.layertreehidden || action.layer.role > LayerRole.USERLAYER,
333
- bbox: VectorLayerUtils.computeFeaturesBBox(action.features)
334
+ bbox: VectorLayerUtils.computeFeaturesBBox(action.features),
335
+ srcid: (_action$layer$srcid2 = action.layer.srcid) !== null && _action$layer$srcid2 !== void 0 ? _action$layer$srcid2 : uuidv4()
334
336
  });
335
337
  var _inspos = 0;
336
338
  for (; _inspos < _newLayers6.length && _newLayer2.role < _newLayers6[_inspos].role; ++_inspos);
package/reducers/task.js CHANGED
@@ -19,7 +19,8 @@ var defaultState = {
19
19
  data: null,
20
20
  blocked: false,
21
21
  unsetOnMapClick: false,
22
- identifyEnabled: true
22
+ identifyEnabled: true,
23
+ unblockedIdentifyEnabled: true
23
24
  };
24
25
  export default function task() {
25
26
  var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultState;
@@ -35,13 +36,15 @@ export default function task() {
35
36
  mode: action.mode,
36
37
  data: action.data,
37
38
  unsetOnMapClick: action.unsetOnMapClick,
38
- identifyEnabled: action.identifyEnabled
39
+ identifyEnabled: action.identifyEnabled,
40
+ unblockedIdentifyEnabled: action.identifyEnabled
39
41
  });
40
42
  }
41
43
  case SET_CURRENT_TASK_BLOCKED:
42
44
  {
43
45
  return _objectSpread(_objectSpread({}, state), {}, {
44
- blocked: action.blocked
46
+ blocked: action.blocked,
47
+ identifyEnabled: action.blocked ? false : state.unblockedIdentifyEnabled
45
48
  });
46
49
  }
47
50
  default:
@@ -374,7 +374,7 @@ var EditingInterface = {
374
374
  * @param editConfig The edit config of the feature dataset
375
375
  * @param featureId The feature ID
376
376
  * @param mapCrs The CRS of the map, as an EPSG code
377
- * @param tables Comma separated string of relation table names
377
+ * @param tables Comma separated string of relation table references in the form `<table_name>:<fk_name>:<sort_col>`
378
378
  * @param editConfigs The theme editConfig block, containing all theme dataset edit configs
379
379
  * @param callback Callback invoked with the relation values, taking `{<tablename>: {<relation_values>}}` on success and `{}` on failure
380
380
  */
@@ -163,7 +163,8 @@ var LayerUtils = {
163
163
  type: "separator",
164
164
  title: title,
165
165
  role: LayerRole.USERLAYER,
166
- id: uuidv4()
166
+ id: uuidv4(),
167
+ srcid: uuidv4()
167
168
  }]);
168
169
  },
169
170
  createExternalLayerPlaceholder: function createExternalLayerPlaceholder(layerConfig, externalLayers, id) {