import {
forwardRef,
useContext,
useEffect,
useImperativeHandle,
useMemo,
useRef,
} from "react";
import { GoogleMapsContext, useMapsLibrary } from "@vis.gl/react-google-maps";
import type { Ref } from "react";
type CircleEventProps = {
onClick?: (e: google.maps.MapMouseEvent) => void;
onDrag?: (e: google.maps.MapMouseEvent) => void;
onDragStart?: (e: google.maps.MapMouseEvent) => void;
onDragEnd?: (e: google.maps.MapMouseEvent) => void;
onMouseOver?: (e: google.maps.MapMouseEvent) => void;
onMouseOut?: (e: google.maps.MapMouseEvent) => void;
};
export type CircleProps = google.maps.CircleOptions & CircleEventProps;
export type CircleRef = Ref<google.maps.Circle | null>;
function useCircle(props: CircleProps) {
const {
onClick,
onDrag,
onDragStart,
onDragEnd,
onMouseOver,
onMouseOut,
center,
radius,
...circleOptions
} = props;
// This is here to avoid triggering the useEffect below when the callbacks change (which happen if the user didn't memoize them)
const callbacks = useRef<Record<string, (e: unknown) => void>>({});
Object.assign(callbacks.current, {
onClick,
onDrag,
onDragStart,
onDragEnd,
onMouseOver,
onMouseOut,
});
const geometryLibrary = useMapsLibrary("geometry");
const circle = useRef(new google.maps.Circle()).current;
// update CircleOptions (note the dependencies aren't properly checked
// here, we just assume that setOptions is smart enough to not waste a
// lot of time updating values that didn't change)
useMemo(() => {
circle.setOptions(circleOptions);
}, [circle, circleOptions]);
const map = useContext(GoogleMapsContext)?.map;
useMemo(() => {
if (!center || !radius || !geometryLibrary) return;
circle.setCenter(center);
circle.setRadius(radius);
}, [circle, center, geometryLibrary]);
// create circle instance and add to the map once the map is available
useEffect(() => {
if (!map) {
if (map === undefined)
console.error("<Circle> has to be inside a Map component.");
return;
}
circle.setMap(map);
return () => {
circle.setMap(null);
};
}, [map]);
// attach and re-attach event-handlers when any of the properties change
useEffect(() => {
if (!circle) return;
// Add event listeners
const gme = google.maps.event;
const eventsMapper: [string, string][] = [
["click", "onClick"],
["drag", "onDrag"],
["dragstart", "onDragStart"],
["dragend", "onDragEnd"],
["mouseover", "onMouseOver"],
["mouseout", "onMouseOut"],
];
for (const el of eventsMapper) {
const [eventName, eventCallback] = el;
gme.addListener(circle, eventName, (e: google.maps.MapMouseEvent) => {
const callback = callbacks.current[eventCallback];
if (callback) callback(e);
});
}
return () => {
gme.clearInstanceListeners(circle);
};
}, [circle]);
return circle;
}
/**
* Component to render a circle on a map
*/
export const Circle = forwardRef((props: CircleProps, ref: CircleRef) => {
const circle = useCircle(props);
useImperativeHandle(ref, () => circle, []);
return null;
});