@theseam/ui-common 2.0.1-beta.110 → 2.0.1-beta.111

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.
@@ -7,6 +7,7 @@ import { Observable } from 'rxjs';
7
7
  import { CanDisableCtor, CanDisable } from '@theseam/ui-common/core';
8
8
  import * as i10 from '@theseam/ui-common/menu';
9
9
  import { MenuComponent } from '@theseam/ui-common/menu';
10
+ import { Feature, Polygon, MultiPolygon } from 'geojson';
10
11
  import { ComponentType } from '@theseam/ui-common/models';
11
12
  import * as i11 from '@theseam/ui-common/icon';
12
13
  import { SeamIcon } from '@theseam/ui-common/icon';
@@ -16,12 +17,50 @@ import * as i7 from '@angular/common';
16
17
  import * as i8 from '@angular/cdk/observers';
17
18
  import * as i9 from '@theseam/ui-common/shared';
18
19
  import * as i13 from '@angular/google-maps';
19
- import { Polygon } from 'geojson';
20
+
21
+ /** A set of features that belong to the same logical thing, such as one field. */
22
+ interface TheSeamMapFeatureGroup {
23
+ /**
24
+ * `properties[featureGroupProperty]`.
25
+ *
26
+ * Falls back to a map-assigned key for features that carry no group value —
27
+ * always the case when no group property is configured, and for imported
28
+ * features the consumer did not assign one to. An assigned key is
29
+ * session-scoped: it does not survive an external value write, so it must not
30
+ * be persisted or held across a write and passed back to `selectGroup()`.
31
+ */
32
+ key: string;
33
+ features: Feature<Polygon | MultiPolygon>[];
34
+ }
35
+ /** A group, plus the individual feature an interaction landed on. */
36
+ interface TheSeamMapGroupTarget {
37
+ group: TheSeamMapFeatureGroup;
38
+ /**
39
+ * The polygon the interaction landed on. For a selection this is the one
40
+ * `Delete` acts on; for a hover, the one under the cursor.
41
+ *
42
+ * A reference into `group.features`, not a copy.
43
+ */
44
+ feature: Feature<Polygon | MultiPolygon> | null;
45
+ }
20
46
 
21
47
  declare abstract class TheSeamGoogleMapsApiLoader {
22
48
  abstract load(): Observable<void>;
23
49
  }
24
50
 
