✏️ Drawing

Spatialized founder Jozef Sorocin
Book a consultation ↗
20 min read  •  Updated 06/19/2026

What You'll Master in This Chapter

  • DrawingManager → Terra Draw Migration: Understand the concepts behind Google's (now-removed) DrawingManager, then migrate to Terra Draw with before/after code
  • Custom Freehand Drawing: Advanced techniques that rival Zillow, Airbnb, and Compass
  • GeoJSON Output: Extract, validate, and use drawn shapes (Terra Draw's biggest advantage over the old API)

Once you've grasped the concepts behind 🐉 Dragging & Editing , you can delight your users by letting them draw on your Google map.

Drawing on the map canvas enables a variety of use cases. Your users may want to:

  • Emphasize custom region(s) by drawing polygons, just like on Garages Near Me .
  • “Clip” neighborhoods or buildings out of the basemap, just like with Felt .
  • Restrict area(s) of interest by freehand drawing, just like on Zillow or Compass .

As a matter of fact, being able to draw as part of an advanced search is one of the top community feature requests at Airbnb:

Original tweet of drawing request

Original tweet link

Original tweet of drawing request

Now, implementing user-friendly map drawing capabilities is daunting but not impossible.

In this chapter you'll learn how to A) understand the concepts behind Google's Drawing Library, B) migrate to Terra Draw, and C) implement a custom, self-closing, freehand drawing tool.

2026 Update: Google's Drawing Library was deprecated in August 2025 and removed in v3.65.3b (June 4, 2026). If your app broke with an error like:

The DrawingManager functionality in the Maps JavaScript API is no longer available in the Maps JavaScript API as of version 3.65.

… you need to migrate to Terra Draw , the Google-endorsed replacement. Jump to the migration section for before/after code and a feature-parity table.

The DrawingManager tutorial below is still useful for understanding the concepts that Terra Draw builds on, but the code will no longer run on Maps JS API v3.65+.

To use Google's Drawing Library (i.e. the google.maps.drawing.DrawingManager class), you first needed to load this library as part of your initialization script :

<script
  async
  defer
  src=".../maps/api/js?key=YOUR_KEY_STARTING_WITH_AIza&libraries=drawing&callback=initMap"
></script>

From there, you could create a DrawingManager instance that looked like this by default:

The default MARKER, CIRCLE, POLYGON, POLYLINE, and RECTANGLE drawing modes.

The default MARKER, CIRCLE, POLYGON, POLYLINE, and RECTANGLE drawing modes.

The default MARKER, CIRCLE, POLYGON, POLYLINE, and RECTANGLE drawing modes.

To limit the drawing modes to e.g. only polygons, specify the drawingModes parameter .

To define the initial drawing state of the manager, set the drawingMode parameter .

const drawingManager = new google.maps.drawing.DrawingManager({
  map,

  drawingControlOptions: {
    drawingModes: [google.maps.drawing.OverlayType.POLYGON],
  },

  drawingMode: google.maps.drawing.OverlayType.POLYGON
  polygonOptions: {
    editable: true,
    draggable: true,
  },
}));

The drawback of the default drawing manager is that it is not easily customizable — the buttons are too small, there's no “delete” button, etc.

The good news is, the drawing manager's functionality can be reused to create aesthetically pleasing and functional drawing tools:

Editable & draggable polygon drawing using a custom “pencil” & “trash” trigger buttons

Let's break that down.

Our pencil button will be a traditional HTML <button> element

<button id="pencil"></button>
#pencil {
  position: absolute;
  appearance: none;
  border: none;
  border-radius: 2px;

  width: 36px;
  height: 36px;

  background: white;
  cursor: pointer;
  box-shadow: rgba(0, 0, 0, 0.3) 0px 1px 4px -1px;

  background-position: center center;
  background-size: 22px;
  background-repeat: no-repeat;

  top: 12px;
  right: 12px;
}

whose background is the encoded pencil SVG:

