Visitar URL original
Chore(google map): update to latest by herefishyfish · Pull Request #669 · NativeScript/plugins · GitHub
Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(google-map-utils): better handling of events
  • Loading branch information
herefishyfish committed Jul 23, 2026
commit 5ef58440da704f91f75773cc66f65c030fb7b8be
70 changes: 61 additions & 9 deletions packages/google-maps-utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,25 @@ const heatmapProvider = map.heatmapProvider(heatmapOptions);

### HeatmapOptions

| Property | Type
|:---------|:-----
| `coordinates` | `Coordinate[]`;
| `opacity` | `number`;
| `radius` | `number`;
| `maxIntensity` | `number`;
| `gradient` | ` IGradient[]`;
| Property | Type | Description
|:---------|:-----|:-----------
| `coordinates` | `Coordinate[]` | Uniform-weight points (each treated as intensity `1`)
| `weightedData` | `WeightedLatLng[]` | Weighted points; combined with `coordinates` if both are set
| `opacity` | `number` |
| `radius` | `number` |
| `maxIntensity` | `number` |
| `gradient` | `IGradient[]` |

A `WeightedLatLng` is `{ coordinate: Coordinate; intensity?: number }` (intensity defaults to `1`). Weighted data can also be set later with `provider.setWeightedData(data)`.

```javascript
const provider = new HeatmapTileProvider({
weightedData: [
{ coordinate: { lat: -33.87, lng: 151.2 }, intensity: 5 },
{ coordinate: { lat: -33.88, lng: 151.21 }, intensity: 1 },
],
});
```

---

Expand All @@ -64,6 +76,34 @@ addClusteredMarkers(map: GoogleMap, markers: MarkerOptions[]) {
}
```

`clusterManager()` takes an optional `ClusterManagerOptions`:

| Option | Type | Description
|:-------|:-----|:----------
| `algorithm` | `'distance'` \| `'grid'` | Clustering algorithm (default `'distance'`)
| `minClusterSize` | `number` | Minimum items before they render as a cluster
| `animate` | `boolean` | Whether clusters animate on zoom
| `animationDuration` | `number` | Animation duration in milliseconds

```javascript
map.clusterManager(markers, { algorithm: 'grid', minClusterSize: 3, animationDuration: 200 });
```

`ClusterManager` is an `Observable` and emits two events:

| Event | Data | Fires when
|:------|:-----|:----------
| `clusterTap` | `ClusterTapEventData` (`position`, `size`, `items`) | A cluster (group of items) is tapped
| `clusterItemTap` | `ClusterItemTapEventData` (`item`) | A single, unclustered item is tapped

```javascript
import { ClusterManagerBase, ClusterTapEventData } from '@nativescript/google-maps-utils';

