1/*! 2 * Google Maps Drawing Manager Polyfill 3 * * A zero-dependency, drop-in replacement for the deprecated google.maps.drawing library. 4 * * Author: Robert McMahon 5 * Website: https://www.mapchannels.com/ 6 * GitHub: https://github.com/mapchannels/google-maps-drawing-polyfill 7 * Demo: https://mapchannels.github.io/google-maps-drawing-polyfill/ 8 * * Released under the MIT License. 9 * Copyright (c) 2026 Map Channels 10 * * Permission is hereby granted, free of charge, to any person obtaining a copy 11 * of this software and associated documentation files (the "Software"), to deal 12 * in the Software without restriction... (see GitHub repo for full license). 13 */ 14 15(function () 16{ 17 'use strict'; 18 19 // Guard: only inject if the native library is absent 20 window.google = window.google || {}; 21 window.google.maps = window.google.maps || {}; 22 23 if (window.google.maps.drawing) 24 { 25 return; // Native library present â do nothing 26 } 27 28 // ââ CSS Injection ââââââââââââââââââââââââââââââââââââââ 29 30 var TOOLBAR_CSS = [ 31 '.mcx-draw-toolbar {', 32 ' display: flex;', 33 ' align-items: center;', 34 ' background: #fff;', 35 ' border-radius: 2px;', 36 ' box-shadow: rgba(0,0,0,0.3) 0 1px 4px -1px;', 37 ' margin: 10px;', 38 ' overflow: hidden;', 39 '}', 40 '.mcx-draw-btn {', 41 ' width: 40px;', 42 ' height: 40px;', 43 ' display: flex;', 44 ' align-items: center;', 45 ' justify-content: center;', 46 ' cursor: pointer;', 47 ' border: none;', 48 ' background: #fff;', 49 ' border-right: 1px solid #e0e0e0;', 50 ' padding: 0;', 51 ' transition: background 0.15s;', 52 ' flex-shrink: 0;', 53 '}', 54 '.mcx-draw-btn:last-child { border-right: none; }', 55 '.mcx-draw-btn:hover { background: #f5f5f5; }', 56 '.mcx-draw-btn.mcx-draw-active {', 57 ' background: #e8e8e8;', 58 ' box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);', 59 '}', 60 '.mcx-draw-btn svg { pointer-events: none; }', 61 ].join('\n'); 62 63 var _stylesInjected = false; 64 function _injectStyles() 65 { 66 if (_stylesInjected) return; 67 var style = document.createElement('style'); 68 style.id = 'mcx-drawing-polyfill-css'; 69 style.textContent = TOOLBAR_CSS; 70 document.head.appendChild(style); 71 _stylesInjected = true; 72 } 73 74 // ââ SVG Icons ââââââââââââââââââââââââââââââââââââââââââ 75 76 var ICONS = { 77 hand: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#555" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 11V6a2 2 0 0 0-4 0v5"/><path d="M14 10V4a2 2 0 0 0-4 0v6"/><path d="M10 10.5V6a2 2 0 0 0-4 0v8"/><path d="M6 14a4 4 0 0 0 4 4h4a4 4 0 0 0 4-4v-2.5a.5.5 0 0 0-.5-.5H6.5a.5.5 0 0 0-.5.5V14z"/></svg>', 78 marker: '<svg width="18" height="18" viewBox="0 0 24 24" fill="#555" stroke="#555" stroke-width="0"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>', 79 polyline: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#555" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3,17 9,7 15,13 21,5"/></svg>', 80 polygon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="rgba(85,85,85,0.15)" stroke="#555" stroke-width="2" stroke-linejoin="round"><polygon points="12,3 21,9 18,20 6,20 3,9"/></svg>', 81 }; 82 83 // ââ OverlayType enum âââââââââââââââââââââââââââââââââââ 84 85 var OverlayType = { 86 MARKER: 'marker', 87 POLYGON: 'polygon', 88 POLYLINE: 'polyline' 89 }; 90 91 // ââ DrawingManager Class âââââââââââââââââââââââââââââââ 92 93 var DrawingManager = (function () 94 { 95 96 function DrawingManager(options) 97 { 98 options = options || {}; 99 100 this._map = null; 101 this._currentMode = options.drawingMode || null; 102 this._options = options; 103 104 // In-progress drawing state 105 this._coords = []; // accumulated coordinates 106 this._activeShape = null; // primary google.maps.Polyline being drawn (even for polygons!) 107 this._ghostLine = null; // dashed preview line to cursor 108 this._finishingMarker = null; // The interactive node to close/finish shapes 109 110 this._ignoreMapClick = false; // Safety guard to prevent double-firing 111 112 // Map event listener handles (for cleanup) 113 this._listeners = []; 114 115 // Toolbar DOM reference 116 this._toolbar = null; 117 this._btnElements = {}; // mode â button element 118 119 // Bind stable handler references 120 this._onMapClick = this._handleMapClick.bind(this); 121 this._onMouseMove = this._handleMouseMove.bind(this); 122 this._onFinishingNodeClick = this._handleFinishingNodeClick.bind(this); 123 124 // Attach to map if provided 125 if (options.map) 126 { 127 this.setMap(options.map); 128 } 129 } 130 131 // ââ Public API âââââââââââââââââââââââââââââââââââââ 132 133 DrawingManager.prototype.setMap = function (map) 134 { 135 if (this._map === map) return; 136 137 // Detach from old map 138 if (this._map) 139 { 140 this._detachFromMap(); 141 } 142 143 this._map = map; 144 145 if (map) 146 { 147 this._attachToMap(); 148 if (this._options.drawingControl !== false) 149 { 150 this._buildToolbar(); 151 } 152 } 153 }; 154 155 DrawingManager.prototype.getMap = function () 156 { 157 return this._map; 158 }; 159 160 DrawingManager.prototype.setDrawingMode = function (mode) 161 { 162 this._cancelCurrentDraw(); 163 this._currentMode = mode;
164 this._updateCursor(); 165 this._updateToolbarState(); 166 }; 167 168 DrawingManager.prototype.getDrawingMode = function () 169 { 170 return this._currentMode; 171 }; 172 173 // ââ Map attachment / detachment ââââââââââââââââââââ 174 175 DrawingManager.prototype._attachToMap = function () 176 { 177 var map = this._map; 178 var self = this; 179 180 var clickHandle = google.maps.event.addListener(map, 'click', self._onMapClick); 181 var moveHandle = google.maps.event.addListener(map, 'mousemove', self._onMouseMove); 182 183 this._listeners = [clickHandle, moveHandle]; 184 this._updateCursor(); 185 }; 186 187 DrawingManager.prototype._detachFromMap = function () 188 { 189 this._listeners.forEach(function (l) 190 { 191 google.maps.event.removeListener(l); 192 }); 193 this._listeners = []; 194 this._destroyGhostLine(); 195 this._destroyActiveShape(); 196 197 if (this._finishingMarker) 198 { 199 this._finishingMarker.setMap(null); 200 this._finishingMarker = null; 201 } 202 203 this._coords = []; 204 205 if (this._toolbar && this._toolbar.parentElement) 206 { 207 this._toolbar.parentElement.removeChild(this._toolbar); 208 this._toolbar = null; 209 } 210 }; 211 212 // ââ Map event handlers âââââââââââââââââââââââââââââ 213 214 DrawingManager.prototype._handleMapClick = function (e) 215 { 216 if (!this._currentMode) return; 217 if (!e.latLng) return; 218 if (this._ignoreMapClick) return; 219 220 var mode = this._currentMode; 221 222 if (mode === OverlayType.MARKER) 223 { 224 this._finishMarker(e.latLng); 225 return; 226 } 227 228 if (mode === OverlayType.POLYLINE || mode === OverlayType.POLYGON) 229 { 230 var now = Date.now(); 231 232 // FIX: Time Debounce - ignore double-clicks caused by physical mouse bounce 233 if (this._lastMapClickTime && (now - this._lastMapClickTime) < 150) 234 { 235 return; 236 } 237 238 // FIX: Spatial Jitter - ignore clicks that are microscopic distances from the 239 // last node (less than ~1 meter) to prevent invisible micro-segments from forming. 240 var lastCoord = this._coords[this._coords.length - 1]; 241 if (lastCoord) 242 { 243 var dLat = lastCoord.lat() - e.latLng.lat(); 244 var dLng = lastCoord.lng() - e.latLng.lng(); 245 var distSq = (dLat * dLat) + (dLng * dLng); 246 if (distSq < 0.0000000001) return; 247 } 248 249 this._lastMapClickTime = now; 250 this._coords.push(e.latLng); 251 252 if (this._coords.length === 1) 253 { 254 this._initActiveShape(); 255 } else 256 { 257 this._updateActiveShape(); 258 } 259 260 this._updateFinishingNode(); 261 this._updateGhostLine(e.latLng); 262 } 263 }; 264 265 DrawingManager.prototype._handleMouseMove = function (e) 266 { 267 if (!this._currentMode) return; 268 if (!e.latLng) return; 269 270 var mode = this._currentMode; 271 if ((mode === OverlayType.POLYLINE || mode === OverlayType.POLYGON) && this._coords.length > 0) 272 { 273 this._updateGhostLine(e.latLng); 274 } 275 }; 276 277 DrawingManager.prototype._handleFinishingNodeClick = function () 278 { 279 var self = this; 280 var mode = this._currentMode; 281 if (!mode) return; 282 283 // FIX: If the Map click event fired just milliseconds before this Marker click event, 284 // it means we caught an event bubble. Remove the bogus point to prevent stroke overshoot. 285 if (this._lastMapClickTime && (Date.now() - this._lastMapClickTime) < 200) 286 { 287 this._coords.pop(); 288 } 289 290 this._ignoreMapClick = true; 291 setTimeout(function () { self._ignoreMapClick = false; }, 150); 292 293 var minPoints = (mode === OverlayType.POLYGON) ? 3 : 2; 294 if (this._coords.length >= minPoints) 295 { 296 // FIX: Native google.maps.Polygon closes itself. If the last node matches the 297 // start node, it creates a sharp visual spike. We remove it here. 298 if (mode === OverlayType.POLYGON) 299 { 300 var first = this._coords[0]; 301 var last = this._coords[this._coords.length - 1]; 302 if (first.equals(last)) 303 { 304 this._coords.pop(); 305 } 306 } 307 308 this._finishShape(mode); 309 } else 310 { 311 this._cancelCurrentDraw(); 312 } 313 }; 314 315 // ââ Shape lifecycle ââââââââââââââââââââââââââââââââ 316 317 DrawingManager.prototype._initActiveShape = function () 318 { 319 var map = this._map; 320 var coords = this._coords; 321 322 this._activeShape = new google.maps.Polyline({ 323 path: coords, 324 map: map, 325 strokeColor: '#1a73e8', 326 strokeWeight: 3, 327 strokeOpacity: 0.9, 328 // FIX: Force Google Maps SVG renderer to use round joints instead of square caps. 329 // This completely eliminates sharp overlapping corners/horns on acute angles. 330 strokeLineJoin: 'round', 331 strokeLineCap: 'round', 332 clickable: false, 333 zIndex: 200 334 }); 335 }; 336 337 DrawingManager.prototype._updateActiveShape = function () 338 { 339 if (!this._activeShape) return; 340 // Since it's always a polyline during drawing, we just set the continuous path 341 this._activeShape.setPath(this._coords); 342 }; 343 344 DrawingManager.prototype._updateGhostLine = function (cursorLatLng) 345 { 346 var lastCoord = this._coords[this._coords.length - 1]; 347 if (!lastCoord) return; 348 349 // FIX: If the mouse hasn't moved from the exact spot you clicked, 350 // hide the ghost line. A zero-length line has no angle, which causes 351 // the Google Maps renderer to draw a crooked "spike" artifact! 352 if (lastCoord.equals(cursorLatLng)) 353 { 354 if (this._ghostLine) this._ghostLine.setVisible(false); 355 return; 356 } 357 358 var ghostPath = [lastCoord, cursorLatLng]; 359 360 if (!this._ghostLine) 361 { 362 this._ghostLine = new google.maps.Polyline({ 363 path: ghostPath, 364 map: this._map, 365 strokeOpacity: 0, // The main solid stroke must be hidden for dots to work 366 icons: [{ 367 icon: { 368 // FIX: Changed from a dashed line to a dotted line as requested 369 path: google.maps.SymbolPath.CIRCLE, 370 fillColor: '#1a73e8', 371 fillOpacity: 0.7, 372 strokeOpacity: 0, 373 scale: 2 374 }, 375 offset: '0', 376 repeat: '4px' 377 }], 378 clickable: false, 379 zIndex: 201 380 }); 381 } else 382 { 383 this._ghostLine.setPath(ghostPath); 384 this._ghostLine.setVisible(true); // Bring it back once the mouse moves 385 } 386 }; 387 388 DrawingManager.prototype._updateFinishingNode = function () 389 { 390 var coords = this._coords; 391 var mode = this._currentMode; 392 var self = this; 393 394 if (coords.length === 0) 395 { 396 if (this._finishingMarker) this._finishingMarker.setVisible(false); 397 return; 398 } 399 400 if (!this._finishingMarker) 401 { 402 this._finishingMarker = new google.maps.Marker({ 403 map: this._map, 404 icon: { 405 path: google.maps.SymbolPath.CIRCLE, 406 fillColor: '#ffffff', 407 fillOpacity: 1, 408 strokeColor: '#1a73e8', 409 strokeWeight: 2, 410 scale: 5 411 }, 412 cursor: 'pointer', // Creates the "Hand" icon on hover automatically 413 zIndex: 300 414 }); 415 416 google.maps.event.addListener(this._finishingMarker, 'click', function (e) 417 { 418 if (e.domEvent) 419 { 420 e.domEvent.preventDefault(); 421 e.domEvent.stopPropagation(); 422 } 423 self._onFinishingNodeClick(); 424 }); 425 } 426 427 if (mode === OverlayType.POLYLINE) 428 { 429 // For Polylines, the finishing node lives on the LAST clicked point 430 this._finishingMarker.setPosition(coords[coords.length - 1]); 431 this._finishingMarker.setVisible(true); 432 } 433 else if (mode === OverlayType.POLYGON) 434 { 435 // For Polygons, the finishing node lives on the FIRST point to close the shape. 436 // We only show it once a line segment actually exists. 437 if (coords.length >= 2) 438 { 439 this._finishingMarker.setPosition(coords[0]); 440 this._finishingMarker.setVisible(true); 441 } else 442 { 443 this._finishingMarker.setVisible(false); 444 } 445 } 446 }; 447 448 DrawingManager.prototype._destroyGhostLine = function () 449 { 450 if (this._ghostLine) 451 { 452 this._ghostLine.setMap(null); 453 this._ghostLine = null; 454 } 455 }; 456 457 DrawingManager.prototype._destroyActiveShape = function () 458 { 459 if (this._activeShape) 460 { 461 this._activeShape.setMap(null); 462 this._activeShape = null; 463 } 464 }; 465 466 DrawingManager.prototype._cancelCurrentDraw = function () 467 { 468 this._destroyGhostLine(); 469 this._destroyActiveShape(); 470 if (this._finishingMarker) 471 { 472 this._finishingMarker.setVisible(false); 473 } 474 this._coords = []; 475 }; 476 477 // ââ Finish handlers ââââââââââââââââââââââââââââââââ 478 479 DrawingManager.prototype._finishMarker = function (latLng) 480 { 481 var markerOptions = {}; 482 if (this._options.markerOptions) 483 { 484 for (var k in this._options.markerOptions) 485 { 486 markerOptions[k] = this._options.markerOptions[k]; 487 } 488 } 489 markerOptions.position = latLng; 490 markerOptions.map = this._map; 491 492 var mockMarker = new google.maps.Marker(markerOptions); 493 494 var self = this; 495 google.maps.event.trigger(self, 'overlaycomplete', { 496 type: OverlayType.MARKER, 497 overlay: mockMarker 498 }); 499 google.maps.event.trigger(self, 'markercomplete', mockMarker); 500 501 // Exit draw mode automatically so user isn't stuck holding the marker tool 502 this.setDrawingMode(null); 503 }; 504 505 DrawingManager.prototype._finishShape = function (mode) 506 { 507 var coords = this._coords.slice(); // snapshot 508 509 // Clear all temporary drawing assets before dispatching the final shape 510 this._cancelCurrentDraw(); 511 512 var self = this; 513 var mockOverlay; 514 515 if (mode === OverlayType.POLYLINE) 516 { 517 mockOverlay = new google.maps.Polyline({ 518 path: coords, 519 map: this._map, 520 strokeColor: '#1a73e8', 521 strokeWeight: 3, 522 strokeOpacity: 0.9, 523 clickable: true 524 }); 525 google.maps.event.trigger(self, 'overlaycomplete', { 526 type: OverlayType.POLYLINE, 527 overlay: mockOverlay 528 }); 529 google.maps.event.trigger(self, 'polylinecomplete', mockOverlay); 530 531 } else if (mode === OverlayType.POLYGON) 532 { 533 mockOverlay = new google.maps.Polygon({ 534 paths: [coords], 535 map: this._map, 536 strokeColor: '#1a73e8', 537 strokeWeight: 2, 538 strokeOpacity: 0.9, 539 fillColor: '#1a73e8', 540 fillOpacity: 0.25, 541 clickable: true 542 }); 543 google.maps.event.trigger(self, 'overlaycomplete', { 544 type: OverlayType.POLYGON, 545 overlay: mockOverlay 546 }); 547 google.maps.event.trigger(self, 'polygoncomplete', mockOverlay); 548 } 549 550 // Exit draw mode automatically so Place Edit panel can take priority cleanly 551 this.setDrawingMode(null); 552 }; 553 554 // ââ Cursor & state helpers âââââââââââââââââââââââââ 555 556 DrawingManager.prototype._updateCursor = function () 557 { 558 if (!this._map) return; 559 var container = this._map.getDiv ? this._map.getDiv() : null; 560 561 if (this._currentMode) 562 { 563 if (container) container.style.cursor = 'crosshair'; 564 // Lock crosshairs and disable double click zooming while drawing 565 this._map.setOptions({ 566 draggableCursor: 'crosshair', 567 disableDoubleClickZoom: true 568 }); 569 } else 570 { 571 if (container) container.style.cursor = ''; 572 // Restore map defaults 573 this._map.setOptions({ 574 draggableCursor: '', 575 disableDoubleClickZoom: false 576 }); 577 } 578 }; 579 580 DrawingManager.prototype._updateToolbarState = function () 581 { 582 for (var mode in this._btnElements) 583 { 584 var btn = this._btnElements[mode]; 585 if (btn) 586 { 587 btn.classList.toggle('mcx-draw-active', mode === (this._currentMode || 'hand')); 588 } 589 } 590 }; 591 592 // ââ Toolbar ââââââââââââââââââââââââââââââââââââââââ 593 594 DrawingManager.prototype._buildToolbar = function () 595 { 596 if (this._toolbar) return; 597 _injectStyles(); 598 599 var opts = this._options; 600 var drawCtrlOpts = opts.drawingControlOptions || {}; 601 var drawModes = drawCtrlOpts.drawingModes || [ 602 OverlayType.MARKER, 603 OverlayType.POLYLINE, 604 OverlayType.POLYGON 605 ]; 606 var position = drawCtrlOpts.position != null 607 ? drawCtrlOpts.position 608 : google.maps.ControlPosition.TOP_CENTER; 609 610 var toolbar = document.createElement('div'); 611 toolbar.className = 'mcx-draw-toolbar'; 612 613 var self = this; 614 615 // Hand / Pan button (always present) 616 var handBtn = _makeToolbarButton('hand', ICONS.hand, 'Pan', true, function () 617 { 618 self.setDrawi
618ngMode(null); 619 }); 620 toolbar.appendChild(handBtn); 621 this._btnElements['hand'] = handBtn; 622 623 // Mode buttons 624 var modeLabels = { 625 marker: 'Add Marker', 626 polyline: 'Draw Line', 627 polygon: 'Draw Polygon' 628 }; 629 630 drawModes.forEach(function (mode) 631 { 632 var icon = ICONS[mode] || ICONS.marker; 633 var label = modeLabels[mode] || mode; 634 var btn = _makeToolbarButton(mode, icon, label, false, function () 635 { 636 self.setDrawingMode(mode); 637 }); 638 toolbar.appendChild(btn); 639 self._btnElements[mode] = btn; 640 }); 641 642 this._toolbar = toolbar; 643 this._map.controls[position].push(toolbar); 644 645 // Set initial active state 646 this._updateToolbarState(); 647 }; 648 649 // ââ Private helpers ââââââââââââââââââââââââââââââââ 650 651 function _makeToolbarButton(mode, svgHtml, title, isActive, onClick) 652 { 653 var btn = document.createElement('button'); 654 btn.type = 'button'; 655 btn.className = 'mcx-draw-btn' + (isActive ? ' mcx-draw-active' : ''); 656 btn.title = title; 657 btn.setAttribute('data-mode', mode); 658 btn.innerHTML = svgHtml; 659 660 btn.addEventListener('click', function (e) 661 { 662 e.stopPropagation(); 663 onClick(); 664 }); 665 666 // Prevent the button from passing click events to the map 667 btn.addEventListener('mousedown', function (e) { e.stopPropagation(); }); 668 669 return btn; 670 } 671 672 return DrawingManager; 673 }()); 674 675 // ââ Namespace injection ââââââââââââââââââââââââââââââââ 676 677 window.google.maps.drawing = { 678 OverlayType: OverlayType, 679 DrawingManager: DrawingManager 680 }; 681 682 console.log('[MCX] Drawing Manager Polyfill loaded (google.maps.drawing replacement).'); 683 684}());
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.