# React Google Maps: Drawing Tools

Google Maps is more than just a navigation tool. While it excels at providing core features like adding markers, info windows, and navigation, it also offers advanced techniques that we are going to show you how to use and implement in your project.

- URL: https://sudolabs.com/insights/react-google-maps-drawing-tools
- Published: 2023-09-21
- Updated: 2024-01-16
- Author: Pavlo Chabanenko
- Categories: Tech
- Language: English

Let's imagine that you are building your app for someone who needs to implement tools that allow you to measure distances between some objects or mark some places using arbitrary polygons that can be modified on the go by dragging their vertices and changing their color, along with some other built-in properties. This is precisely what Google Drawing Manager is capable of.

Should you need to learn some basics and get acquainted with some core principles of Google Maps, please check out my first article [here](https://sudolabs.com/blog/integrating-google-maps-in-react). It can be quite essential that you do so because I will not go into details about the aspects already described there in order not to repeat myself and make the current article less bulky and complex. But, of course, I will point you to a particular place where you can learn more about some basics if you are a new reader or willing to refresh your memory.

## Prerequisites

The project we are going to implement has 2 main differences compared to what we implemented in my previous articles. Although the project setup is quite the same (it is still built with [Gatsby](https://www.gatsbyjs.com/) and uses [styled-components](https://styled-components.com/) and [styled-system](https://styled-system.com/)), we will be using **Typescript** this time because more and more people are switching to Typescript, which is steadily becoming a standard on the market. In addition, we will be using another library to build our project with. As you could notice, we previously used [react-google-maps](https://www.npmjs.com/package/react-google-maps) for a number of reasons: it was the most popular library back then (although it has not been supported for a number of years), and it covered all the needs we had at that time. Now, we will stick to [@react-google-maps/api](https://www.npmjs.com/package/@react-google-maps/api), which is a complete re-write of `react-google-maps`, and it has finally doubled the number of weekly downloads on NPM compared to its unmaintained competitor. Furthermore, it supports Typescript and, of course, is constantly maintained by its community.

Before we start, please [create your API key](https://developers.google.com/maps/documentation/javascript/get-api-key) and enable the Maps JavaScript API. In order to make things work, we will need to "activate" 2 libraries (**drawing** and **geometry**); this will be done at the very end of the article once our map component is ready.

## Adding patches and making first steps

Let's install `@react-google-maps/api` - write this in your terminal inside your project folder:

`npm i react-google-maps`

Then we need to add a couple of amendments in order to prevent TypeScript from throwing errors (if you are not willing to use TypeScript, you can skip the rest of the current section and jump to the next one). As of the time of writing this article, the version `~2.13.1` of @react-google-maps/api is missing some types essential for our task. Let's install `patch-package`:

`npm i patch-package`

Then we should add `"postinstall": "patch-package"` to `"scripts"` inside our `package.json` file.

We need to add 2 patches:

1. Go to the `index.d.ts` file inside `node_modules/@types/google.maps` and add the following types to the `MapMouseEvent` interface:

```typescript
 edge?: number

 vertex?: number
```

Then we need to create our patch by typing in our terminal `npx patch-package @types/google.maps`. This patch adds 2 missing types for the mouse event: `edge` and `vertex` - we will use them later in order to implement our logic for dragging and modifying shapes where we need to differentiate between specific click types.

1. Go to the `index.d.ts` file inside `node_modules/@react-google-maps/api/dist/` and add the `export` keyword to the `Libraries` type declaration:

```typescript
export type Libraries = (
  | "drawing"
  | "geometry"
  | "localContext"
  | "places"
  | "visualization"
)[];
```

Then we need to create our patch by typing in our terminal `npx patch-package @react-google-maps/api` just as we did above. This will help us export the `Libraries` type and define our drawing and geometry libraries avoiding, Typescript errors.

## Adding layout components

In this section we will be adding the necessary layout components and declaring some important constants inside them that we will subsequently use in our Map component.

#### Drawing buttons

First of all, let's add the Drawing Buttons that will be displayed at the bottom of the map and activate our drawing modes - either **Polygon**, **Polyline** or **Circle**. Please, create a `DrawingButtonsGroup.tsx` file inside our `routes/home` directory:

```typescript
import React from "react";
import CircleIcon from "images/svg/circle.inline.svg";
import LineIcon from "images/svg/line.inline.svg";
import PolygonIcon from "images/svg/polygon.inline.svg";
import { rem } from "polished";
import styled from "styled-components";
import { SpaceProps } from "styled-system";
import { BoxShadow, Color } from "Theme";
import { Box, Flex } from "components/Layout";

export enum ButtonName {
  Line = "line",
  Circle = "circle",
  Polygon = "polygon",
}

type SelectedValue = ButtonName | null;

interface ButtonProps {
  onClick: () => void;
  name: ButtonName;
  selectedValue?: SelectedValue;
}

interface GroupProps extends SpaceProps {
  selectedValue: SelectedValue;
  children: React.ReactElement<ButtonProps>[] | React.ReactElement<ButtonProps>;
}

const BUTTONS_HEIGHT = rem(40);
const BUTTONS_WIDTH = rem(200);

const getIconCompByName = (name: ButtonName) => {
  switch (name) {
    case ButtonName.Line:
      return <LineIcon />;
    case ButtonName.Circle:
      return <CircleIcon />;
    case ButtonName.Polygon:
      return <PolygonIcon />;

    default:
      return <Box />;
  }
};

const Container = styled(Flex)`
  width: ${BUTTONS_WIDTH};
  height: ${BUTTONS_HEIGHT};

  box-shadow: ${BoxShadow.Small};
  background-color: ${Color.White};

  div:not(:last-child) {
    border-right: 1px solid ${Color.BlackOak};
  }
`;

function DrawingButton({ onClick, selectedValue, name }: ButtonProps) {
  return (
    <Flex
      onClick={onClick}
      flex={1}
      alignItems="center"
      justifyContent="center"
      bg={selectedValue === name ? Color.BleachedSilk : Color.White}
    >
      {getIconCompByName(name)}
    </Flex>
  );
}

export function DrawingButtonsGroup({
  children,
  selectedValue,
  ...props
}: GroupProps) {
  return (
    <Container {...props}>
      {React.Children.map(children, (child) =>
        React.cloneElement(child, {
          selectedValue,
        })
      )}
    </Container>
  );
}

DrawingButtonsGroup.Button = DrawingButton;

export const useDrawingButtons = () => {
  const [buttonName, setButtonName] = React.useState<ButtonName | null>(null);

  const resetButtonName = () => {
    setButtonName(null);
  };

  return { buttonName, setButtonName, resetButtonName };
};
```

As you can see above, we have created a group of buttons that we export along with their custom hook `useDrawingButtons`. Each of the 3 buttons is assigned a value from the `ButtonName` enum that is passed as a `name` prop and accepts the `selectedValue` that can be either `ButtonName` or `undefined | null`. If the `name` equals the `selectedValue`, the button background color gets changed, meaning the button is pressed. And, of course, our `useDrawingButtons` hook helps manipulate the above-mentioned actions by setting and storing the selected button name.

![Screenshot of the Google Maps Drawing Tool interface, featuring a variety of drawing buttons and controls for customizing maps](https://a.storyblok.com/f/117609/480x142/8780367cc9/drawing-buttons.png)

### Polygon card

Now let's create in the same directory a `PolygonCard` folder where we will create the `ColorButtonsGroup.tsx` file:

```typescript
import React from "react";
import { rem } from "polished";
import { SpaceProps } from "styled-system";
import { Color, Radius } from "Theme";
import { Flex } from "components/Layout";
import { Container } from "./Container";

export enum ButtonColor {
  Blue = "blue",
  LightBlue = "lightBlue",
  Pink = "pink",
  Red = "red",
  Green = "green",
}

export const NamesToColors = {
  [ButtonColor.Blue]: Color.OlympicBlue,
  [ButtonColor.LightBlue]: Color.PicnicDaySky,
  [ButtonColor.Pink]: Color.FugitiveFlamingo,
  [ButtonColor.Red]: Color.GlowingBrakeDisc,
  [ButtonColor.Green]: Color.PeppermintToad,
};

type SelectedValue = ButtonColor | null;

interface ButtonProps {
  color: ButtonColor;
  selectedValue?: SelectedValue;
  onClick: (value: ButtonColor) => void;
}

interface GroupProps extends SpaceProps {
  selectedValue: SelectedValue;
  children: React.ReactElement<ButtonProps>[] | React.ReactElement<ButtonProps>;
}

const BUTTON_SIZE = rem(24);

function ColorButton({ onClick, selectedValue, color }: ButtonProps) {
  return (
    <Flex
      as="button"
      onClick={() => onClick(color)}
      alignItems="center"
      justifyContent="center"
      width={BUTTON_SIZE}
      height={BUTTON_SIZE}
      borderRadius={Radius.Circle}
      bg={NamesToColors[color]}
      border={
        selectedValue === color ? `3px solid ${Color.BlackOak}` : undefined
      }
    />
  );
}

export function ColorButtonsGroup({
  children,
  selectedValue,
  ...props
}: GroupProps) {
  return (
    <Container {...props}>
      {React.Children.map(children, (child) =>
        React.cloneElement(child, {
          selectedValue,
        })
      )}
    </Container>
  );
}

ColorButtonsGroup.Button = ColorButton;
```

This is also a group of buttons that has a structure similar to the `DrawingButtonsGroup` component. These buttons will help us select both the border and background colors of the Polygon shape we will implement further in the article. The important part here is that we declare the `ButtonColor` enum, meaning we will have 5 possible colors.

Let's create another group of buttons in the same folder: `BorderButtonsGroup.tsx`:

```typescript
import React from "react";
import { rem } from "polished";
import { SpaceProps } from "styled-system";
import { Color, Radius } from "Theme";
import { Flex } from "components/Layout";
import { Text } from "components/Typography";
import { Container } from "./Container";

export enum BorderWidth {
  ExtraSmall = 1,
  Small,
  Medium,
  Large,
  ExtraLarge,
}

interface ButtonProps {
  borderWidth: BorderWidth;
  selectedValue?: BorderWidth | null;
  onClick: (value: BorderWidth) => void;
}

interface GroupProps extends SpaceProps {
  selectedValue: number | null;
  children: React.ReactElement<ButtonProps>[] | React.ReactElement<ButtonProps>;
}

const BUTTON_SIZE = rem(32);

function ColorButton({ onClick, selectedValue, borderWidth }: ButtonProps) {
  return (
    <Flex
      as="button"
      onClick={() => onClick(borderWidth)}
      alignItems="center"
      justifyContent="center"
      width={BUTTON_SIZE}
      height={BUTTON_SIZE}
      borderRadius={Radius.Circle}
      border={`1px solid ${Color.BleachedSilk}`}
      bg={selectedValue === borderWidth ? Color.BleachedSilk : Color.White}
    >
      <Text color={Color.GreekFlagBlue}>{`${borderWidth}px`}</Text>
    </Flex>
  );
}

export function BorderButtonsGroup({
  children,
  selectedValue,
  ...props
}: GroupProps) {
  return (
    <Container {...props}>
      {React.Children.map(children, (child) =>
        React.cloneElement(child, {
          selectedValue,
        })
      )}
    </Container>
  );
}

BorderButtonsGroup.Button = ColorButton;
```

This is the exact group of buttons that have some minor differences in the CSS. This component also introduces another enum - `BorderWidth`, that will also be used inside our Map component.

Both `ColorButtonsGroup` and `BorderButtonsGroup` share the same Container component; let's add it inside the same folder:

```typescript
import styled from "styled-components";
import { Space } from "Theme";
import { Flex } from "components/Layout";

export const Container = styled(Flex)`
  align-items: center;

  button:not(:last-child) {
    margin-right: ${Space.S};
  }
`;
```

It is now time to add the polygon card component itself and use there the button groups declared above. Please add this code to the `PolygonCard.tsx` file in the same folder:

```typescript
import React from "react";
import TrashIcon from "images/svg/trash.inline.svg";
import { rem } from "polished";
import styled from "styled-components";
import { Color, CustomSpace, FontSize, FontWeight, Space } from "Theme";
import { Box, Flex } from "components/Layout";
import { Text } from "components/Typography";
import { BorderButtonsGroup, BorderWidth } from "./BorderButtonsGroup";
import { ButtonColor, ColorButtonsGroup } from "./ColorButtonsGroup";

type Props = {
  area: number | string,
  selectedBorderWidth: BorderWidth,
  selectedBorderColor: ButtonColor,
  selectedBackgroundColor: ButtonColor,
  onDelete: () => void,
  onChangeBorderWidth: (value: BorderWidth) => void,
  onChangeBorderColor: (value: ButtonColor) => void,
  onChangeBackgroundColor: (value: ButtonColor) => void,
};

export const POLYGON_CARD_SIZE = 300;
const LEFT_OFFSET = Space.M;

const Title = styled(Text).attrs({ as: "p" })`
  font-size: ${FontSize.L};
  font-weight: ${FontWeight.Normal};
  margin: ${CustomSpace[12]} 0 ${CustomSpace[12]} ${LEFT_OFFSET};
`;

export function PolygonCard({
  area,
  onDelete,
  onChangeBorderColor,
  onChangeBorderWidth,
  onChangeBackgroundColor,
  selectedBorderColor,
  selectedBorderWidth,
  selectedBackgroundColor,
}: Props) {
  return (
    <Box
      width={rem(POLYGON_CARD_SIZE)}
      height={rem(POLYGON_CARD_SIZE)}
      bg={Color.White}
      p={CustomSpace[12]}
    >
      <Flex alignItems="center" justifyContent="flex-end">
        <TrashIcon onClick={onDelete} cursor="pointer" />
      </Flex>

      <Text
        fontSize={FontSize.XXL}
        fontWeight={FontWeight.Normal}
        color={Color.GreekFlagBlue}
        mt={Space.S}
        ml={LEFT_OFFSET}
      >
        {area}
      </Text>

      <Title>Border Color</Title>

      <ColorButtonsGroup selectedValue={selectedBorderColor} ml={LEFT_OFFSET}>
        {[
          ButtonColor.Blue,
          ButtonColor.LightBlue,
          ButtonColor.Pink,
          ButtonColor.Red,
          ButtonColor.Green,
        ].map((color) => (
          <ColorButtonsGroup.Button
            key={color}
            color={color}
            onClick={onChangeBorderColor}
          />
        ))}
      </ColorButtonsGroup>

      <Title>Border width</Title>

      <BorderButtonsGroup selectedValue={selectedBorderWidth} ml={LEFT_OFFSET}>
        {[
          BorderWidth.ExtraSmall,
          BorderWidth.Small,
          BorderWidth.Medium,
          BorderWidth.Large,
          BorderWidth.ExtraLarge,
        ].map((borderWidth) => (
          <BorderButtonsGroup.Button
            key={borderWidth}
            borderWidth={borderWidth}
            onClick={onChangeBorderWidth}
          />
        ))}
      </BorderButtonsGroup>

      <Title>Background color</Title>

      <ColorButtonsGroup
        selectedValue={selectedBackgroundColor}
        ml={LEFT_OFFSET}
      >
        {[
          ButtonColor.Blue,
          ButtonColor.LightBlue,
          ButtonColor.Pink,
          ButtonColor.Red,
          ButtonColor.Green,
        ].map((color) => (
          <ColorButtonsGroup.Button
            key={color}
            color={color}
            onClick={onChangeBackgroundColor}
          />
        ))}
      </ColorButtonsGroup>
    </Box>
  );
}
```

If you look at the `PolygonCard` component, you can see that it has the following props:

- `area` of the polygon that will be calculated in one of our utils later.
- `onDelete` removes our polygon from the map.
- `selectedBorderColor`, `selectedBorderWidth`, `selectedBackgroundColor` inform our button groups about what button is currently selected.
- `onChangeBorderColor`, `onChangeBorderWidth`, `onChangeBackgroundColor` perform the similarly named actions on clicking the above - mentioned buttons.

And, of course, let's export all the components by creating the `index.ts` file in the same folder:

```typescript
export * from "./BorderButtonsGroup";
export * from "./ColorButtonsGroup";
export * from "./PolygonCard";
```

Our newly created Polygon card will now look like this:

![Screenshot of the Google Maps drawing tool interface, featuring a variety of drawing buttons and controls for customizing maps](https://a.storyblok.com/f/117609/650x658/3ba92f6797/polygon-card.png)

### Undo button

Now let's create the `UndoButton.tsx` file inside the `routes/home` directory:

```typescript
import React from "react";
import UndoIcon from "images/svg/undo.inline.svg";
import { rem } from "polished";
import { Color } from "Theme";
import { Flex } from "components/Layout";

type Props = {
  onClick: () => void,
};

const BUTTON_SIZE = rem(40);

export function UndoButton({ onClick }: Props) {
  return (
    <Flex
      onClick={onClick}
      alignItems="center"
      justifyContent="center"
      width={BUTTON_SIZE}
      height={BUTTON_SIZE}
      bg={Color.White}
    >
      <UndoIcon cursor="pointer" />
    </Flex>
  );
}
```

This button will appear every time we drag either the vertex or the edge of our future polygons; clicking on it will undo the changes to the polygon shape. This is how the button will look:

![](https://a.storyblok.com/f/117609/942x506/40deeccf3b/undo-button.png)

## Adding map constants and config

Now it's time to add some crucial constants that we need to use in the config of our map. Should you need to refresh your memory or learn more about the `MapConfig`, you can go to the `Setting data, constants, and making first steps` section in my [React Google Maps article](https://sudolabs.com/blog/integrating-google-maps-in-react) and dive into their detailed description. Let's just briefly mention that in our `MapConfigOptions` I disabled all the default map elements that we don't need in our project and set the `gestureHandling` to `greedy` in order to navigate our map with more ease and without having to use both fingers or CTRL. You can learn more about gesture handling [here](https://developers.google.com/maps/documentation/javascript/interaction).

```typescript
import { Color } from "Theme";
import { POLYGON_CARD_SIZE } from "./PolygonCard";

const NATIVE_TRIANGLE_SIZE = 12;
const POLYGON_CARD_RIGHT_OFFSET = 20;

const PlusSymbol = {
  path: "M0,-1 V1 M-1,0 H1",
  strokeOpacity: 1,
  scale: 3,
};

const MapConfigOptions: google.maps.MapOptions = {
  zoomControl: false,
  mapTypeControl: false,
  disableDefaultUI: true,
  fullscreenControl: false,
  streetViewControl: false,
  gestureHandling: "greedy",
};

const MapConfig = {
  zoom: 12,
  center: { lat: 48.716385, lng: 21.261074 },
  options: MapConfigOptions,
  mapContainerStyle: {
    width: "100vw",
    height: "100vh",
  },
};

const PolygonOptions = {
  strokeWeight: 5,
  fillOpacity: 0.3,
  fillColor: Color.OlympicBlue,
  strokeColor: Color.OlympicBlue,
  suppressUndo: true,
};

const PolylineOptions = {
  strokeWeight: 5,
  fillOpacity: 0.3,
  strokeColor: Color.OlympicBlue,
  suppressUndo: true,
  icons: [
    {
      icon: PlusSymbol,
      repeat: "22px",
    },
  ],
};

export const MapSettings = {
  MapConfig,
  PolygonOptions,
  PolylineOptions,
  CircleOptions: PolygonOptions,
  InfoWindowOptions: {
    Triangle: {
      width: 13,
      height: 8,
    },
    PixelOffset: { X: 0, Y: -5 },
    Polygon: {
      UndoPixelOffset: { X: 0, Y: -5 },
      CardPixelOffset: {
        X: POLYGON_CARD_SIZE / 2 + POLYGON_CARD_RIGHT_OFFSET,
        Y: (POLYGON_CARD_SIZE + NATIVE_TRIANGLE_SIZE) / 2,
      },
    },
  },
};
```

Except for the `MapConfig` we have added a number of other parameters to the `MapSettings` object. These parameters represent a config for our drawing tools:

- We set all the options for our future polygons, polylines, and circles (you may notice that circles and polygons have identical properties). As a footnote, `PolylineOptions` have the `icons` key - this is where we add our custom SVG symbol and repeat it along the length of the polyline to achieve the ruler shape.
- we set the `InfoWindowOptions`; here we primarily define all the necessary offsets for all the cards (cards will be displayed inside the native InfoWindow module ) and shapes we will have in our map.

## Adding Map and Drawing Manager

Now it's finally time to add our basic Map component - let's add it to the `routes/home` directory:

```typescript
import React, { forwardRef, useImperativeHandle } from "react";
import {
  Circle,
  DrawingManager,
  GoogleMap,
  GoogleMapProps,
  InfoWindow,
  Polygon,
  Polyline,
} from "@react-google-maps/api";
import styled from "styled-components";
import { BoxShadow } from "Theme";
import { Box } from "components/Layout";
import { MapSettings } from "./constants";

export type MapRef = {
  drawCircle: () => void;
  drawPolygon: () => void;
  drawPolyline: () => void;
};

interface MapProps extends GoogleMapProps {
  resetDrawingButtons?: () => void;
}

const {
  MapConfig,
  CircleOptions,
  PolygonOptions,
  PolylineOptions,
  InfoWindowOptions,
} = MapSettings;

const MapStyles = styled(Box)`
  .gm-ui-hover-effect {
    display: none !important;
  }
  .gm-style .gm-style-iw-tc {
    display: none;
  }
  .gm-style .gm-style-iw-tc::after {
    width: ${InfoWindowOptions.Triangle.width}px;
    height: ${InfoWindowOptions.Triangle.height}px;
    transform: translateX(50%);
  }
  .gm-style-iw.gm-style-iw-c {
    box-shadow: ${BoxShadow.Small};
    padding: 0;

    .gm-style-iw-d {
      overflow: hidden !important;
    }
  }
`;

export const Map = forwardRef<MapRef, MapProps>(
  ({ children, resetDrawingButtons, ...rest }, ref) => {
    const drawingRef = React.useRef<DrawingManager | null>(null);

    useImperativeHandle(ref, () => ({
      drawPolygon: () => {
        if (drawingRef.current) {
          drawingRef.current?.state.drawingManager?.setDrawingMode(
            google.maps.drawing.OverlayType.POLYGON
          );
        }
      },
      drawCircle: () => {
        if (drawingRef.current) {
          drawingRef.current?.state.drawingManager?.setDrawingMode(
            google.maps.drawing.OverlayType.CIRCLE
          );
        }
      },
      drawPolyline: () => {
        if (drawingRef.current) {
          drawingRef.current?.state.drawingManager?.setDrawingMode(
            google.maps.drawing.OverlayType.POLYLINE
          );
        }
      },
    }));

    return (
      <MapStyles>
        <GoogleMap {...MapConfig} {...rest}>
          <DrawingManager
            ref={drawingRef}
            options={{
              drawingControl: false,
              circleOptions: CircleOptions,
              polygonOptions: PolygonOptions,
              polylineOptions: PolylineOptions,
            }}
          />

          {children}
        </GoogleMap>
      </MapStyles>
    );
  }
);
```

So, first of all, this code already enables you to create all the shapes via Drawing Manager. The only problem here is that you won't have any control over the resulting drawings - you won't be able to trigger any events (opening InfoWindows on click, etc.) or change their properties, like a border or fill color, and their coordinates will be hidden from you, meaning you can't pass them to your locale storage or database. And this is precisely what this article is about.

Now, let's have a closer look at the code. You probably noticed that we rewrite some of the native CSS styles in `MapStyles` - you can learn more about that in the `Changing native CSS rules` section of my first [article](https://sudolabs.com/blog/integrating-google-maps-in-react). We also declare the `drawingRef` in order to get access to the state and native methods of the `DrawingManager` - these are used inside the `useImperativeHandle` where we declare our 3 drawing functions: `drawPolygon`, `drawCircle`, `drawPolyline`. From now on, we can pass the `ref` to the Map component and use these methods from the outside. This is how we can now pass our Drawing buttons as children to the Map component and assign each button the respective method.
We also pass our `MapConfig` to the `GoogleMap` module and set the default options for our shapes in the `DrawingManager`. We also set the `drawingControl` to false in order to hide the default drawing buttons and create our own.

## Adding Home page and building the App

Now, let's create the Home.tsx file inside the same directory: `routes/home`. It will actually create our home page and allow us to build the App. Please, don't forget to import this page from the `src/pages` directory inside the index.ts file - this is how Gatsby works. If you have some problems with the project structure, you can have a look at the finished version of the project on my [Github page](https://github.com/chabanenkopp/react-google-maps-drawing-tools-article).

```typescript
import React from "react";
import { Libraries, LoadScript } from "@react-google-maps/api";
import { rem } from "polished";
import styled from "styled-components";
import { GenericPage } from "components/GenericPage";
import {
  ButtonName,
  DrawingButtonsGroup,
  useDrawingButtons,
} from "./DrawingButtonsGroup";
import { Map, MapRef } from "./Map";

const MAP_BUTTONS_OFFSET_Y = rem(10);

const LIBRARIES: Libraries = ["drawing", "geometry"];

const ButtonsContainer = styled("div")`
  position: absolute;
  left: 50%;
  bottom: ${MAP_BUTTONS_OFFSET_Y};

  transform: translateX(-50%);
`;

function Home() {
  const mapRef = React.useRef < MapRef > null;
  const { buttonName, setButtonName, resetButtonName } = useDrawingButtons();

  return (
    <GenericPage>
      <LoadScript
        googleMapsApiKey={process.env.GATSBY_GOOGLE_KEY ?? ""}
        libraries={LIBRARIES}
      >
        <Map ref={mapRef} resetDrawingButtons={resetButtonName}>
          <ButtonsContainer>
            <DrawingButtonsGroup selectedValue={buttonName}>
              <DrawingButtonsGroup.Button
                name={ButtonName.Polygon}
                onClick={() => {
                  mapRef.current?.drawPolygon();
                  setButtonName(ButtonName.Polygon);
                }}
              />
              <DrawingButtonsGroup.Button
                name={ButtonName.Circle}
                onClick={() => {
                  mapRef.current?.drawCircle();
                  setButtonName(ButtonName.Circle);
                }}
              />
              <DrawingButtonsGroup.Button
                name={ButtonName.Line}
                onClick={() => {
                  mapRef.current?.drawPolyline();
                  setButtonName(ButtonName.Line);
                }}
              />
            </DrawingButtonsGroup>
          </ButtonsContainer>
        </Map>
      </LoadScript>
    </GenericPage>
  );
}

export default Home;
```

Here, I would like to draw your attention to the `LoadScript` module that we import from `@react-google-maps/api`. We must pass it our Map component as children along with 2 parameters: `googleMapsApiKey` and `libraries`. Please, pass the API key you were supposed to create at the beginning of the article - this key is sensitive, and thus I used `.env` to keep it secret. As I also mentioned, we will need to activate 2 libraries: `drawing` and `geometry`, and we do that by declaring the `LIBRARIES` array and passing it to the `LoadScript` module. And this is where we finally use our `Libraries` type that we patched with `patch-package`.
Evidently, we also pass our `DrawingButtonsGroup` that actually represents the control buttons, and each button is assigned its respective drawing method that we get via `mapRef`.

## Using Drawing Manager to create controlled polygons

So, as we have already stated, the current implementation doesn't allow us to take control over the shapes created via `DrawingManager`. What actually happens is that `DrawingManager` works as a black box that we don't have very much access to. That means we should use `DrawingManager` for the drawing capabilities only and then display our shape by means of the corresponding shape module (Polygon, Polyline, Circle) that we can pass to the `GoogleMap` as children.

Let's first create a `mapTypes.ts` file in the `routes/home` directory. Here we will store the declarations of all our map related types. Please pass the following code there:

```typescript
import { BorderWidth, ButtonColor } from "./PolygonCard";

export interface PolygonOptionsType {
  borderWidth: BorderWidth;
  borderColor: ButtonColor;
  backgroundColor: ButtonColor;
}

export interface PolygonType {
  id: number;
  coordinates: google.maps.LatLngLiteral[];
  prevCoordinates: google.maps.LatLngLiteral[] | [];
  polygonInstance: google.maps.Polygon | null;
  options: PolygonOptionsType;
}
```

Let's create a utils folder in the `routes/home` directory and pass our first util there in the file `coordinateFuncsToCoordinates.ts`:

```typescript
export const coordinateFuncsToCoordinates = (
  coordinates: google.maps.LatLng[]
) =>
  coordinates.map(({ lat, lng }) => ({
    lat: lat(),
    lng: lng(),
  }));
```

We will use this function quite frequently; it will help us convert the native Google Maps coordinates type - an array of objects containing coordinate functions to an array of objects containing real coordinates instead. It is a lot more convenient to operate this data format, and, in addition, you can then store this data either in your local storage or database (you can't store functions because you will lose their context).

Then let's add this line to the map component:

```typescript
const [polygons, setPolygons] = React.useState<MT.PolygonType[]>([]);
```

So, `polygons` is the React state where we keep the data of all the polygons we create. Each polygon will be assigned its `id` so that we can find it in the `polygons` array. It will also have `coordinates` - current coordinates of the polygon, whose position on the map is based on, `prevCoordinates` - these will be used to implement the undo functionality, `polygonInstance` - the instance of the newly created polygon that will enable us to change polygon options (fill color, border width, etc.) via the PolygonCard component, and `options` - current options assigned to the polygon.

Then we should add the `resetDrawingToolsMode` method to our Map component. It resets the drawing mode selected to draw a certain shape - we will use it once the shape drawing is finished. We use the `drawingManager` ref here:

```typescript
const resetDrawingToolsMode = () => {
  drawingRef.current?.state.drawingManager?.setDrawingMode(null);
};
```

Let's create in the `Map.tsx` file the default polygon options that will be assigned to the newly created polygon and thus passed to the `polygons` React state:

```typescript
import { BorderWidth, ButtonColor } from "./PolygonCard";

export const DefaultPolygonOptions = {
  borderColor: ButtonColor.Blue,
  backgroundColor: ButtonColor.Blue,
  borderWidth: BorderWidth.ExtraLarge,
};
```

Now, let's import all the types along with the Ramda library that you should install. For more information, click [here](https://ramdajs.com/).

```typescript
import * as R from "ramda";
import * as MT from "./mapTypes";
```

Then, please add the following event handler to the `DrawingManager` module:

```typescript
onOverlayComplete={(evt) => {
  const { type, overlay } = evt;

  if (overlay) {
    window.google.maps.event.clearInstanceListeners(overlay);
    overlay.setMap(null);
    resetDrawingButtons?.();

    if (type === google.maps.drawing.OverlayType.POLYGON) {
      const typedOverlay = overlay as google.maps.Polygon;
      const coordinateFuncs: google.maps.LatLng[] = typedOverlay
        .getPath()
        .getArray();

      const coordinates =
        coordinateFuncsToCoordinates(coordinateFuncs);

      if (coordinates.length < POLYGON_NODES_MIN_QUANTITY) {
        return;
      }

      setPolygons((prevPolygons) => [
        ...prevPolygons,
        {
          id: R.inc(polygons.length),
          coordinates,
          options: DefaultPolygonOptions,
          prevCoordinates: [],
          polygonInstance: null,
        },
      ]);

      resetDrawingToolsMode();
    }
  }
}}
```

The `DrawingManager` has the `onOverlayComplete` event that returns us the `type` of the shape (Polygon, Circle, Polyline) we have just finished drawing and its instance that is called `overlay`. The main idea behind using this event and its props is that we need to create our polygon, save its coordinates to the `polygons` state, and then remove this uncontrolled polygon that we have no access to. This is exactly what happens in the `clearInstanceListeners(overlay)` and `overlay.setMap(null)`. Then we call the `resetDrawingButtons` passed as props to the Map component in order to disable the drawing button that was pressed to activate the proper drawing mode.

As you can see, we then check the type of our shape to make sure it's a polygon (we will add the logic for Circles and Polylines later). Then we extract `coordinateFuncs` from the `overlay` and get the polygon coordinates via our `coordinateFuncsToCoordinates` util. Then we have another check: we want to make sure that the newly drawn polygon has at least **three** nodes. Otherwise, the user will be able to draw a line or just a dot instead. That is why we proceed in the handler only if the `coordinates.length` is greater than `POLYGON_NODES_MIN_QUANTITY`. By the way, let's add this constant above our component:

```typescript
const POLYGON_NODES_MIN_QUANTITY = 3;
```

We then finally pass the default options and extracted coordinates of the newly drawn (and already removed) polygon to the `setPolygons` React state, while `prevCoordinates` are assigned an empty array (we have none at the moment) and the `polygonInstance` is set to `null` - we removed our polygon instance created by the Drawing Manager, and we will use another instance from the Polygon module when the controlled Polygon is mounted. And, finally, we assign this polygon its `id` by incrementing the `polygon` array length -  `R.inc(polygons.length)`. Please check the Ramda in the docs [here](https://ramdajs.com/docs/#inc). So, at this point, we have all the necessary data to render our own controlled polygon, and that is what we are going to do in the next section.

## Creating controlled polygons using a Polygon module

Now we are ready to render our controlled polygon, but before doing that, let's add `dropAndReturnLastElement` util to the `routes/home/utils` directory. We will use this a number of times when dealing with the React state. It will help us drop the last element from an array and return both this `lastElement` and the rest of the array elements as `elementsWithoutLast`:

```typescript
import * as R from "ramda";

export const dropAndReturnLastElement = <T>(elements: T[]): [T[], T] => {
  const lastElement = R.last(elements)!;
  const elementsWithoutLast = R.dropLast(1, elements);

  return [elementsWithoutLast, lastElement];
};
```

Let's render our controlled polygons by adding this code to the Map component and passing it as children to the `GoogleMap` module:

```typescript
{
  polygons.map((polygon) => (
    <Polygon
      key={polygon.id}
      editable
      draggable
      paths={polygon.coordinates}
      onLoad={(polygonInstance) => {
        polygonInstance.setOptions(PolygonOptions);

        setPolygons((prevPolygons) => {
          const [polygonsWithoutLast, lastPolygon] =
            dropAndReturnLastElement(prevPolygons);

          return [...polygonsWithoutLast, { ...lastPolygon, polygonInstance }];
        });

        handleSetPolygonCardLatLng(polygon);
      }}
    />
  ));
}
```

Here we render our polygons based on the data extracted from the `DrawingManager`. We pass the `coordinates` from the polygon state as `paths` as well as set the `editability` and `draggability` of our polygons. Inside the `onLoad` callback, we finally get our `polygonInstance` that we use to set the initial `PolygonOptions` of the newly rendered polygon and then update the `polygons` state by passing it this `polygonInstance` that was set to null in the `onOverlayComplete` callback of the DrawingManager.
The last thing we need to do is open a Polygon card once a new polygon is created to show the user all the options this newly rendered polygon has, and demonstrate the actions the user can take to modify the polygon. This is what is done via the `handleSetPolygonCardLatLng` that is called at the very end of the `onLoad` callback.

So, let's implement the `handleSetPolygonCardLatLng` by declaring another React state in our Map component:

```typescript
const [polygonCardData, setPolygonCardData] =
  React.useState<MT.PolygonCardType | null>(null);
```

Let's also add the `PolygonCardType` to the `mapTypes.ts` file:

```typescript
export interface PolygonCardType {
  id: number;
  lat: number;
  lng: number;
}
```

Please notice that `polygons` along with `polygonCardData` have all the necessary data we need to display our polygon card. The card itself will be displayed every time `polygonCardData` is not set to `null`. The data we pass to the `polygonCardData` is polygon `id` (we need it to find the right polygon in the `polygons` state and extract all the options), and, of course, coordinates of the point that we need to display our card over: `lat`, `lng`.

Since we would like to display the polygon card on the right of the polygon itself and properly center it, we need to create a bounding box to wrap the polygon with.

![](https://a.storyblok.com/f/117609/1874x874/104c6470b2/bounding_box.png)

No matter how tricky and complex the shape of our polygon is, we can easily create a box around it and place the card on the right-hand side in relation to the bounding box center. Let's declare the `handleSetPolygonCardLatLng` inside the map component:

```typescript
const handleSetPolygonCardLatLng = (polygon: MT.PolygonType) => {
  const bounds = new window.google.maps.LatLngBounds();

  polygon.coordinates.forEach(({ lat, lng }) =>
    bounds.extend(new window.google.maps.LatLng(lat, lng))
  );

  const polygonBoundingBoxCenter = bounds.getCenter();
  const polygonBoundingBoxCenterNorthEast = bounds.getNorthEast();

  setPolygonCardData({
    id: polygon.id,
    lat: polygonBoundingBoxCenter.lat(),
    lng: polygonBoundingBoxCenterNorthEast.lng(),
  });
};
```

Our function above receives a polygon element from the `polygons` React state and thus has access to its coordinates - they are used to create the bounding box around our polygon via the `bounds.extend` method. Once the bounding box is ready, we need to get the coordinates of the point where we will display our card. In other words, we need the latitude of the box center (`bounds.getCenter`) and the longitude of the box's right corner (`bounds.getNorthEast`). We pass these coordinates to the `setPolygonCardData` state along with the polygon `id`.

## Adding Polygon Card

We will display our Polygon Card with the help of the InfoWindow module. We will also need to improve our user experience by closing InfoWindows via outside click functionality. It would also be great not to close them when dragging the map. Let's install a library called `react-outside-click-handler`. You can do that by typing in your terminal inside the project folder: `npm i react-outside-click-handler`.

We also need to declare another useState:

```typescript
const [isClickOutsideDisabled, setIsClickOutsideDisabled] =
  React.useState(false);
```

And finally let's pass `onDragStart` and `onDragEnd` to our `GoogleMap` module:

```typescript
  <GoogleMap
    ref={mapRef}
    // Just added
    onDragStart={() => {
      setIsClickOutsideDisabled(true);
    }}
    onDragEnd={() => {
      setIsClickOutsideDisabled(false);
    }}
    {...MapConfig}
    {...rest}
  >
```

Before adding the InfoWindow component that wraps the PolygonCard, we need to create all the necessary utils and handlers. As you may remember, our PolygonCard needs to know what properties (border width, color, etc.) the specific polygon has, and requires some methods that will be able to change all these properties.
The first handler we will create is `handleChangePolygonOptions`. Please create a new file `dropAndReturnElementById.ts` in the `routes/home/utils` directory:

```typescript
interface Id {
  id: number;
}

interface Props<T> {
  id: number;
  elements: T[];
}

export const dropAndReturnElementById = <T extends Id>({
  id,
  elements,
}: Props<T>): [T[], T] => {
  const currentElement = elements.find(
    ({ id: elementId }: { id: number }) => elementId === id
  )!;

  const elementsWithoutCurrent = elements.filter(
    ({ id: elementId }: { id: number }) => id !== elementId
  );

  return [elementsWithoutCurrent, currentElement];
};
```

This util will be used a number of times around the project when updating React state by dropping a certain element found by `id` and adding it back to an array of elements it was dropped from.

Now, let's add our handler:

```typescript
const handleChangePolygonOptions = ({
  id,
  options,
  polygonOption,
}: MT.ChangePolygonOptions) => {
  const [polygonsWithoutCurrent, currentPolygon] = dropAndReturnElementById({
    id,
    elements: polygons,
  });

  currentPolygon.polygonInstance?.setOptions(options);

  const updatedPolygons = R.append(
    {
      ...currentPolygon,
      options: {
        ...currentPolygon.options,
        ...polygonOption,
      },
    },
    polygonsWithoutCurrent
  );

  setPolygons(updatedPolygons);
};
```

We also need to add the `ChangePolygonOptions` to the mapTypes.ts file:

```typescript
export interface ChangePolygonOptions {
  id: number;
  options: google.maps.PolygonOptions;
  polygonOption: { [key: string]: BorderWidth | ButtonColor };
}
```

Our handler expects a polygon `id` so that we could use `dropAndReturnElementById`, `options` - actual option (strokeColor, fillColor, etc.) that we need to change using the polygon instance, and `polygonOption` that helps us update our `polygons` React state. In other words, this function updates 1 of the 3 options of our polygons and does it in 2 places: the polygon instance gotten from the Polygon module in the onLoad callback and the `polygons` state that is used to render our polygons, which works as a source of truth for the polygon properties.

Let's add our next util as a `getPolygonOptionByName.ts` file:

```typescript
import { PolygonOptionsType, PolygonType } from "../mapTypes";
import { dropAndReturnElementById } from "./dropAndReturnElementById";

interface Props {
  polygonId: number;
  polygons: PolygonType[];
  name: keyof PolygonOptionsType;
}

export const getPolygonOptionByName = ({
  name,
  polygons,
  polygonId,
}: Props) => {
  const [, currentPolygon] = dropAndReturnElementById({
    id: polygonId,
    elements: polygons,
  });

  return currentPolygon.options[name];
};
```

This util will help us pass our PolygonCard the actual property values assigned to the selected polygon: each polygon should know what value is chosen for a particular property (border width, fill color, etc.). This util accepts the `name` of a certain property, finds the necessary element from the `polygons` via `polygonId`, and extracts the property value by the `name`.

Our PolygonCard component also expects the area param and thus we need a `calculatePolygonArea` util:

```typescript
import { PolygonType } from "../mapTypes";

export const calculatePolygonArea = ({
  polygons,
  polygonId,
}: {
  polygonId: number,
  polygons: PolygonType[],
}) => {
  const fallBackValue = 0;
  const maxValueInSquareMeters = 10000;
  const squareMetersToSquareKilometersRatio = 10 ** -6;

  const { coordinates } = polygons.find(({ id }) => id === polygonId) ?? {};

  if (coordinates) {
    const squareMeters =
      google.maps.geometry.spherical.computeArea(coordinates);

    const isValueDisplayedInSquareMeters =
      squareMeters <= maxValueInSquareMeters;

    const area = parseFloat(
      (isValueDisplayedInSquareMeters
        ? squareMeters
        : squareMeters * squareMetersToSquareKilometersRatio
      ).toFixed(2)
    );

    return isValueDisplayedInSquareMeters ? `${area} m²` : `${area} km²`;
  }

  return `${fallBackValue} m²`;
};
```

Polygon area is calculated in square meters by the `computeArea` method from `google.maps.geometry.spherical` that accepts polygon coordinates (we find them from `polygons` by `polygonId`). Our util returns the area value in square meters if it is no more than 10000, otherwise, it converts the value to square kilometers.

Now, let's pass this component as children to the `GoogleMap` module:

```typescript
{polygonCardData && (
  <InfoWindow
    position={{
      lat: polygonCardData.lat,
      lng: polygonCardData.lng,
    }}
    options={{
      pixelOffset: new window.google.maps.Size(
        InfoWindowOptions.Polygon.CardPixelOffset.X,
        InfoWindowOptions.Polygon.CardPixelOffset.Y
      ),
    }}
  >
    <OutsideClickHandler
      onOutsideClick={() => {
        setPolygonCardData(null);
      }}
      disabled={isClickOutsideDisabled}
    >
      <PolygonCard
        area={calculatePolygonArea({
          polygons,
          polygonId: polygonCardData.id,
        })}
        selectedBackgroundColor={
          getPolygonOptionByName({
            polygons,
            name: "backgroundColor",
            polygonId: polygonCardData.id,
          }) as ButtonColor
        }
        selectedBorderColor={
          getPolygonOptionByName({
            polygons,
            name: "borderColor",
            polygonId: polygonCardData.id,
          }) as ButtonColor
        }
        selectedBorderWidth={
          getPolygonOptionByName({
            polygons,
            name: "borderWidth",
            polygonId: polygonCardData.id,
          }) as BorderWidth
        }
        onChangeBorderColor={(color: ButtonColor) => {
          handleChangePolygonOptions({
            id: polygonCardData.id,
            polygonOption: { borderColor: color },
            options: { strokeColor: NamesToColors[color] },
          });
        }}
        onChangeBackgroundColor={(color: ButtonColor) => {
          handleChangePolygonOptions({
            id: polygonCardData.id,
            polygonOption: { backgroundColor: color },
            options: { fillColor: NamesToColors[color] },
          });
        }}
        onChangeBorderWidth={(borderWidth: BorderWidth) => {
          handleChangePolygonOptions({
            id: polygonCardData.id,
            polygonOption: { borderWidth },
            options: { strokeWeight: borderWidth },
          });
        }}
        onDelete={() => {
          const [polygonsWithoutCurrent] = dropAndReturnElementById({
            id: polygonCardData.id,
            elements: polygons,
          });

          setPolygons(polygonsWithoutCurrent);

          setPolygonCardData(null);
        }}
      />
    </OutsideClickHandler>
  </InfoWindow>
)}
```

Here, we should take a closer look at what is going on in the code snippet above. First of all, we display our Polygon card conditionally - only when the `polygonCardData` is not set to `null`. Our `PolygonCard` has 2 wrappers: `InfoWindow` (a native Google Maps module that represents a window on the map and requires coordinates and some other options such as vertical and horizontal offsets), and `OutsideClickHandler`, which helps us close the polygon card if the user clicks outside the card boundaries. We also disable the `OutsideClickHandler` by passing it `disabled={isClickOutsideDisabled}` when map dragging events are triggered.

If you comment out `options` in the InfoWindow module, build the app and draw a rectangle, you will see the following result:

![](https://a.storyblok.com/f/117609/1256x960/3282773966/card-without-offsets.png)

You can see that the `InfoWindow` is displayed over the dot that has the coordinates of the bounding box right side center. We achieved this in the `handleSetPolygonCardLatLng` mentioned above. But our goal was to display the InfoWindow a bit differently - on the right of the bounding box with a particular right offset and center the card vertically. This can be easily achieved by passing the proper offset X and Y options to the InfoWindow module. If you go to our `constants.ts` file, you will see that their values look the following way:

```typescript
CardPixelOffset: {
  X: POLYGON_CARD_SIZE / 2 + POLYGON_CARD_RIGHT_OFFSET,
  Y: (POLYGON_CARD_SIZE + NATIVE_TRIANGLE_SIZE) / 2,
},
```

To fix the X axis, we move the card to the right by 50% and add a small right offset so that it does not intersect the bounding box/polygon, and the Y axis is also amended by moving the card by 50% to the bottom, including the size of the native triangle (we removed it by rewriting native CSS, but the offset remains anyway and is approximately 12px). Now if you rebuild the app and draw a rectangle again, the card position will be correct:

![](https://a.storyblok.com/f/117609/1678x764/cfbd11511c/card-with-offsets.png)

You can also see that the `PolygonCard` has an `onDelete` param - this functionality is achieved by removing the polygon element from the `polygons` React state and setting the card data to `null`.

At this point, we have finally created our controlled polygons with a proper card displaying their area and properties. You can build the app and try to change polygon properties by selecting them via the card buttons. Since our polygons are controlled (we render them based on values in React state), we know exactly what coordinates and properties they have and can subsequently use them in our database. Now we need to implement dragging, shape-changing, and undo functionality - this is what we are going to focus on in the next chapter.

## Polygon dragging

The main idea behind polygon dragging is updating `polygons` React state, or, more precisely, the coordinates of the polygon you are interacting with at the end of the drag action. `Polygon` module has both `onDragStart` and `onDragEnd` events. We will disable the polygon card once dragging is initiated:

```typescript
onDragStart={() => {
  setPolygonCardData(null);
}}
```

At the end of the dragging action, we need to update both previous and current coordinates and open our polygon card:

```typescript
onDragEnd={() => {
  const { polygonInstance } = polygon;

  if (polygonInstance) {
    const coordinateFuncs: google.maps.LatLng[] = polygonInstance
      .getPath()
      .getArray();

    const coordinates =
      coordinateFuncsToCoordinates(coordinateFuncs);

    setPolygons((prevPolygons) => {
      const [polygonsWithoutCurrent] = dropAndReturnElementById({
        id: polygon.id,
        elements: prevPolygons,
      });

      const draggedPolygon = {
        ...polygon,
        coordinates,
        prevCoordinates: [],
      };

      handleSetPolygonCardLatLng(draggedPolygon);

      return R.append(draggedPolygon, polygonsWithoutCurrent);
    });
  }
}}
```

In the code snippet above, we extract the new coordinates from the `polygonInstance` changed after dragging and then update the `polygons` React state. Please, keep in mind that we should always set `prevCoordinates` to an empty array. This is due to the fact that we will implement the undo action only for changing our polygon's shape (replication of the native polygon behavior). And, finally, we need to use the `handleSetPolygonCardLatLng` to open the polygon card.

## Changing polygon shape and implementing undo functionality

It is worth mentioning that a polygon's shape can be modified in 2 different ways: by dragging either a `vertex` or an `edge`. This is where our patched `vertex` and `edge` types will come in handy. Edges and vertices are displayed automatically if you pass `editable` to your `Polygon` module:

![](https://a.storyblok.com/f/117609/1870x976/aa929dde35/edge-vertex.png)

Unfortunately, the Polygon module doesn't have either `onVertexClick` or `onEdgeClick` events, which means we will have to make use of the `onMouseUp` event and come up with a way to differentiate between the click types. It turns out, that when you interact with one of the vertices, the `MapMouseEvent` returns the coordinates of the vertex you interacted with, even if no changes were made. So, we can use these coordinates and compare them to the polygon's coordinates to find out whether there are any differences between them, and if yes, display our undo button over the point whose coordinates got changed. But when it comes to interacting with the polygon's edges, we have to apply another approach. If you drag an edge, it finally becomes another vertex of the polygon and thus creates 2 new edges placed on its respective sides. Unfortunately, in this case, the `MapMouseEvent` does not return the coordinates of this edge that was transformed into a vertex; on the contrary, it returns the coordinates of one of the 2 newly created edges, and this won't help us since we always want to display our undo button over the edge or vertex we interacted with.
The simplest solution would be to compare our polygon's coordinates with the ones extracted from the `polygonInstance` after the interaction with either an edge or a vertex is finished. Let's create a `processOnVertexOrEdgeClick` util:

```typescript
interface VertexWithChangedCoordinatesProps {
  prevCoordinates: google.maps.LatLngLiteral[];
  currentCoordinates: google.maps.LatLngLiteral[];
}

const getVertexWithChangedCoordinates = ({
  prevCoordinates,
  currentCoordinates,
}: VertexWithChangedCoordinatesProps) =>
  currentCoordinates.find(
    ({ lat: prevLat, lng: prevLng }) =>
      !prevCoordinates.some(
        ({ lat: currLat, lng: currLng }) =>
          prevLat === currLat && prevLng === currLng
      )
  );

interface processOnVertexOrEdgeClickProps {
  event: google.maps.MapMouseEvent;
  prevCoordinates: google.maps.LatLngLiteral[];
  currentCoordinates: google.maps.LatLngLiteral[];
}

export const processOnVertexOrEdgeClick = ({
  event,
  prevCoordinates,
  currentCoordinates,
}: processOnVertexOrEdgeClickProps) => {
  const isClickedOnEdgeOrVertex =
    event.edge !== undefined || event.vertex !== undefined;

  const vertexWithChangedCoordinates = isClickedOnEdgeOrVertex
    ? getVertexWithChangedCoordinates({
        prevCoordinates,
        currentCoordinates,
      })
    : undefined;

  return { isClickedOnEdgeOrVertex, vertexWithChangedCoordinates };
};
```

In `getVertexWithChangedCoordinates` we compare 2 arrays of polygon coordinates (`prevCoordinates` and `currentCoordinates`) and return the coordinates of the vertex that was dragged or the vertex that was an edge before dragging (in this case, the `currentCoordinates` array will have + 1 element). And, of course, if no changed vertex was found, we return `undefined`.
In `processOnVertexOrEdgeClick` we check if there was any interaction with either a vertex or an edge, and if yes, we use the previously discussed `getVertexWithChangedCoordinates` function and finally return 2 values: the `isClickedOnEdgeOrVertex` flag along with the `vertexWithChangedCoordinates`.

In order to display the undo button using the `vertexWithChangedCoordinates`, we need to declare the `undoData` React state:

```typescript
const [undoData, setUndoData] = React.useState<MT.UndoDataType | null>(null);
```

We will use the undoData state to display the `UndoButton` component and perform the undo action on the button click.
We also need to add the `UndoDataType` to the mapTypes.ts file, which will include our polygon `id` and `vertexWithChangedCoordinates` represented by latitude and longitude.

```typescript
export interface UndoDataType {
  id: number;
  lat: number;
  lng: number;
}
```

Let's now implement our `onMouseUp` handler:

```typescript
onMouseUp={(event) => {
  const { polygonInstance } = polygon;

  if (polygonInstance) {
    const coordinateFuncs: google.maps.LatLng[] = polygonInstance
      .getPath()
      .getArray();

    const currentCoordinates =
      coordinateFuncsToCoordinates(coordinateFuncs);

    const {
      isClickedOnEdgeOrVertex,
      vertexWithChangedCoordinates,
    } = processOnVertexOrEdgeClick({
      event,
      currentCoordinates,
      prevCoordinates: polygon.coordinates,
    });

    if (isClickedOnEdgeOrVertex && vertexWithChangedCoordinates) {
      if (polygonCardData) {
        setPolygonCardData(null);
      }

      setUndoData({
        id: polygon.id,
        lat: vertexWithChangedCoordinates.lat,
        lng: vertexWithChangedCoordinates.lng,
      });

      setPolygons((prevPolygons) => {
        const [polygonsWithoutCurrent] = dropAndReturnElementById(
          {
            id: polygon.id,
            elements: prevPolygons,
          }
        );

        return R.append(
          {
            ...polygon,
            coordinates: currentCoordinates,
            prevCoordinates: polygon.coordinates,
          },
          polygonsWithoutCurrent
        );
      });
    } else if (
      !polygonCardData &&
      !vertexWithChangedCoordinates &&
      !isClickOutsideDisabled
    ) {
      handleSetPolygonCardLatLng(polygon);
    }
  }
}}
```

In the `onMouseUp` callback we extract the polygon coordinates from the `polygonInstance` using the `processOnVertexOrEdgeClick` utility discussed above. If one of the polygon's vertices or edges is clicked and the vertex with changed coordinates is detected, we remove our polygon card, pass the necessary data to the `setUndoData` in order to display our `UndoButton` and update the polygon's state by setting the old coordinates as `prevCoordinates` and using the coordinates retrieved from the `polygonInstance` as the basic `coordinates` of the polygon.
You can also see the `else if` expression used to detect an ordinary click on the polygon (not on its vertex or edge) that just opens the polygon card if none was opened.
The last thing we need to change in our Polygon module is to disable the `UndoButton` once polygons get dragged:

```typescript
onDragStart={() => {
  setUndoData(null);
  setPolygonCardData(null);
}}
```

Now it is time to add our `UndoButton` component that will also be passed to the `GoogleMap` as children and wrapped both with the native `InfoWindow` component and `OutsideClickHandler` that we discussed earlier:

```typescript
{
  undoData && (
    <InfoWindow
      position={{
        lat: undoData.lat,
        lng: undoData.lng,
      }}
      options={{
        pixelOffset: new window.google.maps.Size(
          InfoWindowOptions.Polygon.UndoPixelOffset.X,
          InfoWindowOptions.Polygon.UndoPixelOffset.Y
        ),
      }}
    >
      <OutsideClickHandler
        onOutsideClick={() => {
          setUndoData(null);
        }}
        disabled={isClickOutsideDisabled}
      >
        <UndoButton
          onClick={() => {
            const [polygonsWithoutCurrent, currentPolygon] =
              dropAndReturnElementById({
                id: undoData.id,
                elements: polygons,
              });

            const updatedPolygons = R.append(
              {
                ...currentPolygon,
                coordinates: currentPolygon.prevCoordinates,
                prevCoordinates: [],
              },
              polygonsWithoutCurrent
            );

            setPolygons(updatedPolygons);

            setUndoData(null);
          }}
        />
      </OutsideClickHandler>
    </InfoWindow>
  );
}
```

I would like to draw your attention to what happens when the button gets clicked - we find the proper polygon item from the `polygons` state via the `id` stored in the `undoData` and update it by passing the `prevCoordinates` value to the `coordinates` key and setting `prevCoordinates` to an empty array. And, of course, we need to hide the `UndoButton` by passing `null` to the `setUndoData` state.

![](https://a.storyblok.com/f/117609/800x477/3b6987b685/ready-polygon.gif)

## Creating controlled Polylines

Drawing polylines is very similar to drawing polygons, moreover, we can define a polyline as an open polygon. It can also have edges and vertices (if the number of nodes is more than 2) and our approach to creating a controlled polyline element will be quite the same.
The part of this article dedicated to the creation of controlled polygons also involves the implementation of the polygon card, where we placed some control elements in order to change polygon properties and exemplify the way such interactive cards can be created. In the rest of the sections, we will be focusing only on drawing since you will be able to replicate the card functionality and apply it to the rest of the drawable elements.

First of all, we need to declare our polylines React state just the way we did that for polygons:

```typescript
const [distancePolylines, setDistancePolylines] = React.useState<
  MT.DistancePolylineType[]
>([]);
```

Now we need to add the `DistancePolylineType` to the `mapTypes.ts` file:

```typescript
export interface DistancePolylineType {
  id: number;
  coordinates: google.maps.LatLngLiteral[];
  polylineInstance: google.maps.Polyline | null;
}
```

You may have noticed that `DistancePolylineType` is somewhat simpler than `PolygonType` - this is so because we won't have the undo functionality, thus we don't need to store previous coordinates, and we also won't change polyline options, meaning there is no need to store their actual values.

Now we need to add to the `onOverlayComplete` callback of the `DrawingManager` the similar logic of removing a polyline instance after the drawing is finished and extracting its coordinates in order to render a controlled polyline via the Polyline module from `@react-google-maps/api`:

```typescript
if (type === google.maps.drawing.OverlayType.POLYLINE) {
  const typedOverlay = overlay as google.maps.Polyline;
  const coordinateFuncs: google.maps.LatLng[] = typedOverlay
    .getPath()
    .getArray();

  const coordinates =
    coordinateFuncsToCoordinates(coordinateFuncs);

  if (coordinates.length < POLYLINE_NODES_MIN_QUANTITY) {
    return;
  }

  setDistancePolylines((prevPolylines) => [
    ...prevPolylines,
    {
      id: R.inc(distancePolylines.length),
      coordinates,
      polylineInstance: null,
    },
  ]);

  resetDrawingToolsMode();
}
```

This condition is very similar to what we had with our polygons, the only difference is that now we use the `distancePolylines` state to store polyline items. We also don't want to create a polyline in case only 1 node exists, because obviously having just 1 node on the map is not what you expect from a polyline element. That is why we need to check the length of the `coordinates` array. And let's also declare the `POLYLINE_NODES_MIN_QUANTITY` constant at the very top of our Map component:

```typescript
const POLYLINE_NODES_MIN_QUANTITY = 2;
```

We can now pass the `Polyline` module to the `GoogleMap` as children:

```typescript
{
  distancePolylines.map((polyline) => (
    <Polyline
      editable
      draggable
      key={polyline.id}
      path={polyline.coordinates}
      onLoad={(polylineInstance) => {
        polylineInstance.setOptions(PolylineOptions);

        setDistancePolylines((prevPolylines) => {
          const [polylinesWithoutLast, lastPolyline] =
            dropAndReturnLastElement(prevPolylines);

          return [
            ...polylinesWithoutLast,
            { ...lastPolyline, polylineInstance },
          ];
        });
      }}
      onMouseUp={(event) => {
        const { polylineInstance } = polyline;

        if (polylineInstance) {
          const coordinateFuncs: google.maps.LatLng[] = polylineInstance
            .getPath()
            .getArray();

          const currentCoordinates =
            coordinateFuncsToCoordinates(coordinateFuncs);

          const { isClickedOnEdgeOrVertex, vertexWithChangedCoordinates } =
            processOnVertexOrEdgeClick({
              event,
              currentCoordinates,
              prevCoordinates: polyline.coordinates,
            });

          if (isClickedOnEdgeOrVertex && vertexWithChangedCoordinates) {
            handleChangePolylineCoordinates({
              id: polyline.id,
              coordinates: currentCoordinates,
            });
          }
        }
      }}
      onDragEnd={() => {
        const { polylineInstance } = polyline;

        if (polylineInstance) {
          const coordinateFuncs: google.maps.LatLng[] = polylineInstance
            .getPath()
            .getArray();

          const currentCoordinates =
            coordinateFuncsToCoordinates(coordinateFuncs);

          handleChangePolylineCoordinates({
            id: polyline.id,
            coordinates: currentCoordinates,
          });
        }
      }}
    />
  ));
}
```

And again, when the polyline is rendered for the first time, the logic in the `onLoad` callback is triggered - we need it to update the respective polyline item from the `distancePolylines` state with the newly obtained `polylineInstance` and use the `setOptions` method to set our predefined `PolylineOptions` from the config.
We also implement the `onMouseUp` callback to differentiate between click types and find out whether an edge or a vertex of our polyline was dragged (reusing `processOnVertexOrEdgeClick`). If yes, we then call our `handleChangePolylineCoordinates` which updates the `distancePolylines` state with the changed coordinates; the same logic is applied to the `onDragEnd` callback. Let's declare our `handleChangePolylineCoordinates` function inside the Map component:

```typescript
const handleChangePolylineCoordinates = ({
  id,
  coordinates,
}: {
  id: number,
  coordinates: google.maps.LatLngLiteral[],
}) => {
  setDistancePolylines((prevPolylines) => {
    const [polylinesWithoutCurrent, currentPolyline] = dropAndReturnElementById(
      {
        id,
        elements: prevPolylines,
      }
    );

    return R.append(
      {
        ...currentPolyline,
        coordinates,
      },
      polylinesWithoutCurrent
    );
  });
};
```

![](https://a.storyblok.com/f/117609/1964x980/28c572577c/polyline.png)

## Creating controlled Circles

In order to draw controlled circles we need to follow the same steps, but circles themselves use a bit different data - we need to define center coordinates and a radius instead of an array of coordinates that were required for polygons and polylines.

Let's begin by declaring `circles` React state:

```typescript
const [circles, setCircles] = React.useState<MT.CircleType[]>([]);
```

We also need to declare `CircleType` inside our `mapTypes.ts` file:

```typescript
export interface CircleType {
  id: number;
  radius: number;
  center: google.maps.LatLng;
  circleInstance: google.maps.Circle | null;
}
```

Now, as usual, we need to add the last if statement to the `onOverlayComplete` callback of the `DrawingManager`:

```typescript
if (type === google.maps.drawing.OverlayType.CIRCLE) {
  const typedOverlay = overlay as google.maps.Circle;
  const center = typedOverlay.getCenter();
  const radius = typedOverlay.getRadius();

  if (center) {
    setCircles((prevCircles) => [
      ...prevCircles,
      {
        center,
        radius,
        circleInstance: null,
        id: R.inc(circles.length),
      },
    ]);
  }

  resetDrawingToolsMode();

  return;
}
```

Here we have the same logic we used for polygons and polylines, and, as mentioned above, we should extract the center coordinates and radius of the newly drawn circle element via `getCenter` and `getRadius`.

Circles don't have vertices or edges, and thus our logic for updating circle items inside the `circles` state will be a bit different: we just need to implement a function that will be called in 2 callbacks: `onRadiusChanged` and `onCenterChanged`. So, let's declare `handleSetCircleDataOnCoordinatesChange` inside our Map component:

```typescript
const handleSetCircleDataOnCoordinatesChange = (circle: MT.CircleType) => {
  const { circleInstance } = circle;
  const radius = circleInstance?.getRadius();
  const center = circleInstance?.getCenter();

  if (circleInstance && radius && center) {
    setCircles((prevCircles) => {
      const [circlesWithoutCurrent] = dropAndReturnElementById({
        id: circle.id,
        elements: prevCircles,
      });

      return R.append(
        {
          ...circle,
          radius,
          center,
        },
        circlesWithoutCurrent
      );
    });
  }
};
```

As you can see, the function uses the `circleInstance` in order to get the newly changed radius and center coordinates and then updates the respective item in the `circles` state.
Finally, we can pass our Circle module to `GoogleMap` as children:

```typescript
{
  circles.map((circle) => (
    <Circle
      editable
      key={circle.id}
      center={circle.center}
      radius={circle.radius}
      onLoad={(circleInstance) => {
        circleInstance.setOptions(CircleOptions);

        setCircles((prevCircles) => {
          const [circlesWithoutLast, lastCircle] =
            dropAndReturnLastElement(prevCircles);

          return [...circlesWithoutLast, { ...lastCircle, circleInstance }];
        });
      }}
      onRadiusChanged={() => {
        handleSetCircleDataOnCoordinatesChange(circle);
      }}
      onCenterChanged={() => {
        handleSetCircleDataOnCoordinatesChange(circle);
      }}
    />
  ));
}
```

Expectedly, the logic of the `onLoad` callback works the same way as before: we take the newly created circle element from the `circles` state and update it with the `circleInstance` retrieved from the rendered Circle module. And, of course, we use the `handleSetCircleDataOnCoordinatesChange` to update the state if either radius or center coordinates get changed.

![](https://a.storyblok.com/f/117609/1448x890/ab13c67b5b/circle.png)

## Summary

This article demonstrates how the **Google Drawing Manager** can be used to draw elements in your React project. We have also covered all the main obstacles you can face when implementing this functionality, e.g., `@react-google-maps/api` does not natively provide an easy way to modify your elements (polylines, polygons, and circles), and the resulting element represents a black box with limited functionality. We have learned how to create a `controlled` element: we use the **Google Drawing Manager** just for drawing and extracting the coordinates from the newly created element, and then clear its instance and render this element via a respective module and use the extracted coordinates to place the element correctly on the map. We also replicated all the core functionality, such as dragging and modifying our elements, implemented the undo button functionality for our polygons, and added a nice card to change their props, such as color, border color, and width.

The code for a complete project can be found [here](https://github.com/chabanenkopp/react-google-maps-drawing-tools-article) - you can use it as a playground and experiment with different modules and settings. Please, don't forget to create the .env.development file in the root of your project and place a valid API key there: GATSBY\_GOOGLE\_KEY='your valid key'.
