@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.
- package/fesm2022/theseam-ui-common-google-maps.mjs +543 -75
- package/fesm2022/theseam-ui-common-google-maps.mjs.map +1 -1
- package/fesm2022/theseam-ui-common-utils.mjs +102 -35
- package/fesm2022/theseam-ui-common-utils.mjs.map +1 -1
- package/google-maps/index.d.ts +245 -12
- package/package.json +1 -1
- package/utils/index.d.ts +12 -1
|
@@ -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
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
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 =
|
|
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
|
|
568
|
-
//
|
|
569
|
-
options.draggable =
|
|
570
|
-
|
|
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
|
|
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
|
-
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2581
|
-
|
|
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
|
-
|
|
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
|
|
2857
|
-
//
|
|
2858
|
-
// `
|
|
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(([
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
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(
|
|
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(
|
|
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
|