@theseam/ui-common 2.0.1-beta.114 → 2.0.1-beta.119

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.
@@ -510,6 +510,27 @@ function mergeStyleOptions(target, source) {
510
510
  }
511
511
  return target;
512
512
  }
513
+ /**
514
+ * Whether the feature declines to opt OUT of `option`.
515
+ *
516
+ * Resolved per key, not per object: a selected feature consults
517
+ * `styleOptionsSelected` first and falls back to `styleOptions` only for a key
518
+ * `styleOptionsSelected` does not itself mention. Otherwise a feature
519
+ * declaring `styleOptionsSelected` for an unrelated reason, such as a
520
+ * different selected fill colour, would silently lose an unrelated
521
+ * `editable: false` from `styleOptions` the moment it is selected.
522
+ *
523
+ * Only an explicit `false` opts out. Shared by `computeFeatureStyle` and the
524
+ * delete gate in `GoogleMapsService`, so the lock a consumer declares means
525
+ * the same thing to both.
526
+ */
527
+ function featureAllows(feature, option) {
528
+ const selectedDeclared = isFeatureSelected(feature)
529
+ ? getSelectedStyleOptionsDefinedByFeature(feature)
530
+ : undefined;
531
+ const baseDeclared = getStyleOptionsDefinedByFeature(feature);
532
+ return (selectedDeclared?.[option] ?? baseDeclared?.[option]) !== false;
533
+ }
513
534
  /**
514
535
  * The style for a feature, in a fixed precedence order:
515
536
  *
@@ -537,37 +558,29 @@ function mergeStyleOptions(target, source) {
537
558
  * feature that opts out of one opts out of the other. This implication runs
538
559
  * only one way — `draggable: false` alone does not imply `editable: false`, so
539
560
  * a feature may still opt out of dragging while remaining reshapeable via its
540
- * vertex handles. Resolved through the same per-key `wants()` lookup as
541
- * `editable` itself, so a feature that locks `editable: false` in
542
- * `styleOptions` cannot be dragged even if `styleOptionsSelected` declares
543
- * `draggable` for an unrelated reason (or not at all).
561
+ * vertex handles. Resolved through `featureAllows`, which the delete gate
562
+ * shares, so a feature that locks `editable: false` in `styleOptions` cannot
563
+ * be dragged even if `styleOptionsSelected` declares `draggable` for an
564
+ * unrelated reason (or not at all).
544
565
  */
545
566
  function computeFeatureStyle(feature, context) {
546
567
  const selected = isFeatureSelected(feature);
547
- const baseDeclared = getStyleOptionsDefinedByFeature(feature);
548
- const selectedDeclared = selected
549
- ? getSelectedStyleOptionsDefinedByFeature(feature)
550
- : undefined;
551
568
  const options = FEATURE_STYLE_OPTIONS_DEFAULT();
552
569
  mergeStyleOptions(options, getStyleOptionsDefinedByFeature(feature));
553
570
  if (selected) {
554
571
  mergeStyleOptions(options, FEATURE_STYLE_OPTIONS_SELECTED());
555
572
  mergeStyleOptions(options, getSelectedStyleOptionsDefinedByFeature(feature));
556
573
  }
557
- // Resolved per key, not per object. A feature that opts out in
558
- // `styleOptions` must stay opted out even when it also declares a
559
- // `styleOptionsSelected` for unrelated reasons such as colour — otherwise
560
- // the clamp stops being one-directional and a retired field becomes
561
- // editable the moment it is selected.
562
- const wants = (option) => (selectedDeclared?.[option] ?? baseDeclared?.[option]) !== false;
563
574
  const editingArmed = context.editingEnabled && context.geometryEditingArmed && selected;
564
- const wantsEditable = wants('editable');
575
+ const wantsEditable = featureAllows(feature, 'editable');
565
576
  options.editable = wantsEditable && editingArmed;
566
577
  // editable: false implies draggable: false (see the doc comment above) —
567
- // the reverse does not hold, so wants('draggable') is still consulted on
568
- // its own for a feature that opts out of dragging alone.
569
- options.draggable = wants('draggable') && wantsEditable && editingArmed;
570
- options.clickable = wants('clickable') && context.clicksAllowed;
578
+ // the reverse does not hold, so 'draggable' is still resolved on its own
579
+ // for a feature that opts out of dragging alone.
580
+ options.draggable =
581
+ featureAllows(feature, 'draggable') && wantsEditable && editingArmed;
582
+ options.clickable =
583
+ featureAllows(feature, 'clickable') && context.clicksAllowed;
571
584
  return options;
572
585
  }
573
586
  /**
@@ -1091,7 +1104,7 @@ class GoogleMapsService {
1091
1104
  * delay-free discriminator: a stale click's timestamp should predate the
1092
1105
  * moment the draw finished. It does not hold up empirically. Driving real
1093
1106
  * mouse draws against the live Storybook and logging both the map `click`
1094
- * listener and `stopDrawing()` (see .superpowers/closing-click-report.md)
1107
+ * listener and `stopDrawing()` (see docs/superpowers/reports/closing-click-report.md)
1095
1108
  * caught the echo repeatedly, and every single time its `domEvent.timeStamp`
1096
1109
  * was a few milliseconds AFTER `stopDrawing()`'s own timestamp, not before
1097
1110
  * — indistinguishable from a genuinely fresh click by timestamp alone.
@@ -1117,6 +1130,21 @@ class GoogleMapsService {
1117
1130
  _labelProperty;
1118
1131
  _labelsOverlay;
1119
1132
  _warnedAboutLabelDisagreement = false;
1133
+ _canDelete;
1134
+ _deleteBlockedSubject = new Subject();
1135
+ /**
1136
+ * A delete that was attempted and refused. Fires only from the three delete
1137
+ * commands, never from their `canDelete*` queries — rendering a menu is not
1138
+ * an attempt.
1139
+ */
1140
+ deleteBlocked$ = this._deleteBlockedSubject.asObservable();
1141
+ /**
1142
+ * A file reached one of the import paths and could not be read. Plain
1143
+ * `Subject`, not `BehaviorSubject`: a consumer subscribing later should not
1144
+ * be handed a failure from before it was listening.
1145
+ */
1146
+ _fileImportErrorSubject = new Subject();
1147
+ fileImportError$ = this._fileImportErrorSubject.asObservable();
1120
1148
  _selectionSubject = new BehaviorSubject(null);
1121
1149
  /**
1122
1150
  * `distinctUntilChanged` collapses repeated `null`s. Several paths clear the
@@ -1171,6 +1199,8 @@ class GoogleMapsService {
1171
1199
  this._contextMenuTargetSubject.complete();
1172
1200
  this._editModeSubject.complete();
1173
1201
  this._interactionModeSubject.complete();
1202
+ this._deleteBlockedSubject.complete();
1203
+ this._fileImportErrorSubject.complete();
1174
1204
  this._groups = undefined;
1175
1205
  this._labelsOverlay?.destroy();
1176
1206
  this._labelsOverlay = undefined;
@@ -1226,7 +1256,7 @@ class GoogleMapsService {
1226
1256
  /**
1227
1257
  * Iterates the map's features and removes any that are selected.
1228
1258
  */
