diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 0000000..869443b --- /dev/null +++ b/.bowerrc @@ -0,0 +1,4 @@ +{ + "directory": "bower_components", + "interactive": false +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..b478f93 --- /dev/null +++ b/README.md @@ -0,0 +1,139 @@ +# OverlappingMarkerSpiderfier + +[![npm version](https://img.shields.io/npm/v/OverlappingMarkerSpiderfier.svg)](https://www.npmjs.com/package/OverlappingMarkerSpiderfier) +[![GitHub stars](https://img.shields.io/github/stars/nmccready/OverlappingMarkerSpiderfier.svg?style=social)](https://github.com/nmccready/OverlappingMarkerSpiderfier) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +**Google Earth-style spiderfying for overlapping markers on Google Maps API v3.** + +Ever noticed how in Google Earth, marker pins that overlap each other spring apart gracefully when you click them? This library brings that behavior to the Google Maps JavaScript API v3. Small clusters (up to 8) fan into a circle; larger groups spiral outward for efficient use of space. + +The compiled code has no dependencies beyond Google Maps. Under 3KB minified and gzipped. + +> Originally created by [George MacKerron](https://github.com/jawj) for [Mappiness](http://www.mappiness.org.uk/maps/). This fork is maintained for npm/bower compatibility. + +## Install + +```bash +npm install OverlappingMarkerSpiderfier +# or +bower install OverlappingMarkerSpiderfier +``` + +Or include directly: + +```html + +``` + +## Demo + +See the [live demo](http://jawj.github.com/OverlappingMarkerSpiderfier/demo.html) (reload to reposition random markers). + +## Quick Start + +```js +const map = new google.maps.Map(document.getElementById('map'), { + center: { lat: 50, lng: 0 }, + zoom: 6, +}); + +// Create the spiderfier +const oms = new OverlappingMarkerSpiderfier(map, { + markersWontMove: true, + markersWontHide: true, + keepSpiderfied: true, +}); + +// Add a click listener +const infoWindow = new google.maps.InfoWindow(); +oms.addListener('click', (marker) => { + infoWindow.setContent(marker.desc); + infoWindow.open(map, marker); +}); + +// Close info window on spiderfy +oms.addListener('spiderfy', () => infoWindow.close()); + +// Add markers +markers.forEach((m) => oms.addMarker(m)); +``` + +## Clustering vs Spiderfying + +The [marker clustering library](https://github.com/googlemaps/js-markerclusterer) groups nearby markers at lower zoom levels. Spiderfying complements clustering by handling markers at the **same location** or very close together at max zoom — where clustering can't help users distinguish individual markers. + +They work great together: clustering at low zoom, spiderfying at high zoom. + +## API + +### Constructor + +```js +new OverlappingMarkerSpiderfier(map, options?) +``` + +#### Options + +| Option | Default | Description | +|--------|---------|-------------| +| `markersWontMove` | `false` | Skip position change tracking (saves memory) | +| `markersWontHide` | `false` | Skip visibility change tracking (saves memory) | +| `keepSpiderfied` | `false` | Keep markers spiderfied after clicking one | +| `nearbyDistance` | `20` | Pixel radius for overlap detection | +| `circleSpiralSwitchover` | `9` | Marker count threshold for spiral vs circle layout | +| `legWeight` | `1.5` | Thickness of connecting lines | + +### Marker Management + +| Method | Description | +|--------|-------------| +| `addMarker(marker)` | Track a marker | +| `removeMarker(marker)` | Stop tracking a marker (doesn't remove from map) | +| `clearMarkers()` | Stop tracking all markers | +| `getMarkers()` | Get array of all tracked markers (copy) | + +### Event Listeners + +| Method | Description | +|--------|-------------| +| `addListener(event, fn)` | Listen for `'click'`, `'spiderfy'`, or `'unspiderfy'` | +| `removeListener(event, fn)` | Remove a specific listener | +| `clearListeners(event)` | Remove all listeners for an event | +| `unspiderfy()` | Manually unspiderfy all markers | + +### Advanced + +| Method | Description | +|--------|-------------| +| `markersNearMarker(marker, firstOnly?)` | Find markers within `nearbyDistance` of a marker | +| `markersNearAnyOtherMarker()` | Find all markers that overlap with at least one other | + +### Properties + +Customize leg colors per map type: + +```js +oms.legColors.usual[google.maps.MapTypeId.SATELLITE] = '#fff'; +oms.legColors.highlighted[google.maps.MapTypeId.SATELLITE] = '#f00'; +``` + +## Build from Source + +```bash +npm install +bower install +gulp +``` + +## Contributing + +Contributions are welcome! Please open an issue or submit a pull request. + +## Sponsor + +If you find this project useful, consider [sponsoring @nmccready](https://github.com/sponsors/nmccready) to support ongoing maintenance and development. ❤️ + +## License + +[MIT](http://www.opensource.org/licenses/mit-license.php) diff --git a/bower.json b/bower.json index 851d14c..cb6ce8c 100644 --- a/bower.json +++ b/bower.json @@ -12,10 +12,10 @@ "bower_components" ], - "main": "oms.min.js", - + "main": "dist/oms.min.js", + "devDependencies": { "closure-compiler": "https://closure-compiler.googlecode.com/files/compiler-20131014.zip", - "google-maps-externs": "https://github.com/yagoferrer/google-maps-closure-externs.git#3.7" + "google-maps-externs": "https://github.com/nmccready/google-maps-closure-externs.git#3.20" } } diff --git a/dist/oms.js b/dist/oms.js new file mode 100644 index 0000000..6852d3a --- /dev/null +++ b/dist/oms.js @@ -0,0 +1,561 @@ + +/** @preserve OverlappingMarkerSpiderfier +https://github.com/jawj/OverlappingMarkerSpiderfier +Copyright (c) 2011 - 2013 George MacKerron +Released under the MIT licence: http://opensource.org/licenses/mit-license +Note: The Google Maps API v3 must be included *before* this code + */ +var hasProp = {}.hasOwnProperty, + slice = [].slice; + +this['OverlappingMarkerSpiderfier'] = (function() { + var ge, gm, j, lcH, lcU, len, mt, p, ref, twoPi, x; + + p = _Class.prototype; + + ref = [_Class, p]; + for (j = 0, len = ref.length; j < len; j++) { + x = ref[j]; + x['VERSION'] = '0.3.3'; + } + + gm = void 0; + + ge = void 0; + + mt = void 0; + + twoPi = Math.PI * 2; + + p['keepSpiderfied'] = false; + + p['markersWontHide'] = false; + + p['markersWontMove'] = false; + + p['nearbyDistance'] = 20; + + p['circleSpiralSwitchover'] = 9; + + p['circleFootSeparation'] = 23; + + p['circleStartAngle'] = twoPi / 12; + + p['spiralFootSeparation'] = 26; + + p['spiralLengthStart'] = 11; + + p['spiralLengthFactor'] = 4; + + p['spiderfiedZIndex'] = 1000; + + p['usualLegZIndex'] = 10; + + p['highlightedLegZIndex'] = 20; + + p['event'] = 'click'; + + p['minZoomLevel'] = false; + + p['legWeight'] = 1.5; + + p['legColors'] = { + 'usual': {}, + 'highlighted': {} + }; + + lcU = p['legColors']['usual']; + + lcH = p['legColors']['highlighted']; + + _Class['initializeGoogleMaps'] = function(google) { + gm = google.maps; + ge = gm.event; + mt = gm.MapTypeId; + lcU[mt.HYBRID] = lcU[mt.SATELLITE] = '#fff'; + lcH[mt.HYBRID] = lcH[mt.SATELLITE] = '#f00'; + lcU[mt.TERRAIN] = lcU[mt.ROADMAP] = '#444'; + lcH[mt.TERRAIN] = lcH[mt.ROADMAP] = '#f00'; + this.ProjHelper = function(map) { + return this.setMap(map); + }; + this.ProjHelper.prototype = new gm.OverlayView(); + return this.ProjHelper.prototype['draw'] = function() {}; + }; + + function _Class(map1, opts) { + var e, k, l, len1, ref1, v; + this.map = map1; + if (opts == null) { + opts = {}; + } + for (k in opts) { + if (!hasProp.call(opts, k)) continue; + v = opts[k]; + this[k] = v; + } + this.projHelper = new this.constructor.ProjHelper(this.map); + this.initMarkerArrays(); + this.listeners = {}; + ref1 = ['click', 'zoom_changed', 'maptypeid_changed']; + for (l = 0, len1 = ref1.length; l < len1; l++) { + e = ref1[l]; + ge.addListener(this.map, e, (function(_this) { + return function() { + return _this['unspiderfy'](); + }; + })(this)); + } + } + + p.initMarkerArrays = function() { + this.markers = []; + return this.markerListenerRefs = []; + }; + + p['addMarker'] = function(marker) { + var listenerRefs; + if (marker['_oms'] != null) { + return this; + } + marker['_oms'] = true; + listenerRefs = [ + ge.addListener(marker, this['event'], (function(_this) { + return function(event) { + return _this.spiderListener(marker, event); + }; + })(this)) + ]; + if (!this['markersWontHide']) { + listenerRefs.push(ge.addListener(marker, 'visible_changed', (function(_this) { + return function() { + return _this.markerChangeListener(marker, false); + }; + })(this))); + } + if (!this['markersWontMove']) { + listenerRefs.push(ge.addListener(marker, 'position_changed', (function(_this) { + return function() { + return _this.markerChangeListener(marker, true); + }; + })(this))); + } + this.markerListenerRefs.push(listenerRefs); + this.markers.push(marker); + return this; + }; + + p.markerChangeListener = function(marker, positionChanged) { + if ((marker['_omsData'] != null) && (positionChanged || !marker.getVisible()) && !((this.spiderfying != null) || (this.unspiderfying != null))) { + return this['unspiderfy'](positionChanged ? marker : null); + } + }; + + p['getMarkers'] = function() { + return this.markers.slice(0); + }; + + p['removeMarker'] = function(marker) { + var i, l, len1, listenerRef, listenerRefs; + if (marker['_omsData'] != null) { + this['unspiderfy'](); + } + i = this.arrIndexOf(this.markers, marker); + if (i < 0) { + return this; + } + listenerRefs = this.markerListenerRefs.splice(i, 1)[0]; + for (l = 0, len1 = listenerRefs.length; l < len1; l++) { + listenerRef = listenerRefs[l]; + ge.removeListener(listenerRef); + } + delete marker['_oms']; + this.markers.splice(i, 1); + return this; + }; + + p['clearMarkers'] = function() { + var i, l, len1, len2, listenerRef, listenerRefs, marker, n, ref1; + this['unspiderfy'](); + ref1 = this.markers; + for (i = l = 0, len1 = ref1.length; l < len1; i = ++l) { + marker = ref1[i]; + listenerRefs = this.markerListenerRefs[i]; + for (n = 0, len2 = listenerRefs.length; n < len2; n++) { + listenerRef = listenerRefs[n]; + ge.removeListener(listenerRef); + } + delete marker['_oms']; + } + this.initMarkerArrays(); + return this; + }; + + p['addListener'] = function(event, func) { + var base; + ((base = this.listeners)[event] != null ? base[event] : base[event] = []).push(func); + return this; + }; + + p['removeListener'] = function(event, func) { + var i; + i = this.arrIndexOf(this.listeners[event], func); + if (!(i < 0)) { + this.listeners[event].splice(i, 1); + } + return this; + }; + + p['clearListeners'] = function(event) { + this.listeners[event] = []; + return this; + }; + + p.trigger = function() { + var args, event, func, l, len1, ref1, ref2, results; + event = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + ref2 = (ref1 = this.listeners[event]) != null ? ref1 : []; + results = []; + for (l = 0, len1 = ref2.length; l < len1; l++) { + func = ref2[l]; + results.push(func.apply(null, args)); + } + return results; + }; + + p.generatePtsCircle = function(count, centerPt) { + var angle, angleStep, circumference, i, l, legLength, ref1, results; + circumference = this['circleFootSeparation'] * (2 + count); + legLength = circumference / twoPi; + angleStep = twoPi / count; + results = []; + for (i = l = 0, ref1 = count; 0 <= ref1 ? l < ref1 : l > ref1; i = 0 <= ref1 ? ++l : --l) { + angle = this['circleStartAngle'] + i * angleStep; + results.push(new gm.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))); + } + return results; + }; + + p.generatePtsSpiral = function(count, centerPt) { + var angle, i, l, legLength, pt, ref1, results; + legLength = this['spiralLengthStart']; + angle = 0; + results = []; + for (i = l = 0, ref1 = count; 0 <= ref1 ? l < ref1 : l > ref1; i = 0 <= ref1 ? ++l : --l) { + angle += this['spiralFootSeparation'] / legLength + i * 0.0005; + pt = new gm.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle)); + legLength += twoPi * this['spiralLengthFactor'] / angle; + results.push(pt); + } + return results; + }; + + p.spiderListener = function(marker, event) { + var $this, clear, l, len1, m, mPt, markerPt, markerSpiderfied, nDist, nearbyMarkerData, nonNearbyMarkers, pxSq, ref1; + markerSpiderfied = marker['_omsData'] != null; + if (!(markerSpiderfied && this['keepSpiderfied'])) { + if (this['event'] === 'mouseover') { + $this = this; + clear = function() { + return $this['unspiderfy'](); + }; + window.clearTimeout(p.timeout); + p.timeout = setTimeout(clear, 3000); + } else { + this['unspiderfy'](); + } + } + if (markerSpiderfied || this.map.getStreetView().getVisible() || this.map.getMapTypeId() === 'GoogleEarthAPI') { + return this.trigger('click', marker, event); + } else { + nearbyMarkerData = []; + nonNearbyMarkers = []; + nDist = this['nearbyDistance']; + pxSq = nDist * nDist; + markerPt = this.llToPt(marker.position); + ref1 = this.markers; + for (l = 0, len1 = ref1.length; l < len1; l++) { + m = ref1[l]; + if (!((m.map != null) && m.getVisible())) { + continue; + } + mPt = this.llToPt(m.position); + if (this.ptDistanceSq(mPt, markerPt) < pxSq) { + nearbyMarkerData.push({ + marker: m, + markerPt: mPt + }); + } else { + nonNearbyMarkers.push(m); + } + } + if (nearbyMarkerData.length === 1) { + return this.trigger('click', marker, event); + } else { + return this.spiderfy(nearbyMarkerData, nonNearbyMarkers); + } + } + }; + + p['markersNearMarker'] = function(marker, firstOnly) { + var l, len1, m, mPt, markerPt, markers, nDist, pxSq, ref1, ref2, ref3; + if (firstOnly == null) { + firstOnly = false; + } + if (this.projHelper.getProjection() == null) { + throw "Must wait for 'idle' event on map before calling markersNearMarker"; + } + nDist = this['nearbyDistance']; + pxSq = nDist * nDist; + markerPt = this.llToPt(marker.position); + markers = []; + ref1 = this.markers; + for (l = 0, len1 = ref1.length; l < len1; l++) { + m = ref1[l]; + if (m === marker || (m.map == null) || !m.getVisible()) { + continue; + } + mPt = this.llToPt((ref2 = (ref3 = m['_omsData']) != null ? ref3.usualPosition : void 0) != null ? ref2 : m.position); + if (this.ptDistanceSq(mPt, markerPt) < pxSq) { + markers.push(m); + if (firstOnly) { + break; + } + } + } + return markers; + }; + + p['markersNearAnyOtherMarker'] = function() { + var i, i1, i2, l, len1, len2, len3, m, m1, m1Data, m2, m2Data, mData, n, nDist, pxSq, q, ref1, ref2, ref3, results; + if (this.projHelper.getProjection() == null) { + throw "Must wait for 'idle' event on map before calling markersNearAnyOtherMarker"; + } + nDist = this['nearbyDistance']; + pxSq = nDist * nDist; + mData = (function() { + var l, len1, ref1, ref2, ref3, results; + ref1 = this.markers; + results = []; + for (l = 0, len1 = ref1.length; l < len1; l++) { + m = ref1[l]; + results.push({ + pt: this.llToPt((ref2 = (ref3 = m['_omsData']) != null ? ref3.usualPosition : void 0) != null ? ref2 : m.position), + willSpiderfy: false + }); + } + return results; + }).call(this); + ref1 = this.markers; + for (i1 = l = 0, len1 = ref1.length; l < len1; i1 = ++l) { + m1 = ref1[i1]; + if (!((m1.map != null) && m1.getVisible())) { + continue; + } + m1Data = mData[i1]; + if (m1Data.willSpiderfy) { + continue; + } + ref2 = this.markers; + for (i2 = n = 0, len2 = ref2.length; n < len2; i2 = ++n) { + m2 = ref2[i2]; + if (i2 === i1) { + continue; + } + if (!((m2.map != null) && m2.getVisible())) { + continue; + } + m2Data = mData[i2]; + if (i2 < i1 && !m2Data.willSpiderfy) { + continue; + } + if (this.ptDistanceSq(m1Data.pt, m2Data.pt) < pxSq) { + m1Data.willSpiderfy = m2Data.willSpiderfy = true; + break; + } + } + } + ref3 = this.markers; + results = []; + for (i = q = 0, len3 = ref3.length; q < len3; i = ++q) { + m = ref3[i]; + if (mData[i].willSpiderfy) { + results.push(m); + } + } + return results; + }; + + p.makeHighlightListenerFuncs = function(marker) { + return { + highlight: (function(_this) { + return function() { + return marker['_omsData'].leg.setOptions({ + strokeColor: _this['legColors']['highlighted'][_this.map.mapTypeId], + zIndex: _this['highlightedLegZIndex'] + }); + }; + })(this), + unhighlight: (function(_this) { + return function() { + return marker['_omsData'].leg.setOptions({ + strokeColor: _this['legColors']['usual'][_this.map.mapTypeId], + zIndex: _this['usualLegZIndex'] + }); + }; + })(this) + }; + }; + + p.spiderfy = function(markerData, nonNearbyMarkers) { + var bodyPt, footLl, footPt, footPts, highlightListenerFuncs, leg, marker, md, nearestMarkerDatum, numFeet, spiderfiedMarkers; + if (this['minZoomLevel'] && this.map.getZoom() < this['minZoomLevel']) { + return false; + } + this.spiderfying = true; + numFeet = markerData.length; + bodyPt = this.ptAverage((function() { + var l, len1, results; + results = []; + for (l = 0, len1 = markerData.length; l < len1; l++) { + md = markerData[l]; + results.push(md.markerPt); + } + return results; + })()); + footPts = numFeet >= this['circleSpiralSwitchover'] ? this.generatePtsSpiral(numFeet, bodyPt).reverse() : this.generatePtsCircle(numFeet, bodyPt); + spiderfiedMarkers = (function() { + var l, len1, results; + results = []; + for (l = 0, len1 = footPts.length; l < len1; l++) { + footPt = footPts[l]; + footLl = this.ptToLl(footPt); + nearestMarkerDatum = this.minExtract(markerData, (function(_this) { + return function(md) { + return _this.ptDistanceSq(md.markerPt, footPt); + }; + })(this)); + marker = nearestMarkerDatum.marker; + leg = new gm.Polyline({ + map: this.map, + path: [marker.position, footLl], + strokeColor: this['legColors']['usual'][this.map.mapTypeId], + strokeWeight: this['legWeight'], + zIndex: this['usualLegZIndex'] + }); + marker['_omsData'] = { + usualPosition: marker.position, + leg: leg + }; + if (this['legColors']['highlighted'][this.map.mapTypeId] !== this['legColors']['usual'][this.map.mapTypeId]) { + highlightListenerFuncs = this.makeHighlightListenerFuncs(marker); + marker['_omsData'].hightlightListeners = { + highlight: ge.addListener(marker, 'mouseover', highlightListenerFuncs.highlight), + unhighlight: ge.addListener(marker, 'mouseout', highlightListenerFuncs.unhighlight) + }; + } + marker.setPosition(footLl); + marker.setZIndex(Math.round(this['spiderfiedZIndex'] + footPt.y)); + results.push(marker); + } + return results; + }).call(this); + delete this.spiderfying; + this.spiderfied = true; + return this.trigger('spiderfy', spiderfiedMarkers, nonNearbyMarkers); + }; + + p['unspiderfy'] = function(markerNotToMove) { + var l, len1, listeners, marker, nonNearbyMarkers, ref1, unspiderfiedMarkers; + if (markerNotToMove == null) { + markerNotToMove = null; + } + if (this.spiderfied == null) { + return this; + } + this.unspiderfying = true; + unspiderfiedMarkers = []; + nonNearbyMarkers = []; + ref1 = this.markers; + for (l = 0, len1 = ref1.length; l < len1; l++) { + marker = ref1[l]; + if (marker['_omsData'] != null) { + marker['_omsData'].leg.setMap(null); + if (marker !== markerNotToMove) { + marker.setPosition(marker['_omsData'].usualPosition); + } + marker.setZIndex(null); + listeners = marker['_omsData'].hightlightListeners; + if (listeners != null) { + ge.removeListener(listeners.highlight); + ge.removeListener(listeners.unhighlight); + } + delete marker['_omsData']; + unspiderfiedMarkers.push(marker); + } else { + nonNearbyMarkers.push(marker); + } + } + delete this.unspiderfying; + delete this.spiderfied; + this.trigger('unspiderfy', unspiderfiedMarkers, nonNearbyMarkers); + return this; + }; + + p.ptDistanceSq = function(pt1, pt2) { + var dx, dy; + dx = pt1.x - pt2.x; + dy = pt1.y - pt2.y; + return dx * dx + dy * dy; + }; + + p.ptAverage = function(pts) { + var l, len1, numPts, pt, sumX, sumY; + sumX = sumY = 0; + for (l = 0, len1 = pts.length; l < len1; l++) { + pt = pts[l]; + sumX += pt.x; + sumY += pt.y; + } + numPts = pts.length; + return new gm.Point(sumX / numPts, sumY / numPts); + }; + + p.llToPt = function(ll) { + return this.projHelper.getProjection().fromLatLngToDivPixel(ll); + }; + + p.ptToLl = function(pt) { + return this.projHelper.getProjection().fromDivPixelToLatLng(pt); + }; + + p.minExtract = function(set, func) { + var bestIndex, bestVal, index, item, l, len1, val; + for (index = l = 0, len1 = set.length; l < len1; index = ++l) { + item = set[index]; + val = func(item); + if ((typeof bestIndex === "undefined" || bestIndex === null) || val < bestVal) { + bestVal = val; + bestIndex = index; + } + } + return set.splice(bestIndex, 1)[0]; + }; + + p.arrIndexOf = function(arr, obj) { + var i, l, len1, o; + if (arr.indexOf != null) { + return arr.indexOf(obj); + } + for (i = l = 0, len1 = arr.length; l < len1; i = ++l) { + o = arr[i]; + if (o === obj) { + return i; + } + } + return -1; + }; + + return _Class; + +})(); diff --git a/dist/oms.min.js b/dist/oms.min.js new file mode 100644 index 0000000..c9ba423 --- /dev/null +++ b/dist/oms.min.js @@ -0,0 +1,23 @@ +(function(){/* + OverlappingMarkerSpiderfier +https://github.com/jawj/OverlappingMarkerSpiderfier +Copyright (c) 2011 - 2013 George MacKerron +Released under the MIT licence: http://opensource.org/licenses/mit-license +Note: The Google Maps API v3 must be included *before* this code +*/ +var B={}.hasOwnProperty,C=[].slice; +this.OverlappingMarkerSpiderfier=function(){function w(b,c){var a,g,e,f;this.map=b;null==c&&(c={});for(a in c)B.call(c,a)&&(g=c[a],this[a]=g);this.e=new this.constructor.g(this.map);this.n();this.b={};f=["click","zoom_changed","maptypeid_changed"];g=0;for(e=f.length;gc)return this;f=this.j.splice(c,1)[0];a=0;for(g=f.length;a< +g;a++)e=f[a],q.removeListener(e);delete b._oms;this.a.splice(c,1);return this};d.clearMarkers=function(){var b,c,a,g,e,f,d,h;this.unspiderfy();h=this.a;b=c=0;for(a=h.length;ca||this.b[b].splice(a,1);return this};d.clearListeners=function(b){this.b[b]= +[];return this};d.trigger=function(){var b,c,a,g,e,f;c=arguments[0];b=2<=arguments.length?C.call(arguments,1):[];c=null!=(a=this.b[c])?a:[];f=[];g=0;for(e=c.length;gb;a=0<=b?++e:--e)a=this.circleStartAngle+a*g,d.push(new s.Point(c.x+f*Math.cos(a),c.y+f*Math.sin(a)));return d};d.v=function(b,c){var a,g,e,f,d;f=this.spiralLengthStart;a=0;d=[];for(g=e=0;0<= +b?eb;g=0<=b?++e:--e)a+=this.spiralFootSeparation/f+5E-4*g,g=new s.Point(c.x+f*Math.cos(a),c.y+f*Math.sin(a)),f+=v*this.spiralLengthFactor/a,d.push(g);return d};d.G=function(b,c){var a,g,e,f,k,h,m,l,n,p;l=null!=b._omsData;l&&this.keepSpiderfied||("mouseover"===this.event?(a=this,g=function(){return a.unspiderfy()},window.clearTimeout(d.timeout),d.timeout=setTimeout(g,3E3)):this.unspiderfy());if(l||this.map.getStreetView().getVisible()||"GoogleEarthAPI"===this.map.getMapTypeId())return this.trigger("click", +b,c);g=[];l=[];e=this.nearbyDistance;n=e*e;m=this.c(b.position);p=this.a;e=0;for(f=p.length;e=this.circleSpiralSwitchover?this.v(p,a).reverse():this.u(p,a);a=function(){var a,c,l;l=[];a=0;for(c=f.length;a