#pencil {
  background-image: url("data:image/svg+xml,%3Csvg width='34' height='34' viewBox='0 0 34 34' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='22.2622' width='12.8898' height='3.50367' rx='1' transform='rotate(43.322 22.2622 0)' fill='%23AC1717'/%3E%3Cpath d='M4.57277 21.7427L2.85168 30.8399C2.6367 31.9763 3.73422 32.9181 4.82477 32.5332L13.1844 29.5828C14.2488 29.2071 14.5325 27.8346 13.7042 27.0677L7.06574 20.9209C6.20058 20.1199 4.79196 20.5842 4.57277 21.7427Z' fill='%23E7C3B4' stroke='%23E3B495'/%3E%3Cpath d='M4.89312 20.1657L5.28403 20.4758L17.841 4.64311C18.0194 4.41812 18.3503 4.38932 18.5649 4.58011L27.5472 12.5644C27.7518 12.7462 27.7721 13.0587 27.5929 13.2656L14.4498 28.4307C14.272 28.6358 13.9349 28.5101 13.9349 28.2386C13.9349 27.5244 13.3559 26.9454 12.6417 26.9454H10.1945C9.95004 26.9454 9.74145 26.7687 9.70126 26.5276L9.39585 24.6951C9.27531 23.9718 8.64952 23.4417 7.91626 23.4417H5.67578C5.39963 23.4417 5.17578 23.2179 5.17578 22.9417V20.7865C5.17578 20.6737 5.21393 20.5642 5.28403 20.4758L4.89312 20.1657Z' fill='%23F5AF00' stroke='%23EEA502'/%3E%3Cpath d='M2.07456 33.6272L2.88261 29.587C2.92206 29.3897 3.19534 29.3676 3.26599 29.556L3.99754 31.5068C4.02365 31.5764 4.08622 31.6258 4.16 31.635L6.02763 31.8685C6.23008 31.8938 6.26914 32.1704 6.08161 32.2507L2.34946 33.8502C2.20145 33.9137 2.04298 33.7851 2.07456 33.6272Z' fill='%23696969' stroke='%234D4D4D' stroke-width='0.6'/%3E%3Cpath shape-rendering='geometricPrecision' d='M7.1 23.8L21.38 6.8' stroke='%23EBA000'/%3E%3Cpath shape-rendering='geometricPrecision' d='M10.5 27.2L25.46 10.2' stroke='%23EBA000'/%3E%3Crect x='19.9264' y='1.16789' width='15.1826' height='3.50367' rx='1' transform='rotate(43.322 19.9264 1.16789)' fill='%23D9D9D9'/%3E%3C/svg%3E%0A");
}

This SVG icon, when converted to a PNG , can be used as a mouse cursor image when the user hovers their mouse over the map canvas:

Speaking from personal experience, using SVGs as cursor images isn't recommended because … they don't work properly across browsers, esp. in Safari on MacOS.


In reality, the CSS selector will be a bit more complicated than just #map.will-draw:

#map.will-draw .gm-style div + div,
#map.will-draw .gm-style div div div div {
  cursor:
    url(https://.../pencil.png) 3 31,
    crosshair !important;
}

Does the .gm-style class look familiar? We talked about it in 🏷️ Custom Info Windows .


Now, the trick is to dynamically toggle the .will-draw class when the pencil button is clicked:

const toggleMapCls = () => {
  document.getElementById("map").classList.toggle("will-draw");
};

const handlePencilClick = () => {
  toggleMapCls();
  // more logic later on ...
};

document.getElementById("pencil").addEventListener("click", handlePencilClick);

Once the “pencil” button is set up, we can move onto the “trash” button.

The idea is to:

  • Keep showing the “pencil” button while the user draws their polygon.
  • Hide the “pencil” button after the polygon is finished and show the “trash” button.
  • Once the “trash” button is clicked, the polygon is removed and the “pencil” button reappears.
