@tileflow/react-native 0.1.0-alpha.8 → 0.1.0-alpha.9

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 CHANGED
@@ -91,7 +91,7 @@ Add `timeOfDay="dusk"` beside `theme` to select a map's optional `day`, `dawn`,
91
91
  `night` preset. For Streets, `theme="light"` with this preset selects the existing `dusk` palette.
92
92
  Maps without this capability ignore the option; omitting it keeps the selected theme unchanged.
93
93
  Changing it uses the existing theme transition. `themeBlend` takes precedence. See the
94
- [authoring contract](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/README.md#style-with-themes-and-semantic-modules).
94
+ [authoring contract](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/REFERENCE.md#style-with-themes-and-semantic-modules).
95
95
 
96
96
  Theme and `timeOfDay` changes cross-fade over 450 ms by default: a snapshot of the drawn map covers it, follows the camera and fades out once the new theme is drawn. Use `themeTransition={{duration: 700}}` to customize the duration, or `duration: 0` for an immediate change. `themeBlend={{themes: ['night', 'dusk', 'day'], position}}` shows a continuous blend of the map's themes instead of `theme`; position 0 is the first theme and 1.5 is halfway from the second to the third:
97
97
 
@@ -275,6 +275,13 @@ wrapper. A bounded accessible marker is rendered when `renderMarker` is omitted.
275
275
  remain in native vector or GeoJSON layers and are selected through semantic rendered-feature
276
276
  queries rather than thousands of React marker views.
277
277
 
278
+ `Map` also takes the web adapters' `dataSources`, `dataOverlays` and `annotationClusters`, with
279
+ `geojsonSource()`, `clusterOverlay()` and `heatmapOverlay()` from `@tileflow/core/native`. Tapping a
280
+ cluster zooms in unless the application controls `view`, and reports a `cluster` target through
281
+ `onInteractionEvent`. Compiled clusters and heatmaps are checked by
282
+ `tileflow validate --renderer native`. Polygon drawing with Terra Draw is web-only. See the
283
+ [interactions reference](https://github.com/tileflow/tileflow-sdk/blob/main/packages/interactions/REFERENCE.md).
284
+
278
285
  The application owns persistent selection and its presentation. The SDK emits normalized
279
286
  annotation or POI targets but no popup, callout, tooltip, sheet, panel or modal renderer.
280
287
 
@@ -1,8 +1,8 @@
1
1
  import { MapProps as MapProps$1 } from '@maplibre/maplibre-react-native';
2
2
  import { ReactElement, ReactNode, Ref } from 'react';
3
3
  import { ViewProps } from 'react-native';
4
- import { TileflowNativeInitialView, TileflowNativeInitialViewOptions, TileflowNativeSource, TileflowNativeSourceOptions, TileflowBoundaryValues, TileflowBoundaryFamily, TileflowNativeSourceState, TileflowNativeSourceError } from '@tileflow/core/native';
5
- import { TileflowAnnotation, TileflowInteractionBinding, TileflowInteractionEvent, TileflowResolvedAnnotationTarget, TileflowResolvedPoiFeatureTarget, TileflowInteractionDiagnostic, TileflowAnnotationViewContext } from '@tileflow/interactions';
4
+ import { TileflowMapLibreOverlay, TileflowGeoJsonSource, TileflowNativeInitialView, TileflowNativeInitialViewOptions, TileflowNativeSource, TileflowNativeSourceOptions, TileflowBoundaryValues, TileflowBoundaryFamily, TileflowNativeSourceState, TileflowNativeSourceError } from '@tileflow/core/native';
5
+ import { TileflowAnnotation, TileflowAnnotationClusterOptions, TileflowInteractionBinding, TileflowInteractionEvent, TileflowResolvedAnnotationTarget, TileflowResolvedClusterTarget, TileflowResolvedPoiFeatureTarget, TileflowResolvedStyleFeatureTarget, TileflowInteractionDiagnostic, TileflowAnnotationViewContext } from '@tileflow/interactions';
6
6
 
7
7
  /** Marker composition only. Selection presentation and native handles are not part of this context. */
8
8
  type MapMarkerRenderContext<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = Readonly<Pick<TileflowAnnotationViewContext<TAnnotation>, 'annotation' | 'target' | 'content' | 'viewName'>>;
@@ -10,11 +10,17 @@ type MapMarkerRenderContext<TAnnotation extends TileflowAnnotation = TileflowAnn
10
10
  type MapMarkerRenderer<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = (context: MapMarkerRenderContext<TAnnotation>) => ReactElement;
11
11
  /** Mobile reports activation; the application owns any persistent selection. */
12
12
  type MapInteractionEvent<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = Readonly<Omit<TileflowInteractionEvent<TAnnotation>, 'target' | 'type'> & {
13
- target: TileflowResolvedAnnotationTarget<TAnnotation> | TileflowResolvedPoiFeatureTarget;
13
+ target: TileflowResolvedAnnotationTarget<TAnnotation> | TileflowResolvedClusterTarget | TileflowResolvedPoiFeatureTarget | TileflowResolvedStyleFeatureTarget;
14
14
  type: 'target:activate';
15
15
  }>;
16
16
  type MapInteractionProps<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = Readonly<{
17
+ /** Group `annotations` into accessible native cluster buttons. Up to 1,000 annotations. */
18
+ annotationClusters?: TileflowAnnotationClusterOptions;
17
19
  annotations?: readonly TAnnotation[];
20
+ /** Overlays drawn over `dataSources` or compiled sources, keyed as in `defineMap`. */
21
+ dataOverlays?: Readonly<Record<string, TileflowMapLibreOverlay>>;
22
+ /** Application GeoJSON, keyed as in `defineMap`. A compiled source of the same key is filled. */
23
+ dataSources?: Readonly<Record<string, TileflowGeoJsonSource>>;
18
24
  interactions?: readonly TileflowInteractionBinding[];
19
25
  onInteractionEvent?: (event: MapInteractionEvent<TAnnotation>) => void;
20
26
  onInteractionDiagnostic?: (diagnostic: TileflowInteractionDiagnostic) => void;
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { ReactElement } from 'react';
2
2
  import { TileflowAnnotation } from '@tileflow/interactions';
3
3
  export { TileflowAnnotation, TileflowInteractionBinding, TileflowInteractionCoordinate, TileflowInteractionDiagnostic, TileflowPoiCategory, TileflowResolvedAnnotationTarget, TileflowResolvedPoiFeatureTarget } from '@tileflow/interactions';
4
- import { d as MapProps } from './contract-BWT1jxqg.js';
5
- export { e as MapBaseProps, f as MapBoundaryFeatureState, c as MapCameraProps, g as MapColorScheme, h as MapErrorEvent, i as MapEventHandlers, b as MapInitialViewInputs, j as MapInteractionEvent, k as MapInteractionProps, l as MapLoadEvent, m as MapMarkerRenderContext, n as MapMarkerRenderer, o as MapOptions, p as MapPresentationProps, q as MapReadinessChangeEvent, r as MapRef, s as MapSelection, t as MapSessionError, u as MapSource, v as MapSourceError, w as MapSourceProps, x as MapSourceState, y as MapTheme, z as MapThemeBlend, A as MapThemeChangeEvent, B as MapThemeSelection, C as MapThemeTransition, M as MapView, a as MapViewChangeEvent } from './contract-BWT1jxqg.js';
4
+ import { d as MapProps } from './contract-BWCbfMOx.js';
5
+ export { e as MapBaseProps, f as MapBoundaryFeatureState, c as MapCameraProps, g as MapColorScheme, h as MapErrorEvent, i as MapEventHandlers, b as MapInitialViewInputs, j as MapInteractionEvent, k as MapInteractionProps, l as MapLoadEvent, m as MapMarkerRenderContext, n as MapMarkerRenderer, o as MapOptions, p as MapPresentationProps, q as MapReadinessChangeEvent, r as MapRef, s as MapSelection, t as MapSessionError, u as MapSource, v as MapSourceError, w as MapSourceProps, x as MapSourceState, y as MapTheme, z as MapThemeBlend, A as MapThemeChangeEvent, B as MapThemeSelection, C as MapThemeTransition, M as MapView, a as MapViewChangeEvent } from './contract-BWCbfMOx.js';
6
6
  import '@maplibre/maplibre-react-native';
7
7
  import 'react-native';
8
8
  import '@tileflow/core/native';
package/dist/index.js CHANGED
@@ -15,16 +15,25 @@ import {
15
15
  Map as NativeMap
16
16
  } from "@maplibre/maplibre-react-native";
17
17
  import {
18
- Fragment,
18
+ Fragment as Fragment2,
19
19
  useImperativeHandle,
20
20
  useLayoutEffect as useLayoutEffect3,
21
21
  useMemo as useMemo3,
22
- useRef,
22
+ useRef as useRef2,
23
23
  useState as useState2,
24
24
  useSyncExternalStore
25
25
  } from "react";
26
26
  import { AppState, findNodeHandle, View as View2 } from "react-native";
27
27
 
28
+ // src/native-annotation-clusters.ts
29
+ import {
30
+ createTileflowAnnotationClusterIndex
31
+ } from "@tileflow/interactions";
32
+ import { withTileflowClusterLabelFont } from "@tileflow/interactions";
33
+ function createNativeAnnotationClusters(annotations, options) {
34
+ return createTileflowAnnotationClusterIndex(annotations, options);
35
+ }
36
+
28
37
  // src/native-interaction-input.ts
29
38
  import {
30
39
  tileflowInteractionJsonValueSchema,
@@ -36,6 +45,7 @@ var messages = {
36
45
  DUPLICATE_ANNOTATION_ID: "Annotation IDs must be unique.",
37
46
  INVALID_FIELD: "An interaction field is invalid.",
38
47
  INVALID_ANNOTATION: "An annotation is invalid.",
48
+ INVALID_DATA_LAYER: "A data source or overlay is invalid.",
39
49
  INVALID_DOCUMENT: "The interaction document is invalid.",
40
50
  LIMIT_EXCEEDED: "The portable interaction limit was exceeded.",
41
51
  MISSING_VIEW: "The interaction view is unavailable.",
@@ -828,8 +838,8 @@ function item(value, index) {
828
838
  return descriptor.value;
829
839
  }
830
840
  function array2(value, maximum2) {
831
- const count = length(value, maximum2);
832
- return Array.from({ length: count }, (_, index) => item(value, index));
841
+ const count2 = length(value, maximum2);
842
+ return Array.from({ length: count2 }, (_, index) => item(value, index));
833
843
  }
834
844
  function name2(value) {
835
845
  return typeof value === "string" && value.length > 0 && value.length <= 256;
@@ -914,8 +924,8 @@ function createNativeInteractionQuery(style, current, query) {
914
924
  cancel();
915
925
  return;
916
926
  }
917
- const count = length(hits);
918
- const read = Math.min(count, nativePoiQueryLimit + 1 - features.length);
927
+ const count2 = length(hits);
928
+ const read = Math.min(count2, nativePoiQueryLimit + 1 - features.length);
919
929
  for (let index = 0; index < read; index++) {
920
930
  const feature = item(hits, index);
921
931
  if (nativeInteractionField(feature, "type") !== "Feature") throw new Error();
@@ -1501,10 +1511,245 @@ function NativeBoundaryFeatureState({
1501
1511
  );
1502
1512
  }
1503
1513
 
1514
+ // src/native-data-layers.tsx
1515
+ import {
1516
+ GeoJSONSource,
1517
+ Layer,
1518
+ Marker
1519
+ } from "@maplibre/maplibre-react-native";
1520
+ import { useRef } from "react";
1521
+ import { Pressable, Text } from "react-native";
1522
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
1523
+ var bubble = Object.freeze({
1524
+ alignItems: "center",
1525
+ backgroundColor: "#2563eb",
1526
+ borderColor: "#ffffff",
1527
+ borderRadius: 22,
1528
+ borderWidth: 2,
1529
+ justifyContent: "center",
1530
+ minHeight: 44,
1531
+ minWidth: 44,
1532
+ paddingHorizontal: 6
1533
+ });
1534
+ var count = Object.freeze({ color: "#ffffff", fontSize: 14, fontWeight: "600" });
1535
+ function NativeDataLayers({
1536
+ camera,
1537
+ expand,
1538
+ onActivate,
1539
+ plan
1540
+ }) {
1541
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1542
+ plan.sources.map((source) => /* @__PURE__ */ jsx2(
1543
+ NativeDataSource,
1544
+ {
1545
+ camera,
1546
+ expand,
1547
+ onActivate,
1548
+ source
1549
+ },
1550
+ source.id
1551
+ )),
1552
+ plan.layers.map((layer) => /* @__PURE__ */ jsx2(Layer, { ...layer }, layer.id))
1553
+ ] });
1554
+ }
1555
+ function NativeDataSource({
1556
+ camera,
1557
+ expand,
1558
+ onActivate,
1559
+ source
1560
+ }) {
1561
+ const ref = useRef(null);
1562
+ const pressable = source.clusterLayers.length > 0 || source.pointLayers.length > 0;
1563
+ return /* @__PURE__ */ jsx2(
1564
+ GeoJSONSource,
1565
+ {
1566
+ ...source.definition,
1567
+ id: source.id,
1568
+ ref,
1569
+ data: source.data,
1570
+ onPress: pressable ? (event) => {
1571
+ const feature = event.nativeEvent.features[0];
1572
+ const coordinate2 = pointCoordinate(feature);
1573
+ if (!feature || !coordinate2) return;
1574
+ const properties2 = jsonProperties(feature.properties);
1575
+ const clusterId = properties2.cluster_id;
1576
+ if (typeof clusterId !== "number") {
1577
+ const layerId = source.pointLayers[0];
1578
+ if (!layerId) return;
1579
+ onActivate({
1580
+ coordinate: coordinate2,
1581
+ feature: { ...feature.id === void 0 ? {} : { id: feature.id }, properties: properties2 },
1582
+ kind: "style-feature",
1583
+ layerId
1584
+ });
1585
+ return;
1586
+ }
1587
+ onActivate({
1588
+ clusterId,
1589
+ coordinate: coordinate2,
1590
+ kind: "cluster",
1591
+ pointCount: typeof properties2.point_count === "number" ? properties2.point_count : 0,
1592
+ properties: properties2,
1593
+ source: source.id
1594
+ });
1595
+ if (!expand || !source.expandOnClick) return;
1596
+ ref.current?.getClusterExpansionZoom(clusterId).then(
1597
+ (zoom) => camera.current?.easeTo({
1598
+ center: [coordinate2[0], coordinate2[1]],
1599
+ duration: 400,
1600
+ zoom
1601
+ }),
1602
+ () => void 0
1603
+ );
1604
+ } : void 0,
1605
+ children: source.layers.map((layer) => /* @__PURE__ */ jsx2(Layer, { ...layer }, layer.id))
1606
+ }
1607
+ );
1608
+ }
1609
+ function NativeClusterMarker({
1610
+ cluster,
1611
+ label,
1612
+ onPress
1613
+ }) {
1614
+ const press = (event) => {
1615
+ event?.stopPropagation();
1616
+ onPress();
1617
+ };
1618
+ return /* @__PURE__ */ jsx2(
1619
+ Marker,
1620
+ {
1621
+ id: `tileflow-cluster-${cluster.clusterId}`,
1622
+ lngLat: [cluster.coordinate[0], cluster.coordinate[1]],
1623
+ accessible: true,
1624
+ accessibilityLabel: label,
1625
+ accessibilityRole: "button",
1626
+ onAccessibilityTap: onPress,
1627
+ onPress: press,
1628
+ children: /* @__PURE__ */ jsx2(Pressable, { accessible: false, onPress: press, style: bubble, children: /* @__PURE__ */ jsx2(Text, { style: count, children: cluster.label }) })
1629
+ }
1630
+ );
1631
+ }
1632
+ function pointCoordinate(feature) {
1633
+ const coordinates = feature?.geometry?.coordinates;
1634
+ return feature?.geometry?.type === "Point" && Array.isArray(coordinates) && typeof coordinates[0] === "number" && typeof coordinates[1] === "number" ? [coordinates[0], coordinates[1]] : void 0;
1635
+ }
1636
+ function jsonProperties(properties2) {
1637
+ return Object.fromEntries(
1638
+ Object.entries(properties2 ?? {}).filter(([, value]) => value !== void 0)
1639
+ );
1640
+ }
1641
+
1642
+ // src/native-data-layers-plan.ts
1643
+ var clusterMetadataKey = "tileflow:cluster";
1644
+ var placementMetadataKey = "tileflow:overlay-placement-manifest";
1645
+ function planNativeDataLayers(input) {
1646
+ const styleSources = input.style.sources ?? {};
1647
+ const styleLayers = input.style.layers ?? [];
1648
+ const styleLayerIds = new Set(styleLayers.map(({ id }) => String(id)));
1649
+ const anchors = input.style.metadata?.[placementMetadataKey]?.anchors;
1650
+ const diagnostics = [];
1651
+ const entries = /* @__PURE__ */ new Map();
1652
+ const freeLayers = [];
1653
+ const entry = (id) => entries.get(id);
1654
+ const addRoles = (sourceId, layer) => {
1655
+ const target = entry(sourceId);
1656
+ const cluster = layer.metadata?.[clusterMetadataKey];
1657
+ if (!target || !cluster) return;
1658
+ if (cluster.role === "cluster") target.clusterLayers.push(String(layer.id));
1659
+ if (cluster.role === "point") target.pointLayers.push(String(layer.id));
1660
+ if (cluster.expandOnClick === false) target.expandOnClick = false;
1661
+ };
1662
+ for (const [id, source] of Object.entries(input.dataSources ?? {}).sort(compare)) {
1663
+ const compiled2 = styleSources[id];
1664
+ if (source.kind !== "geojson" || compiled2 && compiled2.type !== "geojson") {
1665
+ diagnostics.push({ message: `Source "${id}" is not GeoJSON.`, path: `/dataSources/${id}` });
1666
+ continue;
1667
+ }
1668
+ const { data, kind: _kind, ...definition } = source;
1669
+ entries.set(id, {
1670
+ adopted: compiled2 !== void 0,
1671
+ clusterLayers: [],
1672
+ data,
1673
+ // A reused source keeps its compiled cluster options.
1674
+ definition: compiled2 ? {} : definition,
1675
+ expandOnClick: true,
1676
+ layers: [],
1677
+ pointLayers: []
1678
+ });
1679
+ }
1680
+ for (const layer of styleLayers) {
1681
+ const sourceId = typeof layer.source === "string" ? layer.source : void 0;
1682
+ const cluster = layer.metadata?.[clusterMetadataKey];
1683
+ if (!sourceId || !cluster || entries.has(sourceId)) continue;
1684
+ const compiled2 = styleSources[sourceId];
1685
+ if (compiled2?.type !== "geojson") continue;
1686
+ entries.set(sourceId, {
1687
+ adopted: true,
1688
+ clusterLayers: [],
1689
+ data: compiled2.data,
1690
+ definition: {},
1691
+ expandOnClick: true,
1692
+ layers: [],
1693
+ pointLayers: []
1694
+ });
1695
+ }
1696
+ for (const layer of styleLayers) {
1697
+ if (typeof layer.source === "string") addRoles(layer.source, layer);
1698
+ }
1699
+ for (const [id, overlay] of Object.entries(input.dataOverlays ?? {}).sort(compare)) {
1700
+ const path = `/dataOverlays/${id}`;
1701
+ if (overlay.kind !== "maplibre-overlay") {
1702
+ diagnostics.push({ message: `Overlay "${id}" is not a Tileflow overlay.`, path });
1703
+ continue;
1704
+ }
1705
+ const target = entry(overlay.source);
1706
+ if (!target && !styleSources[overlay.source]) {
1707
+ diagnostics.push({
1708
+ message: `Overlay "${id}" references unknown source "${overlay.source}".`,
1709
+ path: `${path}/source`
1710
+ });
1711
+ continue;
1712
+ }
1713
+ const anchor = anchors?.[overlay.placement];
1714
+ const beforeId = typeof anchor === "string" && styleLayerIds.has(anchor) ? anchor : void 0;
1715
+ for (const layer of overlay.layers) {
1716
+ if (styleLayerIds.has(layer.id)) continue;
1717
+ const prepared = withTileflowClusterLabelFont(layer, styleLayers);
1718
+ if (!prepared) {
1719
+ diagnostics.push({
1720
+ message: `Layer "${layer.id}" needs font: the style has no label font.`,
1721
+ path
1722
+ });
1723
+ continue;
1724
+ }
1725
+ const native = {
1726
+ ...prepared,
1727
+ ...beforeId ? { beforeId } : {},
1728
+ id: layer.id,
1729
+ source: overlay.source
1730
+ };
1731
+ if (target) {
1732
+ target.layers.push(native);
1733
+ addRoles(overlay.source, layer);
1734
+ } else {
1735
+ freeLayers.push(native);
1736
+ }
1737
+ }
1738
+ }
1739
+ return Object.freeze({
1740
+ diagnostics: Object.freeze(diagnostics),
1741
+ layers: Object.freeze(freeLayers),
1742
+ sources: Object.freeze([...entries].map(([id, value]) => Object.freeze({ id, ...value })))
1743
+ });
1744
+ }
1745
+ function compare([left], [right]) {
1746
+ return left < right ? -1 : left > right ? 1 : 0;
1747
+ }
1748
+
1504
1749
  // src/native-marker.tsx
1505
1750
  import { Marker as NativeMarker } from "@maplibre/maplibre-react-native";
1506
1751
  import { useLayoutEffect as useLayoutEffect2, useMemo as useMemo2 } from "react";
1507
- import { Pressable, processColor, View } from "react-native";
1752
+ import { Pressable as Pressable2, processColor, View } from "react-native";
1508
1753
 
1509
1754
  // src/native-marker-model.ts
1510
1755
  import { isValidElement } from "react";
@@ -1550,7 +1795,7 @@ function resolveNativeMarkerContent(context, render) {
1550
1795
  }
1551
1796
 
1552
1797
  // src/native-marker.tsx
1553
- import { jsx as jsx2 } from "react/jsx-runtime";
1798
+ import { jsx as jsx3 } from "react/jsx-runtime";
1554
1799
  var button = Object.freeze({
1555
1800
  minWidth: 44,
1556
1801
  minHeight: 44,
@@ -1593,7 +1838,7 @@ function NativeAnnotationMarker({
1593
1838
  event?.stopPropagation();
1594
1839
  lifecycle.markerPress(sceneKey, annotation);
1595
1840
  };
1596
- return /* @__PURE__ */ jsx2(
1841
+ return /* @__PURE__ */ jsx3(
1597
1842
  NativeMarker,
1598
1843
  {
1599
1844
  id: `tileflow-annotation-${model.key}`,
@@ -1604,8 +1849,8 @@ function NativeAnnotationMarker({
1604
1849
  lifecycle.beginTouch(sceneKey);
1605
1850
  press();
1606
1851
  },
1607
- children: /* @__PURE__ */ jsx2(
1608
- Pressable,
1852
+ children: /* @__PURE__ */ jsx3(
1853
+ Pressable2,
1609
1854
  {
1610
1855
  accessible: false,
1611
1856
  disabled: !enabled,
@@ -1615,14 +1860,14 @@ function NativeAnnotationMarker({
1615
1860
  event.stopPropagation();
1616
1861
  },
1617
1862
  onPress: press,
1618
- children: /* @__PURE__ */ jsx2(
1863
+ children: /* @__PURE__ */ jsx3(
1619
1864
  View,
1620
1865
  {
1621
1866
  pointerEvents: "none",
1622
1867
  accessible: false,
1623
1868
  accessibilityElementsHidden: true,
1624
1869
  importantForAccessibility: "no-hide-descendants",
1625
- children: rendered.content ?? /* @__PURE__ */ jsx2(View, { style: [dot, { backgroundColor: color }] })
1870
+ children: rendered.content ?? /* @__PURE__ */ jsx3(View, { style: [dot, { backgroundColor: color }] })
1626
1871
  }
1627
1872
  )
1628
1873
  }
@@ -2823,7 +3068,7 @@ function decodeUtf8(bytes) {
2823
3068
  if (index + remaining > bytes.length) {
2824
3069
  throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_UTF8_INVALID");
2825
3070
  }
2826
- for (let count = 0; count < remaining; count += 1) {
3071
+ for (let count2 = 0; count2 < remaining; count2 += 1) {
2827
3072
  const byte = bytes[index++];
2828
3073
  if ((byte & 192) !== 128) {
2829
3074
  throw new HostedNativeSessionError("NATIVE_SESSION_RESPONSE_UTF8_INVALID");
@@ -3890,27 +4135,27 @@ function decode(bytes) {
3890
4135
  for (let index = 0; index < bytes.length; ) {
3891
4136
  const first = bytes[index++];
3892
4137
  let code;
3893
- let count;
4138
+ let count2;
3894
4139
  let minimum;
3895
4140
  if (first <= 127) {
3896
4141
  code = first;
3897
- count = 0;
4142
+ count2 = 0;
3898
4143
  minimum = 0;
3899
4144
  } else if (first >= 194 && first <= 223) {
3900
4145
  code = first & 31;
3901
- count = 1;
4146
+ count2 = 1;
3902
4147
  minimum = 128;
3903
4148
  } else if (first >= 224 && first <= 239) {
3904
4149
  code = first & 15;
3905
- count = 2;
4150
+ count2 = 2;
3906
4151
  minimum = 2048;
3907
4152
  } else if (first >= 240 && first <= 244) {
3908
4153
  code = first & 7;
3909
- count = 3;
4154
+ count2 = 3;
3910
4155
  minimum = 65536;
3911
4156
  } else throw new NativePreparationError();
3912
- if (index + count > bytes.length) throw new NativePreparationError();
3913
- for (let part = 0; part < count; part++) {
4157
+ if (index + count2 > bytes.length) throw new NativePreparationError();
4158
+ for (let part = 0; part < count2; part++) {
3914
4159
  const next = bytes[index++];
3915
4160
  if ((next & 192) !== 128) throw new NativePreparationError();
3916
4161
  code = code * 64 + (next & 63);
@@ -5594,7 +5839,7 @@ function fontMetadata(input, developmentOrigin) {
5594
5839
  }
5595
5840
  function validateFontFaces(faces, documentUrl, metadata, add, rewrites, policy, discriminate, developmentOrigin) {
5596
5841
  if (Object.keys(faces).length > nativePreparationLimits.fontFaces) throw invalid3();
5597
- let count = 0;
5842
+ let count2 = 0;
5598
5843
  const matched = /* @__PURE__ */ new Set();
5599
5844
  for (const name3 of Object.keys(faces).sort()) {
5600
5845
  if (name3.length === 0 || name3.length > 256 || name3 !== name3.trim()) throw invalid3();
@@ -5602,7 +5847,7 @@ function validateFontFaces(faces, documentUrl, metadata, add, rewrites, policy,
5602
5847
  if (!Array.isArray(definitions) || definitions.length === 0) throw invalid3();
5603
5848
  const rewritten = [];
5604
5849
  for (const definition of definitions) {
5605
- if (++count > nativePreparationLimits.fontFaces || !record2(definition)) throw invalid3();
5850
+ if (++count2 > nativePreparationLimits.fontFaces || !record2(definition)) throw invalid3();
5606
5851
  const keys = Object.keys(definition).sort();
5607
5852
  if (keys.length !== 4 || !["font-family", "font-style", "font-weight", "url"].every((key2) => keys.includes(key2)))
5608
5853
  throw invalid3();
@@ -7703,7 +7948,7 @@ function createNativeMapOwner() {
7703
7948
  }
7704
7949
 
7705
7950
  // src/map.tsx
7706
- import { jsx as jsx3, jsxs } from "react/jsx-runtime";
7951
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
7707
7952
  var fill = Object.freeze({ flex: 1 });
7708
7953
  function NativeScene({
7709
7954
  scene,
@@ -7713,17 +7958,18 @@ function NativeScene({
7713
7958
  interactions,
7714
7959
  interactionEnabled,
7715
7960
  renderMarker,
7716
- boundaryFeatureState
7961
+ boundaryFeatureState,
7962
+ data
7717
7963
  }) {
7718
- const view = useRef(null);
7719
- const camera = useRef(null);
7720
- const nativeMap = useRef(null);
7721
- const committedHost = useRef(void 0);
7722
- const tag = useRef(null);
7723
- const laidOut = useRef(false);
7724
- const styleLoaded = useRef(false);
7725
- const attachRequested = useRef(false);
7726
- const latestStyle = useRef(scene.style);
7964
+ const view = useRef2(null);
7965
+ const camera = useRef2(null);
7966
+ const nativeMap = useRef2(null);
7967
+ const committedHost = useRef2(void 0);
7968
+ const tag = useRef2(null);
7969
+ const laidOut = useRef2(false);
7970
+ const styleLoaded = useRef2(false);
7971
+ const attachRequested = useRef2(false);
7972
+ const latestStyle = useRef2(scene.style);
7727
7973
  latestStyle.current = scene.style;
7728
7974
  const [loadedStyle, setLoadedStyle] = useState2(void 0);
7729
7975
  const host = useMemo3(() => {
@@ -7778,7 +8024,42 @@ function NativeScene({
7778
8024
  };
7779
8025
  const annotations = interactions?.annotations;
7780
8026
  const byId = new globalThis.Map(annotations?.map((annotation) => [annotation.id, annotation]));
7781
- return /* @__PURE__ */ jsx3(
8027
+ const [zoomLevel, setZoomLevel] = useState2(() => Math.floor(scene.initialView.zoom));
8028
+ const followZoom = (event) => {
8029
+ if (Number.isFinite(event.nativeEvent.zoom)) setZoomLevel(Math.floor(event.nativeEvent.zoom));
8030
+ };
8031
+ const clusterSettings = data.annotationClusters;
8032
+ const clusterKey = JSON.stringify(clusterSettings ?? null);
8033
+ const clusterIndex = useMemo3(
8034
+ () => clusterSettings && annotations ? createNativeAnnotationClusters(annotations, clusterSettings) : void 0,
8035
+ // Options written inline are new objects each render; only a real change rebuilds groups.
8036
+ // eslint-disable-next-line react-hooks/exhaustive-deps
8037
+ [annotations, clusterKey]
8038
+ );
8039
+ const clusterView = clusterIndex?.at(zoomLevel);
8040
+ const styleReady = loadedStyle === scene.style;
8041
+ const dataPlan = useMemo3(
8042
+ () => planNativeDataLayers({
8043
+ dataOverlays: data.dataOverlays,
8044
+ dataSources: data.dataSources,
8045
+ style: scene.style
8046
+ }),
8047
+ [data.dataOverlays, data.dataSources, scene.style]
8048
+ );
8049
+ const reportedPlan = useRef2(void 0);
8050
+ useLayoutEffect3(() => {
8051
+ if (reportedPlan.current === dataPlan) return;
8052
+ reportedPlan.current = dataPlan;
8053
+ for (const { message, path } of dataPlan.diagnostics)
8054
+ data.onDiagnostic({ code: "INVALID_DATA_LAYER", level: "error", message, path });
8055
+ }, [data, dataPlan]);
8056
+ const activate = (target) => data.onActivate({
8057
+ coordinate: target.coordinate,
8058
+ inputModality: "touch",
8059
+ target,
8060
+ type: "target:activate"
8061
+ });
8062
+ return /* @__PURE__ */ jsx4(
7782
8063
  View2,
7783
8064
  {
7784
8065
  ref: bind2,
@@ -7795,13 +8076,15 @@ function NativeScene({
7795
8076
  lifecycle.layoutChanged(scene.key);
7796
8077
  attach();
7797
8078
  },
7798
- children: /* @__PURE__ */ jsxs(
8079
+ children: /* @__PURE__ */ jsxs2(
7799
8080
  NativeMap,
7800
8081
  {
7801
8082
  ...mapOptions,
7802
8083
  ref: nativeMap,
7803
8084
  style: fill,
7804
8085
  mapStyle,
8086
+ onRegionIsChanging: followZoom,
8087
+ onRegionDidChange: followZoom,
7805
8088
  onPress: (event) => {
7806
8089
  if (!host.current()) return;
7807
8090
  try {
@@ -7820,7 +8103,7 @@ function NativeScene({
7820
8103
  attach();
7821
8104
  },
7822
8105
  children: [
7823
- /* @__PURE__ */ jsx3(
8106
+ /* @__PURE__ */ jsx4(
7824
8107
  NativeCamera,
7825
8108
  {
7826
8109
  ref: camera,
@@ -7830,21 +8113,57 @@ function NativeScene({
7830
8113
  },
7831
8114
  "tileflow-camera"
7832
8115
  ),
7833
- /* @__PURE__ */ jsx3(Fragment, { children: interactions?.plan.order.map((id) => {
7834
- const annotation = byId.get(id);
7835
- return annotation ? /* @__PURE__ */ jsx3(
7836
- NativeAnnotationMarker,
8116
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
8117
+ interactions?.plan.order.map((id) => {
8118
+ const annotation = byId.get(id);
8119
+ return annotation && !clusterView?.hiddenAnnotationIds.has(id) ? /* @__PURE__ */ jsx4(
8120
+ NativeAnnotationMarker,
8121
+ {
8122
+ annotation,
8123
+ enabled: interactionEnabled,
8124
+ sceneKey: scene.key,
8125
+ lifecycle,
8126
+ renderMarker
8127
+ },
8128
+ annotation.id
8129
+ ) : null;
8130
+ }),
8131
+ clusterView?.clusters.map((cluster) => /* @__PURE__ */ jsx4(
8132
+ NativeClusterMarker,
7837
8133
  {
7838
- annotation,
7839
- enabled: interactionEnabled,
7840
- sceneKey: scene.key,
7841
- lifecycle,
7842
- renderMarker
8134
+ cluster,
8135
+ label: clusterSettings?.formatAriaLabel?.(cluster.pointCount) ?? `${cluster.pointCount} places`,
8136
+ onPress: () => {
8137
+ activate({
8138
+ annotationIds: cluster.annotationIds,
8139
+ clusterId: cluster.clusterId,
8140
+ coordinate: cluster.coordinate,
8141
+ kind: "cluster",
8142
+ pointCount: cluster.pointCount,
8143
+ properties: { point_count: cluster.pointCount }
8144
+ });
8145
+ if (data.expand && clusterSettings?.expandOnClick !== false)
8146
+ camera.current?.easeTo({
8147
+ center: [cluster.coordinate[0], cluster.coordinate[1]],
8148
+ duration: 400,
8149
+ zoom: cluster.expansionZoom
8150
+ });
8151
+ }
7843
8152
  },
7844
- annotation.id
7845
- ) : null;
7846
- }) }, "tileflow-annotations"),
7847
- boundaryFeatureState ? /* @__PURE__ */ jsx3(
8153
+ `cluster-${cluster.clusterId}`
8154
+ ))
8155
+ ] }, "tileflow-annotations"),
8156
+ styleReady ? /* @__PURE__ */ jsx4(
8157
+ NativeDataLayers,
8158
+ {
8159
+ camera,
8160
+ expand: data.expand,
8161
+ onActivate: activate,
8162
+ plan: dataPlan
8163
+ },
8164
+ "tileflow-data-layers"
8165
+ ) : null,
8166
+ boundaryFeatureState ? /* @__PURE__ */ jsx4(
7848
8167
  NativeBoundaryFeatureState,
7849
8168
  {
7850
8169
  input: boundaryFeatureState,
@@ -7869,6 +8188,22 @@ function Map2(props) {
7869
8188
  lifecycle.getSnapshot,
7870
8189
  lifecycle.getSnapshot
7871
8190
  );
8191
+ const latestProps = useRef2(props);
8192
+ latestProps.current = props;
8193
+ const [callbacks] = useState2(() => ({
8194
+ onActivate: (event) => latestProps.current.onInteractionEvent?.({ ...event, type: "target:activate" }),
8195
+ onDiagnostic: (diagnostic) => latestProps.current.onInteractionDiagnostic?.(diagnostic)
8196
+ }));
8197
+ const data = useMemo3(
8198
+ () => ({
8199
+ annotationClusters: props.annotationClusters,
8200
+ dataOverlays: props.dataOverlays,
8201
+ dataSources: props.dataSources,
8202
+ expand: props.view === void 0,
8203
+ ...callbacks
8204
+ }),
8205
+ [callbacks, props.annotationClusters, props.dataOverlays, props.dataSources, props.view]
8206
+ );
7872
8207
  useImperativeHandle(
7873
8208
  props.ref,
7874
8209
  () => Object.freeze({ getSourceState: lifecycle.getSourceState, retry: lifecycle.retry }),
@@ -7889,7 +8224,7 @@ function Map2(props) {
7889
8224
  useLayoutEffect3(() => {
7890
8225
  lifecycle.update(props);
7891
8226
  });
7892
- return /* @__PURE__ */ jsx3(View2, { style: props.style, testID: props.testID, children: snapshot.renderer ? /* @__PURE__ */ jsx3(
8227
+ return /* @__PURE__ */ jsx4(View2, { style: props.style, testID: props.testID, children: snapshot.renderer ? /* @__PURE__ */ jsx4(
7893
8228
  NativeScene,
7894
8229
  {
7895
8230
  scene: snapshot.renderer,
@@ -7899,6 +8234,7 @@ function Map2(props) {
7899
8234
  interactionEnabled: snapshot.interactionEnabled === true,
7900
8235
  renderMarker: props.renderMarker,
7901
8236
  boundaryFeatureState: props.boundaryFeatureState,
8237
+ data,
7902
8238
  children: props.children
7903
8239
  },
7904
8240
  snapshot.renderer.key
@@ -1,4 +1,4 @@
1
- import { M as MapView, a as MapViewChangeEvent, b as MapInitialViewInputs, c as MapCameraProps } from '../contract-BWT1jxqg.js';
1
+ import { M as MapView, a as MapViewChangeEvent, b as MapInitialViewInputs, c as MapCameraProps } from '../contract-BWCbfMOx.js';
2
2
  import '@maplibre/maplibre-react-native';
3
3
  import 'react';
4
4
  import 'react-native';
@@ -1,4 +1,4 @@
1
- import { b as MapInitialViewInputs, M as MapView } from '../contract-BWT1jxqg.js';
1
+ import { b as MapInitialViewInputs, M as MapView } from '../contract-BWCbfMOx.js';
2
2
  import '@maplibre/maplibre-react-native';
3
3
  import 'react';
4
4
  import 'react-native';
@@ -9,6 +9,7 @@ var messages = {
9
9
  DUPLICATE_ANNOTATION_ID: "Annotation IDs must be unique.",
10
10
  INVALID_FIELD: "An interaction field is invalid.",
11
11
  INVALID_ANNOTATION: "An annotation is invalid.",
12
+ INVALID_DATA_LAYER: "A data source or overlay is invalid.",
12
13
  INVALID_DOCUMENT: "The interaction document is invalid.",
13
14
  LIMIT_EXCEEDED: "The portable interaction limit was exceeded.",
14
15
  MISSING_VIEW: "The interaction view is unavailable.",
@@ -1,4 +1,4 @@
1
- import { B as MapThemeSelection, g as MapColorScheme } from '../contract-BWT1jxqg.js';
1
+ import { B as MapThemeSelection, g as MapColorScheme } from '../contract-BWCbfMOx.js';
2
2
  import '@maplibre/maplibre-react-native';
3
3
  import 'react';
4
4
  import 'react-native';
@@ -1,5 +1,5 @@
1
1
  import { TileflowNativeSourceState } from '@tileflow/core/native';
2
- import { x as MapSourceState } from '../contract-BWT1jxqg.js';
2
+ import { x as MapSourceState } from '../contract-BWCbfMOx.js';
3
3
  import '@maplibre/maplibre-react-native';
4
4
  import 'react';
5
5
  import 'react-native';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tileflow/react-native",
3
- "version": "0.1.0-alpha.8",
3
+ "version": "0.1.0-alpha.9",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Tileflow maps, camera control and semantic interactions for React Native.",
6
6
  "type": "module",
@@ -35,8 +35,8 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@tileflow/core": ">=0.1.0-alpha.43 <0.1.0-beta.0",
39
- "@tileflow/interactions": ">=0.1.0-alpha.7 <0.1.0-beta.0"
38
+ "@tileflow/core": ">=0.1.0-alpha.48 <0.1.0-beta.0",
39
+ "@tileflow/interactions": ">=0.1.0-alpha.8 <0.1.0-beta.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@maplibre/maplibre-react-native": "11.5.0",