ol-wfst 4.3.0 → 5.0.0

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 (102) hide show
  1. package/README.md +240 -173
  2. package/dist/@types.d.ts +25 -9
  3. package/dist/@types.d.ts.map +1 -1
  4. package/dist/Geoserver.d.ts +6 -4
  5. package/dist/Geoserver.d.ts.map +1 -1
  6. package/dist/WfsLayer.d.ts +10 -6
  7. package/dist/WfsLayer.d.ts.map +1 -1
  8. package/dist/WmsLayer.d.ts +34 -4
  9. package/dist/WmsLayer.d.ts.map +1 -1
  10. package/dist/css/ol-wfst.bootstrap5.css +102 -90
  11. package/dist/css/ol-wfst.bootstrap5.min.css +1 -1
  12. package/dist/css/ol-wfst.css +432 -54
  13. package/dist/css/ol-wfst.min.css +1 -1
  14. package/dist/defaults.d.ts.map +1 -1
  15. package/dist/jsx-runtime.d.ts +4 -0
  16. package/dist/jsx-runtime.d.ts.map +1 -0
  17. package/dist/modules/EditControlChanges.d.ts +2 -1
  18. package/dist/modules/EditControlChanges.d.ts.map +1 -1
  19. package/dist/modules/EditFieldsModal.d.ts +10 -6
  20. package/dist/modules/EditFieldsModal.d.ts.map +1 -1
  21. package/dist/modules/EditOverlay.d.ts +1 -1
  22. package/dist/modules/EditOverlay.d.ts.map +1 -1
  23. package/dist/modules/ExtendLineOverlay.d.ts +12 -0
  24. package/dist/modules/ExtendLineOverlay.d.ts.map +1 -0
  25. package/dist/modules/LayersControl.d.ts +5 -0
  26. package/dist/modules/LayersControl.d.ts.map +1 -1
  27. package/dist/modules/QueryOverlay.d.ts +21 -0
  28. package/dist/modules/QueryOverlay.d.ts.map +1 -0
  29. package/dist/modules/base/BaseLayer.d.ts +7 -3
  30. package/dist/modules/base/BaseLayer.d.ts.map +1 -1
  31. package/dist/modules/base/WfsSource.d.ts.map +1 -1
  32. package/dist/modules/editLayer.d.ts.map +1 -1
  33. package/dist/modules/errors.d.ts +1 -1
  34. package/dist/modules/errors.d.ts.map +1 -1
  35. package/dist/modules/helpers.d.ts.map +1 -1
  36. package/dist/modules/i18n/en.d.ts.map +1 -1
  37. package/dist/modules/i18n/es.d.ts.map +1 -1
  38. package/dist/modules/i18n/index.d.ts.map +1 -1
  39. package/dist/modules/i18n/zh.d.ts.map +1 -1
  40. package/dist/modules/loading.d.ts.map +1 -1
  41. package/dist/modules/state.d.ts +9 -1
  42. package/dist/modules/state.d.ts.map +1 -1
  43. package/dist/modules/styleFunction.d.ts +36 -3
  44. package/dist/modules/styleFunction.d.ts.map +1 -1
  45. package/dist/ol-wfst.d.ts +243 -6
  46. package/dist/ol-wfst.d.ts.map +1 -1
  47. package/dist/ol-wfst.js +1855 -386
  48. package/dist/ol-wfst.js.map +1 -1
  49. package/dist/ol-wfst.min.js +3 -3
  50. package/dist/ol-wfst.min.js.map +1 -1
  51. package/lib/@types.d.ts +25 -9
  52. package/lib/@types.d.ts.map +1 -1
  53. package/lib/Geoserver.d.ts +6 -4
  54. package/lib/Geoserver.d.ts.map +1 -1
  55. package/lib/WfsLayer.d.ts +10 -6
  56. package/lib/WfsLayer.d.ts.map +1 -1
  57. package/lib/WmsLayer.d.ts +34 -4
  58. package/lib/WmsLayer.d.ts.map +1 -1
  59. package/lib/defaults.d.ts.map +1 -1
  60. package/lib/jsx-runtime.d.ts +4 -0
  61. package/lib/jsx-runtime.d.ts.map +1 -0
  62. package/lib/modules/EditControlChanges.d.ts +2 -1
  63. package/lib/modules/EditControlChanges.d.ts.map +1 -1
  64. package/lib/modules/EditFieldsModal.d.ts +10 -6
  65. package/lib/modules/EditFieldsModal.d.ts.map +1 -1
  66. package/lib/modules/EditOverlay.d.ts +1 -1
  67. package/lib/modules/EditOverlay.d.ts.map +1 -1
  68. package/lib/modules/ExtendLineOverlay.d.ts +12 -0
  69. package/lib/modules/ExtendLineOverlay.d.ts.map +1 -0
  70. package/lib/modules/LayersControl.d.ts +5 -0
  71. package/lib/modules/LayersControl.d.ts.map +1 -1
  72. package/lib/modules/QueryOverlay.d.ts +21 -0
  73. package/lib/modules/QueryOverlay.d.ts.map +1 -0
  74. package/lib/modules/base/BaseLayer.d.ts +7 -3
  75. package/lib/modules/base/BaseLayer.d.ts.map +1 -1
  76. package/lib/modules/base/WfsSource.d.ts.map +1 -1
  77. package/lib/modules/editLayer.d.ts.map +1 -1
  78. package/lib/modules/errors.d.ts +1 -1
  79. package/lib/modules/errors.d.ts.map +1 -1
  80. package/lib/modules/helpers.d.ts.map +1 -1
  81. package/lib/modules/i18n/en.d.ts.map +1 -1
  82. package/lib/modules/i18n/es.d.ts.map +1 -1
  83. package/lib/modules/i18n/index.d.ts.map +1 -1
  84. package/lib/modules/i18n/zh.d.ts.map +1 -1
  85. package/lib/modules/loading.d.ts.map +1 -1
  86. package/lib/modules/state.d.ts +9 -1
  87. package/lib/modules/state.d.ts.map +1 -1
  88. package/lib/modules/styleFunction.d.ts +36 -3
  89. package/lib/modules/styleFunction.d.ts.map +1 -1
  90. package/lib/ol-wfst.d.ts +243 -6
  91. package/lib/ol-wfst.d.ts.map +1 -1
  92. package/lib/ol-wfst.js +1857 -384
  93. package/lib/ol-wfst.js.map +1 -1
  94. package/lib/style/css/ol-gisify.css +432 -54
  95. package/lib/style/css/ol-wfst.bootstrap5.css +102 -90
  96. package/lib/style/scss/_variables.scss +2 -1
  97. package/lib/style/scss/ol-wfst.scss +441 -42
  98. package/package.json +3 -3
  99. package/dist/myPragma.d.ts +0 -2
  100. package/dist/myPragma.d.ts.map +0 -1
  101. package/lib/myPragma.d.ts +0 -2
  102. package/lib/myPragma.d.ts.map +0 -1
package/dist/ol-wfst.js CHANGED
@@ -1,13 +1,13 @@
1
1
  /*!
2
- * ol-wfst - v4.3.0
2
+ * ol-wfst - v4.4.0
3
3
  * https://github.com/GastonZalba/ol-wfst#readme
4
- * Built: Thu May 02 2024 18:20:01 GMT-0300 (Argentina Standard Time)
4
+ * Built: Thu Sep 10 2026 23:06:10 GMT-0300 (Argentina Standard Time)
5
5
  */
