Skip to content
Merged
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
3 changes: 1 addition & 2 deletions examples/drawing/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Drawing Tools Example

This example shows how to use the [google.maps.drawing.DrawingManager][drawing-manager] to draw shapes or markers on the map. In addition the example implements an undo/redo flow for the drawing tools. If you only want to add the the drawing tools to your map take a look at the `use-drawing-manager` hook.
This example shows how to build custom drawing tools using Maps JavaScript API overlays to draw shapes or markers on the map. In addition the example implements an undo/redo flow for drawing interactions.

## Google Maps Platform API Key

Expand Down Expand Up @@ -33,4 +33,3 @@ npm run start-local
The regular `npm start` task is only used for the standalone versions of the example (CodeSandbox for example)

[get-api-key]: https://developers.google.com/maps/documentation/javascript/get-api-key
[drawing-manager]: https://developers.google.com/maps/documentation/javascript/drawinglayer
2 changes: 1 addition & 1 deletion examples/drawing/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const API_KEY =

const App = () => {
return (
<APIProvider apiKey={API_KEY}>
<APIProvider apiKey={API_KEY} libraries={['geometry', 'marker']}>
<DrawingExample />
</APIProvider>
);
Expand Down
8 changes: 4 additions & 4 deletions examples/drawing/src/control-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import * as React from 'react';
function ControlPanel() {
return (
<div className="control-panel">
<h3>Drawing Tools Example</h3>
<h3>Drawing Example</h3>
<p>
Shows how to use the drawing tools of the Maps JavaScript API and
implements an undo/redo flow to show how to integrate the drawing
manager and its events into the state of a react-application.
Shows how to build custom drawing tools using Maps JavaScript API
overlays and implements an undo/redo flow to integrate drawing events
into the state of a React application.
</p>
<div className="links">
<a
Expand Down
16 changes: 14 additions & 2 deletions examples/drawing/src/drawing-example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,37 @@ import React from 'react';
import {ControlPosition, Map, MapControl} from '@vis.gl/react-google-maps';

import {UndoRedoControl} from './undo-redo-control';
import {DrawingToolbar} from './drawing-toolbar';
import {useDrawingManager} from './use-drawing-manager';
import ControlPanel from './control-panel';

const DrawingExample = () => {
const drawingManager = useDrawingManager();
const drawingController = useDrawingManager();

return (
<>
<Map
defaultZoom={3}
defaultCenter={{lat: 22.54992, lng: 0}}
mapId="712dec71c4c9382b"
gestureHandling={'greedy'}
disableDefaultUI={true}
disableDoubleClickZoom={true}
/>

<ControlPanel />

<MapControl position={ControlPosition.TOP_CENTER}>
<UndoRedoControl drawingManager={drawingManager} />
<div className="drawing-controls">
<DrawingToolbar
activeMode={drawingController.activeMode}
setActiveMode={drawingController.setActiveMode}
/>
<UndoRedoControl
drawingController={drawingController.eventTarget}
onOverlaySelect={() => drawingController.setActiveMode(null)}
/>
</div>
</MapControl>
</>
);
Expand Down
33 changes: 33 additions & 0 deletions examples/drawing/src/drawing-toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';

import {DrawingMode} from './types';

const MODES: Array<{mode: Exclude<DrawingMode, null>; label: string}> = [
{mode: 'marker', label: 'Marker'},
{mode: 'circle', label: 'Circle'},
{mode: 'polygon', label: 'Polygon'},
{mode: 'polyline', label: 'Polyline'},
{mode: 'rectangle', label: 'Rectangle'}
];

interface Props {
activeMode: DrawingMode;
setActiveMode: (mode: DrawingMode) => void;
}

export const DrawingToolbar = ({activeMode, setActiveMode}: Props) => {
return (
<div className="drawing-toolbar" role="toolbar" aria-label="Drawing tools">
{MODES.map(({mode, label}) => (
<button
key={mode}
type="button"
className={activeMode === mode ? 'is-active' : undefined}
onClick={() => setActiveMode(mode)}
aria-pressed={activeMode === mode}>
{label}
</button>
))}
</div>
);
};
96 changes: 90 additions & 6 deletions examples/drawing/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
export type OverlayGeometry =
| google.maps.Marker
| google.maps.marker.AdvancedMarkerElement
| google.maps.Polygon
| google.maps.Polyline
| google.maps.Rectangle
| google.maps.Circle;

export type DrawingMode =
| 'marker'
| 'circle'
| 'polygon'
| 'polyline'
| 'rectangle'
| null;

export type OverlayType = Exclude<DrawingMode, null>;

export interface DrawResult {
type: google.maps.drawing.OverlayType;
type: OverlayType;
overlay: OverlayGeometry;
}

Expand All @@ -19,7 +30,7 @@ export interface Snapshot {
}

export interface Overlay {
type: google.maps.drawing.OverlayType;
type: OverlayType;
geometry: OverlayGeometry;
snapshot: Snapshot;
}
Expand All @@ -33,20 +44,32 @@ export interface State {
export enum DrawingActionKind {
SET_OVERLAY = 'SET_OVERLAY',
UPDATE_OVERLAYS = 'UPDATE_OVERLAYS',
DELETE_OVERLAY = 'DELETE_OVERLAY',
UNDO = 'UNDO',
REDO = 'REDO'
}

export interface ActionWithTypeOnly {
type: Exclude<DrawingActionKind, DrawingActionKind.SET_OVERLAY>;
type: Exclude<
DrawingActionKind,
DrawingActionKind.SET_OVERLAY | DrawingActionKind.DELETE_OVERLAY
>;
}

export interface SetOverlayAction {
type: DrawingActionKind.SET_OVERLAY;
payload: DrawResult;
}

export type Action = ActionWithTypeOnly | SetOverlayAction;
export interface DeleteOverlayAction {
type: DrawingActionKind.DELETE_OVERLAY;
payload: OverlayGeometry;
}

export type Action =
| ActionWithTypeOnly
| SetOverlayAction
| DeleteOverlayAction;

export function isCircle(
overlay: OverlayGeometry
Expand All @@ -56,8 +79,69 @@ export function isCircle(

export function isMarker(
overlay: OverlayGeometry
): overlay is google.maps.Marker {
return (overlay as google.maps.Marker).getPosition !== undefined;
): overlay is google.maps.Marker | google.maps.marker.AdvancedMarkerElement {
return (
(overlay as google.maps.Marker).getPosition !== undefined ||
(overlay as google.maps.marker.AdvancedMarkerElement).gmpDraggable !==
undefined
);
}

export function isAdvancedMarker(
overlay: OverlayGeometry
): overlay is google.maps.marker.AdvancedMarkerElement {
return (
(overlay as google.maps.marker.AdvancedMarkerElement).gmpDraggable !==
undefined
);
}

// AdvancedMarkerElement exposes `position` while legacy Marker uses getters.
// These helpers normalize read/write access across both APIs.
// AdvancedMarkerElement exposes `position` while legacy Marker uses getters.
// These helpers normalize read/write access across both APIs.
export function getMarkerPosition(
overlay: google.maps.Marker | google.maps.marker.AdvancedMarkerElement
): google.maps.LatLngLiteral | undefined {
if (isAdvancedMarker(overlay)) {
if (!overlay.position) return undefined;

return overlay.position instanceof google.maps.LatLng
? overlay.position.toJSON()
: overlay.position;
}

return overlay.getPosition()?.toJSON();
}

export function setMarkerPosition(
overlay: google.maps.Marker | google.maps.marker.AdvancedMarkerElement,
position?: google.maps.LatLngLiteral
) {
if (!position) return;

if (isAdvancedMarker(overlay)) {
overlay.position = position;
return;
}

overlay.setPosition(position);
}

// AdvancedMarkerElement uses the `map` property instead of `setMap`.
// Keep all overlay map changes centralized here.
// AdvancedMarkerElement uses the `map` property instead of `setMap`.
// Keep all overlay map changes centralized here.
export function setOverlayMap(
overlay: OverlayGeometry,
map: google.maps.Map | null
) {
if (isAdvancedMarker(overlay)) {
overlay.map = map;
return;
}

overlay.setMap(map);
}

export function isPolygon(
Expand Down
56 changes: 51 additions & 5 deletions examples/drawing/src/undo-redo-control.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
import React, {useReducer, useRef} from 'react';
import React, {
useCallback,
useEffect,
useReducer,
useRef,
useState
} from 'react';
import {useMap} from '@vis.gl/react-google-maps';

import reducer, {
useDrawingManagerEvents,
useOverlaySelection,
useOverlaySnapshots
} from './undo-redo';

import {DrawingActionKind} from './types';
import {DrawingActionKind, OverlayGeometry} from './types';

interface Props {
drawingManager: google.maps.drawing.DrawingManager | null;
drawingController: google.maps.MVCObject | null;
onOverlaySelect?: () => void;
}

export const UndoRedoControl = ({drawingManager}: Props) => {
export const UndoRedoControl = ({
drawingController,
onOverlaySelect
}: Props) => {
const map = useMap();

const [deleteMode, setDeleteMode] = useState(false);
const deleteModeRef = useRef(deleteMode);
const selectedOverlayRef = useRef<OverlayGeometry | null>(null);
const ignoreNextMapClickRef = useRef(false);
const onOverlaySelectRef = useRef(onOverlaySelect);

const [state, dispatch] = useReducer(reducer, {
past: [],
now: [],
Expand All @@ -28,7 +45,29 @@ export const UndoRedoControl = ({drawingManager}: Props) => {
// off the "updating" when snapshot changes are applied back to the overlays.
const overlaysShouldUpdateRef = useRef<boolean>(false);

useDrawingManagerEvents(drawingManager, overlaysShouldUpdateRef, dispatch);
useEffect(() => {
deleteModeRef.current = deleteMode;
}, [deleteMode]);

useEffect(() => {
onOverlaySelectRef.current = onOverlaySelect;
}, [onOverlaySelect]);

const handleOverlaySelect = useCallback(() => {
onOverlaySelectRef.current?.();
}, []);

useDrawingManagerEvents(drawingController, overlaysShouldUpdateRef, dispatch);
useOverlaySelection(
map,
state.now,
selectedOverlayRef,
deleteModeRef,
setDeleteMode,
ignoreNextMapClickRef,
dispatch,
handleOverlaySelect
);
useOverlaySnapshots(map, state, overlaysShouldUpdateRef);

return (
Expand All @@ -55,6 +94,13 @@ export const UndoRedoControl = ({drawingManager}: Props) => {
<path d="M396-200q-97 0-166.5-63T160-420q0-94 69.5-157T396-640h252L544-744l56-56 200 200-200 200-56-56 104-104H396q-63 0-109.5 40T240-420q0 60 46.5 100T396-280h284v80H396Z" />
</svg>
</button>
<button
className={deleteMode ? 'is-active' : undefined}
onClick={() => setDeleteMode(previous => !previous)}
aria-pressed={deleteMode}
type="button">
Delete
</button>
</div>
);
};
Loading