qwc2 2026.4.29 → 2026.5.7

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 (50) hide show
  1. package/actions/theme.js +3 -2
  2. package/components/AutoEditForm.js +4 -3
  3. package/components/IdentifyViewer.js +3 -0
  4. package/components/PickFeature.js +2 -1
  5. package/components/widgets/DateTimeInput.js +1 -0
  6. package/icons/label.svg +77 -0
  7. package/package.json +1 -1
  8. package/plugins/API.js +1 -1
  9. package/plugins/Cyclomedia.js +53 -21
  10. package/plugins/Editing.js +1 -1
  11. package/plugins/Help.js +6 -1
  12. package/plugins/MapExport.js +6 -4
  13. package/plugins/MapFilter.js +5 -3
  14. package/plugins/Redlining.js +3 -2
  15. package/plugins/map/EditingSupport.js +72 -30
  16. package/plugins/map/RedliningSupport.js +6 -6
  17. package/plugins/redlining/RedliningFeatureLabelSupport.js +239 -0
  18. package/plugins/style/Redlining.css +4 -1
  19. package/reducers/layers.js +6 -4
  20. package/reducers/task.js +6 -3
  21. package/static/translations/bg-BG.json +3 -0
  22. package/static/translations/ca-ES.json +3 -0
  23. package/static/translations/cs-CZ.json +3 -0
  24. package/static/translations/de-CH.json +3 -0
  25. package/static/translations/de-DE.json +3 -0
  26. package/static/translations/en-US.json +3 -0
  27. package/static/translations/es-ES.json +3 -0
  28. package/static/translations/fi-FI.json +3 -0
  29. package/static/translations/fr-FR.json +3 -0
  30. package/static/translations/hu-HU.json +3 -0
  31. package/static/translations/it-IT.json +3 -0
  32. package/static/translations/ja-JP.json +3 -0
  33. package/static/translations/nl-NL.json +3 -0
  34. package/static/translations/no-NO.json +3 -0
  35. package/static/translations/pl-PL.json +3 -0
  36. package/static/translations/pt-BR.json +3 -0
  37. package/static/translations/pt-PT.json +3 -0
  38. package/static/translations/ro-RO.json +3 -0
  39. package/static/translations/ru-RU.json +3 -0
  40. package/static/translations/sv-SE.json +3 -0
  41. package/static/translations/tr-TR.json +3 -0
  42. package/static/translations/tsconfig.json +3 -0
  43. package/static/translations/uk-UA.json +3 -0
  44. package/utils/CoordinatesUtils.js +8 -0
  45. package/utils/EditingInterface.js +1 -1
  46. package/utils/FeatureStyles.js +31 -0
  47. package/utils/LayerUtils.js +2 -1
  48. package/utils/MiscUtils.js +10 -0
  49. package/utils/ThemeUtils.js +1 -0
  50. package/utils/VectorLayerUtils.js +68 -21
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"
@@ -0,0 +1,77 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ version="1.1"
7
+ id="svg8"
8
+ inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
9
+ sodipodi:docname="label.svg"
10
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
11
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ xmlns:svg="http://www.w3.org/2000/svg"
14
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
15
+ xmlns:cc="http://creativecommons.org/ns#"
16
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
17
+ <defs
18
+ id="defs2" />
19
+ <sodipodi:namedview
20
+ id="base"
21
+ pagecolor="#ffffff"
22
+ bordercolor="#666666"
23
+ borderopacity="1.0"
24
+ inkscape:pageopacity="0.0"
25
+ inkscape:pageshadow="2"
26
+ inkscape:zoom="15.839192"
27
+ inkscape:cx="17.488266"
28
+ inkscape:cy="13.037281"
29
+ inkscape:document-units="px"
30
+ inkscape:current-layer="layer1"
31
+ showgrid="true"
32
+ inkscape:window-width="1920"
33
+ inkscape:window-height="1172"
34
+ inkscape:window-x="0"
35
+ inkscape:window-y="0"
36
+ inkscape:window-maximized="1"
37
+ inkscape:snap-bbox="true"
38
+ inkscape:bbox-paths="true"
39
+ inkscape:bbox-nodes="true"
40
+ inkscape:snap-bbox-edge-midpoints="true"
41
+ inkscape:snap-bbox-midpoints="true"
42
+ units="px"
43
+ inkscape:document-rotation="0"
44
+ inkscape:showpageshadow="2"
45
+ inkscape:pagecheckerboard="0"
46
+ inkscape:deskcolor="#d1d1d1">
47
+ <inkscape:grid
48
+ type="xygrid"
49
+ id="grid4231"
50
+ originx="0"
51
+ originy="0"
52
+ spacingy="1"
53
+ spacingx="1"
54
+ units="px" />
55
+ </sodipodi:namedview>
56
+ <metadata
57
+ id="metadata5">
58
+ <rdf:RDF>
59
+ <cc:Work
60
+ rdf:about="">
61
+ <dc:format>image/svg+xml</dc:format>
62
+ <dc:type
63
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
64
+ </cc:Work>
65
+ </rdf:RDF>
66
+ </metadata>
67
+ <g
68
+ inkscape:label="Layer 1"
69
+ inkscape:groupmode="layer"
70
+ id="layer1"
71
+ transform="translate(0,-290.65)">
72
+ <path
73
+ style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;stroke-linecap:round;stroke-linejoin:round;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
74
+ d="m 4.3007813,292.43097 c -1.8095107,0 -3.30078138,1.48932 -3.3007813,3.29883 v 11 c -8e-8,1.80951 1.4912706,3.30078 3.3007813,3.30078 h 3.4882812 c 0.2921367,6e-5 0.5708031,0.11566 0.7773437,0.32227 l 1.8789068,1.87695 a 1.10011,1.10011 0 0 0 0,0.002 c 0.849893,0.84964 2.259481,0.84964 3.109375,0 a 1.10011,1.10011 0 0 0 0,-0.002 l 1.878906,-1.87695 c 0.20654,-0.20661 0.485207,-0.32221 0.777344,-0.32227 h 3.488281 C 21.508729,310.03058 23,308.53931 23,306.7298 v -11 c 0,-1.80951 -1.491271,-3.29883 -3.300781,-3.29883 z m 0,2.19922 H 19.699219 c 0.620542,0 1.101562,0.47907 1.101562,1.09961 v 11 c 0,0.62054 -0.48102,1.10156 -1.101562,1.10156 h -3.488281 c -0.874721,1.9e-4 -1.715558,0.34818 -2.333985,0.9668 L 12,310.67511 10.123047,308.79816 c -0.6184275,-0.61862 -1.459264,-0.96661 -2.3339845,-0.9668 H 4.3007813 c -0.6205423,0 -1.1015626,-0.48102 -1.1015625,-1.10156 v -11 c -1e-7,-0.62054 0.4810202,-1.09961 1.1015625,-1.09961 z"
75
+ id="path1" />
76
+ </g>
77
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwc2",
3
- "version": "2026.04.29",
3
+ "version": "2026.05.07",
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 = _this.props.filter.filterGeom ? 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
  }
