PageSourceSearch

https://www.belvoir.co.uk/wp-content/plugins/tpj-wp-plugin/assets/js/mcx-drawing-polyfill.js?ver=v1

js belvoir.co.uk collected 2026-09-25 20:30:01 UTC 24,954 bytes, 684 lines download raw bytes

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.