From 3e340255bef40edbaa028be8049454f2c8105743 Mon Sep 17 00:00:00 2001 From: Nikischin Date: Mon, 4 Nov 2024 21:40:54 +0100 Subject: [PATCH 1/9] WiP Not refreshing --- src/components/Annotation.tsx | 4 +- src/components/AnnotationCluster.tsx | 81 +++++++++++++++++++++++ src/components/AnnotationClusterProps.tsx | 7 ++ src/components/Marker.tsx | 4 +- src/stories/Annotation.stories.tsx | 65 +++++++++++++++++- src/stories/Marker.stories.tsx | 61 ++++++++++++++++- 6 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 src/components/AnnotationCluster.tsx create mode 100644 src/components/AnnotationClusterProps.tsx diff --git a/src/components/Annotation.tsx b/src/components/Annotation.tsx index c5e541f..bf194fe 100644 --- a/src/components/Annotation.tsx +++ b/src/components/Annotation.tsx @@ -12,6 +12,7 @@ import AnnotationProps from './AnnotationProps'; import forwardMapkitEvent from '../util/forwardMapkitEvent'; import CalloutContainer from './CalloutContainer'; import { toMapKitDisplayPriority } from '../util/parameters'; +import { AnnotationClusterContext } from './AnnotationCluster'; export default function Annotation({ latitude, @@ -42,7 +43,7 @@ export default function Annotation({ appearanceAnimation = '', visible = true, - clusteringIdentifier = null, + clusteringIdentifier: deprecatedClusterIdentifier = null, displayPriority = undefined, collisionMode = undefined, @@ -63,6 +64,7 @@ export default function Annotation({ const [annotation, setAnnotation] = useState(null); const contentEl = useMemo(() => document.createElement('div'), []); const map = useContext(MapContext); + const clusteringIdentifier = useContext(AnnotationClusterContext) ?? deprecatedClusterIdentifier; // Padding useEffect(() => { diff --git a/src/components/AnnotationCluster.tsx b/src/components/AnnotationCluster.tsx new file mode 100644 index 0000000..6c335c0 --- /dev/null +++ b/src/components/AnnotationCluster.tsx @@ -0,0 +1,81 @@ +import React, { + createContext, useContext, useEffect, useState, +} from 'react'; +import { createPortal } from 'react-dom'; +import AnnotationClusterProps from './AnnotationClusterProps'; +import MapContext from '../context/MapContext'; + +export const AnnotationClusterContext = createContext(null); + +export default function AnnotationCluster({ + children, + annotationForCluster, + clusterIdenfiier, +}: AnnotationClusterProps) { + const map = useContext(MapContext); + const [ + existingClusterFunc, setExistingClusterFunc, + ] = useState<((clusterAnnotation: mapkit.Annotation) => void) | undefined>(undefined); + + // Coordinates + useEffect(() => { + if (map === null) return undefined; + + if (existingClusterFunc === undefined) { + setExistingClusterFunc(map.annotationForCluster); + } + + map.annotationForCluster = (clusterAnnotation) => { + if (clusterAnnotation.clusteringIdentifier === clusterIdenfiier) { + if (annotationForCluster) { + const annotation = annotationForCluster(clusterAnnotation.memberAnnotations); + const { children: annotationChildren } = annotation; + delete annotation.children; + if (annotationChildren) { + const contentEl = document.createElement('div'); + createPortal(annotationChildren, contentEl); + + const a = new mapkit.Annotation( + new mapkit.Coordinate( + clusterAnnotation.coordinate.latitude, + clusterAnnotation.coordinate.longitude, + ), + () => contentEl, + ); + + /* Object.assign( + a, + annotation, + ); */ + + return a; + } + + Object.assign( + clusterAnnotation, + annotation, + ); + } + return clusterAnnotation; + } + + return existingClusterFunc ? existingClusterFunc(clusterAnnotation) : clusterAnnotation; + }; + + return () => { + if (existingClusterFunc === undefined && map.annotationForCluster !== undefined) { + // @ts-ignore + delete map.annotationForCluster; + } else { + // @ts-ignore + map.annotationForCluster = existingClusterFunc; + } + }; + }, [map]); + + return ( + + {children} + + ); +} diff --git a/src/components/AnnotationClusterProps.tsx b/src/components/AnnotationClusterProps.tsx new file mode 100644 index 0000000..cc1efee --- /dev/null +++ b/src/components/AnnotationClusterProps.tsx @@ -0,0 +1,7 @@ +export default interface AnnotationClusterProps { + annotationForCluster: ( + memberAnnotations: mapkit.Annotation[] + ) => Partial & { children?: React.ReactNode }; + children: React.ReactNode; + clusterIdenfiier: string; +} diff --git a/src/components/Marker.tsx b/src/components/Marker.tsx index cd01c62..203ad3d 100644 --- a/src/components/Marker.tsx +++ b/src/components/Marker.tsx @@ -7,6 +7,7 @@ import { FeatureVisibility, toMapKitDisplayPriority, toMapKitFeatureVisibility } import MarkerProps from './MarkerProps'; import forwardMapkitEvent from '../util/forwardMapkitEvent'; import CalloutContainer from './CalloutContainer'; +import { AnnotationClusterContext } from './AnnotationCluster'; export default function Marker({ latitude, @@ -18,7 +19,7 @@ export default function Marker({ subtitleVisibility = FeatureVisibility.Adaptive, titleVisibility = FeatureVisibility.Adaptive, - clusteringIdentifier = null, + clusteringIdentifier: deprecatedClusterIdentifier = null, displayPriority = undefined, collisionMode = undefined, @@ -60,6 +61,7 @@ export default function Marker({ }: MarkerProps) { const [marker, setMarker] = useState(null); const map = useContext(MapContext); + const clusteringIdentifier = useContext(AnnotationClusterContext) ?? deprecatedClusterIdentifier; // Enum properties useEffect(() => { diff --git a/src/stories/Annotation.stories.tsx b/src/stories/Annotation.stories.tsx index b3ff26d..629c307 100644 --- a/src/stories/Annotation.stories.tsx +++ b/src/stories/Annotation.stories.tsx @@ -1,9 +1,10 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { Meta, StoryFn } from '@storybook/react'; import Map from '../components/Map'; import Annotation from '../components/Annotation'; import { CoordinateRegion, FeatureVisibility } from '../util/parameters'; +import AnnotationCluster from '../components/AnnotationCluster'; // @ts-ignore const token = import.meta.env.STORYBOOK_MAPKIT_JS_TOKEN!; @@ -170,3 +171,65 @@ export const CustomAnnotationCallout = () => { ); }; CustomAnnotationCallout.storyName = 'Annotation with custom callout element'; + +export const AnnotationClustering = () => { + const clusteringIdentifier = 'id'; + const [selected, setSelected] = useState(null); + + const initialRegion: CoordinateRegion = useMemo(() => ({ + centerLatitude: 46.20738751546706, + centerLongitude: 6.155891756231, + latitudeDelta: 1, + longitudeDelta: 1, + }), []); + + const coordinates = [ + { latitude: 46.20738751546706, longitude: 6.155891756231 }, + { latitude: 46.25738751546706, longitude: 6.185891756231 }, + { latitude: 46.28738751546706, longitude: 6.2091756231 }, + ]; + + return ( + <> + + ({ + title: 'GROUP', + subtitle: memberAnnotations + .reduce((total, clusterAnnotation) => `${total} & ${clusterAnnotation.title}`, ''), + children: ( +
+ Test +
+ ), + })} + > + {coordinates.map(({ latitude, longitude }, index) => ( + setSelected(index + 1)} + onDeselect={() => setSelected(null)} + collisionMode="Circle" + displayPriority={750} + key={index} + > + + + ))} +
+
+ +
+
+

{selected ? `Selected annotation #${selected}` : 'Not selected'}

+
+
+ + ); +}; + +AnnotationClustering.storyName = 'Clustering three annotations into one'; diff --git a/src/stories/Marker.stories.tsx b/src/stories/Marker.stories.tsx index 625004e..0a6879a 100644 --- a/src/stories/Marker.stories.tsx +++ b/src/stories/Marker.stories.tsx @@ -4,6 +4,7 @@ import './stories.css'; import Map from '../components/Map'; import Marker from '../components/Marker'; import { CoordinateRegion, FeatureVisibility } from '../util/parameters'; +import AnnotationCluster from '../components/AnnotationCluster'; // @ts-ignore const token = import.meta.env.STORYBOOK_MAPKIT_JS_TOKEN!; @@ -152,7 +153,7 @@ export const MoveableMarker = () => { ); }; -export const MarkerClustering = () => { +export const MarkerClusteringOld = () => { const clusteringIdentifier = 'id'; const [selected, setSelected] = useState(null); @@ -198,6 +199,64 @@ export const MarkerClustering = () => { ); }; +MarkerClusteringOld.storyName = 'Clustering three markers into one (old)'; + +export const MarkerClustering = () => { + const clusteringIdentifier = 'id'; + const [selected, setSelected] = useState(null); + + const initialRegion: CoordinateRegion = useMemo(() => ({ + centerLatitude: 46.20738751546706, + centerLongitude: 6.155891756231, + latitudeDelta: 1, + longitudeDelta: 1, + }), []); + + const coordinates = [ + { latitude: 46.20738751546706, longitude: 6.155891756231 }, + { latitude: 46.25738751546706, longitude: 6.185891756231 }, + { latitude: 46.28738751546706, longitude: 6.2091756231 }, + ]; + + return ( + <> + + ({ + title: 'GROUP', + subtitle: memberAnnotations + .reduce((total, clusterAnnotation) => `${total} & ${clusterAnnotation.title}`, ''), + })} + > + + { + coordinates.map(({ latitude, longitude }, index) => ( + setSelected(index + 1)} + onDeselect={() => setSelected(null)} + collisionMode="Circle" + displayPriority={750} + key={index} + /> + )) + } + + + +
+
+

