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/lib/ol-wfst.js CHANGED
@@ -1,27 +1,33 @@
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:22 GMT-0300 (Argentina Standard Time)
4
+ * Built: Thu Sep 10 2026 23:06:22 GMT-0300 (Argentina Standard Time)
5
5
  */
6
- import CircleStyle from 'ol/style/Circle.js';
7
6
  import Fill$1 from 'ol/style/Fill.js';
8
7
  import Stroke$1 from 'ol/style/Stroke.js';
9
8
  import Style$1 from 'ol/style/Style.js';
10
9
  import Control from 'ol/control/Control.js';
10
+ import FullScreen from 'ol/control/FullScreen.js';
11
11
  import Draw from 'ol/interaction/Draw.js';
12
+ import DragBox from 'ol/interaction/DragBox.js';
12
13
  import Modify from 'ol/interaction/Modify.js';
13
14
  import Select from 'ol/interaction/Select.js';
14
15
  import Snap from 'ol/interaction/Snap.js';
16
+ import LineString from 'ol/geom/LineString.js';
17
+ import MultiLineString$1 from 'ol/geom/MultiLineString.js';
18
+ import GeometryCollection$1 from 'ol/geom/GeometryCollection.js';
15
19
  import Collection from 'ol/Collection.js';
20
+ import VectorLayer from 'ol/layer/Vector.js';
21
+ import VectorSource, { VectorSourceEvent } from 'ol/source/Vector.js';
16
22
  import BaseEvent from 'ol/events/Event.js';
17
- import { primaryAction, never } from 'ol/events/condition.js';
23
+ import { primaryAction, shiftKeyOnly } from 'ol/events/condition.js';
18
24
  import Observable, { unByKey } from 'ol/Observable.js';
25
+ import { squaredDistance, squaredDistanceToSegment } from 'ol/coordinate.js';
26
+ import { getCenter, createEmpty, extend } from 'ol/extent.js';
19
27
  import Modal from 'modal-vanilla';
20
- import VectorLayer from 'ol/layer/Vector.js';
21
28
  import { Mixin } from 'ts-mixer';
22
29
  import Layer from 'ol/layer/Base.js';
23
30
  import GeoJSON from 'ol/format/GeoJSON.js';
24
- import VectorSource, { VectorSourceEvent } from 'ol/source/Vector.js';
25
31
  import { transformExtent } from 'ol/proj.js';
26
32
  import { bbox } from 'ol/loadingstrategy.js';
27
33
  import TileLayer from 'ol/layer/Tile.js';
@@ -33,11 +39,9 @@ import WFS from 'ol/format/WFS.js';
33
39
  import { Style, Stroke, Fill, Circle } from 'ol/style.js';
34
40
  import BaseObject from 'ol/Object.js';
35
41
  import Circle$1 from 'ol/geom/Circle.js';
36
- import GeometryCollection$1 from 'ol/geom/GeometryCollection.js';
37
42
  import Feature from 'ol/Feature.js';
38
43
  import { fromCircle } from 'ol/geom/Polygon.js';
39
44
  import WFSCapabilities from 'ol-wfs-capabilities';
40
- import { getCenter } from 'ol/extent.js';
41
45
  import Overlay from 'ol/Overlay.js';
42
46
 
43
47
  // External