6
6
  (function (global, factory) {
7
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('ol/style/Circle.js'), require('ol/style/Fill.js'), require('ol/style/Stroke.js'), require('ol/style/Style.js'), require('ol/control/Control.js'), require('ol/interaction/Draw.js'), require('ol/interaction/Modify.js'), require('ol/interaction/Select.js'), require('ol/interaction/Snap.js'), require('ol/Collection.js'), require('ol/events/Event.js'), require('ol/events/condition.js'), require('ol/Observable.js'), require('ol/layer/Vector.js'), require('ol/layer/Base.js'), require('ol/format/GeoJSON.js'), require('ol/source/Vector.js'), require('ol/proj.js'), require('ol/loadingstrategy.js'), require('ol/layer/Tile.js'), require('ol/source/TileWMS.js'), require('ol/geom.js'), require('ol/format/KML.js'), require('ol/format/WFS.js'), require('ol/style.js'), require('ol/Object.js'), require('ol/geom/Circle.js'), require('ol/geom/GeometryCollection.js'), require('ol/Feature.js'), require('ol/geom/Polygon.js'), require('ol/format/XML.js'), require('ol/xml.js'), require('ol/format/xlink.js'), require('ol/format/xsd.js'), require('ol/extent.js'), require('ol/Overlay.js')) :
8
- typeof define === 'function' && define.amd ? define(['ol/style/Circle.js', 'ol/style/Fill.js', 'ol/style/Stroke.js', 'ol/style/Style.js', 'ol/control/Control.js', 'ol/interaction/Draw.js', 'ol/interaction/Modify.js', 'ol/interaction/Select.js', 'ol/interaction/Snap.js', 'ol/Collection.js', 'ol/events/Event.js', 'ol/events/condition.js', 'ol/Observable.js', 'ol/layer/Vector.js', 'ol/layer/Base.js', 'ol/format/GeoJSON.js', 'ol/source/Vector.js', 'ol/proj.js', 'ol/loadingstrategy.js', 'ol/layer/Tile.js', 'ol/source/TileWMS.js', 'ol/geom.js', 'ol/format/KML.js', 'ol/format/WFS.js', 'ol/style.js', 'ol/Object.js', 'ol/geom/Circle.js', 'ol/geom/GeometryCollection.js', 'ol/Feature.js', 'ol/geom/Polygon.js', 'ol/format/XML.js', 'ol/xml.js', 'ol/format/xlink.js', 'ol/format/xsd.js', 'ol/extent.js', 'ol/Overlay.js'], factory) :
9
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Wfst = factory(global.ol.style.Circle, global.ol.style.Fill, global.ol.style.Stroke, global.ol.style.Style, global.ol.control.Control, global.ol.interaction.Draw, global.ol.interaction.Modify, global.ol.interaction.Select, global.ol.interaction.Snap, global.ol.Collection, global.ol.events.Event, global.ol.events.condition, global.ol.Observable, global.ol.layer.Vector, global.ol.layer.Base, global.ol.format.GeoJSON, global.ol.source.Vector, global.ol.proj, global.ol.loadingstrategy, global.ol.layer.Tile, global.ol.source.TileWMS, global.ol.geom, global.ol.format.KML, global.ol.format.WFS, global.ol.style, global.ol.Object, global.ol.geom.Circle, global.ol.geom.GeometryCollection, global.ol.Feature, global.ol.geom.Polygon, global.ol.format.XML, global.ol.xml, global.ol.format.xlink, global.ol.format.xsd, global.ol.extent, global.ol.Overlay));
10
- })(this, (function (CircleStyle, Fill, Stroke, Style, Control, Draw, Modify, Select, Snap, Collection, BaseEvent, condition_js, Observable, VectorLayer, Layer, GeoJSON, VectorSource, proj_js, loadingstrategy_js, TileLayer, TileWMS, geom_js, KML, WFS, style_js, BaseObject, Circle, GeometryCollection, Feature, Polygon_js, XML, xml_js, xlink_js, xsd_js, extent_js, Overlay) { 'use strict';
7
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('ol/style/Fill.js'), require('ol/style/Stroke.js'), require('ol/style/Style.js'), require('ol/control/Control.js'), require('ol/control/FullScreen.js'), require('ol/interaction/Draw.js'), require('ol/interaction/DragBox.js'), require('ol/interaction/Modify.js'), require('ol/interaction/Select.js'), require('ol/interaction/Snap.js'), require('ol/geom/LineString.js'), require('ol/geom/MultiLineString.js'), require('ol/geom/GeometryCollection.js'), require('ol/Collection.js'), require('ol/layer/Vector.js'), require('ol/source/Vector.js'), require('ol/events/Event.js'), require('ol/events/condition.js'), require('ol/Observable.js'), require('ol/coordinate.js'), require('ol/extent.js'), require('ol/layer/Base.js'), require('ol/format/GeoJSON.js'), require('ol/proj.js'), require('ol/loadingstrategy.js'), require('ol/layer/Tile.js'), require('ol/source/TileWMS.js'), require('ol/geom.js'), require('ol/format/KML.js'), require('ol/format/WFS.js'), require('ol/style.js'), require('ol/Object.js'), require('ol/geom/Circle.js'), require('ol/Feature.js'), require('ol/geom/Polygon.js'), require('ol/format/XML.js'), require('ol/xml.js'), require('ol/format/xlink.js'), require('ol/format/xsd.js'), require('ol/Overlay.js')) :
8
+ typeof define === 'function' && define.amd ? define(['ol/style/Fill.js', 'ol/style/Stroke.js', 'ol/style/Style.js', 'ol/control/Control.js', 'ol/control/FullScreen.js', 'ol/interaction/Draw.js', 'ol/interaction/DragBox.js', 'ol/interaction/Modify.js', 'ol/interaction/Select.js', 'ol/interaction/Snap.js', 'ol/geom/LineString.js', 'ol/geom/MultiLineString.js', 'ol/geom/GeometryCollection.js', 'ol/Collection.js', 'ol/layer/Vector.js', 'ol/source/Vector.js', 'ol/events/Event.js', 'ol/events/condition.js', 'ol/Observable.js', 'ol/coordinate.js', 'ol/extent.js', 'ol/layer/Base.js', 'ol/format/GeoJSON.js', 'ol/proj.js', 'ol/loadingstrategy.js', 'ol/layer/Tile.js', 'ol/source/TileWMS.js', 'ol/geom.js', 'ol/format/KML.js', 'ol/format/WFS.js', 'ol/style.js', 'ol/Object.js', 'ol/geom/Circle.js', 'ol/Feature.js', 'ol/geom/Polygon.js', 'ol/format/XML.js', 'ol/xml.js', 'ol/format/xlink.js', 'ol/format/xsd.js', 'ol/Overlay.js'], factory) :
9
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Wfst = factory(global.ol.style.Fill, global.ol.style.Stroke, global.ol.style.Style, global.ol.control.Control, global.ol.control.FullScreen, global.ol.interaction.Draw, global.ol.interaction.DragBox, global.ol.interaction.Modify, global.ol.interaction.Select, global.ol.interaction.Snap, global.ol.geom.LineString, global.ol.geom.MultiLineString, global.ol.geom.GeometryCollection, global.ol.Collection, global.ol.layer.Vector, global.ol.source.Vector, global.ol.events.Event, global.ol.events.condition, global.ol.Observable, global.ol.coordinate, global.ol.extent, global.ol.layer.Base, global.ol.format.GeoJSON, global.ol.proj, global.ol.loadingstrategy, global.ol.layer.Tile, global.ol.source.TileWMS, global.ol.geom, global.ol.format.KML, global.ol.format.WFS, global.ol.style, global.ol.Object, global.ol.geom.Circle, global.ol.Feature, global.ol.geom.Polygon, global.ol.format.XML, global.ol.xml, global.ol.format.xlink, global.ol.format.xsd, global.ol.Overlay));
10
+ })(this, (function (Fill, Stroke, Style, Control, FullScreen, Draw, DragBox, Modify, Select, Snap, LineString, MultiLineString, GeometryCollection, Collection, VectorLayer, VectorSource, BaseEvent, condition_js, Observable, coordinate_js, extent_js, Layer, GeoJSON, proj_js, loadingstrategy_js, TileLayer, TileWMS, geom_js, KML, WFS, style_js, BaseObject, Circle, Feature, Polygon_js, XML, xml_js, xlink_js, xsd_js, Overlay) { 'use strict';
11
11
 
12
12
  var domain;
13
13
 
@@ -1140,6 +1140,10 @@
1140
1140
  const es = {
1141
1141
  labels: {
1142
1142
  select: 'Seleccionar',
1143
+ selectBox: 'Seleccionar elementos dibujando una caja',
1144
+ selectFreehand: 'Seleccionar elementos dibujando a mano alzada',
1145
+ query: 'Consultar información del elemento',
1146
+ featureInfo: 'Información del elemento',
1143
1147
  addElement: 'Modo dibujo',
1144
1148
  editElement: 'Editar elemento',
1145
1149
  save: 'Guardar',
@@ -1149,9 +1153,11 @@
1149
1153
  upload: 'Subir',
1150
1154
  editMode: 'Modo Edición',
1151
1155
  confirmDelete: '¿Estás seguro de borrar el elemento?',
1156
+ confirmDeleteElements: '¿Borrar los {} elementos?',
1152
1157
  geomTypeNotSupported: 'Geometría no compatible con la capa',
1153
1158
  editFields: 'Editar campos',
1154
1159
  editGeom: 'Editar geometría',
1160
+ continueLine: 'Continuar dibujando desde aquí',
1155
1161
  selectDrawType: 'Tipo de geometría para dibujar',
1156
1162
  uploadToLayer: 'Subir archivo a la capa seleccionada',
1157
1163
  uploadFeatures: 'Subida de elementos a la capa',
@@ -1159,7 +1165,12 @@
1159
1165
  invalidFeatures: 'Invalidas',
1160
1166
  loading: 'Cargando...',
1161
1167
  toggleVisibility: 'Cambiar visibilidad de la capa',
1162
- close: 'Cerrar'
1168
+ fullscreen: 'Pantalla completa',
1169
+ close: 'Cerrar',
1170
+ multipleValues: 'Valores múltiples',
1171
+ editElements: 'Editar elementos ({})',
1172
+ multipleEditNotice: 'Edición múltiple activada. Los cambios se aplicarán a los {} elementos seleccionados.',
1173
+ geoserverLayers: 'Capas de GeoServer'
1163
1174
  },
1164
1175
  errors: {
1165
1176
  capabilities: 'No se pudieron obtener las Capabilidades del GeoServer',
@@ -1180,6 +1191,10 @@
1180
1191
  const en = {
1181
1192
  labels: {
1182
1193
  select: 'Select',
1194
+ selectBox: 'Select features by drawing a box',
1195
+ selectFreehand: 'Select features by freehand drawing',
1196
+ query: 'Query feature info',
1197
+ featureInfo: 'Feature info',
1183
1198
  addElement: 'Toggle Draw mode',
1184
1199
  editElement: 'Edit feature',
1185
1200
  save: 'Save',
@@ -1189,9 +1204,11 @@
1189
1204
  upload: 'Upload',
1190
1205
  editMode: 'Edit Mode',
1191
1206
  confirmDelete: 'Are you sure to delete the feature?',
1207
+ confirmDeleteElements: 'Delete the {} selected features?',
1192
1208
  geomTypeNotSupported: 'Geometry not supported by layer',
1193
1209
  editFields: 'Edit fields',
1194
1210
  editGeom: 'Edit geometry',
1211
+ continueLine: 'Continue drawing from here',
1195
1212
  selectDrawType: 'Geometry type to draw',
1196
1213
  uploadToLayer: 'Upload file to selected layer',
1197
1214
  uploadFeatures: 'Uploaded features to layer',
@@ -1199,7 +1216,12 @@
1199
1216
  invalidFeatures: 'Invalid',
1200
1217
  loading: 'Loading...',
1201
1218
  toggleVisibility: 'Toggle layer visibility',
1202
- close: 'Close'
1219
+ fullscreen: 'Fullscreen',
1220
+ close: 'Close',
1221
+ multipleValues: 'Multiple values',
1222
+ editElements: 'Edit features ({})',
1223
+ multipleEditNotice: 'Multi-edit active. Changes will be applied to the {} selected features.',
1224
+ geoserverLayers: 'Geoserver Layers'
1203
1225
  },
1204
1226
  errors: {
1205
1227
  capabilities: 'GeoServer Capabilities could not be downloaded.',
@@ -1220,6 +1242,10 @@
1220
1242
  const zh = {
1221
1243
  labels: {
1222
1244
  select: '选择',
1245
+ selectBox: '框选元素',
1246
+ selectFreehand: '手绘选择元素',
1247
+ query: '查询元素信息',
1248
+ featureInfo: '元素信息',
1223
1249
  addElement: '切换绘图类型',
1224
1250
  editElement: '编辑元素',
1225
1251
  save: '保存',
@@ -1229,9 +1255,11 @@
1229
1255
  upload: '上传',
1230
1256
  editMode: '编辑模式',
1231
1257
  confirmDelete: '确认删除元素?',
1258
+ confirmDeleteElements: '确认删除选中的 {} 个元素?',
1232
1259
  geomTypeNotSupported: '图层不支持该几何',
1233
1260
  editFields: '编辑区域',
1234
1261
  editGeom: '编辑几何',
1262
+ continueLine: '从此处继续绘制',
1235
1263
  selectDrawType: '几何类型',
1236
1264
  uploadToLayer: '通过文件上传图层',
1237
1265
  uploadFeatures: '上传元素到图层',
@@ -1239,7 +1267,12 @@
1239
1267
  invalidFeatures: '不合法',
1240
1268
  loading: '加载中...',
1241
1269
  toggleVisibility: '切换图层透明度',
1242
- close: '关闭'
1270
+ fullscreen: '全屏',
1271
+ close: '关闭',
1272
+ multipleValues: '多个值',
1273
+ editElements: '编辑元素({})',
1274
+ multipleEditNotice: '多选编辑已激活。更改将应用于所有 {} 个选定元素。',
1275
+ geoserverLayers: 'GeoServer 图层'
1243
1276
  },
1244
1277
  errors: {
1245
1278
  capabilities: '无法加载GeoServer服务所支持的能力.',
@@ -1274,12 +1307,37 @@
1274
1307
  I18N = Object.assign(Object.assign({}, I18N), customI18n);
1275
1308
  }
1276
1309
  };
1310
+ /**
1311
+ * /**
1312
+ * For translations thas has a variable "{}"" to be replaced inside
1313
+ * @param string
1314
+ * @param args
1315
+ * @returns
1316
+ */
1317
+ const I18N_ = (string, ...args) => {
1318
+ var _a;
1319
+ let text = (_a = I18N.labels) === null || _a === void 0 ? void 0 : _a[string];
1320
+ if (!text) {
1321
+ text = I18N[string];
1322
+ }
1323
+ if (!text) {
1324
+ console.error('Translation not found', string);
1325
+ text = string;
1326
+ }
1327
+ if (args.length) {
1328
+ args.forEach((arg) => {
1329
+ text = text.replace(/{}/, arg);
1330
+ });
1331
+ }
1332
+ return text;
1333
+ };
1277
1334
 
1278
1335
  let loadingDiv;
1279
1336
  const initLoading = () => {
1280
1337
  loadingDiv = document.createElement('div');
1281
1338
  loadingDiv.className = 'ol-wfst--tools-control--loading';
1282
- loadingDiv.innerHTML = I18N.labels.loading;
1339
+ loadingDiv.setAttribute('role', 'progressbar');
1340
+ loadingDiv.setAttribute('aria-label', I18N.labels.loading);
1283
1341
  return loadingDiv;
1284
1342
  };
1285
1343
  const showLoading = (bool = true) => {
@@ -1364,7 +1422,7 @@
1364
1422
  const unique = (arr) => arr.filter((e, i) => arr.indexOf(e) == i);
1365
1423
 
1366
1424
  // Keeps track of constituent classes for every mixin class created by ts-mixer.
1367
- const mixins = new Map();
1425
+ const mixins = new WeakMap();
1368
1426
  const getMixinsForClass = (clazz) => mixins.get(clazz);
1369
1427
  const registerMixins = (mixedClass, constituents) => mixins.set(mixedClass, constituents);
1370
1428
 
@@ -1488,13 +1546,27 @@
1488
1546
  (function (Modes) {
1489
1547
  Modes["Edit"] = "EDIT";
1490
1548
  Modes["Draw"] = "DRAW";
1549
+ Modes["Query"] = "QUERY";
1491
1550
  })(Modes || (Modes = {}));
1551
+ var SelectionMode;
1552
+ (function (SelectionMode) {
1553
+ SelectionMode["Single"] = "SINGLE";
1554
+ SelectionMode["Box"] = "BOX";
1555
+ SelectionMode["Freehand"] = "FREEHAND";
1556
+ })(SelectionMode || (SelectionMode = {}));
1557
+ let selectionMode = SelectionMode.Single;
1492
1558
  function activateMode(m = null) {
1493
1559
  mode = m;
1494
1560
  }
1495
1561
  function getMode() {
1496
1562
  return mode;
1497
1563
  }
1564
+ function setSelectionMode(m) {
1565
+ selectionMode = m;
1566
+ }
1567
+ function getSelectionMode() {
1568
+ return selectionMode;
1569
+ }
1498
1570
  const editedFeatures = new Set();
1499
1571
  const mapLayers = {};
1500
1572
  function setMap(m) {
@@ -1533,16 +1605,23 @@
1533
1605
  */
1534
1606
  class BaseLayer extends Layer {
1535
1607
  /**
1608
+ * Initialize the layer: request DescribeFeatureType and resolve when it
1609
+ * finishes (whether successful or not).
1610
+ *
1536
1611
  * @private
1612
+ * @returns Promise that resolves when the layer is initialized
1537
1613
  */
1538
- _init() {
1614
+ async _init() {
1539
1615
  const geoserver = this.getGeoserver();
1540
1616
  if (geoserver.isLoaded()) {
1541
- this.getAndUpdateDescribeFeatureType();
1617
+ await this.getAndUpdateDescribeFeatureType();
1542
1618
  }
1543
1619
  else {
1544
- geoserver.on('change:capabilities', async () => {
1545
- this.getAndUpdateDescribeFeatureType();
1620
+ await new Promise((resolve) => {
1621
+ geoserver.once('change:capabilities', async () => {
1622
+ await this.getAndUpdateDescribeFeatureType();
1623
+ resolve();
1624
+ });
1546
1625
  });
1547
1626
  }
1548
1627
  }
@@ -1618,13 +1697,13 @@
1618
1697
  }
1619
1698
  /**
1620
1699
  * @public
1621
- * @param featureId
1700
+ * @param featureIds
1622
1701
  * @returns
1623
1702
  */
1624
- async maybeLockFeature(featureId) {
1703
+ async maybeLockFeature(featureIds) {
1625
1704
  const geoserver = this.getGeoserver();
1626
1705
  if (geoserver.getUseLockFeature() && geoserver.hasLockFeature()) {
1627
- return await geoserver.lockFeature(featureId, this.get(BaseLayerProperty.NAME));
1706
+ return await geoserver.lockFeature(featureIds, this.get(BaseLayerProperty.NAME));
1628
1707
  }
1629
1708
  return null;
1630
1709
  }
@@ -1663,44 +1742,48 @@
1663
1742
  */
1664
1743
  class WfsSource extends VectorSource {
1665
1744
  constructor(options) {
1666
- super(Object.assign(Object.assign({}, options), { format: new GeoJSON(), loader: async (extent, resolution, projection, success, failure) => {
1667
- try {
1668
- // If bbox, add extent to the request
1669
- if (options.strategy == loadingstrategy_js.bbox) {
1670
- const extentGeoServer = proj_js.transformExtent(extent, projection.getCode(), options.geoServerAdvanced.projection);
1671
- // https://docs.geoserver.org/stable/en/user/services/wfs/reference.html
1672
- // request features using a bounding box with CRS maybe different from featureTypes native CRS
1673
- this.urlParams.set('bbox', extentGeoServer.toString() +
1674
- `,${options.geoServerAdvanced.projection}`);
1675
- }
1676
- const url_fetch = options.geoserverUrl + '?' + this.urlParams.toString();
1677
- const response = await fetch(url_fetch, {
1678
- headers: options.headers,
1679
- credentials: options.credentials
1680
- });
1681
- if (!response.ok) {
1682
- throw new Error('');
1745
+ super(Object.assign(Object.assign({}, options), { format: new GeoJSON(), loader: (extent, resolution, projection, success, failure) => {
1746
+ void (async () => {
1747
+ try {
1748
+ // If bbox, add extent to the request
1749
+ if (options.strategy == loadingstrategy_js.bbox) {
1750
+ const extentGeoServer = proj_js.transformExtent(extent, projection.getCode(), options.geoServerAdvanced.projection);
1751
+ // https://docs.geoserver.org/stable/en/user/services/wfs/reference.html
1752
+ // request features using a bounding box with CRS maybe different from featureTypes native CRS
1753
+ this.urlParams.set('bbox', extentGeoServer.toString() +
1754
+ `,${options.geoServerAdvanced.projection}`);
1755
+ }
1756
+ const url_fetch = options.geoserverUrl +
1757
+ '?' +
1758
+ this.urlParams.toString();
1759
+ const response = await fetch(url_fetch, {
1760
+ headers: options.headers,
1761
+ credentials: options.credentials
1762
+ });
1763
+ if (!response.ok) {
1764
+ throw new Error('');
1765
+ }
1766
+ const data = await response.json();
1767
+ if (data.exceptions) {
1768
+ throw new Error(parseError(data));
1769
+ }
1770
+ const features = this.getFormat().readFeatures(data, {
1771
+ featureProjection: projection.getCode(),
1772
+ dataProjection: options.geoServerAdvanced.projection
1773
+ });
1774
+ features.forEach((feature) => {
1775
+ feature.set('_layerName_', options.name,
1776
+ /* silent = */ true);
1777
+ });
1778
+ this.addFeatures(features);
1779
+ success(features);
1683
1780
  }
1684
- const data = await response.json();
1685
- if (data.exceptions) {
1686
- throw new Error(parseError(data));
1781
+ catch (err) {
1782
+ this.removeLoadedExtent(extent);
1783
+ showError(I18N.errors.geoserver, err, options.name);
1784
+ failure();
1687
1785
  }
1688
- const features = this.getFormat().readFeatures(data, {
1689
- featureProjection: projection.getCode(),
1690
- dataProjection: options.geoServerAdvanced.projection
1691
- });
1692
- features.forEach((feature) => {
1693
- feature.set('_layerName_', options.name,
1694
- /* silent = */ true);
1695
- });
1696
- this.addFeatures(features);
1697
- success(features);
1698
- }
1699
- catch (err) {
1700
- this.removeLoadedExtent(extent);
1701
- showError(I18N.errors.geoserver, err, options.name);
1702
- failure();
1703
- }
1786
+ })();
1704
1787
  } }));
1705
1788
  this.urlParams = new URLSearchParams({
1706
1789
  SERVICE: 'wfs',
@@ -1867,7 +1950,7 @@
1867
1950
  this._loadingCount = 0;
1868
1951
  this._loadedCount = 0;
1869
1952
  /**
1870
- * Return the full accuracy geometry to replace the feature from GetFEatureInfo
1953
+ * Return the full accuracy geometries to replace the features from GetFeatureInfo
1871
1954
  * @param featuresId
1872
1955
  * @returns
1873
1956
  */
@@ -1878,10 +1961,10 @@
1878
1961
  INFO_FORMAT: 'application/json',
1879
1962
  REQUEST: 'GetFeature',
1880
1963
  TYPENAME: this.get('name'),
1881
- MAXFEATURES: '1',
1964
+ MAXFEATURES: String(featuresId.length),
1882
1965
  OUTPUTFORMAT: 'application/json',
1883
1966
  SRSNAME: getMap().getView().getProjection().getCode(),
1884
- FEATUREID: String(featuresId)
1967
+ FEATUREID: featuresId.join(',')
1885
1968
  });
1886
1969
  const url = this.getSource().getUrls()[0] + '?' + queryParams.toString();
1887
1970
  try {
@@ -1894,7 +1977,11 @@
1894
1977
  throw new Error(`${I18N.errors.getFeatures} ${response.status}`);
1895
1978
  }
1896
1979
  const data = await response.json();
1897
- return this._parseFeaturesFromResponse(data);
1980
+ const fullById = {};
1981
+ this._parseFeaturesFromResponse(data).forEach((feature) => {
1982
+ fullById[String(feature.getId())] = feature;
1983
+ });
1984
+ return featuresId.map((id) => fullById[String(id)]).filter(Boolean);
1898
1985
  }
1899
1986
  catch (err) {
1900
1987
  console.error(err);
@@ -1904,6 +1991,9 @@
1904
1991
  if (options.beforeTransactFeature) {
1905
1992
  this.beforeTransactFeature = options.beforeTransactFeature;
1906
1993
  }
1994
+ if (options.beforeShowFieldsModal) {
1995
+ this.beforeShowFieldsModal = options.beforeShowFieldsModal;
1996
+ }
1907
1997
  this._formatGeoJSON = new GeoJSON();
1908
1998
  const geoserver = options.geoserver;
1909
1999
  const source = new WmsSource({
@@ -1959,6 +2049,112 @@
1959
2049
  FEATURE_COUNT: 1,
1960
2050
  EXCEPTIONS: 'application/json'
1961
2051
  });
2052
+ return this._requestFeatures(url);
2053
+ }
2054
+ /**
2055
+ * Request the WFS features that intersect the given geometry, selecting
2056
+ * them with a WFS GetFeature + CQL INTERSECTS filter. Used to select
2057
+ * features from a WMS layer with a box or a freehand lasso.
2058
+ *
2059
+ * @param geometry Geometry in the map view projection
2060
+ * @returns
2061
+ * @private
2062
+ */
2063
+ async _getFeaturesInGeometry(geometry) {
2064
+ var _a, _b, _c;
2065
+ // Make sure the DescribeFeatureType is loaded to get the geometry
2066
+ // field, waiting for it if the layer is still initializing
2067
+ if (!this.getDescribeFeatureType()) {
2068
+ await this.getAndUpdateDescribeFeatureType();
2069
+ }
2070
+ const geomField = (_b = (_a = this.getDescribeFeatureType()) === null || _a === void 0 ? void 0 : _a._parsed) === null || _b === void 0 ? void 0 : _b.geomField;
2071
+ if (!geomField) {
2072
+ showError(`${I18N.errors.layer} "${this.get('name')}"`);
2073
+ return [];
2074
+ }
2075
+ const viewProj = getMap().getView().getProjection().getCode();
2076
+ const nativeSrs = this._getNativeSrs();
2077
+ // Work on a copy transformed to the layer native projection so the
2078
+ // server evaluates the INTERSECTS filter against the proper SRID
2079
+ const polygon = geometry.clone();
2080
+ if (nativeSrs !== viewProj) {
2081
+ try {
2082
+ polygon.transform(viewProj, nativeSrs);
2083
+ }
2084
+ catch (err) {
2085
+ console.error(err);
2086
+ }
2087
+ }
2088
+ const ring = polygon.getCoordinates()[0] || [];
2089
+ const ringWkt = ring.map(([x, y]) => `${x} ${y}`).join(', ');
2090
+ // Force the filter geometry CRS with the EWKT SRID prefix so the
2091
+ // server evaluates the INTERSECTS against the layer projection
2092
+ // instead of its native storage SRS
2093
+ const sridMatch = (_c = nativeSrs.match(/(?:EPSG\s*::?\s*|EPSG\/0\/)(\d+)/i)) !== null && _c !== void 0 ? _c : nativeSrs.match(/(\d+)$/);
2094
+ const cql = sridMatch
2095
+ ? `INTERSECTS(${geomField}, SRID=${sridMatch[1]};POLYGON((${ringWkt})))`
2096
+ : `INTERSECTS(${geomField}, POLYGON((${ringWkt})))`;
2097
+ const queryParams = new URLSearchParams({
2098
+ SERVICE: 'wfs',
2099
+ VERSION: '2.0.0',
2100
+ REQUEST: 'GetFeature',
2101
+ TYPENAME: this.get('name'),
2102
+ OUTPUTFORMAT: 'application/json',
2103
+ SRSNAME: viewProj,
2104
+ CQL_FILTER: cql
2105
+ });
2106
+ const url = this.getSource().getUrls()[0] + '?' + queryParams.toString();
2107
+ // The server returns the features in the map view projection (same as
2108
+ // the full resolution request), so no further transformation is needed
2109
+ return this._requestFeatures(url);
2110
+ }
2111
+ /**
2112
+ * Resolve the layer native SRS so spatial filters are evaluated in the
2113
+ * correct projection. It reads the DefaultCRS from the WFS capabilities,
2114
+ * falling back to the geoserver advanced projection option and finally to
2115
+ * the map view projection.
2116
+ *
2117
+ * @returns
2118
+ * @private
2119
+ */
2120
+ _getNativeSrs() {
2121
+ var _a;
2122
+ const geoserver = this.getGeoserver();
2123
+ const viewProj = getMap().getView().getProjection().getCode();
2124
+ const layerName = String(this.get('name') || '');
2125
+ const layerLocalName = layerName.split(':').pop();
2126
+ const featureTypeList = (_a = geoserver.getParsedCapabilities()) === null || _a === void 0 ? void 0 : _a.FeatureTypeList;
2127
+ const featureType = Array.isArray(featureTypeList)
2128
+ ? featureTypeList.find((ft) => {
2129
+ const featureTypeName = String((ft === null || ft === void 0 ? void 0 : ft.Name) || '');
2130
+ return (featureTypeName === layerName ||
2131
+ featureTypeName === layerLocalName ||
2132
+ featureTypeName.split(':').pop() === layerLocalName);
2133
+ })
2134
+ : undefined;
2135
+ const defaultCrs = (featureType === null || featureType === void 0 ? void 0 : featureType.DefaultCRS)
2136
+ ? String(featureType.DefaultCRS)
2137
+ : '';
2138
+ const epsgMatch = defaultCrs.match(/(?:EPSG\s*::?\s*|EPSG\/0\/)(\d+)/i);
2139
+ if (epsgMatch) {
2140
+ return `EPSG:${epsgMatch[1]}`;
2141
+ }
2142
+ const advanced = geoserver.getAdvanced().projection;
2143
+ if (advanced) {
2144
+ return String(advanced);
2145
+ }
2146
+ return viewProj;
2147
+ }
2148
+ /**
2149
+ * Fetch and parse the features from a request url, replacing the
2150
+ * low resolution geometries (GetFeatureInfo) with the full resolution
2151
+ * ones requested by FEATUREID when possible.
2152
+ *
2153
+ * @param url
2154
+ * @returns
2155
+ * @private
2156
+ */
2157
+ async _requestFeatures(url) {
1962
2158
  const geoserver = this.getGeoserver();
1963
2159
  try {
1964
2160
  const response = await fetch(url, {
@@ -1971,6 +2167,9 @@
1971
2167
  const data = await response.json();
1972
2168
  let features = this._parseFeaturesFromResponse(data);
1973
2169
  const featuresId = features.map((f) => f.getId());
2170
+ if (!featuresId.length) {
2171
+ return [];
2172
+ }
1974
2173
  const fullResList = await this._getFullResGeometryById(featuresId);
1975
2174
  if (fullResList) {
1976
2175
  features = fullResList;
@@ -2020,50 +2219,95 @@
2020
2219
  }
2021
2220
  }
2022
2221
 
2023
- var img$5 = "data:image/svg+xml,%3csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='512' height='512' viewBox='0 0 512 512'%3e%3cpath d='M240 352h-240v128h480v-128h-240zM448 416h-64v-32h64v32zM112 160l128-128 128 128h-80v160h-96v-160z'%3e%3c/path%3e%3c/svg%3e";
2024
-
2025
- var img$4 = "data:image/svg+xml,%3csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='768' height='768' viewBox='0 0 768 768'%3e %3cpath d='M663 225l-58.5 58.5-120-120 58.5-58.5q9-9 22.5-9t22.5 9l75 75q9 9 9 22.5t-9 22.5zM96 552l354-354 120 120-354 354h-120v-120z'%3e%3c/path%3e%3c/svg%3e";
2026
-
2027
- var img$3 = "data:image/svg+xml,%3csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='768' height='768' viewBox='0 0 768 768'%3e%3cpath d='M384 288q39 0 67.5 28.5t28.5 67.5-28.5 67.5-67.5 28.5-67.5-28.5-28.5-67.5 28.5-67.5 67.5-28.5zM384 544.5q66 0 113.25-47.25t47.25-113.25-47.25-113.25-113.25-47.25-113.25 47.25-47.25 113.25 47.25 113.25 113.25 47.25zM384 144q118.5 0 214.5 66t138 174q-42 108-138 174t-214.5 66-214.5-66-138-174q42-108 138-174t214.5-66z'%3e%3c/path%3e%3c/svg%3e";
2028
-
2029
- var img$2 = "data:image/svg+xml,%3csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='768' height='768' viewBox='0 0 768 768'%3e%3cpath d='M379.5 288h4.5q39 0 67.5 28.5t28.5 67.5v6zM241.5 313.5q-18 36-18 70.5 0 66 47.25 113.25t113.25 47.25q34.5 0 70.5-18l-49.5-49.5q-12 3-21 3-39 0-67.5-28.5t-28.5-67.5q0-9 3-21zM64.5 136.5l40.5-40.5 567 567-40.5 40.5q-7.5-7.5-47.25-46.5t-60.75-60q-64.5 27-139.5 27-118.5 0-214.5-66t-138-174q16.5-39 51.75-86.25t68.25-72.75q-18-18-50.25-51t-36.75-37.5zM384 223.5q-30 0-58.5 12l-69-69q58.5-22.5 127.5-22.5 118.5 0 213.75 66t137.25 174q-36 88.5-109.5 151.5l-93-93q12-28.5 12-58.5 0-66-47.25-113.25t-113.25-47.25z'%3e%3c/path%3e%3c/svg%3e";
2030
-
2031
- function createElement(tagName, attrs = {}, ...children) {
2032
- if (typeof tagName === 'function')
2033
- return tagName(attrs, children);
2034
- const elem = tagName === null
2035
- ? new DocumentFragment()
2222
+ function createElement(tagName, props) {
2223
+ props = props || {};
2224
+ let children = props.children || [];
2225
+ children = Array.isArray(children) ? children : [children];
2226
+ delete props.children;
2227
+ if (typeof tagName === 'function') {
2228
+ return tagName(props, children);
2229
+ }
2230
+ const elem = tagName === 'fragment'
2231
+ ? document.createDocumentFragment()
2036
2232
  : document.createElement(tagName);
2037
- Object.entries(attrs || {}).forEach(([name, value]) => {
2038
- if (typeof value !== 'undefined' &&
2039
- value !== null &&
2040
- value !== undefined) {
2041
- if (name.startsWith('on') && name.toLowerCase() in window)
2042
- elem.addEventListener(name.toLowerCase().substr(2), value);
2043
- else {
2044
- if (name === 'className')
2233
+ if (elem instanceof HTMLElement) {
2234
+ Object.entries(props).forEach(([name, value]) => {
2235
+ if (value != null) {
2236
+ // listener
2237
+ if (name.startsWith('on') && typeof value === 'function') {
2238
+ elem.addEventListener(name
2239
+ .slice(2)
2240
+ .toLowerCase(), value);
2241
+ }
2242
+ else if (name === 'className') {
2045
2243
  elem.setAttribute('class', value.toString());
2046
- else if (name === 'htmlFor')
2244
+ }
2245
+ else if (name === 'htmlFor') {
2047
2246
  elem.setAttribute('for', value.toString());
2048
- else
2247
+ }
2248
+ else {
2049
2249
  elem.setAttribute(name, value.toString());
2250
+ }
2050
2251
  }
2051
- }
2052
- });
2252
+ });
2253
+ }
2053
2254
  for (const child of children) {
2054
- if (!child)
2255
+ if (!child) {
2055
2256
  continue;
2056
- if (Array.isArray(child))
2257
+ }
2258
+ if (Array.isArray(child)) {
2057
2259
  elem.append(...child);
2260
+ }
2261
+ else if (child instanceof Node) {
2262
+ elem.appendChild(child);
2263
+ }
2058
2264
  else {
2059
- if (child.nodeType === undefined)
2060
- elem.innerHTML += child;
2061
- else
2062
- elem.appendChild(child);
2265
+ elem.innerHTML += child;
2063
2266
  }
2064
2267
  }
2065
2268
  return elem;
2066
2269
  }
2270
+ function jsx(type, props) {
2271
+ return createElement(type, props);
2272
+ }
2273
+ function jsxs(type, props) {
2274
+ return createElement(type, props);
2275
+ }
2276
+ function Fragment(type, children) {
2277
+ return createElement('fragment', { children });
2278
+ }
2279
+
2280
+ function uploadSvg() {
2281
+ return (new DOMParser().parseFromString("\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width=\"512\" height=\"512\" viewBox=\"0 0 512 512\">\r\n<path d=\"M240 352h-240v128h480v-128h-240zM448 416h-64v-32h64v32zM112 160l128-128 128 128h-80v160h-96v-160z\"></path>\r\n</svg>\r\n", 'image/svg+xml')).firstChild;
2282
+ }
2283
+
2284
+ function drawSvg() {
2285
+ return (new DOMParser().parseFromString("<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width=\"768\" height=\"768\" viewBox=\"0 0 768 768\">\r\n <path d=\"M663 225l-58.5 58.5-120-120 58.5-58.5q9-9 22.5-9t22.5 9l75 75q9 9 9 22.5t-9 22.5zM96 552l354-354 120 120-354 354h-120v-120z\"></path>\r\n</svg>", 'image/svg+xml')).firstChild;
2286
+ }
2287
+
2288
+ function selectSvg() {
2289
+ return (new DOMParser().parseFromString("<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width=\"289\" height=\"448\" viewBox=\"0 0 289 448\">\r\n <path d=\"M283.25 260.75c4.75 4.5 6 11.5 3.5 17.25-2.5 6-8.25 10-14.75 10h-95.5l50.25 119c3.5 8.25-0.5 17.5-8.5 21l-44.25 18.75c-8.25 3.5-17.5-0.5-21-8.5l-47.75-113-78 78c-3 3-7 4.75-11.25 4.75-2 0-4.25-0.5-6-1.25-6-2.5-10-8.25-10-14.75v-376c0-6.5 4-12.25 10-14.75 1.75-0.75 4-1.25 6-1.25 4.25 0 8.25 1.5 11.25 4.75z\"></path>\r\n</svg>\r\n", 'image/svg+xml')).firstChild;
2290
+ }
2291
+
2292
+ function selectBoxSvg() {
2293
+ return (new DOMParser().parseFromString("<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\r\n <rect x=\"3.5\" y=\"3.5\" width=\"17\" height=\"17\" rx=\"1.5\" fill=\"none\" stroke=\"#000\" stroke-width=\"2\" stroke-dasharray=\"4 2\"></rect>\r\n</svg>", 'image/svg+xml')).firstChild;
2294
+ }
2295
+
2296
+ function selectFreehandSvg() {
2297
+ return (new DOMParser().parseFromString("<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\r\n <path d=\"M4.59 6.89c.7-.71 1.4-1.35 1.71-1.22.5.2 0 1.03-.3 1.52-.25.42-2.86 3.89-2.86 6.31 0 1.28.48 2.34 1.34 2.98.75.56 1.74.73 2.64.46 1.07-.31 1.95-1.4 3.06-2.77 1.21-1.49 2.83-3.44 4.08-3.44 1.63 0 1.65 1.01 1.76 1.79-3.78.64-5.38 3.67-5.38 5.37 0 1.7 1.44 3.09 3.21 3.09 1.63 0 4.29-1.33 4.69-6.1H21v-2.5h-2.47c-.15-1.65-1.09-4.2-4.03-4.2-2.25 0-4.18 1.91-4.94 2.84-.58.73-2.06 2.48-2.29 2.72-.25.3-.68.84-1.11.84-.45 0-.72-.83-.36-1.92.35-1.09 1.4-2.86 1.85-3.52.78-1.14 1.3-1.92 1.3-3.28C8.95 3.69 7.31 3 6.44 3 5.12 3 3.97 4 3.72 4.25c-.36.36-.66.66-.88.93l1.75 1.71zm9.29 11.66c-.31 0-.74-.26-.74-.72 0-.6.73-2.2 2.87-2.76-.3 2.69-1.43 3.48-2.13 3.48z\"></path>\r\n</svg>", 'image/svg+xml')).firstChild;
2298
+ }
2299
+
2300
+ function infoSvg() {
2301
+ return (new DOMParser().parseFromString("<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\r\n <path d=\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z\"></path>\r\n</svg>", 'image/svg+xml')).firstChild;
2302
+ }
2303
+
2304
+ function visibilityOnSvg() {
2305
+ return (new DOMParser().parseFromString("<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width=\"768\" height=\"768\" viewBox=\"0 0 768 768\">\n<path d=\"M384 288q39 0 67.5 28.5t28.5 67.5-28.5 67.5-67.5 28.5-67.5-28.5-28.5-67.5 28.5-67.5 67.5-28.5zM384 544.5q66 0 113.25-47.25t47.25-113.25-47.25-113.25-113.25-47.25-113.25 47.25-47.25 113.25 47.25 113.25 113.25 47.25zM384 144q118.5 0 214.5 66t138 174q-42 108-138 174t-214.5 66-214.5-66-138-174q42-108 138-174t214.5-66z\"></path>\n</svg>\n", 'image/svg+xml')).firstChild;
2306
+ }
2307
+
2308
+ function visibilityOffSvg() {
2309
+ return (new DOMParser().parseFromString("<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width=\"768\" height=\"768\" viewBox=\"0 0 768 768\">\n<path d=\"M379.5 288h4.5q39 0 67.5 28.5t28.5 67.5v6zM241.5 313.5q-18 36-18 70.5 0 66 47.25 113.25t113.25 47.25q34.5 0 70.5-18l-49.5-49.5q-12 3-21 3-39 0-67.5-28.5t-28.5-67.5q0-9 3-21zM64.5 136.5l40.5-40.5 567 567-40.5 40.5q-7.5-7.5-47.25-46.5t-60.75-60q-64.5 27-139.5 27-118.5 0-214.5-66t-138-174q16.5-39 51.75-86.25t68.25-72.75q-18-18-50.25-51t-36.75-37.5zM384 223.5q-30 0-58.5 12l-69-69q58.5-22.5 127.5-22.5 118.5 0 213.75 66t137.25 174q-36 88.5-109.5 151.5l-93-93q12-28.5 12-58.5 0-66-47.25-113.25t-113.25-47.25z\"></path>\n</svg>\n", 'image/svg+xml')).firstChild;
2310
+ }
2067
2311
 
2068
2312
  /**
2069
2313
  * Removes in the DOM the class of the tools
@@ -2080,6 +2324,19 @@
2080
2324
  if (btn) {
2081
2325
  btn.classList.add('wfst--active');
2082
2326
  }
2327
+ deactivateQueryButton();
2328
+ };
2329
+ const activateQueryButton = () => {
2330
+ const btn = document.querySelector('.ol-wfst--tools-control-btn-query');
2331
+ if (btn) {
2332
+ btn.classList.add('wfst--active');
2333
+ }
2334
+ };
2335
+ const deactivateQueryButton = () => {
2336
+ const btn = document.querySelector('.ol-wfst--tools-control-btn-query');
2337
+ if (btn) {
2338
+ btn.classList.remove('wfst--active');
2339
+ }
2083
2340
  };
2084
2341
  const activateDrawButton = () => {
2085
2342
  const btn = document.querySelector('.ol-wfst--tools-control-btn-draw');
@@ -2087,6 +2344,29 @@
2087
2344
  btn.classList.add('wfst--active');
2088
2345
  }
2089
2346
  };
2347
+ const activateSelectModeButton = (mode) => {
2348
+ const activeBtn = document.querySelector('.ol-wfst--tools-control-btn-select.wfst--active, .ol-wfst--tools-control-btn-select-box.wfst--active, .ol-wfst--tools-control-btn-select-freehand.wfst--active');
2349
+ if (activeBtn) {
2350
+ activeBtn.classList.remove('wfst--active');
2351
+ }
2352
+ deactivateQueryButton();
2353
+ const btnMap = {
2354
+ [SelectionMode.Single]: '.ol-wfst--tools-control-btn-select',
2355
+ [SelectionMode.Box]: '.ol-wfst--tools-control-btn-select-box',
2356
+ [SelectionMode.Freehand]: '.ol-wfst--tools-control-btn-select-freehand'
2357
+ };
2358
+ const btn = document.querySelector(btnMap[mode]);
2359
+ if (btn) {
2360
+ btn.classList.add('wfst--active');
2361
+ }
2362
+ };
2363
+ const deactivateSelectModeButton = () => {
2364
+ const activeBtn = document.querySelector('.ol-wfst--tools-control-btn-select.wfst--active, .ol-wfst--tools-control-btn-select-box.wfst--active, .ol-wfst--tools-control-btn-select-freehand.wfst--active');
2365
+ if (activeBtn) {
2366
+ activeBtn.classList.remove('wfst--active');
2367
+ }
2368
+ deactivateQueryButton();
2369
+ };
2090
2370
  class LayersControl extends Observable {
2091
2371
  constructor(uploads, uploadFormats) {
2092
2372
  super();
@@ -2099,23 +2379,15 @@
2099
2379
  * @public
2100
2380
  */
2101
2381
  addLayerEl(layer) {
2102
- const container = document.querySelector('.wfst--tools-control--select-layers');
2382
+ const container = document.querySelector('.wfst--tools-control--select-layers-list');
2103
2383
  const layerName = layer.get(BaseLayerProperty.NAME);
2104
2384
  const checked = layer === getActiveLayerToInsertEls() ? { checked: true } : {};
2105
- const input = (createElement("input", Object.assign({ value: layerName, id: `wfst--${layerName}`, type: "radio", className: "ol-wfst--tools-control-input", name: "wfst--select-layer" }, checked, { onChange: (evt) => this._layerChangeHandler(evt, layer) })));
2106
- const layerDom = (createElement("div", { className: `wfst--layer-control
2385
+ const input = (jsx("input", Object.assign({ value: layerName, id: `wfst--${layerName}`, type: "radio", className: "ol-wfst--tools-control-input", name: "wfst--select-layer" }, checked, { onChange: (evt) => this._layerChangeHandler(evt, layer) })));
2386
+ const layerDom = (jsx("div", { className: `wfst--layer-control
2107
2387
  ${layer.getVisible() ? 'ol-wfst--visible-on' : ''}
2108
2388
  ${layer === getActiveLayerToInsertEls()
2109
2389
  ? 'ol-wfst--selected-on'
2110
- : ''}`, "data-layer": layerName },
2111
- createElement("div", { className: "ol-wfst--tools-control-visible" },
2112
- createElement("span", { className: "ol-wfst--tools-control-visible-btn ol-wfst--visible-btn-on", title: I18N.labels.toggleVisibility, onClick: (evt) => this._visibilityClickHandler(evt) },
2113
- createElement("img", { src: img$3 })),
2114
- createElement("span", { className: "ol-wfst--tools-control-visible-btn ol-wfst--visible-btn-off", title: I18N.labels.toggleVisibility, onClick: (evt) => this._visibilityClickHandler(evt) },
2115
- createElement("img", { src: img$2 }))),
2116
- createElement("label", { htmlFor: `wfst--${layerName}` },
2117
- input,
2118
- createElement("span", { title: layer.getDescribeFeatureType()._parsed.geomType }, layer.get(BaseLayerProperty.LABEL)))));
2390
+ : ''}`, "data-layer": layerName, children: jsxs("label", { htmlFor: `wfst--${layerName}`, children: [jsxs("div", { className: "ol-wfst--tools-control-visible", children: [jsx("span", { className: "ol-wfst--tools-control-visible-btn ol-wfst--visible-btn-on", title: I18N.labels.toggleVisibility, onClick: (evt) => this._visibilityClickHandler(evt), children: visibilityOnSvg() }), jsx("span", { className: "ol-wfst--tools-control-visible-btn ol-wfst--visible-btn-off", title: I18N.labels.toggleVisibility, onClick: (evt) => this._visibilityClickHandler(evt), children: visibilityOffSvg() })] }), input, jsx("span", { title: `${layer instanceof WfsLayer ? 'WFS' : 'WMS'} - ${layer.getDescribeFeatureType()._parsed.geomType}`, children: layer.get(BaseLayerProperty.LABEL) })] }) }));
2119
2391
  container.appendChild(layerDom);
2120
2392
  if (layer === getActiveLayerToInsertEls()) {
2121
2393
  input.dispatchEvent(new Event('change'));
@@ -2211,34 +2483,32 @@
2211
2483
  this.dispatchEvent('changeLayer');
2212
2484
  }
2213
2485
  render() {
2214
- return (createElement(null, null,
2215
- createElement("div", { className: "wfst--tools-control--head" },
2216
- this._uploads && (createElement("div", null,
2217
- createElement("input", { id: "ol-wfst--upload", type: "file", accept: this._uploadFormats, onChange: (evt) => this._uploads.process(evt) }),
2218
- createElement("label", { className: "ol-wfst--tools-control-btn ol-wfst--tools-control-btn-upload", htmlFor: "ol-wfst--upload", title: I18N.labels.uploadToLayer },
2219
- createElement("img", { src: img$5 })))),
2220
- createElement("div", { className: "ol-wfst--tools-control-draw-cnt" },
2221
- createElement("button", { className: "ol-wfst--tools-control-btn ol-wfst--tools-control-btn-draw", type: "button", title: I18N.labels.addElement, onClick: () => {
2222
- this.dispatchEvent('drawMode');
2223
- } },
2224
- createElement("img", { src: img$4 })),
2225
- createElement("select", { title: I18N.labels.selectDrawType, className: "wfst--tools-control--select-draw", onChange: (evt) => {
2226
- const selectedValue = evt.target.value;
2227
- this._changeStateSelect(getActiveLayerToInsertEls(), selectedValue);
2228
- this.dispatchEvent('changeGeom');
2229
- } }, [
2230
- GeometryType.Point,
2231
- GeometryType.MultiPoint,
2232
- GeometryType.LineString,
2233
- GeometryType.MultiLineString,
2234
- GeometryType.Polygon,
2235
- GeometryType.MultiPolygon,
2236
- GeometryType.Circle
2237
- ].map((type) => {
2238
- // Show all options, but enable only the accepted ones
2239
- return createElement("option", { value: type }, type);
2240
- })))),
2241
- createElement("div", { className: "wfst--tools-control--select-layers" })));
2486
+ return (jsxs(Fragment, { children: [jsxs("div", { className: "wfst--tools-control--head", children: [jsxs("div", { className: "ol-wfst--tools-control-select-cnt", children: [jsx("button", { className: "ol-wfst--tools-control-btn ol-wfst--tools-control-btn-select wfst--active", type: "button", title: I18N.labels.select, onClick: () => {
2487
+ this.dispatchEvent('selectModeSingle');
2488
+ }, children: selectSvg() }), jsx("button", { className: "ol-wfst--tools-control-btn ol-wfst--tools-control-btn-select-box", type: "button", title: I18N.labels.selectBox, onClick: () => {
2489
+ this.dispatchEvent('selectModeBox');
2490
+ }, children: selectBoxSvg() }), jsx("button", { className: "ol-wfst--tools-control-btn ol-wfst--tools-control-btn-select-freehand", type: "button", title: I18N.labels.selectFreehand, onClick: () => {
2491
+ this.dispatchEvent('selectModeFreehand');
2492
+ }, children: selectFreehandSvg() })] }), jsx("div", { className: "ol-wfst--tools-control-query-cnt", children: jsx("button", { className: "ol-wfst--tools-control-btn ol-wfst--tools-control-btn-query", type: "button", title: I18N.labels.query, onClick: () => {
2493
+ this.dispatchEvent('queryMode');
2494
+ }, children: infoSvg() }) }), jsxs("div", { className: "ol-wfst--tools-control-draw-cnt", children: [jsx("button", { className: "ol-wfst--tools-control-btn ol-wfst--tools-control-btn-draw", type: "button", title: I18N.labels.addElement, onClick: () => {
2495
+ this.dispatchEvent('drawMode');
2496
+ }, children: drawSvg() }), jsx("select", { title: I18N.labels.selectDrawType, className: "wfst--tools-control--select-draw", onChange: (evt) => {
2497
+ const selectedValue = evt.target.value;
2498
+ this._changeStateSelect(getActiveLayerToInsertEls(), selectedValue);
2499
+ this.dispatchEvent('changeGeom');
2500
+ }, children: [
2501
+ GeometryType.Point,
2502
+ GeometryType.MultiPoint,
2503
+ GeometryType.LineString,
2504
+ GeometryType.MultiLineString,
2505
+ GeometryType.Polygon,
2506
+ GeometryType.MultiPolygon,
2507
+ GeometryType.Circle
2508
+ ].map((type) => {
2509
+ // Show all options, but enable only the accepted ones
2510
+ return jsx("option", { value: type, children: type });
2511
+ }) })] }), this._uploads && (jsxs("div", { children: [jsx("input", { id: "ol-wfst--upload", type: "file", accept: this._uploadFormats, onChange: (evt) => this._uploads.process(evt) }), jsx("label", { className: "ol-wfst--tools-control-btn ol-wfst--tools-control-btn-upload", htmlFor: "ol-wfst--upload", title: I18N.labels.uploadToLayer, children: uploadSvg() })] }))] }), jsxs("div", { className: "wfst--tools-control--select-layers", children: [jsx("div", { className: "wfst--tools-control--select-layers-title", children: I18N.labels.geoserverLayers }), jsx("div", { className: "wfst--tools-control--select-layers-list" })] })] }));
2242
2512
  }
2243
2513
  }
2244
2514
 
@@ -2477,6 +2747,7 @@
2477
2747
  evtType: 'singleclick',
2478
2748
  active: true,
2479
2749
  showControl: true,
2750
+ fullscreen: true,
2480
2751
  language: DEFAULT_LANGUAGE,
2481
2752
  uploadFormats: '.geojson,.json,.kml',
2482
2753
  processUpload: null,
@@ -2494,91 +2765,145 @@
2494
2765
  };
2495
2766
 
2496
2767
  class EditControlChangesEl extends Control {
2497
- constructor(feature) {
2768
+ constructor(features) {
2498
2769
  super({
2499
- element: (createElement("div", { className: "ol-wfst--changes-control" },
2500
- createElement("div", { className: "ol-wfst--changes-control-el" },
2501
- createElement("div", { className: "ol-wfst--changes-control-id" },
2502
- createElement("b", null, I18N.labels.editMode),
2503
- " -",
2504
- ' ',
2505
- createElement("i", null, String(feature.getId()))),
2506
- createElement("button", { type: "button", className: "btn btn-sm btn-secondary", onClick: () => {
2507
- this.dispatchEvent(new VectorSource.VectorSourceEvent('cancel', feature));
2508
- } }, I18N.labels.cancel),
2509
- createElement("button", { type: "button", className: "btn btn-sm btn-primary", onClick: () => {
2510
- this.dispatchEvent(new VectorSource.VectorSourceEvent('apply', feature));
2511
- } }, I18N.labels.apply),
2512
- createElement("button", { type: "button", className: "btn btn-sm btn-danger-outline", onClick: () => {
2513
- this.dispatchEvent(new VectorSource.VectorSourceEvent('delete', feature));
2514
- } }, I18N.labels.delete))))
2770
+ element: (jsx("div", { className: "ol-wfst--changes-control", children: jsxs("div", { className: "ol-wfst--changes-control-el", children: [jsxs("div", { className: "ol-wfst--changes-control-id", children: [jsx("b", { children: I18N.labels.editMode }), " -", ' ', jsx("i", { children: features.length > 1
2771
+ ? I18N_('editElements', features.length)
2772
+ : String(features[0].getId()) })] }), jsx("button", { type: "button", className: "btn btn-sm btn-secondary", onClick: () => {
2773
+ this._dispatch('cancel', features);
2774
+ }, children: I18N.labels.cancel }), jsx("button", { type: "button", className: "btn btn-sm btn-primary", onClick: () => {
2775
+ this._dispatch('apply', features);
2776
+ }, children: I18N.labels.apply }), jsx("button", { type: "button", className: "btn btn-sm btn-danger-outline", onClick: () => {
2777
+ this._dispatch('delete', features);
2778
+ }, children: I18N.labels.delete })] }) }))
2515
2779
  });
2516
2780
  }
2781
+ _dispatch(type, features) {
2782
+ const evt = new VectorSource.VectorSourceEvent(type, features[0]);
2783
+ evt.features = features;
2784
+ this.dispatchEvent(evt);
2785
+ }
2517
2786
  }
2518
2787
 
2519
2788
  // Ol
2789
+ /**
2790
+ * Modern vertex handle mimicking the round "extend line" buttons shown at
2791
+ * line endpoints: a soft halo for depth and a white disc with a subtle
2792
+ * border. When `highlighted` is `true` (e.g. hovering a vertex with the
2793
+ * Modify interaction) the handle gets bigger with a stronger halo and a
2794
+ * darker border.
2795
+ *
2796
+ * @param highlighted Whether to render the bigger/highlighted state
2797
+ * @param geometry Style geometry function to position the handles
2798
+ * @private
2799
+ */
2800
+ const editVertexStyles = (highlighted = false, geometry) => {
2801
+ const halo = highlighted ? 10 : 8;
2802
+ const haloColor = highlighted
2803
+ ? 'rgba(0, 0, 0, 0.40)'
2804
+ : 'rgba(0, 0, 0, 0.28)';
2805
+ const radius = highlighted ? 7 : 6;
2806
+ const border = highlighted ? '#4b5563' : '#b3b3b3';
2807
+ const borderWidth = highlighted ? 2 : 1.5;
2808
+ return [
2809
+ new style_js.Style(Object.assign({ image: new style_js.Circle({
2810
+ radius: halo,
2811
+ fill: new style_js.Fill({
2812
+ color: haloColor
2813
+ })
2814
+ }) }, (geometry ? { geometry } : {}))),
2815
+ new style_js.Style(Object.assign({ image: new style_js.Circle({
2816
+ radius: radius,
2817
+ fill: new style_js.Fill({
2818
+ color: '#ffffff'
2819
+ }),
2820
+ stroke: new style_js.Stroke({
2821
+ width: borderWidth,
2822
+ color: border
2823
+ })
2824
+ }) }, (geometry ? { geometry } : {})))
2825
+ ];
2826
+ };
2827
+ /**
2828
+ * Extract a `MultiPoint` with every vertex of the given feature geometry
2829
+ * (polygons and multi-line strings are flattened). Returns `undefined` when
2830
+ * the geometry has no coordinates.
2831
+ *
2832
+ * @param feature
2833
+ * @private
2834
+ */
2835
+ const getFeatureVertices = (feature) => {
2836
+ let geometry = feature.getGeometry();
2837
+ if (geometry instanceof geom_js.GeometryCollection) {
2838
+ geometry = geometry.getGeometries()[0];
2839
+ }
2840
+ const coordinates = geometry.getCoordinates();
2841
+ let flatCoordinates = null;
2842
+ if (geometry instanceof geom_js.Polygon || geometry instanceof geom_js.MultiLineString) {
2843
+ flatCoordinates = coordinates.flat(1);
2844
+ }
2845
+ else if (geometry instanceof geom_js.MultiPolygon) {
2846
+ flatCoordinates = coordinates.flat(2);
2847
+ }
2848
+ else {
2849
+ flatCoordinates = coordinates;
2850
+ }
2851
+ if (!flatCoordinates || !flatCoordinates.length) {
2852
+ return;
2853
+ }
2854
+ return new geom_js.MultiPoint(flatCoordinates);
2855
+ };
2520
2856
  /**
2521
2857
  * Master style that handles two modes on the Edit Layer:
2522
2858
  * - one is the basic, showing only the vertices
2523
2859
  * - and the other when modify is active, showing bigger vertices
2524
2860
  *
2861
+ * When `hovered` is `true`, a white/red halo is prepended to highlight the
2862
+ * feature, indicating it can be selected on click.
2863
+ *
2525
2864
  * @param feature
2865
+ * @param hovered
2526
2866
  * @private
2527
2867
  */
2528
- function styleFunction(feature) {
2529
- const getVertexs = (feature) => {
2530
- let geometry = feature.getGeometry();
2531
- if (geometry instanceof geom_js.GeometryCollection) {
2532
- geometry = geometry.getGeometries()[0];
2533
- }
2534
- const coordinates = geometry.getCoordinates();
2535
- let flatCoordinates = null;
2536
- if (geometry instanceof geom_js.Polygon ||
2537
- geometry instanceof geom_js.MultiLineString) {
2538
- flatCoordinates = coordinates.flat(1);
2539
- }
2540
- else if (geometry instanceof geom_js.MultiPolygon) {
2541
- flatCoordinates = coordinates.flat(2);
2542
- }
2543
- else {
2544
- flatCoordinates = coordinates;
2545
- }
2546
- if (!flatCoordinates || !flatCoordinates.length) {
2547
- return;
2548
- }
2549
- return new geom_js.MultiPoint(flatCoordinates);
2550
- };
2868
+ function styleFunction(feature, hovered = false) {
2551
2869
  let geometry = feature.getGeometry();
2552
2870
  let type = geometry.getType();
2553
2871
  if (geometry instanceof geom_js.GeometryCollection) {
2554
2872
  geometry = geometry.getGeometries()[0];
2555
2873
  type = geometry.getType();
2556
2874
  }
2875
+ const haloStyle = () => {
2876
+ return [
2877
+ new style_js.Style({
2878
+ image: new style_js.Circle({
2879
+ radius: 9,
2880
+ fill: new style_js.Fill({
2881
+ color: 'rgba(255, 255, 255, 0.9)'
2882
+ }),
2883
+ stroke: new style_js.Stroke({
2884
+ color: '#ff0000',
2885
+ width: 3
2886
+ })
2887
+ }),
2888
+ stroke: new style_js.Stroke({
2889
+ color: 'rgba(255, 255, 255, 0.9)',
2890
+ width: 8
2891
+ }),
2892
+ fill: new style_js.Fill({
2893
+ color: 'rgba(255, 255, 255, 0.9)'
2894
+ })
2895
+ })
2896
+ ];
2897
+ };
2557
2898
  switch (type) {
2558
2899
  case GeometryType.Point:
2559
2900
  case GeometryType.MultiPoint:
2560
2901
  if (getMode() === Modes.Edit) {
2561
- return [
2562
- new style_js.Style({
2563
- image: new style_js.Circle({
2564
- radius: 6,
2565
- fill: new style_js.Fill({
2566
- color: '#000000'
2567
- })
2568
- })
2569
- }),
2570
- new style_js.Style({
2571
- image: new style_js.Circle({
2572
- radius: 4,
2573
- fill: new style_js.Fill({
2574
- color: '#ff0000'
2575
- })
2576
- })
2577
- })
2578
- ];
2902
+ return editVertexStyles();
2579
2903
  }
2580
2904
  else {
2581
2905
  return [
2906
+ ...(hovered ? haloStyle() : []),
2582
2907
  new style_js.Style({
2583
2908
  image: new style_js.Circle({
2584
2909
  radius: 5,
@@ -2610,19 +2935,7 @@
2610
2935
  color: 'rgba(255, 0, 0, 0.7)'
2611
2936
  })
2612
2937
  }),
2613
- new style_js.Style({
2614
- image: new style_js.Circle({
2615
- radius: 4,
2616
- fill: new style_js.Fill({
2617
- color: '#ff0000'
2618
- }),
2619
- stroke: new style_js.Stroke({
2620
- width: 2,
2621
- color: 'rgba(5, 5, 5, 0.9)'
2622
- })
2623
- }),
2624
- geometry: (feature) => getVertexs(feature)
2625
- }),
2938
+ ...editVertexStyles(false, getFeatureVertices),
2626
2939
  new style_js.Style({
2627
2940
  stroke: new style_js.Stroke({
2628
2941
  color: 'rgba(255, 255, 255, 0.7)',
@@ -2633,6 +2946,7 @@
2633
2946
  }
2634
2947
  else {
2635
2948
  return [
2949
+ ...(hovered ? haloStyle() : []),
2636
2950
  new style_js.Style({
2637
2951
  image: new style_js.Circle({
2638
2952
  radius: 2,
@@ -2640,7 +2954,7 @@
2640
2954
  color: '#000000'
2641
2955
  })
2642
2956
  }),
2643
- geometry: (feature) => getVertexs(feature)
2957
+ geometry: (feature) => getFeatureVertices(feature)
2644
2958
  }),
2645
2959
  new style_js.Style({
2646
2960
  stroke: new style_js.Stroke({
@@ -2655,91 +2969,178 @@
2655
2969
  }
2656
2970
  }
2657
2971
  }
2658
-
2659
- // Ol
2660
2972
  /**
2661
- * Shows a fields form in a modal window to allow changes in the properties of the feature.
2973
+ * Style used to highlight a feature after a query, drawn on a dedicated
2974
+ * highlight layer so the source layers keep their own rendering.
2662
2975
  *
2663
2976
  * @param feature
2664
2977
  * @private
2665
2978
  */
2979
+ function queryStyleFunction(_feature) {
2980
+ return [
2981
+ new style_js.Style({
2982
+ image: new style_js.Circle({
2983
+ radius: 6,
2984
+ fill: new style_js.Fill({
2985
+ color: 'rgba(0, 170, 255, 0.4)'
2986
+ }),
2987
+ stroke: new style_js.Stroke({
2988
+ color: '#0066cc',
2989
+ width: 2
2990
+ })
2991
+ }),
2992
+ stroke: new style_js.Stroke({
2993
+ color: '#00aaff',
2994
+ width: 4
2995
+ }),
2996
+ fill: new style_js.Fill({
2997
+ color: 'rgba(0, 170, 255, 0.3)'
2998
+ })
2999
+ })
3000
+ ];
3001
+ }
3002
+
3003
+ /**
3004
+ * Shows a fields form in a modal window to allow changes in the properties of
3005
+ * one or several features. When editing multiple features, fields whose value
3006
+ * differs across the selection show the "multiple values" placeholder and only
3007
+ * the fields actually changed are applied to the whole selection.
3008
+ *
3009
+ * @param features
3010
+ * @private
3011
+ */
2666
3012
  class EditFieldsModal extends Observable {
2667
3013
  constructor(options) {
2668
3014
  super();
2669
3015
  this._options = options;
2670
- const footer = `
2671
- <button type="button" class="btn btn-sm btn-link btn-third" data-action="delete" data-dismiss="modal">
2672
- ${I18N.labels.delete}
2673
- </button>
2674
- <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">
2675
- ${I18N.labels.cancel}
2676
- </button>
2677
- <button type="button" class="btn btn-sm btn-primary" data-action="save" data-dismiss="modal">
2678
- ${I18N.labels.save}
2679
- </button>
2680
- `;
2681
- this._modal = new Modal(Object.assign(Object.assign({}, this._options), { header: true, headerClose: true, title: '', content: '<div></div>', footer: footer }));
3016
+ this._modal = new Modal(Object.assign(Object.assign({}, this._options.modal), { header: true, headerClose: true, title: '', content: '<div></div>', footer: `
3017
+ <button
3018
+ type="button"
3019
+ class="btn btn-sm btn-third"
3020
+ data-action="delete"
3021
+ data-dismiss="modal"
3022
+ >
3023
+ ${I18N.labels.delete}
3024
+ </button>
3025
+ <button
3026
+ type="button"
3027
+ class="btn btn-sm btn-secondary"
3028
+ data-dismiss="modal"
3029
+ >
3030
+ ${I18N.labels.cancel}
3031
+ </button>
3032
+ <button
3033
+ type="button"
3034
+ class="btn btn-sm btn-primary"
3035
+ data-action="save"
3036
+ data-dismiss="modal"
3037
+ >
3038
+ ${I18N.labels.save}
3039
+ </button>
3040
+ ` }));
2682
3041
  this._modal.on('dismiss', (modal, event) => {
2683
3042
  // On saving changes
2684
3043
  if (event.target.dataset.action === 'save') {
2685
- const inputs = modal.el.querySelectorAll('input');
2686
- inputs.forEach((el) => {
3044
+ const formElements = modal.el.querySelector('form')
3045
+ .elements;
3046
+ const multipleFields = new Set(Array.from(formElements)
3047
+ .filter((el) => el.dataset.multiple === 'true')
3048
+ .map((el) => el.name));
3049
+ Array.from(formElements).forEach((el) => {
2687
3050
  const value = el.value;
2688
3051
  const field = el.name;
2689
- this._feature.set(field, value, /*isSilent = */ true);
3052
+ // Do not overwrite untouched fields that had
3053
+ // multiple values in the selection
3054
+ if (multipleFields.has(field) && value === '') {
3055
+ return;
3056
+ }
3057
+ this._features.forEach((feature) => {
3058
+ if (feature.get(field) !== value) {
3059
+ feature.set(field, value, /* isSilent = */ true);
3060
+ }
3061
+ });
3062
+ });
3063
+ this._features.forEach((feature) => {
3064
+ feature.changed();
3065
+ addFeatureToEditedList(feature);
2690
3066
  });
2691
- this._feature.changed();
2692
- addFeatureToEditedList(this._feature);
2693
- this.dispatchEvent(new VectorSource.VectorSourceEvent('save', this._feature));
3067
+ this._dispatch('save', this._features);
2694
3068
  }
2695
3069
  else if (event.target.dataset.action === 'delete') {
2696
- this.dispatchEvent(new VectorSource.VectorSourceEvent('delete', this._feature));
3070
+ this._dispatch('delete', this._features);
2697
3071
  }
2698
3072
  });
2699
3073
  }
2700
- show(feature) {
2701
- this._feature = feature;
2702
- const title = `${I18N.labels.editElement} ${feature.getId()} `;
2703
- const properties = feature.getProperties();
2704
- const layerName = feature.get('_layerName_');
3074
+ show(features) {
3075
+ this._features = Array.isArray(features) ? features : [features];
3076
+ const multiple = this._features.length > 1;
3077
+ const modalTitle = multiple
3078
+ ? I18N_('editElements', this._features.length)
3079
+ : `${I18N.labels.editElement} ${this._features[0].getId()} `;
3080
+ const layerName = this._features[0].get('_layerName_');
2705
3081
  // Data schema from the geoserver
2706
3082
  const layer = getStoredLayer(layerName);
2707
- const dataSchema = layer.getDescribeFeatureType()._parsed.properties;
2708
- let content = '<form autocomplete="false">';
2709
- Object.keys(properties).forEach((key) => {
2710
- // If the feature field exists in the geoserver and is not added by openlayers
2711
- const field = dataSchema.find((data) => data.name === key);
2712
- if (field) {
2713
- const typeXsd = field.type;
2714
- let type;
2715
- switch (typeXsd) {
2716
- case 'xsd:string':
2717
- type = 'text';
2718
- break;
2719
- case 'xsd:number':
2720
- case 'xsd:int':
2721
- type = 'number';
2722
- break;
2723
- case 'xsd:date-time':
2724
- type = 'datetime';
2725
- break;
2726
- default:
2727
- type = 'text';
2728
- }
2729
- if (type) {
2730
- content += `
2731
- <div class="ol-wfst--input-field-container">
2732
- <label class="ol-wfst--input-field-label" for="${key}">${key}</label>
2733
- <input placeholder="NULL" class="ol-wfst--input-field-input" type="${type}" name="${key}" value="${properties[key] || ''}">
2734
- </div>`;
2735
- }
2736
- }
2737
- });
2738
- content += '</form>';
2739
- this._modal._html.body.innerHTML = content;
2740
- this._modal._html.header.innerHTML = title;
3083
+ const describeFeatureType = layer.getDescribeFeatureType()._parsed;
3084
+ const fieldList = describeFeatureType.properties.filter((field) => field.name !== describeFeatureType.geomField);
3085
+ this._modal._html.body.innerHTML = '';
3086
+ this._modal._html.body.append(jsxs("div", { children: [multiple && (jsx("div", { className: "ol-wfst--edit-modal-notice", children: I18N_('multipleEditNotice', this._features.length) })), jsx("form", { autocomplete: "false", children: fieldList.flatMap((field) => {
3087
+ const key = field.name;
3088
+ const values = this._features.map((feature) => feature.get(key));
3089
+ const uniqueValues = Array.from(new Set(values.map((value) => String(value !== null && value !== void 0 ? value : ''))));
3090
+ const hasMultipleValues = multiple && uniqueValues.length > 1;
3091
+ const value = hasMultipleValues
3092
+ ? null
3093
+ : uniqueValues[0] || null;
3094
+ const typeXsd = field.type;
3095
+ let type;
3096
+ switch (typeXsd) {
3097
+ case 'xsd:double':
3098
+ case 'xsd:number':
3099
+ case 'xsd:int':
3100
+ type = 'number';
3101
+ break;
3102
+ case 'xsd:date':
3103
+ type = 'date';
3104
+ break;
3105
+ case 'xsd:date-time':
3106
+ type = 'datetime';
3107
+ break;
3108
+ case 'xsd:string':
3109
+ default:
3110
+ type = 'text';
3111
+ }
3112
+ let input = (jsx("input", { placeholder: hasMultipleValues
3113
+ ? I18N.labels.multipleValues
3114
+ : 'NULL', className: 'ol-wfst--input-field-input' +
3115
+ (hasMultipleValues
3116
+ ? ' ol-wfst--input-multiple'
3117
+ : ''), type: type, name: key, value: value, "data-multiple": hasMultipleValues ? 'true' : null }));
3118
+ if (layer.beforeShowFieldsModal) {
3119
+ const hookInput = layer.beforeShowFieldsModal(field, value, input);
3120
+ if (!hookInput) {
3121
+ return [];
3122
+ }
3123
+ if (typeof hookInput === 'string') {
3124
+ input = new DOMParser().parseFromString(hookInput, 'text/html').body.childNodes[0];
3125
+ }
3126
+ else {
3127
+ input = hookInput;
3128
+ }
3129
+ }
3130
+ if (hasMultipleValues) {
3131
+ input.dataset.multiple = 'true';
3132
+ input.classList.add('ol-wfst--input-multiple');
3133
+ }
3134
+ return (jsxs("div", { className: "ol-wfst--input-field-container", children: [jsx("label", { className: "ol-wfst--input-field-label", htmlFor: key, children: key }), input] }));
3135
+ }) })] }));
3136
+ this._modal._html.header.innerHTML = modalTitle;
2741
3137
  this._modal.show();
2742
3138
  }
3139
+ _dispatch(type, features) {
3140
+ const evt = new VectorSource.VectorSourceEvent(type, features[0]);
3141
+ evt.features = features;
3142
+ this.dispatchEvent(evt);
3143
+ }
2743
3144
  }
2744
3145
 
2745
3146
  /**
@@ -3558,8 +3959,8 @@
3558
3959
  wfsTransactionVersion: '1.1.0',
3559
3960
  projection: DEFAULT_GEOSERVER_SRS,
3560
3961
  lockFeatureParams: {
3561
- expiry: 5,
3562
- lockId: 'GeoServer',
3962
+ expiry: 5, // minutes
3963
+ lockId: 'WFST-editor',
3563
3964
  releaseAction: 'SOME'
3564
3965
  }
3565
3966
  },
@@ -3577,6 +3978,7 @@
3577
3978
  this._insertFeatures = [];
3578
3979
  this._updateFeatures = [];
3579
3980
  this._deleteFeatures = [];
3981
+ this._lockedId = this._options.advanced.lockFeatureParams.lockId;
3580
3982
  // Formats
3581
3983
  this._formatWFS = new WFS();
3582
3984
  this._formatGeoJSON = new GeoJSON();
@@ -3915,7 +4317,7 @@
3915
4317
  if (this.hasLockFeature &&
3916
4318
  this.getUseLockFeature() &&
3917
4319
  transactionType !== TransactionType.Insert) {
3918
- payload = payload.replace(`</Transaction>`, `<LockId>${this._options.advanced.lockFeatureParams.lockId}</LockId></Transaction>`);
4320
+ payload = payload.replace(`</Transaction>`, `<LockId>${this._lockedId}</LockId></Transaction>`);
3919
4321
  }
3920
4322
  const headers = Object.assign({ 'Content-Type': 'text/xml' }, this.getHeaders());
3921
4323
  const response = await fetch(this.getUrl(), {
@@ -3943,6 +4345,9 @@
3943
4345
  if (transactionType !== TransactionType.Delete) {
3944
4346
  this._removeFeatures(features);
3945
4347
  }
4348
+ features.forEach((feature) => {
4349
+ removeFeatureFromEditList(feature);
4350
+ });
3946
4351
  wlayer.refresh();
3947
4352
  showLoading(false);
3948
4353
  this._insertFeatures = [];
@@ -4002,30 +4407,34 @@
4002
4407
  const featureProperties = feature.getProperties();
4003
4408
  delete featureProperties.boundedBy;
4004
4409
  delete featureProperties._layerName_;
4410
+ delete featureProperties._editOverlayCoord_;
4005
4411
  const clone = new Feature(featureProperties);
4006
4412
  clone.setId(feature.getId());
4007
4413
  return clone;
4008
4414
  }
4009
4415
  /**
4010
- * Lock a feature in the geoserver. Useful before editing a geometry,
4011
- * to avoid changes from multiples suers
4416
+ * Lock one or several features in the geoserver. Useful before editing,
4417
+ * to avoid changes from multiples users. Locking several features at once
4418
+ * with a single request keeps the lock covered by one LockId
4012
4419
  *
4013
- * @param featureId
4420
+ * @param featureIds
4014
4421
  * @param layerName
4015
4422
  * @param retry
4016
4423
  * @public
4017
4424
  */
4018
- async lockFeature(featureId, layerName, retry = 0) {
4425
+ async lockFeature(featureIds, layerName, retry = 0) {
4019
4426
  const params = new URLSearchParams({
4020
4427
  service: 'wfs',
4021
4428
  version: this.getAdvanced().lockFeatureVersion,
4022
4429
  request: 'LockFeature',
4023
4430
  typeName: layerName,
4024
4431
  expiry: String(this._options.advanced.lockFeatureParams.expiry),
4025
- LockId: this._options.advanced.lockFeatureParams.lockId,
4432
+ handle: this._options.advanced.lockFeatureParams.lockId,
4026
4433
  releaseAction: this._options.advanced.lockFeatureParams.releaseAction,
4027
4434
  exceptions: 'application/json',
4028
- featureid: `${featureId}`
4435
+ featureid: Array.isArray(featureIds)
4436
+ ? featureIds.join(',')
4437
+ : String(featureIds)
4029
4438
  });
4030
4439
  const url_fetch = this.getUrl() + '?' + params.toString();
4031
4440
  try {
@@ -4046,7 +4455,7 @@
4046
4455
  if (exceptions[0].code === 'CannotLockAllFeatures') {
4047
4456
  // Maybe the Feature is already blocked, ant thats trigger error, so, we try one locking more time again
4048
4457
  if (!retry) {
4049
- this.lockFeature(featureId, layerName, 1);
4458
+ this.lockFeature(featureIds, layerName, 1);
4050
4459
  }
4051
4460
  else {
4052
4461
  throw error;
@@ -4058,21 +4467,20 @@
4058
4467
  }
4059
4468
  }
4060
4469
  catch (err) {
4061
- /*
4062
-
4063
- let dataDoc = (new window.DOMParser()).parseFromString(data, 'text/xml');
4064
-
4065
- let lockId = dataDoc.getElementsByTagName('wfs:LockId');
4066
-
4067
- let featuresLocked: HTMLCollectionOf<Element> = dataDoc.getElementsByTagName('ogc:FeatureId');
4068
-
4069
- for (let featureLocked of featuresLocked as any) {
4070
-
4071
- console.log(featureLocked.getAttribute('fid'));
4072
-
4470
+ // XML response (not JSON): parse the real lock id returned by
4471
+ // the server so the transaction references the lock that was
4472
+ // actually created instead of the configured one
4473
+ try {
4474
+ const dataDoc = new window.DOMParser().parseFromString(data, 'text/xml');
4475
+ const lockIdNode = dataDoc.getElementsByTagName('wfs:LockId')[0] ||
4476
+ dataDoc.getElementsByTagNameNS('http://www.opengis.net/wfs', 'LockId')[0];
4477
+ if (lockIdNode && lockIdNode.textContent) {
4478
+ this._lockedId = lockIdNode.textContent;
4479
+ }
4480
+ }
4481
+ catch (parseErr) {
4482
+ // Keep the configured lockId
4073
4483
  }
4074
-
4075
- */
4076
4484
  }
4077
4485
  return data;
4078
4486
  }
@@ -4096,34 +4504,107 @@
4096
4504
  GeoserverProperty["ISLOADED"] = "isLoaded";
4097
4505
  })(GeoserverProperty || (GeoserverProperty = {}));
4098
4506
 
4099
- var img$1 = "data:image/svg+xml,%3csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='448' height='448' viewBox='0 0 448 448'%3e %3cpath d='M222 296l29-29-38-38-29 29v14h24v24h14zM332 116c-2.25-2.25-6-2-8.25 0.25l-87.5 87.5c-2.25 2.25-2.5 6-0.25 8.25s6 2 8.25-0.25l87.5-87.5c2.25-2.25 2.5-6 0.25-8.25zM352 264.5v47.5c0 39.75-32.25 72-72 72h-208c-39.75 0-72-32.25-72-72v-208c0-39.75 32.25-72 72-72h208c10 0 20 2 29.25 6.25 2.25 1 4 3.25 4.5 5.75 0.5 2.75-0.25 5.25-2.25 7.25l-12.25 12.25c-2.25 2.25-5.25 3-8 2-3.75-1-7.5-1.5-11.25-1.5h-208c-22 0-40 18-40 40v208c0 22 18 40 40 40h208c22 0 40-18 40-40v-31.5c0-2 0.75-4 2.25-5.5l16-16c2.5-2.5 5.75-3 8.75-1.75s5 4 5 7.25zM328 80l72 72-168 168h-72v-72zM439 113l-23 23-72-72 23-23c9.25-9.25 24.75-9.25 34 0l38 38c9.25 9.25 9.25 24.75 0 34z'%3e%3c/path%3e%3c/svg%3e";
4507
+ function editFieldsSvg() {
4508
+ return (new DOMParser().parseFromString("<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width=\"448\" height=\"448\" viewBox=\"0 0 448 448\">\r\n <path d=\"M222 296l29-29-38-38-29 29v14h24v24h14zM332 116c-2.25-2.25-6-2-8.25 0.25l-87.5 87.5c-2.25 2.25-2.5 6-0.25 8.25s6 2 8.25-0.25l87.5-87.5c2.25-2.25 2.5-6 0.25-8.25zM352 264.5v47.5c0 39.75-32.25 72-72 72h-208c-39.75 0-72-32.25-72-72v-208c0-39.75 32.25-72 72-72h208c10 0 20 2 29.25 6.25 2.25 1 4 3.25 4.5 5.75 0.5 2.75-0.25 5.25-2.25 7.25l-12.25 12.25c-2.25 2.25-5.25 3-8 2-3.75-1-7.5-1.5-11.25-1.5h-208c-22 0-40 18-40 40v208c0 22 18 40 40 40h208c22 0 40-18 40-40v-31.5c0-2 0.75-4 2.25-5.5l16-16c2.5-2.5 5.75-3 8.75-1.75s5 4 5 7.25zM328 80l72 72-168 168h-72v-72zM439 113l-23 23-72-72 23-23c9.25-9.25 24.75-9.25 34 0l38 38c9.25 9.25 9.25 24.75 0 34z\"></path>\r\n</svg>", 'image/svg+xml')).firstChild;
4509
+ }
4100
4510
 
4101
- var img = "data:image/svg+xml,%3csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='541' height='512' viewBox='0 0 541 512'%3e %3cpath fill='black' d='M103.306 228.483l129.493-125.249c-17.662-4.272-31.226-18.148-34.98-35.663l-0.055-0.307-129.852 125.248c17.812 4.15 31.53 18.061 35.339 35.662l0.056 0.308z'%3e%3c/path%3e %3cpath fill='black' d='M459.052 393.010c-13.486-8.329-22.346-23.018-22.373-39.779v-0.004c-0.053-0.817-0.082-1.772-0.082-2.733s0.030-1.916 0.089-2.863l-0.007 0.13-149.852 71.94c9.598 8.565 15.611 20.969 15.611 34.779 0 0.014 0 0.029 0 0.043v-0.002c-0.048 5.164-0.94 10.104-2.544 14.711l0.098-0.322z'%3e%3c/path%3e %3cpath fill='black' d='M290.207 57.553c-0.009 15.55-7.606 29.324-19.289 37.819l-0.135 0.093 118.054 46.69c-0.216-1.608-0.346-3.48-0.36-5.379v-0.017c0.033-16.948 9.077-31.778 22.596-39.953l0.209-0.118-122.298-48.056c0.659 2.633 1.098 5.693 1.221 8.834l0.002 0.087z'%3e%3c/path%3e %3cpath fill='black' d='M241.36 410.132l-138.629-160.067c-4.734 17.421-18.861 30.61-36.472 33.911l-0.29 0.045 143.881 166.255c1.668-18.735 14.197-34.162 31.183-40.044l0.327-0.099z'%3e%3c/path%3e %3cpath fill='black' d='M243.446 115.105c-31.785 0-57.553-25.767-57.553-57.553s25.767-57.553 57.553-57.553c31.785 0 57.552 25.767 57.552 57.553v0c0 31.786-25.767 57.553-57.553 57.553v0zM243.446 21.582c-19.866 0-35.97 16.105-35.97 35.97s16.105 35.97 35.97 35.97c19.866 0 35.97-16.105 35.97-35.97v0c0-19.866-16.104-35.97-35.97-35.97v0z'%3e%3c/path%3e %3cpath fill='black' d='M483.224 410.78c-31.786 0-57.553-25.767-57.553-57.553s25.767-57.553 57.553-57.553c31.786 0 57.552 25.767 57.552 57.553v0c0 31.786-25.767 57.553-57.553 57.553v0zM483.224 317.257c-19.866 0-35.97 16.104-35.97 35.97s16.105 35.97 35.97 35.97c19.866 0 35.97-16.105 35.97-35.97v0c0-19.866-16.105-35.97-35.97-35.97v0z'%3e%3c/path%3e %3cpath fill='black' d='M57.553 295.531c-31.785 0-57.553-25.767-57.553-57.553s25.767-57.553 57.553-57.553c31.785 0 57.553 25.767 57.553 57.553v0c0 31.786-25.767 57.553-57.553 57.553v0zM57.553 202.008c-19.866 0-35.97 16.105-35.97 35.97s16.105 35.97 35.97 35.97c19.866 0 35.97-16.105 35.97-35.97v0c-0.041-19.835-16.13-35.898-35.97-35.898 0 0 0 0 0 0v0z'%3e%3c/path%3e %3cpath fill='black' d='M256.036 512.072c-31.786 0-57.553-25.767-57.553-57.553s25.767-57.553 57.553-57.553c31.786 0 57.553 25.767 57.553 57.553v0c0 31.786-25.767 57.553-57.553 57.553v0zM256.036 418.55c-19.866 0-35.97 16.104-35.97 35.97s16.105 35.97 35.97 35.97c19.866 0 35.97-16.105 35.97-35.97v0c0-19.866-16.105-35.97-35.97-35.97v0z'%3e%3c/path%3e %3cpath fill='black' d='M435.24 194.239c-31.786 0-57.553-25.767-57.553-57.553s25.767-57.553 57.553-57.553c31.786 0 57.553 25.767 57.553 57.553v0c0 31.785-25.767 57.553-57.553 57.553v0zM435.24 100.716c-19.866 0-35.97 16.105-35.97 35.97s16.105 35.97 35.97 35.97c19.866 0 35.97-16.105 35.97-35.97v0c0-19.866-16.105-35.97-35.97-35.97v0z'%3e%3c/path%3e%3c/svg%3e";
4511
+ function editGeomSvg() {
4512
+ return (new DOMParser().parseFromString(" <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width=\"541\" height=\"512\" viewBox=\"0 0 541 512\">\r\n <path fill=\"#000\" d=\"M103.306 228.483l129.493-125.249c-17.662-4.272-31.226-18.148-34.98-35.663l-0.055-0.307-129.852 125.248c17.812 4.15 31.53 18.061 35.339 35.662l0.056 0.308z\"></path>\r\n <path fill=\"#000\" d=\"M459.052 393.010c-13.486-8.329-22.346-23.018-22.373-39.779v-0.004c-0.053-0.817-0.082-1.772-0.082-2.733s0.030-1.916 0.089-2.863l-0.007 0.13-149.852 71.94c9.598 8.565 15.611 20.969 15.611 34.779 0 0.014 0 0.029 0 0.043v-0.002c-0.048 5.164-0.94 10.104-2.544 14.711l0.098-0.322z\"></path>\r\n <path fill=\"#000\" d=\"M290.207 57.553c-0.009 15.55-7.606 29.324-19.289 37.819l-0.135 0.093 118.054 46.69c-0.216-1.608-0.346-3.48-0.36-5.379v-0.017c0.033-16.948 9.077-31.778 22.596-39.953l0.209-0.118-122.298-48.056c0.659 2.633 1.098 5.693 1.221 8.834l0.002 0.087z\"></path>\r\n <path fill=\"#000\" d=\"M241.36 410.132l-138.629-160.067c-4.734 17.421-18.861 30.61-36.472 33.911l-0.29 0.045 143.881 166.255c1.668-18.735 14.197-34.162 31.183-40.044l0.327-0.099z\"></path>\r\n <path fill=\"#000\" d=\"M243.446 115.105c-31.785 0-57.553-25.767-57.553-57.553s25.767-57.553 57.553-57.553c31.785 0 57.552 25.767 57.552 57.553v0c0 31.786-25.767 57.553-57.553 57.553v0zM243.446 21.582c-19.866 0-35.97 16.105-35.97 35.97s16.105 35.97 35.97 35.97c19.866 0 35.97-16.105 35.97-35.97v0c0-19.866-16.104-35.97-35.97-35.97v0z\"></path>\r\n <path fill=\"#000\" d=\"M483.224 410.78c-31.786 0-57.553-25.767-57.553-57.553s25.767-57.553 57.553-57.553c31.786 0 57.552 25.767 57.552 57.553v0c0 31.786-25.767 57.553-57.553 57.553v0zM483.224 317.257c-19.866 0-35.97 16.104-35.97 35.97s16.105 35.97 35.97 35.97c19.866 0 35.97-16.105 35.97-35.97v0c0-19.866-16.105-35.97-35.97-35.97v0z\"></path>\r\n <path fill=\"#000\" d=\"M57.553 295.531c-31.785 0-57.553-25.767-57.553-57.553s25.767-57.553 57.553-57.553c31.785 0 57.553 25.767 57.553 57.553v0c0 31.786-25.767 57.553-57.553 57.553v0zM57.553 202.008c-19.866 0-35.97 16.105-35.97 35.97s16.105 35.97 35.97 35.97c19.866 0 35.97-16.105 35.97-35.97v0c-0.041-19.835-16.13-35.898-35.97-35.898 0 0 0 0 0 0v0z\"></path>\r\n <path fill=\"#000\" d=\"M256.036 512.072c-31.786 0-57.553-25.767-57.553-57.553s25.767-57.553 57.553-57.553c31.786 0 57.553 25.767 57.553 57.553v0c0 31.786-25.767 57.553-57.553 57.553v0zM256.036 418.55c-19.866 0-35.97 16.104-35.97 35.97s16.105 35.97 35.97 35.97c19.866 0 35.97-16.105 35.97-35.97v0c0-19.866-16.105-35.97-35.97-35.97v0z\"></path>\r\n <path fill=\"#000\" d=\"M435.24 194.239c-31.786 0-57.553-25.767-57.553-57.553s25.767-57.553 57.553-57.553c31.786 0 57.553 25.767 57.553 57.553v0c0 31.785-25.767 57.553-57.553 57.553v0zM435.24 100.716c-19.866 0-35.97 16.105-35.97 35.97s16.105 35.97 35.97 35.97c19.866 0 35.97-16.105 35.97-35.97v0c0-19.866-16.105-35.97-35.97-35.97v0z\"></path>\r\n</svg>", 'image/svg+xml')).firstChild;
4513
+ }
4102
4514
 
4103
4515
  class EditOverlay extends Overlay {
4104
- constructor(feature, coordinate = null) {
4516
+ constructor(feature = null, coordinate = null, id) {
4105
4517
  super({
4106
- id: feature.getId(),
4107
- position: coordinate || extent_js.getCenter(feature.getGeometry().getExtent()),
4518
+ id: id !== null && id !== void 0 ? id : (feature ? feature.getId() : undefined),
4519
+ position: coordinate ||
4520
+ (feature
4521
+ ? extent_js.getCenter(feature.getGeometry().getExtent())
4522
+ : undefined),
4108
4523
  positioning: 'center-center',
4109
4524
  offset: [0, -40],
4110
4525
  stopEvent: true,
4111
- element: (createElement("div", null,
4112
- createElement("div", { className: "ol-wfst--edit-button-cnt", onClick: () => {
4113
- this.dispatchEvent('editFields');
4114
- } },
4115
- createElement("button", { className: "ol-wfst--edit-button", type: "button", title: I18N.labels.editFields },
4116
- createElement("img", { src: img$1, alt: I18N.labels.editFields }))),
4117
- createElement("div", { className: "ol-wfst--edit-button-cnt", onClick: () => {
4118
- this.dispatchEvent('editGeom');
4119
- } },
4120
- createElement("button", { class: "ol-wfst--edit-button", type: "button", title: I18N.labels.editGeom },
4121
- createElement("img", { src: img, alt: I18N.labels.editGeom })))))
4526
+ element: (jsxs("div", { children: [jsx("div", { className: "ol-wfst--edit-button-cnt", onClick: () => {
4527
+ this.dispatchEvent('editFields');
4528
+ }, children: jsx("button", { className: "ol-wfst--edit-button", type: "button", title: I18N.labels.editFields, children: editFieldsSvg() }) }), jsx("div", { className: "ol-wfst--edit-button-cnt", onClick: () => {
4529
+ this.dispatchEvent('editGeom');
4530
+ }, children: jsx("button", { class: "ol-wfst--edit-button", type: "button", title: I18N.labels.editGeom, children: editGeomSvg() }) })] }))
4531
+ });
4532
+ }
4533
+ }
4534
+
4535
+ function addLineSvg() {
4536
+ return (new DOMParser().parseFromString("<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\r\n <path d=\"M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z\"></path>\r\n</svg>", 'image/svg+xml')).firstChild;
4537
+ }
4538
+
4539
+ class ExtendLineOverlay extends Overlay {
4540
+ constructor(feature, coordinate, side, component = 0, onClick) {
4541
+ super({
4542
+ position: coordinate,
4543
+ positioning: 'center-center',
4544
+ stopEvent: true,
4545
+ element: (jsx("div", { className: "ol-wfst--extend-button-cnt", children: jsx("button", { className: "ol-wfst--extend-button", type: "button", title: I18N.labels.continueLine, onClick: () => {
4546
+ onClick(side);
4547
+ }, children: addLineSvg() }) }))
4548
+ });
4549
+ this.feature = feature;
4550
+ this.side = side;
4551
+ this.component = component;
4552
+ }
4553
+ }
4554
+
4555
+ const QUERY_OVERLAY_ID = 'ol-wfst--query-overlay';
4556
+ /**
4557
+ * Popup overlay that shows the attributes of a queried feature and its
4558
+ * layer label. The geometry is highlighted separately on a dedicated layer.
4559
+ *
4560
+ * @extends {ol/Overlay~Overlay}
4561
+ * @param feature
4562
+ * @param coordinate
4563
+ * @param layer
4564
+ * @private
4565
+ */
4566
+ class QueryOverlay extends Overlay {
4567
+ constructor(feature, coordinate, layer) {
4568
+ const describeFeatureType = layer.getDescribeFeatureType();
4569
+ const parsed = describeFeatureType === null || describeFeatureType === void 0 ? void 0 : describeFeatureType._parsed;
4570
+ const fields = (parsed === null || parsed === void 0 ? void 0 : parsed.properties) || [];
4571
+ const geomField = parsed === null || parsed === void 0 ? void 0 : parsed.geomField;
4572
+ const rows = fields
4573
+ .filter((field) => field.name !== geomField)
4574
+ .map((field) => {
4575
+ let value = feature.get(field.name);
4576
+ if (value === undefined || value === null) {
4577
+ value = '';
4578
+ }
4579
+ else if (typeof value === 'object') {
4580
+ value = JSON.stringify(value);
4581
+ }
4582
+ return (jsxs("div", { className: "ol-wfst--query-popup-row", children: [jsx("span", { className: "ol-wfst--query-popup-key", children: field.name }), jsx("span", { className: "ol-wfst--query-popup-value", children: String(value) })] }));
4583
+ });
4584
+ super({
4585
+ id: QUERY_OVERLAY_ID,
4586
+ position: coordinate || extent_js.getCenter(feature.getGeometry().getExtent()),
4587
+ positioning: 'bottom-center',
4588
+ offset: [0, -14],
4589
+ stopEvent: true,
4590
+ autoPan: true,
4591
+ element: (jsxs("div", { className: "ol-wfst--query-popup", children: [jsxs("div", { className: "ol-wfst--query-popup-head", children: [jsx("div", { className: "ol-wfst--query-popup-title", children: I18N.labels.featureInfo }), jsx("button", { className: "ol-wfst--query-popup-close", type: "button", title: I18N.labels.close, onClick: () => {
4592
+ this.dispatchEvent('close');
4593
+ }, children: "\u00D7" })] }), jsxs("div", { className: "ol-wfst--query-popup-subtitle", children: [jsx("b", { children: layer.get(BaseLayerProperty.LABEL) }), jsx("i", { children: String(feature.getId()) })] }), jsx("div", { className: "ol-wfst--query-popup-body", children: rows })] }))
4122
4594
  });
4123
4595
  }
4124
4596
  }
4125
4597
 
4598
+ function fullscreenSvg() {
4599
+ return (new DOMParser().parseFromString("<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\r\n <path d=\"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z\"></path>\r\n</svg>", 'image/svg+xml')).firstChild;
4600
+ }
4601
+
4602
+ function fullscreenExitSvg() {
4603
+ return (new DOMParser().parseFromString("<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\r\n <path d=\"M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z\"></path>\r\n</svg>", 'image/svg+xml')).firstChild;
4604
+ }
4605
+
4126
4606
  const controlElement = document.createElement('div');
4607
+ const INIT_LOADING_TIMEOUT = 20000;
4127
4608
  /**
4128
4609
  * Tiny WFS-T client to insert (drawing/uploading), modify and delete
4129
4610
  * features on GeoServers using OpenLayers. Layers with these types
@@ -4152,6 +4633,14 @@
4152
4633
  }
4153
4634
  });
4154
4635
  this._initialized = false;
4636
+ this._queryOverlayId = 'ol-wfst--query-overlay';
4637
+ this._shiftPressed = false;
4638
+ this._hoveredFeature = null;
4639
+ this._keySelectBound = false;
4640
+ // Editing
4641
+ this._editFeaturesOriginal = {};
4642
+ this._multiOverlayId = 'ol-wfst--multi-edit-overlay';
4643
+ this._extendOverlays = [];
4155
4644
  setLang(options.language, options.i18n);
4156
4645
  const defaultOptions = getDefaultOptions();
4157
4646
  this._options = deepObjectAssign(defaultOptions, options);
@@ -4160,7 +4649,7 @@
4160
4649
  this._controlWidgetToolsDiv = controlElement;
4161
4650
  this._controlWidgetToolsDiv.className = 'ol-wfst--tools-control';
4162
4651
  this._uploads = new Uploads(this._options);
4163
- this._editFields = new EditFieldsModal(this._options.modal);
4652
+ this._editFields = new EditFieldsModal(this._options);
4164
4653
  }
4165
4654
  /**
4166
4655
  * Get all the layers in the ol-wfst instance
@@ -4192,8 +4681,10 @@
4192
4681
  if (layers.length) {
4193
4682
  let layerRendered = 0;
4194
4683
  let layersNumber = 0; // Only count visibles
4684
+ showLoading();
4685
+ const layerInitPromises = [];
4195
4686
  layers.forEach((layer) => {
4196
- if (layer.getVisible())
4687
+ if (layer.isVisible(this._view))
4197
4688
  layersNumber++;
4198
4689
  layer.on('layerRendered', () => {
4199
4690
  layerRendered++;
@@ -4207,15 +4698,17 @@
4207
4698
  }
4208
4699
  });
4209
4700
  layer.on('change:describeFeatureType', () => {
4210
- const domEl = this._layersControl.addLayerEl(layer);
4211
- layer.on('change:isVisible', () => {
4212
- const layerNotVisible = 'ol-wfst--layer-not-visible';
4213
- const visible = layer.isVisibleByZoom();
4214
- if (visible)
4215
- domEl.classList.remove(layerNotVisible);
4216
- else
4217
- domEl.classList.add(layerNotVisible);
4218
- });
4701
+ if (this._options.showControl && layers.length > 1) {
4702
+ const domEl = this._layersControl.addLayerEl(layer);
4703
+ layer.on('change:isVisible', () => {
4704
+ const layerNotVisible = 'ol-wfst--layer-not-visible';
4705
+ const visible = layer.isVisibleByZoom();
4706
+ if (visible)
4707
+ domEl.classList.remove(layerNotVisible);
4708
+ else
4709
+ domEl.classList.add(layerNotVisible);
4710
+ });
4711
+ }
4219
4712
  layer.set(BaseLayerProperty.ISVISIBLE, this._currentZoom > layer.getMinZoom());
4220
4713
  this.dispatchEvent(new WfstEvent({
4221
4714
  type: 'describeFeatureType',
@@ -4223,13 +4716,21 @@
4223
4716
  data: layer.getDescribeFeatureType()
4224
4717
  }));
4225
4718
  });
4226
- layer._init();
4719
+ layerInitPromises.push(layer._init());
4227
4720
  this._map.addLayer(layer);
4228
4721
  setMapLayers({
4229
4722
  [layer.get(BaseLayerProperty.NAME)]: layer
4230
4723
  });
4231
4724
  });
4232
4725
  this._createMapElements(this._options.showControl, this._options.active);
4726
+ // Show the loading bar until every layer has resolved its
4727
+ // DescribeFeatureType (success or error), with a fail-safe
4728
+ // timeout so it never stays visible on a dead connection.
4729
+ await Promise.race([
4730
+ Promise.all(layerInitPromises),
4731
+ new Promise((resolve) => setTimeout(resolve, INIT_LOADING_TIMEOUT))
4732
+ ]);
4733
+ showLoading(false);
4233
4734
  }
4234
4735
  }
4235
4736
  catch (err) {
@@ -4262,17 +4763,26 @@
4262
4763
  });
4263
4764
  });
4264
4765
  // @ts-expect-error
4265
- this._editFields.on('save', ({ feature }) => {
4266
- // Force deselect to trigger handler
4267
- this._collectionModify.remove(feature);
4766
+ this._editFields.on('save', async ({ feature, features }) => {
4767
+ const list = features || [feature];
4768
+ const ok = await this._transactEditList(list);
4769
+ if (ok) {
4770
+ Array.from(list).forEach((f) => this._collectionModify.remove(f));
4771
+ }
4268
4772
  });
4269
4773
  // @ts-expect-error
4270
- this._editFields.on('delete', ({ feature }) => {
4271
- this._deleteFeature(feature, true);
4774
+ this._editFields.on('delete', ({ features }) => {
4775
+ this._deleteFeature(features, true);
4272
4776
  });
4273
4777
  this._addMapEvents();
4778
+ this._addMiddleButtonPan();
4274
4779
  initModal(this._options['modal']);
4275
- this._controlWidgetToolsDiv.append(initLoading());
4780
+ this._layersWidgetDiv = document.createElement('div');
4781
+ this._layersWidgetDiv.className = 'ol-wfst--layers-control';
4782
+ this._layersWidgetDiv.append(initLoading());
4783
+ this._map.addControl(new Control({
4784
+ element: this._layersWidgetDiv
4785
+ }));
4276
4786
  this._initMapAndLayers();
4277
4787
  }
4278
4788
  /**
@@ -4288,8 +4798,18 @@
4288
4798
  this._prepareEditLayer();
4289
4799
  this._addInteractions();
4290
4800
  this._addInteractionHandlers();
4801
+ this._prepareHighlightLayer();
4802
+ this._addQueryInteraction();
4291
4803
  if (showControl) {
4292
4804
  this._addMapControl();
4805
+ if (this._options.fullscreen) {
4806
+ this._map.addControl(new FullScreen({
4807
+ className: 'ol-wfst--fullscreen',
4808
+ tipLabel: I18N.labels.fullscreen,
4809
+ label: fullscreenSvg(),
4810
+ labelActive: fullscreenExitSvg()
4811
+ }));
4812
+ }
4293
4813
  }
4294
4814
  // By default, init in edit mode
4295
4815
  this.activateEditMode(active);
@@ -4308,9 +4828,10 @@
4308
4828
  this._interactionWfsSelect = new Select({
4309
4829
  hitTolerance: 10,
4310
4830
  style: (feature) => styleFunction(feature),
4311
- toggleCondition: condition_js.never,
4831
+ toggleCondition: condition_js.shiftKeyOnly, // Allow adding features to the selection with shift
4312
4832
  filter: (feature, layer) => {
4313
4833
  return (getMode() !== Modes.Edit &&
4834
+ getMode() !== Modes.Query &&
4314
4835
  layer &&
4315
4836
  layer instanceof WfsLayer &&
4316
4837
  layer === getActiveLayerToInsertEls());
@@ -4319,6 +4840,11 @@
4319
4840
  this._map.addInteraction(this._interactionWfsSelect);
4320
4841
  this._interactionWfsSelect.on('select', ({ selected, deselected, mapBrowserEvent }) => {
4321
4842
  const coordinate = mapBrowserEvent.coordinate;
4843
+ // Clear hover to avoid keeping the style on the selected feature
4844
+ this._clearHoverState();
4845
+ if (getMode() === Modes.Query) {
4846
+ return;
4847
+ }
4322
4848
  if (selected.length) {
4323
4849
  selected.forEach((feature) => {
4324
4850
  if (!isFeatureEdited(feature)) {
@@ -4328,6 +4854,7 @@
4328
4854
  this._addFeatureToEditMode(feature, coordinate, layer.get(BaseLayerProperty.NAME));
4329
4855
  }
4330
4856
  });
4857
+ this._lockSelectedFeatures();
4331
4858
  }
4332
4859
  if (deselected.length) {
4333
4860
  if (getMode() !== Modes.Edit) {
@@ -4349,18 +4876,33 @@
4349
4876
  this._interactionSelectModify = new Select({
4350
4877
  style: (feature) => styleFunction(feature),
4351
4878
  layers: [getEditLayer()],
4352
- toggleCondition: condition_js.never,
4879
+ toggleCondition: condition_js.shiftKeyOnly, // Allow adding features with shift
4353
4880
  removeCondition: () => (getMode() === Modes.Edit ? true : false) // Prevent deselect on clicking outside the feature
4354
4881
  });
4355
4882
  this._map.addInteraction(this._interactionSelectModify);
4356
- this._collectionModify =
4357
- this._interactionSelectModify.getFeatures();
4358
- this._keyClickWms = this._map.on(this._options.evtType, async (evt) => {
4359
- if (this._map.hasFeatureAtPixel(evt.pixel)) {
4883
+ // When clicking without shift, replace the current selection: remove
4884
+ // any previously selected and not re-selected feature from the edit list
4885
+ this._interactionSelectModify.on('select', ({ selected, mapBrowserEvent }) => {
4886
+ if (!selected.length ||
4887
+ !mapBrowserEvent ||
4888
+ condition_js.shiftKeyOnly(mapBrowserEvent)) {
4360
4889
  return;
4361
4890
  }
4362
- // Only get other features if editmode is disabled
4363
- if (getMode() !== Modes.Edit) {
4891
+ const selectedSet = new Set(selected);
4892
+ Array.from(this._collectionModify.getArray()).forEach((feature) => {
4893
+ if (!selectedSet.has(feature)) {
4894
+ this._collectionModify.remove(feature);
4895
+ }
4896
+ });
4897
+ });
4898
+ this._collectionModify =
4899
+ this._interactionSelectModify.getFeatures();
4900
+ this._keyClickWms = this._map.on(this._options.evtType || 'singleclick', async (evt) => {
4901
+ if (this._map.hasFeatureAtPixel(evt.pixel)) {
4902
+ return;
4903
+ }
4904
+ // Only get other features if editmode is disabled
4905
+ if (getMode() !== Modes.Edit && getMode() !== Modes.Query) {
4364
4906
  const layer = getActiveLayerToInsertEls();
4365
4907
  // If layer is hidden or is a wfs, skip
4366
4908
  if (!layer.getVisible() ||
@@ -4372,8 +4914,14 @@
4372
4914
  if (!(features === null || features === void 0 ? void 0 : features.length)) {
4373
4915
  return;
4374
4916
  }
4375
- // For now, support is only for one feature at time
4917
+ // Without shift, replace the current selection
4918
+ if (!condition_js.shiftKeyOnly(evt)) {
4919
+ Array.from(this._collectionModify.getArray()).forEach((feature) => {
4920
+ this._collectionModify.remove(feature);
4921
+ });
4922
+ }
4376
4923
  this._addFeatureToEditMode(features[0], evt.coordinate, layer.get(BaseLayerProperty.NAME));
4924
+ this._lockSelectedFeatures();
4377
4925
  }
4378
4926
  });
4379
4927
  };
@@ -4384,20 +4932,9 @@
4384
4932
  prepareWmsInteraction();
4385
4933
  }
4386
4934
  this._interactionModify = new Modify({
4387
- style: () => {
4935
+ style: (feature) => {
4388
4936
  if (getMode() === Modes.Edit) {
4389
- return new Style({
4390
- image: new CircleStyle({
4391
- radius: 6,
4392
- fill: new Fill({
4393
- color: '#ff0000'
4394
- }),
4395
- stroke: new Stroke({
4396
- width: 2,
4397
- color: 'rgba(5, 5, 5, 0.9)'
4398
- })
4399
- })
4400
- });
4937
+ return editVertexStyles(feature.get('existing') === true);
4401
4938
  }
4402
4939
  else {
4403
4940
  return;
@@ -4413,6 +4950,175 @@
4413
4950
  source: getEditLayer().getSource()
4414
4951
  });
4415
4952
  this._map.addInteraction(this._interactionSnap);
4953
+ this._addSelectionInteractions();
4954
+ }
4955
+ /**
4956
+ * Interactions to select features by drawing a box (DragBox) or a freehand
4957
+ * polygon (freehand Draw lasso). The selected features are added to the
4958
+ * edit list, keeping the same behaviour as the single click selection.
4959
+ *
4960
+ * @private
4961
+ */
4962
+ _addSelectionInteractions() {
4963
+ const selectionStyle = new Style({
4964
+ fill: new Fill({
4965
+ color: 'rgba(255, 200, 0, 0.1)'
4966
+ }),
4967
+ stroke: new Stroke({
4968
+ color: '#ffc800',
4969
+ width: 2,
4970
+ lineDash: [6, 4]
4971
+ })
4972
+ });
4973
+ // Box selection (dragging a rectangle)
4974
+ this._interactionDragBox = new DragBox({
4975
+ condition: (evt) => condition_js.primaryAction(evt) && this._isSelectMode(SelectionMode.Box),
4976
+ className: 'ol-wfst--dragbox'
4977
+ });
4978
+ this._interactionDragBox.on('boxend', (evt) => {
4979
+ if (!this._isSelectMode(SelectionMode.Box)) {
4980
+ return;
4981
+ }
4982
+ // Without shift, replace the current selection
4983
+ if (!evt.mapBrowserEvent.originalEvent.shiftKey) {
4984
+ this._collectionModify.clear();
4985
+ }
4986
+ this._selectByGeometry(this._interactionDragBox.getGeometry());
4987
+ });
4988
+ this._map.addInteraction(this._interactionDragBox);
4989
+ // Freehand selection (holding the mouse to draw a closed lasso)
4990
+ this._interactionFreehandSelect = new Draw({
4991
+ type: GeometryType.Polygon,
4992
+ freehand: true,
4993
+ stopClick: true,
4994
+ style: selectionStyle,
4995
+ condition: (evt) => condition_js.primaryAction(evt) && this._isSelectMode(SelectionMode.Freehand)
4996
+ });
4997
+ this._interactionFreehandSelect.on('drawend', (evt) => {
4998
+ if (!this._isSelectMode(SelectionMode.Freehand)) {
4999
+ return;
5000
+ }
5001
+ const geometry = evt.feature.getGeometry();
5002
+ const ring = geometry ? geometry.getCoordinates()[0] : [];
5003
+ // Ignore degenerate lassos (a simple click without dragging)
5004
+ if (!ring.length || ring.length < 4 || geometry.getArea() <= 0) {
5005
+ return;
5006
+ }
5007
+ // Without shift, replace the current selection
5008
+ if (!this._shiftPressed) {
5009
+ this._collectionModify.clear();
5010
+ }
5011
+ this._selectByGeometry(geometry);
5012
+ });
5013
+ this._map.addInteraction(this._interactionFreehandSelect);
5014
+ this._refreshSelectionInteractions();
5015
+ }
5016
+ /**
5017
+ * Whether a selection tool mode is currently usable: only outside the
5018
+ * edit/draw modes.
5019
+ *
5020
+ * @param mode
5021
+ * @private
5022
+ */
5023
+ _isSelectMode(mode) {
5024
+ return getMode() === null && getSelectionMode() === mode;
5025
+ }
5026
+ /**
5027
+ * Enable/disable the box and freehand selection interactions according to
5028
+ * the current selection tool and map mode.
5029
+ *
5030
+ * @private
5031
+ */
5032
+ _refreshSelectionInteractions() {
5033
+ const active = getMode() === null;
5034
+ if (this._interactionDragBox) {
5035
+ this._interactionDragBox.setActive(active && getSelectionMode() === SelectionMode.Box);
5036
+ }
5037
+ if (this._interactionFreehandSelect) {
5038
+ this._interactionFreehandSelect.setActive(active && getSelectionMode() === SelectionMode.Freehand);
5039
+ }
5040
+ }
5041
+ /**
5042
+ * Select the features that intersect the given polygon on the active layer
5043
+ * and add them to the edit list. WFS features are picked from the already
5044
+ * loaded source, while WMS features are requested to the GeoServer with a
5045
+ * WFS GetFeature INTERSECTS filter.
5046
+ *
5047
+ * @param geometry
5048
+ * @private
5049
+ */
5050
+ async _selectByGeometry(geometry) {
5051
+ const layer = getActiveLayerToInsertEls();
5052
+ if (!layer || !layer.getVisible() || !layer.isVisibleByZoom()) {
5053
+ showError(I18N.errors.layerNotVisible);
5054
+ return;
5055
+ }
5056
+ let features;
5057
+ if (layer instanceof WfsLayer) {
5058
+ const candidates = layer
5059
+ .getSource()
5060
+ .getFeaturesInExtent(geometry.getExtent());
5061
+ // The extent is only a bounding box; for the freehand lasso be
5062
+ // more precise and keep the features whose vertices fall inside it
5063
+ if (getSelectionMode() === SelectionMode.Freehand) {
5064
+ features = candidates.filter((feature) => {
5065
+ const featureGeom = feature.getGeometry();
5066
+ if (!featureGeom) {
5067
+ return false;
5068
+ }
5069
+ const flatCoords = featureGeom.getFlatCoordinates();
5070
+ const stride = featureGeom.getStride();
5071
+ for (let i = 0; i < flatCoords.length; i += stride) {
5072
+ if (geometry.intersectsCoordinate([
5073
+ flatCoords[i],
5074
+ flatCoords[i + 1]
5075
+ ])) {
5076
+ return true;
5077
+ }
5078
+ }
5079
+ return false;
5080
+ });
5081
+ }
5082
+ else {
5083
+ features = candidates;
5084
+ }
5085
+ }
5086
+ else if (layer instanceof WmsLayer) {
5087
+ features = await layer._getFeaturesInGeometry(geometry);
5088
+ }
5089
+ if (features) {
5090
+ this._selectFeatures(features);
5091
+ }
5092
+ }
5093
+ /**
5094
+ * Add the given features to the edit mode:
5095
+ * remove them from their original layer and push them into the edit
5096
+ * collection so they can be modified/deleted like the ones selected
5097
+ * with a single click.
5098
+ *
5099
+ * @param features
5100
+ * @private
5101
+ */
5102
+ _selectFeatures(features) {
5103
+ if (!features || !features.length) {
5104
+ return;
5105
+ }
5106
+ const layer = getActiveLayerToInsertEls();
5107
+ const layerName = layer.get(BaseLayerProperty.NAME);
5108
+ const selectedIds = new Set(this._collectionModify.getArray().map((feature) => feature.getId()));
5109
+ features.forEach((feature) => {
5110
+ if (isFeatureEdited(feature) || selectedIds.has(feature.getId())) {
5111
+ return;
5112
+ }
5113
+ // WFS features are stored in their layer source: remove them from
5114
+ // there while they are being edited
5115
+ if (layer instanceof WfsLayer &&
5116
+ layer.getSource().hasFeature(feature)) {
5117
+ layer.getSource().removeFeature(feature);
5118
+ }
5119
+ this._addFeatureToEditMode(feature, null, layerName);
5120
+ });
5121
+ this._lockSelectedFeatures();
4416
5122
  }
4417
5123
  /**
4418
5124
  * Layer to store temporary the elements to be edited
@@ -4421,6 +5127,149 @@
4421
5127
  _prepareEditLayer() {
4422
5128
  this._map.addLayer(getEditLayer());
4423
5129
  }
5130
+ /**
5131
+ * Create the dedicated layer used to highlight the queried feature, so
5132
+ * the source layers keep their own rendering untouched.
5133
+ * @private
5134
+ */
5135
+ _prepareHighlightLayer() {
5136
+ this._highlightSource = new VectorSource();
5137
+ this._highlightLayer = new VectorLayer({
5138
+ source: this._highlightSource,
5139
+ style: (feature) => queryStyleFunction(),
5140
+ zIndex: 100
5141
+ });
5142
+ this._map.addLayer(this._highlightLayer);
5143
+ }
5144
+ /**
5145
+ * Listen for clicks while the query mode is active: get the clicked
5146
+ * feature of the active layer (from the already loaded source for WFS,
5147
+ * or through a GetFeatureInfo request for WMS), highlight it and show
5148
+ * its attributes in a popup.
5149
+ * @private
5150
+ */
5151
+ _addQueryInteraction() {
5152
+ this._keyClickQuery = this._map.on(this._options.evtType || 'singleclick', async (evt) => {
5153
+ if (getMode() !== Modes.Query) {
5154
+ return;
5155
+ }
5156
+ const layer = getActiveLayerToInsertEls();
5157
+ // Skip hidden layers
5158
+ if (!layer.getVisible() || !layer.isVisibleByZoom()) {
5159
+ return;
5160
+ }
5161
+ let feature = null;
5162
+ if (layer instanceof WfsLayer) {
5163
+ const featuresAtPixel = this._map.getFeaturesAtPixel(evt.pixel, {
5164
+ hitTolerance: 10,
5165
+ layerFilter: (candidate) => candidate === layer
5166
+ });
5167
+ if (featuresAtPixel && featuresAtPixel.length) {
5168
+ feature = featuresAtPixel[0];
5169
+ }
5170
+ }
5171
+ else if (layer instanceof WmsLayer) {
5172
+ const features = await layer._getFeaturesByClickEvent(evt);
5173
+ if (features === null || features === void 0 ? void 0 : features.length) {
5174
+ feature = features[0];
5175
+ }
5176
+ }
5177
+ // Click on empty space: clear the previous result
5178
+ if (!feature) {
5179
+ this._clearQueryResult();
5180
+ return;
5181
+ }
5182
+ this._showQueryResult(feature, layer, evt.coordinate);
5183
+ });
5184
+ }
5185
+ /**
5186
+ * Highlight the given feature and attach a popup with its attributes.
5187
+ * @param feature
5188
+ * @param layer
5189
+ * @param coordinate
5190
+ * @private
5191
+ */
5192
+ _showQueryResult(feature, layer, coordinate) {
5193
+ this._clearQueryResult();
5194
+ this._highlightSource.addFeature(feature);
5195
+ this._queryOverlay = new QueryOverlay(feature, coordinate, layer);
5196
+ // @ts-expect-error
5197
+ this._queryOverlay.on('close', () => {
5198
+ this._clearQueryResult();
5199
+ });
5200
+ this._map.addOverlay(this._queryOverlay);
5201
+ }
5202
+ /**
5203
+ * Remove the query popup overlay if any.
5204
+ * @private
5205
+ */
5206
+ _removeQueryOverlay() {
5207
+ if (this._queryOverlay) {
5208
+ this._map.removeOverlay(this._queryOverlay);
5209
+ this._queryOverlay = null;
5210
+ return;
5211
+ }
5212
+ const overlay = this._map.getOverlayById(this._queryOverlayId);
5213
+ if (overlay) {
5214
+ this._map.removeOverlay(overlay);
5215
+ }
5216
+ }
5217
+ /**
5218
+ * Clear the highlight and remove the query popup.
5219
+ * @private
5220
+ */
5221
+ _clearQueryResult() {
5222
+ var _a;
5223
+ (_a = this._highlightSource) === null || _a === void 0 ? void 0 : _a.clear();
5224
+ this._removeQueryOverlay();
5225
+ }
5226
+ /**
5227
+ * Activate/deactivate the query mode: while it is active, clicking a
5228
+ * feature of the active layer shows its info in a popup instead of
5229
+ * selecting it for editing.
5230
+ *
5231
+ * @param bool
5232
+ * @public
5233
+ */
5234
+ activateQueryMode(bool = true) {
5235
+ var _a, _b;
5236
+ if (bool) {
5237
+ // Leave any draw interaction active
5238
+ if (this._interactionDraw) {
5239
+ this._map.removeInteraction(this._interactionDraw);
5240
+ this._viewport.classList.remove('draw-mode');
5241
+ }
5242
+ resetStateButtons();
5243
+ deactivateSelectModeButton();
5244
+ activateQueryButton();
5245
+ this._clearHoverState();
5246
+ activateMode(Modes.Query);
5247
+ // Disable the edit selection interactions while querying
5248
+ if (this._interactionSelectModify) {
5249
+ this._interactionSelectModify.setActive(false);
5250
+ }
5251
+ if (this._interactionWfsSelect) {
5252
+ this._interactionWfsSelect.setActive(false);
5253
+ }
5254
+ (_a = this._interactionModify) === null || _a === void 0 ? void 0 : _a.setActive(false);
5255
+ this._syncExtendLineOverlays();
5256
+ this._refreshSelectionInteractions();
5257
+ }
5258
+ else {
5259
+ activateMode(null);
5260
+ deactivateQueryButton();
5261
+ activateSelectModeButton(getSelectionMode());
5262
+ if (this._interactionSelectModify) {
5263
+ this._interactionSelectModify.setActive(true);
5264
+ }
5265
+ if (this._interactionWfsSelect) {
5266
+ this._interactionWfsSelect.setActive(true);
5267
+ }
5268
+ (_b = this._interactionModify) === null || _b === void 0 ? void 0 : _b.setActive(true);
5269
+ this._refreshSelectionInteractions();
5270
+ }
5271
+ this._clearQueryResult();
5272
+ }
4424
5273
  /**
4425
5274
  * @private
4426
5275
  */
@@ -4435,16 +5284,74 @@
4435
5284
  return;
4436
5285
  }
4437
5286
  if (key === 'Delete') {
4438
- const selectedFeatures = this._collectionModify;
4439
- if (selectedFeatures) {
4440
- selectedFeatures.forEach((feature) => {
4441
- this._deleteFeature(feature, true);
4442
- });
5287
+ const selectedFeatures = this._collectionModify.getArray();
5288
+ if (selectedFeatures.length) {
5289
+ this._deleteFeature(selectedFeatures, true);
4443
5290
  }
4444
5291
  }
4445
5292
  });
4446
5293
  };
4447
5294
  keyboardEvents();
5295
+ /**
5296
+ * Change the cursor to a pointing hand and highlight the feature
5297
+ * when hovering over a WFS feature of the active layer.
5298
+ * @private
5299
+ */
5300
+ const pointerMoveHandler = (evt) => {
5301
+ if (evt.dragging) {
5302
+ return;
5303
+ }
5304
+ // In edit mode, give pointer feedback on the editable
5305
+ // vertices/segments
5306
+ if (getMode() === Modes.Edit) {
5307
+ this._clearHoverState();
5308
+ this._updateEditCursor(evt);
5309
+ return;
5310
+ }
5311
+ // Only show the hover outside edit and draw modes
5312
+ if (getMode() !== null) {
5313
+ this._clearHoverState();
5314
+ return;
5315
+ }
5316
+ const layer = getActiveLayerToInsertEls();
5317
+ // Hover only works on visible WFS layers
5318
+ if (!layer ||
5319
+ !layer.getVisible() ||
5320
+ !layer.isVisibleByZoom() ||
5321
+ !(layer instanceof WfsLayer)) {
5322
+ this._clearHoverState();
5323
+ return;
5324
+ }
5325
+ const features = this._map.getFeaturesAtPixel(evt.pixel, {
5326
+ hitTolerance: 10,
5327
+ layerFilter: (candidate) => candidate === layer
5328
+ });
5329
+ if (features && features.length) {
5330
+ const feature = features[0];
5331
+ if (feature !== this._hoveredFeature) {
5332
+ this._clearHoverState();
5333
+ this._hoveredFeature = feature;
5334
+ this._hoveredFeature.setStyle((f) => styleFunction(f, true));
5335
+ }
5336
+ if (this._viewport.style.cursor !== 'pointer') {
5337
+ this._viewport.style.cursor = 'pointer';
5338
+ }
5339
+ }
5340
+ else {
5341
+ this._clearHoverState();
5342
+ }
5343
+ };
5344
+ this._map.on('pointermove', pointerMoveHandler);
5345
+ // Store the shift state when a gesture starts, so the freehand
5346
+ // selection can decide between adding or replacing the selection
5347
+ this._viewport.addEventListener('pointerdown', (evt) => {
5348
+ this._shiftPressed = evt.shiftKey;
5349
+ });
5350
+ // Clear the hover before firing a click, otherwise the Select interaction
5351
+ // would capture the hover style as the original style of the feature and
5352
+ // restore it on deselect, leaving the highlight permanently enabled.
5353
+ this._viewport.addEventListener('pointerdown', () => this._clearHoverState());
5354
+ this._viewport.addEventListener('pointerleave', () => this._clearHoverState(), { once: false });
4448
5355
  this._map.on('moveend', () => {
4449
5356
  this._currentZoom = this._view.getZoom();
4450
5357
  if (this._currentZoom !== this._lastZoom) {
@@ -4468,6 +5375,62 @@
4468
5375
  }
4469
5376
  });
4470
5377
  }
5378
+ /**
5379
+ * Allow panning with the middle mouse button at all times, even when the
5380
+ * draw/freehand/box interactions are consuming the left button. The
5381
+ * middle pointerdown is intercepted in the capture phase so OpenLayers
5382
+ * never processes it and the active tool is not affected.
5383
+ *
5384
+ * @private
5385
+ */
5386
+ _addMiddleButtonPan() {
5387
+ const viewport = this._viewport;
5388
+ let panning = false;
5389
+ let lastX = 0;
5390
+ let lastY = 0;
5391
+ const isMiddleButton = (evt) => evt.pointerType === 'mouse' && evt.button === 1;
5392
+ const stopPanning = () => {
5393
+ panning = false;
5394
+ };
5395
+ viewport.addEventListener('pointerdown', (evt) => {
5396
+ if (!isMiddleButton(evt)) {
5397
+ return;
5398
+ }
5399
+ evt.preventDefault();
5400
+ evt.stopPropagation();
5401
+ viewport.setPointerCapture(evt.pointerId);
5402
+ panning = true;
5403
+ lastX = evt.clientX;
5404
+ lastY = evt.clientY;
5405
+ if (this._view.getAnimating()) {
5406
+ this._view.cancelAnimations();
5407
+ }
5408
+ }, true);
5409
+ viewport.addEventListener('pointermove', (evt) => {
5410
+ if (!panning) {
5411
+ return;
5412
+ }
5413
+ evt.preventDefault();
5414
+ const delta = [lastX - evt.clientX, evt.clientY - lastY];
5415
+ lastX = evt.clientX;
5416
+ lastY = evt.clientY;
5417
+ const resolution = this._view.getResolution();
5418
+ const rotation = this._view.getRotation();
5419
+ const cosAngle = Math.cos(rotation);
5420
+ const sinAngle = Math.sin(rotation);
5421
+ const x = (delta[0] * cosAngle - delta[1] * sinAngle) * resolution;
5422
+ const y = (delta[1] * cosAngle + delta[0] * sinAngle) * resolution;
5423
+ this._view.adjustCenterInternal([x, y]);
5424
+ });
5425
+ const endPan = () => {
5426
+ if (!panning) {
5427
+ return;
5428
+ }
5429
+ stopPanning();
5430
+ };
5431
+ viewport.addEventListener('pointerup', endPan);
5432
+ viewport.addEventListener('pointercancel', endPan);
5433
+ }
4471
5434
  /**
4472
5435
  * Add map handlers
4473
5436
  * @private
@@ -4477,13 +5440,19 @@
4477
5440
  // This prevent events fired on select and deselect features that has no changes and should
4478
5441
  // not be updated in the geoserver
4479
5442
  this._interactionModify.on('modifyend', (evt) => {
4480
- const feature = evt.features.item(0);
4481
- addFeatureToEditedList(feature);
5443
+ evt.features.forEach((feature) => {
5444
+ addFeatureToEditedList(feature);
5445
+ });
4482
5446
  super.dispatchEvent(evt);
4483
5447
  });
4484
5448
  this._interactionModify.on('modifystart', (evt) => {
4485
5449
  super.dispatchEvent(evt);
4486
5450
  });
5451
+ // Keep the "continue drawing" buttons glued to the line endpoints
5452
+ // @ts-expect-error - 'modify' is a valid Modify event not typed in the `on` signature
5453
+ this._interactionModify.on('modify', () => {
5454
+ this._updateExtendOverlayPositions();
5455
+ });
4487
5456
  this._onDeselectFeatureEvent();
4488
5457
  this._onRemoveFeatureEvent();
4489
5458
  }
@@ -4495,6 +5464,9 @@
4495
5464
  this._layersControl = new LayersControl(this._options.showUpload ? this._uploads : null, this._options.uploadFormats);
4496
5465
  // @ts-expect-error
4497
5466
  this._layersControl.on('drawMode', () => {
5467
+ if (getMode() === Modes.Query) {
5468
+ this.activateQueryMode(false);
5469
+ }
4498
5470
  if (getMode() === Modes.Draw) {
4499
5471
  resetStateButtons();
4500
5472
  this.activateEditMode();
@@ -4521,9 +5493,37 @@
4521
5493
  this.activateDrawMode(getActiveLayerToInsertEls());
4522
5494
  }
4523
5495
  });
5496
+ [
5497
+ ['selectModeSingle', SelectionMode.Single],
5498
+ ['selectModeBox', SelectionMode.Box],
5499
+ ['selectModeFreehand', SelectionMode.Freehand]
5500
+ ].forEach(([evtName, mode]) => {
5501
+ // @ts-expect-error
5502
+ this._layersControl.on(evtName, () => {
5503
+ // Leaving the draw mode when a select tool is chosen
5504
+ if (getMode() === Modes.Draw) {
5505
+ resetStateButtons();
5506
+ this.activateEditMode();
5507
+ }
5508
+ // Leaving the query mode when a select tool is chosen
5509
+ if (getMode() === Modes.Query) {
5510
+ this.activateQueryMode(false);
5511
+ }
5512
+ setSelectionMode(mode);
5513
+ activateSelectModeButton(mode);
5514
+ this._refreshSelectionInteractions();
5515
+ });
5516
+ });
5517
+ // @ts-expect-error
5518
+ this._layersControl.on('queryMode', () => {
5519
+ this.activateQueryMode(getMode() !== Modes.Query);
5520
+ });
4524
5521
  const controlEl = this._layersControl.render();
4525
5522
  this._selectDraw = controlEl.querySelector('.wfst--tools-control--select-draw');
4526
- this._controlWidgetToolsDiv.append(controlEl);
5523
+ this._controlWidgetToolsDiv.append(controlEl.querySelector('.wfst--tools-control--head'));
5524
+ if (this._options.layers.length > 1) {
5525
+ this._layersWidgetDiv.append(controlEl.querySelector('.wfst--tools-control--select-layers'));
5526
+ }
4527
5527
  }
4528
5528
  /**
4529
5529
  *
@@ -4542,6 +5542,9 @@
4542
5542
  _restoreFeatureToLayer(feature, layerName) {
4543
5543
  layerName = layerName || feature.get('_layerName_');
4544
5544
  const layer = getStoredMapLayers()[layerName];
5545
+ // Reset any per-feature style (e.g. hover) before returning the feature
5546
+ // to its layer, so it renders with the layer's default style.
5547
+ feature.setStyle(undefined);
4545
5548
  layer.getSource().addFeature(feature);
4546
5549
  }
4547
5550
  /**
@@ -4575,10 +5578,18 @@
4575
5578
  this._removeFeatureFromTmpLayer(feature);
4576
5579
  }
4577
5580
  };
5581
+ if (this._keySelectBound) {
5582
+ return;
5583
+ }
5584
+ this._keySelectBound = true;
4578
5585
  // This is fired when a feature is deselected and fires the transaction process
5586
+ if (this._keySelect) {
5587
+ Observable.unByKey(this._keySelect);
5588
+ }
4579
5589
  this._keySelect = this._collectionModify.on('remove', (evt) => {
4580
5590
  const feature = evt.element;
4581
5591
  this._deselectEditFeature(feature);
5592
+ this._syncEditOverlays();
4582
5593
  checkIfFeatureIsChanged(feature);
4583
5594
  this._editModeOff();
4584
5595
  });
@@ -4599,6 +5610,7 @@
4599
5610
  }
4600
5611
  if (this._keySelect) {
4601
5612
  Observable.unByKey(this._keySelect);
5613
+ this._keySelectBound = false;
4602
5614
  }
4603
5615
  const layerName = feature.get('_layerName_');
4604
5616
  const ll = this.getLayerByName(layerName);
@@ -4614,27 +5626,52 @@
4614
5626
  }
4615
5627
  /**
4616
5628
  *
4617
- * @param feature
5629
+ * @param features
4618
5630
  * @private
4619
5631
  */
4620
- _editModeOn(feature) {
4621
- this._editFeatureOriginal = feature.clone();
5632
+ _editModeOn(features) {
5633
+ features = Array.isArray(features) ? features : [features];
5634
+ features.forEach((feature) => {
5635
+ this._editFeaturesOriginal[String(feature.getId())] =
5636
+ feature.clone();
5637
+ });
4622
5638
  activateMode(Modes.Edit);
5639
+ // Show the "continue drawing" buttons on the line endpoints
5640
+ this._syncExtendLineOverlays();
4623
5641
  // To refresh the style
4624
5642
  getEditLayer().getSource().changed();
4625
- this._removeOverlayHelper(feature);
4626
- this._controlApplyDiscardChanges = new EditControlChangesEl(feature);
4627
- this._controlApplyDiscardChanges.on('cancel', ({ feature }) => {
4628
- feature.setGeometry(this._editFeatureOriginal.getGeometry());
4629
- removeFeatureFromEditList(feature);
4630
- this._collectionModify.remove(feature);
5643
+ this._refreshSelectionInteractions();
5644
+ const multiOverlay = this._map.getOverlayById(this._multiOverlayId);
5645
+ if (multiOverlay) {
5646
+ this._map.removeOverlay(multiOverlay);
5647
+ }
5648
+ features.forEach((feature) => {
5649
+ this._removeOverlayHelper(feature);
5650
+ });
5651
+ this._controlApplyDiscardChanges = new EditControlChangesEl(features);
5652
+ this._controlApplyDiscardChanges.on('cancel', (evt) => {
5653
+ const list = evt.features;
5654
+ Array.from(list).forEach((feature) => {
5655
+ const original = this._editFeaturesOriginal[String(feature.getId())];
5656
+ if (original) {
5657
+ feature.setGeometry(original.getGeometry());
5658
+ }
5659
+ removeFeatureFromEditList(feature);
5660
+ this._collectionModify.remove(feature);
5661
+ });
4631
5662
  });
4632
- this._controlApplyDiscardChanges.on('apply', ({ feature }) => {
5663
+ this._controlApplyDiscardChanges.on('apply', async (evt) => {
4633
5664
  showLoading();
4634
- this._collectionModify.remove(feature);
5665
+ const list = evt.features;
5666
+ const ok = await this._transactEditList(list);
5667
+ if (ok) {
5668
+ Array.from(list).forEach((feature) => {
5669
+ this._collectionModify.remove(feature);
5670
+ });
5671
+ }
4635
5672
  });
4636
- this._controlApplyDiscardChanges.on('delete', ({ feature }) => {
4637
- this._deleteFeature(feature, true);
5673
+ this._controlApplyDiscardChanges.on('delete', (evt) => {
5674
+ this._deleteFeature(evt.features, true);
4638
5675
  });
4639
5676
  this._map.addControl(this._controlApplyDiscardChanges);
4640
5677
  }
@@ -4643,30 +5680,331 @@
4643
5680
  */
4644
5681
  _editModeOff() {
4645
5682
  activateMode(null);
5683
+ this._destroyExtendLineOverlays();
4646
5684
  this._map.removeControl(this._controlApplyDiscardChanges);
5685
+ this._refreshSelectionInteractions();
5686
+ }
5687
+ /**
5688
+ * Get the first and last coordinate of every line component of a
5689
+ * LineString/MultiLineString geometry, including the line components
5690
+ * nested inside a GeometryCollection. Anything else returns an empty
5691
+ * array.
5692
+ *
5693
+ * @param geometry
5694
+ * @private
5695
+ */
5696
+ _getLineEndPoints(geometry) {
5697
+ const endPoints = [];
5698
+ const appendLine = (line, collection) => {
5699
+ if (line instanceof LineString) {
5700
+ if (line.getCoordinates().length) {
5701
+ endPoints.push({
5702
+ first: line.getFirstCoordinate(),
5703
+ last: line.getLastCoordinate(),
5704
+ line,
5705
+ lineIndex: 0,
5706
+ collection
5707
+ });
5708
+ }
5709
+ return;
5710
+ }
5711
+ line.getCoordinates().forEach((coordinates, lineIndex) => {
5712
+ if (coordinates.length) {
5713
+ endPoints.push({
5714
+ first: coordinates[0],
5715
+ last: coordinates[coordinates.length - 1],
5716
+ line,
5717
+ lineIndex,
5718
+ collection
5719
+ });
5720
+ }
5721
+ });
5722
+ };
5723
+ // "getGeometries()" returns deep clones (see ol GeometryCollection),
5724
+ // but the extend/merge must work on the actual members.
5725
+ const collect = (geometry, collection) => {
5726
+ if (geometry instanceof LineString ||
5727
+ geometry instanceof MultiLineString) {
5728
+ appendLine(geometry, collection);
5729
+ }
5730
+ else if (geometry instanceof GeometryCollection) {
5731
+ geometry.getGeometriesArray().forEach((member) => {
5732
+ collect(member, geometry);
5733
+ });
5734
+ }
5735
+ };
5736
+ collect(geometry);
5737
+ return endPoints;
5738
+ }
5739
+ /**
5740
+ * Remove every "continue drawing" overlay and abort any active line
5741
+ * extension draw.
5742
+ *
5743
+ * @private
5744
+ */
5745
+ _destroyExtendLineOverlays() {
5746
+ if (this._extendDraw) {
5747
+ this._finishExtendDraw();
5748
+ }
5749
+ this._extendOverlays.forEach((overlay) => {
5750
+ this._map.removeOverlay(overlay);
5751
+ });
5752
+ this._extendOverlays = [];
5753
+ }
5754
+ /**
5755
+ * Keep the "continue drawing" overlays in sync with the current edit
5756
+ * selection: they are only shown while editing LineString/MultiLineString
5757
+ * features, and they are repositioned on the endpoints of each line
5758
+ * component.
5759
+ *
5760
+ * @private
5761
+ */
5762
+ _syncExtendLineOverlays() {
5763
+ if (getMode() !== Modes.Edit || !this._collectionModify) {
5764
+ this._destroyExtendLineOverlays();
5765
+ return;
5766
+ }
5767
+ const items = this._collectionModify.getArray();
5768
+ // Remove overlays whose feature was deselected or lost its line geometry
5769
+ const removed = [];
5770
+ this._extendOverlays.forEach((overlay) => {
5771
+ const stale = !items.includes(overlay.feature) ||
5772
+ overlay.component >=
5773
+ this._getLineEndPoints(overlay.feature.getGeometry())
5774
+ .length;
5775
+ if (stale) {
5776
+ this._map.removeOverlay(overlay);
5777
+ removed.push(overlay);
5778
+ }
5779
+ });
5780
+ this._extendOverlays = this._extendOverlays.filter((overlay) => !removed.includes(overlay));
5781
+ // Add overlays for new LineString/MultiLineString features
5782
+ items.forEach((feature) => {
5783
+ this._getLineEndPoints(feature.getGeometry()).forEach(({ first, last }, component) => {
5784
+ ['start', 'end'].forEach((side) => {
5785
+ const exists = this._extendOverlays.some((overlay) => overlay.feature === feature &&
5786
+ overlay.component === component &&
5787
+ overlay.side === side);
5788
+ if (exists) {
5789
+ return;
5790
+ }
5791
+ const overlay = new ExtendLineOverlay(feature, side === 'start' ? first : last, side, component, () => this._startExtendLineDraw(feature, side, component));
5792
+ this._extendOverlays.push(overlay);
5793
+ this._map.addOverlay(overlay);
5794
+ });
5795
+ });
5796
+ });
5797
+ this._updateExtendOverlayPositions();
4647
5798
  }
4648
5799
  /**
4649
- * Remove a feature from the edit Layer and from the Geoserver
5800
+ * Move the "continue drawing" overlays to the current position of their
5801
+ * line endpoints, e.g. after a vertex is dragged with the Modify
5802
+ * interaction.
5803
+ *
5804
+ * @private
5805
+ */
5806
+ _updateExtendOverlayPositions() {
5807
+ this._extendOverlays.forEach((overlay) => {
5808
+ const point = this._getLineEndPoints(overlay.feature.getGeometry())[overlay.component];
5809
+ if (!point) {
5810
+ return;
5811
+ }
5812
+ overlay.setPosition(overlay.side === 'start' ? point.first : point.last);
5813
+ });
5814
+ }
5815
+ /**
5816
+ * Start drawing a continuation of a line from one of its endpoints. The
5817
+ * sketch is anchored on the clicked vertex, so the next clicks append the
5818
+ * new points. A double click (or clicking back on the last point) ends
5819
+ * the drawing and merges the segment into the feature.
5820
+ *
5821
+ * @param feature
5822
+ * @param side 'start' prepends the new segment, 'end' appends it
5823
+ * @param component index of the clicked line component
5824
+ * @private
5825
+ */
5826
+ _startExtendLineDraw(feature, side, component = 0) {
5827
+ var _a;
5828
+ if (this._extendDraw || getMode() !== Modes.Edit) {
5829
+ return;
5830
+ }
5831
+ const points = this._getLineEndPoints(feature.getGeometry());
5832
+ if (!points.length || component >= points.length) {
5833
+ return;
5834
+ }
5835
+ const anchor = side === 'start' ? points[component].first : points[component].last;
5836
+ // While extending, the Modify/Select interactions must not grab the clicks
5837
+ (_a = this._interactionModify) === null || _a === void 0 ? void 0 : _a.setActive(false);
5838
+ if (this._interactionSelectModify) {
5839
+ this._interactionSelectModify.setActive(false);
5840
+ }
5841
+ if (this._interactionWfsSelect) {
5842
+ this._interactionWfsSelect.setActive(false);
5843
+ }
5844
+ this._extendDraw = new Draw({
5845
+ type: GeometryType.LineString,
5846
+ style: (f) => styleFunction(f),
5847
+ stopClick: true // To prevent firing a map/wms click
5848
+ });
5849
+ this._extendDraw.on('drawend', (evt) => {
5850
+ this._mergeExtendLine(feature, side, evt.feature, component);
5851
+ });
5852
+ this._extendDraw.on('drawabort', () => {
5853
+ this._finishExtendDraw();
5854
+ });
5855
+ this._map.addInteraction(this._extendDraw);
5856
+ // Anchor the sketch on the clicked vertex: the user only has to digitize
5857
+ // the continuation points (this also dispatches the "drawstart" event)
5858
+ this._extendDraw.appendCoordinates([anchor]);
5859
+ }
5860
+ /**
5861
+ * Merge the just-drawn segment into the edited feature's line geometry and
5862
+ * restore the editing interactions.
4650
5863
  *
4651
5864
  * @param feature
5865
+ * @param side
5866
+ * @param sketch
5867
+ * @param component index of the extended line component
5868
+ * @private
5869
+ */
5870
+ _mergeExtendLine(feature, side, sketch, component = 0) {
5871
+ const points = this._getLineEndPoints(feature.getGeometry());
5872
+ const point = points[component];
5873
+ if (!point) {
5874
+ this._finishExtendDraw();
5875
+ return;
5876
+ }
5877
+ const segment = sketch.getGeometry().getCoordinates();
5878
+ const { line, lineIndex } = point;
5879
+ if (line instanceof LineString) {
5880
+ const coords = line.getCoordinates();
5881
+ if (side === 'start') {
5882
+ line.setCoordinates([...segment.slice(1).reverse(), ...coords]);
5883
+ }
5884
+ else {
5885
+ line.setCoordinates([...coords, ...segment.slice(1)]);
5886
+ }
5887
+ }
5888
+ else if (line instanceof MultiLineString) {
5889
+ const coords = line.getCoordinates();
5890
+ if (side === 'start') {
5891
+ coords[lineIndex] = [
5892
+ ...segment.slice(1).reverse(),
5893
+ ...coords[lineIndex]
5894
+ ];
5895
+ }
5896
+ else {
5897
+ coords[lineIndex] = [...coords[lineIndex], ...segment.slice(1)];
5898
+ }
5899
+ line.setCoordinates(coords);
5900
+ }
5901
+ // Only mark the feature as edited when the segment added new vertices
5902
+ if (segment.length > 1) {
5903
+ addFeatureToEditedList(feature);
5904
+ }
5905
+ this._finishExtendDraw();
5906
+ this._syncExtendLineOverlays();
5907
+ }
5908
+ /**
5909
+ * Remove the active line extension draw and restore the interactions that
5910
+ * were disabled while drawing.
5911
+ *
5912
+ * @private
5913
+ */
5914
+ _finishExtendDraw() {
5915
+ var _a;
5916
+ if (this._extendDraw) {
5917
+ this._map.removeInteraction(this._extendDraw);
5918
+ this._extendDraw = null;
5919
+ }
5920
+ if (getMode() !== Modes.Edit) {
5921
+ return;
5922
+ }
5923
+ (_a = this._interactionModify) === null || _a === void 0 ? void 0 : _a.setActive(true);
5924
+ if (this._interactionSelectModify) {
5925
+ this._interactionSelectModify.setActive(true);
5926
+ }
5927
+ if (this._interactionWfsSelect) {
5928
+ this._interactionWfsSelect.setActive(true);
5929
+ }
5930
+ }
5931
+ /**
5932
+ * Send all the given features in a single WFS-T Update transaction per
5933
+ * layer, so a multi selection is saved with only one request. The "edited"
5934
+ * flag is cleared before transacting, so a deselect fired while the
5935
+ * request is in flight does not trigger a duplicate transaction.
5936
+ *
5937
+ * @param list
5938
+ * @returns false when there is nothing to transact or the transaction fails
5939
+ * @private
5940
+ */
5941
+ async _transactEditList(list) {
5942
+ if (!list.length) {
5943
+ return true;
5944
+ }
5945
+ const byLayer = {};
5946
+ list.forEach((feature) => {
5947
+ const layerName = feature.get('_layerName_');
5948
+ if (!byLayer[layerName]) {
5949
+ byLayer[layerName] = [];
5950
+ }
5951
+ byLayer[layerName].push(feature);
5952
+ });
5953
+ for (const layerName of Object.keys(byLayer)) {
5954
+ const layer = this._options.layers.find((layer) => layer.get(BaseLayerProperty.NAME) === layerName);
5955
+ if (!layer) {
5956
+ continue;
5957
+ }
5958
+ byLayer[layerName].forEach((feature) => {
5959
+ removeFeatureFromEditList(feature);
5960
+ });
5961
+ let ok;
5962
+ try {
5963
+ ok = await layer.transactFeatures(TransactionType.Update, byLayer[layerName]);
5964
+ }
5965
+ catch (err) {
5966
+ return false;
5967
+ }
5968
+ if (ok === false) {
5969
+ return false;
5970
+ }
5971
+ }
5972
+ return true;
5973
+ }
5974
+ /**
5975
+ * Remove features from the edit Layer and from the Geoserver
5976
+ *
5977
+ * @param feature
5978
+ * @param confirm
4652
5979
  * @private
4653
5980
  */
4654
5981
  _deleteFeature(feature, confirm) {
4655
5982
  const deleteEl = () => {
5983
+ var _a;
4656
5984
  const features = Array.isArray(feature) ? feature : [feature];
4657
5985
  features.forEach((feature) => {
4658
5986
  feature.set('_delete_', true, true);
4659
5987
  getEditLayer().getSource().removeFeature(feature);
4660
5988
  });
4661
5989
  this._collectionModify.clear();
4662
- const layerName = feature.get('_layerName_');
4663
- const layer = this._options.layers.find((layer) => layer.get(BaseLayerProperty.NAME) === layerName);
5990
+ this._editModeOff();
5991
+ this._syncEditOverlays();
5992
+ const layerName = (_a = features[0]) === null || _a === void 0 ? void 0 : _a.get('_layerName_');
5993
+ const layer = layerName
5994
+ ? this._options.layers.find((layer) => layer.get(BaseLayerProperty.NAME) === layerName)
5995
+ : null;
4664
5996
  if (layer instanceof WfsLayer) {
4665
- this._interactionWfsSelect.getFeatures().remove(feature);
5997
+ features.forEach((feature) => {
5998
+ this._interactionWfsSelect.getFeatures().remove(feature);
5999
+ });
4666
6000
  }
4667
6001
  };
4668
6002
  if (confirm) {
4669
- const confirmModal = Modal.confirm(I18N.labels.confirmDelete, Object.assign({}, this._options.modal));
6003
+ const features = Array.isArray(feature) ? feature : [feature];
6004
+ const message = features.length > 1
6005
+ ? I18N_('confirmDeleteElements', features.length)
6006
+ : I18N.labels.confirmDelete;
6007
+ const confirmModal = Modal.confirm(message, Object.assign({}, this._options.modal));
4670
6008
  confirmModal.show().once('dismiss', function (modal, ev, button) {
4671
6009
  if (button && button.value) {
4672
6010
  deleteEl();
@@ -4686,33 +6024,102 @@
4686
6024
  * @private
4687
6025
  */
4688
6026
  _addFeatureToEditMode(feature, coordinate = null, layerName = null) {
4689
- // For now, only allow one element at time
4690
- // @TODO: allow edit multiples elements
4691
- if (this._collectionModify.getLength())
4692
- return;
4693
6027
  if (layerName) {
4694
6028
  // Store the layer information inside the feature
4695
6029
  feature.set('_layerName_', layerName);
4696
6030
  }
6031
+ if (coordinate) {
6032
+ // Keep the click coordinate to position the overlay helper
6033
+ feature.set('_editOverlayCoord_', coordinate);
6034
+ }
4697
6035
  const props = feature ? feature.getProperties() : '';
4698
6036
  if (props) {
4699
6037
  if (feature.getGeometry()) {
4700
6038
  getEditLayer().getSource().addFeature(feature);
4701
6039
  this._collectionModify.push(feature);
4702
- const overlay = new EditOverlay(feature, coordinate);
4703
- // @ts-expect-error
4704
- overlay.on('editFields', () => {
4705
- this._editFields.show(feature);
4706
- });
4707
- // @ts-expect-error
4708
- overlay.on('editGeom', () => {
4709
- this._editModeOn(feature);
4710
- });
4711
- this._map.addOverlay(overlay);
4712
- const layer = getStoredLayer(layerName);
4713
- if (layer) {
4714
- layer.maybeLockFeature(feature.getId());
4715
- }
6040
+ this._syncEditOverlays();
6041
+ }
6042
+ }
6043
+ }
6044
+ /**
6045
+ * Lock the whole current selection with a single LockFeature request per
6046
+ * layer, so the transaction LockId covers all the selected features
6047
+ *
6048
+ * @private
6049
+ */
6050
+ async _lockSelectedFeatures() {
6051
+ const byLayer = {};
6052
+ this._collectionModify.getArray().forEach((selected) => {
6053
+ const layerName = selected.get('_layerName_');
6054
+ if (!layerName) {
6055
+ return;
6056
+ }
6057
+ (byLayer[layerName] || (byLayer[layerName] = [])).push(selected.getId());
6058
+ });
6059
+ const locks = [];
6060
+ Object.keys(byLayer).forEach((layerName) => {
6061
+ const layer = getStoredLayer(layerName);
6062
+ if (layer) {
6063
+ locks.push(layer.maybeLockFeature(byLayer[layerName]));
6064
+ }
6065
+ });
6066
+ if (locks.length) {
6067
+ await Promise.all(locks);
6068
+ }
6069
+ }
6070
+ /**
6071
+ * Create an EditOverlay helper and attach the edit listeners
6072
+ *
6073
+ * @param feature
6074
+ * @param coordinate
6075
+ * @param id
6076
+ * @private
6077
+ */
6078
+ _createEditOverlay(feature = null, coordinate = null, id = null) {
6079
+ coordinate =
6080
+ coordinate || (feature ? feature.get('_editOverlayCoord_') : null);
6081
+ const overlay = new EditOverlay(feature, coordinate, id);
6082
+ // @ts-expect-error
6083
+ overlay.on('editFields', () => {
6084
+ this._editFields.show(this._collectionModify.getArray());
6085
+ });
6086
+ // @ts-expect-error
6087
+ overlay.on('editGeom', () => {
6088
+ this._editModeOn(this._collectionModify.getArray());
6089
+ });
6090
+ this._map.addOverlay(overlay);
6091
+ }
6092
+ /**
6093
+ * Sync the edit overlay helpers with the current selection: a single
6094
+ * per-feature overlay while a feature is selected, or a unique overlay
6095
+ * centered between all the selected features when editing several at once
6096
+ *
6097
+ * @private
6098
+ */
6099
+ _syncEditOverlays() {
6100
+ if (getMode() === Modes.Edit) {
6101
+ return;
6102
+ }
6103
+ const multiOverlay = this._map.getOverlayById(this._multiOverlayId);
6104
+ if (multiOverlay) {
6105
+ this._map.removeOverlay(multiOverlay);
6106
+ }
6107
+ const items = this._collectionModify.getArray();
6108
+ if (items.length > 1) {
6109
+ items.forEach((feature) => {
6110
+ this._removeOverlayHelper(feature);
6111
+ });
6112
+ const extent = extent_js.createEmpty();
6113
+ items.forEach((feature) => {
6114
+ extent_js.extend(extent, feature.getGeometry().getExtent());
6115
+ });
6116
+ this._createEditOverlay(null, extent_js.getCenter(extent), this._multiOverlayId);
6117
+ }
6118
+ else if (items.length === 1) {
6119
+ const feature = items[0];
6120
+ const featureId = feature.getId();
6121
+ if (featureId && !this._map.getOverlayById(featureId)) {
6122
+ this._createEditOverlay(feature);
4716
6123
  }
4717
6124
  }
4718
6125
  }
@@ -4759,15 +6166,20 @@
4759
6166
  if (!layer.getVisible()) {
4760
6167
  return;
4761
6168
  }
6169
+ // Clear hover style while drawing
6170
+ this._clearHoverState();
4762
6171
  activateDrawButton();
6172
+ deactivateSelectModeButton();
4763
6173
  this._viewport.classList.add('draw-mode');
4764
6174
  addDrawInteraction(layer);
4765
6175
  }
4766
6176
  else {
4767
6177
  this._map.removeInteraction(this._interactionDraw);
4768
6178
  this._viewport.classList.remove('draw-mode');
6179
+ activateSelectModeButton(getSelectionMode());
4769
6180
  }
4770
6181
  activateMode(layer ? Modes.Draw : null);
6182
+ this._refreshSelectionInteractions();
4771
6183
  }
4772
6184
  /**
4773
6185
  * Activate/desactivate the edit mode
@@ -4783,6 +6195,8 @@
4783
6195
  else {
4784
6196
  // Deselct features
4785
6197
  this._collectionModify.clear();
6198
+ // Clear hover state when leaving the edit mode
6199
+ this._clearHoverState();
4786
6200
  }
4787
6201
  if (this._interactionSelectModify) {
4788
6202
  this._interactionSelectModify.setActive(bool);
@@ -4790,6 +6204,61 @@
4790
6204
  this._interactionModify.setActive(bool);
4791
6205
  if (this._interactionWfsSelect)
4792
6206
  this._interactionWfsSelect.setActive(bool);
6207
+ this._refreshSelectionInteractions();
6208
+ }
6209
+ /**
6210
+ * Reset the cursor and remove the hover style of the last hovered feature
6211
+ * @private
6212
+ */
6213
+ _clearHoverState() {
6214
+ if (this._viewport) {
6215
+ this._viewport.style.cursor = '';
6216
+ }
6217
+ if (this._hoveredFeature) {
6218
+ this._hoveredFeature.setStyle(undefined);
6219
+ this._hoveredFeature = null;
6220
+ }
6221
+ }
6222
+ /**
6223
+ * Change the map cursor while in edit mode: `move` when the pointer is
6224
+ * over an editable vertex and `copy` when it is over a segment (where a
6225
+ * new vertex can be inserted), mirroring the Modify interaction hit
6226
+ * tolerance.
6227
+ *
6228
+ * @param evt
6229
+ * @private
6230
+ */
6231
+ _updateEditCursor(evt) {
6232
+ if (!this._collectionModify) {
6233
+ return;
6234
+ }
6235
+ const tolerance = 12;
6236
+ const toleranceSq = tolerance * tolerance;
6237
+ const pixel = evt.pixel;
6238
+ for (const feature of this._collectionModify.getArray()) {
6239
+ const vertices = getFeatureVertices(feature);
6240
+ const coordinates = vertices ? vertices.getCoordinates() : [];
6241
+ if (!coordinates.length) {
6242
+ continue;
6243
+ }
6244
+ // Hovering an existing vertex (draggable handle)
6245
+ for (const coordinate of coordinates) {
6246
+ const vertexPixel = this._map.getPixelFromCoordinate(coordinate);
6247
+ if (coordinate_js.squaredDistance(vertexPixel, pixel) <= toleranceSq) {
6248
+ this._viewport.style.cursor = 'move';
6249
+ return;
6250
+ }
6251
+ }
6252
+ // Hovering a segment (allows inserting a new vertex)
6253
+ for (let i = 0; i < coordinates.length - 1; i++) {
6254
+ const start = this._map.getPixelFromCoordinate(coordinates[i]);
6255
+ const end = this._map.getPixelFromCoordinate(coordinates[i + 1]);
6256
+ if (coordinate_js.squaredDistanceToSegment(pixel, [start, end]) <= toleranceSq) {
6257
+ this._viewport.style.cursor = 'copy';
6258
+ return;
6259
+ }
6260
+ }
6261
+ }
4793
6262
  }
4794
6263
  /**
4795
6264
  * Remove the overlay helper atttached to a specify feature