{selected ? `Selected marker #${selected}` : 'Not selected'}

+
+
+ + ); +}; + MarkerClustering.storyName = 'Clustering three markers into one'; function CustomCalloutElement( From 15ebef33f5f4cf4a067830a42a9e5b53ab2548ca Mon Sep 17 00:00:00 2001 From: Nikischin Date: Wed, 6 Nov 2024 23:43:44 +0100 Subject: [PATCH 2/9] Working implementation yay --- src/components/Annotation.tsx | 4 +- src/components/AnnotationCluster.tsx | 71 +++++++++++++---------- src/components/AnnotationClusterProps.tsx | 6 +- src/components/Marker.tsx | 4 +- src/stories/Annotation.stories.tsx | 34 ++++++----- src/stories/Marker.stories.tsx | 2 +- 6 files changed, 66 insertions(+), 55 deletions(-) diff --git a/src/components/Annotation.tsx b/src/components/Annotation.tsx index bf194fe..329859e 100644 --- a/src/components/Annotation.tsx +++ b/src/components/Annotation.tsx @@ -12,7 +12,7 @@ import AnnotationProps from './AnnotationProps'; import forwardMapkitEvent from '../util/forwardMapkitEvent'; import CalloutContainer from './CalloutContainer'; import { toMapKitDisplayPriority } from '../util/parameters'; -import { AnnotationClusterContext } from './AnnotationCluster'; +import { AnnotationClusterIdentifierContext } from './AnnotationCluster'; export default function Annotation({ latitude, @@ -64,7 +64,7 @@ export default function Annotation({ const [annotation, setAnnotation] = useState(null); const contentEl = useMemo(() => document.createElement('div'), []); const map = useContext(MapContext); - const clusteringIdentifier = useContext(AnnotationClusterContext) ?? deprecatedClusterIdentifier; + const clusteringIdentifier = useContext(AnnotationClusterIdentifierContext) ?? deprecatedClusterIdentifier; // Padding useEffect(() => { diff --git a/src/components/AnnotationCluster.tsx b/src/components/AnnotationCluster.tsx index 6c335c0..b02e843 100644 --- a/src/components/AnnotationCluster.tsx +++ b/src/components/AnnotationCluster.tsx @@ -1,22 +1,41 @@ import React, { - createContext, useContext, useEffect, useState, + createContext, useContext, useEffect, useMemo, useState, } from 'react'; -import { createPortal } from 'react-dom'; import AnnotationClusterProps from './AnnotationClusterProps'; import MapContext from '../context/MapContext'; -export const AnnotationClusterContext = createContext(null); +export const AnnotationClusterContext = createContext<{ + memberAnnotations: mapkit.Annotation[], coordinate: mapkit.Coordinate +}>({ + memberAnnotations: [], + // @ts-ignore + coordinate: undefined, +}); + +export const AnnotationClusterIdentifierContext = createContext(null); + +export const useClusterAnnotation = () => useContext(AnnotationClusterContext); export default function AnnotationCluster({ children, annotationForCluster, - clusterIdenfiier, + clusterIdenfier, }: AnnotationClusterProps) { + const contentEl = useMemo(() => document.createElement('div'), []); + const map = useContext(MapContext); const [ existingClusterFunc, setExistingClusterFunc, ] = useState<((clusterAnnotation: mapkit.Annotation) => void) | undefined>(undefined); + const [memberAnnotations, setMemberAnnotations] = useState([]); + const [clusterCoordinate, setClusterCoordinate] = useState(); + + const annotationContextValue = useMemo(() => ({ + memberAnnotations, + coordinate: clusterCoordinate, + }), [memberAnnotations, clusterCoordinate]); + // Coordinates useEffect(() => { if (map === null) return undefined; @@ -26,34 +45,18 @@ export default function AnnotationCluster({ } map.annotationForCluster = (clusterAnnotation) => { - if (clusterAnnotation.clusteringIdentifier === clusterIdenfiier) { + if (clusterAnnotation.clusteringIdentifier === clusterIdenfier) { if (annotationForCluster) { - const annotation = annotationForCluster(clusterAnnotation.memberAnnotations); - const { children: annotationChildren } = annotation; - delete annotation.children; - if (annotationChildren) { - const contentEl = document.createElement('div'); - createPortal(annotationChildren, contentEl); - - const a = new mapkit.Annotation( - new mapkit.Coordinate( - clusterAnnotation.coordinate.latitude, - clusterAnnotation.coordinate.longitude, - ), - () => contentEl, - ); - - /* Object.assign( - a, - annotation, - ); */ - - return a; - } + setMemberAnnotations(clusterAnnotation.memberAnnotations); + setClusterCoordinate(clusterAnnotation.coordinate); - Object.assign( - clusterAnnotation, - annotation, + // Return an empty annotation to remove the default cluster + return new mapkit.Annotation( + new mapkit.Coordinate( + clusterAnnotation.coordinate.latitude, + clusterAnnotation.coordinate.longitude, + ), + () => contentEl, ); } return clusterAnnotation; @@ -74,8 +77,12 @@ export default function AnnotationCluster({ }, [map]); return ( - - {children} + // @ts-ignore + + {clusterCoordinate ? annotationForCluster : null} + + {children} + ); } diff --git a/src/components/AnnotationClusterProps.tsx b/src/components/AnnotationClusterProps.tsx index cc1efee..e4b928e 100644 --- a/src/components/AnnotationClusterProps.tsx +++ b/src/components/AnnotationClusterProps.tsx @@ -1,7 +1,5 @@ export default interface AnnotationClusterProps { - annotationForCluster: ( - memberAnnotations: mapkit.Annotation[] - ) => Partial & { children?: React.ReactNode }; + annotationForCluster?: React.ReactNode; children: React.ReactNode; - clusterIdenfiier: string; + clusterIdenfier: string; } diff --git a/src/components/Marker.tsx b/src/components/Marker.tsx index 203ad3d..93f1bea 100644 --- a/src/components/Marker.tsx +++ b/src/components/Marker.tsx @@ -7,7 +7,7 @@ import { FeatureVisibility, toMapKitDisplayPriority, toMapKitFeatureVisibility } import MarkerProps from './MarkerProps'; import forwardMapkitEvent from '../util/forwardMapkitEvent'; import CalloutContainer from './CalloutContainer'; -import { AnnotationClusterContext } from './AnnotationCluster'; +import { AnnotationClusterIdentifierContext } from './AnnotationCluster'; export default function Marker({ latitude, @@ -61,7 +61,7 @@ export default function Marker({ }: MarkerProps) { const [marker, setMarker] = useState(null); const map = useContext(MapContext); - const clusteringIdentifier = useContext(AnnotationClusterContext) ?? deprecatedClusterIdentifier; + const clusteringIdentifier = useContext(AnnotationClusterIdentifierContext) ?? deprecatedClusterIdentifier; // Enum properties useEffect(() => { diff --git a/src/stories/Annotation.stories.tsx b/src/stories/Annotation.stories.tsx index 377dfb5..f168f32 100644 --- a/src/stories/Annotation.stories.tsx +++ b/src/stories/Annotation.stories.tsx @@ -5,13 +5,13 @@ import { fn } from '@storybook/test'; import Map from '../components/Map'; import Annotation from '../components/Annotation'; import { CoordinateRegion, FeatureVisibility } from '../util/parameters'; -import AnnotationCluster from '../components/AnnotationCluster'; +import AnnotationCluster, { useClusterAnnotation } from '../components/AnnotationCluster'; // @ts-ignore const token = import.meta.env.STORYBOOK_MAPKIT_JS_TOKEN!; // SVG from https://webkul.github.io/vivid -function CustomMarker() { +function CustomMarker({ color = '#FF6E6E' }: { color?: string }) { return ( { }; CustomAnnotationCallout.storyName = 'Annotation with custom callout element'; +function ClusterAnnotation() { + const { coordinate, memberAnnotations } = useClusterAnnotation(); + + return ( + clusterAnnotation.title).join(' & ')} + > + + + ); +} + export const AnnotationClustering = () => { const clusteringIdentifier = 'id'; const [selected, setSelected] = useState(null); @@ -242,17 +257,8 @@ export const AnnotationClustering = () => { <> ({ - title: 'GROUP', - subtitle: memberAnnotations - .reduce((total, clusterAnnotation) => `${total} & ${clusterAnnotation.title}`, ''), - children: ( -
- Test -
- ), - })} + clusterIdenfier={clusteringIdentifier} + annotationForCluster={} > {coordinates.map(({ latitude, longitude }, index) => ( { <> ({ title: 'GROUP', subtitle: memberAnnotations From dcd06726dc38221e428c985180969938d5b9838e Mon Sep 17 00:00:00 2001 From: Nikischin Date: Wed, 6 Nov 2024 23:55:00 +0100 Subject: [PATCH 3/9] Forgot to update the Marker example (with default cluster marker) --- src/stories/Marker.stories.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/stories/Marker.stories.tsx b/src/stories/Marker.stories.tsx index 0429b52..afc1bc1 100644 --- a/src/stories/Marker.stories.tsx +++ b/src/stories/Marker.stories.tsx @@ -234,11 +234,6 @@ export const MarkerClustering = () => { ({ - title: 'GROUP', - subtitle: memberAnnotations - .reduce((total, clusterAnnotation) => `${total} & ${clusterAnnotation.title}`, ''), - })} > { From 76a22c3a88fc3619a43d6133be69c409100cbc8a Mon Sep 17 00:00:00 2001 From: Nikischin Date: Thu, 7 Nov 2024 01:04:26 +0100 Subject: [PATCH 4/9] Buggy implementation with native clusterAnnotation --- src/components/Annotation.tsx | 18 ++++-- src/components/AnnotationCluster.tsx | 75 +++++++++++++---------- src/components/AnnotationClusterProps.tsx | 5 +- src/components/Marker.tsx | 13 ++-- src/stories/Annotation.stories.tsx | 32 +++++----- 5 files changed, 81 insertions(+), 62 deletions(-) diff --git a/src/components/Annotation.tsx b/src/components/Annotation.tsx index 329859e..7fcfa86 100644 --- a/src/components/Annotation.tsx +++ b/src/components/Annotation.tsx @@ -12,7 +12,7 @@ import AnnotationProps from './AnnotationProps'; import forwardMapkitEvent from '../util/forwardMapkitEvent'; import CalloutContainer from './CalloutContainer'; import { toMapKitDisplayPriority } from '../util/parameters'; -import { AnnotationClusterIdentifierContext } from './AnnotationCluster'; +import { AnnotationClusterIdentifierContext, ClusterAnnotationContext } from './AnnotationCluster'; export default function Annotation({ latitude, @@ -64,6 +64,7 @@ export default function Annotation({ const [annotation, setAnnotation] = useState(null); const contentEl = useMemo(() => document.createElement('div'), []); const map = useContext(MapContext); + const clusterAnnotation = useContext(ClusterAnnotationContext); const clusteringIdentifier = useContext(AnnotationClusterIdentifierContext) ?? deprecatedClusterIdentifier; // Padding @@ -225,11 +226,18 @@ export default function Annotation({ new mapkit.Coordinate(latitude, longitude), () => contentEl, ); - map.addAnnotation(a); - setAnnotation(a); + + if (clusterAnnotation !== undefined) { + setAnnotation(clusterAnnotation); + } else { + map.addAnnotation(a); + setAnnotation(a); + } return () => { - map.removeAnnotation(a); + if (!clusterAnnotation) { + map.removeAnnotation(a); + } }; }, [map, latitude, longitude]); @@ -272,7 +280,7 @@ export default function Annotation({ , document.body, )} - {createPortal(children, contentEl)} + {clusterAnnotation !== undefined ? children : createPortal(children, contentEl)} ); } diff --git a/src/components/AnnotationCluster.tsx b/src/components/AnnotationCluster.tsx index b02e843..222376d 100644 --- a/src/components/AnnotationCluster.tsx +++ b/src/components/AnnotationCluster.tsx @@ -1,20 +1,12 @@ import React, { createContext, useContext, useEffect, useMemo, useState, } from 'react'; +import { createPortal } from 'react-dom'; import AnnotationClusterProps from './AnnotationClusterProps'; import MapContext from '../context/MapContext'; -export const AnnotationClusterContext = createContext<{ - memberAnnotations: mapkit.Annotation[], coordinate: mapkit.Coordinate -}>({ - memberAnnotations: [], - // @ts-ignore - coordinate: undefined, -}); - export const AnnotationClusterIdentifierContext = createContext(null); - -export const useClusterAnnotation = () => useContext(AnnotationClusterContext); +export const ClusterAnnotationContext = createContext(undefined); export default function AnnotationCluster({ children, @@ -30,11 +22,7 @@ export default function AnnotationCluster({ const [memberAnnotations, setMemberAnnotations] = useState([]); const [clusterCoordinate, setClusterCoordinate] = useState(); - - const annotationContextValue = useMemo(() => ({ - memberAnnotations, - coordinate: clusterCoordinate, - }), [memberAnnotations, clusterCoordinate]); + const [clusterAnnotation, setClusterAnnotation] = useState(); // Coordinates useEffect(() => { @@ -44,25 +32,35 @@ export default function AnnotationCluster({ setExistingClusterFunc(map.annotationForCluster); } - map.annotationForCluster = (clusterAnnotation) => { - if (clusterAnnotation.clusteringIdentifier === clusterIdenfier) { - if (annotationForCluster) { - setMemberAnnotations(clusterAnnotation.memberAnnotations); - setClusterCoordinate(clusterAnnotation.coordinate); + map.annotationForCluster = (clusterAnnotationData) => { + if (clusterAnnotationData.clusteringIdentifier === clusterIdenfier) { + setMemberAnnotations(clusterAnnotationData.memberAnnotations); + setClusterCoordinate(clusterAnnotationData.coordinate); - // Return an empty annotation to remove the default cluster - return new mapkit.Annotation( - new mapkit.Coordinate( - clusterAnnotation.coordinate.latitude, - clusterAnnotation.coordinate.longitude, - ), - () => contentEl, - ); + if (annotationForCluster) { + if (!clusterAnnotation) { + const a = new mapkit.Annotation( + new mapkit.Coordinate( + clusterAnnotationData.coordinate.latitude, + clusterAnnotationData.coordinate.longitude, + ), + () => contentEl, + ); + setClusterAnnotation(a); + return a; + } + return clusterAnnotation; } - return clusterAnnotation; + + return clusterAnnotationData; } - return existingClusterFunc ? existingClusterFunc(clusterAnnotation) : clusterAnnotation; + setMemberAnnotations([]); + setClusterCoordinate(undefined); + + return existingClusterFunc + ? existingClusterFunc(clusterAnnotationData) + : clusterAnnotationData; }; return () => { @@ -77,12 +75,21 @@ export default function AnnotationCluster({ }, [map]); return ( - // @ts-ignore - - {clusterCoordinate ? annotationForCluster : null} + <> + {clusterCoordinate && annotationForCluster && clusterAnnotation + ? createPortal( + + {annotationForCluster( + memberAnnotations, + clusterCoordinate, + )} + , + contentEl, + ) + : null} {children} - + ); } diff --git a/src/components/AnnotationClusterProps.tsx b/src/components/AnnotationClusterProps.tsx index e4b928e..ca81ef0 100644 --- a/src/components/AnnotationClusterProps.tsx +++ b/src/components/AnnotationClusterProps.tsx @@ -1,5 +1,8 @@ export default interface AnnotationClusterProps { - annotationForCluster?: React.ReactNode; + annotationForCluster?: ( + memberAnnotations: mapkit.Annotation[], + coordinate: mapkit.Coordinate, + ) => React.ReactNode; children: React.ReactNode; clusterIdenfier: string; } diff --git a/src/components/Marker.tsx b/src/components/Marker.tsx index 93f1bea..68a1950 100644 --- a/src/components/Marker.tsx +++ b/src/components/Marker.tsx @@ -7,7 +7,7 @@ import { FeatureVisibility, toMapKitDisplayPriority, toMapKitFeatureVisibility } import MarkerProps from './MarkerProps'; import forwardMapkitEvent from '../util/forwardMapkitEvent'; import CalloutContainer from './CalloutContainer'; -import { AnnotationClusterIdentifierContext } from './AnnotationCluster'; +import { AnnotationClusterIdentifierContext, ClusterAnnotationContext } from './AnnotationCluster'; export default function Marker({ latitude, @@ -61,6 +61,7 @@ export default function Marker({ }: MarkerProps) { const [marker, setMarker] = useState(null); const map = useContext(MapContext); + const clusterAnnotation = useContext(ClusterAnnotationContext); const clusteringIdentifier = useContext(AnnotationClusterIdentifierContext) ?? deprecatedClusterIdentifier; // Enum properties @@ -235,13 +236,17 @@ export default function Marker({ const m = new mapkit.MarkerAnnotation( new mapkit.Coordinate(latitude, longitude), ); - map.addAnnotation(m); - setMarker(m); + if (clusterAnnotation !== undefined) { + setMarker(clusterAnnotation); + } else { + map.addAnnotation(m); + setMarker(m); + } return () => { map.removeAnnotation(m); }; - }, [map, latitude, longitude]); + }, [map, latitude, longitude, clusterAnnotation]); return createPortal(
diff --git a/src/stories/Annotation.stories.tsx b/src/stories/Annotation.stories.tsx index f168f32..66b9b51 100644 --- a/src/stories/Annotation.stories.tsx +++ b/src/stories/Annotation.stories.tsx @@ -1,11 +1,11 @@ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { Meta, StoryFn } from '@storybook/react'; import { fn } from '@storybook/test'; import Map from '../components/Map'; import Annotation from '../components/Annotation'; import { CoordinateRegion, FeatureVisibility } from '../util/parameters'; -import AnnotationCluster, { useClusterAnnotation } from '../components/AnnotationCluster'; +import AnnotationCluster from '../components/AnnotationCluster'; // @ts-ignore const token = import.meta.env.STORYBOOK_MAPKIT_JS_TOKEN!; @@ -221,21 +221,6 @@ export const CustomAnnotationCallout = () => { }; CustomAnnotationCallout.storyName = 'Annotation with custom callout element'; -function ClusterAnnotation() { - const { coordinate, memberAnnotations } = useClusterAnnotation(); - - return ( - clusterAnnotation.title).join(' & ')} - > - - - ); -} - export const AnnotationClustering = () => { const clusteringIdentifier = 'id'; const [selected, setSelected] = useState(null); @@ -253,12 +238,23 @@ export const AnnotationClustering = () => { { latitude: 46.28738751546706, longitude: 6.2091756231 }, ]; + const annotationClusterFunc = useCallback((memberAnnotations: mapkit.Annotation[], coordinate: mapkit.Coordinate) => ( + clusterAnnotation.title).join(' & ')} + > + + + ), []); + return ( <> } + annotationForCluster={annotationClusterFunc} > {coordinates.map(({ latitude, longitude }, index) => ( Date: Thu, 7 Nov 2024 20:45:49 +0100 Subject: [PATCH 5/9] Mounting and unmount works but interactions does not --- src/components/Annotation.tsx | 3 +- src/components/AnnotationCluster.tsx | 81 ++++++++++++++++------------ src/stories/Annotation.stories.tsx | 11 ++-- src/util/forwardMapkitEvent.ts | 1 - 4 files changed, 55 insertions(+), 41 deletions(-) diff --git a/src/components/Annotation.tsx b/src/components/Annotation.tsx index 7fcfa86..81cca30 100644 --- a/src/components/Annotation.tsx +++ b/src/components/Annotation.tsx @@ -93,7 +93,6 @@ export default function Annotation({ // Callout useLayoutEffect(() => { if (!annotation) return; - const callOutObj: mapkit.AnnotationCalloutDelegate = {}; if (calloutElement && calloutElementRef.current !== null) { // @ts-expect-error @@ -239,7 +238,7 @@ export default function Annotation({ map.removeAnnotation(a); } }; - }, [map, latitude, longitude]); + }, [map, latitude, longitude, clusterAnnotation]); return ( <> diff --git a/src/components/AnnotationCluster.tsx b/src/components/AnnotationCluster.tsx index 222376d..23815ec 100644 --- a/src/components/AnnotationCluster.tsx +++ b/src/components/AnnotationCluster.tsx @@ -1,5 +1,5 @@ import React, { - createContext, useContext, useEffect, useMemo, useState, + createContext, Fragment, useContext, useEffect, useState, } from 'react'; import { createPortal } from 'react-dom'; import AnnotationClusterProps from './AnnotationClusterProps'; @@ -13,16 +13,16 @@ export default function AnnotationCluster({ annotationForCluster, clusterIdenfier, }: AnnotationClusterProps) { - const contentEl = useMemo(() => document.createElement('div'), []); - const map = useContext(MapContext); const [ existingClusterFunc, setExistingClusterFunc, ] = useState<((clusterAnnotation: mapkit.Annotation) => void) | undefined>(undefined); - - const [memberAnnotations, setMemberAnnotations] = useState([]); - const [clusterCoordinate, setClusterCoordinate] = useState(); - const [clusterAnnotation, setClusterAnnotation] = useState(); + const [clusterAnnotations, setClusterAnnotations] = useState<{ + contentElement: HTMLDivElement, + annotation: mapkit.Annotation, + coordinate: mapkit.Coordinate, + memberAnnotations: mapkit.Annotation[] + }[]>([]); // Coordinates useEffect(() => { @@ -34,30 +34,35 @@ export default function AnnotationCluster({ map.annotationForCluster = (clusterAnnotationData) => { if (clusterAnnotationData.clusteringIdentifier === clusterIdenfier) { - setMemberAnnotations(clusterAnnotationData.memberAnnotations); - setClusterCoordinate(clusterAnnotationData.coordinate); - if (annotationForCluster) { - if (!clusterAnnotation) { - const a = new mapkit.Annotation( - new mapkit.Coordinate( - clusterAnnotationData.coordinate.latitude, - clusterAnnotationData.coordinate.longitude, - ), - () => contentEl, - ); - setClusterAnnotation(a); - return a; + const annotation = clusterAnnotations.find((a) => a.coordinate.latitude == clusterAnnotationData.coordinate.latitude + && a.coordinate.longitude == clusterAnnotationData.coordinate.longitude); + if (annotation) { + return annotation.annotation; } - return clusterAnnotation; + + const contentElement = document.createElement('div'); + const a = new mapkit.Annotation( + new mapkit.Coordinate( + clusterAnnotationData.coordinate.latitude, + clusterAnnotationData.coordinate.longitude, + ), + () => contentElement, + ); + + setClusterAnnotations((annotations) => [...annotations, { + contentElement, + annotation: a, + coordinate: clusterAnnotationData.coordinate, + memberAnnotations: clusterAnnotationData.memberAnnotations, + }]); + + return a; } return clusterAnnotationData; } - setMemberAnnotations([]); - setClusterCoordinate(undefined); - return existingClusterFunc ? existingClusterFunc(clusterAnnotationData) : clusterAnnotationData; @@ -74,19 +79,25 @@ export default function AnnotationCluster({ }; }, [map]); + console.log('clusterAnnotation', clusterAnnotations); + return ( <> - {clusterCoordinate && annotationForCluster && clusterAnnotation - ? createPortal( - - {annotationForCluster( - memberAnnotations, - clusterCoordinate, - )} - , - contentEl, - ) - : null} + {annotationForCluster && clusterAnnotations.map(({ + contentElement, annotation, coordinate, memberAnnotations, + }) => ( + + {createPortal( + + {annotationForCluster( + memberAnnotations, + coordinate, + )} + , + contentElement, + )} + + ))} {children} diff --git a/src/stories/Annotation.stories.tsx b/src/stories/Annotation.stories.tsx index 66b9b51..04d7b50 100644 --- a/src/stories/Annotation.stories.tsx +++ b/src/stories/Annotation.stories.tsx @@ -236,18 +236,23 @@ export const AnnotationClustering = () => { { latitude: 46.20738751546706, longitude: 6.155891756231 }, { latitude: 46.25738751546706, longitude: 6.185891756231 }, { latitude: 46.28738751546706, longitude: 6.2091756231 }, + { latitude: 46.20738751546706, longitude: 6.185891756231 }, + { latitude: 46.25738751546706, longitude: 6.2091756231 }, ]; const annotationClusterFunc = useCallback((memberAnnotations: mapkit.Annotation[], coordinate: mapkit.Coordinate) => ( clusterAnnotation.title).join(' & ')} + calloutElement={( +
{memberAnnotations.map((clusterAnnotation) => clusterAnnotation.title).join(' & ')}
+ )} + onSelect={() => setSelected(memberAnnotations.map((clusterAnnotation) => clusterAnnotation.title).join(' & '))} + selected={selected === memberAnnotations.map((clusterAnnotation) => clusterAnnotation.title).join(' & ')} >
- ), []); + ), [selected]); return ( <> diff --git a/src/util/forwardMapkitEvent.ts b/src/util/forwardMapkitEvent.ts index e1a9c47..b4217e3 100644 --- a/src/util/forwardMapkitEvent.ts +++ b/src/util/forwardMapkitEvent.ts @@ -17,7 +17,6 @@ export default function forwardMapkitEvent( ) { useEffect(() => { if (!element || !handler) return undefined; - // @ts-ignore const mapkitHandler = (e) => { handler(eventMap(e)); From 4a94aded7ca44453599647ce589b0102ccdf41e7 Mon Sep 17 00:00:00 2001 From: Nikischin Date: Tue, 23 Dec 2025 11:31:22 +0100 Subject: [PATCH 6/9] wip --- src/components/AnnotationCluster.tsx | 13 +++++++++---- src/components/AnnotationClusterProps.tsx | 1 - src/stories/Annotation.stories.tsx | 7 ++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/AnnotationCluster.tsx b/src/components/AnnotationCluster.tsx index 23815ec..1f939a1 100644 --- a/src/components/AnnotationCluster.tsx +++ b/src/components/AnnotationCluster.tsx @@ -1,5 +1,5 @@ import React, { - createContext, Fragment, useContext, useEffect, useState, + createContext, Fragment, useContext, useEffect, useId, useState, } from 'react'; import { createPortal } from 'react-dom'; import AnnotationClusterProps from './AnnotationClusterProps'; @@ -11,12 +11,14 @@ export const ClusterAnnotationContext = createContext void) | undefined>(undefined); + const [clusterAnnotations, setClusterAnnotations] = useState<{ contentElement: HTMLDivElement, annotation: mapkit.Annotation, @@ -35,8 +37,11 @@ export default function AnnotationCluster({ map.annotationForCluster = (clusterAnnotationData) => { if (clusterAnnotationData.clusteringIdentifier === clusterIdenfier) { if (annotationForCluster) { - const annotation = clusterAnnotations.find((a) => a.coordinate.latitude == clusterAnnotationData.coordinate.latitude - && a.coordinate.longitude == clusterAnnotationData.coordinate.longitude); + const annotation = clusterAnnotations.find( + (a) => a.coordinate.latitude === clusterAnnotationData.coordinate.latitude + && a.coordinate.longitude === clusterAnnotationData.coordinate.longitude, + ); + if (annotation) { return annotation.annotation; } diff --git a/src/components/AnnotationClusterProps.tsx b/src/components/AnnotationClusterProps.tsx index ca81ef0..90142c9 100644 --- a/src/components/AnnotationClusterProps.tsx +++ b/src/components/AnnotationClusterProps.tsx @@ -4,5 +4,4 @@ export default interface AnnotationClusterProps { coordinate: mapkit.Coordinate, ) => React.ReactNode; children: React.ReactNode; - clusterIdenfier: string; } diff --git a/src/stories/Annotation.stories.tsx b/src/stories/Annotation.stories.tsx index 04d7b50..46d3024 100644 --- a/src/stories/Annotation.stories.tsx +++ b/src/stories/Annotation.stories.tsx @@ -245,7 +245,7 @@ export const AnnotationClustering = () => { latitude={coordinate.latitude} longitude={coordinate.longitude} calloutElement={( -
{memberAnnotations.map((clusterAnnotation) => clusterAnnotation.title).join(' & ')}
+
{memberAnnotations.map((clusterAnnotation) => clusterAnnotation.title).join(' & ')}
)} onSelect={() => setSelected(memberAnnotations.map((clusterAnnotation) => clusterAnnotation.title).join(' & '))} selected={selected === memberAnnotations.map((clusterAnnotation) => clusterAnnotation.title).join(' & ')} @@ -257,10 +257,7 @@ export const AnnotationClustering = () => { return ( <> - + {coordinates.map(({ latitude, longitude }, index) => ( Date: Tue, 23 Dec 2025 13:35:58 +0100 Subject: [PATCH 7/9] Use Reference --- src/components/AnnotationCluster.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/AnnotationCluster.tsx b/src/components/AnnotationCluster.tsx index 1f939a1..e56fe11 100644 --- a/src/components/AnnotationCluster.tsx +++ b/src/components/AnnotationCluster.tsx @@ -1,5 +1,5 @@ import React, { - createContext, Fragment, useContext, useEffect, useId, useState, + createContext, Fragment, useContext, useEffect, useId, useState, useRef, } from 'react'; import { createPortal } from 'react-dom'; import AnnotationClusterProps from './AnnotationClusterProps'; @@ -25,6 +25,12 @@ export default function AnnotationCluster({ coordinate: mapkit.Coordinate, memberAnnotations: mapkit.Annotation[] }[]>([]); + const clusterAnnotationsRef = useRef<{ + contentElement: HTMLDivElement, + annotation: mapkit.Annotation, + coordinate: mapkit.Coordinate, + memberAnnotations: mapkit.Annotation[] + }[]>([]); // Coordinates useEffect(() => { @@ -37,7 +43,7 @@ export default function AnnotationCluster({ map.annotationForCluster = (clusterAnnotationData) => { if (clusterAnnotationData.clusteringIdentifier === clusterIdenfier) { if (annotationForCluster) { - const annotation = clusterAnnotations.find( + const annotation = clusterAnnotationsRef.current.find( (a) => a.coordinate.latitude === clusterAnnotationData.coordinate.latitude && a.coordinate.longitude === clusterAnnotationData.coordinate.longitude, ); From 5ec4b012c43b8e87ae382f26c2f29c3211a4d26c Mon Sep 17 00:00:00 2001 From: Nikischin Date: Tue, 23 Dec 2025 18:48:11 +0100 Subject: [PATCH 8/9] CoPilot implementation --- .gitignore | 1 + src/components/Annotation.tsx | 13 +- src/components/AnnotationCluster.tsx | 222 ++++++++++++++++++++++----- src/stories/Annotation.stories.tsx | 25 +-- 4 files changed, 209 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index 60a5673..ffaf263 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ dist node_modules storybook-static +.env \ No newline at end of file diff --git a/src/components/Annotation.tsx b/src/components/Annotation.tsx index 81cca30..ae85022 100644 --- a/src/components/Annotation.tsx +++ b/src/components/Annotation.tsx @@ -173,7 +173,7 @@ export default function Annotation({ size, - selected, + // Note: 'selected' is handled separately to avoid conflicts with MapKit's internal selection animates, appearanceAnimation, draggable, @@ -194,6 +194,17 @@ export default function Annotation({ }, [annotation, prop]); }); + // Handle 'selected' separately to avoid fighting with MapKit's selection + // Only set selected when the prop value differs from the current annotation state + useEffect(() => { + if (!annotation) return; + if (selected === undefined) return; + // Only update if the values actually differ to avoid deselecting when MapKit just selected + if (annotation.selected !== selected) { + annotation.selected = selected; + } + }, [annotation, selected]); + // Events const handlerWithoutParameters = () => { }; const events = [ diff --git a/src/components/AnnotationCluster.tsx b/src/components/AnnotationCluster.tsx index e56fe11..b4cdf20 100644 --- a/src/components/AnnotationCluster.tsx +++ b/src/components/AnnotationCluster.tsx @@ -1,5 +1,6 @@ import React, { createContext, Fragment, useContext, useEffect, useId, useState, useRef, + useLayoutEffect, } from 'react'; import { createPortal } from 'react-dom'; import AnnotationClusterProps from './AnnotationClusterProps'; @@ -8,6 +9,77 @@ import MapContext from '../context/MapContext'; export const AnnotationClusterIdentifierContext = createContext(null); export const ClusterAnnotationContext = createContext(undefined); +interface ClusterAnnotationData { + contentElement: HTMLDivElement; + annotation: mapkit.Annotation; + coordinate: mapkit.Coordinate; + memberAnnotations: mapkit.Annotation[]; + // Fingerprint based on member coordinates for stable identification + fingerprint: string; + // Track a unique ID for React keys + id: string; +} + +// Generate a unique ID for each cluster annotation +let clusterIdCounter = 0; +function generateClusterId(): string { + clusterIdCounter += 1; + return `cluster-${clusterIdCounter}`; +} + +// Create a fingerprint for a member annotation based on its stable properties +function getAnnotationFingerprint(annotation: mapkit.Annotation): string { + // Use coordinate with reasonable precision (8 decimal places is ~1mm) + return `${annotation.coordinate.latitude.toFixed(8)},${annotation.coordinate.longitude.toFixed(8)}`; +} + +// Create a fingerprint for a cluster based on its members' coordinates +function getClusterFingerprint(members: mapkit.Annotation[]): string { + return members + .map(getAnnotationFingerprint) + .sort() + .join('|'); +} + +// Check if two arrays contain the same annotation references +function haveSameMembers(a: mapkit.Annotation[], b: mapkit.Annotation[]): boolean { + if (a.length !== b.length) return false; + const setA = new Set(a); + return b.every((item) => setA.has(item)); +} + +// Helper component to measure content size and update callout offset +function ClusterContentMeasurer({ annotation, children }: { annotation: mapkit.Annotation, children: React.ReactNode }) { + const ref = useRef(null); + const initialCalloutOffset = useRef(null); + + useLayoutEffect(() => { + // Capture the initial callout offset set by the child Annotation component + initialCalloutOffset.current = annotation.calloutOffset; + }, []); + + useLayoutEffect(() => { + if (!ref.current) return; + const updateOffset = () => { + const height = ref.current?.offsetHeight || 0; + // Only apply default callout offset if user hasn't explicitly set one + if (initialCalloutOffset.current?.y === 0 && initialCalloutOffset.current?.x === 0) { + annotation.calloutOffset = new DOMPoint(0, -height / 2); + } + }; + + // Initial + updateOffset(); + + // Observer for size changes + const observer = new ResizeObserver(updateOffset); + observer.observe(ref.current); + return () => observer.disconnect(); + }, [annotation]); + + return
{children}
; +} + export default function AnnotationCluster({ children, annotationForCluster, @@ -15,58 +87,125 @@ export default function AnnotationCluster({ const clusterIdenfier = useId(); const map = useContext(MapContext); - const [ - existingClusterFunc, setExistingClusterFunc, - ] = useState<((clusterAnnotation: mapkit.Annotation) => void) | undefined>(undefined); - - const [clusterAnnotations, setClusterAnnotations] = useState<{ - contentElement: HTMLDivElement, - annotation: mapkit.Annotation, - coordinate: mapkit.Coordinate, - memberAnnotations: mapkit.Annotation[] - }[]>([]); - const clusterAnnotationsRef = useRef<{ - contentElement: HTMLDivElement, - annotation: mapkit.Annotation, - coordinate: mapkit.Coordinate, - memberAnnotations: mapkit.Annotation[] - }[]>([]); + // Use ref for the existing function to avoid stale closure issues + // Note: MapKit types declare this as returning void, but it actually returns an Annotation + const existingClusterFuncRef = useRef< + ((clusterAnnotation: mapkit.Annotation) => mapkit.Annotation | void) | undefined + >(undefined); + const hasStoredExistingFunc = useRef(false); + + const [clusterAnnotations, setClusterAnnotations] = useState([]); + + // Keep a ref in sync with state for use in the callback + const clusterAnnotationsRef = useRef([]); + useEffect(() => { + clusterAnnotationsRef.current = clusterAnnotations; + }, [clusterAnnotations]); + + // Store the annotationForCluster prop in a ref so the callback always has the latest + const annotationForClusterRef = useRef(annotationForCluster); + useEffect(() => { + annotationForClusterRef.current = annotationForCluster; + }, [annotationForCluster]); // Coordinates useEffect(() => { if (map === null) return undefined; - if (existingClusterFunc === undefined) { - setExistingClusterFunc(map.annotationForCluster); + // Store the existing function only once + if (!hasStoredExistingFunc.current) { + existingClusterFuncRef.current = map.annotationForCluster; + hasStoredExistingFunc.current = true; } map.annotationForCluster = (clusterAnnotationData) => { if (clusterAnnotationData.clusteringIdentifier === clusterIdenfier) { - if (annotationForCluster) { - const annotation = clusterAnnotationsRef.current.find( - (a) => a.coordinate.latitude === clusterAnnotationData.coordinate.latitude - && a.coordinate.longitude === clusterAnnotationData.coordinate.longitude, + if (annotationForClusterRef.current) { + // Create a fingerprint based on member coordinates (stable across calls) + const fingerprint = getClusterFingerprint(clusterAnnotationData.memberAnnotations); + + // Find existing cluster by fingerprint + const existingEntry = clusterAnnotationsRef.current.find( + (entry) => entry.fingerprint === fingerprint, ); - if (annotation) { - return annotation.annotation; + if (existingEntry) { + // Update coordinate if it changed + const coordChanged = existingEntry.coordinate.latitude + !== clusterAnnotationData.coordinate.latitude + || existingEntry.coordinate.longitude + !== clusterAnnotationData.coordinate.longitude; + + if (coordChanged) { + // IMPORTANT: Always update the annotation's coordinate to match what MapKit expects + existingEntry.annotation.coordinate = new mapkit.Coordinate( + clusterAnnotationData.coordinate.latitude, + clusterAnnotationData.coordinate.longitude, + ); + } + + // Check if members changed (reference check) + const membersChanged = !haveSameMembers( + existingEntry.memberAnnotations, + clusterAnnotationData.memberAnnotations, + ); + + // Update state if anything changed to ensure React renders correctly + if (coordChanged || membersChanged) { + setClusterAnnotations((prev) => prev.map((entry) => { + if (entry === existingEntry) { + return { + ...entry, + coordinate: clusterAnnotationData.coordinate, + memberAnnotations: clusterAnnotationData.memberAnnotations, + }; + } + return entry; + })); + } + + return existingEntry.annotation; } + // Create a new cluster annotation const contentElement = document.createElement('div'); + + // Fix offset issues by centering content around a 0x0 container + contentElement.style.width = '0px'; + contentElement.style.height = '0px'; + contentElement.style.overflow = 'visible'; + contentElement.style.display = 'flex'; + contentElement.style.justifyContent = 'center'; + contentElement.style.alignItems = 'center'; + + // Ensure pointer events work + contentElement.style.pointerEvents = 'auto'; + const a = new mapkit.Annotation( new mapkit.Coordinate( clusterAnnotationData.coordinate.latitude, clusterAnnotationData.coordinate.longitude, ), () => contentElement, + { + title: clusterAnnotationData.title, + subtitle: clusterAnnotationData.subtitle, + calloutEnabled: true, + }, ); - setClusterAnnotations((annotations) => [...annotations, { + const newEntry: ClusterAnnotationData = { contentElement, annotation: a, coordinate: clusterAnnotationData.coordinate, memberAnnotations: clusterAnnotationData.memberAnnotations, - }]); + fingerprint, + id: generateClusterId(), + }; + + // Update the ref immediately so subsequent calls in the same cycle can find it + clusterAnnotationsRef.current = [...clusterAnnotationsRef.current, newEntry]; + setClusterAnnotations((prev) => [...prev, newEntry]); return a; } @@ -74,36 +213,41 @@ export default function AnnotationCluster({ return clusterAnnotationData; } - return existingClusterFunc - ? existingClusterFunc(clusterAnnotationData) + // Delegate to the previous function if it exists + return existingClusterFuncRef.current + ? existingClusterFuncRef.current(clusterAnnotationData) : clusterAnnotationData; }; return () => { - if (existingClusterFunc === undefined && map.annotationForCluster !== undefined) { + // Cleanup: remove cluster annotations that are no longer valid + setClusterAnnotations([]); + clusterAnnotationsRef.current = []; + + if (!hasStoredExistingFunc.current || existingClusterFuncRef.current === undefined) { // @ts-ignore delete map.annotationForCluster; } else { // @ts-ignore - map.annotationForCluster = existingClusterFunc; + map.annotationForCluster = existingClusterFuncRef.current; } }; - }, [map]); - - console.log('clusterAnnotation', clusterAnnotations); + }, [map, clusterIdenfier]); return ( <> {annotationForCluster && clusterAnnotations.map(({ - contentElement, annotation, coordinate, memberAnnotations, + contentElement, annotation, coordinate, memberAnnotations, id, }) => ( - + {createPortal( - {annotationForCluster( - memberAnnotations, - coordinate, - )} + + {annotationForCluster( + memberAnnotations, + coordinate, + )} + , contentElement, )} diff --git a/src/stories/Annotation.stories.tsx b/src/stories/Annotation.stories.tsx index 46d3024..71b5800 100644 --- a/src/stories/Annotation.stories.tsx +++ b/src/stories/Annotation.stories.tsx @@ -223,7 +223,7 @@ CustomAnnotationCallout.storyName = 'Annotation with custom callout element'; export const AnnotationClustering = () => { const clusteringIdentifier = 'id'; - const [selected, setSelected] = useState(null); + const [selected, setSelected] = useState(null); const initialRegion: CoordinateRegion = useMemo(() => ({ centerLatitude: 46.20738751546706, @@ -233,11 +233,11 @@ export const AnnotationClustering = () => { }), []); const coordinates = [ - { latitude: 46.20738751546706, longitude: 6.155891756231 }, - { latitude: 46.25738751546706, longitude: 6.185891756231 }, - { latitude: 46.28738751546706, longitude: 6.2091756231 }, - { latitude: 46.20738751546706, longitude: 6.185891756231 }, - { latitude: 46.25738751546706, longitude: 6.2091756231 }, + { latitude: 46.20738751546706, longitude: 6.155891756231, someId: 'A' }, + { latitude: 46.25738751546706, longitude: 6.185891756231, someId: 'B' }, + { latitude: 46.28738751546706, longitude: 6.2091756231, someId: 'C' }, + { latitude: 46.20738751546706, longitude: 6.185891756231, someId: 'D' }, + { latitude: 46.25738751546706, longitude: 6.2091756231, someId: 'E' }, ]; const annotationClusterFunc = useCallback((memberAnnotations: mapkit.Annotation[], coordinate: mapkit.Coordinate) => ( @@ -245,10 +245,11 @@ export const AnnotationClustering = () => { latitude={coordinate.latitude} longitude={coordinate.longitude} calloutElement={( -
{memberAnnotations.map((clusterAnnotation) => clusterAnnotation.title).join(' & ')}
+
{memberAnnotations.map((clusterAnnotation) => clusterAnnotation.title).join(' & ')}
)} onSelect={() => setSelected(memberAnnotations.map((clusterAnnotation) => clusterAnnotation.title).join(' & '))} selected={selected === memberAnnotations.map((clusterAnnotation) => clusterAnnotation.title).join(' & ')} + calloutOffsetY={-40} >
@@ -258,17 +259,17 @@ export const AnnotationClustering = () => { <> - {coordinates.map(({ latitude, longitude }, index) => ( + {coordinates.map(({ latitude, longitude, someId }) => ( setSelected(index + 1)} + title={`Marker ${someId}`} + selected={selected === someId} + onSelect={() => setSelected(someId)} onDeselect={() => setSelected(null)} collisionMode="Circle" displayPriority={750} - key={index} + key={someId} > From d0b5fce9f29bf6c7f00894b2104b9c8e984b2fdd Mon Sep 17 00:00:00 2001 From: Nikischin Date: Tue, 23 Dec 2025 19:05:49 +0100 Subject: [PATCH 9/9] Improve offset behavior --- src/components/AnnotationCluster.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/AnnotationCluster.tsx b/src/components/AnnotationCluster.tsx index b4cdf20..63385b2 100644 --- a/src/components/AnnotationCluster.tsx +++ b/src/components/AnnotationCluster.tsx @@ -62,10 +62,9 @@ function ClusterContentMeasurer({ annotation, children }: { annotation: mapkit.A if (!ref.current) return; const updateOffset = () => { const height = ref.current?.offsetHeight || 0; - // Only apply default callout offset if user hasn't explicitly set one - if (initialCalloutOffset.current?.y === 0 && initialCalloutOffset.current?.x === 0) { - annotation.calloutOffset = new DOMPoint(0, -height / 2); - } + // Always apply vertical centering, but preserve any user-set horizontal offset + const xOffset = initialCalloutOffset.current?.x ?? 0; + annotation.calloutOffset = new DOMPoint(xOffset, height / 2); }; // Initial