Draw on a map using Terra Draw

The following example shows how to add a drawing layer to a map using Terra Draw JavaScript libraries. The layer relies on OverlayView and enables users to draw, edit, and select features like polygons, lines, and points directly on the map interface. All geometries created by the user are returned as standard GeoJSON objects.

TypeScript

import { Loader } from '@googlemaps/js-api-loader';

import {
  TerraDraw,
  TerraDrawSelectMode,
  TerraDrawPointMode,
  TerraDrawLineStringMode,
  TerraDrawPolygonMode,
  TerraDrawRectangleMode,
  TerraDrawCircleMode,
  TerraDrawFreehandMode
} from 'terra-draw';
import { TerraDrawGoogleMapsAdapter } from 'terra-draw-google-maps-adapter';


const colorPalette = [
  "#E74C3C",
  "#FF0066",
  "#9B59B6",
  "#673AB7",
  "#3F51B5",
  "#3498DB",
  "#03A9F4",
  "#00BCD4",
  "#009688",
  "#27AE60",
  "#8BC34A",
  "#CDDC39",
  "#F1C40F",
  "#FFC107",
  "#F39C12",
  "#FF5722",
  "#795548"
];

const getRandomColor = () => colorPalette[Math.floor(Math.random() * colorPalette.length)] as `#${string}`;

function processSnapshotForUndo(snapshot: any[]): any[] {
    // console.log("Processing snapshot for undo:", snapshot);
    return snapshot.map(feature => {
        const newFeature = JSON.parse(JSON.stringify(feature));

        if (newFeature.properties.mode === 'rectangle') {
            // console.log("Processing rectangle for undo:", newFeature);
            newFeature.geometry.type = 'Polygon';
            newFeature.properties.mode = 'polygon';
        } else if (newFeature.properties.mode === 'circle') {
            // console.log("Processing circle for undo:", newFeature);
            newFeature.geometry.type = 'Polygon';
            // The radius is already in properties, so we just need to ensure the mode is correct for re-creation
            newFeature.properties.mode = 'circle';
        }
        return newFeature;
    });
}

function setupModeButtons(): void {
  const modeUI = document.getElementById('mode-ui');
  if (!modeUI) {
    return;
  }

  const modeButtons: { [key: string]: string } = {
    'select-mode': 'select',
    'point-mode': 'point',
    'linestring-mode': 'linestring',
    'polygon-mode': 'polygon',
    'rectangle-mode': 'rectangle',
    'circle-mode': 'circle',
    'freehand-mode': 'freehand',
    'clear-mode': 'static'
  };

  for (const buttonId in modeButtons) {
    const button = document.getElementById(buttonId);
    if (button) {
      button.onclick = () => {
        setActiveButton(buttonId);
        const modeName = modeButtons[buttonId];

        if (!draw) {
          return;
        }
        if (modeName === 'static') {
          draw.clear();
          draw.setMode('static');
        } else if (modeName) {
          draw.setMode(modeName);
        }
      };
    }
  }
}

function setActiveButton(buttonId: string): void {
    const buttons = document.querySelectorAll('.mode-button');
    const resizeButton = document.getElementById('resize-button');
    const isResizeActive = resizeButton?.classList.contains('active');

    buttons.forEach(button => {
        if (button.id !== 'resize-button') {
            button.classList.remove('active');
        }
    });

    const activeButton = document.getElementById(buttonId);
    if (activeButton) {
        activeButton.classList.add('active');
    }

    if (isResizeActive) {
        resizeButton?.classList.add('active');
    }
}

function initUI(): void {
  setActiveButton('point-mode');
}

let map: google.maps.Map;
let draw: TerraDraw;
let currentMode: string = 'static';
let history: any[] = [];
let redoHistory: any[] = [];
let selectedFeatureId: string | null = null;
let isRestoring = false;
let resizingEnabled = false;
let debounceTimeout: number | undefined;

const loader = new Loader({
  apiKey: "AIzaSyA6myHzS10YXdcazAFalmXvDkrYCp5cLc8",
  version: "weekly",
  libraries: ["maps", "drawing", "marker"]
});