<button id="trash" hidden></button>
#pencil,
#trash {
  position: absolute;
  appearance: none;
  border: none;
  border-radius: 2px;

  width: 36px;
  height: 36px;

  background: white;
  cursor: pointer;
  box-shadow: rgba(0, 0, 0, 0.3) 0px 1px 4px -1px;

  background-position: center center;
  background-size: 22px;
  background-repeat: no-repeat;

  top: 12px;
  right: 12px;
}
#trash {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' stroke='none' stroke-linecap='round' stroke-linejoin='round' fill='none' shape-rendering='geometricPrecision' preserveAspectRatio='xMidYMid meet' style='vertical-align: top;'%3E%3Cpath fill='%23FF0000' d='m1.65139,6.0885l0,-1.47806a1.47806,1.47806 0 0 1 1.47806,-1.47806l5.90927,0a2.95611,2.95611 0 1 1 5.91222,0l5.91961,0a1.47806,1.47806 0 0 1 1.47806,1.47806l0,1.47806l-20.69721,0zm2.21413,2.95611l16.25861,0l-0.66808,13.3764a1.47806,1.47806 0 0 1 -1.47806,1.40415l-11.96634,0a1.47806,1.47806 0 0 1 -1.47658,-1.40415l-0.66956,-13.3764z'/%3E%3C/svg%3E");
}

To keep the toggling in sync, we can swap the elements' hidden attribute :

const toggleTrashVisibility = () => {
  document.getElementById("trash").hidden =
    !document.getElementById("trash").hidden;
};

const togglePencilVisibility = () => {
  document.getElementById("pencil").hidden =
    !document.getElementById("pencil").hidden;
};

const handleTrashClick = () => {
  toggleTrashVisibility();
  togglePencilVisibility();

  // more logic later on ...
};

document.getElementById("trash").addEventListener("click", handleTrashClick);

Now comes the fun part. We can instantiate the DrawingManager and hide it from the UI by setting the drawingControl property to false:

const initPencilDrawing = () => {
  const drawingManager = (window.drawingManager =
    new google.maps.drawing.DrawingManager({
      map: window.map,

      drawingMode: google.maps.drawing.OverlayType.POLYGON,
      drawingControl: false,

      polygonOptions: {
        editable: true,
        draggable: true,
      },
    }));

  drawingManager.addListener("polygoncomplete", handlePolygonComplete);
};

Notice that we're storing the drawingManager on the global window object for easier access throughout the app. To safely access this foreign prop in Typescript, we'll declare a global interface and store the map and drawnPolygon for later use as well:

declare global {
  interface Window {
    map?: google.maps.Map;
    drawingManager?: google.maps.drawing.DrawingManager;
    drawnPolygon?: google.maps.Polygon;
  }
}

Finally, we'll attach a handler to be notified when the polygon drawing was completed :

drawingManager.addListener("polygoncomplete", handlePolygonComplete);

In this handler, we'll:

  • Turn off the “pencil” cursor.
  • Toggle the “trash” and “pencil” buttons.
  • Store the drawnPolygon.
  • Delete the drawingManager because we don't need it anymore.
const handlePolygonComplete = (drawnPolygon: google.maps.Polygon) => {
  toggleMapCls();
  toggleTrashVisibility();
  togglePencilVisibility();

  window.drawnPolygon = drawnPolygon;

  window.drawingManager.setMap(null);
  window.drawingManager = undefined;

  // more logic later on ...
};

Note that we've used .setMap(null) on the drawing manager — just like we did when we talked about removing artifacts from the map.

So far so good. We've got a handler that'll be fired when the polygon is completed. How do we listen to polygon adjustments and dragging?

We'll recycle the knowledge from the previous chapter , including debouncing :

import { debounce } from './debounce';

export const detectChanges = (
  poly: google.maps.Polygon,
  onChange: Function
) => {
  poly.getPaths().forEach(function (path) {
    ['insert_at', 'remove_at', 'set_at'].map((evt) => {
      path.addListener(
        evt,
        debounce(() => {
          onChange();
        }, 150)
      );
    });
  });
};