51
+ interface FeatureGroupRegistryOptions {
52
+ /**
53
+ * Name of the GeoJSON property that groups features. When unset, every
54
+ * feature is its own group.
55
+ */
56
+ groupProperty?: string | null;
57
+ /** Generates the key written for a newly created group. */
58
+ newGroupKeyFactory?: () => string;
59
+ }
60
+
61
+ /** Which interaction model a map uses. */
62
+ type TheSeamMapInteractionMode = 'legacy' | 'grouped';
63
+
25
64
  declare enum MapValueSource {
26
65
  Input = "input",
27
66
  FeatureChange = "featurechange"
@@ -51,6 +90,26 @@ declare class GoogleMapsService implements OnDestroy {
51
90
  private readonly _editingEnabledSubject;
52
91
  private _terraDraw?;
53
92
  private _terraDrawReady;
93
+ private _pendingStartDrawing;
94
+ /**
95
+ * One-shot suppression for the map `click` listener, armed the instant a
96
+ * draw finishes (see `_armMapClickSuppression()` for why, and its call
97
+ * site in `_onDrawFinished()`).
98
+ *
99
+ * `domEvent.timeStamp` was tried first, since it is a `DOMHighResTimeStamp`
100
+ * on the same origin as `performance.now()` and looks like a clean,
101
+ * delay-free discriminator: a stale click's timestamp should predate the
102
+ * moment the draw finished. It does not hold up empirically. Driving real
103
+ * mouse draws against the live Storybook and logging both the map `click`
104
+ * listener and `stopDrawing()` (see .superpowers/closing-click-report.md)
105
+ * caught the echo repeatedly, and every single time its `domEvent.timeStamp`
106
+ * was a few milliseconds AFTER `stopDrawing()`'s own timestamp, not before
107
+ * — indistinguishable from a genuinely fresh click by timestamp alone.
108
+ * Google appears to stamp its own synthetic `click` at the moment IT
109
+ * dispatches, not at the original pointer event's time, so there is no
110
+ * "stale-looking" timestamp to compare against.
111
+ */
112
+ private _suppressNextMapClick;
54
113
  private readonly _drawingSubject;
55
114
  readonly drawing$: Observable<boolean>;
56
115
  private _featureContextMenu;
@@ -58,6 +117,47 @@ declare class GoogleMapsService implements OnDestroy {
58
117
  private _baseLatLng?;
59
118
  private _padding?;
60
119
  private _allowDrawingHoleInPolygon;
120
+ private _model;
121
+ private readonly _interactionModeSubject;
122
+ readonly interactionMode$: Observable<TheSeamMapInteractionMode>;
123
+ private _groups?;
124
+ private _groupOptions;
125
+ private _focusedFeature;
126
+ private _styleFn?;
127
+ private _labelProperty;
128
+ private _labelsOverlay?;
129
+ private _warnedAboutLabelDisagreement;
130
+ private readonly _selectionSubject;
131
+ /**
132
+ * `distinctUntilChanged` collapses repeated `null`s. Several paths clear the
133
+ * selection defensively — `setData`, `clearSelection`, the declarative
134
+ * `selectedGroupKey` application on map-ready — and each would otherwise
135
+ * emit its own `null` to consumers before the user has touched anything.
136
+ *
137
+ * Non-null targets are rebuilt on every change, so reference equality never
138
+ * suppresses a real one, including re-selecting the same group with a
139
+ * different focused polygon.
140
+ */
141
+ readonly selection$: Observable<TheSeamMapGroupTarget | null>;
142
+ private readonly _hoverSubject;
143
+ /** Same reasoning as `selection$`; `mouseout` repeats `null` freely. */
144
+ readonly hover$: Observable<TheSeamMapGroupTarget | null>;
145
+ /**
146
+ * The group of the feature a `contextmenu` event last landed on. Kept
147
+ * distinct from `selection$` even though `allowsContextMenu()` now requires
148
+ * the right-clicked feature's group to already be the selection — so, for
149
+ * as long as the menu stays open, the two are provably the same group.
150
+ * Collapsing onto `selection$` would re-couple "what the menu acts on" to
151
+ * "whatever is currently selected", which is exactly the coupling this
152
+ * subject was introduced to break when the menu could still open for a
153
+ * non-selected group. That rule has already moved twice; keeping this
154
+ * separate means a future loosening of `allowsContextMenu()` does not have
155
+ * to re-invent it.
156
+ */
157
+ private readonly _contextMenuTargetSubject;
158
+ readonly contextMenuTarget$: Observable<TheSeamMapGroupTarget | null>;
159
+ private readonly _editModeSubject;
160
+ readonly editMode$: Observable<boolean>;
61
161
  private _fileInputHandler;
62
162
  googleMap?: google.maps.Map;
63
163
  readonly mapReady$: Observable<boolean>;
@@ -77,26 +177,192 @@ declare class GoogleMapsService implements OnDestroy {
77
177
  * Iterates the map's features and removes any that are selected.
78
178
  */
79
179
  deleteSelection(): void;
80
- /** Whether polygon drawing mode is currently active. */
180
+ /**
181
+ * Delete every feature in `key`'s group. Backs the grouped "Delete Field"
182
+ * context-menu item.
183
+ *
184
+ * `allowsContextMenu()` now requires the right-clicked feature's group to
185
+ * already be the selection, so `key` here is always the selected group's
186
+ * key by the time this runs — making this call equivalent in practice to
187
+ * `deleteSelection()`. Kept as its own method anyway: it names the thing
188
+ * the menu item actually acts on (the group the menu opened for) rather
189
+ * than relying on the coincidence that it currently matches whatever is
190
+ * selected, a coincidence this design has already stopped being true once
191
+ * before and could again.
192
+ */
193
+ deleteGroup(key: string): void;
194
+ /**
195
+ * Whether polygon drawing mode is currently active.
196
+ *
197
+ * Reads `_drawingSubject` — set by `startDrawing()`/`stopDrawing()`, which
198
+ * this service fully controls — rather than asking Terra Draw's own
199
+ * `getMode()`. The two are kept in lockstep by every call to those two
200
+ * methods, so this is not a behaviour change; it exists so a stuck Terra
201
+ * Draw mode can never silently disagree with, and disarm, the rest of this
202
+ * service's drawing-state bookkeeping (F3's `geometryEditingArmed`
203
+ * included). `stopDrawing()` still always calls `_terraDraw.setMode('static')`
204
+ * to make Terra Draw itself release its cursor override and any
205
+ * in-progress geometry, regardless of which source `isDrawing()` reads.
206
+ */
81
207
  isDrawing(): boolean;
82
208
  /** Enter polygon drawing mode. */
83
209
  startDrawing(): void;
210
+ /**
211
+ * Clear any selection when entering drawing mode, but only in 'legacy'
212
+ * mode. There, a selected feature and the shape being drawn are unrelated,
213
+ * so leaving the old one visibly selected would read as one shape. In
214
+ * 'grouped' mode they are related: the selected group is exactly the
215
+ * target the drawn polygon will join, so it keeps its selected styling —
216
+ * `GroupedInteractionModel.featureFlags()` disarms its edit handles for
217
+ * the duration instead (F3), so they don't compete with Terra Draw for
218
+ * pointer events.
219
+ *
220
+ * Split out of `startDrawing()` so a recreate-in-flight (see
221
+ * `_pendingStartDrawing`) can defer this until the new instance's `ready`
222
+ * event actually fires, instead of entering drawing mode against an
223
+ * instance that isn't ready yet.
224
+ */
225
+ private _enterDrawingMode;
84
226
  /**
85
227
  * Cancel any in-progress drawing and leave drawing mode. Switching to the
86
228
  * `static` mode clears an unfinished line.
87
229
  */
88
230
  stopDrawing(): void;
231
+ /**
232
+ * Whether the active Terra Draw mode has actually placed a coordinate.
233
+ * Terra Draw's own mode state machine transitions from `'started'`
234
+ * (armed, no coordinate committed yet) to `'drawing'` the instant the
235
+ * first vertex lands (`TerraDrawPolyLineMode`'s internal `setDrawing()`,
236
+ * confirmed against `terra-draw`'s own source). Read directly from Terra
237
+ * Draw's state rather than inferred from this service's own flags, so it
238
+ * reflects whether the adapter's pointer capture was actually
239
+ * engaged — the thing terra-draw#710 is actually about — rather than
240
+ * merely whether this service thought a draw was armed.
241
+ */
242
+ private _hasPlacedVertex;
243
+ /**
244
+ * Stop (if running) and drop the current `TerraDraw` instance. `TerraDraw#
245
+ * stop()` deregisters its adapter, which for the Google Maps adapter's
246
+ * `isolatedData: true` mode calls `this._data.setMap(null)` and drops the
247
+ * reference — so this never leaves an orphaned or duplicate Data layer
248
+ * behind, across any number of calls.
249
+ */
250
+ private _disposeTerraDraw;
251
+ /**
252
+ * Dispose the current `TerraDraw` instance (and its adapter) and build a
253
+ * fresh one in its place. This is the terra-draw#710 recovery: the old
254
+ * instance's listeners die with it (its adapter is deregistered by
255
+ * `stop()`, so nothing further reaches it), and `_createTerraDraw()` wires
256
+ * up brand new `ready`/`finish` listeners on the new instance — never the
257
+ * stale closures from a previous instance.
258
+ */
259
+ private _recreateTerraDraw;
89
260
  private _initTerraDraw;
261
+ /**
262
+ * Build and start a new `TerraDraw` instance with its Google Maps adapter,
263
+ * wiring up this instance's own `ready`/`finish` listeners. Called both by
264
+ * `_initTerraDraw()` (first-ever init, guarded against running twice) and
265
+ * by `_recreateTerraDraw()` (every terra-draw#710 recovery afterward) —
266
+ * the guard against accidental double-init only lives in `_initTerraDraw()`
267
+ * itself, so recreation is free to call this as many times as needed.
268
+ */
269
+ private _createTerraDraw;
90
270
  addControl(element: HTMLElement, position: google.maps.ControlPosition): void;
91
271
  setData(data: any): Promise<void>;
92
272
  reCenterOnFeatures(): void;
93
273
  allowDrawingHoleInPolygon(allow: boolean): void;
94
274
  setFileInputHandler(handler: ((file: File) => void) | undefined | null): void;
95
275
  getFileInputHandler(): ((file: File) => void) | undefined | null;
276
+ setInteractionMode(mode: TheSeamMapInteractionMode): void;
277
+ setGroupOptions(options: FeatureGroupRegistryOptions): void;
278
+ /**
279
+ * Name of the GeoJSON property to render as a per-group label. `undefined`
280
+ * removes labels entirely (and, until now set, means the overlay is never
281
+ * even constructed).
282
+ */
283
+ setLabelProperty(property: string | undefined): void;
284
+ private _ensureLabelsOverlay;
285
+ private _buildLabels;
286
+ private _warnAboutLabelDisagreement;
287
+ isEditMode(): boolean;
288
+ setEditMode(enabled: boolean): void;
289
+ /**
290
+ * Re-run the style callback for every feature.
291
+ *
292
+ * Google re-evaluates the callback whenever the style is set, so handing
293
+ * back the same stored function is enough to repaint after a mode change.
294
+ */
295
+ private _refreshStyles;
296
+ private get _registry();
297
+ private _interactionContext;
298
+ /**
299
+ * Find an existing feature that fully contains `polygon`, restricted to a
300
+ * group when `groupKey` is given and to features `accept` returns true for
301
+ * when given. Matches any part of a MultiPolygon.
302
+ *
303
+ * `accept` is applied DURING iteration — a rejected candidate is skipped,
304
+ * not treated as ending the search — so legacy mode's Polygon-only filter
305
+ * can let a later Polygon candidate still match.
306
+ */
307
+ private _findContainingFeature;
308
+ private _applySelection;
309
+ /**
310
+ * Pair a group with the emitted GeoJSON for one of its `Data.Feature`s.
311
+ *
312
+ * Indexes into the returned source array rather than `featuresIn()`, because
313
+ * a feature with unsupported geometry is dropped from the emitted list and
314
+ * would shift every index after it.
315
+ */
316
+ private _targetFor;
317
+ selectGroup(key: string | null): boolean;
318
+ clearSelection(): void;
319
+ getGroups(): TheSeamMapFeatureGroup[];
320
+ private _boundsForGroup;
321
+ fitGroup(key: string, padding?: number | google.maps.Padding): boolean;
322
+ panToGroup(key: string): boolean;
323
+ /**
324
+ * Delete only the polygon the last interaction landed on.
325
+ *
326
+ * Re-applies the selection's group key afterward (with no focused feature)
327
+ * rather than leaving `selection$` holding a `TheSeamMapGroupTarget` whose
328
+ * `feature` no longer exists — `_applySelection` naturally clears to null
329
+ * when the group is now empty, via `groupWithSources`.
330
+ */
331
+ deleteFocusedFeature(): void;
332
+ /** Escape cascades: cancel a draw, then clear selection, then leave edit mode. */
333
+ handleEscape(): void;
334
+ /**
335
+ * `startDrawing()` raw-deselects every feature (via `setFeatureSelected`)
336
+ * without touching `_selectionSubject`, so that `onDrawFinished` can still
337
+ * read `getSelectedKey()` for the group being drawn into. When a draw ends
338
+ * WITHOUT producing a finished feature — cancelled by `Escape` or
339
+ * `setEditMode(false)`, or a `finish` event with no valid geometry — nothing
340
+ * else re-applies those raw flags, so the map renders the selection as gone
341
+ * while `selection$`/`getSelectedKey()` still report it. Call this at every
342
+ * such stopping point to bring the two back in sync.
343
+ *
344
+ * Not called from the successful-finish path in `_onDrawFinished`: that
345
+ * path calls `_applySelection` itself with the new/joined selection, and
346
+ * doing it here first would only add a redundant, momentarily-stale
347
+ * `selectionChange` emission ahead of the real one.
348
+ */
349
+ private _reapplyCurrentSelection;
96
350
  private _initFeatureStyling;
97
351
  setFeatureHoveredStyleOverride(feature: google.maps.Data.Feature): void;
98
- private _mergeStyleOptions;
99
352
  private _initFeatureChangeListeners;
353
+ /**
354
+ * Establish `feature` as what the context menu is about to open for:
355
+ * `_focusedFeature` (what "Delete Polygon" acts on) and
356
+ * `contextMenuTarget$` (what "Delete Field" is gated on and acts on) both
357
+ * follow it. Shared by the `contextmenu` mouse listener and
358
+ * `openContextMenu()`'s keyboard path so the two establish the target
359
+ * identically — the menu's target must always be the feature the menu was
360
+ * opened for, on either path. Before this, only the mouse listener set
361
+ * `contextMenuTarget$`, so pressing the `ContextMenu` key could open a menu
362
+ * over whatever was CURRENTLY selected while still offering "Delete Field"
363
+ * for whatever a PRIOR right-click had targeted.
364
+ */
365
+ private _setContextMenuTarget;
100
366
  hasSelectedFeature(): boolean;
101
367
  getSelectedFeature(): google.maps.Data.Feature | null;
102
368
  openContextMenu(): void;
@@ -105,6 +371,21 @@ declare class GoogleMapsService implements OnDestroy {
105
371
  getGeoJson(removeAppProperties?: boolean): Promise<object>;
106
372
  /** Asserts that the map has been initialized. */
107
373
  private _assertInitialized;
374
+ /**
375
+ * Arm `_suppressNextMapClick`, and guarantee it cannot linger forever if
376
+ * the echo it exists for never arrives — confirmed to be the normal case
377
+ * for most real closes, and true of every synthetic/`play()`-driven draw,
378
+ * per .superpowers/closing-click-report.md. A `setTimeout` would "solve"
379
+ * this by guessing a safe wall-clock delay, which is exactly the kind of
380
+ * timing window this fix is trying to avoid introducing. Two
381
+ * `requestAnimationFrame` callbacks bound the window instead: the observed
382
+ * echo arrives a few milliseconds after `stopDrawing()`, comfortably
383
+ * inside a single frame, so two frames is generous headroom without
384
+ * guessing at a duration — and two real user actions (even fast automated
385
+ * ones) are never going to land within two animation frames of each other,
386
+ * so an unrelated later click is never at risk of being swallowed.
387
+ */
388
+ private _armMapClickSuppression;
108
389
  private _onDrawFinished;
109
390
  /**
110
391
  * Normalize a finished Terra Draw geometry into a Polygon. A Polygon (the
@@ -113,12 +394,6 @@ declare class GoogleMapsService implements OnDestroy {
113
394
  * they drew is discarded. Any other geometry type yields undefined.
114
395
  */
115
396
  private _toDrawnPolygon;
116
- /**
117
- * Find an existing Polygon feature that fully contains the drawn polygon, so
118
- * the drawing can be applied as a cutout. Returns the existing feature
119
- * instance (never a copy).
120
- */
121
- private _getPossibleExteriorFeature;
122
397
  static ɵfac: i0.ɵɵFactoryDeclaration<GoogleMapsService, never>;
123
398
  static ɵprov: i0.ɵɵInjectableDeclaration<GoogleMapsService>;
124
399
  }
@@ -164,6 +439,7 @@ declare class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase imp
164
439
  static ngAcceptInputType_streetViewControlEnabled: BooleanInput;
165
440
  static ngAcceptInputType_allowDrawingHoleInPolygon: BooleanInput;
166
441
  static ngAcceptInputType_editingEnabled: BooleanInput;
442
+ static ngAcceptInputType_selectedGroupKey: string | null;
167
443
  private readonly _changeDetectorRef;
168
444
  private readonly _ngUnsubscribe;
169
445
  readonly _gmApiLoaded: Observable<boolean>;
@@ -198,7 +474,32 @@ declare class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase imp
198
474
  longitude: number;
199
475
  latitude: number;
200
476
  padding: number | google.maps.Padding | undefined;
477
+ /**
478
+ * Which interaction model the map uses.
479
+ *
480
+ * `'legacy'` is the single-boundary behaviour this component has always had
481
+ * and is the default, so existing consumers are unaffected. `'grouped'`
482
+ * separates selection from geometry editing; see the design doc.
483
+ */
484
+ interactionMode: TheSeamMapInteractionMode;
485
+ /**
486
+ * Name of the GeoJSON property that groups features into one logical thing,
487
+ * such as a field. Features sharing a value select, style, and edit together.
488
+ * Unset means every feature is its own group.
489
+ */
490
+ featureGroupProperty: string | undefined;
491
+ /** Name of the GeoJSON property holding a group's label text. */
492
+ featureLabelProperty: string | undefined;
493
+ /**
494
+ * Generates the key written to `featureGroupProperty` for a newly drawn
495
+ * group. Consumer-supplied so the format is one the app recognises.
496
+ */
497
+ newGroupKeyFactory: (() => string) | undefined;
498
+ /** Preselect a group. Applied on map-ready and after each external value write. */
499
+ selectedGroupKey: string | null;
201
500
  mapReady: EventEmitter<google.maps.Map | undefined>;
501
+ selectionChange: EventEmitter<TheSeamMapGroupTarget | null>;
502
+ featureHoverChange: EventEmitter<TheSeamMapGroupTarget | null>;
202
503
  featureContextMenu: MenuComponent;
203
504
  _options: {
204
505
  mapTypeControl: boolean;
@@ -212,6 +513,7 @@ declare class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase imp
212
513
  ngOnInit(): void;
213
514
  ngOnDestroy(): void;
214
515
  ngOnChanges(changes: SimpleChanges): void;
516
+ private _applySelectedGroupKey;
215
517
  writeValue(value: MapValue): void;
216
518
  registerOnChange(fn: any): void;
217
519
  registerOnTouched(fn: any): void;
@@ -224,8 +526,18 @@ declare class TheSeamGoogleMapsComponent extends _TheSeamGoogleMapsMixinBase imp
224
526
  private _getHostElement;
225
527
  _onMapReady(theMap: google.maps.Map): void;
226
528
  _onClickDeleteFeature(): void;
529
+ /** Select a group by key. Returns false when no such group exists. */
530
+ selectGroup(key: string): boolean;
531
+ clearSelection(): void;
532
+ /** Fit the viewport to a group. Returns false when no such group exists. */
533
+ fitGroup(key: string, padding?: number | google.maps.Padding): boolean;
534
+ /** Pan to a group's centre. Returns false when no such group exists. */
535
+ panToGroup(key: string): boolean;
536
+ getGroups(): TheSeamMapFeatureGroup[];
537
+ setEditMode(enabled: boolean): void;
538
+ isEditMode(): boolean;
227
539
  static ɵfac: i0.ɵɵFactoryDeclaration<TheSeamGoogleMapsComponent, never>;
228
- static ɵcmp: i0.ɵɵComponentDeclaration<TheSeamGoogleMapsComponent, "seam-google-maps", ["seamGoogleMaps"], { "disabled": { "alias": "disabled"; "required": false; }; "value": { "alias": "value"; "required": false; }; "tabIndex": { "alias": "tabIndex"; "required": false; }; "fileDropEnabled": { "alias": "fileDropEnabled"; "required": false; }; "fileUploadControlEnabled": { "alias": "fileUploadControlEnabled"; "required": false; }; "fullscreenControlEnabled": { "alias": "fullscreenControlEnabled"; "required": false; }; "reCenterControlEnabled": { "alias": "reCenterControlEnabled"; "required": false; }; "mapTypeControlEnabled": { "alias": "mapTypeControlEnabled"; "required": false; }; "streetViewControlEnabled": { "alias": "streetViewControlEnabled"; "required": false; }; "allowDrawingHoleInPolygon": { "alias": "allowDrawingHoleInPolygon"; "required": false; }; "editingEnabled": { "alias": "editingEnabled"; "required": false; }; "fileImportHandler": { "alias": "fileImportHandler"; "required": false; }; "zoom": { "alias": "zoom"; "required": false; }; "longitude": { "alias": "longitude"; "required": false; }; "latitude": { "alias": "latitude"; "required": false; }; "padding": { "alias": "padding"; "required": false; }; }, { "mapReady": "mapReady"; }, never, never, false, never>;
540
+ static ɵcmp: i0.ɵɵComponentDeclaration<TheSeamGoogleMapsComponent, "seam-google-maps", ["seamGoogleMaps"], { "disabled": { "alias": "disabled"; "required": false; }; "value": { "alias": "value"; "required": false; }; "tabIndex": { "alias": "tabIndex"; "required": false; }; "fileDropEnabled": { "alias": "fileDropEnabled"; "required": false; }; "fileUploadControlEnabled": { "alias": "fileUploadControlEnabled"; "required": false; }; "fullscreenControlEnabled": { "alias": "fullscreenControlEnabled"; "required": false; }; "reCenterControlEnabled": { "alias": "reCenterControlEnabled"; "required": false; }; "mapTypeControlEnabled": { "alias": "mapTypeControlEnabled"; "required": false; }; "streetViewControlEnabled": { "alias": "streetViewControlEnabled"; "required": false; }; "allowDrawingHoleInPolygon": { "alias": "allowDrawingHoleInPolygon"; "required": false; }; "editingEnabled": { "alias": "editingEnabled"; "required": false; }; "fileImportHandler": { "alias": "fileImportHandler"; "required": false; }; "zoom": { "alias": "zoom"; "required": false; }; "longitude": { "alias": "longitude"; "required": false; }; "latitude": { "alias": "latitude"; "required": false; }; "padding": { "alias": "padding"; "required": false; }; "interactionMode": { "alias": "interactionMode"; "required": false; }; "featureGroupProperty": { "alias": "featureGroupProperty"; "required": false; }; "featureLabelProperty": { "alias": "featureLabelProperty"; "required": false; }; "newGroupKeyFactory": { "alias": "newGroupKeyFactory"; "required": false; }; "selectedGroupKey": { "alias": "selectedGroupKey"; "required": false; }; }, { "mapReady": "mapReady"; "selectionChange": "selectionChange"; "featureHoverChange": "featureHoverChange"; }, never, never, false, never>;
229
541
  }
230
542
 
231
543
  /**
@@ -542,7 +854,8 @@ declare class TheSeamGoogleMapsModule {
542
854
  }
543
855
 
544
856
  declare enum AppFeaturePropertyName {
545
- IsSelected = "__app__isSelected"
857
+ IsSelected = "__app__isSelected",
858
+ GroupKey = "__app__groupKey"
546
859
  }
547
860
  declare function isAppFeatureProperty(propertyName: string): propertyName is AppFeaturePropertyName;
548
861
  declare function isFeatureSelected(feature: google.maps.Data.Feature): boolean;
@@ -560,6 +873,34 @@ declare function dataPolygonFromGeoJson(polygon: Polygon): google.maps.Data.Poly
560
873
  * Returns undefined for non-Polygon geometries. Does not mutate the feature.
561
874
  */
562
875
  declare function geoJsonPolygonFromDataFeature(feature: google.maps.Data.Feature): Polygon | undefined;
876
+ /** Build a google.maps.Data.MultiPolygon from a GeoJSON MultiPolygon. */
877
+ declare function dataMultiPolygonFromGeoJson(multiPolygon: MultiPolygon): google.maps.Data.MultiPolygon;
878
+ /**
879
+ * Every Polygon part of a feature, as closed GeoJSON. A Polygon feature yields
880
+ * one; a MultiPolygon yields one per part; anything else yields none.
881
+ *
882
+ * Callers that need to match a drawn shape against existing geometry must use
883
+ * this rather than `geoJsonPolygonFromDataFeature`, which sees only Polygons
884
+ * and silently ignores MultiPolygon features.
885
+ */
886
+ declare function polygonsFromDataFeature(feature: google.maps.Data.Feature): Polygon[];
887
+ /**
888
+ * Cut `hole` into whichever part of `feature`'s geometry fully contains it.
889
+ *
890
+ * Mutates the EXISTING feature (via `setGeometry`) rather than replacing it,
891
+ * so its identity and properties survive. A single-part Polygon feature stays
892
+ * a Polygon; a MultiPolygon feature stays a MultiPolygon with only the
893
+ * containing part modified. Returns whether a containing part was found —
894
+ * when none is, the feature is left untouched.
895
+ */
896
+ declare function applyHoleToFeature(feature: google.maps.Data.Feature, hole: Polygon): boolean;
897
+ /**
898
+ * Read a feature as a GeoJSON Feature, geometry and properties together.
899
+ *
900
+ * `excludeProperty` filters properties out by name — used to keep internal
901
+ * `__app__` bookkeeping out of anything handed to a consumer.
902
+ */
903
+ declare function geoJsonFeatureFromDataFeature(feature: google.maps.Data.Feature, excludeProperty?: (name: string) => boolean): Feature<Polygon | MultiPolygon> | undefined;
563
904
  declare function getBoundsWithAllFeatures(data: google.maps.Data): google.maps.LatLngBounds;
564
905
  declare function getFeatureBounds(feature: google.maps.Data.Feature): google.maps.LatLngBounds;
565
906
  declare function getFeatureCenter(feature: google.maps.Data.Feature): google.maps.LatLng;
@@ -570,6 +911,78 @@ declare function getFeaturesCount(data: google.maps.Data): number;
570
911
  */
571
912
  declare function createFeatureChangeObservable(data: google.maps.Data, ngZone: NgZone): Observable<void>;
572
913
 
914
+ declare const FEATURE_STYLE_OPTIONS_DEFAULT: () => google.maps.Data.StyleOptions;
915
+ declare const FEATURE_STYLE_OPTIONS_SELECTED: () => google.maps.Data.StyleOptions;
916
+ declare const FEATURE_STYLE_OVERRIDE_OPTIONS_HOVERED: () => google.maps.Data.StyleOptions;
917
+ /**
918
+ * Style options a feature may set through its GeoJSON properties.
919
+ *
920
+ * `editable`, `draggable`, and `clickable` are here so a feature can opt OUT —
921
+ * a retired field stays unreshapeable while its neighbours do not. They cannot
922
+ * opt in: the clamp in `computeFeatureStyle` runs last and only ever narrows.
923
+ */
924
+ declare const SUPPORTED_PROPERTY_STYLE_OPTIONS: (keyof google.maps.Data.StyleOptions)[];
925
+ /** What the interaction model and the global gate permit for a feature. */
926
+ interface TheSeamMapFeatureStyleContext {
927
+ /** The `editingEnabled` input. */
928
+ editingEnabled: boolean;
929
+ /** Whether the model currently arms geometry editing at all. */
930
+ geometryEditingArmed: boolean;
931
+ /** Whether the model lets this feature receive clicks. */
932
+ clicksAllowed: boolean;
933
+ }
934
+ /** Merge only the supported subset of one options object into another. */
935
+ declare function mergeStyleOptions(target: google.maps.Data.StyleOptions, source: google.maps.Data.StyleOptions | undefined): google.maps.Data.StyleOptions;
936
+ /**
937
+ * The style for a feature, in a fixed precedence order:
938
+ *
939
+ * defaults
940
+ * -> properties.styleOptions
941
+ * -> selected defaults (when selected)
942
+ * -> properties.styleOptionsSelected (when selected)
943
+ * -> interaction clamp (always last)
944
+ *
945
+ * That chain governs the *visual* options (colours, etc.) only. The opt-out
946
+ * flags (`editable`/`draggable`/`clickable`) resolve separately, per key: a
947
+ * selected feature consults `styleOptionsSelected` first and falls back to
948
+ * `styleOptions` only for a key `styleOptionsSelected` doesn't itself
949
+ * mention — never a whole-object fallback. Otherwise a feature that declares
950
+ * `styleOptionsSelected` for an unrelated reason, such as a different
951
+ * selected fill colour, would silently lose an unrelated `editable: false`
952
+ * from `styleOptions` the moment it's selected.
953
+ *
954
+ * The clamp is one-directional. A feature can only narrow what the context
955
+ * permits, never widen it, so no property in an uploaded file can arm editing
956
+ * when the mode says no.
957
+ *
958
+ * A declared `editable: false` also implies `draggable: false`: moving a
959
+ * polygon changes the map's value exactly as much as reshaping it does, so a
960
+ * feature that opts out of one opts out of the other. This implication runs
961
+ * only one way — `draggable: false` alone does not imply `editable: false`, so
962
+ * a feature may still opt out of dragging while remaining reshapeable via its
963
+ * vertex handles. Resolved through the same per-key `wants()` lookup as
964
+ * `editable` itself, so a feature that locks `editable: false` in
965
+ * `styleOptions` cannot be dragged even if `styleOptionsSelected` declares
966
+ * `draggable` for an unrelated reason (or not at all).
967
+ */
968
+ declare function computeFeatureStyle(feature: google.maps.Data.Feature, context: TheSeamMapFeatureStyleContext): google.maps.Data.StyleOptions;
969
+ /**
970
+ * The hover override, merged with anything the feature declares for hover.
971
+ *
972
+ * `mergeStyleOptions` is shared with `computeFeatureStyle`, and
973
+ * `SUPPORTED_PROPERTY_STYLE_OPTIONS` includes `editable`/`draggable` so a
974
+ * feature can opt OUT of them via `styleOptions`/`styleOptionsSelected`. But
975
+ * this override is applied via `overrideStyle`, with no clamp downstream at
976
+ * all — unlike `computeFeatureStyle`, where the interaction clamp runs last
977
+ * and narrows whatever came before it. So if a consumer's
978
+ * `styleOptionsHovered` declared `editable` or `draggable`, it would apply
979
+ * unclamped: the one path where the "clamp is one-directional and always
980
+ * runs last" guarantee would not hold. A hover override has no business
981
+ * touching either, so both are stripped unconditionally, regardless of what
982
+ * `styleOptionsHovered` declares.
983
+ */
984
+ declare function computeFeatureHoverStyle(feature: google.maps.Data.Feature): google.maps.Data.StyleOptions;
985
+
573
986
  declare class GoogleMapsContextMenu {
574
987
  private readonly _map;
575
988
  private readonly _menu;
@@ -665,5 +1078,5 @@ declare class TheSeamNoopGoogleMapsAPILoader extends TheSeamGoogleMapsApiLoader
665
1078
  static ɵprov: i0.ɵɵInjectableDeclaration<TheSeamNoopGoogleMapsAPILoader>;
666
1079
  }
667
1080
 
668
- export { AppFeaturePropertyName, GoogleMapsContextMenu, GoogleMapsControlsService, GoogleMapsService, MAP_CONTROLS_SERVICE, MAP_CONTROL_DATA, MapControlRef, MapValueManagerService, MapValueSource, SEAM_GOOGLE_PLACES_AUTOCOMPLETE_DEFAULT_OPTIONS, THESEAM_LAZY_MAPS_API_CONFIG, TheSeamGoogleMapsApiLoader, TheSeamGoogleMapsComponent, TheSeamGoogleMapsModule, TheSeamGoogleMapsPlacesAutoCompleteComponent, TheSeamGoogleMapsPlacesAutocompleteDirective, TheSeamGoogleMapsRecenterButtonControlComponent, TheSeamGoogleMapsUploadButtonControlComponent, TheSeamLazyMapsApiLoader, TheSeamMapFileDropComponent, TheSeamMapsControlComponent, TheSeamNoopGoogleMapsAPILoader, createFeatureChangeObservable, dataPolygonFromGeoJson, geoJsonPolygonFromDataFeature, getBoundsWithAllFeatures, getFeatureBounds, getFeatureCenter, getFeaturesCount, getHoveredStyleOptionsDefinedByFeature, getSelectedStyleOptionsDefinedByFeature, getStyleOptionsDefinedByFeature, isAppFeatureProperty, isFeatureSelected, polygonCoordinates, polygonHasValidPathsLengths, removeAllFeatures, setFeatureSelected, stripAppFeaturePropertiesFromJson };
669
- export type { GoogleMapsRecenterButtonControlData, GoogleMapsUploadButtonControlData, MapControl, MapControlsService, MapValue, MapValueChange, TheSeamLazyMapsApiLoaderConfig };
1081
+ 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 };
1082
+ export type { GoogleMapsRecenterButtonControlData, GoogleMapsUploadButtonControlData, MapControl, MapControlsService, MapValue, MapValueChange, TheSeamLazyMapsApiLoaderConfig, TheSeamMapFeatureGroup, TheSeamMapFeatureStyleContext, TheSeamMapGroupTarget, TheSeamMapInteractionMode };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theseam/ui-common",
3
- "version": "2.0.1-beta.110",
3
+ "version": "2.0.1-beta.111",
4
4
  "peerDependencies": {
5
5
  "@angular/cdk": "^20.2.3",
6
6
  "@angular/common": "^20.3.0",
@@ -1,6 +1,6 @@
1
1
  import { HandlerFunction } from 'storybook/actions';
2
2
  import * as i0 from '@angular/core';
3
- import { ModuleWithProviders, Injector, TemplateRef, InjectionToken, ComponentRef } from '@angular/core';
3
+ import { ModuleWithProviders, Injector, TemplateRef, InjectionToken, ComponentRef, Type } from '@angular/core';
4
4
  import * as i2 from '@angular/common';
5
5
  import * as i3 from '@angular/router';
6
6
  import * as i4 from '@theseam/ui-common/scrollbar';
@@ -194,5 +194,7 @@ declare const INJECTOR_TO_ARGS_PROPERTY_NAME = "__getInjector";
194
194
  declare const addInjectorGetterToArgs: (argName?: string) => DecoratorFunction<AngularRenderer>;
195
195
  declare const getInjectorFromArgs: (args: any, argName?: string) => Injector;
196
196
 
197
- export { INJECTOR_TO_ARGS_PROPERTY_NAME, STORY_INITIAL_ROUTE_URL, StoryEmptyComponent, StoryEmptyWithRouteComponent, StoryHelperComponentsModule, StoryInitialRouteModule, StoryInitialRouteService, StoryModalContainerComponent, StoryPreferencesAccessorService, addInjectorGetterToArgs, argsToTpl, buttonTypeArgType, getInjectorFromArgs, onStoryBootstrappedTrigger, routeButton, routesArgType, sizeArgType, storyInitialRouteFactory, storyModalDecorator, themeArgType, themeWithOutlineArgType };
197
+ declare function getComponentInstance<T>(element: Element | null | undefined, componentType?: Type<T>): T | null;
198
+
199
+ export { INJECTOR_TO_ARGS_PROPERTY_NAME, STORY_INITIAL_ROUTE_URL, StoryEmptyComponent, StoryEmptyWithRouteComponent, StoryHelperComponentsModule, StoryInitialRouteModule, StoryInitialRouteService, StoryModalContainerComponent, StoryPreferencesAccessorService, addInjectorGetterToArgs, argsToTpl, buttonTypeArgType, getComponentInstance, getInjectorFromArgs, onStoryBootstrappedTrigger, routeButton, routesArgType, sizeArgType, storyInitialRouteFactory, storyModalDecorator, themeArgType, themeWithOutlineArgType };
198
200
  export type { ArgsTplOptions, ArgsTplParts, StoryModalDecoratorOptions };