@tileflow/interactions 0.1.0-alpha.6 → 0.1.0-alpha.8
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/README.md +12 -4
- package/REFERENCE.md +185 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +71 -1
- package/dist/maplibre-entry.d.ts +92 -2
- package/dist/maplibre-entry.js +467 -1
- package/dist/{validation-Cy6Tz3lz.d.ts → validation-DBRKrV__.d.ts} +59 -9
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @tileflow/interactions
|
|
2
2
|
|
|
3
|
-
Portable annotations, tooltips, popups, state, and interaction bindings for Tileflow maps.
|
|
3
|
+
Portable annotations, clusters, tooltips, popups, state, and interaction bindings for Tileflow maps.
|
|
4
4
|
|
|
5
5
|
The package has two boundaries:
|
|
6
6
|
|
|
@@ -58,6 +58,17 @@ Every new annotation needs a stable `id`, singular `coordinate`, and non-empty `
|
|
|
58
58
|
Coordinates use `[longitude, latitude]`. Optional `data` must be recursively JSON-safe. Call `validateTileflowAnnotations` at an untrusted
|
|
59
59
|
boundary to receive atomic validation and structured JSON-Pointer diagnostics.
|
|
60
60
|
|
|
61
|
+
## Clusters, heatmaps, and drawing
|
|
62
|
+
|
|
63
|
+
Group up to 1,000 annotations with the `annotationClusters` prop of a framework adapter. Each group
|
|
64
|
+
is a focusable button that zooms in. For larger sets, pass GeoJSON through `dataSources` and
|
|
65
|
+
`dataOverlays`, using `geojsonSource()`, `clusterOverlay()`, and `heatmapOverlay()` from Tileflow
|
|
66
|
+
Core. Polygon drawing is a documented Terra Draw integration.
|
|
67
|
+
|
|
68
|
+
The [interactions reference](https://github.com/tileflow/tileflow-sdk/blob/main/packages/interactions/REFERENCE.md)
|
|
69
|
+
covers each path, events, and what React Native does not support. `REFERENCE.md` is also included in
|
|
70
|
+
the installed package.
|
|
71
|
+
|
|
61
72
|
## Popup state
|
|
62
73
|
|
|
63
74
|
<!-- docs:check -->
|
|
@@ -114,6 +125,3 @@ Direct MapLibre consumers can use `createTileflowMapLibreDomRuntime` for annotat
|
|
|
114
125
|
objects and schedulers are injected at attach time, so both subpath imports remain SSR-safe.
|
|
115
126
|
`createTileflowMapLibreInteractionCoordinator` joins them around one controlled or uncontrolled
|
|
116
127
|
popup state and commits the closing runtime before the opening runtime.
|
|
117
|
-
|
|
118
|
-
See [`docs/contracts/map-interactions.md`](https://github.com/tileflow/tileflow-sdk/blob/main/docs/contracts/map-interactions.md) for lifecycle,
|
|
119
|
-
accessibility, security, capture, and framework parity requirements.
|
package/REFERENCE.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Interactions reference: clusters, heatmaps, and drawing
|
|
2
|
+
|
|
3
|
+
Public contracts for grouping application points and drawing on a mounted map. Option and layer
|
|
4
|
+
names follow the [MapLibre style specification](https://maplibre.org/maplibre-style-spec/) and match
|
|
5
|
+
Mapbox: `cluster`, `clusterRadius`, `clusterMaxZoom`, `clusterMinPoints`, `clusterProperties`,
|
|
6
|
+
`point_count`, `point_count_abbreviated`, and `cluster_id`.
|
|
7
|
+
|
|
8
|
+
## Choose a path
|
|
9
|
+
|
|
10
|
+
| Data | Use | Limit |
|
|
11
|
+
| ---------------------------------------- | ------------------------------------------------------- | ------------------------------ |
|
|
12
|
+
| Up to 1,000 points with popups | `annotations` with `annotationClusters` | 1,000 annotations |
|
|
13
|
+
| Thousands of points, filtered at runtime | `dataSources` with `dataOverlays` | MapLibre's GeoJSON worker |
|
|
14
|
+
| Fixed points or a design in config | `geojsonSource()` and `clusterOverlay()` in `defineMap` | 512,000 bytes inline, or a URL |
|
|
15
|
+
| Density of points | `heatmapOverlay()` over GeoJSON or a vector tileset | Vector tilesets cannot cluster |
|
|
16
|
+
|
|
17
|
+
## Runtime data
|
|
18
|
+
|
|
19
|
+
`dataSources` and `dataOverlays` take the same values as `sources` and `overlays` in `defineMap`, so
|
|
20
|
+
a map on an official style needs no config. Import the helpers from `@tileflow/core/runtime`.
|
|
21
|
+
|
|
22
|
+
<!-- docs:check -->
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import {clusterOverlay, geojsonSource, heatmapOverlay} from '@tileflow/core/runtime';
|
|
26
|
+
|
|
27
|
+
const data = {
|
|
28
|
+
type: 'FeatureCollection',
|
|
29
|
+
features: [
|
|
30
|
+
{
|
|
31
|
+
type: 'Feature',
|
|
32
|
+
id: 'store-42',
|
|
33
|
+
geometry: {type: 'Point', coordinates: [-3.7038, 40.4168]},
|
|
34
|
+
properties: {name: 'Sol', revenue: 1200},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
export const dataSources = {
|
|
40
|
+
stores: geojsonSource({
|
|
41
|
+
cluster: true,
|
|
42
|
+
clusterProperties: {revenue: ['+', ['get', 'revenue']]},
|
|
43
|
+
clusterRadius: 50,
|
|
44
|
+
data,
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const dataOverlays = {
|
|
49
|
+
demand: heatmapOverlay({id: 'demand', placement: 'below-labels', source: 'stores'}),
|
|
50
|
+
stores: clusterOverlay({id: 'stores', placement: 'above-labels', source: 'stores'}),
|
|
51
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Pass both to the map component of `@tileflow/react`, `@tileflow/vue`, or `@tileflow/svelte`:
|
|
55
|
+
|
|
56
|
+
- A key the compiled style already declares is filled with `setData` and keeps its compiled options.
|
|
57
|
+
An overlay layer the style already draws keeps its compiled form.
|
|
58
|
+
- A key the style does not declare is added at the overlay's semantic `placement`. Sources and
|
|
59
|
+
overlays return after a theme change.
|
|
60
|
+
- New `data` replaces the source's contents. Keep the value stable between renders, because each new
|
|
61
|
+
reference is sent to the worker. Changing a cluster option rebuilds the source.
|
|
62
|
+
- Pass plain data. Svelte `$state` proxies cannot reach a worker, so use `$state.raw`.
|
|
63
|
+
- Remove a key to remove what it added.
|
|
64
|
+
|
|
65
|
+
A problem is reported through the interaction diagnostic callback with code `INVALID_DATA_LAYER`
|
|
66
|
+
and a JSON Pointer such as `/dataOverlays/stores/source`.
|
|
67
|
+
|
|
68
|
+
## Cluster overlays
|
|
69
|
+
|
|
70
|
+
`clusterOverlay({id, source, placement})` draws three layers: `<id>-clusters`, `<id>-cluster-count`,
|
|
71
|
+
and `<id>-points`. Options are `colors`, `radii`, `steps`, `pointColor`, `textColor`, `font`, and
|
|
72
|
+
`expandOnClick`. The count uses the map's label font unless `font` names another stack. For anything
|
|
73
|
+
else, write the layers with `maplibreOverlay()` and the same `point_count` expressions.
|
|
74
|
+
|
|
75
|
+
Web adapters find these layers through their metadata, in compiled and runtime styles alike:
|
|
76
|
+
|
|
77
|
+
- Clicking or tapping a bubble eases to `getClusterExpansionZoom()` unless `expandOnClick` is false.
|
|
78
|
+
- The interaction event callback receives `target:activate` with a `cluster` target: `clusterId`,
|
|
79
|
+
`coordinate`, `pointCount`, `properties`, and `source`.
|
|
80
|
+
- A separate point arrives as a `style-feature` target with its `feature.id` and `properties`.
|
|
81
|
+
- A click a cluster takes does not also open a semantic POI below it.
|
|
82
|
+
|
|
83
|
+
Read a cluster's members from the map with `map.getSource(source).getClusterLeaves(clusterId)`.
|
|
84
|
+
Points at one coordinate separate only above `clusterMaxZoom`, so keep that below the map's maximum
|
|
85
|
+
zoom. Tileflow clusters are WebGL features and cannot take keyboard focus: use annotation clusters,
|
|
86
|
+
or a list beside the map, when each point must be reachable by keyboard.
|
|
87
|
+
|
|
88
|
+
## Annotation clusters
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
<Map annotations={stores} annotationClusters={{clusterRadius: 60}} source={source} />
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`annotationClusters` accepts `clusterRadius`, `clusterMaxZoom`, `clusterMinPoints`, `expandOnClick`,
|
|
95
|
+
and `formatAriaLabel(count)`. Each group is a real `button.tileflow-interaction-cluster`, so it takes
|
|
96
|
+
focus and Enter, and the map needs no glyphs or extra source. The default accessible name is
|
|
97
|
+
`12 places`; localize it with `formatAriaLabel`. Set the colour with
|
|
98
|
+
`--tileflow-interaction-cluster-color`, or restyle the class.
|
|
99
|
+
|
|
100
|
+
The `cluster` target also carries `annotationIds`. A marker hidden by a cluster keeps its annotation
|
|
101
|
+
and state. An open popup keeps its marker visible. Focus moves to the map when the button pressed
|
|
102
|
+
is replaced.
|
|
103
|
+
|
|
104
|
+
## Heatmaps
|
|
105
|
+
|
|
106
|
+
`heatmapOverlay({id, source, placement})` accepts `weight`, `radius`, `intensity`, `opacity`,
|
|
107
|
+
`colors`, `maxZoom`, and `sourceLayer`. `weight`, `radius`, `intensity`, and `opacity` also take
|
|
108
|
+
MapLibre expressions. Use `maxZoom` with a cluster overlay to hand over from density to points.
|
|
109
|
+
Put heatmaps at `below-labels` so that labels stay readable.
|
|
110
|
+
|
|
111
|
+
## Polygon drawing
|
|
112
|
+
|
|
113
|
+
Tileflow does not include a drawing tool. Use [Terra Draw](https://terradraw.io) (MIT): vertex
|
|
114
|
+
editing, snapping, touch, and self-intersection rules are a product of their own, and Terra Draw
|
|
115
|
+
keeps them maintained behind a MapLibre adapter. The map component already gives you the map.
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
npm install terra-draw terra-draw-maplibre-gl-adapter
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
import {TerraDraw, TerraDrawPolygonMode, TerraDrawSelectMode} from 'terra-draw';
|
|
123
|
+
import {TerraDrawMapLibreGLAdapter} from 'terra-draw-maplibre-gl-adapter';
|
|
124
|
+
|
|
125
|
+
// A theme change replaces the style, so keep Terra Draw's own sources and layers.
|
|
126
|
+
const mapOptions = {
|
|
127
|
+
transformStyle(previous, next) {
|
|
128
|
+
if (!previous) return next;
|
|
129
|
+
const keep = (id) => id.startsWith('td-');
|
|
130
|
+
return {
|
|
131
|
+
...next,
|
|
132
|
+
layers: [...next.layers, ...previous.layers.filter(({id}) => keep(id))],
|
|
133
|
+
sources: {
|
|
134
|
+
...next.sources,
|
|
135
|
+
...Object.fromEntries(Object.entries(previous.sources).filter(([id]) => keep(id))),
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
<Map
|
|
142
|
+
mapOptions={mapOptions}
|
|
143
|
+
onLoad={(map) => {
|
|
144
|
+
const draw = new TerraDraw({
|
|
145
|
+
adapter: new TerraDrawMapLibreGLAdapter({map}),
|
|
146
|
+
modes: [new TerraDrawPolygonMode(), new TerraDrawSelectMode()],
|
|
147
|
+
});
|
|
148
|
+
draw.start();
|
|
149
|
+
draw.setMode('polygon');
|
|
150
|
+
draw.on('finish', (id) => savePolygon(draw.getSnapshotFeature(id)));
|
|
151
|
+
}}
|
|
152
|
+
source={source}
|
|
153
|
+
/>;
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The result is GeoJSON, which `geojsonSource()` can draw and an API can store. Create Terra Draw once
|
|
157
|
+
per map, in `onLoad` (Vue `@load`, Svelte `on:load`), and call `draw.stop()` before unmounting.
|
|
158
|
+
|
|
159
|
+
## Support by surface
|
|
160
|
+
|
|
161
|
+
| Capability | React, Vue, Svelte | `tileflow preview`, `capture` | React Native |
|
|
162
|
+
| ----------------------------------------- | ------------------ | ----------------------------- | ------------ |
|
|
163
|
+
| Compiled GeoJSON clusters and heatmaps | Yes | Yes | Yes |
|
|
164
|
+
| Click or tap to expand a compiled cluster | Yes | No | Yes |
|
|
165
|
+
| `dataSources`, `dataOverlays` | Yes | No | Yes |
|
|
166
|
+
| `annotationClusters` | Yes | No | Yes |
|
|
167
|
+
| Terra Draw | Yes | No | No |
|
|
168
|
+
|
|
169
|
+
`mode="image"` draws no runtime layers and reports `UNSUPPORTED_MODE`.
|
|
170
|
+
|
|
171
|
+
React Native takes the same three props and reports the same `cluster` and `style-feature` targets
|
|
172
|
+
through `onInteractionEvent`. It differs in four ways:
|
|
173
|
+
|
|
174
|
+
- Data reaches MapLibre React Native's own `GeoJSONSource`, which reuses a compiled source of the
|
|
175
|
+
same ID and returns after a theme change. A reused source keeps its compiled cluster options.
|
|
176
|
+
- A tapped point reports the first point layer of its source as `layerId`.
|
|
177
|
+
- A map with a controlled `view` reports the cluster but does not move the camera, because the
|
|
178
|
+
application owns the view. Use the target's coordinate to set it.
|
|
179
|
+
- Annotation clusters are native buttons with `accessibilityRole="button"`, grouped by the same
|
|
180
|
+
`createTileflowAnnotationClusterIndex` as the web, so both platforms split at the same zooms.
|
|
181
|
+
|
|
182
|
+
Terra Draw is a GL JS library and has no native equivalent here.
|
|
183
|
+
|
|
184
|
+
Direct MapLibre consumers use `createTileflowMapLibreDataRuntime` from
|
|
185
|
+
`@tileflow/interactions/maplibre`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import { T as TileflowInteractionState, a as TileflowInteractionAction, b as TileflowInteractionTargetRef } from './validation-
|
|
2
|
-
export { c as TileflowAnnotation, d as
|
|
1
|
+
import { T as TileflowInteractionState, a as TileflowInteractionAction, b as TileflowInteractionTargetRef } from './validation-DBRKrV__.js';
|
|
2
|
+
export { c as TileflowAnnotation, d as TileflowAnnotationCluster, e as TileflowAnnotationClusterIndex, f as TileflowAnnotationClusterOptions, g as TileflowAnnotationClusterView, h as TileflowAnnotationInteractionEvent, i as TileflowAnnotationMarker, j as TileflowAnnotationSurface, k as TileflowAnnotationValidationResult, l as TileflowAnnotationViewContext, m as TileflowInteractionBinding, n as TileflowInteractionBindingValidationResult, o as TileflowInteractionContent, p as TileflowInteractionCoordinate, q as TileflowInteractionDiagnostic, r as TileflowInteractionDiagnosticCode, s as TileflowInteractionEvent, t as TileflowInteractionEventType, u as TileflowInteractionFieldContent, v as TileflowInteractionInputModality, w as TileflowInteractionJsonPrimitive, x as TileflowInteractionJsonValue, y as TileflowInteractionTarget, z as TileflowInteractionTextContent, A as TileflowInteractionViewContent, B as TileflowInteractionViewContext, C as TileflowPoiCategory, D as TileflowPoiFilterRank, E as TileflowPoiProperties, F as TileflowPoiSizeRank, G as TileflowResolvedAnnotationTarget, H as TileflowResolvedClusterTarget, I as TileflowResolvedInteractionTarget, J as TileflowResolvedMapTarget, K as TileflowResolvedPoiFeature, L as TileflowResolvedPoiFeatureTarget, M as TileflowResolvedSemanticFeature, N as TileflowResolvedSemanticFeatureTarget, O as TileflowResolvedStyleFeatureTarget, P as createTileflowAnnotationClusterIndex, Q as tileflowAnnotationSchema, R as tileflowAnnotationsSchema, S as tileflowInteractionActionSchema, U as tileflowInteractionBindingSchema, V as tileflowInteractionBindingsSchema, W as tileflowInteractionContentSchema, X as tileflowInteractionJsonValueSchema, Y as tileflowInteractionLimits, Z as tileflowInteractionSchemaVersion, _ as tileflowInteractionStateSchema, $ as tileflowInteractionTargetRefSchema, a0 as tileflowInteractionTargetSchema, a1 as tileflowPoiCategories, a2 as validateTileflowAnnotations, a3 as validateTileflowInteractionBindings } from './validation-DBRKrV__.js';
|
|
3
3
|
import 'zod/mini';
|
|
4
4
|
|
|
5
5
|
declare const initialTileflowInteractionState: TileflowInteractionState;
|
|
6
6
|
declare function reduceTileflowInteractionState(state: TileflowInteractionState, action: TileflowInteractionAction): TileflowInteractionState;
|
|
7
7
|
declare function tileflowInteractionTargetRefsEqual(left: TileflowInteractionTargetRef | null, right: TileflowInteractionTargetRef | null): boolean;
|
|
8
8
|
|
|
9
|
+
type Json = Readonly<Record<string, unknown>>;
|
|
10
|
+
/**
|
|
11
|
+
* Gives a `clusterOverlay()` count layer the style's label font. Other layers and counts with their
|
|
12
|
+
* own font return unchanged; `undefined` means the style has no label font to share.
|
|
13
|
+
*/
|
|
14
|
+
declare function withTileflowClusterLabelFont<TLayer extends Json>(layer: TLayer, styleLayers: readonly Json[]): TLayer | undefined;
|
|
15
|
+
|
|
9
16
|
/** Compact machine-readable vocabulary for agents and code generators. */
|
|
10
17
|
declare const tileflowInteractionReference: Readonly<{
|
|
11
18
|
readonly actions: readonly ["open-popup", "close-popup"];
|
|
@@ -50,4 +57,4 @@ declare const tileflowInteractionReference: Readonly<{
|
|
|
50
57
|
readonly version: 1;
|
|
51
58
|
}>;
|
|
52
59
|
|
|
53
|
-
export { TileflowInteractionAction, TileflowInteractionState, TileflowInteractionTargetRef, initialTileflowInteractionState, reduceTileflowInteractionState, tileflowInteractionReference, tileflowInteractionTargetRefsEqual };
|
|
60
|
+
export { TileflowInteractionAction, TileflowInteractionState, TileflowInteractionTargetRef, initialTileflowInteractionState, reduceTileflowInteractionState, tileflowInteractionReference, tileflowInteractionTargetRefsEqual, withTileflowClusterLabelFont };
|
package/dist/index.js
CHANGED
|
@@ -398,6 +398,74 @@ function tileflowInteractionTargetRefsEqual(left, right) {
|
|
|
398
398
|
}
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
+
// src/annotation-clusters.ts
|
|
402
|
+
import Supercluster from "supercluster";
|
|
403
|
+
function createTileflowAnnotationClusterIndex(annotations, options = {}) {
|
|
404
|
+
const index = new Supercluster({
|
|
405
|
+
extent: 512,
|
|
406
|
+
maxZoom: Math.floor(options.clusterMaxZoom ?? 14),
|
|
407
|
+
minPoints: options.clusterMinPoints ?? 2,
|
|
408
|
+
radius: options.clusterRadius ?? 50
|
|
409
|
+
}).load(
|
|
410
|
+
annotations.map(({ coordinate, id }) => ({
|
|
411
|
+
geometry: { coordinates: [coordinate[0], coordinate[1]], type: "Point" },
|
|
412
|
+
properties: { id },
|
|
413
|
+
type: "Feature"
|
|
414
|
+
}))
|
|
415
|
+
);
|
|
416
|
+
const views = /* @__PURE__ */ new Map();
|
|
417
|
+
return Object.freeze({
|
|
418
|
+
at(zoom) {
|
|
419
|
+
const level = Math.max(0, Math.floor(Number.isFinite(zoom) ? zoom : 0));
|
|
420
|
+
const cached = views.get(level);
|
|
421
|
+
if (cached) return cached;
|
|
422
|
+
const hidden = new Set(annotations.map(({ id }) => id));
|
|
423
|
+
const clusters = [];
|
|
424
|
+
for (const feature of index.getClusters([-180, -90, 180, 90], level)) {
|
|
425
|
+
const properties = feature.properties;
|
|
426
|
+
if (!properties.cluster) {
|
|
427
|
+
hidden.delete(properties.id);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const clusterId = properties.cluster_id;
|
|
431
|
+
const [longitude, latitude] = feature.geometry.coordinates;
|
|
432
|
+
clusters.push(
|
|
433
|
+
Object.freeze({
|
|
434
|
+
annotationIds: Object.freeze(
|
|
435
|
+
index.getLeaves(clusterId, Infinity).map((leaf) => leaf.properties.id)
|
|
436
|
+
),
|
|
437
|
+
clusterId,
|
|
438
|
+
coordinate: Object.freeze([longitude, latitude]),
|
|
439
|
+
expansionZoom: index.getClusterExpansionZoom(clusterId),
|
|
440
|
+
label: String(properties.point_count_abbreviated),
|
|
441
|
+
pointCount: properties.point_count
|
|
442
|
+
})
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const view = Object.freeze({ clusters: Object.freeze(clusters), hiddenAnnotationIds: hidden });
|
|
446
|
+
views.set(level, view);
|
|
447
|
+
return view;
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/cluster-label-font.ts
|
|
453
|
+
var clusterMetadataKey = "tileflow:cluster";
|
|
454
|
+
var lineBackgroundStack = / lines-v1-t\d+-l\d+-p\d+$/u;
|
|
455
|
+
function withTileflowClusterLabelFont(layer, styleLayers) {
|
|
456
|
+
const metadata = layer.metadata;
|
|
457
|
+
const layout = layer.layout ?? {};
|
|
458
|
+
if (metadata?.[clusterMetadataKey]?.role !== "count") return layer;
|
|
459
|
+
if (layout["text-font"] !== void 0) return layer;
|
|
460
|
+
for (const candidate of styleLayers) {
|
|
461
|
+
const font = candidate.layout?.["text-font"];
|
|
462
|
+
if (Array.isArray(font) && font.length > 0 && font.every((face) => typeof face === "string" && !lineBackgroundStack.test(face))) {
|
|
463
|
+
return { ...layer, layout: { ...layout, "text-font": font } };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return void 0;
|
|
467
|
+
}
|
|
468
|
+
|
|
401
469
|
// src/reference.ts
|
|
402
470
|
var tileflowInteractionReference = Object.freeze({
|
|
403
471
|
actions: ["open-popup", "close-popup"],
|
|
@@ -617,6 +685,7 @@ function jsonPointer(path) {
|
|
|
617
685
|
return path.map((segment) => String(segment).replaceAll("~", "~0").replaceAll("/", "~1")).map((segment) => `/${segment}`).join("");
|
|
618
686
|
}
|
|
619
687
|
export {
|
|
688
|
+
createTileflowAnnotationClusterIndex,
|
|
620
689
|
initialTileflowInteractionState,
|
|
621
690
|
reduceTileflowInteractionState,
|
|
622
691
|
tileflowAnnotationSchema,
|
|
@@ -635,5 +704,6 @@ export {
|
|
|
635
704
|
tileflowInteractionTargetSchema,
|
|
636
705
|
tileflowPoiCategories,
|
|
637
706
|
validateTileflowAnnotations,
|
|
638
|
-
validateTileflowInteractionBindings
|
|
707
|
+
validateTileflowInteractionBindings,
|
|
708
|
+
withTileflowClusterLabelFont
|
|
639
709
|
};
|
package/dist/maplibre-entry.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { q as TileflowInteractionDiagnostic, T as TileflowInteractionState, c as TileflowAnnotation, h as TileflowAnnotationInteractionEvent, f as TileflowAnnotationClusterOptions, s as TileflowInteractionEvent, m as TileflowInteractionBinding, L as TileflowResolvedPoiFeatureTarget, v as TileflowInteractionInputModality, N as TileflowResolvedSemanticFeatureTarget, o as TileflowInteractionContent } from './validation-DBRKrV__.js';
|
|
2
2
|
import 'zod/mini';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -135,6 +135,8 @@ type TileflowMapLibreDomRuntime<TAnnotation extends TileflowAnnotation = Tileflo
|
|
|
135
135
|
openPopup: (annotationId: string) => boolean;
|
|
136
136
|
reconcile: (annotations: readonly TAnnotation[]) => void;
|
|
137
137
|
setCustomRenderers: (renderers: TileflowMapLibreDomCustomRenderers) => void;
|
|
138
|
+
/** Hide markers that a cluster stands in for. An open popup keeps its own marker visible. */
|
|
139
|
+
setHiddenAnnotations: (annotationIds: ReadonlySet<string>) => void;
|
|
138
140
|
setInteractionState: (state: TileflowInteractionState) => void;
|
|
139
141
|
subscribeEvents: (listener: (event: TileflowAnnotationInteractionEvent<TAnnotation>) => void) => () => void;
|
|
140
142
|
subscribeDiagnostics: (listener: TileflowMapLibreDomDiagnosticListener) => () => void;
|
|
@@ -149,6 +151,94 @@ type TileflowMapLibreDomRuntime<TAnnotation extends TileflowAnnotation = Tileflo
|
|
|
149
151
|
*/
|
|
150
152
|
declare function createTileflowMapLibreDomRuntime<TMap, TMarker extends TileflowMapLibrePositioned<TMap>, TOverlay extends TileflowMapLibrePositioned<TMap>, TAnnotation extends TileflowAnnotation = TileflowAnnotation>(options: TileflowMapLibreDomRuntimeOptions<TMap, TMarker, TOverlay, TAnnotation>): TileflowMapLibreDomRuntime<TAnnotation>;
|
|
151
153
|
|
|
154
|
+
type Json = Readonly<Record<string, unknown>>;
|
|
155
|
+
/** A `geojsonSource()` value from `@tileflow/core`. */
|
|
156
|
+
type TileflowMapLibreDataSource = Json & Readonly<{
|
|
157
|
+
data: unknown;
|
|
158
|
+
kind: 'geojson';
|
|
159
|
+
}>;
|
|
160
|
+
/** A `clusterOverlay()`, `heatmapOverlay()` or `maplibreOverlay()` value from `@tileflow/core`. */
|
|
161
|
+
type TileflowMapLibreDataOverlay = Readonly<{
|
|
162
|
+
kind: 'maplibre-overlay';
|
|
163
|
+
layers: readonly (Json & Readonly<{
|
|
164
|
+
id: string;
|
|
165
|
+
type: string;
|
|
166
|
+
}>)[];
|
|
167
|
+
placement: string;
|
|
168
|
+
source: string;
|
|
169
|
+
}>;
|
|
170
|
+
type TileflowMapLibreDataInput<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = Readonly<{
|
|
171
|
+
annotationClusters?: TileflowAnnotationClusterOptions | undefined;
|
|
172
|
+
annotations?: readonly TAnnotation[] | undefined;
|
|
173
|
+
dataOverlays?: Readonly<Record<string, TileflowMapLibreDataOverlay>> | undefined;
|
|
174
|
+
dataSources?: Readonly<Record<string, TileflowMapLibreDataSource>> | undefined;
|
|
175
|
+
}>;
|
|
176
|
+
type TileflowMapLibreDataFeature = Readonly<{
|
|
177
|
+
geometry?: unknown;
|
|
178
|
+
id?: number | string;
|
|
179
|
+
layer: Readonly<{
|
|
180
|
+
id: string;
|
|
181
|
+
}>;
|
|
182
|
+
properties?: Json | null;
|
|
183
|
+
source?: string;
|
|
184
|
+
}>;
|
|
185
|
+
/** The MapLibre methods the runtime uses; a `maplibregl.Map` satisfies this. */
|
|
186
|
+
type TileflowMapLibreDataMap = Readonly<{
|
|
187
|
+
addLayer: (layer: never, beforeId?: string) => unknown;
|
|
188
|
+
addSource: (id: string, source: never) => unknown;
|
|
189
|
+
easeTo: (options: {
|
|
190
|
+
center: [number, number];
|
|
191
|
+
zoom: number;
|
|
192
|
+
}) => unknown;
|
|
193
|
+
getCanvas: () => Readonly<{
|
|
194
|
+
style: {
|
|
195
|
+
cursor: string;
|
|
196
|
+
};
|
|
197
|
+
focus?: (o?: FocusOptions) => void;
|
|
198
|
+
}>;
|
|
199
|
+
getLayer: (id: string) => Readonly<{
|
|
200
|
+
metadata?: unknown;
|
|
201
|
+
}> | undefined;
|
|
202
|
+
getLayersOrder: () => readonly string[];
|
|
203
|
+
getSource: (id: string) => unknown;
|
|
204
|
+
getStyle: () => unknown;
|
|
205
|
+
getZoom: () => number;
|
|
206
|
+
isStyleLoaded: () => boolean | void;
|
|
207
|
+
off: (event: never, listener: never) => unknown;
|
|
208
|
+
on: (event: never, listener: never) => unknown;
|
|
209
|
+
queryRenderedFeatures: (point: never, options: Readonly<{
|
|
210
|
+
layers: string[];
|
|
211
|
+
}>) => readonly TileflowMapLibreDataFeature[];
|
|
212
|
+
removeLayer: (id: string) => unknown;
|
|
213
|
+
removeSource: (id: string) => unknown;
|
|
214
|
+
}>;
|
|
215
|
+
type TileflowMapLibreDataRuntimeOptions<TMap, TMarker extends TileflowMapLibrePositioned<TMap>> = Readonly<{
|
|
216
|
+
/** Builds the marker that carries one annotation cluster's button. */
|
|
217
|
+
createMarker: (input: Readonly<{
|
|
218
|
+
element: HTMLElement;
|
|
219
|
+
}>) => TMarker;
|
|
220
|
+
document: Pick<Document, 'createElement'>;
|
|
221
|
+
map: TMap & TileflowMapLibreDataMap;
|
|
222
|
+
onDiagnostic?: (diagnostic: TileflowInteractionDiagnostic) => void;
|
|
223
|
+
onInteractionEvent?: (event: TileflowInteractionEvent) => void;
|
|
224
|
+
/** Receives the annotation IDs a cluster stands in for. */
|
|
225
|
+
setHiddenAnnotations?: (annotationIds: ReadonlySet<string>) => void;
|
|
226
|
+
}>;
|
|
227
|
+
type TileflowMapLibreDataRuntime<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = Readonly<{
|
|
228
|
+
dispose: () => void;
|
|
229
|
+
reconcile: (input: TileflowMapLibreDataInput<TAnnotation>) => void;
|
|
230
|
+
}>;
|
|
231
|
+
/**
|
|
232
|
+
* Keeps application GeoJSON, its overlays and clustered annotations on a mounted map.
|
|
233
|
+
*
|
|
234
|
+
* Runtime sources and overlays take the same `geojsonSource()` and overlay values as a map's config.
|
|
235
|
+
* A source the compiled style already declares is filled rather than added, and an overlay layer
|
|
236
|
+
* it already draws keeps its compiled form. Both return after a style change, which removes
|
|
237
|
+
* whatever the new style does not declare. Layers built by `clusterOverlay()` are found through
|
|
238
|
+
* their metadata, so compiled and runtime clusters expand the same way.
|
|
239
|
+
*/
|
|
240
|
+
declare function createTileflowMapLibreDataRuntime<TMap, TMarker extends TileflowMapLibrePositioned<TMap>, TAnnotation extends TileflowAnnotation = TileflowAnnotation>(options: TileflowMapLibreDataRuntimeOptions<TMap, TMarker>): TileflowMapLibreDataRuntime<TAnnotation>;
|
|
241
|
+
|
|
152
242
|
type TileflowMapLibreInteractionParticipantKind = 'annotation' | 'semantic';
|
|
153
243
|
type TileflowMapLibreInteractionParticipant = Readonly<{
|
|
154
244
|
setInteractionState: (state: TileflowInteractionState) => void;
|
|
@@ -287,4 +377,4 @@ type TileflowMapLibreSemanticDomRuntime = Readonly<{
|
|
|
287
377
|
*/
|
|
288
378
|
declare function createTileflowMapLibreSemanticDomRuntime<TMap, TOverlay extends TileflowMapLibrePositioned<TMap>>(options: TileflowMapLibreSemanticDomRuntimeOptions<TMap, TOverlay>): TileflowMapLibreSemanticDomRuntime;
|
|
289
379
|
|
|
290
|
-
export { type TileflowAnnotationRegistry, type TileflowAnnotationRegistryAdapter, type TileflowAnnotationRegistryEntry, type TileflowMapLibreDomCustomRenderers, type TileflowMapLibreDomDiagnosticListener, type TileflowMapLibreDomDocument, type TileflowMapLibreDomInteractionStateListener, type TileflowMapLibreDomMarkerFactoryInput, type TileflowMapLibreDomOverlayFactoryInput, type TileflowMapLibreDomOverlayKind, type TileflowMapLibreDomRenderTarget, type TileflowMapLibreDomRenderTargetKind, type TileflowMapLibreDomRenderTargetListener, type TileflowMapLibreDomRuntime, type TileflowMapLibreDomRuntimeOptions, type TileflowMapLibreDomStateChangeReason, type TileflowMapLibreInteractionCoordinator, type TileflowMapLibreInteractionCoordinatorOptions, type TileflowMapLibreInteractionParticipant, type TileflowMapLibreInteractionParticipantKind, type TileflowMapLibrePoiController, type TileflowMapLibrePoiControllerOptions, type TileflowMapLibrePoiFeature, type TileflowMapLibrePoiMap, type TileflowMapLibrePoiMatch, type TileflowMapLibrePoiPointerEvent, type TileflowMapLibrePositioned, type TileflowMapLibreSemanticDomOverlayFactoryInput, type TileflowMapLibreSemanticDomRenderTarget, type TileflowMapLibreSemanticDomRuntime, type TileflowMapLibreSemanticDomRuntimeOptions, type TileflowOverlayState, type TileflowOverlayStateChangeReason, type TileflowOverlayStateController, type TileflowOverlayStateListener, createTileflowAnnotationRegistry, createTileflowMapLibreDomRuntime, createTileflowMapLibreInteractionCoordinator, createTileflowMapLibrePoiController, createTileflowMapLibreSemanticDomRuntime, createTileflowOverlayStateController };
|
|
380
|
+
export { TileflowAnnotationClusterOptions, type TileflowAnnotationRegistry, type TileflowAnnotationRegistryAdapter, type TileflowAnnotationRegistryEntry, type TileflowMapLibreDataFeature, type TileflowMapLibreDataInput, type TileflowMapLibreDataMap, type TileflowMapLibreDataOverlay, type TileflowMapLibreDataRuntime, type TileflowMapLibreDataRuntimeOptions, type TileflowMapLibreDataSource, type TileflowMapLibreDomCustomRenderers, type TileflowMapLibreDomDiagnosticListener, type TileflowMapLibreDomDocument, type TileflowMapLibreDomInteractionStateListener, type TileflowMapLibreDomMarkerFactoryInput, type TileflowMapLibreDomOverlayFactoryInput, type TileflowMapLibreDomOverlayKind, type TileflowMapLibreDomRenderTarget, type TileflowMapLibreDomRenderTargetKind, type TileflowMapLibreDomRenderTargetListener, type TileflowMapLibreDomRuntime, type TileflowMapLibreDomRuntimeOptions, type TileflowMapLibreDomStateChangeReason, type TileflowMapLibreInteractionCoordinator, type TileflowMapLibreInteractionCoordinatorOptions, type TileflowMapLibreInteractionParticipant, type TileflowMapLibreInteractionParticipantKind, type TileflowMapLibrePoiController, type TileflowMapLibrePoiControllerOptions, type TileflowMapLibrePoiFeature, type TileflowMapLibrePoiMap, type TileflowMapLibrePoiMatch, type TileflowMapLibrePoiPointerEvent, type TileflowMapLibrePositioned, type TileflowMapLibreSemanticDomOverlayFactoryInput, type TileflowMapLibreSemanticDomRenderTarget, type TileflowMapLibreSemanticDomRuntime, type TileflowMapLibreSemanticDomRuntimeOptions, type TileflowOverlayState, type TileflowOverlayStateChangeReason, type TileflowOverlayStateController, type TileflowOverlayStateListener, createTileflowAnnotationRegistry, createTileflowMapLibreDataRuntime, createTileflowMapLibreDomRuntime, createTileflowMapLibreInteractionCoordinator, createTileflowMapLibrePoiController, createTileflowMapLibreSemanticDomRuntime, createTileflowOverlayStateController };
|
package/dist/maplibre-entry.js
CHANGED
|
@@ -330,6 +330,12 @@ function createTileflowMapLibreDomRuntime(options) {
|
|
|
330
330
|
let disposeStarted = false;
|
|
331
331
|
let disposeComplete = false;
|
|
332
332
|
let overlaySequence = 0;
|
|
333
|
+
let hiddenAnnotations = /* @__PURE__ */ new Set();
|
|
334
|
+
const applyAnnotationVisibility = (instance, annotationId) => {
|
|
335
|
+
const hidden = hiddenAnnotations.has(annotationId) && activePopup?.annotation.id !== annotationId;
|
|
336
|
+
if (hidden) closeTooltipVisual(annotationId);
|
|
337
|
+
instance.element.style.display = hidden ? "none" : "";
|
|
338
|
+
};
|
|
333
339
|
const emit = (type, annotation, inputModality2) => {
|
|
334
340
|
const event = Object.freeze({
|
|
335
341
|
coordinate: annotation.coordinate,
|
|
@@ -388,6 +394,7 @@ function createTileflowMapLibreDomRuntime(options) {
|
|
|
388
394
|
}
|
|
389
395
|
marker?.element.setAttribute("aria-expanded", "false");
|
|
390
396
|
marker?.element.removeAttribute("aria-controls");
|
|
397
|
+
if (marker) applyAnnotationVisibility(marker, current.annotation.id);
|
|
391
398
|
const errors = safelyRunAll([() => current.overlay.remove(), () => current.shell.remove()]);
|
|
392
399
|
publishRenderTargets();
|
|
393
400
|
emit("popup:close", current.annotation, inputModality2);
|
|
@@ -537,6 +544,7 @@ function createTileflowMapLibreDomRuntime(options) {
|
|
|
537
544
|
clearOverlayFailure(entry.definition.id, "popup");
|
|
538
545
|
entry.instance.element.setAttribute("aria-controls", shell.id);
|
|
539
546
|
entry.instance.element.setAttribute("aria-expanded", "true");
|
|
547
|
+
applyAnnotationVisibility(entry.instance, annotationId);
|
|
540
548
|
publishRenderTargets();
|
|
541
549
|
emit("popup:open", entry.definition, inputModality2);
|
|
542
550
|
closeButton.focus({ preventScroll: true });
|
|
@@ -766,6 +774,7 @@ function createTileflowMapLibreDomRuntime(options) {
|
|
|
766
774
|
container.className = "tileflow-interaction-marker-content";
|
|
767
775
|
element.append(container);
|
|
768
776
|
configureAnnotationElement({ annotation, container, element }, annotation);
|
|
777
|
+
if (hiddenAnnotations.has(annotation.id)) element.style.display = "none";
|
|
769
778
|
const marker = options.createMarker({ annotation, element });
|
|
770
779
|
const instance = {
|
|
771
780
|
annotation,
|
|
@@ -1021,6 +1030,11 @@ function createTileflowMapLibreDomRuntime(options) {
|
|
|
1021
1030
|
publishRenderTargets();
|
|
1022
1031
|
}
|
|
1023
1032
|
},
|
|
1033
|
+
setHiddenAnnotations(annotationIds) {
|
|
1034
|
+
assertRuntimeActive(disposeStarted);
|
|
1035
|
+
hiddenAnnotations = annotationIds;
|
|
1036
|
+
for (const entry of registry.entries()) applyAnnotationVisibility(entry.instance, entry.key);
|
|
1037
|
+
},
|
|
1024
1038
|
setInteractionState(state) {
|
|
1025
1039
|
assertRuntimeActive(disposeStarted);
|
|
1026
1040
|
const popupId = getAnnotationPopupId(state);
|
|
@@ -1180,6 +1194,455 @@ function throwCollectedErrors(errors, message) {
|
|
|
1180
1194
|
throw new AggregateError(errors, message);
|
|
1181
1195
|
}
|
|
1182
1196
|
|
|
1197
|
+
// src/annotation-clusters.ts
|
|
1198
|
+
import Supercluster from "supercluster";
|
|
1199
|
+
function createTileflowAnnotationClusterIndex(annotations, options = {}) {
|
|
1200
|
+
const index = new Supercluster({
|
|
1201
|
+
extent: 512,
|
|
1202
|
+
maxZoom: Math.floor(options.clusterMaxZoom ?? 14),
|
|
1203
|
+
minPoints: options.clusterMinPoints ?? 2,
|
|
1204
|
+
radius: options.clusterRadius ?? 50
|
|
1205
|
+
}).load(
|
|
1206
|
+
annotations.map(({ coordinate, id }) => ({
|
|
1207
|
+
geometry: { coordinates: [coordinate[0], coordinate[1]], type: "Point" },
|
|
1208
|
+
properties: { id },
|
|
1209
|
+
type: "Feature"
|
|
1210
|
+
}))
|
|
1211
|
+
);
|
|
1212
|
+
const views = /* @__PURE__ */ new Map();
|
|
1213
|
+
return Object.freeze({
|
|
1214
|
+
at(zoom) {
|
|
1215
|
+
const level = Math.max(0, Math.floor(Number.isFinite(zoom) ? zoom : 0));
|
|
1216
|
+
const cached = views.get(level);
|
|
1217
|
+
if (cached) return cached;
|
|
1218
|
+
const hidden = new Set(annotations.map(({ id }) => id));
|
|
1219
|
+
const clusters = [];
|
|
1220
|
+
for (const feature of index.getClusters([-180, -90, 180, 90], level)) {
|
|
1221
|
+
const properties = feature.properties;
|
|
1222
|
+
if (!properties.cluster) {
|
|
1223
|
+
hidden.delete(properties.id);
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
const clusterId = properties.cluster_id;
|
|
1227
|
+
const [longitude, latitude] = feature.geometry.coordinates;
|
|
1228
|
+
clusters.push(
|
|
1229
|
+
Object.freeze({
|
|
1230
|
+
annotationIds: Object.freeze(
|
|
1231
|
+
index.getLeaves(clusterId, Infinity).map((leaf) => leaf.properties.id)
|
|
1232
|
+
),
|
|
1233
|
+
clusterId,
|
|
1234
|
+
coordinate: Object.freeze([longitude, latitude]),
|
|
1235
|
+
expansionZoom: index.getClusterExpansionZoom(clusterId),
|
|
1236
|
+
label: String(properties.point_count_abbreviated),
|
|
1237
|
+
pointCount: properties.point_count
|
|
1238
|
+
})
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
const view = Object.freeze({ clusters: Object.freeze(clusters), hiddenAnnotationIds: hidden });
|
|
1242
|
+
views.set(level, view);
|
|
1243
|
+
return view;
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// src/cluster-label-font.ts
|
|
1249
|
+
var clusterMetadataKey = "tileflow:cluster";
|
|
1250
|
+
var lineBackgroundStack = / lines-v1-t\d+-l\d+-p\d+$/u;
|
|
1251
|
+
function withTileflowClusterLabelFont(layer, styleLayers) {
|
|
1252
|
+
const metadata = layer.metadata;
|
|
1253
|
+
const layout = layer.layout ?? {};
|
|
1254
|
+
if (metadata?.[clusterMetadataKey]?.role !== "count") return layer;
|
|
1255
|
+
if (layout["text-font"] !== void 0) return layer;
|
|
1256
|
+
for (const candidate of styleLayers) {
|
|
1257
|
+
const font = candidate.layout?.["text-font"];
|
|
1258
|
+
if (Array.isArray(font) && font.length > 0 && font.every((face) => typeof face === "string" && !lineBackgroundStack.test(face))) {
|
|
1259
|
+
return { ...layer, layout: { ...layout, "text-font": font } };
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return void 0;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// src/maplibre-data.ts
|
|
1266
|
+
var clusterMetadataKey2 = "tileflow:cluster";
|
|
1267
|
+
var overlayMetadataKey = "tileflow:overlay";
|
|
1268
|
+
var placementMetadataKey = "tileflow:overlay-placement-manifest";
|
|
1269
|
+
function createTileflowMapLibreDataRuntime(options) {
|
|
1270
|
+
const map = options.map;
|
|
1271
|
+
const overlays = /* @__PURE__ */ new Map();
|
|
1272
|
+
const sources = /* @__PURE__ */ new Map();
|
|
1273
|
+
const clusterMarkers = /* @__PURE__ */ new Map();
|
|
1274
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1275
|
+
let input = {};
|
|
1276
|
+
let annotationClusterKey = "";
|
|
1277
|
+
let index;
|
|
1278
|
+
let renderedZoom;
|
|
1279
|
+
let clusterLayers = [];
|
|
1280
|
+
let pointLayers = [];
|
|
1281
|
+
let hovered = [];
|
|
1282
|
+
let cursorOwned = false;
|
|
1283
|
+
let applying = false;
|
|
1284
|
+
let disposed = false;
|
|
1285
|
+
const on = (event, listener) => {
|
|
1286
|
+
map.on(event, listener);
|
|
1287
|
+
return () => map.off(event, listener);
|
|
1288
|
+
};
|
|
1289
|
+
const onLayers = (event, layers, listener) => {
|
|
1290
|
+
map.on(
|
|
1291
|
+
event,
|
|
1292
|
+
[...layers],
|
|
1293
|
+
listener
|
|
1294
|
+
);
|
|
1295
|
+
return () => map.off(
|
|
1296
|
+
event,
|
|
1297
|
+
[...layers],
|
|
1298
|
+
listener
|
|
1299
|
+
);
|
|
1300
|
+
};
|
|
1301
|
+
const report = (message, path) => {
|
|
1302
|
+
if (reported.has(message)) return;
|
|
1303
|
+
reported.add(message);
|
|
1304
|
+
options.onDiagnostic?.({ code: "INVALID_DATA_LAYER", level: "error", message, path });
|
|
1305
|
+
};
|
|
1306
|
+
const removeOverlay = (id) => {
|
|
1307
|
+
for (const layer of overlays.get(id)?.layers ?? []) {
|
|
1308
|
+
if (map.getLayer(layer)) map.removeLayer(layer);
|
|
1309
|
+
}
|
|
1310
|
+
overlays.delete(id);
|
|
1311
|
+
};
|
|
1312
|
+
const removeSource = (id) => {
|
|
1313
|
+
for (const [overlayId, overlay] of [...overlays]) {
|
|
1314
|
+
if (overlay.source === id) removeOverlay(overlayId);
|
|
1315
|
+
}
|
|
1316
|
+
if (map.getSource(id)) map.removeSource(id);
|
|
1317
|
+
sources.delete(id);
|
|
1318
|
+
};
|
|
1319
|
+
const syncSources = () => {
|
|
1320
|
+
for (const id of sources.keys()) {
|
|
1321
|
+
if (!input.dataSources?.[id]) removeSource(id);
|
|
1322
|
+
}
|
|
1323
|
+
for (const [id, source] of Object.entries(input.dataSources ?? {}).sort(compareEntries)) {
|
|
1324
|
+
const { data, kind: _kind, ...definition } = source;
|
|
1325
|
+
const signature = JSON.stringify(definition);
|
|
1326
|
+
const owned = sources.get(id);
|
|
1327
|
+
if (owned && owned.signature !== signature) removeSource(id);
|
|
1328
|
+
const current = map.getSource(id);
|
|
1329
|
+
if (!current) {
|
|
1330
|
+
map.addSource(id, { ...definition, data, type: "geojson" });
|
|
1331
|
+
sources.set(id, { signature });
|
|
1332
|
+
} else if (typeof current.setData !== "function") {
|
|
1333
|
+
report(`Source "${id}" already exists and is not GeoJSON.`, `/dataSources/${id}`);
|
|
1334
|
+
} else if (current.serialize?.().data !== data) {
|
|
1335
|
+
current.setData(data);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
const syncOverlays = () => {
|
|
1340
|
+
for (const id of overlays.keys()) {
|
|
1341
|
+
if (!input.dataOverlays?.[id]) removeOverlay(id);
|
|
1342
|
+
}
|
|
1343
|
+
const style = map.getStyle();
|
|
1344
|
+
const anchors = style?.metadata?.[placementMetadataKey]?.anchors;
|
|
1345
|
+
for (const [id, overlay] of Object.entries(input.dataOverlays ?? {}).sort(compareEntries)) {
|
|
1346
|
+
const path = `/dataOverlays/${id}`;
|
|
1347
|
+
if (overlay.kind !== "maplibre-overlay") {
|
|
1348
|
+
report(`Overlay "${id}" is not a Tileflow overlay.`, path);
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
if (!map.getSource(overlay.source)) {
|
|
1352
|
+
report(`Overlay "${id}" references unknown source "${overlay.source}".`, `${path}/source`);
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
const signature = JSON.stringify(overlay);
|
|
1356
|
+
if (overlays.get(id)?.signature !== signature) removeOverlay(id);
|
|
1357
|
+
const anchor = anchors?.[overlay.placement];
|
|
1358
|
+
const beforeId = typeof anchor === "string" && map.getLayer(anchor) ? anchor : void 0;
|
|
1359
|
+
const owned = overlays.get(id)?.layers ?? [];
|
|
1360
|
+
for (const layer of overlay.layers) {
|
|
1361
|
+
if (map.getLayer(layer.id)) continue;
|
|
1362
|
+
const prepared = withTileflowClusterLabelFont(layer, style?.layers ?? []);
|
|
1363
|
+
if (!prepared) {
|
|
1364
|
+
report(`Layer "${layer.id}" needs font: the style has no label font.`, path);
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
const metadata = { ...layer.metadata, [overlayMetadataKey]: id };
|
|
1368
|
+
map.addLayer({ ...prepared, metadata, source: overlay.source }, beforeId);
|
|
1369
|
+
if (!owned.includes(layer.id)) owned.push(layer.id);
|
|
1370
|
+
}
|
|
1371
|
+
overlays.set(id, { layers: owned, signature, source: overlay.source });
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
const syncClusterLayers = () => {
|
|
1375
|
+
const clusters = [];
|
|
1376
|
+
const points = [];
|
|
1377
|
+
for (const id of map.getLayersOrder()) {
|
|
1378
|
+
const metadata = map.getLayer(id)?.metadata;
|
|
1379
|
+
const cluster = metadata?.[clusterMetadataKey2];
|
|
1380
|
+
if (cluster?.role === "cluster") clusters.push(id);
|
|
1381
|
+
if (cluster?.role === "point") points.push(id);
|
|
1382
|
+
}
|
|
1383
|
+
clusterLayers = clusters;
|
|
1384
|
+
pointLayers = points;
|
|
1385
|
+
const interactive = [...clusters, ...points];
|
|
1386
|
+
if (interactive.join("\0") === hovered.join("\0")) return;
|
|
1387
|
+
stopHover();
|
|
1388
|
+
hovered = interactive;
|
|
1389
|
+
if (interactive.length === 0) return;
|
|
1390
|
+
const enter = () => {
|
|
1391
|
+
cursorOwned = true;
|
|
1392
|
+
map.getCanvas().style.cursor = "pointer";
|
|
1393
|
+
};
|
|
1394
|
+
const leave = () => {
|
|
1395
|
+
if (cursorOwned) map.getCanvas().style.cursor = "";
|
|
1396
|
+
cursorOwned = false;
|
|
1397
|
+
};
|
|
1398
|
+
const stops2 = [
|
|
1399
|
+
onLayers("mouseenter", interactive, enter),
|
|
1400
|
+
onLayers("mouseleave", interactive, leave)
|
|
1401
|
+
];
|
|
1402
|
+
stopHover = () => {
|
|
1403
|
+
for (const stop of stops2) stop();
|
|
1404
|
+
leave();
|
|
1405
|
+
stopHover = () => void 0;
|
|
1406
|
+
};
|
|
1407
|
+
};
|
|
1408
|
+
let stopHover = () => void 0;
|
|
1409
|
+
const syncStyle = () => {
|
|
1410
|
+
if (applying || disposed) return;
|
|
1411
|
+
applying = true;
|
|
1412
|
+
try {
|
|
1413
|
+
syncSources();
|
|
1414
|
+
syncOverlays();
|
|
1415
|
+
syncClusterLayers();
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
if (map.isStyleLoaded() === true) {
|
|
1418
|
+
report(error instanceof Error ? error.message : "Tileflow data layers failed.", "");
|
|
1419
|
+
}
|
|
1420
|
+
} finally {
|
|
1421
|
+
applying = false;
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
const activateCluster = (target, modality) => options.onInteractionEvent?.({
|
|
1425
|
+
coordinate: target.coordinate,
|
|
1426
|
+
inputModality: modality,
|
|
1427
|
+
target,
|
|
1428
|
+
type: "target:activate"
|
|
1429
|
+
});
|
|
1430
|
+
const expand = (center, zoom) => map.easeTo({ center: [center[0], center[1]], zoom });
|
|
1431
|
+
const handleClick = (event) => {
|
|
1432
|
+
const layers = [...clusterLayers, ...pointLayers];
|
|
1433
|
+
if (disposed || layers.length === 0 || !event.point) return;
|
|
1434
|
+
const feature = map.queryRenderedFeatures(event.point, { layers })[0];
|
|
1435
|
+
if (!feature) return;
|
|
1436
|
+
event.preventDefault?.();
|
|
1437
|
+
const properties = jsonProperties(feature.properties);
|
|
1438
|
+
const modality = pointerModality(event);
|
|
1439
|
+
const coordinate = pointCoordinate(feature.geometry) ?? eventCoordinate(event);
|
|
1440
|
+
if (!coordinate) return;
|
|
1441
|
+
if (!clusterLayers.includes(feature.layer.id)) {
|
|
1442
|
+
options.onInteractionEvent?.({
|
|
1443
|
+
coordinate,
|
|
1444
|
+
inputModality: modality,
|
|
1445
|
+
target: {
|
|
1446
|
+
coordinate,
|
|
1447
|
+
feature: { ...feature.id === void 0 ? {} : { id: feature.id }, properties },
|
|
1448
|
+
kind: "style-feature",
|
|
1449
|
+
layerId: feature.layer.id
|
|
1450
|
+
},
|
|
1451
|
+
type: "target:activate"
|
|
1452
|
+
});
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
const clusterId = properties.cluster_id;
|
|
1456
|
+
if (typeof clusterId !== "number" || !feature.source) return;
|
|
1457
|
+
activateCluster(
|
|
1458
|
+
{
|
|
1459
|
+
clusterId,
|
|
1460
|
+
coordinate,
|
|
1461
|
+
kind: "cluster",
|
|
1462
|
+
pointCount: typeof properties.point_count === "number" ? properties.point_count : 0,
|
|
1463
|
+
properties,
|
|
1464
|
+
source: feature.source
|
|
1465
|
+
},
|
|
1466
|
+
modality
|
|
1467
|
+
);
|
|
1468
|
+
const metadata = map.getLayer(feature.layer.id)?.metadata;
|
|
1469
|
+
if (metadata?.[clusterMetadataKey2]?.expandOnClick === false) return;
|
|
1470
|
+
const source = map.getSource(feature.source);
|
|
1471
|
+
void source?.getClusterExpansionZoom?.(clusterId).then(
|
|
1472
|
+
(zoom) => {
|
|
1473
|
+
if (!disposed) expand(coordinate, zoom);
|
|
1474
|
+
},
|
|
1475
|
+
() => void 0
|
|
1476
|
+
);
|
|
1477
|
+
};
|
|
1478
|
+
const isFocused = ({ element }) => element.ownerDocument?.activeElement === element;
|
|
1479
|
+
const hasFocusedClusterMarker = () => [...clusterMarkers.values()].some(isFocused);
|
|
1480
|
+
const removeClusterMarker = (id, entry) => {
|
|
1481
|
+
entry.element.removeEventListener("click", entry.listener);
|
|
1482
|
+
entry.marker.remove();
|
|
1483
|
+
entry.element.remove();
|
|
1484
|
+
clusterMarkers.delete(id);
|
|
1485
|
+
};
|
|
1486
|
+
const clusterLabel = (count) => input.annotationClusters?.formatAriaLabel?.(count) ?? `${count} places`;
|
|
1487
|
+
const createClusterMarker = (cluster) => {
|
|
1488
|
+
const element = options.document.createElement("button");
|
|
1489
|
+
element.type = "button";
|
|
1490
|
+
element.className = "tileflow-interaction-cluster";
|
|
1491
|
+
element.textContent = cluster.label;
|
|
1492
|
+
element.setAttribute("aria-label", clusterLabel(cluster.pointCount));
|
|
1493
|
+
element.setAttribute("data-tileflow-interaction", "cluster");
|
|
1494
|
+
configureClusterStyles(element);
|
|
1495
|
+
const listener = (event) => {
|
|
1496
|
+
activateCluster(
|
|
1497
|
+
{
|
|
1498
|
+
annotationIds: cluster.annotationIds,
|
|
1499
|
+
clusterId: cluster.clusterId,
|
|
1500
|
+
coordinate: cluster.coordinate,
|
|
1501
|
+
kind: "cluster",
|
|
1502
|
+
pointCount: cluster.pointCount,
|
|
1503
|
+
properties: { point_count: cluster.pointCount }
|
|
1504
|
+
},
|
|
1505
|
+
// Browsers report a click from Enter or Space with a detail of zero.
|
|
1506
|
+
event.detail === 0 ? "keyboard" : "pointer"
|
|
1507
|
+
);
|
|
1508
|
+
if (input.annotationClusters?.expandOnClick !== false) {
|
|
1509
|
+
expand(cluster.coordinate, cluster.expansionZoom);
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
element.addEventListener("click", listener);
|
|
1513
|
+
const marker = options.createMarker({ element });
|
|
1514
|
+
marker.setLngLat([cluster.coordinate[0], cluster.coordinate[1]]);
|
|
1515
|
+
marker.addTo(map);
|
|
1516
|
+
clusterMarkers.set(cluster.clusterId, {
|
|
1517
|
+
element,
|
|
1518
|
+
listener,
|
|
1519
|
+
marker,
|
|
1520
|
+
pointCount: cluster.pointCount
|
|
1521
|
+
});
|
|
1522
|
+
};
|
|
1523
|
+
const renderAnnotationClusters = () => {
|
|
1524
|
+
if (disposed) return;
|
|
1525
|
+
if (!index) {
|
|
1526
|
+
for (const [id, entry] of [...clusterMarkers]) removeClusterMarker(id, entry);
|
|
1527
|
+
options.setHiddenAnnotations?.(/* @__PURE__ */ new Set());
|
|
1528
|
+
renderedZoom = void 0;
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
const zoom = Math.floor(map.getZoom());
|
|
1532
|
+
renderedZoom = zoom;
|
|
1533
|
+
const view = index.at(zoom);
|
|
1534
|
+
const clusters = new Map(view.clusters.map((cluster) => [cluster.clusterId, cluster]));
|
|
1535
|
+
let focusLost = false;
|
|
1536
|
+
for (const [id, entry] of [...clusterMarkers]) {
|
|
1537
|
+
if (clusters.has(id)) continue;
|
|
1538
|
+
focusLost ||= isFocused(entry);
|
|
1539
|
+
removeClusterMarker(id, entry);
|
|
1540
|
+
}
|
|
1541
|
+
for (const [id, cluster] of clusters) {
|
|
1542
|
+
if (!clusterMarkers.has(id)) createClusterMarker(cluster);
|
|
1543
|
+
}
|
|
1544
|
+
options.setHiddenAnnotations?.(view.hiddenAnnotationIds);
|
|
1545
|
+
if (focusLost) map.getCanvas().focus?.({ preventScroll: true });
|
|
1546
|
+
};
|
|
1547
|
+
const handleZoom = () => {
|
|
1548
|
+
if (index && Math.floor(map.getZoom()) !== renderedZoom) renderAnnotationClusters();
|
|
1549
|
+
};
|
|
1550
|
+
const syncAnnotationClusters = () => {
|
|
1551
|
+
const settings = input.annotationClusters;
|
|
1552
|
+
let focusLost = false;
|
|
1553
|
+
if (!settings) {
|
|
1554
|
+
index = void 0;
|
|
1555
|
+
} else {
|
|
1556
|
+
index = createTileflowAnnotationClusterIndex(input.annotations ?? [], settings);
|
|
1557
|
+
focusLost = hasFocusedClusterMarker();
|
|
1558
|
+
for (const [id, entry] of [...clusterMarkers]) removeClusterMarker(id, entry);
|
|
1559
|
+
}
|
|
1560
|
+
renderAnnotationClusters();
|
|
1561
|
+
if (focusLost) map.getCanvas().focus?.({ preventScroll: true });
|
|
1562
|
+
};
|
|
1563
|
+
const refreshClusterLabels = () => {
|
|
1564
|
+
for (const { element, pointCount } of clusterMarkers.values()) {
|
|
1565
|
+
element.setAttribute("aria-label", clusterLabel(pointCount));
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
const stops = [
|
|
1569
|
+
on("style.load", syncStyle),
|
|
1570
|
+
on("styledata", syncStyle),
|
|
1571
|
+
on("click", handleClick),
|
|
1572
|
+
on("zoom", handleZoom)
|
|
1573
|
+
];
|
|
1574
|
+
return Object.freeze({
|
|
1575
|
+
dispose() {
|
|
1576
|
+
if (disposed) return;
|
|
1577
|
+
disposed = true;
|
|
1578
|
+
for (const stop of stops) stop();
|
|
1579
|
+
stopHover();
|
|
1580
|
+
for (const [id, entry] of [...clusterMarkers]) removeClusterMarker(id, entry);
|
|
1581
|
+
try {
|
|
1582
|
+
if (index) options.setHiddenAnnotations?.(/* @__PURE__ */ new Set());
|
|
1583
|
+
} catch {
|
|
1584
|
+
}
|
|
1585
|
+
try {
|
|
1586
|
+
for (const id of [...overlays.keys()]) removeOverlay(id);
|
|
1587
|
+
for (const id of [...sources.keys()]) removeSource(id);
|
|
1588
|
+
} catch {
|
|
1589
|
+
}
|
|
1590
|
+
},
|
|
1591
|
+
reconcile(next) {
|
|
1592
|
+
if (disposed) return;
|
|
1593
|
+
const key = clusterInputKey(next);
|
|
1594
|
+
const changed = key !== annotationClusterKey;
|
|
1595
|
+
annotationClusterKey = key;
|
|
1596
|
+
input = next;
|
|
1597
|
+
reported.clear();
|
|
1598
|
+
syncStyle();
|
|
1599
|
+
if (changed) syncAnnotationClusters();
|
|
1600
|
+
else refreshClusterLabels();
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
function clusterInputKey(input) {
|
|
1605
|
+
if (!input.annotationClusters) return "";
|
|
1606
|
+
const positions = (input.annotations ?? []).map(({ coordinate, id }) => [id, ...coordinate]);
|
|
1607
|
+
return JSON.stringify([input.annotationClusters, positions]);
|
|
1608
|
+
}
|
|
1609
|
+
function compareEntries([left], [right]) {
|
|
1610
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1611
|
+
}
|
|
1612
|
+
function pointerModality(event) {
|
|
1613
|
+
const original = event.originalEvent;
|
|
1614
|
+
return original?.pointerType === "touch" || original?.touches !== void 0 ? "touch" : "pointer";
|
|
1615
|
+
}
|
|
1616
|
+
function eventCoordinate(event) {
|
|
1617
|
+
return event.lngLat ? [event.lngLat.lng, event.lngLat.lat] : void 0;
|
|
1618
|
+
}
|
|
1619
|
+
function pointCoordinate(geometry) {
|
|
1620
|
+
const value = geometry;
|
|
1621
|
+
const coordinates = value?.coordinates;
|
|
1622
|
+
return value?.type === "Point" && Array.isArray(coordinates) && typeof coordinates[0] === "number" && typeof coordinates[1] === "number" ? [coordinates[0], coordinates[1]] : void 0;
|
|
1623
|
+
}
|
|
1624
|
+
function jsonProperties(properties) {
|
|
1625
|
+
return Object.fromEntries(
|
|
1626
|
+
Object.entries(properties ?? {}).filter(([, value]) => value !== void 0)
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
function configureClusterStyles(element) {
|
|
1630
|
+
element.style.alignItems = "center";
|
|
1631
|
+
element.style.background = "var(--tileflow-interaction-cluster-color, #2563eb)";
|
|
1632
|
+
element.style.borderColor = "var(--tileflow-interaction-border-color, #ffffff)";
|
|
1633
|
+
element.style.borderRadius = "9999px";
|
|
1634
|
+
element.style.borderStyle = "solid";
|
|
1635
|
+
element.style.borderWidth = "2px";
|
|
1636
|
+
element.style.boxShadow = "var(--tileflow-interaction-shadow, 0 1px 4px rgb(0 0 0 / 0.25))";
|
|
1637
|
+
element.style.color = "var(--tileflow-interaction-foreground, #ffffff)";
|
|
1638
|
+
element.style.cursor = "pointer";
|
|
1639
|
+
element.style.display = "inline-flex";
|
|
1640
|
+
element.style.justifyContent = "center";
|
|
1641
|
+
element.style.minHeight = "2.25rem";
|
|
1642
|
+
element.style.minWidth = "2.25rem";
|
|
1643
|
+
element.style.padding = "0.25rem";
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1183
1646
|
// src/reducer.ts
|
|
1184
1647
|
var initialTileflowInteractionState = Object.freeze({
|
|
1185
1648
|
popup: null
|
|
@@ -1789,7 +2252,9 @@ function createTileflowMapLibrePoiController(options) {
|
|
|
1789
2252
|
};
|
|
1790
2253
|
const handleClick = (value) => {
|
|
1791
2254
|
const event = parsePointerEvent(value);
|
|
1792
|
-
if (!event || disposed)
|
|
2255
|
+
if (!event || disposed || value.defaultPrevented === true) {
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
1793
2258
|
const modality = inputModality(event);
|
|
1794
2259
|
const match = hitTest(event);
|
|
1795
2260
|
if (!match) return;
|
|
@@ -2888,6 +3353,7 @@ function unsubscribe(values, value) {
|
|
|
2888
3353
|
}
|
|
2889
3354
|
export {
|
|
2890
3355
|
createTileflowAnnotationRegistry,
|
|
3356
|
+
createTileflowMapLibreDataRuntime,
|
|
2891
3357
|
createTileflowMapLibreDomRuntime,
|
|
2892
3358
|
createTileflowMapLibreInteractionCoordinator,
|
|
2893
3359
|
createTileflowMapLibrePoiController,
|
|
@@ -152,7 +152,20 @@ type TileflowResolvedMapTarget = {
|
|
|
152
152
|
coordinate: TileflowInteractionCoordinate;
|
|
153
153
|
kind: 'map';
|
|
154
154
|
};
|
|
155
|
-
|
|
155
|
+
/** One clustered group of annotations or GeoJSON points. */
|
|
156
|
+
type TileflowResolvedClusterTarget = {
|
|
157
|
+
/** The annotations inside the group, when annotations were clustered. */
|
|
158
|
+
annotationIds?: readonly string[];
|
|
159
|
+
clusterId: number;
|
|
160
|
+
coordinate: TileflowInteractionCoordinate;
|
|
161
|
+
kind: 'cluster';
|
|
162
|
+
pointCount: number;
|
|
163
|
+
/** Cluster properties, including any `clusterProperties` aggregates. */
|
|
164
|
+
properties: Readonly<Record<string, TileflowInteractionJsonValue>>;
|
|
165
|
+
/** The GeoJSON source, when a source was clustered. */
|
|
166
|
+
source?: string;
|
|
167
|
+
};
|
|
168
|
+
type TileflowResolvedInteractionTarget<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = TileflowResolvedAnnotationTarget<TAnnotation> | TileflowResolvedClusterTarget | TileflowResolvedSemanticFeatureTarget | TileflowResolvedStyleFeatureTarget | TileflowResolvedMapTarget;
|
|
156
169
|
type TileflowAnnotationViewContext<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = {
|
|
157
170
|
annotation: TAnnotation;
|
|
158
171
|
close: () => void;
|
|
@@ -427,10 +440,10 @@ declare const tileflowInteractionTargetRefSchema: z.ZodMiniDiscriminatedUnion<[z
|
|
|
427
440
|
kind: z.ZodMiniLiteral<"annotation">;
|
|
428
441
|
}, z.core.$strict>, z.ZodMiniObject<{
|
|
429
442
|
domain: z.ZodMiniString<string>;
|
|
430
|
-
featureId: z.ZodMiniUnion<readonly [z.
|
|
443
|
+
featureId: z.ZodMiniUnion<readonly [z.ZodMiniInt, z.ZodMiniString<string>]>;
|
|
431
444
|
kind: z.ZodMiniLiteral<"semantic-feature">;
|
|
432
445
|
}, z.core.$strict>, z.ZodMiniObject<{
|
|
433
|
-
featureId: z.ZodMiniUnion<readonly [z.
|
|
446
|
+
featureId: z.ZodMiniUnion<readonly [z.ZodMiniInt, z.ZodMiniString<string>]>;
|
|
434
447
|
kind: z.ZodMiniLiteral<"style-feature">;
|
|
435
448
|
layerId: z.ZodMiniString<string>;
|
|
436
449
|
}, z.core.$strict>, z.ZodMiniObject<{
|
|
@@ -443,10 +456,10 @@ declare const tileflowInteractionStateSchema: z.ZodMiniObject<{
|
|
|
443
456
|
kind: z.ZodMiniLiteral<"annotation">;
|
|
444
457
|
}, z.core.$strict>, z.ZodMiniObject<{
|
|
445
458
|
domain: z.ZodMiniString<string>;
|
|
446
|
-
featureId: z.ZodMiniUnion<readonly [z.
|
|
459
|
+
featureId: z.ZodMiniUnion<readonly [z.ZodMiniInt, z.ZodMiniString<string>]>;
|
|
447
460
|
kind: z.ZodMiniLiteral<"semantic-feature">;
|
|
448
461
|
}, z.core.$strict>, z.ZodMiniObject<{
|
|
449
|
-
featureId: z.ZodMiniUnion<readonly [z.
|
|
462
|
+
featureId: z.ZodMiniUnion<readonly [z.ZodMiniInt, z.ZodMiniString<string>]>;
|
|
450
463
|
kind: z.ZodMiniLiteral<"style-feature">;
|
|
451
464
|
layerId: z.ZodMiniString<string>;
|
|
452
465
|
}, z.core.$strict>, z.ZodMiniObject<{
|
|
@@ -460,10 +473,10 @@ declare const tileflowInteractionActionSchema: z.ZodMiniDiscriminatedUnion<[z.Zo
|
|
|
460
473
|
kind: z.ZodMiniLiteral<"annotation">;
|
|
461
474
|
}, z.core.$strict>, z.ZodMiniObject<{
|
|
462
475
|
domain: z.ZodMiniString<string>;
|
|
463
|
-
featureId: z.ZodMiniUnion<readonly [z.
|
|
476
|
+
featureId: z.ZodMiniUnion<readonly [z.ZodMiniInt, z.ZodMiniString<string>]>;
|
|
464
477
|
kind: z.ZodMiniLiteral<"semantic-feature">;
|
|
465
478
|
}, z.core.$strict>, z.ZodMiniObject<{
|
|
466
|
-
featureId: z.ZodMiniUnion<readonly [z.
|
|
479
|
+
featureId: z.ZodMiniUnion<readonly [z.ZodMiniInt, z.ZodMiniString<string>]>;
|
|
467
480
|
kind: z.ZodMiniLiteral<"style-feature">;
|
|
468
481
|
layerId: z.ZodMiniString<string>;
|
|
469
482
|
}, z.core.$strict>, z.ZodMiniObject<{
|
|
@@ -475,7 +488,44 @@ declare const tileflowInteractionActionSchema: z.ZodMiniDiscriminatedUnion<[z.Zo
|
|
|
475
488
|
type: z.ZodMiniLiteral<"close-popup">;
|
|
476
489
|
}, z.core.$strict>], "type">;
|
|
477
490
|
|
|
478
|
-
type
|
|
491
|
+
type TileflowAnnotationClusterOptions = Readonly<{
|
|
492
|
+
/** The last zoom that clusters; annotations are separate above it. Defaults to 14. */
|
|
493
|
+
clusterMaxZoom?: number;
|
|
494
|
+
/** The fewest annotations that form a cluster. Defaults to 2. */
|
|
495
|
+
clusterMinPoints?: number;
|
|
496
|
+
/** Cluster radius in CSS pixels or density-independent points. Defaults to 50. */
|
|
497
|
+
clusterRadius?: number;
|
|
498
|
+
/** Zoom in when a cluster is activated. Defaults to true. */
|
|
499
|
+
expandOnClick?: boolean;
|
|
500
|
+
/** The accessible name of a cluster. Defaults to `${count} places`. */
|
|
501
|
+
formatAriaLabel?: (count: number) => string;
|
|
502
|
+
}>;
|
|
503
|
+
type TileflowAnnotationCluster = Readonly<{
|
|
504
|
+
annotationIds: readonly string[];
|
|
505
|
+
clusterId: number;
|
|
506
|
+
coordinate: TileflowInteractionCoordinate;
|
|
507
|
+
/** The zoom at which this cluster splits. */
|
|
508
|
+
expansionZoom: number;
|
|
509
|
+
/** `point_count_abbreviated`, such as `1.2k`. */
|
|
510
|
+
label: string;
|
|
511
|
+
pointCount: number;
|
|
512
|
+
}>;
|
|
513
|
+
type TileflowAnnotationClusterView = Readonly<{
|
|
514
|
+
clusters: readonly TileflowAnnotationCluster[];
|
|
515
|
+
/** Annotations a cluster stands in for at this zoom. */
|
|
516
|
+
hiddenAnnotationIds: ReadonlySet<string>;
|
|
517
|
+
}>;
|
|
518
|
+
type TileflowAnnotationClusterIndex = Readonly<{
|
|
519
|
+
/** Groups for one map zoom. Results depend on its integer part only. */
|
|
520
|
+
at: (zoom: number) => TileflowAnnotationClusterView;
|
|
521
|
+
}>;
|
|
522
|
+
/**
|
|
523
|
+
* Groups annotations the way a clustering GeoJSON source groups points, with the same option names.
|
|
524
|
+
* It is pure, so web and native renderers share one result.
|
|
525
|
+
*/
|
|
526
|
+
declare function createTileflowAnnotationClusterIndex(annotations: readonly TileflowAnnotation[], options?: TileflowAnnotationClusterOptions): TileflowAnnotationClusterIndex;
|
|
527
|
+
|
|
528
|
+
type TileflowInteractionDiagnosticCode = 'DUPLICATE_ANNOTATION_ID' | 'INVALID_FIELD' | 'INVALID_ANNOTATION' | 'INVALID_DATA_LAYER' | 'INVALID_DOCUMENT' | 'LIMIT_EXCEEDED' | 'MISSING_VIEW' | 'OVERLAY_FAILURE' | 'SEMANTIC_MANIFEST_MISMATCH' | 'STALE_TARGET' | 'UNSTABLE_FEATURE_IDENTITY' | 'UNSUPPORTED_MODE' | 'UNSUPPORTED_TARGET';
|
|
479
529
|
type TileflowInteractionDiagnostic = {
|
|
480
530
|
code: TileflowInteractionDiagnosticCode;
|
|
481
531
|
level: 'error' | 'warning';
|
|
@@ -503,4 +553,4 @@ type TileflowInteractionBindingValidationResult = {
|
|
|
503
553
|
declare function validateTileflowAnnotations(input: unknown): TileflowAnnotationValidationResult;
|
|
504
554
|
declare function validateTileflowInteractionBindings(input: unknown): TileflowInteractionBindingValidationResult;
|
|
505
555
|
|
|
506
|
-
export { type
|
|
556
|
+
export { tileflowInteractionTargetRefSchema as $, type TileflowInteractionViewContent as A, type TileflowInteractionViewContext as B, type TileflowPoiCategory as C, type TileflowPoiFilterRank as D, type TileflowPoiProperties as E, type TileflowPoiSizeRank as F, type TileflowResolvedAnnotationTarget as G, type TileflowResolvedClusterTarget as H, type TileflowResolvedInteractionTarget as I, type TileflowResolvedMapTarget as J, type TileflowResolvedPoiFeature as K, type TileflowResolvedPoiFeatureTarget as L, type TileflowResolvedSemanticFeature as M, type TileflowResolvedSemanticFeatureTarget as N, type TileflowResolvedStyleFeatureTarget as O, createTileflowAnnotationClusterIndex as P, tileflowAnnotationSchema as Q, tileflowAnnotationsSchema as R, tileflowInteractionActionSchema as S, type TileflowInteractionState as T, tileflowInteractionBindingSchema as U, tileflowInteractionBindingsSchema as V, tileflowInteractionContentSchema as W, tileflowInteractionJsonValueSchema as X, tileflowInteractionLimits as Y, tileflowInteractionSchemaVersion as Z, tileflowInteractionStateSchema as _, type TileflowInteractionAction as a, tileflowInteractionTargetSchema as a0, tileflowPoiCategories as a1, validateTileflowAnnotations as a2, validateTileflowInteractionBindings as a3, type TileflowInteractionTargetRef as b, type TileflowAnnotation as c, type TileflowAnnotationCluster as d, type TileflowAnnotationClusterIndex as e, type TileflowAnnotationClusterOptions as f, type TileflowAnnotationClusterView as g, type TileflowAnnotationInteractionEvent as h, type TileflowAnnotationMarker as i, type TileflowAnnotationSurface as j, type TileflowAnnotationValidationResult as k, type TileflowAnnotationViewContext as l, type TileflowInteractionBinding as m, type TileflowInteractionBindingValidationResult as n, type TileflowInteractionContent as o, type TileflowInteractionCoordinate as p, type TileflowInteractionDiagnostic as q, type TileflowInteractionDiagnosticCode as r, type TileflowInteractionEvent as s, type TileflowInteractionEventType as t, type TileflowInteractionFieldContent as u, type TileflowInteractionInputModality as v, type TileflowInteractionJsonPrimitive as w, type TileflowInteractionJsonValue as x, type TileflowInteractionTarget as y, type TileflowInteractionTextContent as z };
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tileflow/interactions",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.8",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"description": "Portable annotation and interaction contracts for Tileflow maps.",
|
|
5
|
+
"description": "Portable annotation, clustering and interaction contracts for Tileflow maps.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
8
8
|
"homepage": "https://tileflow.dev",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"types": "./dist/index.d.ts",
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
|
-
"LICENSE"
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"REFERENCE.md"
|
|
28
29
|
],
|
|
29
30
|
"exports": {
|
|
30
31
|
".": {
|
|
@@ -43,7 +44,8 @@
|
|
|
43
44
|
"access": "public"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
|
-
"
|
|
47
|
+
"supercluster": "^8.0.1",
|
|
48
|
+
"zod": "^4.6.7"
|
|
47
49
|
},
|
|
48
50
|
"peerDependencies": {
|
|
49
51
|
"maplibre-gl": ">=6.4.1 <7"
|
|
@@ -53,6 +55,10 @@
|
|
|
53
55
|
"optional": true
|
|
54
56
|
}
|
|
55
57
|
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/supercluster": "^7.1.3",
|
|
60
|
+
"maplibre-gl": "6.16.0"
|
|
61
|
+
},
|
|
56
62
|
"scripts": {
|
|
57
63
|
"build": "tsup src/index.ts src/maplibre-entry.ts --format esm --dts --clean --no-splitting",
|
|
58
64
|
"verify": "pnpm run build && tsx --test test/*.test.ts",
|