const handlePolygonComplete = (drawnPolygon: google.maps.Polygon) => {
  ...
  detectChanges(window.drawnPolygon, triggerSearch);
  ...
};

This detection mechanism will cover changes to the polygon's vertices as well as to arbitrary dragging of the polygon artifact.

Assuming this example powers a search feature, we'll want to “trigger the search” when:

  • The “trash” button is clicked → the polygon has been flushed.
  • Any time a new polygon is drawn.
  • Any time an existing polygon is modified or dragged.

To achieve these goals, we'll go with:

const handleTrashClick = () => {
	...
  triggerSearch();
};

const handlePolygonComplete = (drawnPolygon: google.maps.Polygon) => {
	...
  triggerSearch();

	detectChanges(window.drawnPolygon, triggerSearch);
};

const triggerSearch = () => {
  console.info('Will trigger search', {
    polygonAsGeoJSON: serialize(window.drawnPolygon),
  });
};

Notice the serialize function in the console.info call. You'll learn more about GeoJSON & about serializing polygon coordinates in the next chapters of this book.

Here's a working demo in vanilla Typescript:

Now that you understand how DrawingManager works, here's how to achieve the same results with Terra Draw , the library Google endorses as the replacement .

Terra Draw isn't a drop-in replacement. It's a different architecture: standard GeoJSON instead of Google's proprietary overlay objects, and adapter-based support for Google Maps, Mapbox, MapLibre, OpenLayers, and Leaflet. That means your drawing code can survive the next API removal.

npm install terra-draw terra-draw-google-maps-adapter

Most DrawingManager features have Terra Draw equivalents, but a few work differently or aren't available yet:

DrawingManager Feature Terra Draw Equivalent Notes
OverlayType.POLYGON TerraDrawPolygonMode Full parity: click-to-place vertices, double-click to finish
OverlayType.POLYLINE TerraDrawLineStringMode Full parity
OverlayType.RECTANGLE TerraDrawRectangleMode Full parity
OverlayType.CIRCLE TerraDrawCircleMode Draws as a circle, but editing deforms it into a polygon (no radius constraint)
OverlayType.MARKER TerraDrawPointMode Places GeoJSON points. Custom image markers require a custom render function
Editable + draggable shapes TerraDrawSelectMode with flags Works via click-to-select, not click-and-drag (behavioral difference)
polygoncomplete event draw.on('finish', ...) Returns GeoJSON instead of a Google Maps overlay object
drawingMode switching draw.setMode('polygon') Explicit mode switching, same concept
drawingControl: false (hidden UI) No built-in UI at all You always build your own controls (which is what we did above anyway)
View-only display draw.setMode('static') Shapes render but can't be edited or selected

Known gaps: Terra Draw doesn't support custom image markers natively (point mode renders circles, not icons), doesn't fire dragend events (only continuous position updates during drag), and doesn't support click/double-click listeners on individual features. These are tracked upstream . If your app depends on any of these, you'll need workarounds or a hybrid approach.

Compare the DrawingManager code from earlier in this chapter with its Terra Draw replacement:

// ❌ DrawingManager (removed in v3.65)
const drawingManager = new google.maps.drawing.DrawingManager({
  map,
  drawingMode: google.maps.drawing.OverlayType.POLYGON,
  drawingControl: false,
  polygonOptions: { editable: true, draggable: true },
});

drawingManager.addListener('polygoncomplete', (polygon) => {
  // polygon is a google.maps.Polygon (proprietary object)
  const path = polygon.getPath().getArray();
  console.log('Coordinates:', path.map(p => ({ lat: p.lat(), lng: p.lng() })));
});
// ✅ Terra Draw (v1.x)
import { TerraDraw, TerraDrawPolygonMode, TerraDrawSelectMode } from 'terra-draw';
import { TerraDrawGoogleMapsAdapter } from 'terra-draw-google-maps-adapter';