@@ -257,7 +257,8 @@ var Redlining = /*#__PURE__*/function (_React$Component) {
257
257
  for (var _i = 0, _Object$values = Object.values(_this.props.plugins || {}); _i < _Object$values.length; _i++) {
258
258
  var plugin = _Object$values[_i];
259
259
  if (toolEnabled(plugin.cfg.key)) {
260
- editButtons.push(_objectSpread(_objectSpread({}, plugin.cfg), {}, {
260
+ var buttonGroup = plugin.cfg.tooltype === 'draw' ? drawButtons : editButtons;
261
+ buttonGroup.push(_objectSpread(_objectSpread({}, plugin.cfg), {}, {
261
262
  tooltip: plugin.cfg.tooltip ? LocaleUtils.tr(plugin.cfg.tooltip) : undefined
262
263
  }));
263
264
  }
@@ -638,7 +639,7 @@ _defineProperty(Redlining, "propTypes", {
638
639
  defaultTextFillColor: PropTypes.array,
639
640
  /** Default text outline color. In format `[r, g, b, a]`. */
640
641
  defaultTextOutlineColor: PropTypes.array,
641
- /** Tools to hide. Available tools: `Circle`, `Ellipse`, `Square`, `Box`, `HandDrawing`, `Transform`, `NumericInput`, `Buffer`, `Export`. */
642
+ /** Tools to hide. Available tools: `Circle`, `Ellipse`, `Square`, `Box`, `HandDrawing`, `Transform`, `NumericInput`, `Buffer`, `FeatureLabel`, `Export`. */
642
643
  hiddenTools: PropTypes.array,
643
644
  layers: PropTypes.array,
644
645
  mapCrs: PropTypes.string,
@@ -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);
@@ -176,11 +176,12 @@ var RedliningSupport = /*#__PURE__*/function (_React$Component) {
176
176
  return _defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty({}, isText ? "textOutlineColor" : "borderColor", styleOptions.strokeColor), "strokeDash", styleOptions.strokeDash), "size", (styleOptions.strokeWidth - 1) * 2), isText ? "textFillColor" : "fillColor", styleOptions.fillColor), "text", label), "headmarker", styleOptions.headmarker), "tailmarker", styleOptions.tailmarker);
177
177
  });
