diff --git a/samples/map-drawing-terradraw/README.md b/samples/map-drawing-terradraw/README.md new file mode 100644 index 00000000..63ddda11 --- /dev/null +++ b/samples/map-drawing-terradraw/README.md @@ -0,0 +1,40 @@ +# Basic Terra Draw with Google Maps API Sample + +This sample demonstrates a basic implementation of Terra Draw with the Google Maps JavaScript API. It includes various drawing modes such as Point, LineString, Polygon, Rectangle, Circle, and Freehand. + +![Roadmap View](./screenshots/roadmap-draw.png) +![Satellite View](./screenshots/satellite-draw.png) + +# Google Maps JavaScript Sample + +This sample is generated from @googlemaps/js-samples located at +https://github.com/googlemaps-samples/js-api-samples. + +## Setup + +### Before starting run: + +`$npm i` + +### Run an example on a local web server + +First `cd` to the folder for the sample to run, then: + +`$npm start` + +### Build an individual example + +From `samples/`: + +`$npm run build --workspace=sample-name/` + +### Build all of the examples. + +From `samples/`: +`$npm run build-all` + +## Feedback + +For feedback related to this sample, please open a new issue on +[GitHub](https://github.com/googlemaps-samples/js-api-samples/issues). + diff --git a/samples/map-drawing-terradraw/img/circle.svg b/samples/map-drawing-terradraw/img/circle.svg new file mode 100644 index 00000000..14c34679 --- /dev/null +++ b/samples/map-drawing-terradraw/img/circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/map-drawing-terradraw/img/cursor.svg b/samples/map-drawing-terradraw/img/cursor.svg new file mode 100644 index 00000000..b221f5a2 --- /dev/null +++ b/samples/map-drawing-terradraw/img/cursor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/map-drawing-terradraw/img/delete-selected.svg b/samples/map-drawing-terradraw/img/delete-selected.svg new file mode 100644 index 00000000..f15806a0 --- /dev/null +++ b/samples/map-drawing-terradraw/img/delete-selected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/map-drawing-terradraw/img/delete.svg b/samples/map-drawing-terradraw/img/delete.svg new file mode 100644 index 00000000..ef53c233 --- /dev/null +++ b/samples/map-drawing-terradraw/img/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/map-drawing-terradraw/img/download.svg b/samples/map-drawing-terradraw/img/download.svg new file mode 100644 index 00000000..77e5a2c5 --- /dev/null +++ b/samples/map-drawing-terradraw/img/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/map-drawing-terradraw/img/drawing-icons.png b/samples/map-drawing-terradraw/img/drawing-icons.png new file mode 100644 index 00000000..533c0215 Binary files /dev/null and b/samples/map-drawing-terradraw/img/drawing-icons.png differ diff --git a/samples/map-drawing-terradraw/img/freehand.svg b/samples/map-drawing-terradraw/img/freehand.svg new file mode 100644 index 00000000..03d88325 --- /dev/null +++ b/samples/map-drawing-terradraw/img/freehand.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/map-drawing-terradraw/img/point.svg b/samples/map-drawing-terradraw/img/point.svg new file mode 100644 index 00000000..1ed3445c --- /dev/null +++ b/samples/map-drawing-terradraw/img/point.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/map-drawing-terradraw/img/polygon.png b/samples/map-drawing-terradraw/img/polygon.png new file mode 100644 index 00000000..8f81fd1f Binary files /dev/null and b/samples/map-drawing-terradraw/img/polygon.png differ diff --git a/samples/map-drawing-terradraw/img/polyline.svg b/samples/map-drawing-terradraw/img/polyline.svg new file mode 100644 index 00000000..bded9e38 --- /dev/null +++ b/samples/map-drawing-terradraw/img/polyline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/map-drawing-terradraw/img/rectangle.svg b/samples/map-drawing-terradraw/img/rectangle.svg new file mode 100644 index 00000000..7212d384 --- /dev/null +++ b/samples/map-drawing-terradraw/img/rectangle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/map-drawing-terradraw/img/redo.svg b/samples/map-drawing-terradraw/img/redo.svg new file mode 100644 index 00000000..7530e675 --- /dev/null +++ b/samples/map-drawing-terradraw/img/redo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/map-drawing-terradraw/img/resize.svg b/samples/map-drawing-terradraw/img/resize.svg new file mode 100644 index 00000000..61ef36c6 --- /dev/null +++ b/samples/map-drawing-terradraw/img/resize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/map-drawing-terradraw/img/rotate.svg b/samples/map-drawing-terradraw/img/rotate.svg new file mode 100644 index 00000000..4100e605 --- /dev/null +++ b/samples/map-drawing-terradraw/img/rotate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/map-drawing-terradraw/img/select.svg b/samples/map-drawing-terradraw/img/select.svg new file mode 100644 index 00000000..1cd538ef --- /dev/null +++ b/samples/map-drawing-terradraw/img/select.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/map-drawing-terradraw/img/undo.svg b/samples/map-drawing-terradraw/img/undo.svg new file mode 100644 index 00000000..73fca034 --- /dev/null +++ b/samples/map-drawing-terradraw/img/undo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/map-drawing-terradraw/img/upload.svg b/samples/map-drawing-terradraw/img/upload.svg new file mode 100644 index 00000000..3ff766b6 --- /dev/null +++ b/samples/map-drawing-terradraw/img/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/map-drawing-terradraw/index.html b/samples/map-drawing-terradraw/index.html new file mode 100644 index 00000000..48c18039 --- /dev/null +++ b/samples/map-drawing-terradraw/index.html @@ -0,0 +1,41 @@ + + + + + + Terra Draw with Google Maps API Sample + + + + + +
+ + +
+ + + + + + + + + + + + + + + +
+ + + + + + diff --git a/samples/map-drawing-terradraw/index.ts b/samples/map-drawing-terradraw/index.ts new file mode 100644 index 00000000..51c00895 --- /dev/null +++ b/samples/map-drawing-terradraw/index.ts @@ -0,0 +1,509 @@ +/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// [START maps_map_drawing_terradraw] +// [START maps_map_drawing_terradraw_libraries] +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'; + +// [END maps_map_drawing_terradraw_libraries] + +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", () => { + + // [START maps_drawing_terradraw_modes] + + 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); + }); + + // [END maps_drawing_terradraw_modes] + + 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); +}); +// [END maps_map_drawing_terradraw] diff --git a/samples/map-drawing-terradraw/package.json b/samples/map-drawing-terradraw/package.json new file mode 100644 index 00000000..ab81abec --- /dev/null +++ b/samples/map-drawing-terradraw/package.json @@ -0,0 +1,21 @@ +{ + "name": "map-drawing-terradraw-basic", + "version": "1.0.0", + "description": "Basic sample for Terra Draw with Google Maps API.", + "private": true, + "scripts": { + "start": "vite", + "build": "tsc && vite build", + "test": "tsc && vite build" + }, + "dependencies": { + "@googlemaps/js-api-loader": "^1.16.8", + "terra-draw": "latest", + "terra-draw-google-maps-adapter": "latest" + }, + "devDependencies": { + "@types/google.maps": "latest", + "typescript": "^5.0.0", + "vite": "^5.0.0" + } +} diff --git a/samples/map-drawing-terradraw/screenshots/draw-roadmap.png b/samples/map-drawing-terradraw/screenshots/draw-roadmap.png new file mode 100644 index 00000000..a336bf70 Binary files /dev/null and b/samples/map-drawing-terradraw/screenshots/draw-roadmap.png differ diff --git a/samples/map-drawing-terradraw/screenshots/draw-satellite.png b/samples/map-drawing-terradraw/screenshots/draw-satellite.png new file mode 100644 index 00000000..0d4cde0a Binary files /dev/null and b/samples/map-drawing-terradraw/screenshots/draw-satellite.png differ diff --git a/samples/map-drawing-terradraw/style.css b/samples/map-drawing-terradraw/style.css new file mode 100644 index 00000000..2584b672 --- /dev/null +++ b/samples/map-drawing-terradraw/style.css @@ -0,0 +1,94 @@ +/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* [START maps_map_drawing_terradraw] */ +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 */ +} +/* [END maps_map_drawing_terradraw] */ diff --git a/samples/map-drawing-terradraw/tsconfig.json b/samples/map-drawing-terradraw/tsconfig.json new file mode 100644 index 00000000..c543450e --- /dev/null +++ b/samples/map-drawing-terradraw/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "strict": true, + "noImplicitAny": false, + "lib": [ + "es2015", + "esnext", + "es6", + "dom", + "dom.iterable" + ], + "moduleResolution": "Bundler", + "jsx": "preserve", + "types": ["@types/google.maps"] + } +} \ No newline at end of file