diff --git a/package-lock.json b/package-lock.json
index d440a729d..2b11785f7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1423,6 +1423,10 @@
"resolved": "samples/polyline-simple",
"link": true
},
+ "node_modules/@js-api-samples/polyline-utility": {
+ "resolved": "samples/polyline-utility",
+ "link": true
+ },
"node_modules/@js-api-samples/react-places-ui-kit-search-nearby": {
"resolved": "samples/react-ui-kit-search-nearby",
"link": true
@@ -6946,6 +6950,10 @@
"name": "@js-api-samples/polyline-simple",
"version": "1.0.0"
},
+ "samples/polyline-utility": {
+ "name": "@js-api-samples/polyline-utility",
+ "version": "1.0.0"
+ },
"samples/react-ui-kit-place-details": {
"name": "@js-api-samples/react-ui-kit-place-details",
"version": "1.0.0"
diff --git a/samples/polyline-utility/README.md b/samples/polyline-utility/README.md
new file mode 100644
index 000000000..92c7ecc2a
--- /dev/null
+++ b/samples/polyline-utility/README.md
@@ -0,0 +1,41 @@
+# Google Maps JavaScript Sample
+
+## polyline-utility
+
+This sample demonstrates how to encode and decode polylines using the [Google Maps GeometryLibrary](https://developers.google.com/maps/documentation/javascript/reference/geometry). It also shows how to convert polylines to and from GeoJSON.
+
+## Setup
+
+### Before starting run:
+
+`npm i`
+
+### Run an example on a local web server
+
+`cd samples/polyline-utility`
+`npm start`
+
+### Build an individual example
+
+`cd samples/polyline-utility`
+`npm run build`
+
+From 'samples':
+
+`npm run build --workspace=polyline-utility/`
+
+### Build all of the examples.
+
+From 'samples':
+
+`npm run build-all`
+
+### Run lint to check for problems
+
+`cd samples/polyline-utility`
+`npx eslint index.ts`
+
+## 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/polyline-utility/index.html b/samples/polyline-utility/index.html
new file mode 100644
index 000000000..a2c204cef
--- /dev/null
+++ b/samples/polyline-utility/index.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+ Polyline Encoder / Decoder Utility
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/polyline-utility/index.ts b/samples/polyline-utility/index.ts
new file mode 100644
index 000000000..a5645d713
--- /dev/null
+++ b/samples/polyline-utility/index.ts
@@ -0,0 +1,311 @@
+// [START maps_polyline_utility]
+
+async function init(): Promise {
+ const [
+ { Polyline },
+ { AdvancedMarkerElement },
+ { LatLng, LatLngBounds, event },
+ { PlaceAutocompleteElement },
+ geometryLibrary,
+ ] = await Promise.all([
+ google.maps.importLibrary('maps'),
+ google.maps.importLibrary('marker'),
+ google.maps.importLibrary('core'),
+ google.maps.importLibrary('places'),
+ google.maps.importLibrary('geometry'),
+ ]);
+
+ let markers: google.maps.marker.AdvancedMarkerElement[] = [];
+
+ const mapElement = document.querySelector('gmp-map')!;
+ const map = mapElement.innerMap;
+ map.setOptions({ draggableCursor: 'crosshair' });
+
+ // Setup Polyline
+ const polyline = new Polyline({
+ map,
+ strokeColor: '#1a73e8',
+ strokeOpacity: 0.8,
+ strokeWeight: 4,
+ });
+
+ // Map Click to Add Point
+ map.addListener('click', (e: google.maps.MapMouseEvent) => {
+ if (e.latLng) {
+ addPoint(e.latLng);
+ }
+ });
+
+ // Setup Places Autocomplete
+ const pacContainer = document.getElementById('pac-container')!;
+ const autocomplete = new PlaceAutocompleteElement();
+ pacContainer.appendChild(autocomplete);
+
+ autocomplete.addEventListener(
+ 'gmp-select',
+ async ({
+ placePrediction,
+ }: google.maps.places.PlacePredictionSelectEvent) => {
+ const place = placePrediction.toPlace();
+ try {
+ await place.fetchFields({
+ fields: ['location', 'viewport', 'displayName'],
+ });
+ } catch (e: unknown) {
+ alert(`Failed to fetch place details: ${String(e)}`);
+ return;
+ }
+
+ if (place.viewport) {
+ map.fitBounds(place.viewport);
+ } else if (place.location) {
+ map.setCenter(place.location);
+ map.setZoom(17);
+ } else {
+ const name = place.displayName ?? '';
+ alert(`No details available for input: '${name}'`);
+ }
+ }
+ );
+
+ // Buttons and Textareas
+ document
+ .getElementById('decode-encoded-btn')!
+ .addEventListener('click', decodeEncodedPolyline);
+ document
+ .getElementById('decode-geojson-btn')!
+ .addEventListener('click', decodeGeoJson);
+ document
+ .getElementById('clear-all-btn')!
+ .addEventListener('click', clearAll);
+
+ // Panel Resizer
+ const resizer = document.getElementById('drag-resizer')!;
+ const sidePanel = document.getElementById('side-panel')!;
+ let isResizing = false;
+
+ resizer.addEventListener('mousedown', () => {
+ isResizing = true;
+ document.body.classList.add('is-resizing');
+ resizer.classList.add('dragging');
+ });
+
+ document.addEventListener('mousemove', (e: MouseEvent) => {
+ if (!isResizing) return;
+ sidePanel.style.width = `${String(e.clientX)}px`;
+ });
+
+ document.addEventListener('mouseup', () => {
+ if (isResizing) {
+ isResizing = false;
+ document.body.classList.remove('is-resizing');
+ resizer.classList.remove('dragging');
+ }
+ });
+
+ function addPoint(latLng: google.maps.LatLng) {
+ const path = polyline.getPath();
+ path.push(latLng);
+
+ const marker = new AdvancedMarkerElement({
+ map,
+ position: latLng,
+ gmpDraggable: true,
+ });
+
+ marker.addListener('dragend', () => {
+ rebuildPathFromMarkers();
+ });
+
+ markers.push(marker);
+ updateOutputs();
+ }
+
+ function rebuildPathFromMarkers() {
+ const newPath = markers
+ .map((m) => m.position)
+ .filter(
+ (pos): pos is google.maps.LatLngLiteral | google.maps.LatLng =>
+ pos != null
+ );
+ polyline.setPath(newPath);
+ updateOutputs();
+ }
+
+ function removePoint(index: number) {
+ if (index >= 0 && index < markers.length) {
+ event.clearInstanceListeners(markers[index]);
+ markers[index].map = null;
+ markers.splice(index, 1);
+ rebuildPathFromMarkers();
+ }
+ }
+
+ function clearAll() {
+ markers.forEach((m) => {
+ event.clearInstanceListeners(m);
+ m.map = null;
+ });
+ markers = [];
+ polyline.getPath().clear();
+ updateOutputs();
+ }
+
+ function updateOutputs() {
+ const path = polyline.getPath();
+ const arr = path.getArray();
+
+ // Encoded Polyline
+ const encodedTextarea = document.getElementById(
+ 'encoded-polyline'
+ ) as HTMLTextAreaElement;
+ if (arr.length > 0) {
+ encodedTextarea.value = geometryLibrary.encoding.encodePath(path);
+ } else {
+ encodedTextarea.value = '';
+ }
+
+ // GeoJSON
+ const geojsonTextarea = document.getElementById(
+ 'geojson-polyline'
+ ) as HTMLTextAreaElement;
+ if (arr.length > 0) {
+ const coords = arr.map((ll) => {
+ const { lat, lng } = ll.toJSON();
+ return [lng, lat];
+ });
+ geojsonTextarea.value = JSON.stringify(coords);
+ } else {
+ geojsonTextarea.value = '';
+ }
+
+ // List
+ const list = document.getElementById('locations-list')!;
+ list.replaceChildren();
+ document.getElementById('location-count')!.textContent =
+ arr.length.toString();
+
+ arr.forEach((ll, index) => {
+ const { lat, lng } = ll.toJSON();
+
+ const li = document.createElement('li');
+ li.textContent = `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
+
+ const delBtn = document.createElement('button');
+ delBtn.className = 'delete-point-btn';
+ delBtn.textContent = '×';
+ delBtn.title = 'Remove point';
+ delBtn.onclick = () => {
+ removePoint(index);
+ };
+
+ li.appendChild(delBtn);
+ list.appendChild(li);
+ });
+ }
+
+ function decodeEncodedPolyline() {
+ let encoded = (
+ document.getElementById('encoded-polyline') as HTMLTextAreaElement
+ ).value.trim();
+ const unescape = (
+ document.getElementById('enableunescape') as HTMLInputElement
+ ).checked;
+
+ if (!encoded) return;
+ if (unescape) {
+ encoded = encoded
+ .replace(/\\\\\\\\/g, '\\\\')
+ .replace(/\\\\"/g, '"');
+ }
+
+ try {
+ const decodedPath = geometryLibrary.encoding.decodePath(encoded);
+ applyDecodedPath(decodedPath);
+ } catch {
+ alert('Failed to decode polyline string.');
+ }
+ }
+
+ function decodeGeoJson() {
+ const geojsonStr = (
+ document.getElementById('geojson-polyline') as HTMLTextAreaElement
+ ).value.trim();
+ if (!geojsonStr) return;
+
+ try {
+ // We treat the parsed output as `unknown` to strictly validate it.
+ const parsed: unknown = JSON.parse(geojsonStr);
+ let coordsArray: unknown[] | undefined;
+
+ // Check if it's a Feature or LineString object
+ if (parsed && typeof parsed === 'object') {
+ const obj = parsed as Record;
+ if (obj.type === 'Feature') {
+ const geometry = obj.geometry as
+ Record | undefined;
+ if (
+ geometry?.type === 'LineString' &&
+ Array.isArray(geometry.coordinates)
+ ) {
+ coordsArray = geometry.coordinates as unknown[];
+ }
+ } else if (
+ obj.type === 'LineString' &&
+ Array.isArray(obj.coordinates)
+ ) {
+ coordsArray = obj.coordinates as unknown[];
+ } else if (Array.isArray(parsed)) {
+ coordsArray = parsed;
+ }
+ }
+
+ if (!coordsArray || !Array.isArray(coordsArray)) {
+ throw new Error('Invalid GeoJSON');
+ }
+
+ const decodedPath = coordsArray.map((coord: unknown) => {
+ if (Array.isArray(coord) && coord.length >= 2) {
+ const lng = Number(coord[0]);
+ const lat = Number(coord[1]);
+ if (!isNaN(lat) && !isNaN(lng)) {
+ return new LatLng(lat, lng);
+ }
+ }
+ throw new Error('Invalid coordinates');
+ });
+
+ applyDecodedPath(decodedPath);
+ } catch {
+ alert(
+ 'Failed to decode GeoJSON. Ensure it is a valid array of [lng, lat] coordinates or a LineString object.'
+ );
+ }
+ }
+
+ function applyDecodedPath(decodedPath: google.maps.LatLng[]) {
+ if (decodedPath.length === 0) return;
+
+ if (markers.length > 0) {
+ const replaceConfirm = confirm(
+ 'Are you sure you want to replace the current polyline?'
+ );
+ if (!replaceConfirm) return;
+ }
+
+ clearAll();
+
+ const bounds = new LatLngBounds();
+ decodedPath.forEach((ll) => {
+ addPoint(ll);
+ bounds.extend(ll);
+ });
+
+ if (!bounds.isEmpty()) {
+ map.fitBounds(bounds);
+ }
+ }
+}
+
+void init();
+// [END maps_polyline_utility]
diff --git a/samples/polyline-utility/package.json b/samples/polyline-utility/package.json
new file mode 100644
index 000000000..8b8dd9844
--- /dev/null
+++ b/samples/polyline-utility/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@js-api-samples/polyline-utility",
+ "version": "1.0.0",
+ "scripts": {
+ "build": "bash ../build-single.sh",
+ "test": "tsc && npm run build:vite --workspace=.",
+ "start": "tsc && vite build --config ../../vite.config.js --base './' && vite --config ../../vite.config.js",
+ "build:vite": "vite build --config ../../vite.config.js --base './'",
+ "preview": "vite preview --config ../../vite.config.js"
+ },
+ "author": "Google LLC"
+}
diff --git a/samples/polyline-utility/style.css b/samples/polyline-utility/style.css
new file mode 100644
index 000000000..f39fe9745
--- /dev/null
+++ b/samples/polyline-utility/style.css
@@ -0,0 +1,200 @@
+/*
+ * @license
+ * Copyright 2026 Google LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/* [START maps_polyline_utility] */
+html,
+body {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ font-family: 'Inter', Roboto, sans-serif;
+ background-color: #f8f9fa;
+ color: #202124;
+}
+
+.container {
+ display: flex;
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+}
+
+.panel {
+ width: 400px;
+ min-width: 200px;
+ max-width: 80vw;
+ background: white;
+ padding: 24px;
+ box-shadow: 2px 0 12px rgba(0, 0, 0, 0.1);
+ overflow-y: auto;
+ overflow-x: hidden;
+ z-index: 10;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.header h2 {
+ margin: 0 0 8px 0;
+ font-size: 22px;
+ color: #1a73e8;
+}
+
+.header p {
+ margin: 0;
+ font-size: 14px;
+ color: #5f6368;
+}
+
+.control-group {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+label {
+ font-size: 14px;
+ font-weight: 500;
+ color: #3c4043;
+}
+
+input[type='text'],
+textarea {
+ padding: 10px 12px;
+ border: 1px solid #dadce0;
+ border-radius: 6px;
+ font-size: 14px;
+ font-family: inherit;
+ transition: border-color 0.2s;
+ box-sizing: border-box;
+}
+
+textarea {
+ height: 80px;
+ resize: vertical;
+ font-family: 'Courier New', Courier, monospace;
+}
+
+input[type='text']:focus,
+textarea:focus {
+ outline: none;
+ border-color: #1a73e8;
+}
+
+.checkbox-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 13px;
+}
+.checkbox-row input {
+ margin: 0;
+}
+
+button {
+ cursor: pointer;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 500;
+ transition: background-color 0.2s;
+}
+
+.primary-btn {
+ background-color: #1a73e8;
+ color: white;
+ padding: 10px;
+}
+
+.primary-btn:hover {
+ background-color: #1557b0;
+}
+
+.list-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.text-btn {
+ background: none;
+ color: #d93025;
+ padding: 0;
+ font-size: 13px;
+}
+.text-btn:hover {
+ text-decoration: underline;
+}
+
+#locations-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ border: 1px solid #dadce0;
+ border-radius: 6px;
+ max-height: 200px;
+ overflow-y: auto;
+}
+
+#locations-list li {
+ padding: 8px 12px;
+ font-size: 13px;
+ border-bottom: 1px solid #f1f3f4;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+#locations-list li:last-child {
+ border-bottom: none;
+}
+#locations-list li:hover {
+ background-color: #f8f9fa;
+}
+
+.delete-point-btn {
+ background: none;
+ color: #5f6368;
+ padding: 2px 6px;
+ font-size: 16px;
+ line-height: 1;
+}
+.delete-point-btn:hover {
+ color: #d93025;
+}
+
+.map-container {
+ flex-grow: 1;
+ position: relative;
+}
+
+gmp-map {
+ width: 100%;
+ height: 100%;
+}
+
+.resizer {
+ width: 6px;
+ background-color: #dadce0;
+ cursor: col-resize;
+ transition: background-color 0.2s;
+ flex-shrink: 0;
+}
+
+.resizer:hover,
+.resizer.dragging {
+ background-color: #1a73e8;
+}
+
+body.is-resizing {
+ cursor: col-resize;
+ user-select: none;
+}
+
+body.is-resizing .map-container {
+ pointer-events: none;
+}
+/* [END maps_polyline_utility] */
diff --git a/samples/polyline-utility/tsconfig.json b/samples/polyline-utility/tsconfig.json
new file mode 100644
index 000000000..976bcc6ef
--- /dev/null
+++ b/samples/polyline-utility/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "."
+ },
+ "include": ["./*.ts"]
+}