@@ -83,6 +87,10 @@ const showError = (msg, originalError = null, layerName = '') => {
83
87
  const es = {
84
88
  labels: {
85
89
  select: 'Seleccionar',
90
+ selectBox: 'Seleccionar elementos dibujando una caja',
91
+ selectFreehand: 'Seleccionar elementos dibujando a mano alzada',
92
+ query: 'Consultar información del elemento',
93
+ featureInfo: 'Información del elemento',
86
94
  addElement: 'Modo dibujo',
87
95
  editElement: 'Editar elemento',
88
96
  save: 'Guardar',
@@ -92,9 +100,11 @@ const es = {
92
100
  upload: 'Subir',
93
101
  editMode: 'Modo Edición',
94
102
  confirmDelete: '¿Estás seguro de borrar el elemento?',
103
+ confirmDeleteElements: '¿Borrar los {} elementos?',
95
104
  geomTypeNotSupported: 'Geometría no compatible con la capa',
96
105
  editFields: 'Editar campos',
97
106
  editGeom: 'Editar geometría',
107
+ continueLine: 'Continuar dibujando desde aquí',
98
108
  selectDrawType: 'Tipo de geometría para dibujar',
99
109
  uploadToLayer: 'Subir archivo a la capa seleccionada',
100
110
  uploadFeatures: 'Subida de elementos a la capa',
@@ -102,7 +112,12 @@ const es = {
102
112
  invalidFeatures: 'Invalidas',
103
113
  loading: 'Cargando...',
104
114
  toggleVisibility: 'Cambiar visibilidad de la capa',
105
- close: 'Cerrar'
115
+ fullscreen: 'Pantalla completa',
116
+ close: 'Cerrar',
117
+ multipleValues: 'Valores múltiples',
118
+ editElements: 'Editar elementos ({})',
119
+ multipleEditNotice: 'Edición múltiple activada. Los cambios se aplicarán a los {} elementos seleccionados.',
120
+ geoserverLayers: 'Capas de GeoServer'
106
121
  },
107
122
  errors: {
108
123
  capabilities: 'No se pudieron obtener las Capabilidades del GeoServer',
@@ -123,6 +138,10 @@ const es = {
123
138
  const en = {
124
139
  labels: {
125
140
  select: 'Select',
141
+ selectBox: 'Select features by drawing a box',
142
+ selectFreehand: 'Select features by freehand drawing',
143
+ query: 'Query feature info',
144
+ featureInfo: 'Feature info',
126
145
  addElement: 'Toggle Draw mode',
127
146
  editElement: 'Edit feature',
128
147
  save: 'Save',
@@ -132,9 +151,11 @@ const en = {
132
151
  upload: 'Upload',
133
152
  editMode: 'Edit Mode',
134
153
  confirmDelete: 'Are you sure to delete the feature?',
154
+ confirmDeleteElements: 'Delete the {} selected features?',
135
155
  geomTypeNotSupported: 'Geometry not supported by layer',
136
156
  editFields: 'Edit fields',
137
157
  editGeom: 'Edit geometry',
158
+ continueLine: 'Continue drawing from here',
138
159
  selectDrawType: 'Geometry type to draw',
139
160
  uploadToLayer: 'Upload file to selected layer',
140
161
  uploadFeatures: 'Uploaded features to layer',
@@ -142,7 +163,12 @@ const en = {
142
163
  invalidFeatures: 'Invalid',
143
164
  loading: 'Loading...',
144
165
  toggleVisibility: 'Toggle layer visibility',
145
- close: 'Close'
166
+ fullscreen: 'Fullscreen',
167
+ close: 'Close',
168
+ multipleValues: 'Multiple values',
169
+ editElements: 'Edit features ({})',
170
+ multipleEditNotice: 'Multi-edit active. Changes will be applied to the {} selected features.',
171
+ geoserverLayers: 'Geoserver Layers'
146
172
  },
147
173
  errors: {
148
174
  capabilities: 'GeoServer Capabilities could not be downloaded.',
@@ -163,6 +189,10 @@ const en = {
163
189
  const zh = {
164
190
  labels: {
165
191
  select: '选择',
192
+ selectBox: '框选元素',
193
+ selectFreehand: '手绘选择元素',
194
+ query: '查询元素信息',
195
+ featureInfo: '元素信息',
166
196
  addElement: '切换绘图类型',
167
197
  editElement: '编辑元素',
168
198
  save: '保存',
@@ -172,9 +202,11 @@ const zh = {
172
202
  upload: '上传',
173
203
  editMode: '编辑模式',
174
204
  confirmDelete: '确认删除元素?',
205
+ confirmDeleteElements: '确认删除选中的 {} 个元素?',
175
206
  geomTypeNotSupported: '图层不支持该几何',
176
207
  editFields: '编辑区域',
177
208
  editGeom: '编辑几何',
209
+ continueLine: '从此处继续绘制',
178
210
  selectDrawType: '几何类型',
179
211
  uploadToLayer: '通过文件上传图层',
180
212
  uploadFeatures: '上传元素到图层',
@@ -182,7 +214,12 @@ const zh = {
182
214
  invalidFeatures: '不合法',
183
215
  loading: '加载中...',
184
216
  toggleVisibility: '切换图层透明度',
185
- close: '关闭'
217
+ fullscreen: '全屏',
218
+ close: '关闭',
219
+ multipleValues: '多个值',
220
+ editElements: '编辑元素({})',
221
+ multipleEditNotice: '多选编辑已激活。更改将应用于所有 {} 个选定元素。',
222
+ geoserverLayers: 'GeoServer 图层'
186
223
  },
187
224
  errors: {
188
225
  capabilities: '无法加载GeoServer服务所支持的能力.',
@@ -217,12 +254,37 @@ const setLang = (lang = 'en', customI18n = null) => {
217
254
  I18N = Object.assign(Object.assign({}, I18N), customI18n);
218
255
  }
219
256
  };
257
+ /**
258
+ * /**
259
+ * For translations thas has a variable "{}"" to be replaced inside
260
+ * @param string
261
+ * @param args
262
+ * @returns
263
+ */
264
+ const I18N_ = (string, ...args) => {
265
+ var _a;
266
+ let text = (_a = I18N.labels) === null || _a === void 0 ? void 0 : _a[string];
267
+ if (!text) {
268
+ text = I18N[string];
269
+ }
270
+ if (!text) {
271
+ console.error('Translation not found', string);
272
+ text = string;
273
+ }
274
+ if (args.length) {
275
+ args.forEach((arg) => {
276
+ text = text.replace(/{}/, arg);
277
+ });
278
+ }
279
+ return text;
280
+ };
220
281
 
221
282
  let loadingDiv;
222
283
  const initLoading = () => {
223
284
  loadingDiv = document.createElement('div');
224
285
  loadingDiv.className = 'ol-wfst--tools-control--loading';
225
- loadingDiv.innerHTML = I18N.labels.loading;
286
+ loadingDiv.setAttribute('role', 'progressbar');
287
+ loadingDiv.setAttribute('aria-label', I18N.labels.loading);
226
288
  return loadingDiv;
227
289
  };
228
290
  const showLoading = (bool = true) => {
@@ -260,13 +322,27 @@ var Modes;
260
322
  (function (Modes) {
261
323
  Modes["Edit"] = "EDIT";
262
324
  Modes["Draw"] = "DRAW";
325
+ Modes["Query"] = "QUERY";
263
326
  })(Modes || (Modes = {}));
327
+ var SelectionMode;
328
+ (function (SelectionMode) {
329
+ SelectionMode["Single"] = "SINGLE";
330
+ SelectionMode["Box"] = "BOX";
331
+ SelectionMode["Freehand"] = "FREEHAND";
332
+ })(SelectionMode || (SelectionMode = {}));
333
+ let selectionMode = SelectionMode.Single;
264
334
  function activateMode(m = null) {
265
335
  mode = m;
266
336
  }
267
337
  function getMode() {
268
338
  return mode;
269
339
  }
340
+ function setSelectionMode(m) {
341
+ selectionMode = m;
342
+ }
343
+ function getSelectionMode() {
344
+ return selectionMode;
345
+ }
270
346
  const editedFeatures = new Set();
271
347
  const mapLayers = {};
272
348
  function setMap(m) {
@@ -305,16 +381,23 @@ function isFeatureEdited(feature) {
305
381
  */
306
382
  class BaseLayer extends Layer {
307
383
  /**
384
+ * Initialize the layer: request DescribeFeatureType and resolve when it
385
+ * finishes (whether successful or not).
386
+ *
308
387
  * @private
388
+ * @returns Promise that resolves when the layer is initialized
309
389
  */
310
- _init() {
390
+ async _init() {
311
391
  const geoserver = this.getGeoserver();
312
392
  if (geoserver.isLoaded()) {
313
- this.getAndUpdateDescribeFeatureType();
393
+ await this.getAndUpdateDescribeFeatureType();
314
394
  }
315
395
  else {
316
- geoserver.on('change:capabilities', async () => {
317
- this.getAndUpdateDescribeFeatureType();
396
+ await new Promise((resolve) => {
397
+ geoserver.once('change:capabilities', async () => {
398
+ await this.getAndUpdateDescribeFeatureType();
399
+ resolve();
400
+ });
318
401
  });
319
402
  }
320
403
  }
@@ -390,13 +473,13 @@ class BaseLayer extends Layer {
390
473
  }
391
474
  /**
392
475
  * @public
393
- * @param featureId
476
+ * @param featureIds
394
477
  * @returns
395
478
  */
396
- async maybeLockFeature(featureId) {
479
+ async maybeLockFeature(featureIds) {
397
480
  const geoserver = this.getGeoserver();
398
481
  if (geoserver.getUseLockFeature() && geoserver.hasLockFeature()) {
399
- return await geoserver.lockFeature(featureId, this.get(BaseLayerProperty.NAME));
482
+ return await geoserver.lockFeature(featureIds, this.get(BaseLayerProperty.NAME));
400
483
  }
401
484
  return null;
402
485
  }
@@ -435,44 +518,48 @@ var BaseLayerProperty;
435
518
  */
436
519
  class WfsSource extends VectorSource {
437
520
  constructor(options) {
438
- super(Object.assign(Object.assign({}, options), { format: new GeoJSON(), loader: async (extent, resolution, projection, success, failure) => {
439
- try {
440
- // If bbox, add extent to the request
441
- if (options.strategy == bbox) {
442
- const extentGeoServer = transformExtent(extent, projection.getCode(), options.geoServerAdvanced.projection);
443
- // https://docs.geoserver.org/stable/en/user/services/wfs/reference.html
444
- // request features using a bounding box with CRS maybe different from featureTypes native CRS
445
- this.urlParams.set('bbox', extentGeoServer.toString() +
446
- `,${options.geoServerAdvanced.projection}`);
447
- }
448
- const url_fetch = options.geoserverUrl + '?' + this.urlParams.toString();
449
- const response = await fetch(url_fetch, {
450
- headers: options.headers,
451
- credentials: options.credentials
452
- });
453
- if (!response.ok) {
454
- throw new Error('');
521
+ super(Object.assign(Object.assign({}, options), { format: new GeoJSON(), loader: (extent, resolution, projection, success, failure) => {
522
+ void (async () => {
523
+ try {
524
+ // If bbox, add extent to the request
525
+ if (options.strategy == bbox) {
526
+ const extentGeoServer = transformExtent(extent, projection.getCode(), options.geoServerAdvanced.projection);
527
+ // https://docs.geoserver.org/stable/en/user/services/wfs/reference.html
528
+ // request features using a bounding box with CRS maybe different from featureTypes native CRS
529
+ this.urlParams.set('bbox', extentGeoServer.toString() +
530
+ `,${options.geoServerAdvanced.projection}`);
531
+ }
532
+ const url_fetch = options.geoserverUrl +
533
+ '?' +
534
+ this.urlParams.toString();
535
+ const response = await fetch(url_fetch, {
536
+ headers: options.headers,
537
+ credentials: options.credentials
538
+ });
539
+ if (!response.ok) {
540
+ throw new Error('');
541
+ }
542
+ const data = await response.json();
543
+ if (data.exceptions) {
544
+ throw new Error(parseError(data));
545
+ }
546
+ const features = this.getFormat().readFeatures(data, {
547
+ featureProjection: projection.getCode(),
548
+ dataProjection: options.geoServerAdvanced.projection
549
+ });
550
+ features.forEach((feature) => {
551
+ feature.set('_layerName_', options.name,
552
+ /* silent = */ true);
553
+ });
554
+ this.addFeatures(features);
555
+ success(features);
455
556
  }
456
- const data = await response.json();
457
- if (data.exceptions) {
458
- throw new Error(parseError(data));
557
+ catch (err) {
558
+ this.removeLoadedExtent(extent);
559
+ showError(I18N.errors.geoserver, err, options.name);
560
+ failure();
459
561
  }
460
- const features = this.getFormat().readFeatures(data, {
461
- featureProjection: projection.getCode(),
462
- dataProjection: options.geoServerAdvanced.projection
463
- });
464
- features.forEach((feature) => {
465
- feature.set('_layerName_', options.name,
466
- /* silent = */ true);
467
- });
468
- this.addFeatures(features);
469
- success(features);
470
- }
471
- catch (err) {
472
- this.removeLoadedExtent(extent);
473
- showError(I18N.errors.geoserver, err, options.name);
474
- failure();
475
- }
562
+ })();
476
563
  } }));
477
564
  this.urlParams = new URLSearchParams({
478
565
  SERVICE: 'wfs',
@@ -620,7 +707,7 @@ class WmsLayer extends Mixin(BaseLayer, (TileLayer)) {
620
707
  this._loadingCount = 0;
621
708
  this._loadedCount = 0;
622
709
  /**
623
- * Return the full accuracy geometry to replace the feature from GetFEatureInfo
710
+ * Return the full accuracy geometries to replace the features from GetFeatureInfo
624
711
  * @param featuresId
625
712
  * @returns
626
713
  */
@@ -631,10 +718,10 @@ class WmsLayer extends Mixin(BaseLayer, (TileLayer)) {
631
718
  INFO_FORMAT: 'application/json',
632
719
  REQUEST: 'GetFeature',
633
720
  TYPENAME: this.get('name'),
634
- MAXFEATURES: '1',
721
+ MAXFEATURES: String(featuresId.length),
635
722
  OUTPUTFORMAT: 'application/json',
636
723
  SRSNAME: getMap().getView().getProjection().getCode(),
637
- FEATUREID: String(featuresId)
724
+ FEATUREID: featuresId.join(',')
638
725
  });
639
726
  const url = this.getSource().getUrls()[0] + '?' + queryParams.toString();
640
727
  try {
@@ -647,7 +734,11 @@ class WmsLayer extends Mixin(BaseLayer, (TileLayer)) {
647
734
  throw new Error(`${I18N.errors.getFeatures} ${response.status}`);
648
735
  }
649
736
  const data = await response.json();
650
- return this._parseFeaturesFromResponse(data);
737
+ const fullById = {};
738
+ this._parseFeaturesFromResponse(data).forEach((feature) => {
739
+ fullById[String(feature.getId())] = feature;
740
+ });
741
+ return featuresId.map((id) => fullById[String(id)]).filter(Boolean);
651
742
  }
652
743
  catch (err) {
653
744
  console.error(err);
@@ -657,6 +748,9 @@ class WmsLayer extends Mixin(BaseLayer, (TileLayer)) {
657
748
  if (options.beforeTransactFeature) {
658
749
  this.beforeTransactFeature = options.beforeTransactFeature;
659
750
  }
751
+ if (options.beforeShowFieldsModal) {
752
+ this.beforeShowFieldsModal = options.beforeShowFieldsModal;
753
+ }
660
754
  this._formatGeoJSON = new GeoJSON();
661
755
  const geoserver = options.geoserver;
662
756
  const source = new WmsSource({
@@ -712,6 +806,112 @@ class WmsLayer extends Mixin(BaseLayer, (TileLayer)) {
712
806
  FEATURE_COUNT: 1,
713
807
  EXCEPTIONS: 'application/json'
714
808
  });
809
+ return this._requestFeatures(url);
810
+ }
811
+ /**
812
+ * Request the WFS features that intersect the given geometry, selecting
813
+ * them with a WFS GetFeature + CQL INTERSECTS filter. Used to select
814
+ * features from a WMS layer with a box or a freehand lasso.
815
+ *
816
+ * @param geometry Geometry in the map view projection
817
+ * @returns
818
+ * @private
819
+ */
820
+ async _getFeaturesInGeometry(geometry) {
821
+ var _a, _b, _c;
822
+ // Make sure the DescribeFeatureType is loaded to get the geometry
823
+ // field, waiting for it if the layer is still initializing
824
+ if (!this.getDescribeFeatureType()) {
825
+ await this.getAndUpdateDescribeFeatureType();
826
+ }
827
+ const geomField = (_b = (_a = this.getDescribeFeatureType()) === null || _a === void 0 ? void 0 : _a._parsed) === null || _b === void 0 ? void 0 : _b.geomField;
828
+ if (!geomField) {
829
+ showError(`${I18N.errors.layer} "${this.get('name')}"`);
830
+ return [];
831
+ }
832
+ const viewProj = getMap().getView().getProjection().getCode();
833
+ const nativeSrs = this._getNativeSrs();
834
+ // Work on a copy transformed to the layer native projection so the
835
+ // server evaluates the INTERSECTS filter against the proper SRID
836
+ const polygon = geometry.clone();
837
+ if (nativeSrs !== viewProj) {
838
+ try {
839
+ polygon.transform(viewProj, nativeSrs);
840
+ }
841
+ catch (err) {
842
+ console.error(err);
843
+ }
844
+ }
845
+ const ring = polygon.getCoordinates()[0] || [];
846
+ const ringWkt = ring.map(([x, y]) => `${x} ${y}`).join(', ');
847
+ // Force the filter geometry CRS with the EWKT SRID prefix so the
848
+ // server evaluates the INTERSECTS against the layer projection
849
+ // instead of its native storage SRS
850
+ const sridMatch = (_c = nativeSrs.match(/(?:EPSG\s*::?\s*|EPSG\/0\/)(\d+)/i)) !== null && _c !== void 0 ? _c : nativeSrs.match(/(\d+)$/);
851
+ const cql = sridMatch
852
+ ? `INTERSECTS(${geomField}, SRID=${sridMatch[1]};POLYGON((${ringWkt})))`
853
+ : `INTERSECTS(${geomField}, POLYGON((${ringWkt})))`;
854
+ const queryParams = new URLSearchParams({
855
+ SERVICE: 'wfs',
856
+ VERSION: '2.0.0',
857
+ REQUEST: 'GetFeature',
858
+ TYPENAME: this.get('name'),
859
+ OUTPUTFORMAT: 'application/json',
860
+ SRSNAME: viewProj,
861
+ CQL_FILTER: cql
862
+ });
863
+ const url = this.getSource().getUrls()[0] + '?' + queryParams.toString();
864
+ // The server returns the features in the map view projection (same as
865
+ // the full resolution request), so no further transformation is needed
866
+ return this._requestFeatures(url);
867
+ }
868
+ /**
869
+ * Resolve the layer native SRS so spatial filters are evaluated in the
870
+ * correct projection. It reads the DefaultCRS from the WFS capabilities,
871
+ * falling back to the geoserver advanced projection option and finally to
872
+ * the map view projection.
873
+ *
874
+ * @returns
875
+ * @private
876
+ */
877
+ _getNativeSrs() {
878
+ var _a;
879
+ const geoserver = this.getGeoserver();
880
+ const viewProj = getMap().getView().getProjection().getCode();
881
+ const layerName = String(this.get('name') || '');
882
+ const layerLocalName = layerName.split(':').pop();
883
+ const featureTypeList = (_a = geoserver.getParsedCapabilities()) === null || _a === void 0 ? void 0 : _a.FeatureTypeList;
884
+ const featureType = Array.isArray(featureTypeList)
885
+ ? featureTypeList.find((ft) => {
886
+ const featureTypeName = String((ft === null || ft === void 0 ? void 0 : ft.Name) || '');
887
+ return (featureTypeName === layerName ||
888
+ featureTypeName === layerLocalName ||
889
+ featureTypeName.split(':').pop() === layerLocalName);
890
+ })
891
+ : undefined;
892
+ const defaultCrs = (featureType === null || featureType === void 0 ? void 0 : featureType.DefaultCRS)
893
+ ? String(featureType.DefaultCRS)
894
+ : '';
895
+ const epsgMatch = defaultCrs.match(/(?:EPSG\s*::?\s*|EPSG\/0\/)(\d+)/i);
896
+ if (epsgMatch) {
897
+ return `EPSG:${epsgMatch[1]}`;
898
+ }
899
+ const advanced = geoserver.getAdvanced().projection;
900
+ if (advanced) {
901
+ return String(advanced);
902
+ }
903
+ return viewProj;
904
+ }
905
+ /**
906
+ * Fetch and parse the features from a request url, replacing the
907
+ * low resolution geometries (GetFeatureInfo) with the full resolution
908
+ * ones requested by FEATUREID when possible.
909
+ *
910
+ * @param url
911
+ * @returns
912
+ * @private
913
+ */
914
+ async _requestFeatures(url) {
715
915
  const geoserver = this.getGeoserver();
716
916
  try {
717
917
  const response = await fetch(url, {
@@ -724,6 +924,9 @@ class WmsLayer extends Mixin(BaseLayer, (TileLayer)) {
724
924
  const data = await response.json();
725
925
  let features = this._parseFeaturesFromResponse(data);
726
926
  const featuresId = features.map((f) => f.getId());
927
+ if (!featuresId.length) {
928
+ return [];
929
+ }
727
930
  const fullResList = await this._getFullResGeometryById(featuresId);
728
931
  if (fullResList) {
729
932
  features = fullResList;
@@ -773,50 +976,95 @@ class WmsLayer extends Mixin(BaseLayer, (TileLayer)) {
773
976
  }
774
977
  }
775
978
 
776
- 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";
777
-
778
- 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";
779
-
780
- 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";
781
-
782
- 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";
783
-
784
- function createElement(tagName, attrs = {}, ...children) {
785
- if (typeof tagName === 'function')
786
- return tagName(attrs, children);
787
- const elem = tagName === null
788
- ? new DocumentFragment()
979
+ function createElement(tagName, props) {
980
+ props = props || {};
981
+ let children = props.children || [];
982
+ children = Array.isArray(children) ? children : [children];
983
+ delete props.children;
984
+ if (typeof tagName === 'function') {
985
+ return tagName(props, children);
986
+ }
987
+ const elem = tagName === 'fragment'
988
+ ? document.createDocumentFragment()
789
989
  : document.createElement(tagName);
790
- Object.entries(attrs || {}).forEach(([name, value]) => {
791
- if (typeof value !== 'undefined' &&
792
- value !== null &&
793
- value !== undefined) {
794
- if (name.startsWith('on') && name.toLowerCase() in window)
795
- elem.addEventListener(name.toLowerCase().substr(2), value);
796
- else {
797
- if (name === 'className')
990
+ if (elem instanceof HTMLElement) {
991
+ Object.entries(props).forEach(([name, value]) => {
992
+ if (value != null) {
993
+ // listener
994
+ if (name.startsWith('on') && typeof value === 'function') {
995
+ elem.addEventListener(name
996
+ .slice(2)
997
+ .toLowerCase(), value);
998
+ }
999
+ else if (name === 'className') {
798
1000
  elem.setAttribute('class', value.toString());
799
- else if (name === 'htmlFor')
1001
+ }
1002
+ else if (name === 'htmlFor') {
800
1003
  elem.setAttribute('for', value.toString());
801
- else
1004
+ }
1005
+ else {
802
1006
  elem.setAttribute(name, value.toString());
1007
+ }
803
1008
  }
804
- }
805
- });
1009
+ });
1010
+ }
806
1011
  for (const child of children) {
807
- if (!child)
1012
+ if (!child) {
808
1013
  continue;
809
- if (Array.isArray(child))
1014
+ }
1015
+ if (Array.isArray(child)) {
810
1016
  elem.append(...child);
1017
+ }
1018
+ else if (child instanceof Node) {
1019
+ elem.appendChild(child);
1020
+ }
811
1021
  else {
812
- if (child.nodeType === undefined)
813
- elem.innerHTML += child;
814
- else
815
- elem.appendChild(child);
1022
+ elem.innerHTML += child;
816
1023
  }
817
1024
  }
818
1025
  return elem;
819
1026
  }
1027
+ function jsx(type, props) {
1028
+ return createElement(type, props);
1029
+ }
1030
+ function jsxs(type, props) {
1031
+ return createElement(type, props);
1032
+ }
1033
+ function Fragment(type, children) {
1034
+ return createElement('fragment', { children });
1035
+ }
1036
+
1037
+ function uploadSvg() {
1038
+ 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;
1039
+ }
1040
+
1041
+ function drawSvg() {
1042
+ 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;
1043
+ }
1044
+
1045
+ function selectSvg() {
1046
+ 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;
1047
+ }
1048
+
1049
+ function selectBoxSvg() {
1050
+ 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;
1051
+ }
1052
+
1053
+ function selectFreehandSvg() {
1054
+ 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;
1055
+ }
1056
+
1057
+ function infoSvg() {
1058
+ 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;
1059
+ }
1060
+
1061
+ function visibilityOnSvg() {
1062
+ 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;
1063
+ }
1064
+
1065
+ function visibilityOffSvg() {
1066
+ 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;
1067
+ }
820
1068
 
821
1069
  /**
822
1070
  * Removes in the DOM the class of the tools
@@ -833,6 +1081,19 @@ const activateModeButtons = () => {
833
1081
  if (btn) {
834
1082
  btn.classList.add('wfst--active');
835
1083
  }
1084
+ deactivateQueryButton();
1085
+ };
1086
+ const activateQueryButton = () => {
1087
+ const btn = document.querySelector('.ol-wfst--tools-control-btn-query');
1088
+ if (btn) {
1089
+ btn.classList.add('wfst--active');
1090
+ }
1091
+ };
1092
+ const deactivateQueryButton = () => {
1093
+ const btn = document.querySelector('.ol-wfst--tools-control-btn-query');
1094
+ if (btn) {
1095
+ btn.classList.remove('wfst--active');
1096
+ }
836
1097
  };
837
1098
  const activateDrawButton = () => {
838
1099
  const btn = document.querySelector('.ol-wfst--tools-control-btn-draw');
@@ -840,6 +1101,29 @@ const activateDrawButton = () => {
840
1101
  btn.classList.add('wfst--active');
841
1102
  }
842
1103
  };
1104
+ const activateSelectModeButton = (mode) => {
1105
+ 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');
1106
+ if (activeBtn) {
1107
+ activeBtn.classList.remove('wfst--active');
1108
+ }
1109
+ deactivateQueryButton();
1110
+ const btnMap = {
1111
+ [SelectionMode.Single]: '.ol-wfst--tools-control-btn-select',
1112
+ [SelectionMode.Box]: '.ol-wfst--tools-control-btn-select-box',
1113
+ [SelectionMode.Freehand]: '.ol-wfst--tools-control-btn-select-freehand'
1114
+ };
1115
+ const btn = document.querySelector(btnMap[mode]);
1116
+ if (btn) {
1117
+ btn.classList.add('wfst--active');
1118
+ }
1119
+ };
1120
+ const deactivateSelectModeButton = () => {
1121
+ 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');
1122
+ if (activeBtn) {
1123
+ activeBtn.classList.remove('wfst--active');
1124
+ }
1125
+ deactivateQueryButton();
1126
+ };
843
1127
  class LayersControl extends Observable {
844
1128
  constructor(uploads, uploadFormats) {
845
1129
  super();
@@ -852,23 +1136,15 @@ class LayersControl extends Observable {
852
1136
  * @public
853
1137
  */
854
1138
  addLayerEl(layer) {
855
- const container = document.querySelector('.wfst--tools-control--select-layers');
1139
+ const container = document.querySelector('.wfst--tools-control--select-layers-list');
856
1140
  const layerName = layer.get(BaseLayerProperty.NAME);
857
1141
  const checked = layer === getActiveLayerToInsertEls() ? { checked: true } : {};
858
- 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) })));
859
- const layerDom = (createElement("div", { className: `wfst--layer-control
1142
+ 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) })));
1143
+ const layerDom = (jsx("div", { className: `wfst--layer-control
860
1144
  ${layer.getVisible() ? 'ol-wfst--visible-on' : ''}
861
1145
  ${layer === getActiveLayerToInsertEls()
862
1146
  ? 'ol-wfst--selected-on'
863
- : ''}`, "data-layer": layerName },
864
- createElement("div", { className: "ol-wfst--tools-control-visible" },
865
- createElement("span", { className: "ol-wfst--tools-control-visible-btn ol-wfst--visible-btn-on", title: I18N.labels.toggleVisibility, onClick: (evt) => this._visibilityClickHandler(evt) },
866
- createElement("img", { src: img$3 })),
867
- createElement("span", { className: "ol-wfst--tools-control-visible-btn ol-wfst--visible-btn-off", title: I18N.labels.toggleVisibility, onClick: (evt) => this._visibilityClickHandler(evt) },
868
- createElement("img", { src: img$2 }))),
869
- createElement("label", { htmlFor: `wfst--${layerName}` },
870
- input,
871
- createElement("span", { title: layer.getDescribeFeatureType()._parsed.geomType }, layer.get(BaseLayerProperty.LABEL)))));
1147
+ : ''}`, "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) })] }) }));
872
1148
  container.appendChild(layerDom);
873
1149
  if (layer === getActiveLayerToInsertEls()) {
874
1150
  input.dispatchEvent(new Event('change'));
@@ -964,34 +1240,32 @@ class LayersControl extends Observable {
964
1240
  this.dispatchEvent('changeLayer');
965
1241
  }
966
1242
  render() {
967
- return (createElement(null, null,
968
- createElement("div", { className: "wfst--tools-control--head" },
969
- this._uploads && (createElement("div", null,
970
- createElement("input", { id: "ol-wfst--upload", type: "file", accept: this._uploadFormats, onChange: (evt) => this._uploads.process(evt) }),
971
- createElement("label", { className: "ol-wfst--tools-control-btn ol-wfst--tools-control-btn-upload", htmlFor: "ol-wfst--upload", title: I18N.labels.uploadToLayer },
972
- createElement("img", { src: img$5 })))),
973
- createElement("div", { className: "ol-wfst--tools-control-draw-cnt" },
974
- createElement("button", { className: "ol-wfst--tools-control-btn ol-wfst--tools-control-btn-draw", type: "button", title: I18N.labels.addElement, onClick: () => {
975
- this.dispatchEvent('drawMode');
976
- } },
977
- createElement("img", { src: img$4 })),
978
- createElement("select", { title: I18N.labels.selectDrawType, className: "wfst--tools-control--select-draw", onChange: (evt) => {
979
- const selectedValue = evt.target.value;
980
- this._changeStateSelect(getActiveLayerToInsertEls(), selectedValue);
981
- this.dispatchEvent('changeGeom');
982
- } }, [
983
- GeometryType.Point,
984
- GeometryType.MultiPoint,
985
- GeometryType.LineString,
986
- GeometryType.MultiLineString,
987
- GeometryType.Polygon,
988
- GeometryType.MultiPolygon,
989
- GeometryType.Circle
990
- ].map((type) => {
991
- // Show all options, but enable only the accepted ones
992
- return createElement("option", { value: type }, type);
993
- })))),
994
- createElement("div", { className: "wfst--tools-control--select-layers" })));
1243
+ 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: () => {
1244
+ this.dispatchEvent('selectModeSingle');
1245
+ }, children: selectSvg() }), jsx("button", { className: "ol-wfst--tools-control-btn ol-wfst--tools-control-btn-select-box", type: "button", title: I18N.labels.selectBox, onClick: () => {
1246
+ this.dispatchEvent('selectModeBox');
1247
+ }, children: selectBoxSvg() }), jsx("button", { className: "ol-wfst--tools-control-btn ol-wfst--tools-control-btn-select-freehand", type: "button", title: I18N.labels.selectFreehand, onClick: () => {
1248
+ this.dispatchEvent('selectModeFreehand');
1249
+ }, 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: () => {
1250
+ this.dispatchEvent('queryMode');
1251
+ }, 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: () => {
1252
+ this.dispatchEvent('drawMode');
1253
+ }, children: drawSvg() }), jsx("select", { title: I18N.labels.selectDrawType, className: "wfst--tools-control--select-draw", onChange: (evt) => {
1254
+ const selectedValue = evt.target.value;
1255
+ this._changeStateSelect(getActiveLayerToInsertEls(), selectedValue);
1256
+ this.dispatchEvent('changeGeom');
1257
+ }, children: [
1258
+ GeometryType.Point,
1259
+ GeometryType.MultiPoint,
1260
+ GeometryType.LineString,
1261
+ GeometryType.MultiLineString,
1262
+ GeometryType.Polygon,
1263
+ GeometryType.MultiPolygon,
1264
+ GeometryType.Circle
1265
+ ].map((type) => {
1266
+ // Show all options, but enable only the accepted ones
1267
+ return jsx("option", { value: type, children: type });
1268
+ }) })] }), 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" })] })] }));
995
1269
  }
996
1270
  }
997
1271
 
@@ -1230,6 +1504,7 @@ const getDefaultOptions = () => {
1230
1504
  evtType: 'singleclick',
1231
1505
  active: true,
1232
1506
  showControl: true,
1507
+ fullscreen: true,
1233
1508
  language: DEFAULT_LANGUAGE,
1234
1509
  uploadFormats: '.geojson,.json,.kml',
1235
1510
  processUpload: null,
@@ -1247,91 +1522,145 @@ const getDefaultOptions = () => {
1247
1522
  };
1248
1523
 
1249
1524
  class EditControlChangesEl extends Control {
1250
- constructor(feature) {
1525
+ constructor(features) {
1251
1526
  super({
1252
- element: (createElement("div", { className: "ol-wfst--changes-control" },
1253
- createElement("div", { className: "ol-wfst--changes-control-el" },
1254
- createElement("div", { className: "ol-wfst--changes-control-id" },
1255
- createElement("b", null, I18N.labels.editMode),
1256
- " -",
1257
- ' ',
1258
- createElement("i", null, String(feature.getId()))),
1259
- createElement("button", { type: "button", className: "btn btn-sm btn-secondary", onClick: () => {
1260
- this.dispatchEvent(new VectorSourceEvent('cancel', feature));
1261
- } }, I18N.labels.cancel),
1262
- createElement("button", { type: "button", className: "btn btn-sm btn-primary", onClick: () => {
1263
- this.dispatchEvent(new VectorSourceEvent('apply', feature));
1264
- } }, I18N.labels.apply),
1265
- createElement("button", { type: "button", className: "btn btn-sm btn-danger-outline", onClick: () => {
1266
- this.dispatchEvent(new VectorSourceEvent('delete', feature));
1267
- } }, I18N.labels.delete))))
1527
+ 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
1528
+ ? I18N_('editElements', features.length)
1529
+ : String(features[0].getId()) })] }), jsx("button", { type: "button", className: "btn btn-sm btn-secondary", onClick: () => {
1530
+ this._dispatch('cancel', features);
1531
+ }, children: I18N.labels.cancel }), jsx("button", { type: "button", className: "btn btn-sm btn-primary", onClick: () => {
1532
+ this._dispatch('apply', features);
1533
+ }, children: I18N.labels.apply }), jsx("button", { type: "button", className: "btn btn-sm btn-danger-outline", onClick: () => {
1534
+ this._dispatch('delete', features);
1535
+ }, children: I18N.labels.delete })] }) }))
1268
1536
  });
1269
1537
  }
1538
+ _dispatch(type, features) {
1539
+ const evt = new VectorSourceEvent(type, features[0]);
1540
+ evt.features = features;
1541
+ this.dispatchEvent(evt);
1542
+ }
1270
1543
  }
1271
1544
 
1272
1545
  // Ol
1546
+ /**
1547
+ * Modern vertex handle mimicking the round "extend line" buttons shown at
1548
+ * line endpoints: a soft halo for depth and a white disc with a subtle
1549
+ * border. When `highlighted` is `true` (e.g. hovering a vertex with the
1550
+ * Modify interaction) the handle gets bigger with a stronger halo and a
1551
+ * darker border.
1552
+ *
1553
+ * @param highlighted Whether to render the bigger/highlighted state
1554
+ * @param geometry Style geometry function to position the handles
1555
+ * @private
1556
+ */
1557
+ const editVertexStyles = (highlighted = false, geometry) => {
1558
+ const halo = highlighted ? 10 : 8;
1559
+ const haloColor = highlighted
1560
+ ? 'rgba(0, 0, 0, 0.40)'
1561
+ : 'rgba(0, 0, 0, 0.28)';
1562
+ const radius = highlighted ? 7 : 6;
1563
+ const border = highlighted ? '#4b5563' : '#b3b3b3';
1564
+ const borderWidth = highlighted ? 2 : 1.5;
1565
+ return [
1566
+ new Style(Object.assign({ image: new Circle({
1567
+ radius: halo,
1568
+ fill: new Fill({
1569
+ color: haloColor
1570
+ })
1571
+ }) }, (geometry ? { geometry } : {}))),
1572
+ new Style(Object.assign({ image: new Circle({
1573
+ radius: radius,
1574
+ fill: new Fill({
1575
+ color: '#ffffff'
1576
+ }),
1577
+ stroke: new Stroke({
1578
+ width: borderWidth,
1579
+ color: border
1580
+ })
1581
+ }) }, (geometry ? { geometry } : {})))
1582
+ ];
1583
+ };
1584
+ /**
1585
+ * Extract a `MultiPoint` with every vertex of the given feature geometry
1586
+ * (polygons and multi-line strings are flattened). Returns `undefined` when
1587
+ * the geometry has no coordinates.
1588
+ *
1589
+ * @param feature
1590
+ * @private
1591
+ */
1592
+ const getFeatureVertices = (feature) => {
1593
+ let geometry = feature.getGeometry();
1594
+ if (geometry instanceof GeometryCollection) {
1595
+ geometry = geometry.getGeometries()[0];
1596
+ }
1597
+ const coordinates = geometry.getCoordinates();
1598
+ let flatCoordinates = null;
1599
+ if (geometry instanceof Polygon || geometry instanceof MultiLineString) {
1600
+ flatCoordinates = coordinates.flat(1);
1601
+ }
1602
+ else if (geometry instanceof MultiPolygon) {
1603
+ flatCoordinates = coordinates.flat(2);
1604
+ }
1605
+ else {
1606
+ flatCoordinates = coordinates;
1607
+ }
1608
+ if (!flatCoordinates || !flatCoordinates.length) {
1609
+ return;
1610
+ }
1611
+ return new MultiPoint(flatCoordinates);
1612
+ };
1273
1613
  /**
1274
1614
  * Master style that handles two modes on the Edit Layer:
1275
1615
  * - one is the basic, showing only the vertices
1276
1616
  * - and the other when modify is active, showing bigger vertices
1277
1617
  *
1618
+ * When `hovered` is `true`, a white/red halo is prepended to highlight the
1619
+ * feature, indicating it can be selected on click.
1620
+ *
1278
1621
  * @param feature
1622
+ * @param hovered
1279
1623
  * @private
1280
1624
  */
1281
- function styleFunction(feature) {
1282
- const getVertexs = (feature) => {
1283
- let geometry = feature.getGeometry();
1284
- if (geometry instanceof GeometryCollection) {
1285
- geometry = geometry.getGeometries()[0];
1286
- }
1287
- const coordinates = geometry.getCoordinates();
1288
- let flatCoordinates = null;
1289
- if (geometry instanceof Polygon ||
1290
- geometry instanceof MultiLineString) {
1291
- flatCoordinates = coordinates.flat(1);
1292
- }
1293
- else if (geometry instanceof MultiPolygon) {
1294
- flatCoordinates = coordinates.flat(2);
1295
- }
1296
- else {
1297
- flatCoordinates = coordinates;
1298
- }
1299
- if (!flatCoordinates || !flatCoordinates.length) {
1300
- return;
1301
- }
1302
- return new MultiPoint(flatCoordinates);
1303
- };
1625
+ function styleFunction(feature, hovered = false) {
1304
1626
  let geometry = feature.getGeometry();
1305
1627
  let type = geometry.getType();
1306
1628
  if (geometry instanceof GeometryCollection) {
1307
1629
  geometry = geometry.getGeometries()[0];
1308
1630
  type = geometry.getType();
1309
1631
  }
1632
+ const haloStyle = () => {
1633
+ return [
1634
+ new Style({
1635
+ image: new Circle({
1636
+ radius: 9,
1637
+ fill: new Fill({
1638
+ color: 'rgba(255, 255, 255, 0.9)'
1639
+ }),
1640
+ stroke: new Stroke({
1641
+ color: '#ff0000',
1642
+ width: 3
1643
+ })
1644
+ }),
1645
+ stroke: new Stroke({
1646
+ color: 'rgba(255, 255, 255, 0.9)',
1647
+ width: 8
1648
+ }),
1649
+ fill: new Fill({
1650
+ color: 'rgba(255, 255, 255, 0.9)'
1651
+ })
1652
+ })
1653
+ ];
1654
+ };
1310
1655
  switch (type) {
1311
1656
  case GeometryType.Point:
1312
1657
  case GeometryType.MultiPoint:
1313
1658
  if (getMode() === Modes.Edit) {
1314
- return [
1315
- new Style({
1316
- image: new Circle({
1317
- radius: 6,
1318
- fill: new Fill({
1319
- color: '#000000'
1320
- })
1321
- })
1322
- }),
1323
- new Style({
1324
- image: new Circle({
1325
- radius: 4,
1326
- fill: new Fill({
1327
- color: '#ff0000'
1328
- })
1329
- })
1330
- })
1331
- ];
1659
+ return editVertexStyles();
1332
1660
  }
1333
1661
  else {
1334
1662
  return [
1663
+ ...(hovered ? haloStyle() : []),
1335
1664
  new Style({
1336
1665
  image: new Circle({
1337
1666
  radius: 5,
@@ -1363,19 +1692,7 @@ function styleFunction(feature) {
1363
1692
  color: 'rgba(255, 0, 0, 0.7)'
1364
1693
  })
1365
1694
  }),
1366
- new Style({
1367
- image: new Circle({
1368
- radius: 4,
1369
- fill: new Fill({
1370
- color: '#ff0000'
1371
- }),
1372
- stroke: new Stroke({
1373
- width: 2,
1374
- color: 'rgba(5, 5, 5, 0.9)'
1375
- })
1376
- }),
1377
- geometry: (feature) => getVertexs(feature)
1378
- }),
1695
+ ...editVertexStyles(false, getFeatureVertices),
1379
1696
  new Style({
1380
1697
  stroke: new Stroke({
1381
1698
  color: 'rgba(255, 255, 255, 0.7)',
@@ -1386,6 +1703,7 @@ function styleFunction(feature) {
1386
1703
  }
1387
1704
  else {
1388
1705
  return [
1706
+ ...(hovered ? haloStyle() : []),
1389
1707
  new Style({
1390
1708
  image: new Circle({
1391
1709
  radius: 2,
@@ -1393,7 +1711,7 @@ function styleFunction(feature) {
1393
1711
  color: '#000000'
1394
1712
  })
1395
1713
  }),
1396
- geometry: (feature) => getVertexs(feature)
1714
+ geometry: (feature) => getFeatureVertices(feature)
1397
1715
  }),
1398
1716
  new Style({
1399
1717
  stroke: new Stroke({
@@ -1408,91 +1726,178 @@ function styleFunction(feature) {
1408
1726
  }
1409
1727
  }
1410
1728
  }
1411
-
1412
- // Ol
1413
1729
  /**
1414
- * Shows a fields form in a modal window to allow changes in the properties of the feature.
1730
+ * Style used to highlight a feature after a query, drawn on a dedicated
1731
+ * highlight layer so the source layers keep their own rendering.
1415
1732
  *
1416
1733
  * @param feature
1417
1734
  * @private
1418
1735
  */
1736
+ function queryStyleFunction(_feature) {
1737
+ return [
1738
+ new Style({
1739
+ image: new Circle({
1740
+ radius: 6,
1741
+ fill: new Fill({
1742
+ color: 'rgba(0, 170, 255, 0.4)'
1743
+ }),
1744
+ stroke: new Stroke({
1745
+ color: '#0066cc',
1746
+ width: 2
1747
+ })
1748
+ }),
1749
+ stroke: new Stroke({
1750
+ color: '#00aaff',
1751
+ width: 4
1752
+ }),
1753
+ fill: new Fill({
1754
+ color: 'rgba(0, 170, 255, 0.3)'
1755
+ })
1756
+ })
1757
+ ];
1758
+ }
1759
+
1760
+ /**
1761
+ * Shows a fields form in a modal window to allow changes in the properties of
1762
+ * one or several features. When editing multiple features, fields whose value
1763
+ * differs across the selection show the "multiple values" placeholder and only
1764
+ * the fields actually changed are applied to the whole selection.
1765
+ *
1766
+ * @param features
1767
+ * @private
1768
+ */
1419
1769
  class EditFieldsModal extends Observable {
1420
1770
  constructor(options) {
1421
1771
  super();
1422
1772
  this._options = options;
1423
- const footer = `
1424
- <button type="button" class="btn btn-sm btn-link btn-third" data-action="delete" data-dismiss="modal">
1425
- ${I18N.labels.delete}
1426
- </button>
1427
- <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">
1428
- ${I18N.labels.cancel}
1429
- </button>
1430
- <button type="button" class="btn btn-sm btn-primary" data-action="save" data-dismiss="modal">
1431
- ${I18N.labels.save}
1432
- </button>
1433
- `;
1434
- this._modal = new Modal(Object.assign(Object.assign({}, this._options), { header: true, headerClose: true, title: '', content: '<div></div>', footer: footer }));
1773
+ this._modal = new Modal(Object.assign(Object.assign({}, this._options.modal), { header: true, headerClose: true, title: '', content: '<div></div>', footer: `
1774
+ <button
1775
+ type="button"
1776
+ class="btn btn-sm btn-third"
1777
+ data-action="delete"
1778
+ data-dismiss="modal"
1779
+ >
1780
+ ${I18N.labels.delete}
1781
+ </button>
1782
+ <button
1783
+ type="button"
1784
+ class="btn btn-sm btn-secondary"
1785
+ data-dismiss="modal"
1786
+ >
1787
+ ${I18N.labels.cancel}
1788
+ </button>
1789
+ <button
1790
+ type="button"
1791
+ class="btn btn-sm btn-primary"
1792
+ data-action="save"
1793
+ data-dismiss="modal"
1794
+ >
1795
+ ${I18N.labels.save}
1796
+ </button>
1797
+ ` }));
1435
1798
  this._modal.on('dismiss', (modal, event) => {
1436
1799
  // On saving changes
1437
1800
  if (event.target.dataset.action === 'save') {
1438
- const inputs = modal.el.querySelectorAll('input');
1439
- inputs.forEach((el) => {
1801
+ const formElements = modal.el.querySelector('form')
1802
+ .elements;
1803
+ const multipleFields = new Set(Array.from(formElements)
1804
+ .filter((el) => el.dataset.multiple === 'true')
1805
+ .map((el) => el.name));
1806
+ Array.from(formElements).forEach((el) => {
1440
1807
  const value = el.value;
1441
1808
  const field = el.name;
1442
- this._feature.set(field, value, /*isSilent = */ true);
1809
+ // Do not overwrite untouched fields that had
1810
+ // multiple values in the selection
1811
+ if (multipleFields.has(field) && value === '') {
1812
+ return;
1813
+ }
1814
+ this._features.forEach((feature) => {
1815
+ if (feature.get(field) !== value) {
1816
+ feature.set(field, value, /* isSilent = */ true);
1817
+ }
1818
+ });
1443
1819
  });
1444
- this._feature.changed();
1445
- addFeatureToEditedList(this._feature);
1446
- this.dispatchEvent(new VectorSourceEvent('save', this._feature));
1820
+ this._features.forEach((feature) => {
1821
+ feature.changed();
1822
+ addFeatureToEditedList(feature);
1823
+ });
1824
+ this._dispatch('save', this._features);
1447
1825
  }
1448
1826
  else if (event.target.dataset.action === 'delete') {
1449
- this.dispatchEvent(new VectorSourceEvent('delete', this._feature));
1827
+ this._dispatch('delete', this._features);
1450
1828
  }
1451
1829
  });
1452
1830
  }
1453
- show(feature) {
1454
- this._feature = feature;
1455
- const title = `${I18N.labels.editElement} ${feature.getId()} `;
1456
- const properties = feature.getProperties();
1457
- const layerName = feature.get('_layerName_');
1831
+ show(features) {
1832
+ this._features = Array.isArray(features) ? features : [features];
1833
+ const multiple = this._features.length > 1;
1834
+ const modalTitle = multiple
1835
+ ? I18N_('editElements', this._features.length)
1836
+ : `${I18N.labels.editElement} ${this._features[0].getId()} `;
1837
+ const layerName = this._features[0].get('_layerName_');
1458
1838
  // Data schema from the geoserver
1459
1839
  const layer = getStoredLayer(layerName);
1460
- const dataSchema = layer.getDescribeFeatureType()._parsed.properties;
1461
- let content = '<form autocomplete="false">';
1462
- Object.keys(properties).forEach((key) => {
1463
- // If the feature field exists in the geoserver and is not added by openlayers
1464
- const field = dataSchema.find((data) => data.name === key);
1465
- if (field) {
1466
- const typeXsd = field.type;
1467
- let type;
1468
- switch (typeXsd) {
1469
- case 'xsd:string':
1470
- type = 'text';
1471
- break;
1472
- case 'xsd:number':
1473
- case 'xsd:int':
1474
- type = 'number';
1475
- break;
1476
- case 'xsd:date-time':
1477
- type = 'datetime';
1478
- break;
1479
- default:
1480
- type = 'text';
1481
- }
1482
- if (type) {
1483
- content += `
1484
- <div class="ol-wfst--input-field-container">
1485
- <label class="ol-wfst--input-field-label" for="${key}">${key}</label>
1486
- <input placeholder="NULL" class="ol-wfst--input-field-input" type="${type}" name="${key}" value="${properties[key] || ''}">
1487
- </div>`;
1488
- }
1489
- }
1490
- });
1491
- content += '</form>';
1492
- this._modal._html.body.innerHTML = content;
1493
- this._modal._html.header.innerHTML = title;
1840
+ const describeFeatureType = layer.getDescribeFeatureType()._parsed;
1841
+ const fieldList = describeFeatureType.properties.filter((field) => field.name !== describeFeatureType.geomField);
1842
+ this._modal._html.body.innerHTML = '';
1843
+ 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) => {
1844
+ const key = field.name;
1845
+ const values = this._features.map((feature) => feature.get(key));
1846
+ const uniqueValues = Array.from(new Set(values.map((value) => String(value !== null && value !== void 0 ? value : ''))));
1847
+ const hasMultipleValues = multiple && uniqueValues.length > 1;
1848
+ const value = hasMultipleValues
1849
+ ? null
1850
+ : uniqueValues[0] || null;
1851
+ const typeXsd = field.type;
1852
+ let type;
1853
+ switch (typeXsd) {
1854
+ case 'xsd:double':
1855
+ case 'xsd:number':
1856
+ case 'xsd:int':
1857
+ type = 'number';
1858
+ break;
1859
+ case 'xsd:date':
1860
+ type = 'date';
1861
+ break;
1862
+ case 'xsd:date-time':
1863
+ type = 'datetime';
1864
+ break;
1865
+ case 'xsd:string':
1866
+ default:
1867
+ type = 'text';
1868
+ }
1869
+ let input = (jsx("input", { placeholder: hasMultipleValues
1870
+ ? I18N.labels.multipleValues
1871
+ : 'NULL', className: 'ol-wfst--input-field-input' +
1872
+ (hasMultipleValues
1873
+ ? ' ol-wfst--input-multiple'
1874
+ : ''), type: type, name: key, value: value, "data-multiple": hasMultipleValues ? 'true' : null }));
1875
+ if (layer.beforeShowFieldsModal) {
1876
+ const hookInput = layer.beforeShowFieldsModal(field, value, input);
1877
+ if (!hookInput) {
1878
+ return [];
1879
+ }
1880
+ if (typeof hookInput === 'string') {
1881
+ input = new DOMParser().parseFromString(hookInput, 'text/html').body.childNodes[0];
1882
+ }
1883
+ else {
1884
+ input = hookInput;
1885
+ }
1886
+ }
1887
+ if (hasMultipleValues) {
1888
+ input.dataset.multiple = 'true';
1889
+ input.classList.add('ol-wfst--input-multiple');
1890
+ }
1891
+ return (jsxs("div", { className: "ol-wfst--input-field-container", children: [jsx("label", { className: "ol-wfst--input-field-label", htmlFor: key, children: key }), input] }));
1892
+ }) })] }));
1893
+ this._modal._html.header.innerHTML = modalTitle;
1494
1894
  this._modal.show();
1495
1895
  }
1896
+ _dispatch(type, features) {
1897
+ const evt = new VectorSourceEvent(type, features[0]);
1898
+ evt.features = features;
1899
+ this.dispatchEvent(evt);
1900
+ }
1496
1901
  }
1497
1902
 
1498
1903
  // Ol
@@ -1518,8 +1923,8 @@ class Geoserver extends BaseObject {
1518
1923
  wfsTransactionVersion: '1.1.0',
1519
1924
  projection: DEFAULT_GEOSERVER_SRS,
1520
1925
  lockFeatureParams: {
1521
- expiry: 5,
1522
- lockId: 'GeoServer',
1926
+ expiry: 5, // minutes
1927
+ lockId: 'WFST-editor',
1523
1928
  releaseAction: 'SOME'
1524
1929
  }
1525
1930
  },
@@ -1537,6 +1942,7 @@ class Geoserver extends BaseObject {
1537
1942
  this._insertFeatures = [];
1538
1943
  this._updateFeatures = [];
1539
1944
  this._deleteFeatures = [];
1945
+ this._lockedId = this._options.advanced.lockFeatureParams.lockId;
1540
1946
  // Formats
1541
1947
  this._formatWFS = new WFS();
1542
1948
  this._formatGeoJSON = new GeoJSON();
@@ -1875,7 +2281,7 @@ class Geoserver extends BaseObject {
1875
2281
  if (this.hasLockFeature &&
1876
2282
  this.getUseLockFeature() &&
1877
2283
  transactionType !== TransactionType.Insert) {
1878
- payload = payload.replace(`</Transaction>`, `<LockId>${this._options.advanced.lockFeatureParams.lockId}</LockId></Transaction>`);
2284
+ payload = payload.replace(`</Transaction>`, `<LockId>${this._lockedId}</LockId></Transaction>`);
1879
2285
  }
1880
2286
  const headers = Object.assign({ 'Content-Type': 'text/xml' }, this.getHeaders());
1881
2287
  const response = await fetch(this.getUrl(), {
@@ -1903,6 +2309,9 @@ class Geoserver extends BaseObject {
1903
2309
  if (transactionType !== TransactionType.Delete) {
1904
2310
  this._removeFeatures(features);
1905
2311
  }
2312
+ features.forEach((feature) => {
2313
+ removeFeatureFromEditList(feature);
2314
+ });
1906
2315
  wlayer.refresh();
1907
2316
  showLoading(false);
1908
2317
  this._insertFeatures = [];
@@ -1962,30 +2371,34 @@ class Geoserver extends BaseObject {
1962
2371
  const featureProperties = feature.getProperties();
1963
2372
  delete featureProperties.boundedBy;
1964
2373
  delete featureProperties._layerName_;
2374
+ delete featureProperties._editOverlayCoord_;
1965
2375
  const clone = new Feature(featureProperties);
1966
2376
  clone.setId(feature.getId());
1967
2377
  return clone;
1968
2378
  }
1969
2379
  /**
1970
- * Lock a feature in the geoserver. Useful before editing a geometry,
1971
- * to avoid changes from multiples suers
2380
+ * Lock one or several features in the geoserver. Useful before editing,
2381
+ * to avoid changes from multiples users. Locking several features at once
2382
+ * with a single request keeps the lock covered by one LockId
1972
2383
  *
1973
- * @param featureId
2384
+ * @param featureIds
1974
2385
  * @param layerName
1975
2386
  * @param retry
1976
2387
  * @public
1977
2388
  */
1978
- async lockFeature(featureId, layerName, retry = 0) {
2389
+ async lockFeature(featureIds, layerName, retry = 0) {
1979
2390
  const params = new URLSearchParams({
1980
2391
  service: 'wfs',
1981
2392
  version: this.getAdvanced().lockFeatureVersion,
1982
2393
  request: 'LockFeature',
1983
2394
  typeName: layerName,
1984
2395
  expiry: String(this._options.advanced.lockFeatureParams.expiry),
1985
- LockId: this._options.advanced.lockFeatureParams.lockId,
2396
+ handle: this._options.advanced.lockFeatureParams.lockId,
1986
2397
  releaseAction: this._options.advanced.lockFeatureParams.releaseAction,
1987
2398
  exceptions: 'application/json',
1988
- featureid: `${featureId}`
2399
+ featureid: Array.isArray(featureIds)
2400
+ ? featureIds.join(',')
2401
+ : String(featureIds)
1989
2402
  });
1990
2403
  const url_fetch = this.getUrl() + '?' + params.toString();
1991
2404
  try {
@@ -2006,7 +2419,7 @@ class Geoserver extends BaseObject {
2006
2419
  if (exceptions[0].code === 'CannotLockAllFeatures') {
2007
2420
  // Maybe the Feature is already blocked, ant thats trigger error, so, we try one locking more time again
2008
2421
  if (!retry) {
2009
- this.lockFeature(featureId, layerName, 1);
2422
+ this.lockFeature(featureIds, layerName, 1);
2010
2423
  }
2011
2424
  else {
2012
2425
  throw error;
@@ -2018,21 +2431,20 @@ class Geoserver extends BaseObject {
2018
2431
  }
2019
2432
  }
2020
2433
  catch (err) {
2021
- /*
2022
-
2023
- let dataDoc = (new window.DOMParser()).parseFromString(data, 'text/xml');
2024
-
2025
- let lockId = dataDoc.getElementsByTagName('wfs:LockId');
2026
-
2027
- let featuresLocked: HTMLCollectionOf<Element> = dataDoc.getElementsByTagName('ogc:FeatureId');
2028
-
2029
- for (let featureLocked of featuresLocked as any) {
2030
-
2031
- console.log(featureLocked.getAttribute('fid'));
2032
-
2434
+ // XML response (not JSON): parse the real lock id returned by
2435
+ // the server so the transaction references the lock that was
2436
+ // actually created instead of the configured one
2437
+ try {
2438
+ const dataDoc = new window.DOMParser().parseFromString(data, 'text/xml');
2439
+ const lockIdNode = dataDoc.getElementsByTagName('wfs:LockId')[0] ||
2440
+ dataDoc.getElementsByTagNameNS('http://www.opengis.net/wfs', 'LockId')[0];
2441
+ if (lockIdNode && lockIdNode.textContent) {
2442
+ this._lockedId = lockIdNode.textContent;
2443
+ }
2444
+ }
2445
+ catch (parseErr) {
2446
+ // Keep the configured lockId
2033
2447
  }
2034
-
2035
- */
2036
2448
  }
2037
2449
  return data;
2038
2450
  }
@@ -2056,34 +2468,107 @@ var GeoserverProperty;
2056
2468
  GeoserverProperty["ISLOADED"] = "isLoaded";
2057
2469
  })(GeoserverProperty || (GeoserverProperty = {}));
2058
2470
 
2059
- 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";
2471
+ function editFieldsSvg() {
2472
+ 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;
2473
+ }
2060
2474
 
2061
- 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";
2475
+ function editGeomSvg() {
2476
+ 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;
2477
+ }
2062
2478
 
2063
2479
  class EditOverlay extends Overlay {
2064
- constructor(feature, coordinate = null) {
2480
+ constructor(feature = null, coordinate = null, id) {
2065
2481
  super({
2066
- id: feature.getId(),
2067
- position: coordinate || getCenter(feature.getGeometry().getExtent()),
2482
+ id: id !== null && id !== void 0 ? id : (feature ? feature.getId() : undefined),
2483
+ position: coordinate ||
2484
+ (feature
2485
+ ? getCenter(feature.getGeometry().getExtent())
2486
+ : undefined),
2068
2487
  positioning: 'center-center',
2069
2488
  offset: [0, -40],
2070
2489
  stopEvent: true,
2071
- element: (createElement("div", null,
2072
- createElement("div", { className: "ol-wfst--edit-button-cnt", onClick: () => {
2073
- this.dispatchEvent('editFields');
2074
- } },
2075
- createElement("button", { className: "ol-wfst--edit-button", type: "button", title: I18N.labels.editFields },
2076
- createElement("img", { src: img$1, alt: I18N.labels.editFields }))),
2077
- createElement("div", { className: "ol-wfst--edit-button-cnt", onClick: () => {
2078
- this.dispatchEvent('editGeom');
2079
- } },
2080
- createElement("button", { class: "ol-wfst--edit-button", type: "button", title: I18N.labels.editGeom },
2081
- createElement("img", { src: img, alt: I18N.labels.editGeom })))))
2490
+ element: (jsxs("div", { children: [jsx("div", { className: "ol-wfst--edit-button-cnt", onClick: () => {
2491
+ this.dispatchEvent('editFields');
2492
+ }, 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: () => {
2493
+ this.dispatchEvent('editGeom');
2494
+ }, children: jsx("button", { class: "ol-wfst--edit-button", type: "button", title: I18N.labels.editGeom, children: editGeomSvg() }) })] }))
2495
+ });
2496
+ }
2497
+ }
2498
+
2499
+ function addLineSvg() {
2500
+ 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;
2501
+ }
2502
+
2503
+ class ExtendLineOverlay extends Overlay {
2504
+ constructor(feature, coordinate, side, component = 0, onClick) {
2505
+ super({
2506
+ position: coordinate,
2507
+ positioning: 'center-center',
2508
+ stopEvent: true,
2509
+ element: (jsx("div", { className: "ol-wfst--extend-button-cnt", children: jsx("button", { className: "ol-wfst--extend-button", type: "button", title: I18N.labels.continueLine, onClick: () => {
2510
+ onClick(side);
2511
+ }, children: addLineSvg() }) }))
2512
+ });
2513
+ this.feature = feature;
2514
+ this.side = side;
2515
+ this.component = component;
2516
+ }
2517
+ }
2518
+
2519
+ const QUERY_OVERLAY_ID = 'ol-wfst--query-overlay';
2520
+ /**
2521
+ * Popup overlay that shows the attributes of a queried feature and its
2522
+ * layer label. The geometry is highlighted separately on a dedicated layer.
2523
+ *
2524
+ * @extends {ol/Overlay~Overlay}
2525
+ * @param feature
2526
+ * @param coordinate
2527
+ * @param layer
2528
+ * @private
2529
+ */
2530
+ class QueryOverlay extends Overlay {
2531
+ constructor(feature, coordinate, layer) {
2532
+ const describeFeatureType = layer.getDescribeFeatureType();
2533
+ const parsed = describeFeatureType === null || describeFeatureType === void 0 ? void 0 : describeFeatureType._parsed;
2534
+ const fields = (parsed === null || parsed === void 0 ? void 0 : parsed.properties) || [];
2535
+ const geomField = parsed === null || parsed === void 0 ? void 0 : parsed.geomField;
2536
+ const rows = fields
2537
+ .filter((field) => field.name !== geomField)
2538
+ .map((field) => {
2539
+ let value = feature.get(field.name);
2540
+ if (value === undefined || value === null) {
2541
+ value = '';
2542
+ }
2543
+ else if (typeof value === 'object') {
2544
+ value = JSON.stringify(value);
2545
+ }
2546
+ 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) })] }));
2547
+ });
2548
+ super({
2549
+ id: QUERY_OVERLAY_ID,
2550
+ position: coordinate || getCenter(feature.getGeometry().getExtent()),
2551
+ positioning: 'bottom-center',
2552
+ offset: [0, -14],
2553
+ stopEvent: true,
2554
+ autoPan: true,
2555
+ 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: () => {
2556
+ this.dispatchEvent('close');
2557
+ }, 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 })] }))
2082
2558
  });
2083
2559
  }
2084
2560
  }
2085
2561
 
2562
+ function fullscreenSvg() {
2563
+ 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;
2564
+ }
2565
+
2566
+ function fullscreenExitSvg() {
2567
+ 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;
2568
+ }
2569
+
2086
2570
  const controlElement = document.createElement('div');
2571
+ const INIT_LOADING_TIMEOUT = 20000;
2087
2572
  /**
2088
2573
  * Tiny WFS-T client to insert (drawing/uploading), modify and delete
2089
2574
  * features on GeoServers using OpenLayers. Layers with these types
@@ -2112,6 +2597,14 @@ class Wfst extends Control {
2112
2597
  }
2113
2598
  });
2114
2599
  this._initialized = false;
2600
+ this._queryOverlayId = 'ol-wfst--query-overlay';
2601
+ this._shiftPressed = false;
2602
+ this._hoveredFeature = null;
2603
+ this._keySelectBound = false;
2604
+ // Editing
2605
+ this._editFeaturesOriginal = {};
2606
+ this._multiOverlayId = 'ol-wfst--multi-edit-overlay';
2607
+ this._extendOverlays = [];
2115
2608
  setLang(options.language, options.i18n);
2116
2609
  const defaultOptions = getDefaultOptions();
2117
2610
  this._options = deepObjectAssign(defaultOptions, options);
@@ -2120,7 +2613,7 @@ class Wfst extends Control {
2120
2613
  this._controlWidgetToolsDiv = controlElement;
2121
2614
  this._controlWidgetToolsDiv.className = 'ol-wfst--tools-control';
2122
2615
  this._uploads = new Uploads(this._options);
2123
- this._editFields = new EditFieldsModal(this._options.modal);
2616
+ this._editFields = new EditFieldsModal(this._options);
2124
2617
  }
2125
2618
  /**
2126
2619
  * Get all the layers in the ol-wfst instance
@@ -2152,8 +2645,10 @@ class Wfst extends Control {
2152
2645
  if (layers.length) {
2153
2646
  let layerRendered = 0;
2154
2647
  let layersNumber = 0; // Only count visibles
2648
+ showLoading();
2649
+ const layerInitPromises = [];
2155
2650
  layers.forEach((layer) => {
2156
- if (layer.getVisible())
2651
+ if (layer.isVisible(this._view))
2157
2652
  layersNumber++;
2158
2653
  layer.on('layerRendered', () => {
2159
2654
  layerRendered++;
@@ -2167,15 +2662,17 @@ class Wfst extends Control {
2167
2662
  }
2168
2663
  });
2169
2664
  layer.on('change:describeFeatureType', () => {
2170
- const domEl = this._layersControl.addLayerEl(layer);
2171
- layer.on('change:isVisible', () => {
2172
- const layerNotVisible = 'ol-wfst--layer-not-visible';
2173
- const visible = layer.isVisibleByZoom();
2174
- if (visible)
2175
- domEl.classList.remove(layerNotVisible);
2176
- else
2177
- domEl.classList.add(layerNotVisible);
2178
- });
2665
+ if (this._options.showControl && layers.length > 1) {
2666
+ const domEl = this._layersControl.addLayerEl(layer);
2667
+ layer.on('change:isVisible', () => {
2668
+ const layerNotVisible = 'ol-wfst--layer-not-visible';
2669
+ const visible = layer.isVisibleByZoom();
2670
+ if (visible)
2671
+ domEl.classList.remove(layerNotVisible);
2672
+ else
2673
+ domEl.classList.add(layerNotVisible);
2674
+ });
2675
+ }
2179
2676
  layer.set(BaseLayerProperty.ISVISIBLE, this._currentZoom > layer.getMinZoom());
2180
2677
  this.dispatchEvent(new WfstEvent({
2181
2678
  type: 'describeFeatureType',
@@ -2183,13 +2680,21 @@ class Wfst extends Control {
2183
2680
  data: layer.getDescribeFeatureType()
2184
2681
  }));
2185
2682
  });
2186
- layer._init();
2683
+ layerInitPromises.push(layer._init());
2187
2684
  this._map.addLayer(layer);
2188
2685
  setMapLayers({
2189
2686
  [layer.get(BaseLayerProperty.NAME)]: layer
2190
2687
  });
2191
2688
  });
2192
2689
  this._createMapElements(this._options.showControl, this._options.active);
2690
+ // Show the loading bar until every layer has resolved its
2691
+ // DescribeFeatureType (success or error), with a fail-safe
2692
+ // timeout so it never stays visible on a dead connection.
2693
+ await Promise.race([
2694
+ Promise.all(layerInitPromises),
2695
+ new Promise((resolve) => setTimeout(resolve, INIT_LOADING_TIMEOUT))
2696
+ ]);
2697
+ showLoading(false);
2193
2698
  }
2194
2699
  }
2195
2700
  catch (err) {
@@ -2222,17 +2727,26 @@ class Wfst extends Control {
2222
2727
  });
2223
2728
  });
2224
2729
  // @ts-expect-error
2225
- this._editFields.on('save', ({ feature }) => {
2226
- // Force deselect to trigger handler
2227
- this._collectionModify.remove(feature);
2730
+ this._editFields.on('save', async ({ feature, features }) => {
2731
+ const list = features || [feature];
2732
+ const ok = await this._transactEditList(list);
2733
+ if (ok) {
2734
+ Array.from(list).forEach((f) => this._collectionModify.remove(f));
2735
+ }
2228
2736
  });
2229
2737
  // @ts-expect-error
2230
- this._editFields.on('delete', ({ feature }) => {
2231
- this._deleteFeature(feature, true);
2738
+ this._editFields.on('delete', ({ features }) => {
2739
+ this._deleteFeature(features, true);
2232
2740
  });
2233
2741
  this._addMapEvents();
2742
+ this._addMiddleButtonPan();
2234
2743
  initModal(this._options['modal']);
2235
- this._controlWidgetToolsDiv.append(initLoading());
2744
+ this._layersWidgetDiv = document.createElement('div');
2745
+ this._layersWidgetDiv.className = 'ol-wfst--layers-control';
2746
+ this._layersWidgetDiv.append(initLoading());
2747
+ this._map.addControl(new Control({
2748
+ element: this._layersWidgetDiv
2749
+ }));
2236
2750
  this._initMapAndLayers();
2237
2751
  }
2238
2752
  /**
@@ -2248,8 +2762,18 @@ class Wfst extends Control {
2248
2762
  this._prepareEditLayer();
2249
2763
  this._addInteractions();
2250
2764
  this._addInteractionHandlers();
2765
+ this._prepareHighlightLayer();
2766
+ this._addQueryInteraction();
2251
2767
  if (showControl) {
2252
2768
  this._addMapControl();
2769
+ if (this._options.fullscreen) {
2770
+ this._map.addControl(new FullScreen({
2771
+ className: 'ol-wfst--fullscreen',
2772
+ tipLabel: I18N.labels.fullscreen,
2773
+ label: fullscreenSvg(),
2774
+ labelActive: fullscreenExitSvg()
2775
+ }));
2776
+ }
2253
2777
  }
2254
2778
  // By default, init in edit mode
2255
2779
  this.activateEditMode(active);
@@ -2268,9 +2792,10 @@ class Wfst extends Control {
2268
2792
  this._interactionWfsSelect = new Select({
2269
2793
  hitTolerance: 10,
2270
2794
  style: (feature) => styleFunction(feature),
2271
- toggleCondition: never,
2795
+ toggleCondition: shiftKeyOnly, // Allow adding features to the selection with shift
2272
2796
  filter: (feature, layer) => {
2273
2797
  return (getMode() !== Modes.Edit &&
2798
+ getMode() !== Modes.Query &&
2274
2799
  layer &&
2275
2800
  layer instanceof WfsLayer &&
2276
2801
  layer === getActiveLayerToInsertEls());
@@ -2279,7 +2804,12 @@ class Wfst extends Control {
2279
2804
  this._map.addInteraction(this._interactionWfsSelect);
2280
2805
  this._interactionWfsSelect.on('select', ({ selected, deselected, mapBrowserEvent }) => {
2281
2806
  const coordinate = mapBrowserEvent.coordinate;
2282
- if (selected.length) {
2807
+ // Clear hover to avoid keeping the style on the selected feature
2808
+ this._clearHoverState();
2809
+ if (getMode() === Modes.Query) {
2810
+ return;
2811
+ }
2812
+ if (selected.length) {
2283
2813
  selected.forEach((feature) => {
2284
2814
  if (!isFeatureEdited(feature)) {
2285
2815
  // Remove the feature from the original layer
@@ -2288,6 +2818,7 @@ class Wfst extends Control {
2288
2818
  this._addFeatureToEditMode(feature, coordinate, layer.get(BaseLayerProperty.NAME));
2289
2819
  }
2290
2820
  });
2821
+ this._lockSelectedFeatures();
2291
2822
  }
2292
2823
  if (deselected.length) {
2293
2824
  if (getMode() !== Modes.Edit) {
@@ -2309,18 +2840,33 @@ class Wfst extends Control {
2309
2840
  this._interactionSelectModify = new Select({
2310
2841
  style: (feature) => styleFunction(feature),
2311
2842
  layers: [getEditLayer()],
2312
- toggleCondition: never,
2843
+ toggleCondition: shiftKeyOnly, // Allow adding features with shift
2313
2844
  removeCondition: () => (getMode() === Modes.Edit ? true : false) // Prevent deselect on clicking outside the feature
2314
2845
  });
2315
2846
  this._map.addInteraction(this._interactionSelectModify);
2847
+ // When clicking without shift, replace the current selection: remove
2848
+ // any previously selected and not re-selected feature from the edit list
2849
+ this._interactionSelectModify.on('select', ({ selected, mapBrowserEvent }) => {
2850
+ if (!selected.length ||
2851
+ !mapBrowserEvent ||
2852
+ shiftKeyOnly(mapBrowserEvent)) {
2853
+ return;
2854
+ }
2855
+ const selectedSet = new Set(selected);
2856
+ Array.from(this._collectionModify.getArray()).forEach((feature) => {
2857
+ if (!selectedSet.has(feature)) {
2858
+ this._collectionModify.remove(feature);
2859
+ }
2860
+ });
2861
+ });
2316
2862
  this._collectionModify =
2317
2863
  this._interactionSelectModify.getFeatures();
2318
- this._keyClickWms = this._map.on(this._options.evtType, async (evt) => {
2864
+ this._keyClickWms = this._map.on(this._options.evtType || 'singleclick', async (evt) => {
2319
2865
  if (this._map.hasFeatureAtPixel(evt.pixel)) {
2320
2866
  return;
2321
2867
  }
2322
2868
  // Only get other features if editmode is disabled
2323
- if (getMode() !== Modes.Edit) {
2869
+ if (getMode() !== Modes.Edit && getMode() !== Modes.Query) {
2324
2870
  const layer = getActiveLayerToInsertEls();
2325
2871
  // If layer is hidden or is a wfs, skip
2326
2872
  if (!layer.getVisible() ||
@@ -2332,8 +2878,14 @@ class Wfst extends Control {
2332
2878
  if (!(features === null || features === void 0 ? void 0 : features.length)) {
2333
2879
  return;
2334
2880
  }
2335
- // For now, support is only for one feature at time
2881
+ // Without shift, replace the current selection
2882
+ if (!shiftKeyOnly(evt)) {
2883
+ Array.from(this._collectionModify.getArray()).forEach((feature) => {
2884
+ this._collectionModify.remove(feature);
2885
+ });
2886
+ }
2336
2887
  this._addFeatureToEditMode(features[0], evt.coordinate, layer.get(BaseLayerProperty.NAME));
2888
+ this._lockSelectedFeatures();
2337
2889
  }
2338
2890
  });
2339
2891
  };
@@ -2344,20 +2896,9 @@ class Wfst extends Control {
2344
2896
  prepareWmsInteraction();
2345
2897
  }
2346
2898
  this._interactionModify = new Modify({
2347
- style: () => {
2899
+ style: (feature) => {
2348
2900
  if (getMode() === Modes.Edit) {
2349
- return new Style$1({
2350
- image: new CircleStyle({
2351
- radius: 6,
2352
- fill: new Fill$1({
2353
- color: '#ff0000'
2354
- }),
2355
- stroke: new Stroke$1({
2356
- width: 2,
2357
- color: 'rgba(5, 5, 5, 0.9)'
2358
- })
2359
- })
2360
- });
2901
+ return editVertexStyles(feature.get('existing') === true);
2361
2902
  }
2362
2903
  else {
2363
2904
  return;
@@ -2373,6 +2914,175 @@ class Wfst extends Control {
2373
2914
  source: getEditLayer().getSource()
2374
2915
  });
2375
2916
  this._map.addInteraction(this._interactionSnap);
2917
+ this._addSelectionInteractions();
2918
+ }
2919
+ /**
2920
+ * Interactions to select features by drawing a box (DragBox) or a freehand
2921
+ * polygon (freehand Draw lasso). The selected features are added to the
2922
+ * edit list, keeping the same behaviour as the single click selection.
2923
+ *
2924
+ * @private
2925
+ */
2926
+ _addSelectionInteractions() {
2927
+ const selectionStyle = new Style$1({
2928
+ fill: new Fill$1({
2929
+ color: 'rgba(255, 200, 0, 0.1)'
2930
+ }),
2931
+ stroke: new Stroke$1({
2932
+ color: '#ffc800',
2933
+ width: 2,
2934
+ lineDash: [6, 4]
2935
+ })
2936
+ });
2937
+ // Box selection (dragging a rectangle)
2938
+ this._interactionDragBox = new DragBox({
2939
+ condition: (evt) => primaryAction(evt) && this._isSelectMode(SelectionMode.Box),
2940
+ className: 'ol-wfst--dragbox'
2941
+ });
2942
+ this._interactionDragBox.on('boxend', (evt) => {
2943
+ if (!this._isSelectMode(SelectionMode.Box)) {
2944
+ return;
2945
+ }
2946
+ // Without shift, replace the current selection
2947
+ if (!evt.mapBrowserEvent.originalEvent.shiftKey) {
2948
+ this._collectionModify.clear();
2949
+ }
2950
+ this._selectByGeometry(this._interactionDragBox.getGeometry());
2951
+ });
2952
+ this._map.addInteraction(this._interactionDragBox);
2953
+ // Freehand selection (holding the mouse to draw a closed lasso)
2954
+ this._interactionFreehandSelect = new Draw({
2955
+ type: GeometryType.Polygon,
2956
+ freehand: true,
2957
+ stopClick: true,
2958
+ style: selectionStyle,
2959
+ condition: (evt) => primaryAction(evt) && this._isSelectMode(SelectionMode.Freehand)
2960
+ });
2961
+ this._interactionFreehandSelect.on('drawend', (evt) => {
2962
+ if (!this._isSelectMode(SelectionMode.Freehand)) {
2963
+ return;
2964
+ }
2965
+ const geometry = evt.feature.getGeometry();
2966
+ const ring = geometry ? geometry.getCoordinates()[0] : [];
2967
+ // Ignore degenerate lassos (a simple click without dragging)
2968
+ if (!ring.length || ring.length < 4 || geometry.getArea() <= 0) {
2969
+ return;
2970
+ }
2971
+ // Without shift, replace the current selection
2972
+ if (!this._shiftPressed) {
2973
+ this._collectionModify.clear();
2974
+ }
2975
+ this._selectByGeometry(geometry);
2976
+ });
2977
+ this._map.addInteraction(this._interactionFreehandSelect);
2978
+ this._refreshSelectionInteractions();
2979
+ }
2980
+ /**
2981
+ * Whether a selection tool mode is currently usable: only outside the
2982
+ * edit/draw modes.
2983
+ *
2984
+ * @param mode
2985
+ * @private
2986
+ */
2987
+ _isSelectMode(mode) {
2988
+ return getMode() === null && getSelectionMode() === mode;
2989
+ }
2990
+ /**
2991
+ * Enable/disable the box and freehand selection interactions according to
2992
+ * the current selection tool and map mode.
2993
+ *
2994
+ * @private
2995
+ */
2996
+ _refreshSelectionInteractions() {
2997
+ const active = getMode() === null;
2998
+ if (this._interactionDragBox) {
2999
+ this._interactionDragBox.setActive(active && getSelectionMode() === SelectionMode.Box);
3000
+ }
3001
+ if (this._interactionFreehandSelect) {
3002
+ this._interactionFreehandSelect.setActive(active && getSelectionMode() === SelectionMode.Freehand);
3003
+ }
3004
+ }
3005
+ /**
3006
+ * Select the features that intersect the given polygon on the active layer
3007
+ * and add them to the edit list. WFS features are picked from the already
3008
+ * loaded source, while WMS features are requested to the GeoServer with a
3009
+ * WFS GetFeature INTERSECTS filter.
3010
+ *
3011
+ * @param geometry
3012
+ * @private
3013
+ */
3014
+ async _selectByGeometry(geometry) {
3015
+ const layer = getActiveLayerToInsertEls();
3016
+ if (!layer || !layer.getVisible() || !layer.isVisibleByZoom()) {
3017
+ showError(I18N.errors.layerNotVisible);
3018
+ return;
3019
+ }
3020
+ let features;
3021
+ if (layer instanceof WfsLayer) {
3022
+ const candidates = layer
3023
+ .getSource()
3024
+ .getFeaturesInExtent(geometry.getExtent());
3025
+ // The extent is only a bounding box; for the freehand lasso be
3026
+ // more precise and keep the features whose vertices fall inside it
3027
+ if (getSelectionMode() === SelectionMode.Freehand) {
3028
+ features = candidates.filter((feature) => {
3029
+ const featureGeom = feature.getGeometry();
3030
+ if (!featureGeom) {
3031
+ return false;
3032
+ }
3033
+ const flatCoords = featureGeom.getFlatCoordinates();
3034
+ const stride = featureGeom.getStride();
3035
+ for (let i = 0; i < flatCoords.length; i += stride) {
3036
+ if (geometry.intersectsCoordinate([
3037
+ flatCoords[i],
3038
+ flatCoords[i + 1]
3039
+ ])) {
3040
+ return true;
3041
+ }
3042
+ }
3043
+ return false;
3044
+ });
3045
+ }
3046
+ else {
3047
+ features = candidates;
3048
+ }
3049
+ }
3050
+ else if (layer instanceof WmsLayer) {
3051
+ features = await layer._getFeaturesInGeometry(geometry);
3052
+ }
3053
+ if (features) {
3054
+ this._selectFeatures(features);
3055
+ }
3056
+ }
3057
+ /**
3058
+ * Add the given features to the edit mode:
3059
+ * remove them from their original layer and push them into the edit
3060
+ * collection so they can be modified/deleted like the ones selected
3061
+ * with a single click.
3062
+ *
3063
+ * @param features
3064
+ * @private
3065
+ */
3066
+ _selectFeatures(features) {
3067
+ if (!features || !features.length) {
3068
+ return;
3069
+ }
3070
+ const layer = getActiveLayerToInsertEls();
3071
+ const layerName = layer.get(BaseLayerProperty.NAME);
3072
+ const selectedIds = new Set(this._collectionModify.getArray().map((feature) => feature.getId()));
3073
+ features.forEach((feature) => {
3074
+ if (isFeatureEdited(feature) || selectedIds.has(feature.getId())) {
3075
+ return;
3076
+ }
3077
+ // WFS features are stored in their layer source: remove them from
3078
+ // there while they are being edited
3079
+ if (layer instanceof WfsLayer &&
3080
+ layer.getSource().hasFeature(feature)) {
3081
+ layer.getSource().removeFeature(feature);
3082
+ }
3083
+ this._addFeatureToEditMode(feature, null, layerName);
3084
+ });
3085
+ this._lockSelectedFeatures();
2376
3086
  }
2377
3087
  /**
2378
3088
  * Layer to store temporary the elements to be edited
@@ -2381,6 +3091,149 @@ class Wfst extends Control {
2381
3091
  _prepareEditLayer() {
2382
3092
  this._map.addLayer(getEditLayer());
2383
3093
  }
3094
+ /**
3095
+ * Create the dedicated layer used to highlight the queried feature, so
3096
+ * the source layers keep their own rendering untouched.
3097
+ * @private
3098
+ */
3099
+ _prepareHighlightLayer() {
3100
+ this._highlightSource = new VectorSource();
3101
+ this._highlightLayer = new VectorLayer({
3102
+ source: this._highlightSource,
3103
+ style: (feature) => queryStyleFunction(),
3104
+ zIndex: 100
3105
+ });
3106
+ this._map.addLayer(this._highlightLayer);
3107
+ }
3108
+ /**
3109
+ * Listen for clicks while the query mode is active: get the clicked
3110
+ * feature of the active layer (from the already loaded source for WFS,
3111
+ * or through a GetFeatureInfo request for WMS), highlight it and show
3112
+ * its attributes in a popup.
3113
+ * @private
3114
+ */
3115
+ _addQueryInteraction() {
3116
+ this._keyClickQuery = this._map.on(this._options.evtType || 'singleclick', async (evt) => {
3117
+ if (getMode() !== Modes.Query) {
3118
+ return;
3119
+ }
3120
+ const layer = getActiveLayerToInsertEls();
3121
+ // Skip hidden layers
3122
+ if (!layer.getVisible() || !layer.isVisibleByZoom()) {
3123
+ return;
3124
+ }
3125
+ let feature = null;
3126
+ if (layer instanceof WfsLayer) {
3127
+ const featuresAtPixel = this._map.getFeaturesAtPixel(evt.pixel, {
3128
+ hitTolerance: 10,
3129
+ layerFilter: (candidate) => candidate === layer
3130
+ });
3131
+ if (featuresAtPixel && featuresAtPixel.length) {
3132
+ feature = featuresAtPixel[0];
3133
+ }
3134
+ }
3135
+ else if (layer instanceof WmsLayer) {
3136
+ const features = await layer._getFeaturesByClickEvent(evt);
3137
+ if (features === null || features === void 0 ? void 0 : features.length) {
3138
+ feature = features[0];
3139
+ }
3140
+ }
3141
+ // Click on empty space: clear the previous result
3142
+ if (!feature) {
3143
+ this._clearQueryResult();
3144
+ return;
3145
+ }
3146
+ this._showQueryResult(feature, layer, evt.coordinate);
3147
+ });
3148
+ }
3149
+ /**
3150
+ * Highlight the given feature and attach a popup with its attributes.
3151
+ * @param feature
3152
+ * @param layer
3153
+ * @param coordinate
3154
+ * @private
3155
+ */
3156
+ _showQueryResult(feature, layer, coordinate) {
3157
+ this._clearQueryResult();
3158
+ this._highlightSource.addFeature(feature);
3159
+ this._queryOverlay = new QueryOverlay(feature, coordinate, layer);
3160
+ // @ts-expect-error
3161
+ this._queryOverlay.on('close', () => {
3162
+ this._clearQueryResult();
3163
+ });
3164
+ this._map.addOverlay(this._queryOverlay);
3165
+ }
3166
+ /**
3167
+ * Remove the query popup overlay if any.
3168
+ * @private
3169
+ */
3170
+ _removeQueryOverlay() {
3171
+ if (this._queryOverlay) {
3172
+ this._map.removeOverlay(this._queryOverlay);
3173
+ this._queryOverlay = null;
3174
+ return;
3175
+ }
3176
+ const overlay = this._map.getOverlayById(this._queryOverlayId);
3177
+ if (overlay) {
3178
+ this._map.removeOverlay(overlay);
3179
+ }
3180
+ }
3181
+ /**
3182
+ * Clear the highlight and remove the query popup.
3183
+ * @private
3184
+ */
3185
+ _clearQueryResult() {
3186
+ var _a;
3187
+ (_a = this._highlightSource) === null || _a === void 0 ? void 0 : _a.clear();
3188
+ this._removeQueryOverlay();
3189
+ }
3190
+ /**
3191
+ * Activate/deactivate the query mode: while it is active, clicking a
3192
+ * feature of the active layer shows its info in a popup instead of
3193
+ * selecting it for editing.
3194
+ *
3195
+ * @param bool
3196
+ * @public
3197
+ */
3198
+ activateQueryMode(bool = true) {
3199
+ var _a, _b;
3200
+ if (bool) {
3201
+ // Leave any draw interaction active
3202
+ if (this._interactionDraw) {
3203
+ this._map.removeInteraction(this._interactionDraw);
3204
+ this._viewport.classList.remove('draw-mode');
3205
+ }
3206
+ resetStateButtons();
3207
+ deactivateSelectModeButton();
3208
+ activateQueryButton();
3209
+ this._clearHoverState();
3210
+ activateMode(Modes.Query);
3211
+ // Disable the edit selection interactions while querying
3212
+ if (this._interactionSelectModify) {
3213
+ this._interactionSelectModify.setActive(false);
3214
+ }
3215
+ if (this._interactionWfsSelect) {
3216
+ this._interactionWfsSelect.setActive(false);
3217
+ }
3218
+ (_a = this._interactionModify) === null || _a === void 0 ? void 0 : _a.setActive(false);
3219
+ this._syncExtendLineOverlays();
3220
+ this._refreshSelectionInteractions();
3221
+ }
3222
+ else {
3223
+ activateMode(null);
3224
+ deactivateQueryButton();
3225
+ activateSelectModeButton(getSelectionMode());
3226
+ if (this._interactionSelectModify) {
3227
+ this._interactionSelectModify.setActive(true);
3228
+ }
3229
+ if (this._interactionWfsSelect) {
3230
+ this._interactionWfsSelect.setActive(true);
3231
+ }
3232
+ (_b = this._interactionModify) === null || _b === void 0 ? void 0 : _b.setActive(true);
3233
+ this._refreshSelectionInteractions();
3234
+ }
3235
+ this._clearQueryResult();
3236
+ }
2384
3237
  /**
2385
3238
  * @private
2386
3239
  */
@@ -2395,16 +3248,74 @@ class Wfst extends Control {
2395
3248
  return;
2396
3249
  }
2397
3250
  if (key === 'Delete') {
2398
- const selectedFeatures = this._collectionModify;
2399
- if (selectedFeatures) {
2400
- selectedFeatures.forEach((feature) => {
2401
- this._deleteFeature(feature, true);
2402
- });
3251
+ const selectedFeatures = this._collectionModify.getArray();
3252
+ if (selectedFeatures.length) {
3253
+ this._deleteFeature(selectedFeatures, true);
2403
3254
  }
2404
3255
  }
2405
3256
  });
2406
3257
  };
2407
3258
  keyboardEvents();
3259
+ /**
3260
+ * Change the cursor to a pointing hand and highlight the feature
3261
+ * when hovering over a WFS feature of the active layer.
3262
+ * @private
3263
+ */
3264
+ const pointerMoveHandler = (evt) => {
3265
+ if (evt.dragging) {
3266
+ return;
3267
+ }
3268
+ // In edit mode, give pointer feedback on the editable
3269
+ // vertices/segments
3270
+ if (getMode() === Modes.Edit) {
3271
+ this._clearHoverState();
3272
+ this._updateEditCursor(evt);
3273
+ return;
3274
+ }
3275
+ // Only show the hover outside edit and draw modes
3276
+ if (getMode() !== null) {
3277
+ this._clearHoverState();
3278
+ return;
3279
+ }
3280
+ const layer = getActiveLayerToInsertEls();
3281
+ // Hover only works on visible WFS layers
3282
+ if (!layer ||
3283
+ !layer.getVisible() ||
3284
+ !layer.isVisibleByZoom() ||
3285
+ !(layer instanceof WfsLayer)) {
3286
+ this._clearHoverState();
3287
+ return;
3288
+ }
3289
+ const features = this._map.getFeaturesAtPixel(evt.pixel, {
3290
+ hitTolerance: 10,
3291
+ layerFilter: (candidate) => candidate === layer
3292
+ });
3293
+ if (features && features.length) {
3294
+ const feature = features[0];
3295
+ if (feature !== this._hoveredFeature) {
3296
+ this._clearHoverState();
3297
+ this._hoveredFeature = feature;
3298
+ this._hoveredFeature.setStyle((f) => styleFunction(f, true));
3299
+ }
3300
+ if (this._viewport.style.cursor !== 'pointer') {
3301
+ this._viewport.style.cursor = 'pointer';
3302
+ }
3303
+ }
3304
+ else {
3305
+ this._clearHoverState();
3306
+ }
3307
+ };
3308
+ this._map.on('pointermove', pointerMoveHandler);
3309
+ // Store the shift state when a gesture starts, so the freehand
3310
+ // selection can decide between adding or replacing the selection
3311
+ this._viewport.addEventListener('pointerdown', (evt) => {
3312
+ this._shiftPressed = evt.shiftKey;
3313
+ });
3314
+ // Clear the hover before firing a click, otherwise the Select interaction
3315
+ // would capture the hover style as the original style of the feature and
3316
+ // restore it on deselect, leaving the highlight permanently enabled.
3317
+ this._viewport.addEventListener('pointerdown', () => this._clearHoverState());
3318
+ this._viewport.addEventListener('pointerleave', () => this._clearHoverState(), { once: false });
2408
3319
  this._map.on('moveend', () => {
2409
3320
  this._currentZoom = this._view.getZoom();
2410
3321
  if (this._currentZoom !== this._lastZoom) {
@@ -2428,6 +3339,62 @@ class Wfst extends Control {
2428
3339
  }
2429
3340
  });
2430
3341
  }
3342
+ /**
3343
+ * Allow panning with the middle mouse button at all times, even when the
3344
+ * draw/freehand/box interactions are consuming the left button. The
3345
+ * middle pointerdown is intercepted in the capture phase so OpenLayers
3346
+ * never processes it and the active tool is not affected.
3347
+ *
3348
+ * @private
3349
+ */
3350
+ _addMiddleButtonPan() {
3351
+ const viewport = this._viewport;
3352
+ let panning = false;
3353
+ let lastX = 0;
3354
+ let lastY = 0;
3355
+ const isMiddleButton = (evt) => evt.pointerType === 'mouse' && evt.button === 1;
3356
+ const stopPanning = () => {
3357
+ panning = false;
3358
+ };
3359
+ viewport.addEventListener('pointerdown', (evt) => {
3360
+ if (!isMiddleButton(evt)) {
3361
+ return;
3362
+ }
3363
+ evt.preventDefault();
3364
+ evt.stopPropagation();
3365
+ viewport.setPointerCapture(evt.pointerId);
3366
+ panning = true;
3367
+ lastX = evt.clientX;
3368
+ lastY = evt.clientY;
3369
+ if (this._view.getAnimating()) {
3370
+ this._view.cancelAnimations();
3371
+ }
3372
+ }, true);
3373
+ viewport.addEventListener('pointermove', (evt) => {
3374
+ if (!panning) {
3375
+ return;
3376
+ }
3377
+ evt.preventDefault();
3378
+ const delta = [lastX - evt.clientX, evt.clientY - lastY];
3379
+ lastX = evt.clientX;
3380
+ lastY = evt.clientY;
3381
+ const resolution = this._view.getResolution();
3382
+ const rotation = this._view.getRotation();
3383
+ const cosAngle = Math.cos(rotation);
3384
+ const sinAngle = Math.sin(rotation);
3385
+ const x = (delta[0] * cosAngle - delta[1] * sinAngle) * resolution;
3386
+ const y = (delta[1] * cosAngle + delta[0] * sinAngle) * resolution;
3387
+ this._view.adjustCenterInternal([x, y]);
3388
+ });
3389
+ const endPan = () => {
3390
+ if (!panning) {
3391
+ return;
3392
+ }
3393
+ stopPanning();
3394
+ };
3395
+ viewport.addEventListener('pointerup', endPan);
3396
+ viewport.addEventListener('pointercancel', endPan);
3397
+ }
2431
3398
  /**
2432
3399
  * Add map handlers
2433
3400
  * @private
@@ -2437,13 +3404,19 @@ class Wfst extends Control {
2437
3404
  // This prevent events fired on select and deselect features that has no changes and should
2438
3405
  // not be updated in the geoserver
2439
3406
  this._interactionModify.on('modifyend', (evt) => {
2440
- const feature = evt.features.item(0);
2441
- addFeatureToEditedList(feature);
3407
+ evt.features.forEach((feature) => {
3408
+ addFeatureToEditedList(feature);
3409
+ });
2442
3410
  super.dispatchEvent(evt);
2443
3411
  });
2444
3412
  this._interactionModify.on('modifystart', (evt) => {
2445
3413
  super.dispatchEvent(evt);
2446
3414
  });
3415
+ // Keep the "continue drawing" buttons glued to the line endpoints
3416
+ // @ts-expect-error - 'modify' is a valid Modify event not typed in the `on` signature
3417
+ this._interactionModify.on('modify', () => {
3418
+ this._updateExtendOverlayPositions();
3419
+ });
2447
3420
  this._onDeselectFeatureEvent();
2448
3421
  this._onRemoveFeatureEvent();
2449
3422
  }
@@ -2455,6 +3428,9 @@ class Wfst extends Control {
2455
3428
  this._layersControl = new LayersControl(this._options.showUpload ? this._uploads : null, this._options.uploadFormats);
2456
3429
  // @ts-expect-error
2457
3430
  this._layersControl.on('drawMode', () => {
3431
+ if (getMode() === Modes.Query) {
3432
+ this.activateQueryMode(false);
3433
+ }
2458
3434
  if (getMode() === Modes.Draw) {
2459
3435
  resetStateButtons();
2460
3436
  this.activateEditMode();
@@ -2481,9 +3457,37 @@ class Wfst extends Control {
2481
3457
  this.activateDrawMode(getActiveLayerToInsertEls());
2482
3458
  }
2483
3459
  });
3460
+ [
3461
+ ['selectModeSingle', SelectionMode.Single],
3462
+ ['selectModeBox', SelectionMode.Box],
3463
+ ['selectModeFreehand', SelectionMode.Freehand]
3464
+ ].forEach(([evtName, mode]) => {
3465
+ // @ts-expect-error
3466
+ this._layersControl.on(evtName, () => {
3467
+ // Leaving the draw mode when a select tool is chosen
3468
+ if (getMode() === Modes.Draw) {
3469
+ resetStateButtons();
3470
+ this.activateEditMode();
3471
+ }
3472
+ // Leaving the query mode when a select tool is chosen
3473
+ if (getMode() === Modes.Query) {
3474
+ this.activateQueryMode(false);
3475
+ }
3476
+ setSelectionMode(mode);
3477
+ activateSelectModeButton(mode);
3478
+ this._refreshSelectionInteractions();
3479
+ });
3480
+ });
3481
+ // @ts-expect-error
3482
+ this._layersControl.on('queryMode', () => {
3483
+ this.activateQueryMode(getMode() !== Modes.Query);
3484
+ });
2484
3485
  const controlEl = this._layersControl.render();
2485
3486
  this._selectDraw = controlEl.querySelector('.wfst--tools-control--select-draw');
2486
- this._controlWidgetToolsDiv.append(controlEl);
3487
+ this._controlWidgetToolsDiv.append(controlEl.querySelector('.wfst--tools-control--head'));
3488
+ if (this._options.layers.length > 1) {
3489
+ this._layersWidgetDiv.append(controlEl.querySelector('.wfst--tools-control--select-layers'));
3490
+ }
2487
3491
  }
2488
3492
  /**
2489
3493
  *
@@ -2502,6 +3506,9 @@ class Wfst extends Control {
2502
3506
  _restoreFeatureToLayer(feature, layerName) {
2503
3507
  layerName = layerName || feature.get('_layerName_');
2504
3508
  const layer = getStoredMapLayers()[layerName];
3509
+ // Reset any per-feature style (e.g. hover) before returning the feature
3510
+ // to its layer, so it renders with the layer's default style.
3511
+ feature.setStyle(undefined);
2505
3512
  layer.getSource().addFeature(feature);
2506
3513
  }
2507
3514
  /**
@@ -2535,10 +3542,18 @@ class Wfst extends Control {
2535
3542
  this._removeFeatureFromTmpLayer(feature);
2536
3543
  }
2537
3544
  };
3545
+ if (this._keySelectBound) {
3546
+ return;
3547
+ }
3548
+ this._keySelectBound = true;
2538
3549
  // This is fired when a feature is deselected and fires the transaction process
3550
+ if (this._keySelect) {
3551
+ unByKey(this._keySelect);
3552
+ }
2539
3553
  this._keySelect = this._collectionModify.on('remove', (evt) => {
2540
3554
  const feature = evt.element;
2541
3555
  this._deselectEditFeature(feature);
3556
+ this._syncEditOverlays();
2542
3557
  checkIfFeatureIsChanged(feature);
2543
3558
  this._editModeOff();
2544
3559
  });
@@ -2559,6 +3574,7 @@ class Wfst extends Control {
2559
3574
  }
2560
3575
  if (this._keySelect) {
2561
3576
  unByKey(this._keySelect);
3577
+ this._keySelectBound = false;
2562
3578
  }
2563
3579
  const layerName = feature.get('_layerName_');
2564
3580
  const ll = this.getLayerByName(layerName);
@@ -2574,27 +3590,52 @@ class Wfst extends Control {
2574
3590
  }
2575
3591
  /**
2576
3592
  *
2577
- * @param feature
3593
+ * @param features
2578
3594
  * @private
2579
3595
  */
2580
- _editModeOn(feature) {
2581
- this._editFeatureOriginal = feature.clone();
3596
+ _editModeOn(features) {
3597
+ features = Array.isArray(features) ? features : [features];
3598
+ features.forEach((feature) => {
3599
+ this._editFeaturesOriginal[String(feature.getId())] =
3600
+ feature.clone();
3601
+ });
2582
3602
  activateMode(Modes.Edit);
3603
+ // Show the "continue drawing" buttons on the line endpoints
3604
+ this._syncExtendLineOverlays();
2583
3605
  // To refresh the style
2584
3606
  getEditLayer().getSource().changed();
2585
- this._removeOverlayHelper(feature);
2586
- this._controlApplyDiscardChanges = new EditControlChangesEl(feature);
2587
- this._controlApplyDiscardChanges.on('cancel', ({ feature }) => {
2588
- feature.setGeometry(this._editFeatureOriginal.getGeometry());
2589
- removeFeatureFromEditList(feature);
2590
- this._collectionModify.remove(feature);
3607
+ this._refreshSelectionInteractions();
3608
+ const multiOverlay = this._map.getOverlayById(this._multiOverlayId);
3609
+ if (multiOverlay) {
3610
+ this._map.removeOverlay(multiOverlay);
3611
+ }
3612
+ features.forEach((feature) => {
3613
+ this._removeOverlayHelper(feature);
3614
+ });
3615
+ this._controlApplyDiscardChanges = new EditControlChangesEl(features);
3616
+ this._controlApplyDiscardChanges.on('cancel', (evt) => {
3617
+ const list = evt.features;
3618
+ Array.from(list).forEach((feature) => {
3619
+ const original = this._editFeaturesOriginal[String(feature.getId())];
3620
+ if (original) {
3621
+ feature.setGeometry(original.getGeometry());
3622
+ }
3623
+ removeFeatureFromEditList(feature);
3624
+ this._collectionModify.remove(feature);
3625
+ });
2591
3626
  });
2592
- this._controlApplyDiscardChanges.on('apply', ({ feature }) => {
3627
+ this._controlApplyDiscardChanges.on('apply', async (evt) => {
2593
3628
  showLoading();
2594
- this._collectionModify.remove(feature);
3629
+ const list = evt.features;
3630
+ const ok = await this._transactEditList(list);
3631
+ if (ok) {
3632
+ Array.from(list).forEach((feature) => {
3633
+ this._collectionModify.remove(feature);
3634
+ });
3635
+ }
2595
3636
  });
2596
- this._controlApplyDiscardChanges.on('delete', ({ feature }) => {
2597
- this._deleteFeature(feature, true);
3637
+ this._controlApplyDiscardChanges.on('delete', (evt) => {
3638
+ this._deleteFeature(evt.features, true);
2598
3639
  });
2599
3640
  this._map.addControl(this._controlApplyDiscardChanges);
2600
3641
  }
@@ -2603,30 +3644,331 @@ class Wfst extends Control {
2603
3644
  */
2604
3645
  _editModeOff() {
2605
3646
  activateMode(null);
3647
+ this._destroyExtendLineOverlays();
2606
3648
  this._map.removeControl(this._controlApplyDiscardChanges);
3649
+ this._refreshSelectionInteractions();
3650
+ }
3651
+ /**
3652
+ * Get the first and last coordinate of every line component of a
3653
+ * LineString/MultiLineString geometry, including the line components
3654
+ * nested inside a GeometryCollection. Anything else returns an empty
3655
+ * array.
3656
+ *
3657
+ * @param geometry
3658
+ * @private
3659
+ */
3660
+ _getLineEndPoints(geometry) {
3661
+ const endPoints = [];
3662
+ const appendLine = (line, collection) => {
3663
+ if (line instanceof LineString) {
3664
+ if (line.getCoordinates().length) {
3665
+ endPoints.push({
3666
+ first: line.getFirstCoordinate(),
3667
+ last: line.getLastCoordinate(),
3668
+ line,
3669
+ lineIndex: 0,
3670
+ collection
3671
+ });
3672
+ }
3673
+ return;
3674
+ }
3675
+ line.getCoordinates().forEach((coordinates, lineIndex) => {
3676
+ if (coordinates.length) {
3677
+ endPoints.push({
3678
+ first: coordinates[0],
3679
+ last: coordinates[coordinates.length - 1],
3680
+ line,
3681
+ lineIndex,
3682
+ collection
3683
+ });
3684
+ }
3685
+ });
3686
+ };
3687
+ // "getGeometries()" returns deep clones (see ol GeometryCollection),
3688
+ // but the extend/merge must work on the actual members.
3689
+ const collect = (geometry, collection) => {
3690
+ if (geometry instanceof LineString ||
3691
+ geometry instanceof MultiLineString$1) {
3692
+ appendLine(geometry, collection);
3693
+ }
3694
+ else if (geometry instanceof GeometryCollection$1) {
3695
+ geometry.getGeometriesArray().forEach((member) => {
3696
+ collect(member, geometry);
3697
+ });
3698
+ }
3699
+ };
3700
+ collect(geometry);
3701
+ return endPoints;
3702
+ }
3703
+ /**
3704
+ * Remove every "continue drawing" overlay and abort any active line
3705
+ * extension draw.
3706
+ *
3707
+ * @private
3708
+ */
3709
+ _destroyExtendLineOverlays() {
3710
+ if (this._extendDraw) {
3711
+ this._finishExtendDraw();
3712
+ }
3713
+ this._extendOverlays.forEach((overlay) => {
3714
+ this._map.removeOverlay(overlay);
3715
+ });
3716
+ this._extendOverlays = [];
3717
+ }
3718
+ /**
3719
+ * Keep the "continue drawing" overlays in sync with the current edit
3720
+ * selection: they are only shown while editing LineString/MultiLineString
3721
+ * features, and they are repositioned on the endpoints of each line
3722
+ * component.
3723
+ *
3724
+ * @private
3725
+ */
3726
+ _syncExtendLineOverlays() {
3727
+ if (getMode() !== Modes.Edit || !this._collectionModify) {
3728
+ this._destroyExtendLineOverlays();
3729
+ return;
3730
+ }
3731
+ const items = this._collectionModify.getArray();
3732
+ // Remove overlays whose feature was deselected or lost its line geometry
3733
+ const removed = [];
3734
+ this._extendOverlays.forEach((overlay) => {
3735
+ const stale = !items.includes(overlay.feature) ||
3736
+ overlay.component >=
3737
+ this._getLineEndPoints(overlay.feature.getGeometry())
3738
+ .length;
3739
+ if (stale) {
3740
+ this._map.removeOverlay(overlay);
3741
+ removed.push(overlay);
3742
+ }
3743
+ });
3744
+ this._extendOverlays = this._extendOverlays.filter((overlay) => !removed.includes(overlay));
3745
+ // Add overlays for new LineString/MultiLineString features
3746
+ items.forEach((feature) => {
3747
+ this._getLineEndPoints(feature.getGeometry()).forEach(({ first, last }, component) => {
3748
+ ['start', 'end'].forEach((side) => {
3749
+ const exists = this._extendOverlays.some((overlay) => overlay.feature === feature &&
3750
+ overlay.component === component &&
3751
+ overlay.side === side);
3752
+ if (exists) {
3753
+ return;
3754
+ }
3755
+ const overlay = new ExtendLineOverlay(feature, side === 'start' ? first : last, side, component, () => this._startExtendLineDraw(feature, side, component));
3756
+ this._extendOverlays.push(overlay);
3757
+ this._map.addOverlay(overlay);
3758
+ });
3759
+ });
3760
+ });
3761
+ this._updateExtendOverlayPositions();
3762
+ }
3763
+ /**
3764
+ * Move the "continue drawing" overlays to the current position of their
3765
+ * line endpoints, e.g. after a vertex is dragged with the Modify
3766
+ * interaction.
3767
+ *
3768
+ * @private
3769
+ */
3770
+ _updateExtendOverlayPositions() {
3771
+ this._extendOverlays.forEach((overlay) => {
3772
+ const point = this._getLineEndPoints(overlay.feature.getGeometry())[overlay.component];
3773
+ if (!point) {
3774
+ return;
3775
+ }
3776
+ overlay.setPosition(overlay.side === 'start' ? point.first : point.last);
3777
+ });
3778
+ }
3779
+ /**
3780
+ * Start drawing a continuation of a line from one of its endpoints. The
3781
+ * sketch is anchored on the clicked vertex, so the next clicks append the
3782
+ * new points. A double click (or clicking back on the last point) ends
3783
+ * the drawing and merges the segment into the feature.
3784
+ *
3785
+ * @param feature
3786
+ * @param side 'start' prepends the new segment, 'end' appends it
3787
+ * @param component index of the clicked line component
3788
+ * @private
3789
+ */
3790
+ _startExtendLineDraw(feature, side, component = 0) {
3791
+ var _a;
3792
+ if (this._extendDraw || getMode() !== Modes.Edit) {
3793
+ return;
3794
+ }
3795
+ const points = this._getLineEndPoints(feature.getGeometry());
3796
+ if (!points.length || component >= points.length) {
3797
+ return;
3798
+ }
3799
+ const anchor = side === 'start' ? points[component].first : points[component].last;
3800
+ // While extending, the Modify/Select interactions must not grab the clicks
3801
+ (_a = this._interactionModify) === null || _a === void 0 ? void 0 : _a.setActive(false);
3802
+ if (this._interactionSelectModify) {
3803
+ this._interactionSelectModify.setActive(false);
3804
+ }
3805
+ if (this._interactionWfsSelect) {
3806
+ this._interactionWfsSelect.setActive(false);
3807
+ }
3808
+ this._extendDraw = new Draw({
3809
+ type: GeometryType.LineString,
3810
+ style: (f) => styleFunction(f),
3811
+ stopClick: true // To prevent firing a map/wms click
3812
+ });
3813
+ this._extendDraw.on('drawend', (evt) => {
3814
+ this._mergeExtendLine(feature, side, evt.feature, component);
3815
+ });
3816
+ this._extendDraw.on('drawabort', () => {
3817
+ this._finishExtendDraw();
3818
+ });
3819
+ this._map.addInteraction(this._extendDraw);
3820
+ // Anchor the sketch on the clicked vertex: the user only has to digitize
3821
+ // the continuation points (this also dispatches the "drawstart" event)
3822
+ this._extendDraw.appendCoordinates([anchor]);
3823
+ }
3824
+ /**
3825
+ * Merge the just-drawn segment into the edited feature's line geometry and
3826
+ * restore the editing interactions.
3827
+ *
3828
+ * @param feature
3829
+ * @param side
3830
+ * @param sketch
3831
+ * @param component index of the extended line component
3832
+ * @private
3833
+ */
3834
+ _mergeExtendLine(feature, side, sketch, component = 0) {
3835
+ const points = this._getLineEndPoints(feature.getGeometry());
3836
+ const point = points[component];
3837
+ if (!point) {
3838
+ this._finishExtendDraw();
3839
+ return;
3840
+ }
3841
+ const segment = sketch.getGeometry().getCoordinates();
3842
+ const { line, lineIndex } = point;
3843
+ if (line instanceof LineString) {
3844
+ const coords = line.getCoordinates();
3845
+ if (side === 'start') {
3846
+ line.setCoordinates([...segment.slice(1).reverse(), ...coords]);
3847
+ }
3848
+ else {
3849
+ line.setCoordinates([...coords, ...segment.slice(1)]);
3850
+ }
3851
+ }
3852
+ else if (line instanceof MultiLineString$1) {
3853
+ const coords = line.getCoordinates();
3854
+ if (side === 'start') {
3855
+ coords[lineIndex] = [
3856
+ ...segment.slice(1).reverse(),
3857
+ ...coords[lineIndex]
3858
+ ];
3859
+ }
3860
+ else {
3861
+ coords[lineIndex] = [...coords[lineIndex], ...segment.slice(1)];
3862
+ }
3863
+ line.setCoordinates(coords);
3864
+ }
3865
+ // Only mark the feature as edited when the segment added new vertices
3866
+ if (segment.length > 1) {
3867
+ addFeatureToEditedList(feature);
3868
+ }
3869
+ this._finishExtendDraw();
3870
+ this._syncExtendLineOverlays();
3871
+ }
3872
+ /**
3873
+ * Remove the active line extension draw and restore the interactions that
3874
+ * were disabled while drawing.
3875
+ *
3876
+ * @private
3877
+ */
3878
+ _finishExtendDraw() {
3879
+ var _a;
3880
+ if (this._extendDraw) {
3881
+ this._map.removeInteraction(this._extendDraw);
3882
+ this._extendDraw = null;
3883
+ }
3884
+ if (getMode() !== Modes.Edit) {
3885
+ return;
3886
+ }
3887
+ (_a = this._interactionModify) === null || _a === void 0 ? void 0 : _a.setActive(true);
3888
+ if (this._interactionSelectModify) {
3889
+ this._interactionSelectModify.setActive(true);
3890
+ }
3891
+ if (this._interactionWfsSelect) {
3892
+ this._interactionWfsSelect.setActive(true);
3893
+ }
3894
+ }
3895
+ /**
3896
+ * Send all the given features in a single WFS-T Update transaction per
3897
+ * layer, so a multi selection is saved with only one request. The "edited"
3898
+ * flag is cleared before transacting, so a deselect fired while the
3899
+ * request is in flight does not trigger a duplicate transaction.
3900
+ *
3901
+ * @param list
3902
+ * @returns false when there is nothing to transact or the transaction fails
3903
+ * @private
3904
+ */
3905
+ async _transactEditList(list) {
3906
+ if (!list.length) {
3907
+ return true;
3908
+ }
3909
+ const byLayer = {};
3910
+ list.forEach((feature) => {
3911
+ const layerName = feature.get('_layerName_');
3912
+ if (!byLayer[layerName]) {
3913
+ byLayer[layerName] = [];
3914
+ }
3915
+ byLayer[layerName].push(feature);
3916
+ });
3917
+ for (const layerName of Object.keys(byLayer)) {
3918
+ const layer = this._options.layers.find((layer) => layer.get(BaseLayerProperty.NAME) === layerName);
3919
+ if (!layer) {
3920
+ continue;
3921
+ }
3922
+ byLayer[layerName].forEach((feature) => {
3923
+ removeFeatureFromEditList(feature);
3924
+ });
3925
+ let ok;
3926
+ try {
3927
+ ok = await layer.transactFeatures(TransactionType.Update, byLayer[layerName]);
3928
+ }
3929
+ catch (err) {
3930
+ return false;
3931
+ }
3932
+ if (ok === false) {
3933
+ return false;
3934
+ }
3935
+ }
3936
+ return true;
2607
3937
  }
2608
3938
  /**
2609
- * Remove a feature from the edit Layer and from the Geoserver
3939
+ * Remove features from the edit Layer and from the Geoserver
2610
3940
  *
2611
3941
  * @param feature
3942
+ * @param confirm
2612
3943
  * @private
2613
3944
  */
2614
3945
  _deleteFeature(feature, confirm) {
2615
3946
  const deleteEl = () => {
3947
+ var _a;
2616
3948
  const features = Array.isArray(feature) ? feature : [feature];
2617
3949
  features.forEach((feature) => {
2618
3950
  feature.set('_delete_', true, true);
2619
3951
  getEditLayer().getSource().removeFeature(feature);
2620
3952
  });
2621
3953
  this._collectionModify.clear();
2622
- const layerName = feature.get('_layerName_');
2623
- const layer = this._options.layers.find((layer) => layer.get(BaseLayerProperty.NAME) === layerName);
3954
+ this._editModeOff();
3955
+ this._syncEditOverlays();
3956
+ const layerName = (_a = features[0]) === null || _a === void 0 ? void 0 : _a.get('_layerName_');
3957
+ const layer = layerName
3958
+ ? this._options.layers.find((layer) => layer.get(BaseLayerProperty.NAME) === layerName)
3959
+ : null;
2624
3960
  if (layer instanceof WfsLayer) {
2625
- this._interactionWfsSelect.getFeatures().remove(feature);
3961
+ features.forEach((feature) => {
3962
+ this._interactionWfsSelect.getFeatures().remove(feature);
3963
+ });
2626
3964
  }
2627
3965
  };
2628
3966
  if (confirm) {
2629
- const confirmModal = Modal.confirm(I18N.labels.confirmDelete, Object.assign({}, this._options.modal));
3967
+ const features = Array.isArray(feature) ? feature : [feature];
3968
+ const message = features.length > 1
3969
+ ? I18N_('confirmDeleteElements', features.length)
3970
+ : I18N.labels.confirmDelete;
3971
+ const confirmModal = Modal.confirm(message, Object.assign({}, this._options.modal));
2630
3972
  confirmModal.show().once('dismiss', function (modal, ev, button) {
2631
3973
  if (button && button.value) {
2632
3974
  deleteEl();
@@ -2646,33 +3988,102 @@ class Wfst extends Control {
2646
3988
  * @private
2647
3989
  */
2648
3990
  _addFeatureToEditMode(feature, coordinate = null, layerName = null) {
2649
- // For now, only allow one element at time
2650
- // @TODO: allow edit multiples elements
2651
- if (this._collectionModify.getLength())
2652
- return;
2653
3991
  if (layerName) {
2654
3992
  // Store the layer information inside the feature
2655
3993
  feature.set('_layerName_', layerName);
2656
3994
  }
3995
+ if (coordinate) {
3996
+ // Keep the click coordinate to position the overlay helper
3997
+ feature.set('_editOverlayCoord_', coordinate);
3998
+ }
2657
3999
  const props = feature ? feature.getProperties() : '';
2658
4000
  if (props) {
2659
4001
  if (feature.getGeometry()) {
2660
4002
  getEditLayer().getSource().addFeature(feature);
2661
4003
  this._collectionModify.push(feature);
2662
- const overlay = new EditOverlay(feature, coordinate);
2663
- // @ts-expect-error
2664
- overlay.on('editFields', () => {
2665
- this._editFields.show(feature);
2666
- });
2667
- // @ts-expect-error
2668
- overlay.on('editGeom', () => {
2669
- this._editModeOn(feature);
2670
- });
2671
- this._map.addOverlay(overlay);
2672
- const layer = getStoredLayer(layerName);
2673
- if (layer) {
2674
- layer.maybeLockFeature(feature.getId());
2675
- }
4004
+ this._syncEditOverlays();
4005
+ }
4006
+ }
4007
+ }
4008
+ /**
4009
+ * Lock the whole current selection with a single LockFeature request per
4010
+ * layer, so the transaction LockId covers all the selected features
4011
+ *
4012
+ * @private
4013
+ */
4014
+ async _lockSelectedFeatures() {
4015
+ const byLayer = {};
4016
+ this._collectionModify.getArray().forEach((selected) => {
4017
+ const layerName = selected.get('_layerName_');
4018
+ if (!layerName) {
4019
+ return;
4020
+ }
4021
+ (byLayer[layerName] || (byLayer[layerName] = [])).push(selected.getId());
4022
+ });
4023
+ const locks = [];
4024
+ Object.keys(byLayer).forEach((layerName) => {
4025
+ const layer = getStoredLayer(layerName);
4026
+ if (layer) {
4027
+ locks.push(layer.maybeLockFeature(byLayer[layerName]));
4028
+ }
4029
+ });
4030
+ if (locks.length) {
4031
+ await Promise.all(locks);
4032
+ }
4033
+ }
4034
+ /**
4035
+ * Create an EditOverlay helper and attach the edit listeners
4036
+ *
4037
+ * @param feature
4038
+ * @param coordinate
4039
+ * @param id
4040
+ * @private
4041
+ */
4042
+ _createEditOverlay(feature = null, coordinate = null, id = null) {
4043
+ coordinate =
4044
+ coordinate || (feature ? feature.get('_editOverlayCoord_') : null);
4045
+ const overlay = new EditOverlay(feature, coordinate, id);
4046
+ // @ts-expect-error
4047
+ overlay.on('editFields', () => {
4048
+ this._editFields.show(this._collectionModify.getArray());
4049
+ });
4050
+ // @ts-expect-error
4051
+ overlay.on('editGeom', () => {
4052
+ this._editModeOn(this._collectionModify.getArray());
4053
+ });
4054
+ this._map.addOverlay(overlay);
4055
+ }
4056
+ /**
4057
+ * Sync the edit overlay helpers with the current selection: a single
4058
+ * per-feature overlay while a feature is selected, or a unique overlay
4059
+ * centered between all the selected features when editing several at once
4060
+ *
4061
+ * @private
4062
+ */
4063
+ _syncEditOverlays() {
4064
+ if (getMode() === Modes.Edit) {
4065
+ return;
4066
+ }
4067
+ const multiOverlay = this._map.getOverlayById(this._multiOverlayId);
4068
+ if (multiOverlay) {
4069
+ this._map.removeOverlay(multiOverlay);
4070
+ }
4071
+ const items = this._collectionModify.getArray();
4072
+ if (items.length > 1) {
4073
+ items.forEach((feature) => {
4074
+ this._removeOverlayHelper(feature);
4075
+ });
4076
+ const extent = createEmpty();
4077
+ items.forEach((feature) => {
4078
+ extend(extent, feature.getGeometry().getExtent());
4079
+ });
4080
+ this._createEditOverlay(null, getCenter(extent), this._multiOverlayId);
4081
+ }
4082
+ else if (items.length === 1) {
4083
+ const feature = items[0];
4084
+ const featureId = feature.getId();
4085
+ if (featureId && !this._map.getOverlayById(featureId)) {
4086
+ this._createEditOverlay(feature);
2676
4087
  }
2677
4088
  }
2678
4089
  }
@@ -2719,15 +4130,20 @@ class Wfst extends Control {
2719
4130
  if (!layer.getVisible()) {
2720
4131
  return;
2721
4132
  }
4133
+ // Clear hover style while drawing
4134
+ this._clearHoverState();
2722
4135
  activateDrawButton();
4136
+ deactivateSelectModeButton();
2723
4137
  this._viewport.classList.add('draw-mode');
2724
4138
  addDrawInteraction(layer);
2725
4139
  }
2726
4140
  else {
2727
4141
  this._map.removeInteraction(this._interactionDraw);
2728
4142
  this._viewport.classList.remove('draw-mode');
4143
+ activateSelectModeButton(getSelectionMode());
2729
4144
  }
2730
4145
  activateMode(layer ? Modes.Draw : null);
4146
+ this._refreshSelectionInteractions();
2731
4147
  }
2732
4148
  /**
2733
4149
  * Activate/desactivate the edit mode
@@ -2743,6 +4159,8 @@ class Wfst extends Control {
2743
4159
  else {
2744
4160
  // Deselct features
2745
4161
  this._collectionModify.clear();
4162
+ // Clear hover state when leaving the edit mode
4163
+ this._clearHoverState();
2746
4164
  }
2747
4165
  if (this._interactionSelectModify) {
2748
4166
  this._interactionSelectModify.setActive(bool);
@@ -2750,6 +4168,61 @@ class Wfst extends Control {
2750
4168
  this._interactionModify.setActive(bool);
2751
4169
  if (this._interactionWfsSelect)
2752
4170
  this._interactionWfsSelect.setActive(bool);
4171
+ this._refreshSelectionInteractions();
4172
+ }
4173
+ /**
4174
+ * Reset the cursor and remove the hover style of the last hovered feature
4175
+ * @private
4176
+ */
4177
+ _clearHoverState() {
4178
+ if (this._viewport) {
4179
+ this._viewport.style.cursor = '';
4180
+ }
4181
+ if (this._hoveredFeature) {
4182
+ this._hoveredFeature.setStyle(undefined);
4183
+ this._hoveredFeature = null;
4184
+ }
4185
+ }
4186
+ /**
4187
+ * Change the map cursor while in edit mode: `move` when the pointer is
4188
+ * over an editable vertex and `copy` when it is over a segment (where a
4189
+ * new vertex can be inserted), mirroring the Modify interaction hit
4190
+ * tolerance.
4191
+ *
4192
+ * @param evt
4193
+ * @private
4194
+ */
4195
+ _updateEditCursor(evt) {
4196
+ if (!this._collectionModify) {
4197
+ return;
4198
+ }
4199
+ const tolerance = 12;
4200
+ const toleranceSq = tolerance * tolerance;
4201
+ const pixel = evt.pixel;
4202
+ for (const feature of this._collectionModify.getArray()) {
4203
+ const vertices = getFeatureVertices(feature);
4204
+ const coordinates = vertices ? vertices.getCoordinates() : [];
4205
+ if (!coordinates.length) {
4206
+ continue;
4207
+ }
4208
+ // Hovering an existing vertex (draggable handle)
4209
+ for (const coordinate of coordinates) {
4210
+ const vertexPixel = this._map.getPixelFromCoordinate(coordinate);
4211
+ if (squaredDistance(vertexPixel, pixel) <= toleranceSq) {
4212
+ this._viewport.style.cursor = 'move';
4213
+ return;
4214
+ }
4215
+ }
4216
+ // Hovering a segment (allows inserting a new vertex)
4217
+ for (let i = 0; i < coordinates.length - 1; i++) {
4218
+ const start = this._map.getPixelFromCoordinate(coordinates[i]);
4219
+ const end = this._map.getPixelFromCoordinate(coordinates[i + 1]);
4220
+ if (squaredDistanceToSegment(pixel, [start, end]) <= toleranceSq) {
4221
+ this._viewport.style.cursor = 'copy';
4222
+ return;
4223
+ }
4224
+ }
4225
+ }
2753
4226
  }
2754
4227
  /**
2755
4228
  * Remove the overlay helper atttached to a specify feature