loader.load().then(async () => {
  try {
    const { Map } = await google.maps.importLibrary("maps") as google.maps.MapsLibrary;
    const { LatLngBounds } = await google.maps.importLibrary("core") as google.maps.CoreLibrary;
    const { Data } = await google.maps.importLibrary("maps") as google.maps.MapsLibrary;

    const mapOptions: google.maps.MapOptions = {
      center: { lat: 48.862, lng: 2.342 },
      zoom: 12,
      mapId:'c306b3c6dd3ed8d9', // raster '6a17c323f461e521',
      mapTypeId: 'roadmap',
      zoomControl:false,
      tilt: 45,
      mapTypeControl: true,
      clickableIcons:false,
      streetViewControl:false,
      fullscreenControl:false,
    };

    const mapDiv = document.getElementById("map") as HTMLElement;
    map = new Map(mapDiv, mapOptions);

    map.addListener("click", () => {
        if (draw) {
            console.log("Current draw mode on map click:", draw.getMode());
        }
    });

    map.addListener("projection_changed", () => {


      draw = new TerraDraw({
        adapter: new TerraDrawGoogleMapsAdapter({ map, lib: google.maps, coordinatePrecision: 9 }),
        modes: [
            new TerraDrawSelectMode({
                flags: {
                    polygon: {
                        feature: {
                            draggable: true,
                            rotateable: true,
                            coordinates: {
                                midpoints: true,
                                draggable: true,
                                deletable: true,
                            },
                        },
                    },
                    linestring: {
                        feature: {
                            draggable: true,
                            rotateable: true,
                            coordinates: {
                                midpoints: true,
                                draggable: true,
                                deletable: true,
                            },
                        },
                    },
                    point: {
                        feature: {
                            draggable: true,
                            rotateable: true,
                        },
                    },
                    rectangle: {
                        feature: {
                            draggable: true,
                            rotateable: true,
                            coordinates: {
                                midpoints: true,
                                draggable: true,
                                deletable: true,
                            },
                        },
                    },
                    circle: {
                        feature: {
                            draggable: true,
                            rotateable: true,
                            coordinates: {
                                midpoints: true,
                                draggable: true,
                                deletable: true,
                            },
                        },
                    },
                    freehand: {
                        feature: {
                            draggable: true,
                            rotateable: true,
                            coordinates: {
                                midpoints: true,
                                draggable: true,
                                deletable: true,
                            },
                        },
                    },
                },
            }),

            new TerraDrawPointMode({
                editable: true,
                styles: { pointColor: getRandomColor() },
            }),
            new TerraDrawLineStringMode({
                editable: true,
                styles: { lineStringColor: getRandomColor() },
            }),
            new TerraDrawPolygonMode({
                editable: true,
                styles: (() => {
                    const color = getRandomColor();
                    return {
                        fillColor: color,
                        outlineColor: color,
                    };
                })(),
            }),
            new TerraDrawRectangleMode({
                styles: (() => {
                    const color = getRandomColor();
                    return {
                        fillColor: color,
                        outlineColor: color,
                    };
                })(),
            }),
            new TerraDrawCircleMode({
                styles: (() => {
                    const color = getRandomColor();
                    return {
                        fillColor: color,
                        outlineColor: color,
                    };
                })(),
            }),
            new TerraDrawFreehandMode({
                styles: (() => {
                    const color = getRandomColor();
                    return {
                        fillColor: color,
                        outlineColor: color,
                    };
                })(),
            }),
       ],
     });

      draw.start();


      draw.on('ready', () => {
        console.log("TerraDraw is ready!");
        initUI();
        setupModeButtons();
        draw.setMode('point');
        currentMode = 'point';
        setActiveButton('point-mode');

        draw.on("select", (id) => {
            // console.log(`Feature selected: ${id}`);
            if (selectedFeatureId && selectedFeatureId !== id) {
                draw.deselectFeature(selectedFeatureId);
            }
            selectedFeatureId = id as string;
        });

        draw.on("deselect", () => {
            // console.log("Feature deselected");
            selectedFeatureId = null;
        });

        history.push(processSnapshotForUndo(draw.getSnapshot())); // Push initial empty state

        draw.on("change", (ids, type) => {
            if (isRestoring) {
                return;
            }

            if (debounceTimeout) {
                clearTimeout(debounceTimeout);
            }

            debounceTimeout = window.setTimeout(() => {
                const snapshot = draw.getSnapshot();
                const processedSnapshot = processSnapshotForUndo(snapshot);
                const filteredSnapshot = processedSnapshot.filter(
                    (f) => !f.properties.midPoint && !f.properties.selectionPoint
                );
                history.push(filteredSnapshot);
                redoHistory = [];
            }, 200);
        });


        const exportButton = document.getElementById('export-button');
        if (exportButton) {
            exportButton.onclick = () => {
                const features = draw.getSnapshot();
                const geojson = {
                    type: "FeatureCollection",
                    features: features,
                };
                const data = JSON.stringify(geojson, null, 2);
                const blob = new Blob([data], { type: "text/plain" });
                const url = URL.createObjectURL(blob);
                const link = document.createElement("a");
                link.href = url;
                link.download = "drawing.geojson";
                link.click();
                URL.revokeObjectURL(url);
            };
        }

        const uploadButton = document.getElementById('upload-button');
        const uploadInput = document.getElementById('upload-input') as HTMLInputElement;

        if (uploadButton && uploadInput) {
            uploadButton.onclick = () => {
                uploadInput.click();
            };

            uploadInput.onchange = (event) => {
                const file = (event.target as HTMLInputElement).files?.[0];
                if (file) {
                    const reader = new FileReader();
                    reader.onload = (e) => {
                        try {
                            const geojson = JSON.parse(e.target?.result as string);
                            if (geojson.type === "FeatureCollection") {
                                draw.addFeatures(geojson.features);
                            } else {
                                alert("Invalid GeoJSON file: must be a FeatureCollection.");
                            }
                        } catch (error) {
                            alert("Error parsing GeoJSON file.");
                        }
                    };
                    reader.readAsText(file);
                }
            };
        }

        const resizeButton = document.getElementById('resize-button');
        if (resizeButton) {
            resizeButton.onclick = () => {
                resizingEnabled = !resizingEnabled;
                resizeButton.classList.toggle('active', resizingEnabled);

                const flags = {
                    polygon: { feature: { draggable: true, coordinates: { resizable: resizingEnabled ? 'center' : undefined, draggable: !resizingEnabled } } },
                    linestring: { feature: { draggable: true, coordinates: { resizable: resizingEnabled ? 'center' : undefined, draggable: !resizingEnabled } } },
                    rectangle: { feature: { draggable: true, coordinates: { resizable: resizingEnabled ? 'center' : undefined, draggable: !resizingEnabled } } },
                    circle: { feature: { draggable: true, coordinates: { resizable: resizingEnabled ? 'center' : undefined, draggable: !resizingEnabled } } },
                    freehand: { feature: { draggable: true, coordinates: { resizable: resizingEnabled ? 'center' : undefined, draggable: !resizingEnabled } } },
                };

                console.log("Updating flags:", flags);
                draw.updateModeOptions('select', { flags });
            };
        }

        const deleteSelectedButton = document.getElementById('delete-selected-button');
        if (deleteSelectedButton) {
            deleteSelectedButton.onclick = () => {
                if (selectedFeatureId) {
                    draw.removeFeatures([selectedFeatureId]);
                } else {
                    const features = draw.getSnapshot();
                    if (features.length > 0) {
                        const lastFeature = features[features.length - 1];
                        if (lastFeature.id) {
                            draw.removeFeatures([lastFeature.id]);
                        }
                    }
                }
            };
        }

        const undoButton = document.getElementById('undo-button');
        if (undoButton) {
            undoButton.onclick = () => {
                if (history.length > 1) {
                    redoHistory.push(history.pop());
                    const snapshotToRestore = history[history.length - 1];
                    console.log("Restoring snapshot (undo):", snapshotToRestore);
                    isRestoring = true;
                    draw.clear();
                    draw.addFeatures(snapshotToRestore);
                    setTimeout(() => { isRestoring = false; }, 0);
                }
            };
        }

        const redoButton = document.getElementById('redo-button');
        if (redoButton) {
            redoButton.onclick = () => {
                if (redoHistory.length > 0) {
                    const snapshot = redoHistory.pop();
                    console.log("Restoring snapshot (redo):", snapshot);
                    history.push(snapshot);
                    isRestoring = true;
                    draw.clear();
                    draw.addFeatures(snapshot);
                    setTimeout(() => { isRestoring = false; }, 0);
                }
            };
        }
      });

      function rotateFeature(feature, angle) {
          const newFeature = JSON.parse(JSON.stringify(feature));
          const coordinates = newFeature.geometry.coordinates;
          const center = getCenter(coordinates);

          const rotatedCoordinates = coordinates.map(ring => {
              return ring.map(point => {
                  const x = point[0] - center[0];
                  const y = point[1] - center[1];
                  const newX = x * Math.cos(angle * Math.PI / 180) - y * Math.sin(angle * Math.PI / 180);
                  const newY = x * Math.sin(angle * Math.PI / 180) + y * Math.cos(angle * Math.PI / 180);
                  return [newX + center[0], newY + center[1]];
              });
          });

          newFeature.geometry.coordinates = rotatedCoordinates;
          return newFeature;
      }

      function getCenter(coordinates) {
          let x = 0;
          let y = 0;
          let count = 0;
          coordinates.forEach(ring => {
              ring.forEach(point => {
                  x += point[0];
                  y += point[1];
                  count++;
              });
          });
          return [x / count, y / count];
      }

        document.addEventListener('keydown', (event) => {
            if (event.key === 'r' && selectedFeatureId) {
                const features = draw.getSnapshot();
                const selectedFeature = features.find(f => f.id === selectedFeatureId);

                if (selectedFeature) {
                    const newFeature = rotateFeature(selectedFeature, 15);
                    draw.addFeatures([newFeature]);
                }
            }
        });
    });

  } catch (e) {
    console.error("Error loading Google Maps API:", e);
  }
}).catch(e => {
  console.error("Error loading Google Maps API:", e);
});

