@accelint/map-toolkit 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +3 -0
  3. package/catalog-info.yaml +6 -3
  4. package/dist/camera/events.d.ts +15 -0
  5. package/dist/camera/events.js +29 -0
  6. package/dist/camera/events.js.map +1 -0
  7. package/dist/camera/index.d.ts +16 -0
  8. package/dist/camera/index.js +17 -0
  9. package/dist/camera/types.d.ts +84 -0
  10. package/dist/{decorators/deckgl.d.ts → camera/types.js} +0 -7
  11. package/dist/camera/use-camera-state.d.ts +153 -0
  12. package/dist/camera/use-camera-state.js +418 -0
  13. package/dist/camera/use-camera-state.js.map +1 -0
  14. package/dist/cursor-coordinates/use-cursor-coordinates.js +1 -1
  15. package/dist/deckgl/base-map/constants.d.ts +14 -2
  16. package/dist/deckgl/base-map/constants.js +14 -2
  17. package/dist/deckgl/base-map/constants.js.map +1 -1
  18. package/dist/deckgl/base-map/controls.d.ts +34 -0
  19. package/dist/deckgl/base-map/controls.js +50 -0
  20. package/dist/deckgl/base-map/controls.js.map +1 -0
  21. package/dist/deckgl/base-map/events.d.ts +4 -0
  22. package/dist/deckgl/base-map/events.js +5 -1
  23. package/dist/deckgl/base-map/events.js.map +1 -1
  24. package/dist/deckgl/base-map/index.d.ts +10 -23
  25. package/dist/deckgl/base-map/index.js +81 -42
  26. package/dist/deckgl/base-map/index.js.map +1 -1
  27. package/dist/deckgl/base-map/types.d.ts +48 -2
  28. package/dist/deckgl/index.d.ts +11 -4
  29. package/dist/deckgl/index.js +7 -2
  30. package/dist/deckgl/saved-viewports/index.d.ts +32 -0
  31. package/dist/deckgl/saved-viewports/index.js +51 -0
  32. package/dist/deckgl/saved-viewports/index.js.map +1 -0
  33. package/dist/deckgl/saved-viewports/storage.d.ts +21 -0
  34. package/dist/deckgl/saved-viewports/storage.js +39 -0
  35. package/dist/deckgl/saved-viewports/storage.js.map +1 -0
  36. package/dist/deckgl/shapes/display-shape-layer/constants.d.ts +44 -0
  37. package/dist/deckgl/shapes/display-shape-layer/constants.js +61 -0
  38. package/dist/deckgl/shapes/display-shape-layer/constants.js.map +1 -0
  39. package/dist/deckgl/shapes/display-shape-layer/fiber.d.ts +25 -0
  40. package/dist/deckgl/shapes/display-shape-layer/fiber.js +21 -0
  41. package/dist/deckgl/shapes/display-shape-layer/fiber.js.map +1 -0
  42. package/dist/deckgl/shapes/display-shape-layer/index.d.ts +206 -0
  43. package/dist/deckgl/shapes/display-shape-layer/index.js +416 -0
  44. package/dist/deckgl/shapes/display-shape-layer/index.js.map +1 -0
  45. package/dist/deckgl/shapes/display-shape-layer/shape-label-layer.d.ts +66 -0
  46. package/dist/deckgl/shapes/display-shape-layer/shape-label-layer.js +116 -0
  47. package/dist/deckgl/shapes/display-shape-layer/shape-label-layer.js.map +1 -0
  48. package/dist/deckgl/shapes/display-shape-layer/store.d.ts +87 -0
  49. package/dist/deckgl/shapes/display-shape-layer/store.js +316 -0
  50. package/dist/deckgl/shapes/display-shape-layer/store.js.map +1 -0
  51. package/dist/deckgl/shapes/display-shape-layer/types.d.ts +115 -0
  52. package/dist/deckgl/shapes/display-shape-layer/types.js +12 -0
  53. package/dist/deckgl/shapes/display-shape-layer/use-shape-selection.d.ts +89 -0
  54. package/dist/deckgl/shapes/display-shape-layer/use-shape-selection.js +88 -0
  55. package/dist/deckgl/shapes/display-shape-layer/use-shape-selection.js.map +1 -0
  56. package/dist/deckgl/shapes/display-shape-layer/utils/display-style.d.ts +61 -0
  57. package/dist/deckgl/shapes/display-shape-layer/utils/display-style.js +111 -0
  58. package/dist/deckgl/shapes/display-shape-layer/utils/display-style.js.map +1 -0
  59. package/dist/deckgl/shapes/display-shape-layer/utils/labels.d.ts +196 -0
  60. package/dist/deckgl/shapes/display-shape-layer/utils/labels.js +368 -0
  61. package/dist/deckgl/shapes/display-shape-layer/utils/labels.js.map +1 -0
  62. package/dist/deckgl/shapes/index.d.ts +20 -0
  63. package/dist/deckgl/shapes/index.js +20 -0
  64. package/dist/deckgl/shapes/shared/constants.d.ts +78 -0
  65. package/dist/deckgl/shapes/shared/constants.js +109 -0
  66. package/dist/deckgl/shapes/shared/constants.js.map +1 -0
  67. package/dist/deckgl/shapes/shared/events.d.ts +73 -0
  68. package/dist/deckgl/shapes/shared/events.js +58 -0
  69. package/dist/deckgl/shapes/shared/events.js.map +1 -0
  70. package/dist/deckgl/shapes/shared/types.d.ts +158 -0
  71. package/dist/{decorators/deckgl.js → deckgl/shapes/shared/types.js} +12 -15
  72. package/dist/deckgl/shapes/shared/types.js.map +1 -0
  73. package/dist/deckgl/symbol-layer/index.d.ts +1 -1
  74. package/dist/maplibre/hooks/use-maplibre.d.ts +2 -2
  75. package/dist/maplibre/hooks/use-maplibre.js +2 -2
  76. package/dist/maplibre/hooks/use-maplibre.js.map +1 -1
  77. package/dist/viewport/viewport-size.d.ts +2 -2
  78. package/package.json +50 -23
  79. package/dist/decorators/deckgl.js.map +0 -1
