Skip to content
Open
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
90 changes: 90 additions & 0 deletions src/components/Circle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useContext, useEffect, useState } from 'react';
import MapContext from '../context/MapContext';
import CircleProps from './CircleProps';

export default function Circle({
coordinate,
radius,

visible = true,
enabled = true,
selected = false,

onSelect = undefined,
onDeselect = undefined,

lineDash = [],
lineDashOffset = 0,
lineWidth = 1,

strokeColor = 'rgb(0, 122, 255)',
strokeOpacity = 1,

fillColor = 'rgb(0, 122, 255)',
fillOpacity = 0.1,
}: CircleProps) {
const [circle, setCircle] = useState<mapkit.CircleOverlay | null>(null);
const map = useContext(MapContext);

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

const { latitude, longitude } = coordinate;
const mapKitCoordinate = new mapkit.Coordinate(latitude, longitude);
const overlay = new mapkit.CircleOverlay(mapKitCoordinate, radius);
map.addOverlay(overlay);
setCircle(overlay);

return () => {
map.removeOverlay(overlay);
};
}, [map]);

// Simple properties
const properties = { visible, enabled, selected };
Object.entries(properties).forEach(([propertyName, prop]) => {
useEffect(() => {
if (!circle) return;
// @ts-ignore
circle[propertyName] = prop;
}, [circle, prop]);
});

// Simple style properties
const styleProperties = {
lineDash,
lineDashOffset,
lineWidth,

strokeColor,
strokeOpacity,

fillColor,
fillOpacity,
};
Object.entries(styleProperties).forEach(([propertyName, prop]) => {
useEffect(() => {
if (!circle) return;
// @ts-ignore
circle.style[propertyName] = prop;
}, [circle, prop]);
});

// Events
const events = [
{ name: 'select', handler: onSelect },
{ name: 'deselect', handler: onDeselect },
] as const;
events.forEach(({ name, handler }) => {
useEffect(() => {
if (!circle || !handler) return undefined;

const handlerWithoutParameters = () => handler();

circle.addEventListener(name, handlerWithoutParameters);
return () => circle.removeEventListener(name, handlerWithoutParameters);
}, [circle, handler]);
});

return null;
}
97 changes: 97 additions & 0 deletions src/components/CircleProps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Coordinate } from '../util/parameters';

export default interface CircleProps {
/**
* The coordinate of the circle overlay’s center.
* @see {@link https://developer.apple.com/documentation/mapkitjs/circleoverlay/coordinate}
*/
coordinate: Coordinate;

/**
* The radius of the circle overlay, in meters.
* @see {@link https://developer.apple.com/documentation/mapkitjs/circleoverlay/radius}
*/
radius: number;

/**
* A Boolean value that determines whether the circle is visible.
* @see {@link https://developer.apple.com/documentation/mapkitjs/overlay/visible}
*/
visible?: boolean;

/**
* A Boolean value that determines whether the circle responds to user interaction.
* @see {@link https://developer.apple.com/documentation/mapkitjs/overlay/enabled}
*/
enabled?: boolean;

/**
* A Boolean value that determines whether the map displays the circle in a selected state.
* @see {@link https://developer.apple.com/documentation/mapkitjs/overlay/selected}
*/
selected?: boolean;

/**
* Event fired when the circle is selected.
*/
onSelect?: () => void;

/**
* Event fired when the circle is deselected.
*/
onDeselect?: () => void;

/**
* The stroke color of the line. Accepts any valid CSS color value.
* The default is `rgb(0, 122, 255)`.
* Set this to `null` to remove the line stroke.
* @see {@link https://developer.apple.com/documentation/mapkitjs/style/strokeColor}
*/
strokeColor?: string | null;

/**
* The opacity of the stroke as a number between 0 and 1.
* The default value is `1`.
* @see {@link https://developer.apple.com/documentation/mapkitjs/style/strokeOpacity}
*/
strokeOpacity?: number;

/**
* The line width of the stroke for overlays, in CSS pixels.
* The default value is `1`.
* @see {@link https://developer.apple.com/documentation/mapkitjs/style/lineWidth}
*/
lineWidth?: number;

/**
* An array defining the line’s dash pattern, where numbers represent line and
* gap lengths in CSS pixels. For example, `[10, 5]` means draw for 10 pixels
* and leave a 5‑pixel gap repeatedly. Set to `[]` for solid lines (default).
* MapKit JS duplicates the array if it has an odd number of elements.
* @see {@link https://developer.apple.com/documentation/mapkitjs/style/lineDash}
*/
lineDash?: number[];

/**
* The number of CSS pixels to offset the start of the dash pattern.
* Has no effect when `lineDash` is set to draw solid lines.
* The default value is `0`.
* @see {@link https://developer.apple.com/documentation/mapkitjs/style/lineDashOffset}
*/
lineDashOffset?: number;

/**
* The fill color used for the shape. Accepts any valid CSS color value.
* The default is `rgb(0, 122, 255)`.
* Set this to `null` for no fill.
* @see {@link https://developer.apple.com/documentation/mapkitjs/style/fillColor}
*/
fillColor?: string | null;

/**
* The opacity to apply to the fill, as a number between 0 and 1.
* The default value is `0.1`.
* @see {@link https://developer.apple.com/documentation/mapkitjs/style/fillOpacity}
*/
fillOpacity?: number;
}
49 changes: 49 additions & 0 deletions src/stories/Circle.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { useMemo } from 'react';
import { Meta, StoryFn } from '@storybook/react';
import { fn } from '@storybook/test';
import './stories.css';
import Map from '../components/Map';
import { CoordinateRegion } from '../util/parameters';
import Circle from '../components/Circle';

// @ts-ignore
const token = import.meta.env.STORYBOOK_MAPKIT_JS_TOKEN!;

export default {
title: 'Components/Circle',
component: Circle,
parameters: {
layout: 'fullscreen',
},
args: {
onSelect: fn(),
onDeselect: fn(),
},
} as Meta<typeof Circle>;

type CircleProps = React.ComponentProps<typeof Circle>;

const Template: StoryFn<CircleProps> = (args) => {
const initialRegion: CoordinateRegion = useMemo(
() => ({
centerLatitude: 48,
centerLongitude: 14,
latitudeDelta: 22,
longitudeDelta: 55,
}),
[],
);
return (
<Map
token={token}
initialRegion={initialRegion}
minCameraDistance={300}
includedPOICategories={[]}
>
<Circle {...args} />
</Map>
);
};

export const Default = Template.bind({});
Default.args = { coordinate: { latitude: 46.52, longitude: 6.57 }, radius: 100000 };