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

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,14 @@ 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();
1120
1141
  _selectionSubject = new BehaviorSubject(null);
1121
1142
  /**
1122
1143
  * `distinctUntilChanged` collapses repeated `null`s. Several paths clear the
@@ -1171,6 +1192,7 @@ class GoogleMapsService {
1171
1192
  this._contextMenuTargetSubject.complete();
1172
1193
  this._editModeSubject.complete();
1173
1194
  this._interactionModeSubject.complete();
1195
+ this._deleteBlockedSubject.complete();
1174
1196
  this._groups = undefined;
1175
1197
  this._labelsOverlay?.destroy();
1176
1198
  this._labelsOverlay = undefined;
@@ -1226,7 +1248,7 @@ class GoogleMapsService {
1226
1248
  /**
1227
1249
  * Iterates the map's features and removes any that are selected.
1228
1250
  */
1229
- deleteSelection() {
1251
+ _removeSelection() {
1230
1252
  this._assertInitialized();
1231
1253
  const mapData = this.googleMap.data;
1232
1254
  mapData.forEach((f) => {
@@ -1236,13 +1258,201 @@ class GoogleMapsService {
1236
1258
  });
1237
1259
  // Every deleted feature was selected, so nothing should still read as
1238
1260
  // selected afterward. Re-sync `selection$` and `_focusedFeature` the same
1239
- // way `deleteFocusedFeature()` and `setData()` already do, rather than
1261
+ // way `_removeFocusedFeature()` and `setData()` already do, rather than
1240
1262
  // leaving them pointing at a group that no longer exists. In 'legacy'
1241
1263
  // mode nothing consumes selection$ today, and no remaining feature's raw
1242
1264
  // selected flag changes here (they were already false), so this is inert
1243
1265
  // there.
1244
1266
  this.clearSelection();
1245
1267
  }
1268
+ /**
1269
+ * The consumer's veto. Consulted for every delete on every path, and for
1270
+ * whether to offer a delete at all.
1271
+ */
1272
+ setCanDelete(predicate) {
1273
+ this._canDelete = predicate ?? undefined;
1274
+ }
1275
+ /**
1276
+ * Pair a set of features to remove with the target to consult `canDelete`
1277
+ * with. `null` when there is nothing to delete — distinct from a refusal,
1278
+ * and the reason no honest target exists to report.
1279
+ *
1280
+ * Builds `target.feature` itself rather than delegating the decision to
1281
+ * `_targetFor`. There, `feature: null` means only "no particular feature";
1282
+ * here it is a promise that the whole group is about to cease to exist.
1283
+ * Conflating the two is what let a delete of ONE polygon be consulted as a
1284
+ * group delete — see `ResolvedDeletion.unnamable`.
1285
+ */
1286
+ _deletionOf(key, removing) {
1287
+ if (key === null || removing.length === 0) {
1288
+ return null;
1289
+ }
1290
+ const resolved = this._registry.groupWithSources(key);
1291
+ if (!resolved) {
1292
+ return null;
1293
+ }
1294
+ // The empty-group invariant: a delete that leaves the group with no
1295
+ // features at all is a group delete, whichever path asked for it — so
1296
+ // `feature: null` always means "this group is about to cease to exist"
1297
+ // and a consumer never has to count features itself. Compared against
1298
+ // `featuresIn`, not `resolved.sources`, because a feature with
1299
+ // unsupported geometry is missing from `sources` but still occupies the
1300
+ // group.
1301
+ const all = this._registry.featuresIn(key);
1302
+ const emptiesGroup = all.every((f) => removing.indexOf(f) !== -1);
1303
+ if (emptiesGroup) {
1304
+ // `feature: null` is honest here whatever `removing` holds: the group
1305
+ // really is going, unsupported-geometry members included. This case is
1306
+ // never at risk, so it is never `unnamable`.
1307
+ return {
1308
+ removing,
1309
+ target: { group: resolved.group, feature: null },
1310
+ unnamable: false,
1311
+ };
1312
+ }
1313
+ // The group survives, so the target must name the polygon being removed.
1314
+ const index = resolved.sources.indexOf(removing[0]);
1315
+ if (index === -1) {
1316
+ // The feature being removed is absent from `sources`: its geometry is
1317
+ // neither Polygon nor MultiPolygon, so `groupWithSources` dropped it and
1318
+ // it has no emitted GeoJSON counterpart. It has never appeared in any
1319
+ // `group.features` the consumer has seen, so NO value of
1320
+ // `target.feature` describes it — which is exactly why this deletion is
1321
+ // `unnamable` and `_mayDeleteResolved()` refuses it outright whenever a
1322
+ // predicate is set.
1323
+ //
1324
+ // `feature: null` is the least dishonest option available. It overstates
1325
+ // the scope — it reads as "the field is going" when only one polygon
1326
+ // was asked for — but it names the right group, and it is only ever READ
1327
+ // on the `deleteBlocked$` emission that reports the refusal: the
1328
+ // predicate is refused before it is ever consulted with this target, and
1329
+ // with no predicate set nothing observes it at all. A target that only
1330
+ // ever reports a refusal cannot authorise a removal, so the overstatement
1331
+ // is inert. The alternative was emitting nothing, which would leave a
1332
+ // refused delete indistinguishable from a successful one on the design's
1333
+ // only feedback channel.
1334
+ return {
1335
+ removing,
1336
+ target: { group: resolved.group, feature: null },
1337
+ unnamable: true,
1338
+ };
1339
+ }
1340
+ return {
1341
+ removing,
1342
+ target: {
1343
+ group: resolved.group,
1344
+ feature: resolved.group.features[index],
1345
+ },
1346
+ unnamable: false,
1347
+ };
1348
+ }
1349
+ /** Resolves the same features `_removeSelection()` would remove. */
1350
+ _selectionDeletion() {
1351
+ this._assertInitialized();
1352
+ const removing = [];
1353
+ this.googleMap.data.forEach((f) => {
1354
+ if (isFeatureSelected(f)) {
1355
+ removing.push(f);
1356
+ }
1357
+ });
1358
+ if (removing.length === 0) {
1359
+ return null;
1360
+ }
1361
+ return this._deletionOf(this._registry.keyOf(removing[0]), removing);
1362
+ }
1363
+ /** Resolves the same features `_removeFocusedFeature()` would remove. */
1364
+ _focusedFeatureDeletion() {
1365
+ const focused = this._focusedFeature;
1366
+ if (focused === null) {
1367
+ return this._selectionDeletion();
1368
+ }
1369
+ return this._deletionOf(this._registry.keyOf(focused), [focused]);
1370
+ }
1371
+ /** Resolves the same features `_removeGroup(key)` would remove. */
1372
+ _groupDeletion(key) {
1373
+ return this._deletionOf(key, this._registry.featuresIn(key));
1374
+ }
1375
+ /**
1376
+ * Whether `removing` may be deleted: the feature-declared lock first, then
1377
+ * the consumer's predicate.
1378
+ *
1379
+ * MUST stay pure. It also answers menu-render questions, which are not
1380
+ * delete attempts — a side effect here would fire on every right-click.
1381
+ */
1382
+ _mayDelete(removing, target) {
1383
+ // A feature the consumer locked against reshaping must not be removable
1384
+ // by another route: deleting a polygon changes the map's value at least
1385
+ // as much as reshaping it does. Same precedent as editable: false
1386
+ // implying draggable: false in compute-feature-style.ts.
1387
+ if (removing.some((f) => !featureAllows(f, 'editable'))) {
1388
+ return false;
1389
+ }
1390
+ return this._canDelete?.(target) ?? true;
1391
+ }
1392
+ /**
1393
+ * Whether a resolved deletion may proceed. The single gate every query and
1394
+ * every command goes through.
1395
+ *
1396
+ * Wraps `_mayDelete` with the one refusal that cannot be phrased as a
1397
+ * question about a target: an `unnamable` delete. Consulting the predicate
1398
+ * there would mean LYING to it — handing it `feature: null`, which promises
1399
+ * the whole group is about to cease to exist, for a delete that removes one
1400
+ * polygon and leaves the rest standing. A consumer whose rule is "a field
1401
+ * may be deleted, an individual polygon may not" answers `true` to that and
1402
+ * loses a polygon it meant to keep. Fail closed exactly where a consumer's
1403
+ * rule could be subverted.
1404
+ *
1405
+ * Only when a predicate is actually set. With none there is nobody to lie
1406
+ * to, so there is nothing to protect — and refusing unconditionally would
1407
+ * change 'legacy' mode's behaviour for unsupported-geometry features, which
1408
+ * two applications depend on and this work must not drift.
1409
+ *
1410
+ * Pure, like `_mayDelete`: it also answers menu-render questions.
1411
+ */
1412
+ _mayDeleteResolved(deletion) {
1413
+ if (deletion.unnamable && this._canDelete !== undefined) {
1414
+ return false;
1415
+ }
1416
+ return this._mayDelete(deletion.removing, deletion.target);
1417
+ }
1418
+ /** Whether "Delete Polygon" (or the `Delete` key) may act. */
1419
+ canDeleteFocusedFeature() {
1420
+ if (!this.mapReady) {
1421
+ return false;
1422
+ }
1423
+ const deletion = this._focusedFeatureDeletion();
1424
+ return deletion !== null && this._mayDeleteResolved(deletion);
1425
+ }
1426
+ /** Whether "Delete Field" may act on `key`. */
1427
+ canDeleteGroup(key) {
1428
+ if (!this.mapReady) {
1429
+ return false;
1430
+ }
1431
+ const deletion = this._groupDeletion(key);
1432
+ return deletion !== null && this._mayDeleteResolved(deletion);
1433
+ }
1434
+ /** Whether the legacy "Delete" item (or the `Delete` key) may act. */
1435
+ canDeleteSelection() {
1436
+ if (!this.mapReady) {
1437
+ return false;
1438
+ }
1439
+ const deletion = this._selectionDeletion();
1440
+ return deletion !== null && this._mayDeleteResolved(deletion);
1441
+ }
1442
+ /**
1443
+ * Remove every selected feature, unless `canDelete` or a feature's own
1444
+ * `editable: false` refuses. A refused delete emits on `deleteBlocked$` and
1445
+ * changes nothing.
1446
+ */
1447
+ deleteSelection() {
1448
+ this._assertInitialized();
1449
+ const deletion = this._selectionDeletion();
1450
+ if (deletion !== null && !this._mayDeleteResolved(deletion)) {
1451
+ this._deleteBlockedSubject.next(deletion.target);
1452
+ return;
1453
+ }
1454
+ this._removeSelection();
1455
+ }
1246
1456
  /**
1247
1457
  * Delete every feature in `key`'s group. Backs the grouped "Delete Field"
1248
1458
  * context-menu item.
@@ -1256,7 +1466,7 @@ class GoogleMapsService {
1256
1466
  * selected, a coincidence this design has already stopped being true once
1257
1467
  * before and could again.
1258
1468
  */
1259
- deleteGroup(key) {
1469
+ _removeGroup(key) {
1260
1470
  this._assertInitialized();
1261
1471
  const mapData = this.googleMap.data;
1262
1472
  const wasSelected = this._selectionSubject.value?.group.key === key;
@@ -1278,6 +1488,20 @@ class GoogleMapsService {
1278
1488
  this._contextMenuTargetSubject.next(null);
1279
1489
  }
1280
1490
  }
1491
+ /**
1492
+ * Remove every feature in `key`'s group, unless `canDelete` or a feature's
1493
+ * own `editable: false` refuses. A refused delete emits on `deleteBlocked$`
1494
+ * and changes nothing.
1495
+ */
1496
+ deleteGroup(key) {
1497
+ this._assertInitialized();
1498
+ const deletion = this._groupDeletion(key);
1499
+ if (deletion !== null && !this._mayDeleteResolved(deletion)) {
1500
+ this._deleteBlockedSubject.next(deletion.target);
1501
+ return;
1502
+ }
1503
+ this._removeGroup(key);
1504
+ }
1281
1505
  /**
1282
1506
  * Whether polygon drawing mode is currently active.
1283
1507
  *
@@ -1635,6 +1859,51 @@ class GoogleMapsService {
1635
1859
  this._ensureLabelsOverlay();
1636
1860
  this._labelsOverlay?.refresh();
1637
1861
  }
1862
+ /**
1863
+ * Write `label` into `featureLabelProperty` on every feature in `key`'s
1864
+ * group, without re-adding any data. Returns `false` when no feature
1865
+ * carries `key`, or when no `featureLabelProperty` is configured — there is
1866
+ * then no property to write the label into.
1867
+ *
1868
+ * Unlike a write through the map's `value`, this does NOT go through
1869
+ * `setData()`, so the selection and the viewport are untouched. The
1870
+ * repaint and the value update fall out of the data layer's own
1871
+ * `setproperty` event, which `_initFeatureChangeListeners()` already turns
1872
+ * into a `_labelsOverlay.refresh()` and a `MapValueSource.FeatureChange`
1873
+ * emission.
1874
+ *
1875
+ * It DOES emit a value change, because the label is part of the GeoJSON and
1876
+ * the value genuinely changed. Writing that emitted value straight back
1877
+ * through the `value` input is inert: `MapValueManagerService.setValue`
1878
+ * finds the serialized form identical and returns without emitting.
1879
+ */
1880
+ setGroupLabel(key, label) {
1881
+ this._assertInitialized();
1882
+ const property = this._labelProperty;
1883
+ if (!property) {
1884
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
1885
+ console.warn(`[seam-google-maps] setGroupLabel("${key}") was called with no ` +
1886
+ `featureLabelProperty configured, so there is no property to ` +
1887
+ `write the label into. Nothing changed.`);
1888
+ }
1889
+ return false;
1890
+ }
1891
+ const features = this._registry.featuresIn(key);
1892
+ if (features.length === 0) {
1893
+ return false;
1894
+ }
1895
+ for (const feature of features) {
1896
+ // `setProperty` raises `setproperty` whether or not the value differs,
1897
+ // and each one costs a full re-serialization of the map's value. A
1898
+ // rename typed character by character would pay that per keystroke per
1899
+ // feature for writes that change nothing.
1900
+ if (feature.getProperty(property) === label) {
1901
+ continue;
1902
+ }
1903
+ feature.setProperty(property, label);
1904
+ }
1905
+ return true;
1906
+ }
1638
1907
  _ensureLabelsOverlay() {
1639
1908
  this._assertInitialized();
1640
1909
  if (this._labelsOverlay) {
@@ -1886,7 +2155,7 @@ class GoogleMapsService {
1886
2155
  * `feature` no longer exists — `_applySelection` naturally clears to null
1887
2156
  * when the group is now empty, via `groupWithSources`.
1888
2157
  */
1889
- deleteFocusedFeature() {
2158
+ _removeFocusedFeature() {
1890
2159
  this._assertInitialized();
1891
2160
  const key = this._focusedFeature
1892
2161
  ? this._registry.keyOf(this._focusedFeature)
@@ -1897,7 +2166,7 @@ class GoogleMapsService {
1897
2166
  this._focusedFeature = null;
1898
2167
  }
1899
2168
  else {
1900
- this.deleteSelection();
2169
+ this._removeSelection();
1901
2170
  }
1902
2171
  this._applySelection(key, null);
1903
2172
  // Same reasoning as deleteGroup(): a context-menu target naming this
@@ -1907,6 +2176,20 @@ class GoogleMapsService {
1907
2176
  this._contextMenuTargetSubject.next(null);
1908
2177
  }
1909
2178
  }
2179
+ /**
2180
+ * Remove the focused polygon — or, with nothing focused, the selection —
2181
+ * unless `canDelete` or a feature's own `editable: false` refuses. A
2182
+ * refused delete emits on `deleteBlocked$` and changes nothing.
2183
+ */
2184
+ deleteFocusedFeature() {
2185
+ this._assertInitialized();
2186
+ const deletion = this._focusedFeatureDeletion();
2187
+ if (deletion !== null && !this._mayDeleteResolved(deletion)) {
2188
+ this._deleteBlockedSubject.next(deletion.target);
2189
+ return;
2190
+ }
2191
+ this._removeFocusedFeature();
2192
+ }
1910
2193
  /** Escape cascades: cancel a draw, then clear selection, then leave edit mode. */
1911
2194
  handleEscape() {
1912
2195
  if (this.isDrawing()) {
@@ -1955,7 +2238,7 @@ class GoogleMapsService {
1955
2238
  // directly. By the time that second delivery arrives (confirmed
1956
2239
  // against the live Storybook: consistently a few milliseconds later,
1957
2240
  // comfortably within one animation frame — see
1958
- // .superpowers/closing-click-report.md), `isDrawing()` already reads
2241
+ // docs/superpowers/reports/closing-click-report.md), `isDrawing()` already reads
1959
2242
  // false, so the guard just below cannot tell it apart from a fresh
1960
2243
  // click on open map. `_suppressNextMapClick` exists to catch exactly
1961
2244
  // that echo; see its doc comment for why a `domEvent.timeStamp`
@@ -2109,7 +2392,7 @@ class GoogleMapsService {
2109
2392
  * Arm `_suppressNextMapClick`, and guarantee it cannot linger forever if
2110
2393
  * the echo it exists for never arrives — confirmed to be the normal case
2111
2394
  * for most real closes, and true of every synthetic/`play()`-driven draw,
2112
- * per .superpowers/closing-click-report.md. A `setTimeout` would "solve"
2395
+ * per docs/superpowers/reports/closing-click-report.md. A `setTimeout` would "solve"
2113
2396
  * this by guessing a safe wall-clock delay, which is exactly the kind of
2114
2397
  * timing window this fix is trying to avoid introducing. Two
2115
2398
  * `requestAnimationFrame` callbacks bound the window instead: the observed
@@ -2485,6 +2768,56 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
2485
2768
  args: ['click', ['event']]
2486
2769
  }] } });
2487
2770
 
2771
+ /**
2772
+ * The delete items the feature context menu offers.
2773
+ *
2774
+ * A refused item is not rendered at all. The caller is expected to hide the
2775
+ * menu entirely for an empty list, so a consumer that refuses everything gets
2776
+ * no menu rather than an empty one.
2777
+ */
2778
+ function buildDeleteMenuItems(context) {
2779
+ const items = [];
2780
+ if (!context.editingEnabled) {
2781
+ return items;
2782
+ }
2783
+ if (context.mode !== 'grouped') {
2784
+ if (context.canDeleteSelection()) {
2785
+ items.push({ label: 'Delete', action: () => context.deleteSelection() });
2786
+ }
2787
+ return items;
2788
+ }
2789
+ const target = context.target;
2790
+ if (!target) {
2791
+ return items;
2792
+ }
2793
+ const key = target.group.key;
2794
+ // On a one-polygon field, "Delete Polygon" and "Delete Field" are the same
2795
+ // act — the first empties the group without saying so. Offer only the one
2796
+ // that names what happens.
2797
+ if (target.group.features.length <= 1) {
2798
+ if (context.canDeleteGroup(key)) {
2799
+ items.push({
2800
+ label: 'Delete Field',
2801
+ action: () => context.deleteGroup(key),
2802
+ });
2803
+ }
2804
+ return items;
2805
+ }
2806
+ if (context.canDeleteFocusedFeature()) {
2807
+ items.push({
2808
+ label: 'Delete Polygon',
2809
+ action: () => context.deleteFocusedFeature(),
2810
+ });
2811
+ }
2812
+ if (context.canDeleteGroup(key)) {
2813
+ items.push({
2814
+ label: 'Delete Field',
2815
+ action: () => context.deleteGroup(key),
2816
+ });
2817
+ }
2818
+ return items;
2819
+ }
2820
+
2488
2821
  class TheSeamGoogleMapsApiLoader {
2489
2822
  }
2490
2823
 
@@ -2720,7 +3053,8 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
2720
3053
  _fileUploadControlDef = {
2721
3054
  component: TheSeamGoogleMapsUploadButtonControlComponent,
2722
3055
  data: { label: 'Import Geo File', icon: faFileImport },
2723
- position: 6 /* google.maps.ControlPosition.LEFT_BOTTOM */,
3056
+ // position: 6 /* google.maps.ControlPosition.LEFT_BOTTOM */,
3057
+ position: 7, // Below top-right fullscreen button
2724
3058
  };
2725
3059
  _reCenterControlDef = {
2726
3060
  component: TheSeamGoogleMapsRecenterButtonControlComponent,
@@ -2796,9 +3130,37 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
2796
3130
  newGroupKeyFactory;
2797
3131
  /** Preselect a group. Applied on map-ready and after each external value write. */
2798
3132
  selectedGroupKey = null;
3133
+ /**
3134
+ * Vetoes a delete. Return `false` to refuse the target.
3135
+ *
3136
+ * `target.feature === null` means the whole group is about to cease to
3137
+ * exist — every polygon of it is going, however the delete was asked for.
3138
+ * A non-null `feature` means one polygon is being removed from a group that
3139
+ * survives.
3140
+ *
3141
+ * **Consulted when no delete is happening.** It decides whether to render
3142
+ * the context-menu items, so it runs on every context-menu open. A
3143
+ * predicate that logs an attempt or flips state will do so on every
3144
+ * right-click. It must also be deterministic for a given target, or the
3145
+ * menu and the `Delete` key can disagree about the same target.
3146
+ *
3147
+ * That is a correctness warning, not a performance one. The context-menu
3148
+ * target changes only on a right-click, a delete, and a value write, so
3149
+ * this runs a handful of times per gesture.
3150
+ *
3151
+ * Passing nothing refuses nothing, which is the behaviour this component
3152
+ * has always had.
3153
+ */
3154
+ canDelete;
2799
3155
  mapReady = new EventEmitter();
2800
3156
  selectionChange = new EventEmitter();
2801
3157
  featureHoverChange = new EventEmitter();
3158
+ /**
3159
+ * A delete was attempted and refused — in practice, the `Delete` key, since
3160
+ * a refused menu item is never rendered. Emits the target that was refused
3161
+ * so the consumer can explain why; only the consumer knows the reason.
3162
+ */
3163
+ deleteBlocked = new EventEmitter();
2802
3164
  featureContextMenu;
2803
3165
  _options = {
2804
3166
  mapTypeControl: true,
@@ -2851,39 +3213,28 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
2851
3213
  this._googleMaps.hover$
2852
3214
  .pipe(skip(1), tap((hover) => this.featureHoverChange.emit(hover)), takeUntil(this._ngUnsubscribe))
2853
3215
  .subscribe();
3216
+ // No `skip(1)` here, unlike `selection$` and `hover$`: `deleteBlocked$` is
3217
+ // a plain Subject with no replayed initial value to drop.
3218
+ this._googleMaps.deleteBlocked$
3219
+ .pipe(tap((target) => this.deleteBlocked.emit(target)), takeUntil(this._ngUnsubscribe))
3220
+ .subscribe();
2854
3221
  this._contextMenuItems$ = combineLatest([
2855
3222
  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.
3223
+ // The RIGHT-CLICKED feature's group — what "Delete Field" is
3224
+ // conceptually acting on, independent of what happens to be selected.
3225
+ // See `contextMenuTarget$`'s doc comment.
2862
3226
  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) => {
3227
+ ]).pipe(map(([editingEnabled, target]) => buildDeleteMenuItems({
3228
+ mode: this.interactionMode,
3229
+ editingEnabled,
3230
+ target,
3231
+ canDeleteFocusedFeature: () => this._googleMaps.canDeleteFocusedFeature(),
3232
+ canDeleteGroup: (key) => this._googleMaps.canDeleteGroup(key),
3233
+ canDeleteSelection: () => this._googleMaps.canDeleteSelection(),
3234
+ deleteFocusedFeature: () => this._googleMaps.deleteFocusedFeature(),
3235
+ deleteGroup: (key) => this._googleMaps.deleteGroup(key),
3236
+ deleteSelection: () => this._googleMaps.deleteSelection(),
3237
+ })), tap((items) => {
2887
3238
  if (items.length === 0) {
2888
3239
  this._googleMaps.setFeatureContextMenu(null);
2889
3240
  }
@@ -2985,6 +3336,9 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
2985
3336
  if (Object.prototype.hasOwnProperty.call(changes, 'selectedGroupKey')) {
2986
3337
  this._applySelectedGroupKey();
2987
3338
  }
3339
+ if (Object.prototype.hasOwnProperty.call(changes, 'canDelete')) {
3340
+ this._googleMaps.setCanDelete(this.canDelete);
3341
+ }
2988
3342
  if (Object.prototype.hasOwnProperty.call(changes, 'featureLabelProperty')) {
2989
3343
  this._googleMaps.setLabelProperty(this.featureLabelProperty);
2990
3344
  }
@@ -3058,9 +3412,6 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
3058
3412
  this.idleListener?.remove();
3059
3413
  });
3060
3414
  }
3061
- _onClickDeleteFeature() {
3062
- this._googleMaps.deleteSelection();
3063
- }
3064
3415
  // The service delegates below guard on `mapReady` themselves, the same way
3065
3416
  // `_applySelectedGroupKey()` already does for the declarative
3066
3417
  // `selectedGroupKey` input. Their service-side counterparts call
@@ -3101,6 +3452,19 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
3101
3452
  }
3102
3453
  return this._googleMaps.getGroups();
3103
3454
  }
3455
+ /**
3456
+ * Set a group's label text in place. Returns false when no such group
3457
+ * exists, or when no `featureLabelProperty` is set.
3458
+ *
3459
+ * Does not clear the selection or re-fit the viewport, so it is safe to
3460
+ * call while the user is renaming the very group being displayed.
3461
+ */
3462
+ setGroupLabel(key, label) {
3463
+ if (!this._googleMaps.mapReady) {
3464
+ return false;
3465
+ }
3466
+ return this._googleMaps.setGroupLabel(key, label);
3467
+ }
3104
3468
  setEditMode(enabled) {
3105
3469
  this._googleMaps.setEditMode(enabled);
3106
3470
  }
@@ -3108,7 +3472,7 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
3108
3472
  return this._googleMaps.isEditMode();
3109
3473
  }
3110
3474
  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: [
3475
+ 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" }, host: { properties: { "attr.disabled": "this._attrDisabled", "attr.tabindex": "this._attrTabIndex" } }, providers: [
3112
3476
  MapValueManagerService,
3113
3477
  GoogleMapsService,
3114
3478
  { provide: MAP_CONTROLS_SERVICE, useClass: GoogleMapsControlsService },
@@ -3117,7 +3481,7 @@ class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase {
3117
3481
  useExisting: forwardRef(() => TheSeamGoogleMapsComponent),
3118
3482
  multi: true,
3119
3483
  },
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 });
3484
+ ], 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
3485
  }
3122
3486
  __decorate([
3123
3487
  InputBoolean()
@@ -3163,7 +3527,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
3163
3527
  useExisting: forwardRef(() => TheSeamGoogleMapsComponent),
3164
3528
  multi: true,
3165
3529
  },
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"] }]
3530
+ ], 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
3531
  }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i1.FocusMonitor }, { type: GoogleMapsService }, { type: MapValueManagerService }, { type: TheSeamGoogleMapsApiLoader }], propDecorators: { value: [{
3168
3532
  type: Input
3169
3533
  }], tabIndex: [{
@@ -3210,12 +3574,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
3210
3574
  type: Input
3211
3575
  }], selectedGroupKey: [{
3212
3576
  type: Input
3577
+ }], canDelete: [{
3578
+ type: Input
3213
3579
  }], mapReady: [{
3214
3580
  type: Output
3215
3581
  }], selectionChange: [{
3216
3582
  type: Output
3217
3583
  }], featureHoverChange: [{
3218
3584
  type: Output
3585
+ }], deleteBlocked: [{
3586
+ type: Output
3219
3587
  }], featureContextMenu: [{
3220
3588
  type: ViewChild,
3221
3589
  args: ['featureContextMenu', { static: true, read: MenuComponent }]
@@ -3796,5 +4164,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
3796
4164
  * Generated bundle index. Do not edit.
3797
4165
  */
3798
4166
 
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 };
4167
+ 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
4168
  //# sourceMappingURL=theseam-ui-common-google-maps.mjs.map