JavaScript

import { Loader } from '@googlemaps/js-api-loader';
import { TerraDraw, TerraDrawSelectMode, TerraDrawPointMode, TerraDrawLineStringMode, TerraDrawPolygonMode, TerraDrawRectangleMode, TerraDrawCircleMode, TerraDrawFreehandMode } from 'terra-draw';
import { TerraDrawGoogleMapsAdapter } from 'terra-draw-google-maps-adapter';
const colorPalette = [
    "#E74C3C",
    "#FF0066",
    "#9B59B6",
    "#673AB7",
    "#3F51B5",
    "#3498DB",
    "#03A9F4",
    "#00BCD4",
    "#009688",
    "#27AE60",
    "#8BC34A",
    "#CDDC39",
    "#F1C40F",
    "#FFC107",
    "#F39C12",
    "#FF5722",
    "#795548"
];
const getRandomColor = () => colorPalette[Math.floor(Math.random() * colorPalette.length)];
function processSnapshotForUndo(snapshot) {
    // console.log("Processing snapshot for undo:", snapshot);
    return snapshot.map(feature => {
        const newFeature = JSON.parse(JSON.stringify(feature));
        if (newFeature.properties.mode === 'rectangle') {
            // console.log("Processing rectangle for undo:", newFeature);
            newFeature.geometry.type = 'Polygon';
            newFeature.properties.mode = 'polygon';
        }
        else if (newFeature.properties.mode === 'circle') {
            // console.log("Processing circle for undo:", newFeature);
            newFeature.geometry.type = 'Polygon';
            // The radius is already in properties, so we just need to ensure the mode is correct for re-creation
            newFeature.properties.mode = 'circle';
        }
        return newFeature;
    });
}
function setupModeButtons() {
    const modeUI = document.getElementById('mode-ui');
    if (!modeUI) {
        return;
    }
    const modeButtons = {
        'select-mode': 'select',
        'point-mode': 'point',
        'linestring-mode': 'linestring',
        'polygon-mode': 'polygon',
        'rectangle-mode': 'rectangle',
        'circle-mode': 'circle',
        'freehand-mode': 'freehand',
        'clear-mode': 'static'
    };
    for (const buttonId in modeButtons) {
        const button = document.getElementById(buttonId);
        if (button) {
            button.onclick = () => {
                setActiveButton(buttonId);
                const modeName = modeButtons[buttonId];
                if (!draw) {
                    return;
                }
                if (modeName === 'static') {
                    draw.clear();
                    draw.setMode('static');
                }
                else if (modeName) {
                    draw.setMode(modeName);
                }
            };
        }
    }
}
function setActiveButton(buttonId) {
    const buttons = document.querySelectorAll('.mode-button');
    const resizeButton = document.getElementById('resize-button');
    const isResizeActive = resizeButton?.classList.contains('active');
    buttons.forEach(button => {
        if (button.id !== 'resize-button') {
            button.classList.remove('active');
        }
    });
    const activeButton = document.getElementById(buttonId);
    if (activeButton) {
        activeButton.classList.add('active');
    }
    if (isResizeActive) {
        resizeButton?.classList.add('active');
    }
}
function initUI() {
    setActiveButton('point-mode');
}
let map;
let draw;
let currentMode = 'static';
let history = [];
let redoHistory = [];
let selectedFeatureId = null;
let isRestoring = false;
let resizingEnabled = false;
let debounceTimeout;
const loader = new Loader({
    apiKey: "AIzaSyA6myHzS10YXdcazAFalmXvDkrYCp5cLc8",
    version: "weekly",
    libraries: ["maps", "drawing", "marker"]
});
loader.load().then(async () => {
    try {
        const { Map } = await google.maps.importLibrary("maps");
        const { LatLngBounds } = await google.maps.importLibrary("core");
        const { Data } = await google.maps.importLibrary("maps");
        const mapOptions = {
            center: { lat: 48.862, lng: 2.342 },
            zoom: 12,
            mapId: 'c306b3c6dd3ed8d9', // raster '6a17c323f461e521',
            mapTypeId: 'roadmap',
            zoomControl: false,
            tilt: 45,
            mapTypeControl: true,
            clickableIcons: false,
            streetViewControl: false,
            fullscreenControl: false,
        };
        const mapDiv = document.getElementById("map");
        map = new Map(mapDiv, mapOptions);
        map.addListener("click", () => {
            if (draw) {
                console.log("Current draw mode on map click:", draw.getMode());
            }
        });
        map.addListener("projection_changed", () => {
            draw = new TerraDraw({
                adapter: new TerraDrawGoogleMapsAdapter({ map, lib: google.maps, coordinatePrecision: 9 }),
                modes: [
                    new TerraDrawSelectMode({
                        flags: {
                            polygon: {
                                feature: {
                                    draggable: true,
                                    rotateable: true,
                                    coordinates: {
                                        midpoints: true,
                                        draggable: true,
                                        deletable: true,
                                    },
                                },
                            },
                            linestring: {
                                feature: {
                                    draggable: true,
                                    rotateable: true,
                                    coordinates: {
                                        midpoints: true,
                                        draggable: true,
                                        deletable: true,
                                    },
                                },
                            },
                            point: {
                                feature: {
                                    draggable: true,
                                    rotateable: true,
                                },
                            },
                            rectangle: {
                                feature: {
                                    draggable: true,
                                    rotateable: true,
                                    coordinates: {
                                        midpoints: true,
                                        draggable: true,
                                        deletable: true,
                                    },
                                },
                            },
                            circle: {
                                feature: {
                                    draggable: true,
                                    rotateable: true,
                                    coordinates: {
                                        midpoints: true,
                                        draggable: true,
                                        deletable: true,
                                    },
                                },
                            },
                            freehand: {
                                feature: {
                                    draggable: true,
                                    rotateable: true,
                                    coordinates: {
                                        midpoints: true,
                                        draggable: true,
                                        deletable: true,
                                    },
                                },
                            },
                        },
                    }),
                    new TerraDrawPointMode({
                        editable: true,
                        styles: { pointColor: getRandomColor() },
                    }),
                    new TerraDrawLineStringMode({
                        editable: true,
                        styles: { lineStringColor: getRandomColor() },
                    }),
                    new TerraDrawPolygonMode({
                        editable: true,
                        styles: (() => {
                            const color = getRandomColor();
                            return {
                                fillColor: color,
                                outlineColor: color,
                            };
                        })(),
                    }),
                    new TerraDrawRectangleMode({
                        styles: (() => {
                            const color = getRandomColor();
                            return {
                                fillColor: color,
                                outlineColor: color,
                            };
                        })(),
                    }),
                    new TerraDrawCircleMode({
                        styles: (() => {
                            const color = getRandomColor();
                            return {
                                fillColor: color,
                                outlineColor: color,
                            };
                        })(),
                    }),
                    new TerraDrawFreehandMode({
                        styles: (() => {
                            const color = getRandomColor();
                            return {
                                fillColor: color,
                                outlineColor: color,
                            };
                        })(),
                    }),
                ],
            });
            draw.start();
            draw.on('ready', () => {
                console.log("TerraDraw is ready!");
                initUI();
                setupModeButtons();
                draw.setMode('point');
                currentMode = 'point';
                setActiveButton('point-mode');
                draw.on("select", (id) => {
                    // console.log(`Feature selected: ${id}`);
                    if (selectedFeatureId && selectedFeatureId !== id) {
                        draw.deselectFeature(selectedFeatureId);
                    }
                    selectedFeatureId = id;
                });
                draw.on("deselect", () => {
                    // console.log("Feature deselected");
                    selectedFeatureId = null;
                });
                history.push(processSnapshotForUndo(draw.getSnapshot())); // Push initial empty state
                draw.on("change", (ids, type) => {
                    if (isRestoring) {
                        return;
                    }
                    if (debounceTimeout) {
                        clearTimeout(debounceTimeout);
                    }
                    debounceTimeout = window.setTimeout(() => {
                        const snapshot = draw.getSnapshot();
                        const processedSnapshot = processSnapshotForUndo(snapshot);
                        const filteredSnapshot = processedSnapshot.filter((f) => !f.properties.midPoint && !f.properties.selectionPoint);
                        history.push(filteredSnapshot);
                        redoHistory = [];
                    }, 200);
                });
                const exportButton = document.getElementById('export-button');
                if (exportButton) {
                    exportButton.onclick = () => {
                        const features = draw.getSnapshot();
                        const geojson = {
                            type: "FeatureCollection",
                            features: features,
                        };
                        const data = JSON.stringify(geojson, null, 2);
                        const blob = new Blob([data], { type: "text/plain" });
                        const url = URL.createObjectURL(blob);
                        const link = document.createElement("a");
                        link.href = url;
                        link.download = "drawing.geojson";
                        link.click();
                        URL.revokeObjectURL(url);
                    };
                }
                const uploadButton = document.getElementById('upload-button');
                const uploadInput = document.getElementById('upload-input');
                if (uploadButton && uploadInput) {
                    uploadButton.onclick = () => {
                        uploadInput.click();
                    };
                    uploadInput.onchange = (event) => {
                        const file = event.target.files?.[0];
                        if (file) {
                            const reader = new FileReader();
                            reader.onload = (e) => {
                                try {
                                    const geojson = JSON.parse(e.target?.result);
                                    if (geojson.type === "FeatureCollection") {
                                        draw.addFeatures(geojson.features);
                                    }
                                    else {
                                        alert("Invalid GeoJSON file: must be a FeatureCollection.");
                                    }
                                }
                                catch (error) {
                                    alert("Error parsing GeoJSON file.");
                                }
                            };
                            reader.readAsText(file);
                        }
                    };
                }
                const resizeButton = document.getElementById('resize-button');
                if (resizeButton) {
                    resizeButton.onclick = () => {
                        resizingEnabled = !resizingEnabled;
                        resizeButton.classList.toggle('active', resizingEnabled);
                        const flags = {
                            polygon: { feature: { draggable: true, coordinates: { resizable: resizingEnabled ? 'center' : undefined, draggable: !resizingEnabled } } },
                            linestring: { feature: { draggable: true, coordinates: { resizable: resizingEnabled ? 'center' : undefined, draggable: !resizingEnabled } } },
                            rectangle: { feature: { draggable: true, coordinates: { resizable: resizingEnabled ? 'center' : undefined, draggable: !resizingEnabled } } },
                            circle: { feature: { draggable: true, coordinates: { resizable: resizingEnabled ? 'center' : undefined, draggable: !resizingEnabled } } },
                            freehand: { feature: { draggable: true, coordinates: { resizable: resizingEnabled ? 'center' : undefined, draggable: !resizingEnabled } } },
                        };
                        console.log("Updating flags:", flags);
                        draw.updateModeOptions('select', { flags });
                    };
                }
                const deleteSelectedButton = document.getElementById('delete-selected-button');
                if (deleteSelectedButton) {
                    deleteSelectedButton.onclick = () => {
                        if (selectedFeatureId) {
                            draw.removeFeatures([selectedFeatureId]);
                        }
                        else {
                            const features = draw.getSnapshot();
                            if (features.length > 0) {
                                const lastFeature = features[features.length - 1];
                                if (lastFeature.id) {
                                    draw.removeFeatures([lastFeature.id]);
                                }
                            }
                        }
                    };
                }
                const undoButton = document.getElementById('undo-button');
                if (undoButton) {
                    undoButton.onclick = () => {
                        if (history.length > 1) {
                            redoHistory.push(history.pop());
                            const snapshotToRestore = history[history.length - 1];
                            console.log("Restoring snapshot (undo):", snapshotToRestore);
                            isRestoring = true;
                            draw.clear();
                            draw.addFeatures(snapshotToRestore);
                            setTimeout(() => { isRestoring = false; }, 0);
                        }
                    };
                }
                const redoButton = document.getElementById('redo-button');
                if (redoButton) {
                    redoButton.onclick = () => {
                        if (redoHistory.length > 0) {
                            const snapshot = redoHistory.pop();
                            console.log("Restoring snapshot (redo):", snapshot);
                            history.push(snapshot);
                            isRestoring = true;
                            draw.clear();
                            draw.addFeatures(snapshot);
                            setTimeout(() => { isRestoring = false; }, 0);
                        }
                    };
                }
            });
            function rotateFeature(feature, angle) {
                const newFeature = JSON.parse(JSON.stringify(feature));
                const coordinates = newFeature.geometry.coordinates;
                const center = getCenter(coordinates);
                const rotatedCoordinates = coordinates.map(ring => {
                    return ring.map(point => {
                        const x = point[0] - center[0];
                        const y = point[1] - center[1];
                        const newX = x * Math.cos(angle * Math.PI / 180) - y * Math.sin(angle * Math.PI / 180);
                        const newY = x * Math.sin(angle * Math.PI / 180) + y * Math.cos(angle * Math.PI / 180);
                        return [newX + center[0], newY + center[1]];
                    });
                });
                newFeature.geometry.coordinates = rotatedCoordinates;
                return newFeature;
            }
            function getCenter(coordinates) {
                let x = 0;
                let y = 0;
                let count = 0;
                coordinates.forEach(ring => {
                    ring.forEach(point => {
                        x += point[0];
                        y += point[1];
                        count++;
                    });
                });
                return [x / count, y / count];
            }
            document.addEventListener('keydown', (event) => {
                if (event.key === 'r' && selectedFeatureId) {
                    const features = draw.getSnapshot();
                    const selectedFeature = features.find(f => f.id === selectedFeatureId);
                    if (selectedFeature) {
                        const newFeature = rotateFeature(selectedFeature, 15);
                        draw.addFeatures([newFeature]);
                    }
                }
            });
        });
    }
    catch (e) {
        console.error("Error loading Google Maps API:", e);
    }
}).catch(e => {
    console.error("Error loading Google Maps API:", e);
});