178
178
  _defineProperty(_this, "updateFeatureStyle", function (feature) {
179
+ var _feature$get;
179
180
  _this.blockOnChange = true;
180
181
  var styleProps = _this.props.redlining.style;
181
182
  var isText = feature.get("shape") === "Text";
182
- var styleName = isText ? "text" : "default";
183
- var opts = _this.styleOptions(styleProps, isText);
183
+ var styleName = (_feature$get = feature.get("styleName")) !== null && _feature$get !== void 0 ? _feature$get : isText ? "text" : "default";
184
+ var opts = _objectSpread(_objectSpread({}, feature.get('styleOptions')), _this.styleOptions(styleProps, isText));
184
185
  if (!feature.get('measurements') && _this.selectedFeatures.length <= 1 && !(isText && !styleProps.text)) {
185
186
  feature.set('label', styleProps.text);
186
187
  }
@@ -220,12 +221,12 @@ var RedliningSupport = /*#__PURE__*/function (_React$Component) {
220
221
  _defineProperty(_this, "styleFunction", function (feature) {
221
222
  var styleOptions = feature.get("styleOptions");
222
223
  var styleName = feature.get("styleName");
224
+ var shape = feature.get('shape');
223
225
  var styles = [];
224
- if (styleName === "text") {
226
+ if (shape === "Text") {
225
227
  styles.push(_this.selectedTextStyle(feature, styleOptions));
226
228
  }
227
229
  styles.push.apply(styles, _toConsumableArray(FeatureStyles[styleName](feature, styleOptions)));
228
- var shape = feature.get('shape');
229
230
  var geomTypeConfig = GeomTypeConfig[shape];
230
231
  if ((geomTypeConfig || {}).drawNodes !== false) {
231
232
  styles.push(_this.selectedStyle);
@@ -618,8 +619,7 @@ var RedliningSupport = /*#__PURE__*/function (_React$Component) {
618
619
  }
619
620
  });
620
621
  _defineProperty(_this, "deselectFeature", function (feature, updateState) {
621
- var styleName = feature.get("shape") === "Text" ? "text" : "default";
622
- var style = FeatureStyles[styleName](feature, feature.get('styleOptions'));
622
+ var style = FeatureStyles[feature.get("styleName")](feature, feature.get('styleOptions'));
623
623
  feature.setStyle(style);
624
624
  feature.un('change', _this.updateMeasurements);
625
625
  _this.selectedFeatures = _this.selectedFeatures.filter(function (f) {