clusterManager.on(ClusterManagerBase.clusterTapEvent, (args: ClusterTapEventData) => {
console.log(`${args.size} items at ${args.position.lat}, ${args.position.lng}`);
});
```

---

## Geometry Utilities
Expand Down Expand Up @@ -162,6 +202,12 @@ layer.removeLayerFromMap();
### Styling
`IGeometryStyle` options: `strokeColor`, `fillColor`, `width`, `heading`, `anchor`, `title` work on both platforms; `scale` and `iconUrl` are iOS-only.

### Inspecting layers

- `geoJsonLayer.boundingBox` / `feature.boundingBox` — a `{ southwest, northeast }` box, on both platforms.
- `geoJsonLayer.addFeature(feature)` / `removeFeature(feature)` — **Android only** (a no-op warning on iOS).
- `kmlLayer.containers` (`KmlContainer[]`), `kmlLayer.groundOverlays` (`KmlGroundOverlay[]`), `kmlLayer.hasGroundOverlays()` — **Android only** (empty / `false` on iOS, which the GMU library does not expose).

### Feature tap events
Both layers emit a `featureTap` event when a rendered feature is tapped:
```javascript
Expand All @@ -172,12 +218,18 @@ layer.on(GeoJsonLayer.featureTapEvent, (args: FeatureTapEventData) => {
console.log(args.feature.id, args.feature.geometry?.type, args.feature.properties);
});
```
The same works for `KmlLayer` (`KmlLayer.featureTapEvent`). On Android this wraps the native `Layer.OnFeatureClickListener`; on iOS the layer chains onto the map's delegate and matches taps back to the parsed features (all other map events keep working).
The same works for `KmlLayer` (`KmlLayer.featureTapEvent`). On both platforms the tapped overlay is matched back to the parsed feature by geometry, and **all the underlying `@nativescript/google-maps` map events keep firing** (see the note below).

---

## Listener fan-out (map events keep working)

The native map exposes single-slot listeners (one camera-idle listener, one marker-click listener, etc.), and the underlying maps-utils libraries replace them when a data layer or cluster manager is added. To stop add-ons from clobbering each other — or the base plugin — `@nativescript/google-maps` owns those slots through a shared **listener hub** and fans each event out to every registered listener.

The upshot: you can add a `ClusterManager` **and** a GeoJSON/KML layer, and the map's own `cameraPosition`, `markerTap`, `polygon` and `polyline` events still fire, alongside each add-on's `featureTap` / `clusterTap` events. (The demo app's google-maps-utils page wires all of these to the console so the behavior can be verified on-device.)

## Platform notes
- **Android single-slot listeners:** the native maps-utils library installs its own map listeners, which replace the ones `@nativescript/google-maps` registers. Adding a `ClusterManager` replaces the camera-idle listener (the map's `cameraPosition` event stops firing while clustering is active), and adding a GeoJSON/KML data layer replaces the marker/polygon/polyline click listeners (the map's `markerTap`/`polygon`/`polyline` events stop firing while a data layer is on the map). iOS is unaffected — the wrappers there chain onto the existing delegate.
- **iOS `fromNative` wrappers:** `GeoJsonLayer.fromNative` / `KmlLayer.fromNative` cannot expose `features` or `featureTap` (the GMU renderer does not expose its source features). On Android, `fromNative` layers likewise skip `featureTap` wiring (no map reference is available).
- **iOS `fromNative` wrappers:** `GeoJsonLayer.fromNative` / `KmlLayer.fromNative` cannot expose `features` or `featureTap` (the GMU renderer does not expose its source features). Android has no such limitation.
- **`ClusterManager.setRenderer`** is a no-op on iOS — `GMUClusterManager` takes its renderer when it is created.
- **KML on iOS:** `KmlFeature.id` is always `null` and `properties` is synthesized as `{ name, description }` from the placemark title/snippet (GMU exposes neither a property bag nor an identifier).
Expand Down
48 changes: 48 additions & 0 deletions packages/google-maps-utils/clustering/common.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
import { EventData, Observable } from '@nativescript/core';
import { Coordinate } from '@nativescript/google-maps';

/**
* Configuration for a cluster manager, passed to `map.clusterManager(markers, options)`.
*/
export interface ClusterManagerOptions {
/**
* Clustering algorithm. `'distance'` (default) groups nearby items; `'grid'` snaps items to a
* fixed grid (cheaper, coarser).
*/
algorithm?: 'distance' | 'grid';
/** Minimum number of items before they render as a cluster rather than individual markers. */
minClusterSize?: number;
/** Whether clusters animate as they split/merge on zoom. Defaults to the native default (on). */
animate?: boolean;
/** Cluster animation duration in milliseconds. */
animationDuration?: number;
}

export interface IClusterManager<TClusterItem = any> {
addItems: (clusterItems: TClusterItem[]) => void;
addItem: (clusterItem: TClusterItem) => void;
Expand All @@ -10,3 +30,31 @@ export interface IClusterManager<TClusterItem = any> {
ios?: any;
native?: any;
}

/**
* Fired when a cluster (a group of items) is tapped.
*/
export interface ClusterTapEventData<TClusterItem = any> extends EventData {
/** Center position of the cluster. */
position: Coordinate;
/** Number of items in the cluster. */
size: number;
/** The items contained in the cluster. */
items: TClusterItem[];
}

/**
* Fired when a single (unclustered) item is tapped.
*/
export interface ClusterItemTapEventData<TClusterItem = any> extends EventData {
item: TClusterItem;
}

/**
* Base class for the platform `ClusterManager`s. Extends `Observable` so consumers
* can listen for `clusterTap` / `clusterItemTap`.
*/
export abstract class ClusterManagerBase extends Observable {
static clusterTapEvent = 'clusterTap';
static clusterItemTapEvent = 'clusterItemTap';
}
73 changes: 62 additions & 11 deletions packages/google-maps-utils/clustering/index.android.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { ImageSource, Utils } from '@nativescript/core';
import { GoogleMap, MarkerOptions } from '@nativescript/google-maps';
import { hueFromColor } from '@nativescript/google-maps/utils';
import { addOnCameraIdleListener, addOnMarkerClickListener, hueFromColor } from '@nativescript/google-maps/utils';
import { intoNativeColor } from '../utils/common';
import { IClusterManager } from './common';
import { ClusterItemTapEventData, ClusterManagerBase, ClusterManagerOptions, ClusterTapEventData, IClusterManager } from './common';

export * from './common';

export function intoNativeClusterManager(map: GoogleMap) {
const manager = new com.google.maps.android.clustering.ClusterManager(Utils.ad.getApplicationContext(), map.native);
export function intoNativeClusterManager(map: GoogleMap, options?: ClusterManagerOptions) {
// Listener wiring is done in ClusterManager.fromNative via the shared listener hub, so the
// manager no longer clobbers the camera-idle/marker-click listeners google-maps installs.
const manager = new com.google.maps.android.clustering.ClusterManager<com.google.maps.android.clustering.ClusterItem>(Utils.ad.getApplicationContext(), map.native);

if (map?.native?.setOnCameraIdleListener) {
// NOTE: ClusterManager must be the map's OnCameraIdleListener to re-cluster
// on camera changes. This is a single-slot native API, so it replaces the
// listener @nativescript/google-maps registers for its cameraPosition event.
map.native.setOnCameraIdleListener(manager);
if (options?.algorithm === 'grid') {
manager.setAlgorithm(new com.google.maps.android.clustering.algo.GridBasedAlgorithm<com.google.maps.android.clustering.ClusterItem>());
}
if (typeof options?.animate === 'boolean') {
manager.setAnimation(options.animate);
}

return manager;
Expand Down Expand Up @@ -123,18 +125,67 @@ export class ClusterRenderer extends com.google.maps.android.clustering.view.Def
}
}

export class ClusterManager implements IClusterManager<ClusterItem> {
export class ClusterManager extends ClusterManagerBase implements IClusterManager<ClusterItem> {
#native: com.google.maps.android.clustering.ClusterManager<com.google.maps.android.clustering.ClusterItem>;

static fromNative(nativeClusterManager: com.google.maps.android.clustering.ClusterManager<any>) {
static fromNative(nativeClusterManager: com.google.maps.android.clustering.ClusterManager<any>, map?: GoogleMap) {
if (nativeClusterManager instanceof com.google.maps.android.clustering.ClusterManager) {
const clusterManager = new ClusterManager();
clusterManager.#native = nativeClusterManager;
if (map?.native) {
// The native ClusterManager is itself the map's OnCameraIdleListener and
// OnMarkerClickListener; register it as a hub secondary so it re-clusters and consumes
// cluster-marker taps without clobbering google-maps' own listeners.
addOnCameraIdleListener(map.native, nativeClusterManager as any);
addOnMarkerClickListener(map.native, nativeClusterManager as any);

nativeClusterManager.setOnClusterClickListener(
new com.google.maps.android.clustering.ClusterManager.OnClusterClickListener<com.google.maps.android.clustering.ClusterItem>({
onClusterClick: (cluster) => {
clusterManager.#notifyCluster(cluster);
return false;
},
}),
);
nativeClusterManager.setOnClusterItemClickListener(
new com.google.maps.android.clustering.ClusterManager.OnClusterItemClickListener<com.google.maps.android.clustering.ClusterItem>({
onClusterItemClick: (item) => {
clusterManager.#notifyItem(item);
return false;
},
}),
);
}
return clusterManager;
}
return null;
}

#notifyCluster(cluster: com.google.maps.android.clustering.Cluster<com.google.maps.android.clustering.ClusterItem>) {
const items: ClusterItem[] = [];
const iter = cluster.getItems().iterator();
while (iter.hasNext()) {
// items were added as JS ClusterItem instances and come back as the same objects.
items.push(iter.next() as unknown as ClusterItem);
}
const position = cluster.getPosition();
this.notify(<ClusterTapEventData<ClusterItem>>{
eventName: ClusterManagerBase.clusterTapEvent,
object: this,
position: { lat: position.latitude, lng: position.longitude },
size: cluster.getSize(),
items,
});
}

#notifyItem(item: com.google.maps.android.clustering.ClusterItem) {
this.notify(<ClusterItemTapEventData<ClusterItem>>{
eventName: ClusterManagerBase.clusterItemTapEvent,
object: this,
item: item as unknown as ClusterItem,
});
}

get native() {
return this.#native;
}
Expand Down
8 changes: 4 additions & 4 deletions packages/google-maps-utils/clustering/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { GoogleMap, MarkerOptions } from '@nativescript/google-maps';
import { IClusterManager } from './common';
import { ClusterManagerBase, ClusterManagerOptions, IClusterManager } from './common';

export * from './common';

export function intoNativeClusterManager(map: GoogleMap);
export function intoNativeClusterManager(map: GoogleMap, options?: ClusterManagerOptions);

export class ClusterItem {
constructor(options: MarkerOptions);
Expand All @@ -24,8 +24,8 @@ export class ClusterRenderer {
native?: any;
}

export class ClusterManager implements IClusterManager<ClusterItem> {
static fromNative: (nativeClusterManager: any) => ClusterManager | null;
export class ClusterManager extends ClusterManagerBase implements IClusterManager<ClusterItem> {
static fromNative: (nativeClusterManager: any, map?: GoogleMap) => ClusterManager | null;
addItems: (clusterItems: ClusterItem[]) => void;
addItem: (clusterItem: ClusterItem) => void;
removeItems: (clusterItems: ClusterItem[]) => void;
Expand Down
Loading