CSS

html,
body {
    height: 100%;
    margin: 0;
    padding: 0;
    font-family: Arial, sans-serif;
}
#map {
    height: 100%;
    width: 100%;
}
#mode-ui {
    position: absolute;
    top: 10px;
    right: 10px;
    background: white;
    padding: 10px;
    border-radius: 5px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
    z-index: 1000;
    display: flex;
    flex-direction: column;
}
#mode-ui button {
    margin: 5px 0;
    cursor: pointer;
}

.mode-button {
    width: 30px;
    height: 30px;
    border: 1px solid #ccc;
    background-color: white;
    padding: 2px;
    box-sizing: border-box;
}

.mode-button img {
    width: 100%;
    height: 100%;
    display: block;
    user-select: none;
}

/* Active state for shape modes */
.mode-button.active {
    background-color: #e0e0e0; /* light grey */
}

/* Special buttons default state */
#select-mode,
#clear-mode,
#delete-selected-button,
#undo-button,
#redo-button,
#export-button,
#upload-button,
#resize-button {
    background-color: #000000;
}

/* Special buttons icon default state */
#select-mode img,
#clear-mode img,
#delete-selected-button img,
#undo-button img,
#redo-button img,
#export-button img,
#upload-button img,
#resize-button img {
    filter: brightness(0) invert(1);
}