export const initMap = () => {
  const map = new google.maps.Map(mapContainer, {
    ...
  });

  google.maps.event.addListenerOnce(map, 'projection_changed', () => {
    const draw = new TerraDraw({
      adapter: new TerraDrawGoogleMapsAdapter({
        map,
        lib: google.maps
      }),
      modes: [
        new TerraDrawPolygonMode(),
        new TerraDrawSelectMode({
          flags: {
            polygon: {
              feature: {
                draggable: true,
                coordinates: {
                  editable: true
                }
              }
            }
          }
        }),
      ],
    });

    draw.start();
    draw.setMode('polygon');

    draw.on('finish', (id, context) => {
      if (context.action !== 'draw') return;

      // GeoJSON out of the box, no conversion needed
      const snapshot = draw.getSnapshot();
      const feature = snapshot.find(f => f.id === id);
      console.log('GeoJSON:', feature);
    });
  });
};

Why projection_changed? The Google Maps adapter needs the map's projection to convert pixel coordinates to lat/lng. The projection_changed event fires once the map tile data has loaded and the projection is available. Initializing Terra Draw before this point throws an error.

One of Terra Draw's advantages is that you can register multiple drawing modes at once and switch between them. Here's a setup with polygons, rectangles, circles, and freehand:

import {
  TerraDraw,
  TerraDrawPolygonMode,
  TerraDrawRectangleMode,
  TerraDrawCircleMode,
  TerraDrawFreehandMode,
  TerraDrawSelectMode,
} from 'terra-draw';

const draw = new TerraDraw({
  adapter: new TerraDrawGoogleMapsAdapter({ map, lib: google.maps }),
  modes: [
    new TerraDrawPolygonMode(),
    new TerraDrawRectangleMode(),
    new TerraDrawCircleMode(),
    new TerraDrawFreehandMode(),
    new TerraDrawSelectMode({
      flags: {
        polygon: { feature: { draggable: true, coordinates: { editable: true } } },
        rectangle: { feature: { draggable: true, coordinates: { editable: true } } },
        circle: { feature: { draggable: true, coordinates: { editable: true } } },
        freehand: { feature: { draggable: true } },
      },
    }),
  ],
});

draw.start();

// Wire up your UI buttons
document.getElementById('polygon-btn').addEventListener('click', () => draw.setMode('polygon'));
document.getElementById('rectangle-btn').addEventListener('click', () => draw.setMode('rectangle'));
document.getElementById('circle-btn').addEventListener('click', () => draw.setMode('circle'));
document.getElementById('freehand-btn').addEventListener('click', () => draw.setMode('freehand'));
document.getElementById('select-btn').addEventListener('click', () => draw.setMode('select'));
document.getElementById('clear-btn').addEventListener('click', () => draw.clear());

This is Terra Draw's biggest win over DrawingManager. Every shape you draw is stored as standard GeoJSON . No conversion, no getPath().getArray().map(...) gymnastics.

// Get all drawn features as a GeoJSON FeatureCollection
const snapshot = draw.getSnapshot();
const featureCollection = {
  type: 'FeatureCollection',
  features: snapshot,
};

// Send to your API
await fetch('/api/shapes', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(featureCollection),
});

// Validate with Turf.js
import { kinks } from '@turf/kinks';
if (snapshot.length > 0) {
  const selfIntersections = kinks(snapshot[0]);
  if (selfIntersections.features.length > 0) {
    console.warn('Polygon has self-intersections');
  }
}

You can also load saved GeoJSON back into Terra Draw:

draw.addFeatures([
  {
    type: 'Feature',
    geometry: {
      type: 'Polygon',
      coordinates: [[[16.59, 43.46], [16.60, 43.46], [16.60, 43.47], [16.59, 43.47], [16.59, 43.46]]],
    },
    properties: { mode: 'polygon' },
  },
]);