@@ -0,0 +1,416 @@
1
+ /*
2
+ * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at https://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+
14
+ 'use client';
15
+
16
+ import { DASH_ARRAYS, SHAPE_LAYER_IDS } from "../shared/constants.js";
17
+ import { ShapeEvents } from "../shared/events.js";
18
+ import { COFFIN_CORNERS, DEFAULT_DISPLAY_PROPS, MAP_INTERACTION } from "./constants.js";
19
+ import { createShapeLabelLayer } from "./shape-label-layer.js";
20
+ import { getDashArray, getFillColor, getHighlightColor, getHighlightLineWidth, getHoverLineWidth, getStrokeColor } from "./utils/display-style.js";
21
+ import { Broadcast } from "@accelint/bus";
22
+ import { CompositeLayer } from "@deck.gl/core";
23
+ import { PathStyleExtension } from "@deck.gl/extensions";
24
+ import { GeoJsonLayer, IconLayer } from "@deck.gl/layers";
25
+
26
+ //#region src/deckgl/shapes/display-shape-layer/index.ts
27
+ /**
28
+ * Typed event bus instance for shape events.
29
+ * Provides type-safe event emission for shape interactions.
30
+ */
31
+ const shapeBus = Broadcast.getInstance();
32
+ /**
33
+ * DisplayShapeLayer - Read-only shapes visualization layer
34
+ *
35
+ * A composite deck.gl layer for displaying geographic shapes with interactive features.
36
+ * Ideal for rendering shapes from external APIs or displaying read-only geographic data.
37
+ *
38
+ * ## Features
39
+ * - **Multiple geometry types**: Point, LineString, Polygon, and Circle
40
+ * - **Icon support**: Custom icons for Point geometries via icon atlases
41
+ * - **Interactive selection**: Click handling with dotted border and optional highlight
42
+ * - **Hover effects**: Line width increases on hover for better UX
43
+ * - **Customizable labels**: Flexible label positioning with per-shape or global options
44
+ * - **Style properties**: Full control over colors, stroke patterns, and opacity
45
+ * - **Event bus integration**: Automatically emits shape events via @accelint/bus
46
+ * - **Multi-map support**: Events include map instance ID for isolation
47
+ *
48
+ * ## Selection Visual Feedback
49
+ * When a shape is selected via `selectedShapeId`:
50
+ * - The shape's stroke pattern changes to dotted
51
+ * - An optional highlight renders underneath (controlled by `showHighlight` prop)
52
+ *
53
+ * ## Layer Structure
54
+ * Renders up to four sublayers (in order, bottom to top):
55
+ * 1. **Highlight layer**: Selection highlight effect for non-icon-Point shapes (if showHighlight=true)
56
+ * 2. **Coffin corners layer**: Selection/hover feedback for Point shapes with icons
57
+ * 3. **Main GeoJsonLayer**: Shape geometries with styling and interaction
58
+ * 4. **Label layer**: Text labels (if showLabels enabled)
59
+ *
60
+ * ## Icon Atlas Constraint
61
+ * When using icons for Point geometries, all shapes in a single layer must share the
62
+ * same icon atlas. The layer uses the first atlas found across all features. If you
63
+ * need icons from different atlases, use separate DisplayShapeLayer instances.
64
+ *
65
+ * ## Event Bus Integration
66
+ * Automatically emits shape events that can be consumed anywhere in your app:
67
+ * - `shapes:selected` - Emitted when a shape is clicked (includes mapId)
68
+ * - `shapes:hovered` - Emitted when the hovered shape changes (deduplicated, includes mapId)
69
+ *
70
+ * For selection with auto-deselection, use the companion `useShapeSelection` hook which handles
71
+ * all the event wiring automatically. See the example below.
72
+ *
73
+ * @example Basic usage with useShapeSelection hook (recommended)
74
+ * ```tsx
75
+ * import '@accelint/map-toolkit/deckgl/shapes/display-shape-layer/fiber';
76
+ * import { useShapeSelection } from '@accelint/map-toolkit/deckgl/shapes';
77
+ * import { uuid } from '@accelint/core';
78
+ *
79
+ * const MAP_ID = uuid();
80
+ *
81
+ * function MapWithShapes() {
82
+ * const { selectedId } = useShapeSelection(MAP_ID);
83
+ *
84
+ * return (
85
+ * <BaseMap id={MAP_ID}>
86
+ * <displayShapeLayer
87
+ * id="my-shapes"
88
+ * mapId={MAP_ID}
89
+ * data={shapes}
90
+ * selectedShapeId={selectedId}
91
+ * showLabels={true}
92
+ * pickable={true}
93
+ * />
94
+ * </BaseMap>
95
+ * );
96
+ * }
97
+ * ```
98
+ *
99
+ * @example With custom label positioning
100
+ * ```tsx
101
+ * <displayShapeLayer
102
+ * id="my-shapes"
103
+ * data={shapes}
104
+ * showLabels={true}
105
+ * labelOptions={{
106
+ * // Position circle labels at the top
107
+ * circleLabelCoordinateAnchor: 'top',
108
+ * circleLabelVerticalAnchor: 'bottom',
109
+ * circleLabelOffset: [0, -10],
110
+ * // Position line labels at the middle
111
+ * lineStringLabelCoordinateAnchor: 'middle',
112
+ * }}
113
+ * />
114
+ * ```
115
+ */
116
+ var DisplayShapeLayer = class extends CompositeLayer {
117
+ /** Cache for transformed features to avoid recreating objects on every render */
118
+ featuresCache = null;
119
+ static layerName = "DisplayShapeLayer";
120
+ static defaultProps = { ...DEFAULT_DISPLAY_PROPS };
121
+ /**
122
+ * Clean up state and caches when layer is destroyed
123
+ */
124
+ finalizeState() {
125
+ if (this.state?.hoverIndex !== void 0) this.setState({
126
+ hoverIndex: void 0,
127
+ lastHoveredId: void 0
128
+ });
129
+ this.featuresCache = null;
130
+ }
131
+ /**
132
+ * Override getPickingInfo to handle events from sublayers
133
+ * This is the correct pattern for CompositeLayer event handling
134
+ */
135
+ getPickingInfo({ info, mode, sourceLayer }) {
136
+ if (sourceLayer?.id === `${this.props.id}-${SHAPE_LAYER_IDS.DISPLAY}`) {
137
+ if (mode === "query") this.handleShapeClick(info);
138
+ if (mode === "hover" || !mode) {
139
+ if (info.index !== void 0 && info.index !== this.state?.hoverIndex) this.setState({ hoverIndex: info.index });
140
+ else if (info.index === void 0 && this.state?.hoverIndex !== void 0) this.setState({ hoverIndex: void 0 });
141
+ this.handleShapeHover(info);
142
+ }
143
+ }
144
+ return info;
145
+ }
146
+ /**
147
+ * Convert shapes to GeoJSON features with shapeId in properties.
148
+ * Uses caching to avoid recreating objects on every render cycle.
149
+ */
150
+ getFeaturesWithId() {
151
+ const { data } = this.props;
152
+ if (this.featuresCache?.data === data) return this.featuresCache.features;
153
+ const features = data.map((shape) => ({
154
+ ...shape.feature,
155
+ properties: {
156
+ ...shape.feature.properties,
157
+ shapeId: shape.id
158
+ }
159
+ }));
160
+ this.featuresCache = {
161
+ data,
162
+ features
163
+ };
164
+ return features;
165
+ }
166
+ /**
167
+ * Look up a shape by ID from the data prop.
168
+ * Used by event handlers to get full shape without storing in feature properties.
169
+ */
170
+ getShapeById(shapeId) {
171
+ return this.props.data.find((shape) => shape.id === shapeId);
172
+ }
173
+ /**
174
+ * Handle shape click
175
+ */
176
+ handleShapeClick = (info) => {
177
+ const { onShapeClick, mapId } = this.props;
178
+ if (!info.object) return;
179
+ const shapeId = info.object.properties?.shapeId;
180
+ if (!shapeId) return;
181
+ const shape = this.getShapeById(shapeId);
182
+ if (!shape) return;
183
+ shapeBus.emit(ShapeEvents.selected, {
184
+ shapeId: shape.id,
185
+ mapId
186
+ });
187
+ if (onShapeClick) onShapeClick(shape);
188
+ };
189
+ /**
190
+ * Handle shape hover
191
+ */
192
+ handleShapeHover = (info) => {
193
+ const { onShapeHover, mapId } = this.props;
194
+ const shapeId = info.object?.properties?.shapeId ?? null;
195
+ const shape = shapeId ? this.getShapeById(shapeId) ?? null : null;
196
+ if (shapeId !== this.state?.lastHoveredId) {
197
+ this.setState({ lastHoveredId: shapeId });
198
+ shapeBus.emit(ShapeEvents.hovered, {
199
+ shapeId,
200
+ mapId
201
+ });
202
+ }
203
+ if (onShapeHover) onShapeHover(shape);
204
+ };
205
+ /**
206
+ * Render highlight sublayer (underneath main layer)
207
+ * Note: Points with icons use coffin corners instead of highlight layer
208
+ */
209
+ renderHighlightLayer(features) {
210
+ const { selectedShapeId, showHighlight, highlightColor } = this.props;
211
+ if (!selectedShapeId || showHighlight === false) return null;
212
+ const selectedFeature = features.find((f) => f.properties?.shapeId === selectedShapeId);
213
+ if (!selectedFeature) return null;
214
+ if (selectedFeature.geometry.type === "Point") {
215
+ if (!!selectedFeature.properties?.styleProperties?.icon) return null;
216
+ }
217
+ return new GeoJsonLayer({
218
+ id: `${this.props.id}-${SHAPE_LAYER_IDS.DISPLAY_HIGHLIGHT}`,
219
+ data: [selectedFeature],
220
+ filled: true,
221
+ stroked: true,
222
+ lineWidthUnits: "pixels",
223
+ lineWidthMinPixels: MAP_INTERACTION.LINE_WIDTH_MIN_PIXELS,
224
+ getFillColor: () => [
225
+ 0,
226
+ 0,
227
+ 0,
228
+ 0
229
+ ],
230
+ getLineColor: () => highlightColor || getHighlightColor(),
231
+ getLineWidth: getHighlightLineWidth,
232
+ pickable: false,
233
+ updateTriggers: {
234
+ getLineColor: [highlightColor],
235
+ getLineWidth: [selectedShapeId, features]
236
+ }
237
+ });
238
+ }
239
+ /**
240
+ * Render coffin corners layer for Point geometries that have icons on hover/select
241
+ * Coffin corners provide visual feedback for points instead of highlight layer
242
+ */
243
+ renderCoffinCornersLayer(features) {
244
+ const { selectedShapeId } = this.props;
245
+ const pointFeatures = features.filter((f) => {
246
+ if (f.geometry.type !== "Point") return false;
247
+ if (!!!f.properties?.styleProperties?.icon) return false;
248
+ const isSelected = f.properties?.shapeId === selectedShapeId;
249
+ const isHovered = this.state?.hoverIndex !== void 0 && features.indexOf(f) === this.state.hoverIndex;
250
+ return isSelected || isHovered;
251
+ });
252
+ if (pointFeatures.length === 0) return null;
253
+ const firstPointIcon = pointFeatures[0]?.properties?.styleProperties?.icon;
254
+ const iconAtlas = firstPointIcon?.atlas;
255
+ const iconMapping = firstPointIcon?.mapping;
256
+ if (!iconAtlas) return null;
257
+ if (!iconMapping) return null;
258
+ const extendedMapping = {
259
+ ...iconMapping,
260
+ [COFFIN_CORNERS.HOVER_ICON]: {
261
+ x: 0,
262
+ y: 0,
263
+ width: 76,
264
+ height: 76,
265
+ mask: false
266
+ },
267
+ [COFFIN_CORNERS.SELECTED_ICON]: {
268
+ x: 76,
269
+ y: 0,
270
+ width: 76,
271
+ height: 76,
272
+ mask: false
273
+ },
274
+ [COFFIN_CORNERS.SELECTED_HOVER_ICON]: {
275
+ x: 152,
276
+ y: 0,
277
+ width: 76,
278
+ height: 76,
279
+ mask: false
280
+ }
281
+ };
282
+ return new IconLayer({
283
+ id: `${this.props.id}-${SHAPE_LAYER_IDS.DISPLAY}-coffin-corners`,
284
+ data: pointFeatures,
285
+ iconAtlas,
286
+ iconMapping: extendedMapping,
287
+ getIcon: (d) => {
288
+ const isSelected = d.properties?.shapeId === selectedShapeId;
289
+ const isHovered = this.state?.hoverIndex !== void 0 && features.indexOf(d) === this.state.hoverIndex;
290
+ if (isSelected && isHovered) return COFFIN_CORNERS.SELECTED_HOVER_ICON;
291
+ if (isSelected) return COFFIN_CORNERS.SELECTED_ICON;
292
+ return COFFIN_CORNERS.HOVER_ICON;
293
+ },
294
+ getSize: COFFIN_CORNERS.SIZE,
295
+ getPosition: (d) => {
296
+ return d.geometry.type === "Point" ? d.geometry.coordinates : [0, 0];
297
+ },
298
+ getPixelOffset: (d) => {
299
+ return [-1, -(d.properties?.styleProperties?.icon?.size ?? MAP_INTERACTION.ICON_SIZE) / 2];
300
+ },
301
+ billboard: false,
302
+ pickable: false,
303
+ updateTriggers: {
304
+ getIcon: [selectedShapeId, this.state?.hoverIndex],
305
+ data: [
306
+ features,
307
+ selectedShapeId,
308
+ this.state?.hoverIndex
309
+ ]
310
+ }
311
+ });
312
+ }
313
+ /**
314
+ * Extract icon configuration from features in a single pass.
315
+ * Returns the first icon's atlas and mapping (all shapes share the same atlas).
316
+ * Uses early return for O(1) best case when first feature has icons.
317
+ */
318
+ getIconConfig(features) {
319
+ for (const f of features) {
320
+ const icon = f.properties?.styleProperties?.icon;
321
+ if (icon) return {
322
+ hasIcons: true,
323
+ atlas: icon.atlas,
324
+ mapping: icon.mapping
325
+ };
326
+ }
327
+ return { hasIcons: false };
328
+ }
329
+ /**
330
+ * Render main shapes layer
331
+ */
332
+ renderMainLayer(features) {
333
+ const { pickable, applyBaseOpacity, selectedShapeId } = this.props;
334
+ const { hasIcons, atlas: iconAtlas, mapping: iconMapping } = this.getIconConfig(features);
335
+ return new GeoJsonLayer({
336
+ id: `${this.props.id}-${SHAPE_LAYER_IDS.DISPLAY}`,
337
+ data: features,
338
+ filled: true,
339
+ stroked: true,
340
+ getFillColor: (d) => getFillColor(d, applyBaseOpacity),
341
+ getLineColor: getStrokeColor,
342
+ getLineWidth: (d, info) => {
343
+ return getHoverLineWidth(d, info?.index === this.state?.hoverIndex);
344
+ },
345
+ lineWidthUnits: "pixels",
346
+ lineWidthMinPixels: MAP_INTERACTION.LINE_WIDTH_MIN_PIXELS,
347
+ lineWidthMaxPixels: 20,
348
+ pointType: hasIcons ? "icon" : "circle",
349
+ getPointRadius: (d) => {
350
+ return d.properties?.styleProperties?.icon?.size ?? 2;
351
+ },
352
+ pointRadiusUnits: "pixels",
353
+ ...hasIcons && iconAtlas ? { iconAtlas } : {},
354
+ ...hasIcons && iconMapping ? { iconMapping } : {},
355
+ ...hasIcons ? {
356
+ getIcon: (d) => d.properties?.styleProperties?.icon?.name ?? "marker",
357
+ getIconSize: (d) => {
358
+ return d.properties?.styleProperties?.icon?.size ?? MAP_INTERACTION.ICON_SIZE;
359
+ },
360
+ getIconColor: getStrokeColor,
361
+ getIconPixelOffset: (d) => {
362
+ return [-1, -(d.properties?.styleProperties?.icon?.size ?? MAP_INTERACTION.ICON_SIZE) / 2];
363
+ },
364
+ iconBillboard: false
365
+ } : {},
366
+ extensions: [new PathStyleExtension({ dash: true })],
367
+ getDashArray: (d) => {
368
+ if (d.properties?.shapeId === selectedShapeId) return DASH_ARRAYS.dotted;
369
+ return getDashArray(d);
370
+ },
371
+ pickable,
372
+ autoHighlight: false,
373
+ updateTriggers: {
374
+ getFillColor: [features, applyBaseOpacity],
375
+ getLineColor: [features],
376
+ getLineWidth: [features, this.state?.hoverIndex],
377
+ getDashArray: [features, selectedShapeId],
378
+ getPointRadius: [features],
379
+ ...hasIcons ? {
380
+ getIcon: [features],
381
+ getIconSize: [features],
382
+ getIconColor: [features],
383
+ getIconPixelOffset: [features]
384
+ } : {}
385
+ }
386
+ });
387
+ }
388
+ /**
389
+ * Render labels layer
390
+ */
391
+ renderLabelsLayer() {
392
+ const { showLabels, data, labelOptions } = this.props;
393
+ if (!showLabels) return null;
394
+ return createShapeLabelLayer({
395
+ id: `${this.props.id}-${SHAPE_LAYER_IDS.DISPLAY_LABELS}`,
396
+ data,
397
+ labelOptions
398
+ });
399
+ }
400
+ /**
401
+ * Render all sublayers
402
+ */
403
+ renderLayers() {
404
+ const features = this.getFeaturesWithId();
405
+ return [
406
+ this.renderHighlightLayer(features),
407
+ this.renderCoffinCornersLayer(features),
408
+ this.renderMainLayer(features),
409
+ this.renderLabelsLayer()
410
+ ].filter(Boolean);
411
+ }
412
+ };
413
+
414
+ //#endregion
415
+ export { DisplayShapeLayer };
416
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/deckgl/shapes/display-shape-layer/index.ts"],"sourcesContent":["/*\n * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\n'use client';\n\nimport { Broadcast } from '@accelint/bus';\nimport { CompositeLayer } from '@deck.gl/core';\nimport { PathStyleExtension } from '@deck.gl/extensions';\nimport { GeoJsonLayer, IconLayer } from '@deck.gl/layers';\nimport { DASH_ARRAYS, SHAPE_LAYER_IDS } from '../shared/constants';\nimport { type ShapeEvent, ShapeEvents } from '../shared/events';\nimport {\n COFFIN_CORNERS,\n DEFAULT_DISPLAY_PROPS,\n MAP_INTERACTION,\n} from './constants';\nimport { createShapeLabelLayer } from './shape-label-layer';\nimport {\n getDashArray,\n getFillColor,\n getHighlightColor,\n getHighlightLineWidth,\n getHoverLineWidth,\n getStrokeColor,\n} from './utils/display-style';\nimport type { Layer, PickingInfo } from '@deck.gl/core';\nimport type { EditableShape, ShapeId } from '../shared/types';\nimport type { DisplayShapeLayerProps } from './types';\n\n/**\n * Typed event bus instance for shape events.\n * Provides type-safe event emission for shape interactions.\n */\nconst shapeBus = Broadcast.getInstance<ShapeEvent>();\n\n/**\n * State interface for DisplayShapeLayer\n */\ninterface DisplayShapeLayerState {\n /** Index of currently hovered shape, undefined when not hovering */\n hoverIndex?: number;\n /** ID of the last hovered shape for event deduplication */\n lastHoveredId?: ShapeId | null;\n /** Allow additional properties from base layer state */\n [key: string]: unknown;\n}\n\n/**\n * Cache for transformed features to avoid recreating objects on every render.\n */\ninterface FeaturesCache {\n /** Reference to the original data array for identity comparison */\n data: EditableShape[];\n /** Transformed features with shapeId added to properties */\n features: EditableShape['feature'][];\n}\n\n/**\n * DisplayShapeLayer - Read-only shapes visualization layer\n *\n * A composite deck.gl layer for displaying geographic shapes with interactive features.\n * Ideal for rendering shapes from external APIs or displaying read-only geographic data.\n *\n * ## Features\n * - **Multiple geometry types**: Point, LineString, Polygon, and Circle\n * - **Icon support**: Custom icons for Point geometries via icon atlases\n * - **Interactive selection**: Click handling with dotted border and optional highlight\n * - **Hover effects**: Line width increases on hover for better UX\n * - **Customizable labels**: Flexible label positioning with per-shape or global options\n * - **Style properties**: Full control over colors, stroke patterns, and opacity\n * - **Event bus integration**: Automatically emits shape events via @accelint/bus\n * - **Multi-map support**: Events include map instance ID for isolation\n *\n * ## Selection Visual Feedback\n * When a shape is selected via `selectedShapeId`:\n * - The shape's stroke pattern changes to dotted\n * - An optional highlight renders underneath (controlled by `showHighlight` prop)\n *\n * ## Layer Structure\n * Renders up to four sublayers (in order, bottom to top):\n * 1. **Highlight layer**: Selection highlight effect for non-icon-Point shapes (if showHighlight=true)\n * 2. **Coffin corners layer**: Selection/hover feedback for Point shapes with icons\n * 3. **Main GeoJsonLayer**: Shape geometries with styling and interaction\n * 4. **Label layer**: Text labels (if showLabels enabled)\n *\n * ## Icon Atlas Constraint\n * When using icons for Point geometries, all shapes in a single layer must share the\n * same icon atlas. The layer uses the first atlas found across all features. If you\n * need icons from different atlases, use separate DisplayShapeLayer instances.\n *\n * ## Event Bus Integration\n * Automatically emits shape events that can be consumed anywhere in your app:\n * - `shapes:selected` - Emitted when a shape is clicked (includes mapId)\n * - `shapes:hovered` - Emitted when the hovered shape changes (deduplicated, includes mapId)\n *\n * For selection with auto-deselection, use the companion `useShapeSelection` hook which handles\n * all the event wiring automatically. See the example below.\n *\n * @example Basic usage with useShapeSelection hook (recommended)\n * ```tsx\n * import '@accelint/map-toolkit/deckgl/shapes/display-shape-layer/fiber';\n * import { useShapeSelection } from '@accelint/map-toolkit/deckgl/shapes';\n * import { uuid } from '@accelint/core';\n *\n * const MAP_ID = uuid();\n *\n * function MapWithShapes() {\n * const { selectedId } = useShapeSelection(MAP_ID);\n *\n * return (\n * <BaseMap id={MAP_ID}>\n * <displayShapeLayer\n * id=\"my-shapes\"\n * mapId={MAP_ID}\n * data={shapes}\n * selectedShapeId={selectedId}\n * showLabels={true}\n * pickable={true}\n * />\n * </BaseMap>\n * );\n * }\n * ```\n *\n * @example With custom label positioning\n * ```tsx\n * <displayShapeLayer\n * id=\"my-shapes\"\n * data={shapes}\n * showLabels={true}\n * labelOptions={{\n * // Position circle labels at the top\n * circleLabelCoordinateAnchor: 'top',\n * circleLabelVerticalAnchor: 'bottom',\n * circleLabelOffset: [0, -10],\n * // Position line labels at the middle\n * lineStringLabelCoordinateAnchor: 'middle',\n * }}\n * />\n * ```\n */\nexport class DisplayShapeLayer extends CompositeLayer<DisplayShapeLayerProps> {\n // State is typed via DisplayShapeLayerState but deck.gl doesn't support generic state\n declare state: DisplayShapeLayerState;\n\n /** Cache for transformed features to avoid recreating objects on every render */\n private featuresCache: FeaturesCache | null = null;\n\n static override layerName = 'DisplayShapeLayer';\n\n static override defaultProps = {\n ...DEFAULT_DISPLAY_PROPS,\n };\n\n /**\n * Clean up state and caches when layer is destroyed\n */\n override finalizeState(): void {\n // Clear hover state to prevent stale references\n if (this.state?.hoverIndex !== undefined) {\n this.setState({ hoverIndex: undefined, lastHoveredId: undefined });\n }\n // Clear features cache\n this.featuresCache = null;\n }\n\n /**\n * Override getPickingInfo to handle events from sublayers\n * This is the correct pattern for CompositeLayer event handling\n */\n override getPickingInfo({\n info,\n mode,\n sourceLayer,\n }: {\n info: PickingInfo;\n mode?: string;\n // biome-ignore lint/suspicious/noExplicitAny: sourceLayer type from deck.gl is not well-typed\n sourceLayer?: any;\n }) {\n // Check if this picking event came from our main shapes layer\n if (sourceLayer?.id === `${this.props.id}-${SHAPE_LAYER_IDS.DISPLAY}`) {\n // Handle click events (deck.gl uses 'query' mode for clicks)\n if (mode === 'query') {\n this.handleShapeClick(info);\n }\n\n // Handle hover events (including when mode is undefined, which is hover)\n if (mode === 'hover' || !mode) {\n // Update hover state\n if (info.index !== undefined && info.index !== this.state?.hoverIndex) {\n this.setState({ hoverIndex: info.index });\n } else if (\n info.index === undefined &&\n this.state?.hoverIndex !== undefined\n ) {\n this.setState({ hoverIndex: undefined });\n }\n\n // Call hover callback\n this.handleShapeHover(info);\n }\n }\n\n return info;\n }\n\n /**\n * Convert shapes to GeoJSON features with shapeId in properties.\n * Uses caching to avoid recreating objects on every render cycle.\n */\n private getFeaturesWithId(): EditableShape['feature'][] {\n const { data } = this.props;\n\n // Return cached features if data hasn't changed (identity check)\n if (this.featuresCache?.data === data) {\n return this.featuresCache.features;\n }\n\n // Transform features and cache the result\n const features = data.map((shape) => ({\n ...shape.feature,\n properties: {\n ...shape.feature.properties,\n shapeId: shape.id,\n },\n }));\n\n this.featuresCache = { data, features };\n return features;\n }\n\n /**\n * Look up a shape by ID from the data prop.\n * Used by event handlers to get full shape without storing in feature properties.\n */\n private getShapeById(shapeId: ShapeId): EditableShape | undefined {\n return this.props.data.find((shape) => shape.id === shapeId);\n }\n\n /**\n * Handle shape click\n */\n private handleShapeClick = (info: PickingInfo): void => {\n const { onShapeClick, mapId } = this.props;\n\n if (!info.object) {\n return;\n }\n\n // Look up shape from data prop using shapeId stored in feature properties\n const shapeId = info.object.properties?.shapeId as ShapeId | undefined;\n if (!shapeId) {\n return;\n }\n\n const shape = this.getShapeById(shapeId);\n if (!shape) {\n return;\n }\n\n // Emit shape selected event via bus (include mapId for multi-map isolation)\n shapeBus.emit(ShapeEvents.selected, { shapeId: shape.id, mapId });\n\n // Call callback if provided\n if (onShapeClick) {\n onShapeClick(shape);\n }\n };\n\n /**\n * Handle shape hover\n */\n private handleShapeHover = (info: PickingInfo): void => {\n const { onShapeHover, mapId } = this.props;\n\n // Look up shape from data prop using shapeId stored in feature properties\n const shapeId =\n (info.object?.properties?.shapeId as ShapeId | undefined) ?? null;\n const shape = shapeId ? (this.getShapeById(shapeId) ?? null) : null;\n\n // Dedupe hover events - only emit if hovered shape changed\n if (shapeId !== this.state?.lastHoveredId) {\n this.setState({ lastHoveredId: shapeId });\n\n // Emit shape hovered event via bus (include mapId for multi-map isolation)\n shapeBus.emit(ShapeEvents.hovered, {\n shapeId,\n mapId,\n });\n }\n\n // Always call callback if provided (for local state updates)\n if (onShapeHover) {\n onShapeHover(shape);\n }\n };\n\n /**\n * Render highlight sublayer (underneath main layer)\n * Note: Points with icons use coffin corners instead of highlight layer\n */\n private renderHighlightLayer(\n features: EditableShape['feature'][],\n ): GeoJsonLayer | null {\n const { selectedShapeId, showHighlight, highlightColor } = this.props;\n\n if (!selectedShapeId || showHighlight === false) {\n return null;\n }\n\n const selectedFeature = features.find(\n (f) => f.properties?.shapeId === selectedShapeId,\n );\n\n if (!selectedFeature) {\n return null;\n }\n\n // Skip highlight layer for Point geometries with icons - they use coffin corners instead\n // Points without icons should still show the highlight layer\n if (selectedFeature.geometry.type === 'Point') {\n const hasIcon = !!selectedFeature.properties?.styleProperties?.icon;\n if (hasIcon) {\n return null;\n }\n }\n\n return new GeoJsonLayer({\n id: `${this.props.id}-${SHAPE_LAYER_IDS.DISPLAY_HIGHLIGHT}`,\n // biome-ignore lint/suspicious/noExplicitAny: GeoJsonLayer accepts various feature formats\n data: [selectedFeature] as any,\n\n // Styling\n filled: true,\n stroked: true,\n lineWidthUnits: 'pixels',\n lineWidthMinPixels: MAP_INTERACTION.LINE_WIDTH_MIN_PIXELS,\n getFillColor: () => [0, 0, 0, 0], // Transparent fill\n getLineColor: () => highlightColor || getHighlightColor(),\n getLineWidth: getHighlightLineWidth,\n\n // Behavior\n pickable: false,\n updateTriggers: {\n getLineColor: [highlightColor],\n getLineWidth: [selectedShapeId, features],\n },\n });\n }\n\n /**\n * Render coffin corners layer for Point geometries that have icons on hover/select\n * Coffin corners provide visual feedback for points instead of highlight layer\n */\n private renderCoffinCornersLayer(\n features: EditableShape['feature'][],\n ): IconLayer | null {\n const { selectedShapeId } = this.props;\n\n // Find point features that need coffin corners (hovered or selected)\n const pointFeatures = features.filter((f) => {\n if (f.geometry.type !== 'Point') {\n return false;\n }\n const hasIcon = !!f.properties?.styleProperties?.icon;\n if (!hasIcon) {\n return false;\n }\n\n const shapeId = f.properties?.shapeId;\n const isSelected = shapeId === selectedShapeId;\n const isHovered =\n this.state?.hoverIndex !== undefined &&\n features.indexOf(f) === this.state.hoverIndex;\n\n return isSelected || isHovered;\n });\n\n if (pointFeatures.length === 0) {\n return null;\n }\n\n // Get icon atlas from first point feature (all should share the same atlas)\n const firstPointIcon = pointFeatures[0]?.properties?.styleProperties?.icon;\n const iconAtlas = firstPointIcon?.atlas;\n const iconMapping = firstPointIcon?.mapping;\n\n if (!iconAtlas) {\n return null;\n }\n\n if (!iconMapping) {\n return null;\n }\n\n // Add coffin corners icons to the mapping\n const extendedMapping = {\n ...iconMapping,\n [COFFIN_CORNERS.HOVER_ICON]: {\n x: 0,\n y: 0,\n width: 76,\n height: 76,\n mask: false,\n },\n [COFFIN_CORNERS.SELECTED_ICON]: {\n x: 76,\n y: 0,\n width: 76,\n height: 76,\n mask: false,\n },\n [COFFIN_CORNERS.SELECTED_HOVER_ICON]: {\n x: 152,\n y: 0,\n width: 76,\n height: 76,\n mask: false,\n },\n };\n\n return new IconLayer({\n id: `${this.props.id}-${SHAPE_LAYER_IDS.DISPLAY}-coffin-corners`,\n data: pointFeatures,\n iconAtlas,\n iconMapping: extendedMapping,\n getIcon: (d: EditableShape['feature']) => {\n const shapeId = d.properties?.shapeId;\n const isSelected = shapeId === selectedShapeId;\n const isHovered =\n this.state?.hoverIndex !== undefined &&\n features.indexOf(d) === this.state.hoverIndex;\n\n if (isSelected && isHovered) {\n return COFFIN_CORNERS.SELECTED_HOVER_ICON;\n }\n if (isSelected) {\n return COFFIN_CORNERS.SELECTED_ICON;\n }\n return COFFIN_CORNERS.HOVER_ICON;\n },\n getSize: COFFIN_CORNERS.SIZE,\n getPosition: (d: EditableShape['feature']) => {\n const coords =\n d.geometry.type === 'Point' ? d.geometry.coordinates : [0, 0];\n return coords as [number, number];\n },\n getPixelOffset: (d: EditableShape['feature']) => {\n const iconSize =\n d.properties?.styleProperties?.icon?.size ??\n MAP_INTERACTION.ICON_SIZE;\n // Center the coffin corners on the point icon\n return [-1, -iconSize / 2];\n },\n billboard: false,\n pickable: false,\n updateTriggers: {\n getIcon: [selectedShapeId, this.state?.hoverIndex],\n data: [features, selectedShapeId, this.state?.hoverIndex],\n },\n });\n }\n\n /**\n * Extract icon configuration from features in a single pass.\n * Returns the first icon's atlas and mapping (all shapes share the same atlas).\n * Uses early return for O(1) best case when first feature has icons.\n */\n private getIconConfig(features: EditableShape['feature'][]): {\n hasIcons: boolean;\n atlas?: string;\n mapping?: Record<\n string,\n { x: number; y: number; width: number; height: number; mask?: boolean }\n >;\n } {\n for (const f of features) {\n const icon = f.properties?.styleProperties?.icon;\n if (icon) {\n return {\n hasIcons: true,\n atlas: icon.atlas,\n mapping: icon.mapping,\n };\n }\n }\n return { hasIcons: false };\n }\n\n /**\n * Render main shapes layer\n */\n private renderMainLayer(features: EditableShape['feature'][]): GeoJsonLayer {\n const { pickable, applyBaseOpacity, selectedShapeId } = this.props;\n\n // Single-pass icon config extraction (O(1) best case with early return)\n const {\n hasIcons,\n atlas: iconAtlas,\n mapping: iconMapping,\n } = this.getIconConfig(features);\n\n return new GeoJsonLayer({\n id: `${this.props.id}-${SHAPE_LAYER_IDS.DISPLAY}`,\n // biome-ignore lint/suspicious/noExplicitAny: GeoJsonLayer accepts various feature formats\n data: features as any,\n\n // Styling\n filled: true,\n stroked: true,\n getFillColor: (d: EditableShape['feature']) =>\n getFillColor(d, applyBaseOpacity),\n getLineColor: getStrokeColor,\n getLineWidth: (d, info) => {\n const isHovered = info?.index === this.state?.hoverIndex;\n return getHoverLineWidth(d, isHovered);\n },\n lineWidthUnits: 'pixels',\n lineWidthMinPixels: MAP_INTERACTION.LINE_WIDTH_MIN_PIXELS,\n lineWidthMaxPixels: 20,\n\n // Points - use icons if any feature has icon config, otherwise circles\n pointType: hasIcons ? 'icon' : 'circle',\n getPointRadius: (d) => {\n const iconSize = d.properties?.styleProperties?.icon?.size;\n return iconSize ?? 2;\n },\n pointRadiusUnits: 'pixels',\n\n // Icon configuration (only used if pointType includes 'icon')\n ...(hasIcons && iconAtlas ? { iconAtlas } : {}),\n ...(hasIcons && iconMapping ? { iconMapping } : {}),\n ...(hasIcons\n ? {\n getIcon: (d: EditableShape['feature']) =>\n d.properties?.styleProperties?.icon?.name ?? 'marker',\n getIconSize: (d: EditableShape['feature']) => {\n return (\n d.properties?.styleProperties?.icon?.size ??\n MAP_INTERACTION.ICON_SIZE\n );\n },\n getIconColor: getStrokeColor,\n getIconPixelOffset: (d: EditableShape['feature']) => {\n const iconSize =\n d.properties?.styleProperties?.icon?.size ??\n MAP_INTERACTION.ICON_SIZE;\n return [-1, -iconSize / 2];\n },\n iconBillboard: false,\n }\n : {}),\n\n // Dash pattern support - selected shapes get dotted border\n extensions: [new PathStyleExtension({ dash: true })],\n getDashArray: (d: EditableShape['feature']) => {\n const isSelected = d.properties?.shapeId === selectedShapeId;\n if (isSelected) {\n return DASH_ARRAYS.dotted;\n }\n return getDashArray(d);\n },\n\n // Behavior\n pickable,\n autoHighlight: false, // We handle highlighting manually\n // Note: onClick and onHover are handled via getPickingInfo() override\n\n // Update triggers\n updateTriggers: {\n getFillColor: [features, applyBaseOpacity],\n getLineColor: [features],\n getLineWidth: [features, this.state?.hoverIndex],\n getDashArray: [features, selectedShapeId],\n getPointRadius: [features],\n ...(hasIcons\n ? {\n getIcon: [features],\n getIconSize: [features],\n getIconColor: [features],\n getIconPixelOffset: [features],\n }\n : {}),\n },\n });\n }\n\n /**\n * Render labels layer\n */\n private renderLabelsLayer(): ReturnType<typeof createShapeLabelLayer> | null {\n const { showLabels, data, labelOptions } = this.props;\n\n if (!showLabels) {\n return null;\n }\n\n return createShapeLabelLayer({\n id: `${this.props.id}-${SHAPE_LAYER_IDS.DISPLAY_LABELS}`,\n data,\n labelOptions,\n });\n }\n\n /**\n * Render all sublayers\n */\n renderLayers(): Layer[] {\n // Compute features once per render cycle for performance\n const features = this.getFeaturesWithId();\n\n return [\n this.renderHighlightLayer(features),\n this.renderCoffinCornersLayer(features),\n this.renderMainLayer(features),\n this.renderLabelsLayer(),\n ].filter(Boolean) as Layer[];\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /*
2
+ * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at https://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { EditableShape } from "../shared/types.js";
14
+ import { LabelPositionOptions } from "./utils/labels.js";
15
+ import { TextLayer } from "@deck.gl/layers";
16
+
17
+ //#region src/deckgl/shapes/display-shape-layer/shape-label-layer.d.ts
18
+ /**
19
+ * Props for creating a shape label layer
20
+ */
21
+ interface ShapeLabelLayerProps {
22
+ /** Layer ID (defaults to DISPLAY_LABELS constant) */
23
+ id?: string;
24
+ /** Array of shapes to label */
25
+ data: EditableShape[];
26
+ /**
27
+ * Global label positioning options
28
+ * Per-shape properties in styleProperties take precedence
29
+ */
30
+ labelOptions?: LabelPositionOptions;
31
+ }
32
+ /**
33
+ * Creates a TextLayer for rendering shape labels with intelligent positioning
34
+ *
35
+ * ## Features
36
+ * - **Geometry-aware positioning**: Different defaults for Point, LineString, Polygon, and Circle
37
+ * - **Three-tier priority system**: Per-shape properties > labelOptions > defaults
38
+ * - **Coordinate anchoring**: Position labels at start/middle/end (or edge positions for circles)
39
+ * - **Pixel-based offsets**: Consistent label placement at all zoom levels
40
+ * - **Text-only styling**: White uppercase text with black outline for legibility
41
+ * - **Position caching**: Label positions are computed once per shape using a WeakMap cache
42
+ *
43
+ * ## Label Positioning Priority
44
+ * 1. Per-shape `styleProperties` (highest priority)
45
+ * 2. Global `labelOptions` parameter
46
+ * 3. Geometry-specific defaults (fallback)
47
+ *
48
+ * @param props - Shape label layer configuration
49
+ * @returns Configured TextLayer instance
50
+ *
51
+ * @example
52
+ * ```tsx
53
+ * const labelLayer = createShapeLabelLayer({
54
+ * id: 'my-labels',
55
+ * data: shapes,
56
+ * labelOptions: {
57
+ * circleLabelCoordinateAnchor: 'top',
58
+ * pointLabelOffset: [0, -20],
59
+ * },
60
+ * });
61
+ * ```
62
+ */
63
+ declare function createShapeLabelLayer(props: ShapeLabelLayerProps): TextLayer<EditableShape>;
64
+ //#endregion
65
+ export { ShapeLabelLayerProps, createShapeLabelLayer };
66
+ //# sourceMappingURL=shape-label-layer.d.ts.map
@@ -0,0 +1,116 @@
1
+ /*
2
+ * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at https://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+
14
+ 'use client';
15
+
16
+ import { SHAPE_LAYER_IDS } from "../shared/constants.js";
17
+ import { getLabelPosition2d, getLabelText } from "./utils/labels.js";
18
+ import { TextLayer } from "@deck.gl/layers";
19
+
20
+ //#region src/deckgl/shapes/display-shape-layer/shape-label-layer.ts
21
+ /**
22
+ * Creates a cached label position getter to avoid computing position multiple times per shape.
23
+ * Uses WeakMap so positions are garbage collected when shapes are removed.
24
+ */
25
+ function createCachedPositionGetter(labelOptions) {
26
+ const cache = /* @__PURE__ */ new WeakMap();
27
+ const getNullable = (shape) => {
28
+ if (cache.has(shape)) return cache.get(shape) ?? null;
29
+ const position = getLabelPosition2d(shape, labelOptions);
30
+ cache.set(shape, position);
31
+ return position;
32
+ };
33
+ const getRequired = (shape) => {
34
+ const position = getNullable(shape);
35
+ if (!position) throw new Error("Shape has no valid position - should have been filtered");
36
+ return position;
37
+ };
38
+ return {
39
+ getRequired,
40
+ getNullable
41
+ };
42
+ }
43
+ /**
44
+ * Creates a TextLayer for rendering shape labels with intelligent positioning
45
+ *
46
+ * ## Features
47
+ * - **Geometry-aware positioning**: Different defaults for Point, LineString, Polygon, and Circle
48
+ * - **Three-tier priority system**: Per-shape properties > labelOptions > defaults
49
+ * - **Coordinate anchoring**: Position labels at start/middle/end (or edge positions for circles)
50
+ * - **Pixel-based offsets**: Consistent label placement at all zoom levels
51
+ * - **Text-only styling**: White uppercase text with black outline for legibility
52
+ * - **Position caching**: Label positions are computed once per shape using a WeakMap cache
53
+ *
54
+ * ## Label Positioning Priority
55
+ * 1. Per-shape `styleProperties` (highest priority)
56
+ * 2. Global `labelOptions` parameter
57
+ * 3. Geometry-specific defaults (fallback)
58
+ *
59
+ * @param props - Shape label layer configuration
60
+ * @returns Configured TextLayer instance
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * const labelLayer = createShapeLabelLayer({
65
+ * id: 'my-labels',
66
+ * data: shapes,
67
+ * labelOptions: {
68
+ * circleLabelCoordinateAnchor: 'top',
69
+ * pointLabelOffset: [0, -20],
70
+ * },
71
+ * });
72
+ * ```
73
+ */
74
+ function createShapeLabelLayer(props) {
75
+ const { id = SHAPE_LAYER_IDS.DISPLAY_LABELS, data, labelOptions } = props;
76
+ const { getRequired, getNullable } = createCachedPositionGetter(labelOptions);
77
+ return new TextLayer({
78
+ id,
79
+ data: data.filter((shape) => getNullable(shape) !== null),
80
+ getText: getLabelText,
81
+ getPosition: (d) => getRequired(d).coordinates,
82
+ getPixelOffset: (d) => getRequired(d).pixelOffset,
83
+ getTextAnchor: (d) => getRequired(d).textAnchor,
84
+ getAlignmentBaseline: (d) => getRequired(d).alignmentBaseline,
85
+ getColor: [
86
+ 255,
87
+ 255,
88
+ 255,
89
+ 255
90
+ ],
91
+ getSize: 10,
92
+ getAngle: 0,
93
+ outlineWidth: 2,
94
+ outlineColor: [
95
+ 0,
96
+ 0,
97
+ 0,
98
+ 255
99
+ ],
100
+ background: false,
101
+ fontFamily: "Roboto MonoVariable, monospace",
102
+ fontWeight: "bold",
103
+ fontSettings: { sdf: true },
104
+ updateTriggers: {
105
+ getPosition: [labelOptions],
106
+ getPixelOffset: [labelOptions],
107
+ getTextAnchor: [labelOptions],
108
+ getAlignmentBaseline: [labelOptions]
109
+ },
110
+ pickable: false
111
+ });
112
+ }
113
+
114
+ //#endregion
115
+ export { createShapeLabelLayer };
116
+ //# sourceMappingURL=shape-label-layer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shape-label-layer.js","names":[],"sources":["../../../../src/deckgl/shapes/display-shape-layer/shape-label-layer.ts"],"sourcesContent":["/*\n * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\n'use client';\n\nimport { TextLayer } from '@deck.gl/layers';\nimport { SHAPE_LAYER_IDS } from '../shared/constants';\nimport {\n getLabelPosition2d,\n getLabelText,\n type LabelPosition2d,\n type LabelPositionOptions,\n} from './utils/labels';\nimport type { EditableShape } from '../shared/types';\n\n/**\n * Creates a cached label position getter to avoid computing position multiple times per shape.\n * Uses WeakMap so positions are garbage collected when shapes are removed.\n */\nfunction createCachedPositionGetter(\n labelOptions: LabelPositionOptions | undefined,\n) {\n const cache = new WeakMap<EditableShape, LabelPosition2d | null>();\n\n // Returns nullable position for filtering\n const getNullable = (shape: EditableShape): LabelPosition2d | null => {\n if (cache.has(shape)) {\n return cache.get(shape) ?? null;\n }\n const position = getLabelPosition2d(shape, labelOptions);\n cache.set(shape, position);\n return position;\n };\n\n // Returns position, throwing if null (use only after filtering)\n const getRequired = (shape: EditableShape): LabelPosition2d => {\n const position = getNullable(shape);\n if (!position) {\n throw new Error(\n 'Shape has no valid position - should have been filtered',\n );\n }\n return position;\n };\n\n return { getRequired, getNullable };\n}\n\n/**\n * Props for creating a shape label layer\n */\nexport interface ShapeLabelLayerProps {\n /** Layer ID (defaults to DISPLAY_LABELS constant) */\n id?: string;\n /** Array of shapes to label */\n data: EditableShape[];\n /**\n * Global label positioning options\n * Per-shape properties in styleProperties take precedence\n */\n labelOptions?: LabelPositionOptions;\n}\n\n/**\n * Creates a TextLayer for rendering shape labels with intelligent positioning\n *\n * ## Features\n * - **Geometry-aware positioning**: Different defaults for Point, LineString, Polygon, and Circle\n * - **Three-tier priority system**: Per-shape properties > labelOptions > defaults\n * - **Coordinate anchoring**: Position labels at start/middle/end (or edge positions for circles)\n * - **Pixel-based offsets**: Consistent label placement at all zoom levels\n * - **Text-only styling**: White uppercase text with black outline for legibility\n * - **Position caching**: Label positions are computed once per shape using a WeakMap cache\n *\n * ## Label Positioning Priority\n * 1. Per-shape `styleProperties` (highest priority)\n * 2. Global `labelOptions` parameter\n * 3. Geometry-specific defaults (fallback)\n *\n * @param props - Shape label layer configuration\n * @returns Configured TextLayer instance\n *\n * @example\n * ```tsx\n * const labelLayer = createShapeLabelLayer({\n * id: 'my-labels',\n * data: shapes,\n * labelOptions: {\n * circleLabelCoordinateAnchor: 'top',\n * pointLabelOffset: [0, -20],\n * },\n * });\n * ```\n */\nexport function createShapeLabelLayer(\n props: ShapeLabelLayerProps,\n): TextLayer<EditableShape> {\n const { id = SHAPE_LAYER_IDS.DISPLAY_LABELS, data, labelOptions } = props;\n\n // Create cached position getter to avoid computing position 4x per shape\n const { getRequired, getNullable } = createCachedPositionGetter(labelOptions);\n\n // Filter out shapes with invalid positions (null coordinates)\n const validData = data.filter((shape) => getNullable(shape) !== null);\n\n return new TextLayer<EditableShape>({\n id,\n data: validData,\n\n // Text content - uppercase\n getText: getLabelText,\n\n // Position - use cached getter for all position-related properties\n // getRequired is safe because we filtered out null positions above\n getPosition: (d: EditableShape) => getRequired(d).coordinates,\n getPixelOffset: (d: EditableShape) => getRequired(d).pixelOffset,\n getTextAnchor: (d: EditableShape) => getRequired(d).textAnchor,\n getAlignmentBaseline: (d: EditableShape) =>\n getRequired(d).alignmentBaseline,\n\n // Styling - white text with black outline, no background\n getColor: [255, 255, 255, 255], // White text\n getSize: 10,\n getAngle: 0,\n\n // Text outline for legibility (black stroke around white text)\n outlineWidth: 2,\n outlineColor: [0, 0, 0, 255], // Black outline\n\n // No background or border\n background: false,\n\n // Font\n fontFamily: 'Roboto MonoVariable, monospace',\n fontWeight: 'bold',\n fontSettings: {\n sdf: true,\n },\n\n // Update triggers - tell deck.gl to recalculate when labelOptions change\n updateTriggers: {\n getPosition: [labelOptions],\n getPixelOffset: [labelOptions],\n getTextAnchor: [labelOptions],\n getAlignmentBaseline: [labelOptions],\n },\n\n // Behavior\n pickable: false,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAS,2BACP,cACA;CACA,MAAM,wBAAQ,IAAI,SAAgD;CAGlE,MAAM,eAAe,UAAiD;AACpE,MAAI,MAAM,IAAI,MAAM,CAClB,QAAO,MAAM,IAAI,MAAM,IAAI;EAE7B,MAAM,WAAW,mBAAmB,OAAO,aAAa;AACxD,QAAM,IAAI,OAAO,SAAS;AAC1B,SAAO;;CAIT,MAAM,eAAe,UAA0C;EAC7D,MAAM,WAAW,YAAY,MAAM;AACnC,MAAI,CAAC,SACH,OAAM,IAAI,MACR,0DACD;AAEH,SAAO;;AAGT,QAAO;EAAE;EAAa;EAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDrC,SAAgB,sBACd,OAC0B;CAC1B,MAAM,EAAE,KAAK,gBAAgB,gBAAgB,MAAM,iBAAiB;CAGpE,MAAM,EAAE,aAAa,gBAAgB,2BAA2B,aAAa;AAK7E,QAAO,IAAI,UAAyB;EAClC;EACA,MAJgB,KAAK,QAAQ,UAAU,YAAY,MAAM,KAAK,KAAK;EAOnE,SAAS;EAIT,cAAc,MAAqB,YAAY,EAAE,CAAC;EAClD,iBAAiB,MAAqB,YAAY,EAAE,CAAC;EACrD,gBAAgB,MAAqB,YAAY,EAAE,CAAC;EACpD,uBAAuB,MACrB,YAAY,EAAE,CAAC;EAGjB,UAAU;GAAC;GAAK;GAAK;GAAK;GAAI;EAC9B,SAAS;EACT,UAAU;EAGV,cAAc;EACd,cAAc;GAAC;GAAG;GAAG;GAAG;GAAI;EAG5B,YAAY;EAGZ,YAAY;EACZ,YAAY;EACZ,cAAc,EACZ,KAAK,MACN;EAGD,gBAAgB;GACd,aAAa,CAAC,aAAa;GAC3B,gBAAgB,CAAC,aAAa;GAC9B,eAAe,CAAC,aAAa;GAC7B,sBAAsB,CAAC,aAAa;GACrC;EAGD,UAAU;EACX,CAAC"}