/* Special buttons active/click states */
#select-mode.active {
    background-color: #A9A9A9; /* dark grey */
}

#clear-mode:active,
#delete-selected-button:active,
#undo-button:active,
#redo-button:active,
#export-button:active,
#upload-button:active,
#resize-button.active {
    background-color: #A9A9A9; /* dark grey */
}

HTML

<html>
<head>
    <title>Terra Draw with Google Maps API Sample</title>
    <link rel="stylesheet" href="./style.css">
    <!-- Terra Draw CSS (if any needed, add here) -->
</head>
<body>
    <!-- Map Container -->
    <div id="map"></div>

    <!-- Top-right mode selection UI -->
    <div id="mode-ui">
        <button id="point-mode" class="mode-button" title="Point"><img src="./img/point.svg" alt="Point" draggable="false"></button>
        <button id="linestring-mode" class="mode-button" title="Linestring"><img src="./img/polyline.svg" alt="Linestring" draggable="false"></button>
        <button id="polygon-mode" class="mode-button active" title="Polygon"><img src="./img/polygon.png" alt="Polygon" draggable="false"></button>
        <button id="rectangle-mode" class="mode-button" title="Rectangle"><img src="./img/rectangle.svg" alt="Rectangle" draggable="false"></button>
        <button id="circle-mode" class="mode-button" title="Circle"><img src="./img/circle.svg" alt="Circle" draggable="false"></button>
        <button id="freehand-mode" class="mode-button" title="Freehand"><img src="./img/freehand.svg" alt="Freehand" draggable="false"></button>
        <button id="select-mode" class="mode-button" title="Select"><img src="./img/select.svg" alt="Select" draggable="false"></button>
        <button id="resize-button" class="mode-button" title="Resize"><img src="./img/resize.svg" alt="Resize" draggable="false"></button>
        <button id="clear-mode" class="mode-button" title="Clear"><img src="./img/delete.svg" alt="Clear" draggable="false"></button>
        <button id="delete-selected-button" class="mode-button" title="Clear last or Selected"><img src="./img/delete-selected.svg" alt="Delete Selected" draggable="false"></button>
        <button id="undo-button" class="mode-button" title="Undo"><img src="./img/undo.svg" alt="Undo" draggable="false"></button>
        <button id="redo-button" class="mode-button" title="Redo"><img src="./img/redo.svg" alt="Redo" draggable="false"></button>
        <button id="export-button" class="mode-button" title="Export"><img src="./img/download.svg" alt="Export" draggable="false"></button>
        <button id="upload-button" class="mode-button" title="Upload"><img src="./img/upload.svg" alt="Upload" draggable="false"></button>
        <input type="file" id="upload-input" style="display: none;" accept=".geojson,.json">
    </div>

    <script type="module" src="./index.ts"></script>
    <!-- Google Maps API is loaded by the Loader in index.ts -->
</body>
</html>

Try Sample

Clone Sample

Git and Node.js are required to run this sample locally. Follow these instructions to install Node.js and NPM. The following commands clone, install dependencies and start the sample application.

  git clone https://github.com/googlemaps-samples/js-api-samples.git
  cd samples/map-drawing-terradraw
  npm i
  npm start

Key Steps for Integration

  1. Load Libraries: Include the Google Maps JavaScript API script, followed by the Terra Draw core and google-maps scripts. If you are using script tags:
<script src="https://unpkg.com/terra-draw@latest/dist/terra-draw.umd.js"></script>

<script src="https://unpkg.com/terra-draw-google-maps-adapter@latest/dist/terra-draw-google-maps-adapter.umd.js"></script>
  1. Initialize Map: Create your standard google.maps.Map instance.

  2. Create Adapter: Instantiate TerraDrawGoogleMapsAdapter, passing it the google.maps library and your map instance to connect them.

  3. Create TerraDraw: Create a TerraDraw instance, providing the adapter and an array of the drawing modes you want to support.

  4. Activate Drawing: Call draw.start() to enable the tool, then draw.setMode('polygon') to select a drawing shape.

  5. Capture Data: Listen to the draw.on('change', callback) event to get an array of all drawn features as GeoJSON.