1229
- deleteSelection() {
1259
+ _removeSelection() {
1230
1260
  this._assertInitialized();
1231
1261
  const mapData = this.googleMap.data;
1232
1262
  mapData.forEach((f) => {
@@ -1236,13 +1266,201 @@ class GoogleMapsService {
1236
1266
  });
1237
1267
  // Every deleted feature was selected, so nothing should still read as
1238
1268
  // selected afterward. Re-sync `selection$` and `_focusedFeature` the same
1239
- // way `deleteFocusedFeature()` and `setData()` already do, rather than
1269
+ // way `_removeFocusedFeature()` and `setData()` already do, rather than
1240
1270
  // leaving them pointing at a group that no longer exists. In 'legacy'
1241
1271
  // mode nothing consumes selection$ today, and no remaining feature's raw
1242
1272
  // selected flag changes here (they were already false), so this is inert
1243
1273
  // there.
1244
1274
  this.clearSelection();
1245
1275
  }
1276
+ /**
1277
+ * The consumer's veto. Consulted for every delete on every path, and for
1278
+ * whether to offer a delete at all.
1279
+ */
1280
+ setCanDelete(predicate) {
1281
+ this._canDelete = predicate ?? undefined;
1282
+ }
1283
+ /**
1284
+ * Pair a set of features to remove with the target to consult `canDelete`
1285
+ * with. `null` when there is nothing to delete — distinct from a refusal,
1286
+ * and the reason no honest target exists to report.
1287
+ *
1288
+ * Builds `target.feature` itself rather than delegating the decision to
1289
+ * `_targetFor`. There, `feature: null` means only "no particular feature";
1290
+ * here it is a promise that the whole group is about to cease to exist.
1291
+ * Conflating the two is what let a delete of ONE polygon be consulted as a
1292
+ * group delete — see `ResolvedDeletion.unnamable`.
1293
+ */
1294
+ _deletionOf(key, removing) {
1295
+ if (key === null || removing.length === 0) {
1296
+ return null;
1297
+ }
1298
+ const resolved = this._registry.groupWithSources(key);
1299
+ if (!resolved) {
1300
+ return null;
1301
+ }
1302
+ // The empty-group invariant: a delete that leaves the group with no
1303
+ // features at all is a group delete, whichever path asked for it — so
1304
+ // `feature: null` always means "this group is about to cease to exist"
1305
+ // and a consumer never has to count features itself. Compared against
1306
+ // `featuresIn`, not `resolved.sources`, because a feature with
1307
+ // unsupported geometry is missing from `sources` but still occupies the
1308
+ // group.
1309
+ const all = this._registry.featuresIn(key);
1310
+ const emptiesGroup = all.every((f) => removing.indexOf(f) !== -1);
1311
+ if (emptiesGroup) {
1312
+ // `feature: null` is honest here whatever `removing` holds: the group
1313
+ // really is going, unsupported-geometry members included. This case is
1314
+ // never at risk, so it is never `unnamable`.
1315
+ return {
1316
+ removing,
1317
+ target: { group: resolved.group, feature: null },
1318
+ unnamable: false,
1319
+ };
1320
+ }
1321
+ // The group survives, so the target must name the polygon being removed.
1322
+ const index = resolved.sources.indexOf(removing[0]);
1323
+ if (index === -1) {
1324
+ // The feature being removed is absent from `sources`: its geometry is
1325
+ // neither Polygon nor MultiPolygon, so `groupWithSources` dropped it and
1326
+ // it has no emitted GeoJSON counterpart. It has never appeared in any
1327
+ // `group.features` the consumer has seen, so NO value of
1328
+ // `target.feature` describes it — which is exactly why this deletion is
1329
+ // `unnamable` and `_mayDeleteResolved()` refuses it outright whenever a
1330
+ // predicate is set.
1331
+ //
1332
+ // `feature: null` is the least dishonest option available. It overstates
1333
+ // the scope — it reads as "the field is going" when only one polygon
1334
+ // was asked for — but it names the right group, and it is only ever READ
1335
+ // on the `deleteBlocked$` emission that reports the refusal: the
1336
+ // predicate is refused before it is ever consulted with this target, and
1337
+ // with no predicate set nothing observes it at all. A target that only
1338
+ // ever reports a refusal cannot authorise a removal, so the overstatement
1339
+ // is inert. The alternative was emitting nothing, which would leave a
1340
+ // refused delete indistinguishable from a successful one on the design's
1341
+ // only feedback channel.
1342
+ return {
1343
+ removing,
1344
+ target: { group: resolved.group, feature: null },
1345
+ unnamable: true,
1346
+ };
1347
+ }
1348
+ return {
1349
+ removing,
1350
+ target: {
1351
+ group: resolved.group,
1352
+ feature: resolved.group.features[index],
1353
+ },
1354
+ unnamable: false,
1355
+ };
1356
+ }
1357
+ /** Resolves the same features `_removeSelection()` would remove. */
1358
+ _selectionDeletion() {
1359
+ this._assertInitialized();
1360
+ const removing = [];
1361
+ this.googleMap.data.forEach((f) => {
1362
+ if (isFeatureSelected(f)) {
1363
+ removing.push(f);
1364
+ }
1365
+ });
1366
+ if (removing.length === 0) {
1367
+ return null;
1368
+ }
1369
+ return this._deletionOf(this._registry.keyOf(removing[0]), removing);
1370
+ }
1371
+ /** Resolves the same features `_removeFocusedFeature()` would remove. */
1372
+ _focusedFeatureDeletion() {
1373
+ const focused = this._focusedFeature;
1374
+ if (focused === null) {
1375
+ return this._selectionDeletion();
1376
+ }
1377
+ return this._deletionOf(this._registry.keyOf(focused), [focused]);
1378
+ }
1379
+ /** Resolves the same features `_removeGroup(key)` would remove. */
1380
+ _groupDeletion(key) {
1381
+ return this._deletionOf(key, this._registry.featuresIn(key));
1382
+ }
1383
+ /**
1384
+ * Whether `removing` may be deleted: the feature-declared lock first, then
1385
+ * the consumer's predicate.
1386
+ *
1387
+ * MUST stay pure. It also answers menu-render questions, which are not
1388
+ * delete attempts — a side effect here would fire on every right-click.
1389
+ */
1390
+ _mayDelete(removing, target) {
1391
+ // A feature the consumer locked against reshaping must not be removable
1392
+ // by another route: deleting a polygon changes the map's value at least
1393
+ // as much as reshaping it does. Same precedent as editable: false
1394
+ // implying draggable: false in compute-feature-style.ts.
1395
+ if (removing.some((f) => !featureAllows(f, 'editable'))) {
1396
+ return false;
1397
+ }
1398
+ return this._canDelete?.(target) ?? true;
1399
+ }
1400
+ /**
1401
+ * Whether a resolved deletion may proceed. The single gate every query and
1402
+ * every command goes through.
1403
+ *
1404
+ * Wraps `_mayDelete` with the one refusal that cannot be phrased as a
1405
+ * question about a target: an `unnamable` delete. Consulting the predicate
1406
+ * there would mean LYING to it — handing it `feature: null`, which promises
1407
+ * the whole group is about to cease to exist, for a delete that removes one
1408
+ * polygon and leaves the rest standing. A consumer whose rule is "a field
1409
+ * may be deleted, an individual polygon may not" answers `true` to that and
1410
+ * loses a polygon it meant to keep. Fail closed exactly where a consumer's
1411
+ * rule could be subverted.
1412
+ *
1413
+ * Only when a predicate is actually set. With none there is nobody to lie
1414
+ * to, so there is nothing to protect — and refusing unconditionally would
1415
+ * change 'legacy' mode's behaviour for unsupported-geometry features, which
1416
+ * two applications depend on and this work must not drift.
1417
+ *
1418
+ * Pure, like `_mayDelete`: it also answers menu-render questions.
1419
+ */
1420
+ _mayDeleteResolved(deletion) {
1421
+ if (deletion.unnamable && this._canDelete !== undefined) {
1422
+ return false;
1423
+ }
1424
+ return this._mayDelete(deletion.removing, deletion.target);
1425
+ }
1426
+ /** Whether "Delete Polygon" (or the `Delete` key) may act. */
1427
+ canDeleteFocusedFeature() {
1428
+ if (!this.mapReady) {
1429
+ return false;
1430
+ }
1431
+ const deletion = this._focusedFeatureDeletion();
1432
+ return deletion !== null && this._mayDeleteResolved(deletion);
1433
+ }
1434
+ /** Whether "Delete Field" may act on `key`. */
1435
+ canDeleteGroup(key) {
1436
+ if (!this.mapReady) {
1437
+ return false;
1438
+ }
1439
+ const deletion = this._groupDeletion(key);
1440
+ return deletion !== null && this._mayDeleteResolved(deletion);
1441
+ }
1442
+ /** Whether the legacy "Delete" item (or the `Delete` key) may act. */
1443
+ canDeleteSelection() {
1444
+ if (!this.mapReady) {
1445
+ return false;
1446
+ }
1447
+ const deletion = this._selectionDeletion();
1448
+ return deletion !== null && this._mayDeleteResolved(deletion);
1449
+ }
1450
+ /**
1451
+ * Remove every selected feature, unless `canDelete` or a feature's own
1452
+ * `editable: false` refuses. A refused delete emits on `deleteBlocked$` and
1453
+ * changes nothing.
1454
+ */
1455
+ deleteSelection() {
1456
+ this._assertInitialized();
1457
+ const deletion = this._selectionDeletion();
1458
+ if (deletion !== null && !this._mayDeleteResolved(deletion)) {
1459
+ this._deleteBlockedSubject.next(deletion.target);
1460
+ return;
1461
+ }
1462
+ this._removeSelection();
1463
+ }
1246
1464
  /**
1247
1465
  * Delete every feature in `key`'s group. Backs the grouped "Delete Field"
1248
1466
  * context-menu item.
@@ -1256,7 +1474,7 @@ class GoogleMapsService {
1256
1474
  * selected, a coincidence this design has already stopped being true once
1257
1475
  * before and could again.
1258
1476
  */
1259
- deleteGroup(key) {
1477
+ _removeGroup(key) {
1260
1478
  this._assertInitialized();
1261
1479
  const mapData = this.googleMap.data;
1262
1480
  const wasSelected = this._selectionSubject.value?.group.key === key;
@@ -1278,6 +1496,20 @@ class GoogleMapsService {
1278
1496
  this._contextMenuTargetSubject.next(null);
1279
1497
  }
1280
1498
  }
1499
+ /**
1500
+ * Remove every feature in `key`'s group, unless `canDelete` or a feature's
1501
+ * own `editable: false` refuses. A refused delete emits on `deleteBlocked$`
1502
+ * and changes nothing.
1503
+ */
1504
+ deleteGroup(key) {
1505
+ this._assertInitialized();
1506
+ const deletion = this._groupDeletion(key);
1507
+ if (deletion !== null && !this._mayDeleteResolved(deletion)) {
1508
+ this._deleteBlockedSubject.next(deletion.target);
1509
+ return;
1510
+ }
1511
+ this._removeGroup(key);
1512
+ }
1281
1513
  /**
1282
1514
  * Whether polygon drawing mode is currently active.
1283
1515
  *
@@ -1601,6 +1833,14 @@ class GoogleMapsService {
1601
1833
  getFileInputHandler() {
1602
1834
  return this._fileInputHandler;
1603
1835
  }
1836
+ /**
1837
+ * Reports a file the library tried to import and could not read. Called by
1838
+ * the import controls, not by consumers — a consumer that owns importing
1839
+ * through `setFileInputHandler` reports its own failures.
1840
+ */
1841
+ notifyFileImportError(file, error) {
1842
+ this._fileImportErrorSubject.next({ file, error });
1843
+ }
1604
1844
  setInteractionMode(mode) {
1605
1845
  this._model =
1606
1846
  mode === 'grouped'
@@ -1635,6 +1875,51 @@ class GoogleMapsService {
1635
1875
  this._ensureLabelsOverlay();
1636
1876
  this._labelsOverlay?.refresh();
1637
1877
  }
1878
+ /**
1879
+ * Write `label` into `featureLabelProperty` on every feature in `key`'s
1880
+ * group, without re-adding any data. Returns `false` when no feature
1881
+ * carries `key`, or when no `featureLabelProperty` is configured — there is
1882
+ * then no property to write the label into.
1883
+ *
1884
+ * Unlike a write through the map's `value`, this does NOT go through
1885
+ * `setData()`, so the selection and the viewport are untouched. The
1886
+ * repaint and the value update fall out of the data layer's own
1887
+ * `setproperty` event, which `_initFeatureChangeListeners()` already turns
1888
+ * into a `_labelsOverlay.refresh()` and a `MapValueSource.FeatureChange`
1889
+ * emission.
1890
+ *
1891
+ * It DOES emit a value change, because the label is part of the GeoJSON and
1892
+ * the value genuinely changed. Writing that emitted value straight back
1893
+ * through the `value` input is inert: `MapValueManagerService.setValue`
1894
+ * finds the serialized form identical and returns without emitting.
1895
+ */
1896
+ setGroupLabel(key, label) {
1897
+ this._assertInitialized();
1898
+ const property = this._labelProperty;
1899
+ if (!property) {
1900
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
1901
+ console.warn(`[seam-google-maps] setGroupLabel("${key}") was called with no ` +
1902
+ `featureLabelProperty configured, so there is no property to ` +
1903
+ `write the label into. Nothing changed.`);
1904
+ }
1905
+ return false;
1906
+ }
1907
+ const features = this._registry.featuresIn(key);
1908
+ if (features.length === 0) {
1909
+ return false;
1910
+ }
1911
+ for (const feature of features) {
1912
+ // `setProperty` raises `setproperty` whether or not the value differs,
1913
+ // and each one costs a full re-serialization of the map's value. A
1914
+ // rename typed character by character would pay that per keystroke per
1915
+ // feature for writes that change nothing.
1916
+ if (feature.getProperty(property) === label) {
1917
+ continue;
1918
+ }
1919
+ feature.setProperty(property, label);
1920
+ }
1921
+ return true;
1922
+ }
1638
1923
  _ensureLabelsOverlay() {
1639
1924
  this._assertInitialized();
1640
1925
  if (this._labelsOverlay) {
@@ -1886,7 +2171,7 @@ class GoogleMapsService {
1886
2171
  * `feature` no longer exists — `_applySelection` naturally clears to null
1887
2172
  * when the group is now empty, via `groupWithSources`.
1888
2173
  */
1889
- deleteFocusedFeature() {
2174
+ _removeFocusedFeature() {
1890
2175
  this._assertInitialized();
1891
2176
  const key = this._focusedFeature
1892
2177
  ? this._registry.keyOf(this._focusedFeature)
@@ -1897,7 +2182,7 @@ class GoogleMapsService {
1897
2182
  this._focusedFeature = null;
1898
2183
  }
1899
2184
  else {
1900
- this.deleteSelection();
2185
+ this._removeSelection();
1901
2186
  }
1902
2187
  this._applySelection(key, null);
1903
2188
  // Same reasoning as deleteGroup(): a context-menu target naming this
@@ -1907,6 +2192,20 @@ class GoogleMapsService {
1907
2192
  this._contextMenuTargetSubject.next(null);
1908
2193
  }
1909
2194
  }
2195
+ /**
2196
+ * Remove the focused polygon — or, with nothing focused, the selection —
2197
+ * unless `canDelete` or a feature's own `editable: false` refuses. A
2198
+ * refused delete emits on `deleteBlocked$` and changes nothing.
2199
+ */
2200
+ deleteFocusedFeature() {
2201
+ this._assertInitialized();
2202
+ const deletion = this._focusedFeatureDeletion();
2203
+ if (deletion !== null && !this._mayDeleteResolved(deletion)) {
2204
+ this._deleteBlockedSubject.next(deletion.target);
2205
+ return;
2206
+ }
2207
+ this._removeFocusedFeature();
2208
+ }
1910
2209
  /** Escape cascades: cancel a draw, then clear selection, then leave edit mode. */
1911
2210
  handleEscape() {
1912
2211
  if (this.isDrawing()) {
@@ -1955,7 +2254,7 @@ class GoogleMapsService {
1955
2254
  // directly. By the time that second delivery arrives (confirmed
1956
2255
  // against the live Storybook: consistently a few milliseconds later,
1957
2256
  // comfortably within one animation frame — see
1958
- // .superpowers/closing-click-report.md), `isDrawing()` already reads
2257
+ // docs/superpowers/reports/closing-click-report.md), `isDrawing()` already reads
1959
2258
  // false, so the guard just below cannot tell it apart from a fresh
1960
2259
  // click on open map. `_suppressNextMapClick` exists to catch exactly
1961
2260
  // that echo; see its doc comment for why a `domEvent.timeStamp`
@@ -2109,7 +2408,7 @@ class GoogleMapsService {
2109
2408
  * Arm `_suppressNextMapClick`, and guarantee it cannot linger forever if
2110
2409
  * the echo it exists for never arrives — confirmed to be the normal case
2111
2410
  * for most real closes, and true of every synthetic/`play()`-driven draw,
2112
- * per .superpowers/closing-click-report.md. A `setTimeout` would "solve"
2411
+ * per docs/superpowers/reports/closing-click-report.md. A `setTimeout` would "solve"
2113
2412
  * this by guessing a safe wall-clock delay, which is exactly the kind of
2114
2413
  * timing window this fix is trying to avoid introducing. Two
2115
2414
  * `requestAnimationFrame` callbacks bound the window instead: the observed
@@ -2425,7 +2724,12 @@ class TheSeamGoogleMapsUploadButtonControlComponent {
2425
2724
  async _importFile(file) {
2426
2725
  const json = await readGeoFile(file);
2427
2726
  this._mapValueManager.setValue(json, MapValueSource.Input);
2428
- this._resetInput();
2727
+ }
2728
+ _reportImportError(file, error) {
2729
+ if (file === undefined) {
2730
+ return;
2731
+ }
2732
+ this._googleMaps.notifyFileImportError(file, error);
2429
2733
  }
2430
2734
  _createHiddenInput() {
2431
2735
  const fileInputElement = this._renderer.createElement('input');
@@ -2433,16 +2737,32 @@ class TheSeamGoogleMapsUploadButtonControlComponent {
2433
2737
  this._renderer.setAttribute(fileInputElement, 'hidden', '');
2434
2738
  this._renderer.setAttribute(fileInputElement, 'accept', '.json,.geojson,.shp,.zip');
2435
2739
  this._listeners.push(this._renderer.listen(fileInputElement, 'change', (event) => {
2436
- const file = this._getFile();
2740
+ let file;
2741
+ try {
2742
+ file = this._getFile();
2743
+ }
2744
+ catch (err) {
2745
+ // `_getFile` refuses a multi-file selection. Report it like any
2746
+ // other reason the chosen files could not be imported. `_getFile`
2747
+ // has already read `files` by this point, so resetting now cannot
2748
+ // disturb the read.
2749
+ this._reportImportError(this._fileInputElement.files?.[0], err);
2750
+ this._resetInput();
2751
+ return;
2752
+ }
2437
2753
  if (file === null) {
2438
2754
  return;
2439
2755
  }
2440
2756
  const fileImportHandler = this._googleMaps.getFileInputHandler();
2441
2757
  if (fileImportHandler) {
2758
+ // The consumer owns the file now, including reporting its failures.
2442
2759
  fileImportHandler(file);
2760
+ this._resetInput();
2443
2761
  }
2444
2762
  else {
2445
- this._importFile(file);
2763
+ this._importFile(file)
2764
+ .catch((err) => this._reportImportError(file, err))
2765
+ .finally(() => this._resetInput());
2446
2766
  }
2447
2767
  }));
2448
2768
  return fileInputElement;
@@ -2485,6 +2805,56 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
2485
2805
  args: ['click', ['event']]
2486
2806
  }] } });
2487
2807
 
2808
+ /**
2809
+ * The delete items the feature context menu offers.
2810
+ *
2811
+ * A refused item is not rendered at all. The caller is expected to hide the
2812
+ * menu entirely for an empty list, so a consumer that refuses everything gets
2813
+ * no menu rather than an empty one.
2814
+ */
2815
+ function buildDeleteMenuItems(context) {
2816
+ const items = [];
2817
+ if (!context.editingEnabled) {
2818
+ return items;
2819
+ }
2820
+ if (context.mode !== 'grouped') {
2821
+ if (context.canDeleteSelection()) {
2822
+ items.push({ label: 'Delete', action: () => context.deleteSelection() });
2823
+ }
2824
+ return items;
2825
+ }
2826
+ const target = context.target;
2827
+ if (!target) {
2828
+ return items;
2829
+ }
2830
+ const key = target.group.key;
2831
+ // On a one-polygon field, "Delete Polygon" and "Delete Field" are the same
2832
+ // act — the first empties the group without saying so. Offer only the one
2833
+ // that names what happens.
2834
+ if (target.group.features.length <= 1) {
2835
+ if (context.canDeleteGroup(key)) {
2836
+ items.push({
2837
+ label: 'Delete Field',
2838
+ action: () => context.deleteGroup(key),
2839
+ });
2840
+ }
2841
+ return items;
2842
+ }
2843
+ if (context.canDeleteFocusedFeature()) {
2844
+ items.push({
2845
+ label: 'Delete Polygon',
2846
+ action: () => context.deleteFocusedFeature(),
2847
+ });
2848
+ }
2849
+ if (context.canDeleteGroup(key)) {
2850
+ items.push({
2851
+ label: 'Delete Field',
2852
+ action: () => context.deleteGroup(key),
2853
+ });
2854
+ }
2855
+ return items;
2856
+ }
2857
+
2488
2858
  class TheSeamGoogleMapsApiLoader {
2489
2859
  }
2490
2860
 
@@ -2577,16 +2947,55 @@ class TheSeamMapFileDropComponent {
2577
2947
  event.preventDefault();
2578
2948
  event.stopPropagation();
2579
2949
  this._renderer.setStyle(this._elementRef.nativeElement, 'display', 'none');
2580
- // TODO: Show error if multiple files?
2581
- if (!this._isSupportedDataTransfer(event.dataTransfer)) {
2950
+ if (!this._isSupportedDataTransferTypes(event.dataTransfer)) {
2951
+ // Not a file drag at all — selected text, a link, an image dragged out
2952
+ // of a page. Nobody asked for an import and there is no file to name,
2953
+ // so this stays silent rather than reporting a failure.
2954
+ return;
2955
+ }
2956
+ const files = event.dataTransfer.files;
2957
+ if (files.length !== 1) {
2958
+ if (files.length > 1) {
2959
+ // Refused before any handler is consulted, exactly as the upload
2960
+ // button's `_getFile` does: `fileImportHandler` takes a single file
2961
+ // and cannot be handed several, so the consumer never sees these and
2962
+ // this output is its only way to learn the drop was rejected. Same
2963
+ // message as the button, so a consumer matching on it needs one
2964
+ // string rather than two.
2965
+ this._ngZone.run(() => this._googleMaps.notifyFileImportError(files[0], Error('Only one file can be imported at a time.')));
2966
+ }
2582
2967
  return;
2583
2968
  }
2584
2969
  const item = event.dataTransfer.items[0];
2585
2970
  const file = item.getAsFile();
2586
- readGeoFile(file).then((json) => {
2971
+ if (file === null) {
2972
+ return;
2973
+ }
2974
+ // These listeners are registered outside Angular's zone, so re-enter it
2975
+ // before calling back into anything that expects change detection — the
2976
+ // consumer's handler may well render a message from here.
2977
+ this._ngZone.run(() => this._importDroppedFile(file));
2978
+ };
2979
+ /**
2980
+ * Routes a dropped file the same way the upload button routes a chosen one,
2981
+ * so `fileImportHandler` means "the consumer owns imported files" whichever
2982
+ * way the file arrived.
2983
+ */
2984
+ _importDroppedFile(file) {
2985
+ const fileImportHandler = this._googleMaps.getFileInputHandler();
2986
+ if (fileImportHandler) {
2987
+ // The consumer owns the file now, including reporting its failures.
2988
+ fileImportHandler(file);
2989
+ return;
2990
+ }
2991
+ readGeoFile(file)
2992
+ .then((json) => {
2587
2993
  this._mapValueManager.setValue(json, MapValueSource.Input);
2994
+ })
2995
+ .catch((error) => {
2996
+ this._googleMaps.notifyFileImportError(file, error);
2588
2997
  });
2589
- };
2998
+ }
2590
2999
  _handleDragEnterEvent = (event) => {
2591
3000
  if (!this._dropAllowed()) {
2592
3001
  return;
@@ -2605,9 +3014,6 @@ class TheSeamMapFileDropComponent {
2605
3014
  _dropAllowed() {
2606
3015
  return !this._globalDragInProgress;
2607
3016
  }
2608
- _isSupportedDataTransfer(dataTransfer) {
2609
- return dataTransfer.files.length === 1 && dataTransfer.types[0] === 'Files';
2610
- }
2611
3017
  _isSupportedDataTransferTypes(dataTransfer) {
2612
3018
  return dataTransfer.types[0] === 'Files';
2613
3019
  }
@@ -2720,7 +3126,8 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
2720
3126
  _fileUploadControlDef = {
2721
3127
  component: TheSeamGoogleMapsUploadButtonControlComponent,
2722
3128
  data: { label: 'Import Geo File', icon: faFileImport },
2723
- position: 6 /* google.maps.ControlPosition.LEFT_BOTTOM */,
3129
+ // position: 6 /* google.maps.ControlPosition.LEFT_BOTTOM */,
3130
+ position: 7, // Below top-right fullscreen button
2724
3131
  };
2725
3132
  _reCenterControlDef = {
2726
3133
  component: TheSeamGoogleMapsRecenterButtonControlComponent,
@@ -2758,6 +3165,17 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
2758
3165
  streetViewControlEnabled = false;
2759
3166
  allowDrawingHoleInPolygon = false;
2760
3167
  editingEnabled = true;
3168
+ /**
3169
+ * Intercepts a file the user chose or dropped, in place of the library's own
3170
+ * import handling. Covers **both** import paths: a file picked through the
3171
+ * upload button and one dropped onto the map (`fileDropEnabled` defaults to
3172
+ * `true`, so setting this now also intercepts drops a consumer relying on
3173
+ * the default previously had handled internally).
3174
+ *
3175
+ * Once called, the consumer owns the file, including reporting any failure
3176
+ * in its own UI — `fileImportError` will not fire for a file handed to this
3177
+ * handler.
3178
+ */
2761
3179
  set fileImportHandler(value) {
2762
3180
  this._googleMaps.setFileInputHandler(value);
2763
3181
  }
@@ -2796,9 +3214,46 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
2796
3214
  newGroupKeyFactory;
2797
3215
  /** Preselect a group. Applied on map-ready and after each external value write. */
2798
3216
  selectedGroupKey = null;
3217
+ /**
3218
+ * Vetoes a delete. Return `false` to refuse the target.
3219
+ *
3220
+ * `target.feature === null` means the whole group is about to cease to
3221
+ * exist — every polygon of it is going, however the delete was asked for.
3222
+ * A non-null `feature` means one polygon is being removed from a group that
3223
+ * survives.
3224
+ *
3225
+ * **Consulted when no delete is happening.** It decides whether to render
3226
+ * the context-menu items, so it runs on every context-menu open. A
3227
+ * predicate that logs an attempt or flips state will do so on every
3228
+ * right-click. It must also be deterministic for a given target, or the
3229
+ * menu and the `Delete` key can disagree about the same target.
3230
+ *
3231
+ * That is a correctness warning, not a performance one. The context-menu
3232
+ * target changes only on a right-click, a delete, and a value write, so
3233
+ * this runs a handful of times per gesture.
3234
+ *
3235
+ * Passing nothing refuses nothing, which is the behaviour this component
3236
+ * has always had.
3237
+ */
3238
+ canDelete;
2799
3239
  mapReady = new EventEmitter();
2800
3240
  selectionChange = new EventEmitter();
2801
3241
  featureHoverChange = new EventEmitter();
3242
+ /**
3243
+ * A delete was attempted and refused — in practice, the `Delete` key, since
3244
+ * a refused menu item is never rendered. Emits the target that was refused
3245
+ * so the consumer can explain why; only the consumer knows the reason.
3246
+ */
3247
+ deleteBlocked = new EventEmitter();
3248
+ /**
3249
+ * A file reached one of the import paths and could not be read — a corrupt
3250
+ * archive, a `.zip` of something else, malformed GeoJSON. Emitted so the
3251
+ * consumer can say so in its own UI; the library has nowhere to show it.
3252
+ *
3253
+ * Not emitted for a file a `fileImportHandler` took: once the consumer owns
3254
+ * the file, it owns reporting the outcome too.
3255
+ */
3256
+ fileImportError = new EventEmitter();
2802
3257
  featureContextMenu;
2803
3258
  _options = {
2804
3259
  mapTypeControl: true,
@@ -2851,39 +3306,33 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
2851
3306
  this._googleMaps.hover$
2852
3307
  .pipe(skip(1), tap((hover) => this.featureHoverChange.emit(hover)), takeUntil(this._ngUnsubscribe))
2853
3308
  .subscribe();
3309
+ // No `skip(1)` here, unlike `selection$` and `hover$`: `deleteBlocked$` is
3310
+ // a plain Subject with no replayed initial value to drop.
3311
+ this._googleMaps.deleteBlocked$
3312
+ .pipe(tap((target) => this.deleteBlocked.emit(target)), takeUntil(this._ngUnsubscribe))
3313
+ .subscribe();
3314
+ // As with `deleteBlocked$`, a plain Subject with nothing to replay, so no
3315
+ // `skip(1)`.
3316
+ this._googleMaps.fileImportError$
3317
+ .pipe(tap((value) => this.fileImportError.emit(value)), takeUntil(this._ngUnsubscribe))
3318
+ .subscribe();
2854
3319
  this._contextMenuItems$ = combineLatest([
2855
3320
  this._googleMaps.editingEnabled$,
2856
- // The RIGHT-CLICKED feature's group. In 'grouped' mode the menu now
2857
- // only opens when that group is already the selection (see
2858
- // `GroupedInteractionModel.allowsContextMenu()`), so this and
2859
- // `selection$` always agree here — but this is still the one that
2860
- // names what "Delete Field" is conceptually acting on, independent of
2861
- // that coincidence. See `contextMenuTarget$`'s doc comment.
3321
+ // The RIGHT-CLICKED feature's group — what "Delete Field" is
3322
+ // conceptually acting on, independent of what happens to be selected.
3323
+ // See `contextMenuTarget$`'s doc comment.
2862
3324
  this._googleMaps.contextMenuTarget$,
2863
- ]).pipe(map(([enabled, target]) => {
2864
- const items = [];
2865
- if (!enabled) {
2866
- return items;
2867
- }
2868
- if (this.interactionMode === 'grouped') {
2869
- items.push({
2870
- label: 'Delete Polygon',
2871
- action: () => this._googleMaps.deleteFocusedFeature(),
2872
- });
2873
- if (target && target.group.features.length > 1) {
2874
- items.push({
2875
- label: 'Delete Field',
2876
- action: () => this._googleMaps.deleteGroup(target.group.key),
2877
- });
2878
- }
2879
- return items;
2880
- }
2881
- items.push({
2882
- label: 'Delete',
2883
- action: () => this._onClickDeleteFeature(),
2884
- });
2885
- return items;
2886
- }), tap((items) => {
3325
+ ]).pipe(map(([editingEnabled, target]) => buildDeleteMenuItems({
3326
+ mode: this.interactionMode,
3327
+ editingEnabled,
3328
+ target,
3329
+ canDeleteFocusedFeature: () => this._googleMaps.canDeleteFocusedFeature(),
3330
+ canDeleteGroup: (key) => this._googleMaps.canDeleteGroup(key),
3331
+ canDeleteSelection: () => this._googleMaps.canDeleteSelection(),
3332
+ deleteFocusedFeature: () => this._googleMaps.deleteFocusedFeature(),
3333
+ deleteGroup: (key) => this._googleMaps.deleteGroup(key),
3334
+ deleteSelection: () => this._googleMaps.deleteSelection(),
3335
+ })), tap((items) => {
2887
3336
  if (items.length === 0) {
2888
3337
  this._googleMaps.setFeatureContextMenu(null);
2889
3338
  }
@@ -2985,6 +3434,9 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
2985
3434
  if (Object.prototype.hasOwnProperty.call(changes, 'selectedGroupKey')) {
2986
3435
  this._applySelectedGroupKey();
2987
3436
  }
3437
+ if (Object.prototype.hasOwnProperty.call(changes, 'canDelete')) {
3438
+ this._googleMaps.setCanDelete(this.canDelete);
3439
+ }
2988
3440
  if (Object.prototype.hasOwnProperty.call(changes, 'featureLabelProperty')) {
2989
3441
  this._googleMaps.setLabelProperty(this.featureLabelProperty);
2990
3442
  }
@@ -3058,9 +3510,6 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
3058
3510
  this.idleListener?.remove();
3059
3511
  });
3060
3512
  }
3061
- _onClickDeleteFeature() {
3062
- this._googleMaps.deleteSelection();
3063
- }
3064
3513
  // The service delegates below guard on `mapReady` themselves, the same way
3065
3514
  // `_applySelectedGroupKey()` already does for the declarative
3066
3515
  // `selectedGroupKey` input. Their service-side counterparts call
@@ -3101,6 +3550,19 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
3101
3550
  }
3102
3551
  return this._googleMaps.getGroups();
3103
3552
  }
3553
+ /**
3554
+ * Set a group's label text in place. Returns false when no such group
3555
+ * exists, or when no `featureLabelProperty` is set.
3556
+ *
3557
+ * Does not clear the selection or re-fit the viewport, so it is safe to
3558
+ * call while the user is renaming the very group being displayed.
3559
+ */
3560
+ setGroupLabel(key, label) {
3561
+ if (!this._googleMaps.mapReady) {
3562
+ return false;
3563
+ }
3564
+ return this._googleMaps.setGroupLabel(key, label);
3565
+ }
3104
3566
  setEditMode(enabled) {
3105
3567
  this._googleMaps.setEditMode(enabled);
3106
3568
  }
@@ -3108,7 +3570,7 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
3108
3570
  return this._googleMaps.isEditMode();
3109
3571
  }
3110
3572
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamGoogleMapsComponent, deps: [{ token: i0.ElementRef }, { token: i1.FocusMonitor }, { token: GoogleMapsService }, { token: MapValueManagerService }, { token: TheSeamGoogleMapsApiLoader }], target: i0.ɵɵFactoryTarget.Component });
3111
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: TheSeamGoogleMapsComponent, isStandalone: false, selector: "seam-google-maps", inputs: { disabled: "disabled", value: "value", tabIndex: "tabIndex", fileDropEnabled: "fileDropEnabled", fileUploadControlEnabled: "fileUploadControlEnabled", fullscreenControlEnabled: "fullscreenControlEnabled", reCenterControlEnabled: "reCenterControlEnabled", mapTypeControlEnabled: "mapTypeControlEnabled", streetViewControlEnabled: "streetViewControlEnabled", allowDrawingHoleInPolygon: "allowDrawingHoleInPolygon", editingEnabled: "editingEnabled", fileImportHandler: "fileImportHandler", zoom: "zoom", longitude: "longitude", latitude: "latitude", padding: "padding", interactionMode: "interactionMode", featureGroupProperty: "featureGroupProperty", featureLabelProperty: "featureLabelProperty", newGroupKeyFactory: "newGroupKeyFactory", selectedGroupKey: "selectedGroupKey" }, outputs: { mapReady: "mapReady", selectionChange: "selectionChange", featureHoverChange: "featureHoverChange" }, host: { properties: { "attr.disabled": "this._attrDisabled", "attr.tabindex": "this._attrTabIndex" } }, providers: [
3573
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: TheSeamGoogleMapsComponent, isStandalone: false, selector: "seam-google-maps", inputs: { disabled: "disabled", value: "value", tabIndex: "tabIndex", fileDropEnabled: "fileDropEnabled", fileUploadControlEnabled: "fileUploadControlEnabled", fullscreenControlEnabled: "fullscreenControlEnabled", reCenterControlEnabled: "reCenterControlEnabled", mapTypeControlEnabled: "mapTypeControlEnabled", streetViewControlEnabled: "streetViewControlEnabled", allowDrawingHoleInPolygon: "allowDrawingHoleInPolygon", editingEnabled: "editingEnabled", fileImportHandler: "fileImportHandler", zoom: "zoom", longitude: "longitude", latitude: "latitude", padding: "padding", interactionMode: "interactionMode", featureGroupProperty: "featureGroupProperty", featureLabelProperty: "featureLabelProperty", newGroupKeyFactory: "newGroupKeyFactory", selectedGroupKey: "selectedGroupKey", canDelete: "canDelete" }, outputs: { mapReady: "mapReady", selectionChange: "selectionChange", featureHoverChange: "featureHoverChange", deleteBlocked: "deleteBlocked", fileImportError: "fileImportError" }, host: { properties: { "attr.disabled": "this._attrDisabled", "attr.tabindex": "this._attrTabIndex" } }, providers: [
3112
3574
  MapValueManagerService,
3113
3575
  GoogleMapsService,
3114
3576
  { provide: MAP_CONTROLS_SERVICE, useClass: GoogleMapsControlsService },
@@ -3117,7 +3579,7 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
3117
3579
  useExisting: forwardRef(() => TheSeamGoogleMapsComponent),
3118
3580
  multi: true,
3119
3581
  },
3120
- ], viewQueries: [{ propertyName: "featureContextMenu", first: true, predicate: ["featureContextMenu"], descendants: true, read: MenuComponent, static: true }], exportAs: ["seamGoogleMaps"], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "<ng-container *ngIf=\"_gmApiLoaded | async\">\n <google-map\n height=\"100%\"\n width=\"100%\"\n [center]=\"{ lat: latitude, lng: longitude }\"\n [zoom]=\"zoom\"\n [options]=\"_options\"\n (mapInitialized)=\"_onMapReady($any($event))\"\n >\n </google-map>\n</ng-container>\n<seam-menu #featureContextMenu>\n <button\n seamMenuItem\n *ngFor=\"let item of _contextMenuItems$ | async\"\n (click)=\"item.action(item)\"\n >\n {{ item.label }}\n </button>\n</seam-menu>\n<seam-map-file-drop *ngIf=\"fileDropEnabled\"></seam-map-file-drop>\n\n<seam-map-control\n *ngIf=\"fileUploadControlEnabled\"\n [def]=\"_fileUploadControlDef\"\n></seam-map-control>\n<seam-map-control\n *ngIf=\"reCenterControlEnabled\"\n [def]=\"_reCenterControlDef\"\n></seam-map-control>\n<seam-map-control\n *ngIf=\"editingEnabled\"\n [def]=\"_drawControlDef\"\n></seam-map-control>\n", styles: [":host{display:block;position:relative;height:400px;width:100%}:host.show-focus-border.cdk-focused{outline:0;box-shadow:0 0 0 .2rem #357ebd40}:host.show-focus-border[disabled]{opacity:.65}:host ::ng-deep .seam-map-feature-label{color:#fff;font-size:.75rem;font-weight:600;text-shadow:0 0 3px rgba(0,0,0,.9),0 0 6px rgba(0,0,0,.7);-webkit-user-select:none;user-select:none}\n"], dependencies: [{ kind: "directive", type: i5.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i6.MenuComponent, selector: "seam-menu", inputs: ["menuClass", "baseWidth", "animationType"], outputs: ["closed"], exportAs: ["seamMenu"] }, { kind: "component", type: i6.MenuItemComponent, selector: "[seamMenuItem]", inputs: ["disabled", "role", "icon", "iconClass", "sublevelIcon", "subLevelIconClass", "badgeText", "badgeTheme"], exportAs: ["seamMenuItem"] }, { kind: "component", type: i7.GoogleMap, selector: "google-map", inputs: ["height", "width", "mapId", "mapTypeId", "center", "zoom", "options"], outputs: ["mapInitialized", "authFailure", "boundsChanged", "centerChanged", "mapClick", "mapDblclick", "mapDrag", "mapDragend", "mapDragstart", "headingChanged", "idle", "maptypeidChanged", "mapMousemove", "mapMouseout", "mapMouseover", "projectionChanged", "mapRightclick", "tilesloaded", "tiltChanged", "zoomChanged"], exportAs: ["googleMap"] }, { kind: "component", type: TheSeamMapFileDropComponent, selector: "seam-map-file-drop" }, { kind: "component", type: TheSeamMapsControlComponent, selector: "seam-map-control", inputs: ["def"] }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3582
+ ], viewQueries: [{ propertyName: "featureContextMenu", first: true, predicate: ["featureContextMenu"], descendants: true, read: MenuComponent, static: true }], exportAs: ["seamGoogleMaps"], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "<ng-container *ngIf=\"_gmApiLoaded | async\">\n <google-map\n height=\"100%\"\n width=\"100%\"\n [center]=\"{ lat: latitude, lng: longitude }\"\n [zoom]=\"zoom\"\n [options]=\"_options\"\n (mapInitialized)=\"_onMapReady($any($event))\"\n >\n </google-map>\n</ng-container>\n<seam-menu #featureContextMenu>\n <button\n seamMenuItem\n *ngFor=\"let item of _contextMenuItems$ | async\"\n (click)=\"item.action()\"\n >\n {{ item.label }}\n </button>\n</seam-menu>\n<seam-map-file-drop *ngIf=\"fileDropEnabled\"></seam-map-file-drop>\n\n<seam-map-control\n *ngIf=\"fileUploadControlEnabled\"\n [def]=\"_fileUploadControlDef\"\n></seam-map-control>\n<seam-map-control\n *ngIf=\"reCenterControlEnabled\"\n [def]=\"_reCenterControlDef\"\n></seam-map-control>\n<seam-map-control\n *ngIf=\"editingEnabled\"\n [def]=\"_drawControlDef\"\n></seam-map-control>\n", styles: [":host{display:block;position:relative;height:400px;width:100%}:host.show-focus-border.cdk-focused{outline:0;box-shadow:0 0 0 .2rem #357ebd40}:host.show-focus-border[disabled]{opacity:.65}:host ::ng-deep .seam-map-feature-label{color:#fff;font-size:.75rem;font-weight:600;text-shadow:0 0 3px rgba(0,0,0,.9),0 0 6px rgba(0,0,0,.7);-webkit-user-select:none;user-select:none}\n"], dependencies: [{ kind: "directive", type: i5.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i6.MenuComponent, selector: "seam-menu", inputs: ["menuClass", "baseWidth", "animationType"], outputs: ["closed"], exportAs: ["seamMenu"] }, { kind: "component", type: i6.MenuItemComponent, selector: "[seamMenuItem]", inputs: ["disabled", "role", "icon", "iconClass", "sublevelIcon", "subLevelIconClass", "badgeText", "badgeTheme"], exportAs: ["seamMenuItem"] }, { kind: "component", type: i7.GoogleMap, selector: "google-map", inputs: ["height", "width", "mapId", "mapTypeId", "center", "zoom", "options"], outputs: ["mapInitialized", "authFailure", "boundsChanged", "centerChanged", "mapClick", "mapDblclick", "mapDrag", "mapDragend", "mapDragstart", "headingChanged", "idle", "maptypeidChanged", "mapMousemove", "mapMouseout", "mapMouseover", "projectionChanged", "mapRightclick", "tilesloaded", "tiltChanged", "zoomChanged"], exportAs: ["googleMap"] }, { kind: "component", type: TheSeamMapFileDropComponent, selector: "seam-map-file-drop" }, { kind: "component", type: TheSeamMapsControlComponent, selector: "seam-map-control", inputs: ["def"] }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3121
3583
  }
3122
3584
  __decorate([
3123
3585
  InputBoolean()
@@ -3163,7 +3625,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
3163
3625
  useExisting: forwardRef(() => TheSeamGoogleMapsComponent),
3164
3626
  multi: true,
3165
3627
  },
3166
- ], changeDetection: ChangeDetectionStrategy.OnPush, exportAs: 'seamGoogleMaps', standalone: false, template: "<ng-container *ngIf=\"_gmApiLoaded | async\">\n <google-map\n height=\"100%\"\n width=\"100%\"\n [center]=\"{ lat: latitude, lng: longitude }\"\n [zoom]=\"zoom\"\n [options]=\"_options\"\n (mapInitialized)=\"_onMapReady($any($event))\"\n >\n </google-map>\n</ng-container>\n<seam-menu #featureContextMenu>\n <button\n seamMenuItem\n *ngFor=\"let item of _contextMenuItems$ | async\"\n (click)=\"item.action(item)\"\n >\n {{ item.label }}\n </button>\n</seam-menu>\n<seam-map-file-drop *ngIf=\"fileDropEnabled\"></seam-map-file-drop>\n\n<seam-map-control\n *ngIf=\"fileUploadControlEnabled\"\n [def]=\"_fileUploadControlDef\"\n></seam-map-control>\n<seam-map-control\n *ngIf=\"reCenterControlEnabled\"\n [def]=\"_reCenterControlDef\"\n></seam-map-control>\n<seam-map-control\n *ngIf=\"editingEnabled\"\n [def]=\"_drawControlDef\"\n></seam-map-control>\n", styles: [":host{display:block;position:relative;height:400px;width:100%}:host.show-focus-border.cdk-focused{outline:0;box-shadow:0 0 0 .2rem #357ebd40}:host.show-focus-border[disabled]{opacity:.65}:host ::ng-deep .seam-map-feature-label{color:#fff;font-size:.75rem;font-weight:600;text-shadow:0 0 3px rgba(0,0,0,.9),0 0 6px rgba(0,0,0,.7);-webkit-user-select:none;user-select:none}\n"] }]
3628
+ ], changeDetection: ChangeDetectionStrategy.OnPush, exportAs: 'seamGoogleMaps', standalone: false, template: "<ng-container *ngIf=\"_gmApiLoaded | async\">\n <google-map\n height=\"100%\"\n width=\"100%\"\n [center]=\"{ lat: latitude, lng: longitude }\"\n [zoom]=\"zoom\"\n [options]=\"_options\"\n (mapInitialized)=\"_onMapReady($any($event))\"\n >\n </google-map>\n</ng-container>\n<seam-menu #featureContextMenu>\n <button\n seamMenuItem\n *ngFor=\"let item of _contextMenuItems$ | async\"\n (click)=\"item.action()\"\n >\n {{ item.label }}\n </button>\n</seam-menu>\n<seam-map-file-drop *ngIf=\"fileDropEnabled\"></seam-map-file-drop>\n\n<seam-map-control\n *ngIf=\"fileUploadControlEnabled\"\n [def]=\"_fileUploadControlDef\"\n></seam-map-control>\n<seam-map-control\n *ngIf=\"reCenterControlEnabled\"\n [def]=\"_reCenterControlDef\"\n></seam-map-control>\n<seam-map-control\n *ngIf=\"editingEnabled\"\n [def]=\"_drawControlDef\"\n></seam-map-control>\n", styles: [":host{display:block;position:relative;height:400px;width:100%}:host.show-focus-border.cdk-focused{outline:0;box-shadow:0 0 0 .2rem #357ebd40}:host.show-focus-border[disabled]{opacity:.65}:host ::ng-deep .seam-map-feature-label{color:#fff;font-size:.75rem;font-weight:600;text-shadow:0 0 3px rgba(0,0,0,.9),0 0 6px rgba(0,0,0,.7);-webkit-user-select:none;user-select:none}\n"] }]
3167
3629
  }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i1.FocusMonitor }, { type: GoogleMapsService }, { type: MapValueManagerService }, { type: TheSeamGoogleMapsApiLoader }], propDecorators: { value: [{
3168
3630
  type: Input
3169
3631
  }], tabIndex: [{
@@ -3210,12 +3672,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
3210
3672
  type: Input
3211
3673
  }], selectedGroupKey: [{
3212
3674
  type: Input
3675
+ }], canDelete: [{
3676
+ type: Input
3213
3677
  }], mapReady: [{
3214
3678
  type: Output
3215
3679
  }], selectionChange: [{
3216
3680
  type: Output
3217
3681
  }], featureHoverChange: [{
3218
3682
  type: Output
3683
+ }], deleteBlocked: [{
3684
+ type: Output
3685
+ }], fileImportError: [{
3686
+ type: Output
3219
3687
  }], featureContextMenu: [{
3220
3688
  type: ViewChild,
3221
3689
  args: ['featureContextMenu', { static: true, read: MenuComponent }]
@@ -3796,5 +4264,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
3796
4264
  * Generated bundle index. Do not edit.
3797
4265
  */
3798
4266
 
3799
- export { AppFeaturePropertyName, FEATURE_STYLE_OPTIONS_DEFAULT, FEATURE_STYLE_OPTIONS_SELECTED, FEATURE_STYLE_OVERRIDE_OPTIONS_HOVERED, GoogleMapsContextMenu, GoogleMapsControlsService, GoogleMapsService, MAP_CONTROLS_SERVICE, MAP_CONTROL_DATA, MapControlRef, MapValueManagerService, MapValueSource, SEAM_GOOGLE_PLACES_AUTOCOMPLETE_DEFAULT_OPTIONS, SUPPORTED_PROPERTY_STYLE_OPTIONS, THESEAM_LAZY_MAPS_API_CONFIG, TheSeamGoogleMapsApiLoader, TheSeamGoogleMapsComponent, TheSeamGoogleMapsModule, TheSeamGoogleMapsPlacesAutoCompleteComponent, TheSeamGoogleMapsPlacesAutocompleteDirective, TheSeamGoogleMapsRecenterButtonControlComponent, TheSeamGoogleMapsUploadButtonControlComponent, TheSeamLazyMapsApiLoader, TheSeamMapFileDropComponent, TheSeamMapsControlComponent, TheSeamNoopGoogleMapsAPILoader, applyHoleToFeature, computeFeatureHoverStyle, computeFeatureStyle, createFeatureChangeObservable, dataMultiPolygonFromGeoJson, dataPolygonFromGeoJson, geoJsonFeatureFromDataFeature, geoJsonPolygonFromDataFeature, getBoundsWithAllFeatures, getFeatureBounds, getFeatureCenter, getFeaturesCount, getHoveredStyleOptionsDefinedByFeature, getSelectedStyleOptionsDefinedByFeature, getStyleOptionsDefinedByFeature, isAppFeatureProperty, isFeatureSelected, mergeStyleOptions, polygonCoordinates, polygonHasValidPathsLengths, polygonsFromDataFeature, removeAllFeatures, setFeatureSelected, stripAppFeaturePropertiesFromJson };
4267
+ export { AppFeaturePropertyName, FEATURE_STYLE_OPTIONS_DEFAULT, FEATURE_STYLE_OPTIONS_SELECTED, FEATURE_STYLE_OVERRIDE_OPTIONS_HOVERED, GoogleMapsContextMenu, GoogleMapsControlsService, GoogleMapsService, MAP_CONTROLS_SERVICE, MAP_CONTROL_DATA, MapControlRef, MapValueManagerService, MapValueSource, SEAM_GOOGLE_PLACES_AUTOCOMPLETE_DEFAULT_OPTIONS, SUPPORTED_PROPERTY_STYLE_OPTIONS, THESEAM_LAZY_MAPS_API_CONFIG, TheSeamGoogleMapsApiLoader, TheSeamGoogleMapsComponent, TheSeamGoogleMapsModule, TheSeamGoogleMapsPlacesAutoCompleteComponent, TheSeamGoogleMapsPlacesAutocompleteDirective, TheSeamGoogleMapsRecenterButtonControlComponent, TheSeamGoogleMapsUploadButtonControlComponent, TheSeamLazyMapsApiLoader, TheSeamMapFileDropComponent, TheSeamMapsControlComponent, TheSeamNoopGoogleMapsAPILoader, applyHoleToFeature, buildDeleteMenuItems, computeFeatureHoverStyle, computeFeatureStyle, createFeatureChangeObservable, dataMultiPolygonFromGeoJson, dataPolygonFromGeoJson, featureAllows, geoJsonFeatureFromDataFeature, geoJsonPolygonFromDataFeature, getBoundsWithAllFeatures, getFeatureBounds, getFeatureCenter, getFeaturesCount, getHoveredStyleOptionsDefinedByFeature, getSelectedStyleOptionsDefinedByFeature, getStyleOptionsDefinedByFeature, isAppFeatureProperty, isFeatureSelected, mergeStyleOptions, polygonCoordinates, polygonHasValidPathsLengths, polygonsFromDataFeature, removeAllFeatures, setFeatureSelected, stripAppFeaturePropertiesFromJson };
3800
4268
  //# sourceMappingURL=theseam-ui-common-google-maps.mjs.map