Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/annotation cluster #70

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
23 changes: 16 additions & 7 deletions src/components/Annotation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import AnnotationProps from './AnnotationProps';
import forwardMapkitEvent from '../util/forwardMapkitEvent';
import CalloutContainer from './CalloutContainer';
import { toMapKitDisplayPriority } from '../util/parameters';
import { AnnotationClusterIdentifierContext, ClusterAnnotationContext } from './AnnotationCluster';

export default function Annotation({
latitude,
Expand Down Expand Up @@ -42,7 +43,7 @@ export default function Annotation({
appearanceAnimation = '',
visible = true,

clusteringIdentifier = null,
clusteringIdentifier: deprecatedClusterIdentifier = null,
displayPriority = undefined,
collisionMode = undefined,

Expand All @@ -63,6 +64,8 @@ export default function Annotation({
const [annotation, setAnnotation] = useState<mapkit.Annotation | null>(null);
const contentEl = useMemo<HTMLDivElement>(() => document.createElement('div'), []);
const map = useContext(MapContext);
const clusterAnnotation = useContext(ClusterAnnotationContext);
const clusteringIdentifier = useContext(AnnotationClusterIdentifierContext) ?? deprecatedClusterIdentifier;

// Padding
useEffect(() => {
Expand Down Expand Up @@ -90,7 +93,6 @@ export default function Annotation({
// Callout
useLayoutEffect(() => {
if (!annotation) return;

const callOutObj: mapkit.AnnotationCalloutDelegate = {};
if (calloutElement && calloutElementRef.current !== null) {
// @ts-expect-error
Expand Down Expand Up @@ -223,13 +225,20 @@ 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]);
}, [map, latitude, longitude, clusterAnnotation]);

return (
<>
Expand Down Expand Up @@ -270,7 +279,7 @@ export default function Annotation({
</div>,
document.body,
)}
{createPortal(children, contentEl)}
{clusterAnnotation !== undefined ? children : createPortal(children, contentEl)}
</>
);
}
106 changes: 106 additions & 0 deletions src/components/AnnotationCluster.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, {
createContext, Fragment, useContext, useEffect, useState,
} from 'react';
import { createPortal } from 'react-dom';
import AnnotationClusterProps from './AnnotationClusterProps';
import MapContext from '../context/MapContext';

export const AnnotationClusterIdentifierContext = createContext<string | null>(null);
export const ClusterAnnotationContext = createContext<mapkit.Annotation | undefined>(undefined);

export default function AnnotationCluster({
children,
annotationForCluster,
clusterIdenfier,
}: AnnotationClusterProps) {
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[]
}[]>([]);

// Coordinates
useEffect(() => {
if (map === null) return undefined;

if (existingClusterFunc === undefined) {
setExistingClusterFunc(map.annotationForCluster);
}

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);
if (annotation) {
return annotation.annotation;
}

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;
}

return existingClusterFunc
? existingClusterFunc(clusterAnnotationData)
: clusterAnnotationData;
};

return () => {
if (existingClusterFunc === undefined && map.annotationForCluster !== undefined) {
// @ts-ignore
delete map.annotationForCluster;
} else {
// @ts-ignore
map.annotationForCluster = existingClusterFunc;
}
};
}, [map]);

console.log('clusterAnnotation', clusterAnnotations);

return (
<>
{annotationForCluster && clusterAnnotations.map(({
contentElement, annotation, coordinate, memberAnnotations,
}) => (
<Fragment key={`${coordinate.latitude}_${coordinate.longitude}`}>
{createPortal(
<ClusterAnnotationContext.Provider value={annotation}>
{annotationForCluster(
memberAnnotations,
coordinate,
)}
</ClusterAnnotationContext.Provider>,
contentElement,
)}
</Fragment>
))}
<AnnotationClusterIdentifierContext.Provider value={clusterIdenfier}>
{children}
</AnnotationClusterIdentifierContext.Provider>
</>
);
}
8 changes: 8 additions & 0 deletions src/components/AnnotationClusterProps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default interface AnnotationClusterProps {
annotationForCluster?: (
memberAnnotations: mapkit.Annotation[],
coordinate: mapkit.Coordinate,
) => React.ReactNode;
children: React.ReactNode;
clusterIdenfier: string;
}
15 changes: 11 additions & 4 deletions src/components/Marker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FeatureVisibility, toMapKitDisplayPriority, toMapKitFeatureVisibility }
import MarkerProps from './MarkerProps';
import forwardMapkitEvent from '../util/forwardMapkitEvent';
import CalloutContainer from './CalloutContainer';
import { AnnotationClusterIdentifierContext, ClusterAnnotationContext } from './AnnotationCluster';

export default function Marker({
latitude,
Expand All @@ -18,7 +19,7 @@ export default function Marker({
subtitleVisibility = FeatureVisibility.Adaptive,
titleVisibility = FeatureVisibility.Adaptive,

clusteringIdentifier = null,
clusteringIdentifier: deprecatedClusterIdentifier = null,
displayPriority = undefined,
collisionMode = undefined,

Expand Down Expand Up @@ -60,6 +61,8 @@ export default function Marker({
}: MarkerProps) {
const [marker, setMarker] = useState<mapkit.MarkerAnnotation | null>(null);
const map = useContext(MapContext);
const clusterAnnotation = useContext(ClusterAnnotationContext);
const clusteringIdentifier = useContext(AnnotationClusterIdentifierContext) ?? deprecatedClusterIdentifier;

// Enum properties
useEffect(() => {
Expand Down Expand Up @@ -233,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(
<div style={{ display: 'none' }}>
Expand Down
76 changes: 73 additions & 3 deletions src/stories/Annotation.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import React, { useMemo } 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 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 (
<svg
width="24px"
Expand All @@ -27,7 +28,7 @@ function CustomMarker() {
<path
d="M14,0 C21.732,0 28,5.641 28,12.6 C28,23.963 14,36 14,36 C14,36 0,24.064 0,12.6 C0,5.641 6.268,0 14,0 Z"
id="Shape"
fill="#FF6E6E"
fill={color}
/>
<circle
fill="#0C0058"
Expand Down Expand Up @@ -219,3 +220,72 @@ export const CustomAnnotationCallout = () => {
);
};
CustomAnnotationCallout.storyName = 'Annotation with custom callout element';

export const AnnotationClustering = () => {
const clusteringIdentifier = 'id';
const [selected, setSelected] = useState<number | null>(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 },
{ latitude: 46.20738751546706, longitude: 6.185891756231 },
{ latitude: 46.25738751546706, longitude: 6.2091756231 },
];

const annotationClusterFunc = useCallback((memberAnnotations: mapkit.Annotation[], coordinate: mapkit.Coordinate) => (
<Annotation
latitude={coordinate.latitude}
longitude={coordinate.longitude}
calloutElement={(
<div>{memberAnnotations.map((clusterAnnotation) => clusterAnnotation.title).join(' & ')}</div>
)}
onSelect={() => setSelected(memberAnnotations.map((clusterAnnotation) => clusterAnnotation.title).join(' & '))}
selected={selected === memberAnnotations.map((clusterAnnotation) => clusterAnnotation.title).join(' & ')}
>
<CustomMarker color="#0000FF" />
</Annotation>
), [selected]);

return (
<>
<Map token={token} initialRegion={initialRegion} paddingBottom={44}>
<AnnotationCluster
clusterIdenfier={clusteringIdentifier}
annotationForCluster={annotationClusterFunc}
>
{coordinates.map(({ latitude, longitude }, index) => (
<Annotation
latitude={latitude}
longitude={longitude}
title={`Marker #${index + 1}`}
selected={selected === index + 1}
onSelect={() => setSelected(index + 1)}
onDeselect={() => setSelected(null)}
collisionMode="Circle"
displayPriority={750}
key={index}
>
<CustomMarker />
</Annotation>
))}
</AnnotationCluster>
</Map>

<div className="map-overlay">
<div className="map-overlay-box">
<p>{selected ? `Selected annotation #${selected}` : 'Not selected'}</p>
</div>
</div>
</>
);
};

AnnotationClustering.storyName = 'Clustering three annotations into one';
Loading