The properties.mode field tells Terra Draw which mode owns the feature. Without it, the feature renders but can't be selected or edited. Set it to match the mode name you registered ('polygon', 'rectangle', 'freehand', etc.).

With DrawingManager, you had to wire up insert_at, remove_at, and set_at path events manually (as we did in the detecting changes section above). Terra Draw simplifies this with a single change event:

draw.on('change', (ids, type) => {
  // type is 'create' | 'update' | 'delete'
  // ids is an array of feature IDs that changed
  const snapshot = draw.getSnapshot();
  const changed = snapshot.filter(f => ids.includes(f.id));

  triggerSearch(changed);
});

If you're evaluating map libraries beyond Google Maps, Terra Draw works the same way across all of them. The only thing that changes is the adapter:

// Google Maps
import { TerraDrawGoogleMapsAdapter } from 'terra-draw-google-maps-adapter';
const adapter = new TerraDrawGoogleMapsAdapter({ map, lib: google.maps });

// Mapbox GL JS
import { TerraDrawMapboxGLAdapter } from 'terra-draw-mapbox-gl-adapter';
const adapter = new TerraDrawMapboxGLAdapter({ map, lib: mapboxgl });

// MapLibre GL JS
import { TerraDrawMapLibreGLAdapter } from 'terra-draw-maplibre-gl-adapter';
const adapter = new TerraDrawMapLibreGLAdapter({ map, lib: maplibregl });

// Leaflet
import { TerraDrawLeafletAdapter } from 'terra-draw-leaflet-adapter';
const adapter = new TerraDrawLeafletAdapter({ map, lib: L });

Your drawing modes, event handlers, and GeoJSON output stay the same. If you ever switch from Google Maps to MapLibre (or need to support both), the migration is one line.

Need help migrating a complex DrawingManager setup? Get in touch .


I'm running a real estate search website and want to let my users draw on the map with a freehand “pencil tool.” The drawing should produce a polygon and self-close if the starting & ending points aren't equal.

Screen recording courtesy of Garages Near Me . The logic is heavily inspired by this SO answer .

Google Maps' DrawingManager never supported freehand drawing (and is now removed), so we'll write our own. The principles below apply regardless of which library you use, or even if you roll your own.

To start off, we can reuse the “pencil” cursor and “pencil/trash” buttons from above .

From there, we'll need to think about what needs to happen when the “pencil” button is pressed:

  1. We'll want to track when the mouse/trackpad is pressed by listening to an initial mousedown DOM event on the map's div. This will trigger the drawing mode.
  2. Next, we'll create a Polyline . The polyline's path will be continuously updated by listening to the map's mousemove event .
  3. Then, we'll track when the mouse/trackpad is released by listening to the map div's mouseup event .
  4. Finally, we'll extract the polyline's path and use it to initialize the final polygon.

During the drawing process, the user might accidentally drag the map or click on the POIs .

To prevent this from happening, we'll disable the applicable options :

export const toggleMapInteractivity = (isOn: boolean) => {
  window.map.setOptions({
    draggable: isOn,
    zoomControl: isOn,
    scrollwheel: isOn,
    disableDoubleClickZoom: isOn,
    clickableIcons: isOn,
  });
};

To listen to the mousedown DOM event, you may be tempted to use google.maps.event.addDomListenerOnce . Bear in mind that this method is now deprecated and you should use .addEventListener instead :

const initPencilDrawing = () => {
  toggleMapInteractivity(false);

  window.map.getDiv().addEventListener(
    "mousedown",
    () => {
      drawFreehand(onDrawingComplete);
    },
    { once: true }, // <-- one-time listener
  );
};

In the the drawFreehand function, we'll initialize a non-clickable and non-draggable Polyline:

export const drawFreehand = (onDrawingComplete: Function) => {
  // the freehand polyline
  const polyline = new google.maps.Polyline({
    map: window.map,
    clickable: false,
    draggable: false,
  });

  ...

Next, we'll listen to the mousemove event and update the polyline's path by calling .push() on it:

...

// the move-listener
const move = google.maps.event.addListener(
  window.map,
  'mousemove',
  (e: google.maps.MapMouseEvent) => {
    polyline.getPath().push(e.latLng);
  }
);

...

A polyline's path can also be updated by overriding its path like so:

polyline.setPath([...]) or polyline.setOptions({ path: [...] })

but polyline.getPath().push(...) is more elegant.

Finally, we'll register a one-time mouseup event on the map.

In its handler, we'll flush the move listener.

Notice how we stored it in a variable above to be able to remove it now.

The same applies to the mousedown event.

This time, we're using .clearListeners() which is a handy wrapper around the DOM's .removeEventListener() utility.

Next, we'll construct a Polygon from the polyline's path, turn the map's interactivity back on, and call the onDrawingComplete callback to swap the ”pencil/trash” buttons:

 // the mouseup-listener
  google.maps.event.addListenerOnce(window.map, 'mouseup', () => {
    google.maps.event.removeListener(move);
    google.maps.event.clearListeners(window.map.getDiv(), 'mousedown');

    // get rid of the polyline used for drawing assistance
    polyline.setMap(null);

    // construct a polygon from the polyline's path
    window.drawnPolygon = new google.maps.Polygon({
      map: window.map,
      paths: polyline.getPath().getArray(),
      draggable: true,
      editable: true,
    });

    toggleMapInteractivity(true);

    // we're done
    onDrawingComplete();
  });
};

And that's all there's to it. The drawing functionality is self-closing and the final polygon is draggable & editable:

Now that you understand the mechanics of freehand drawing, here's how TerraDrawFreehandMode handles the same job. It takes care of event management, touch support, and self-closing polygons for you.

If you built your own freehand drawing (like the custom approach above) and never used DrawingManager, you don't need to switch to Terra Draw. Your custom implementation still works fine on v3.65+.

import { TerraDraw, TerraDrawFreehandMode } from 'terra-draw';
import { TerraDrawGoogleMapsAdapter } from 'terra-draw-google-maps-adapter';

let terraDraw: TerraDraw;

const initTerraDraw = () => {
  ...
  map.addListener("projection_changed", () => {
    terraDraw = new TerraDraw({
      adapter: new TerraDrawGoogleMapsAdapter({
        map,
        lib: google.maps,
        coordinatePrecision: 6
      }),
      modes: [new TerraDrawFreehandMode()]
    });

    terraDraw.start();

    // Wait for adapter to be ready
    terraDraw.on('ready', () => {
      bindControlBtns();
    });
  });
};

const bindControlBtns = () => {
  const pencilBtn = document.getElementById("pencil") as HTMLButtonElement;
  const trashBtn = document.getElementById("trash") as HTMLButtonElement;

  pencilBtn.addEventListener("click", () => {
    terraDraw.setMode('freehand');
    toggleMapCls();
    togglePencilVisibility();
  });

  trashBtn.addEventListener("click", () => {
    terraDraw.clear();
    toggleTrashVisibility();
    togglePencilVisibility();
    toggleMapCls();
  });

  terraDraw.on('finish', () => {
    terraDraw.setMode('static');
    toggleMapCls();
    toggleTrashVisibility();
    togglePencilVisibility();
    
    triggerSearch();
  });
};

Here's a migrated Terra Draw freehand drawing implementation:

Some geospatial databases don't support intersecting polygons so you may need to split the drawn polygon at the points of intersection.

To check if a polygon is self-intersecting, use the @turf/kinks utillity .
To split the polygon, use @turf/unkink-polygon .

No matter if you're using Terra Draw or your own freehand drawing tool, you may want to check out Google's Snap to Roads API .


Now that you've put Drawing in your Google Maps toolkit, it's time to talk about 🔄 4. Interoperability & Conversions .