From 8760e53a3da196a0a9db991b8ae0629465caf281 Mon Sep 17 00:00:00 2001 From: danieldh206 Date: Sun, 31 Jan 2021 15:15:04 -0800 Subject: [PATCH 1/4] adding marker png files --- simplemonitor/html/marker-shadow.png | Bin 0 -> 797 bytes simplemonitor/html/marker-single-down.png | Bin 0 -> 14761 bytes simplemonitor/html/marker-single-up.png | Bin 0 -> 2244 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 simplemonitor/html/marker-shadow.png create mode 100644 simplemonitor/html/marker-single-down.png create mode 100644 simplemonitor/html/marker-single-up.png diff --git a/simplemonitor/html/marker-shadow.png b/simplemonitor/html/marker-shadow.png new file mode 100644 index 0000000000000000000000000000000000000000..d1e773c715a9b508ebea055c4bb4b0a2ad7f6e52 GIT binary patch literal 797 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 VTZv)2T+7Hd`Uqp~BDmAh{{alvRU7~S literal 0 HcmV?d00001 diff --git a/simplemonitor/html/marker-single-up.png b/simplemonitor/html/marker-single-up.png new file mode 100644 index 0000000000000000000000000000000000000000..df69a9ee3af558f2fb10e15fbd7f9ed28354be98 GIT binary patch literal 2244 zcmbVOX;c&E8ct+UqV(26@e;2vMnu+x2_zwr0D%Mv5Y|Wnf*6v4L`)_o0Roj(C@?#74QAw>5n^SmhXGN_j%s^oY@i_xXKuV z#h_3qW413d6#1GXkL^4|S5YO< zaUY!GbAkbHSOEbfytAXw2?PN$84tRU$Yjs~AUc6Wf)jF+9YGfwiA*C>fR7grv8E8k z(L$L%A8jE!I!>%o$!P>aa&j_0*%=Qj;t3#?O4V@?iH?YdqcTON5~v+z%4M?*Oh_qI zNaQLBECY0mf>=0FMaLnYeh5J-=Wsp|%ak7zg`|w27RU)8-iaWU>f(B@tyG0V|8?WD z+DdMU93q55N;pv=MCuW@Y!-~LW<)_w1HlKzEk|7hLKt`ju>iks{N`H#YWN_8L*YMVeqgI>V8Uu^1 zuKgBAGq++$OE2eP{Dh`cL5$OS{Dla(=F z`;G_47uN1xWqfN>uY1hdkjyDX4==zMFD{}-wU{q`z4G?c3$ze3{nHtJJzbU@lRY6} zZv5?yiLREtGhx{Wdl|f;7Hbb=yj4=#vX&I92Ul);e~kb)Ss`d znrxzqpBv?zm|G{(#+jAA!0vb3%UOJP{5JMxm4dU`HD}|zZ=Rg9N2P~hYpYZ}J*2Oq zAD%R+TWW(A-r+A6PiKBxAIUc7yffVR?<3QVw^rcLqTU4;)Rfa|qn`RLsQ)Ylx}VNqNmkA@01}h-yw6=P=HUnEzP zw;24^;^X6c$6rz%CT}Pc!>ud)19F$#t9aI`=i71pTkB)u^<{_66(#6f(xE#qkIV&v z7wKQm!8Q+-XJw4F3nr7IddmjHi%j(Pu}9R`TAhZ(eT`Q{UlpJ)&oBP*5mwaDb>TYR zAlJBYf8~+30!4$XrA?>R2#!)edNMI-VRi&{s7qT?X6HmK&o|XWq=(mBYPbZNXMUBB7Hn@qY!6T8WUDX3lPXpb#pxZ!}K4g zq0fOclufTXmj03O*S&(%r#o-iTO+A9!+-UTfG9SWq-Vne zceRIRG@<%teuGSQjG59;5TiF+jXBy6Y?;HroL|QGSPrK~5ZKv%HNmy9+uLt;7kCm^`Y>j=X^#7mh~*CoD>MVK3w_sWR^)!?v%1|U1vtARYcRd1Bxt(q?oV1)Kuy*@ z(%|D*LB!ICna&ehE_?f?Yp?UCw}(H%*m53hzL(GIh@w?k+BPwGLHkmr4!IAiZr4Vq z_(;qu-7DHF9DENf&tu#76=6mqOCxRg80?7!-gZmPfu7(K%g%fEpRqp8ue*0Y zku=;~lsJ5F?bhF|?DA!@L+|JUXPO^N}FMv(E1E{+umO6)9$0t z;n6*yndS4s#GOOk_UMIkG>v|F_b*Jeyz`rG^t) Date: Thu, 25 Mar 2021 09:17:42 -0700 Subject: [PATCH 2/4] html map updates --- .../web_development/MarkerCluster.Default.css | 60 + .../html/web_development/MarkerCluster.css | 14 + .../html/web_development/index_cluster.html | 57 + .../leaflet.markercluster-src.js | 2677 +++++++++++++++++ .../web_development/marker-aggregation-up.png | Bin 0 -> 19804 bytes .../web_development/marker-multiple-up.png | Bin 0 -> 2500 bytes .../html/web_development/marker-shadow.png | Bin 0 -> 797 bytes .../web_development/marker-single-down.png | Bin 0 -> 14761 bytes .../html/web_development/marker-single-up.png | Bin 0 -> 2244 bytes .../html/web_development/markercluster-src.js | 2677 +++++++++++++++++ simplemonitor/html/web_development/sites.js | 147 + simplemonitor/html/web_development/sites.txt | 382 +++ simplemonitor/html/web_development/style.css | 28 + 13 files changed, 6042 insertions(+) create mode 100644 simplemonitor/html/web_development/MarkerCluster.Default.css create mode 100644 simplemonitor/html/web_development/MarkerCluster.css create mode 100644 simplemonitor/html/web_development/index_cluster.html create mode 100644 simplemonitor/html/web_development/leaflet.markercluster-src.js create mode 100644 simplemonitor/html/web_development/marker-aggregation-up.png create mode 100644 simplemonitor/html/web_development/marker-multiple-up.png create mode 100644 simplemonitor/html/web_development/marker-shadow.png create mode 100644 simplemonitor/html/web_development/marker-single-down.png create mode 100644 simplemonitor/html/web_development/marker-single-up.png create mode 100644 simplemonitor/html/web_development/markercluster-src.js create mode 100644 simplemonitor/html/web_development/sites.js create mode 100644 simplemonitor/html/web_development/sites.txt create mode 100644 simplemonitor/html/web_development/style.css diff --git a/simplemonitor/html/web_development/MarkerCluster.Default.css b/simplemonitor/html/web_development/MarkerCluster.Default.css new file mode 100644 index 00000000..da330ca8 --- /dev/null +++ b/simplemonitor/html/web_development/MarkerCluster.Default.css @@ -0,0 +1,60 @@ +.marker-cluster-small { + background-color: rgba(181, 226, 140, 0.6); + } +.marker-cluster-small div { + background-color: rgba(110, 204, 57, 0.6); + } + +.marker-cluster-medium { + background-color: rgba(241, 211, 87, 0.6); + } +.marker-cluster-medium div { + background-color: rgba(240, 194, 12, 0.6); + } + +.marker-cluster-large { + background-color: rgba(253, 156, 115, 0.6); + } +.marker-cluster-large div { + background-color: rgba(241, 128, 23, 0.6); + } + + /* IE 6-8 fallback colors */ +.leaflet-oldie .marker-cluster-small { + background-color: rgb(181, 226, 140); + } +.leaflet-oldie .marker-cluster-small div { + background-color: rgb(110, 204, 57); + } + +.leaflet-oldie .marker-cluster-medium { + background-color: rgb(241, 211, 87); + } +.leaflet-oldie .marker-cluster-medium div { + background-color: rgb(240, 194, 12); + } + +.leaflet-oldie .marker-cluster-large { + background-color: rgb(253, 156, 115); + } +.leaflet-oldie .marker-cluster-large div { + background-color: rgb(241, 128, 23); +} + +.marker-cluster { + background-clip: padding-box; + border-radius: 20px; + } +.marker-cluster div { + width: 30px; + height: 30px; + margin-left: 5px; + margin-top: 5px; + + text-align: center; + border-radius: 15px; + font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; + } +.marker-cluster span { + line-height: 30px; + } \ No newline at end of file diff --git a/simplemonitor/html/web_development/MarkerCluster.css b/simplemonitor/html/web_development/MarkerCluster.css new file mode 100644 index 00000000..8ce49a48 --- /dev/null +++ b/simplemonitor/html/web_development/MarkerCluster.css @@ -0,0 +1,14 @@ +.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { + -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in; + -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in; + -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in; + transition: transform 0.3s ease-out, opacity 0.3s ease-in; +} + +.leaflet-cluster-spider-leg { + /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */ + -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in; + -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in; + -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in; + transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in; +} \ No newline at end of file diff --git a/simplemonitor/html/web_development/index_cluster.html b/simplemonitor/html/web_development/index_cluster.html new file mode 100644 index 00000000..aa55a957 --- /dev/null +++ b/simplemonitor/html/web_development/index_cluster.html @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + +
+ Mouse over a cluster to see the bounds of its children and click a cluster to zoom to those bounds + + + \ No newline at end of file diff --git a/simplemonitor/html/web_development/leaflet.markercluster-src.js b/simplemonitor/html/web_development/leaflet.markercluster-src.js new file mode 100644 index 00000000..5c8d60ed --- /dev/null +++ b/simplemonitor/html/web_development/leaflet.markercluster-src.js @@ -0,0 +1,2677 @@ +/* + Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps. + https://github.com/Leaflet/Leaflet.markercluster + (c) 2012-2013, Dave Leaver, smartrak +*/ +(function (window, document, undefined) {/* + * L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within + */ + +L.MarkerClusterGroup = L.FeatureGroup.extend({ + + options: { + maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center + iconCreateFunction: null, + + spiderfyOnMaxZoom: true, + showCoverageOnHover: true, + zoomToBoundsOnClick: true, + singleMarkerMode: false, + + disableClusteringAtZoom: null, + + // Setting this to false prevents the removal of any clusters outside of the viewpoint, which + // is the default behaviour for performance reasons. + removeOutsideVisibleBounds: true, + + // Set to false to disable all animations (zoom and spiderfy). + // If false, option animateAddingMarkers below has no effect. + // If L.DomUtil.TRANSITION is falsy, this option has no effect. + animate: true, + + //Whether to animate adding markers after adding the MarkerClusterGroup to the map + // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains. + animateAddingMarkers: false, + + //Increase to increase the distance away that spiderfied markers appear from the center + spiderfyDistanceMultiplier: 1, + + // Make it possible to specify a polyline options on a spider leg + spiderLegPolylineOptions: { weight: 1.5, color: '#222', opacity: 0.5 }, + + // When bulk adding layers, adds markers in chunks. Means addLayers may not add all the layers in the call, others will be loaded during setTimeouts + chunkedLoading: false, + chunkInterval: 200, // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback) + chunkDelay: 50, // at the end of each interval, give n milliseconds back to system/browser + chunkProgress: null, // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator) + + //Options to pass to the L.Polygon constructor + polygonOptions: {} + }, + + initialize: function (options) { + L.Util.setOptions(this, options); + if (!this.options.iconCreateFunction) { + this.options.iconCreateFunction = this._defaultIconCreateFunction; + } + if (!this.options.clusterPane) { + this.options.clusterPane = L.Marker.prototype.options.pane; + } + + this._featureGroup = L.featureGroup(); + this._featureGroup.addEventParent(this); + + this._nonPointGroup = L.featureGroup(); + this._nonPointGroup.addEventParent(this); + + this._inZoomAnimation = 0; + this._needsClustering = []; + this._needsRemoving = []; //Markers removed while we aren't on the map need to be kept track of + //The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move + this._currentShownBounds = null; + + this._queue = []; + + this._childMarkerEventHandlers = { + 'dragstart': this._childMarkerDragStart, + 'move': this._childMarkerMoved, + 'dragend': this._childMarkerDragEnd, + }; + + // Hook the appropriate animation methods. + var animate = L.DomUtil.TRANSITION && this.options.animate; + L.extend(this, animate ? this._withAnimation : this._noAnimation); + // Remember which MarkerCluster class to instantiate (animated or not). + this._markerCluster = animate ? L.MarkerCluster : L.MarkerClusterNonAnimated; + }, + + addLayer: function (layer) { + + if (layer instanceof L.LayerGroup) { + return this.addLayers([layer]); + } + + //Don't cluster non point data + if (!layer.getLatLng) { + this._nonPointGroup.addLayer(layer); + this.fire('layeradd', { layer: layer }); + return this; + } + + if (!this._map) { + this._needsClustering.push(layer); + this.fire('layeradd', { layer: layer }); + return this; + } + + if (this.hasLayer(layer)) { + return this; + } + + + //If we have already clustered we'll need to add this one to a cluster + + if (this._unspiderfy) { + this._unspiderfy(); + } + + this._addLayer(layer, this._maxZoom); + this.fire('layeradd', { layer: layer }); + + // Refresh bounds and weighted positions. + this._topClusterLevel._recalculateBounds(); + + this._refreshClustersIcons(); + + //Work out what is visible + var visibleLayer = layer, + currentZoom = this._zoom; + if (layer.__parent) { + while (visibleLayer.__parent._zoom >= currentZoom) { + visibleLayer = visibleLayer.__parent; + } + } + + if (this._currentShownBounds.contains(visibleLayer.getLatLng())) { + if (this.options.animateAddingMarkers) { + this._animationAddLayer(layer, visibleLayer); + } else { + this._animationAddLayerNonAnimated(layer, visibleLayer); + } + } + return this; + }, + + removeLayer: function (layer) { + + if (layer instanceof L.LayerGroup) { + return this.removeLayers([layer]); + } + + //Non point layers + if (!layer.getLatLng) { + this._nonPointGroup.removeLayer(layer); + this.fire('layerremove', { layer: layer }); + return this; + } + + if (!this._map) { + if (!this._arraySplice(this._needsClustering, layer) && this.hasLayer(layer)) { + this._needsRemoving.push({ layer: layer, latlng: layer._latlng }); + } + this.fire('layerremove', { layer: layer }); + return this; + } + + if (!layer.__parent) { + return this; + } + + if (this._unspiderfy) { + this._unspiderfy(); + this._unspiderfyLayer(layer); + } + + //Remove the marker from clusters + this._removeLayer(layer, true); + this.fire('layerremove', { layer: layer }); + + // Refresh bounds and weighted positions. + this._topClusterLevel._recalculateBounds(); + + this._refreshClustersIcons(); + + layer.off(this._childMarkerEventHandlers, this); + + if (this._featureGroup.hasLayer(layer)) { + this._featureGroup.removeLayer(layer); + if (layer.clusterShow) { + layer.clusterShow(); + } + } + + return this; + }, + + //Takes an array of markers and adds them in bulk + addLayers: function (layersArray, skipLayerAddEvent) { + if (!L.Util.isArray(layersArray)) { + return this.addLayer(layersArray); + } + + var fg = this._featureGroup, + npg = this._nonPointGroup, + chunked = this.options.chunkedLoading, + chunkInterval = this.options.chunkInterval, + chunkProgress = this.options.chunkProgress, + l = layersArray.length, + offset = 0, + originalArray = true, + m; + + if (this._map) { + var started = (new Date()).getTime(); + var process = L.bind(function () { + var start = (new Date()).getTime(); + for (; offset < l; offset++) { + if (chunked && offset % 200 === 0) { + // every couple hundred markers, instrument the time elapsed since processing started: + var elapsed = (new Date()).getTime() - start; + if (elapsed > chunkInterval) { + break; // been working too hard, time to take a break :-) + } + } + + m = layersArray[offset]; + + // Group of layers, append children to layersArray and skip. + // Side effects: + // - Total increases, so chunkProgress ratio jumps backward. + // - Groups are not included in this group, only their non-group child layers (hasLayer). + // Changing array length while looping does not affect performance in current browsers: + // http://jsperf.com/for-loop-changing-length/6 + if (m instanceof L.LayerGroup) { + if (originalArray) { + layersArray = layersArray.slice(); + originalArray = false; + } + this._extractNonGroupLayers(m, layersArray); + l = layersArray.length; + continue; + } + + //Not point data, can't be clustered + if (!m.getLatLng) { + npg.addLayer(m); + if (!skipLayerAddEvent) { + this.fire('layeradd', { layer: m }); + } + continue; + } + + if (this.hasLayer(m)) { + continue; + } + + this._addLayer(m, this._maxZoom); + if (!skipLayerAddEvent) { + this.fire('layeradd', { layer: m }); + } + + //If we just made a cluster of size 2 then we need to remove the other marker from the map (if it is) or we never will + if (m.__parent) { + if (m.__parent.getChildCount() === 2) { + var markers = m.__parent.getAllChildMarkers(), + otherMarker = markers[0] === m ? markers[1] : markers[0]; + fg.removeLayer(otherMarker); + } + } + } + + if (chunkProgress) { + // report progress and time elapsed: + chunkProgress(offset, l, (new Date()).getTime() - started); + } + + // Completed processing all markers. + if (offset === l) { + + // Refresh bounds and weighted positions. + this._topClusterLevel._recalculateBounds(); + + this._refreshClustersIcons(); + + this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); + } else { + setTimeout(process, this.options.chunkDelay); + } + }, this); + + process(); + } else { + var needsClustering = this._needsClustering; + + for (; offset < l; offset++) { + m = layersArray[offset]; + + // Group of layers, append children to layersArray and skip. + if (m instanceof L.LayerGroup) { + if (originalArray) { + layersArray = layersArray.slice(); + originalArray = false; + } + this._extractNonGroupLayers(m, layersArray); + l = layersArray.length; + continue; + } + + //Not point data, can't be clustered + if (!m.getLatLng) { + npg.addLayer(m); + continue; + } + + if (this.hasLayer(m)) { + continue; + } + + needsClustering.push(m); + } + } + return this; + }, + + //Takes an array of markers and removes them in bulk + removeLayers: function (layersArray) { + var i, m, + l = layersArray.length, + fg = this._featureGroup, + npg = this._nonPointGroup, + originalArray = true; + + if (!this._map) { + for (i = 0; i < l; i++) { + m = layersArray[i]; + + // Group of layers, append children to layersArray and skip. + if (m instanceof L.LayerGroup) { + if (originalArray) { + layersArray = layersArray.slice(); + originalArray = false; + } + this._extractNonGroupLayers(m, layersArray); + l = layersArray.length; + continue; + } + + this._arraySplice(this._needsClustering, m); + npg.removeLayer(m); + if (this.hasLayer(m)) { + this._needsRemoving.push({ layer: m, latlng: m._latlng }); + } + this.fire('layerremove', { layer: m }); + } + return this; + } + + if (this._unspiderfy) { + this._unspiderfy(); + + // Work on a copy of the array, so that next loop is not affected. + var layersArray2 = layersArray.slice(), + l2 = l; + for (i = 0; i < l2; i++) { + m = layersArray2[i]; + + // Group of layers, append children to layersArray and skip. + if (m instanceof L.LayerGroup) { + this._extractNonGroupLayers(m, layersArray2); + l2 = layersArray2.length; + continue; + } + + this._unspiderfyLayer(m); + } + } + + for (i = 0; i < l; i++) { + m = layersArray[i]; + + // Group of layers, append children to layersArray and skip. + if (m instanceof L.LayerGroup) { + if (originalArray) { + layersArray = layersArray.slice(); + originalArray = false; + } + this._extractNonGroupLayers(m, layersArray); + l = layersArray.length; + continue; + } + + if (!m.__parent) { + npg.removeLayer(m); + this.fire('layerremove', { layer: m }); + continue; + } + + this._removeLayer(m, true, true); + this.fire('layerremove', { layer: m }); + + if (fg.hasLayer(m)) { + fg.removeLayer(m); + if (m.clusterShow) { + m.clusterShow(); + } + } + } + + // Refresh bounds and weighted positions. + this._topClusterLevel._recalculateBounds(); + + this._refreshClustersIcons(); + + //Fix up the clusters and markers on the map + this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); + + return this; + }, + + //Removes all layers from the MarkerClusterGroup + clearLayers: function () { + //Need our own special implementation as the LayerGroup one doesn't work for us + + //If we aren't on the map (yet), blow away the markers we know of + if (!this._map) { + this._needsClustering = []; + delete this._gridClusters; + delete this._gridUnclustered; + } + + if (this._noanimationUnspiderfy) { + this._noanimationUnspiderfy(); + } + + //Remove all the visible layers + this._featureGroup.clearLayers(); + this._nonPointGroup.clearLayers(); + + this.eachLayer(function (marker) { + marker.off(this._childMarkerEventHandlers, this); + delete marker.__parent; + }, this); + + if (this._map) { + //Reset _topClusterLevel and the DistanceGrids + this._generateInitialClusters(); + } + + return this; + }, + + //Override FeatureGroup.getBounds as it doesn't work + getBounds: function () { + var bounds = new L.LatLngBounds(); + + if (this._topClusterLevel) { + bounds.extend(this._topClusterLevel._bounds); + } + + for (var i = this._needsClustering.length - 1; i >= 0; i--) { + bounds.extend(this._needsClustering[i].getLatLng()); + } + + bounds.extend(this._nonPointGroup.getBounds()); + + return bounds; + }, + + //Overrides LayerGroup.eachLayer + eachLayer: function (method, context) { + var markers = this._needsClustering.slice(), + needsRemoving = this._needsRemoving, + thisNeedsRemoving, i, j; + + if (this._topClusterLevel) { + this._topClusterLevel.getAllChildMarkers(markers); + } + + for (i = markers.length - 1; i >= 0; i--) { + thisNeedsRemoving = true; + + for (j = needsRemoving.length - 1; j >= 0; j--) { + if (needsRemoving[j].layer === markers[i]) { + thisNeedsRemoving = false; + break; + } + } + + if (thisNeedsRemoving) { + method.call(context, markers[i]); + } + } + + this._nonPointGroup.eachLayer(method, context); + }, + + //Overrides LayerGroup.getLayers + getLayers: function () { + var layers = []; + this.eachLayer(function (l) { + layers.push(l); + }); + return layers; + }, + + //Overrides LayerGroup.getLayer, WARNING: Really bad performance + getLayer: function (id) { + var result = null; + + id = parseInt(id, 10); + + this.eachLayer(function (l) { + if (L.stamp(l) === id) { + result = l; + } + }); + + return result; + }, + + //Returns true if the given layer is in this MarkerClusterGroup + hasLayer: function (layer) { + if (!layer) { + return false; + } + + var i, anArray = this._needsClustering; + + for (i = anArray.length - 1; i >= 0; i--) { + if (anArray[i] === layer) { + return true; + } + } + + anArray = this._needsRemoving; + for (i = anArray.length - 1; i >= 0; i--) { + if (anArray[i].layer === layer) { + return false; + } + } + + return !!(layer.__parent && layer.__parent._group === this) || this._nonPointGroup.hasLayer(layer); + }, + + //Zoom down to show the given layer (spiderfying if necessary) then calls the callback + zoomToShowLayer: function (layer, callback) { + + if (typeof callback !== 'function') { + callback = function () {}; + } + + var showMarker = function () { + if ((layer._icon || layer.__parent._icon) && !this._inZoomAnimation) { + this._map.off('moveend', showMarker, this); + this.off('animationend', showMarker, this); + + if (layer._icon) { + callback(); + } else if (layer.__parent._icon) { + this.once('spiderfied', callback, this); + layer.__parent.spiderfy(); + } + } + }; + + if (layer._icon && this._map.getBounds().contains(layer.getLatLng())) { + //Layer is visible ond on screen, immediate return + callback(); + } else if (layer.__parent._zoom < Math.round(this._map._zoom)) { + //Layer should be visible at this zoom level. It must not be on screen so just pan over to it + this._map.on('moveend', showMarker, this); + this._map.panTo(layer.getLatLng()); + } else { + this._map.on('moveend', showMarker, this); + this.on('animationend', showMarker, this); + layer.__parent.zoomToBounds(); + } + }, + + //Overrides FeatureGroup.onAdd + onAdd: function (map) { + this._map = map; + var i, l, layer; + + if (!isFinite(this._map.getMaxZoom())) { + throw "Map has no maxZoom specified"; + } + + this._featureGroup.addTo(map); + this._nonPointGroup.addTo(map); + + if (!this._gridClusters) { + this._generateInitialClusters(); + } + + this._maxLat = map.options.crs.projection.MAX_LATITUDE; + + //Restore all the positions as they are in the MCG before removing them + for (i = 0, l = this._needsRemoving.length; i < l; i++) { + layer = this._needsRemoving[i]; + layer.newlatlng = layer.layer._latlng; + layer.layer._latlng = layer.latlng; + } + //Remove them, then restore their new positions + for (i = 0, l = this._needsRemoving.length; i < l; i++) { + layer = this._needsRemoving[i]; + this._removeLayer(layer.layer, true); + layer.layer._latlng = layer.newlatlng; + } + this._needsRemoving = []; + + //Remember the current zoom level and bounds + this._zoom = Math.round(this._map._zoom); + this._currentShownBounds = this._getExpandedVisibleBounds(); + + this._map.on('zoomend', this._zoomEnd, this); + this._map.on('moveend', this._moveEnd, this); + + if (this._spiderfierOnAdd) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely + this._spiderfierOnAdd(); + } + + this._bindEvents(); + + //Actually add our markers to the map: + l = this._needsClustering; + this._needsClustering = []; + this.addLayers(l, true); + }, + + //Overrides FeatureGroup.onRemove + onRemove: function (map) { + map.off('zoomend', this._zoomEnd, this); + map.off('moveend', this._moveEnd, this); + + this._unbindEvents(); + + //In case we are in a cluster animation + this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', ''); + + if (this._spiderfierOnRemove) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely + this._spiderfierOnRemove(); + } + + delete this._maxLat; + + //Clean up all the layers we added to the map + this._hideCoverage(); + this._featureGroup.remove(); + this._nonPointGroup.remove(); + + this._featureGroup.clearLayers(); + + this._map = null; + }, + + getVisibleParent: function (marker) { + var vMarker = marker; + while (vMarker && !vMarker._icon) { + vMarker = vMarker.__parent; + } + return vMarker || null; + }, + + //Remove the given object from the given array + _arraySplice: function (anArray, obj) { + for (var i = anArray.length - 1; i >= 0; i--) { + if (anArray[i] === obj) { + anArray.splice(i, 1); + return true; + } + } + }, + + /** + * Removes a marker from all _gridUnclustered zoom levels, starting at the supplied zoom. + * @param marker to be removed from _gridUnclustered. + * @param z integer bottom start zoom level (included) + * @private + */ + _removeFromGridUnclustered: function (marker, z) { + var map = this._map, + gridUnclustered = this._gridUnclustered, + minZoom = Math.floor(this._map.getMinZoom()); + + for (; z >= minZoom; z--) { + if (!gridUnclustered[z].removeObject(marker, map.project(marker.getLatLng(), z))) { + break; + } + } + }, + + _childMarkerDragStart: function (e) { + e.target.__dragStart = e.target._latlng; + }, + + _childMarkerMoved: function (e) { + if (!this._ignoreMove && !e.target.__dragStart) { + var isPopupOpen = e.target._popup && e.target._popup.isOpen(); + + this._moveChild(e.target, e.oldLatLng, e.latlng); + + if (isPopupOpen) { + e.target.openPopup(); + } + } + }, + + _moveChild: function (layer, from, to) { + layer._latlng = from; + this.removeLayer(layer); + + layer._latlng = to; + this.addLayer(layer); + }, + + _childMarkerDragEnd: function (e) { + if (e.target.__dragStart) { + this._moveChild(e.target, e.target.__dragStart, e.target._latlng); + } + delete e.target.__dragStart; + }, + + + //Internal function for removing a marker from everything. + //dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions) + _removeLayer: function (marker, removeFromDistanceGrid, dontUpdateMap) { + var gridClusters = this._gridClusters, + gridUnclustered = this._gridUnclustered, + fg = this._featureGroup, + map = this._map, + minZoom = Math.floor(this._map.getMinZoom()); + + //Remove the marker from distance clusters it might be in + if (removeFromDistanceGrid) { + this._removeFromGridUnclustered(marker, this._maxZoom); + } + + //Work our way up the clusters removing them as we go if required + var cluster = marker.__parent, + markers = cluster._markers, + otherMarker; + + //Remove the marker from the immediate parents marker list + this._arraySplice(markers, marker); + + while (cluster) { + cluster._childCount--; + cluster._boundsNeedUpdate = true; + + if (cluster._zoom < minZoom) { + //Top level, do nothing + break; + } else if (removeFromDistanceGrid && cluster._childCount <= 1) { //Cluster no longer required + //We need to push the other marker up to the parent + otherMarker = cluster._markers[0] === marker ? cluster._markers[1] : cluster._markers[0]; + + //Update distance grid + gridClusters[cluster._zoom].removeObject(cluster, map.project(cluster._cLatLng, cluster._zoom)); + gridUnclustered[cluster._zoom].addObject(otherMarker, map.project(otherMarker.getLatLng(), cluster._zoom)); + + //Move otherMarker up to parent + this._arraySplice(cluster.__parent._childClusters, cluster); + cluster.__parent._markers.push(otherMarker); + otherMarker.__parent = cluster.__parent; + + if (cluster._icon) { + //Cluster is currently on the map, need to put the marker on the map instead + fg.removeLayer(cluster); + if (!dontUpdateMap) { + fg.addLayer(otherMarker); + } + } + } else { + cluster._iconNeedsUpdate = true; + } + + cluster = cluster.__parent; + } + + delete marker.__parent; + }, + + _isOrIsParent: function (el, oel) { + while (oel) { + if (el === oel) { + return true; + } + oel = oel.parentNode; + } + return false; + }, + + //Override L.Evented.fire + fire: function (type, data, propagate) { + if (data && data.layer instanceof L.MarkerCluster) { + //Prevent multiple clustermouseover/off events if the icon is made up of stacked divs (Doesn't work in ie <= 8, no relatedTarget) + if (data.originalEvent && this._isOrIsParent(data.layer._icon, data.originalEvent.relatedTarget)) { + return; + } + type = 'cluster' + type; + } + + L.FeatureGroup.prototype.fire.call(this, type, data, propagate); + }, + + //Override L.Evented.listens + listens: function (type, propagate) { + return L.FeatureGroup.prototype.listens.call(this, type, propagate) || L.FeatureGroup.prototype.listens.call(this, 'cluster' + type, propagate); + }, + + //Default functionality + _defaultIconCreateFunction: function (cluster) { + var childCount = cluster.getChildCount(); + + var c = ' marker-cluster-'; + if (childCount < 10) { + c += 'small'; + } else if (childCount < 100) { + c += 'medium'; + } else { + c += 'large'; + } + + return new L.DivIcon({ html: '
' + childCount + '
', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) }); + }, + + _bindEvents: function () { + var map = this._map, + spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom, + showCoverageOnHover = this.options.showCoverageOnHover, + zoomToBoundsOnClick = this.options.zoomToBoundsOnClick; + + //Zoom on cluster click or spiderfy if we are at the lowest level + if (spiderfyOnMaxZoom || zoomToBoundsOnClick) { + this.on('clusterclick', this._zoomOrSpiderfy, this); + } + + //Show convex hull (boundary) polygon on mouse over + if (showCoverageOnHover) { + this.on('clustermouseover', this._showCoverage, this); + this.on('clustermouseout', this._hideCoverage, this); + map.on('zoomend', this._hideCoverage, this); + } + }, + + _zoomOrSpiderfy: function (e) { + var cluster = e.layer, + bottomCluster = cluster; + + while (bottomCluster._childClusters.length === 1) { + bottomCluster = bottomCluster._childClusters[0]; + } + + if (bottomCluster._zoom === this._maxZoom && + bottomCluster._childCount === cluster._childCount && + this.options.spiderfyOnMaxZoom) { + + // All child markers are contained in a single cluster from this._maxZoom to this cluster. + cluster.spiderfy(); + } else if (this.options.zoomToBoundsOnClick) { + cluster.zoomToBounds(); + } + + // Focus the map again for keyboard users. + if (e.originalEvent && e.originalEvent.keyCode === 13) { + this._map._container.focus(); + } + }, + + _showCoverage: function (e) { + var map = this._map; + if (this._inZoomAnimation) { + return; + } + if (this._shownPolygon) { + map.removeLayer(this._shownPolygon); + } + if (e.layer.getChildCount() > 2 && e.layer !== this._spiderfied) { + this._shownPolygon = new L.Polygon(e.layer.getConvexHull(), this.options.polygonOptions); + map.addLayer(this._shownPolygon); + } + }, + + _hideCoverage: function () { + if (this._shownPolygon) { + this._map.removeLayer(this._shownPolygon); + this._shownPolygon = null; + } + }, + + _unbindEvents: function () { + var spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom, + showCoverageOnHover = this.options.showCoverageOnHover, + zoomToBoundsOnClick = this.options.zoomToBoundsOnClick, + map = this._map; + + if (spiderfyOnMaxZoom || zoomToBoundsOnClick) { + this.off('clusterclick', this._zoomOrSpiderfy, this); + } + if (showCoverageOnHover) { + this.off('clustermouseover', this._showCoverage, this); + this.off('clustermouseout', this._hideCoverage, this); + map.off('zoomend', this._hideCoverage, this); + } + }, + + _zoomEnd: function () { + if (!this._map) { //May have been removed from the map by a zoomEnd handler + return; + } + this._mergeSplitClusters(); + + this._zoom = Math.round(this._map._zoom); + this._currentShownBounds = this._getExpandedVisibleBounds(); + }, + + _moveEnd: function () { + if (this._inZoomAnimation) { + return; + } + + var newBounds = this._getExpandedVisibleBounds(); + + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), this._zoom, newBounds); + this._topClusterLevel._recursivelyAddChildrenToMap(null, Math.round(this._map._zoom), newBounds); + + this._currentShownBounds = newBounds; + return; + }, + + _generateInitialClusters: function () { + var maxZoom = Math.ceil(this._map.getMaxZoom()), + minZoom = Math.floor(this._map.getMinZoom()), + radius = this.options.maxClusterRadius, + radiusFn = radius; + + //If we just set maxClusterRadius to a single number, we need to create + //a simple function to return that number. Otherwise, we just have to + //use the function we've passed in. + if (typeof radius !== "function") { + radiusFn = function () { return radius; }; + } + + if (this.options.disableClusteringAtZoom !== null) { + maxZoom = this.options.disableClusteringAtZoom - 1; + } + this._maxZoom = maxZoom; + this._gridClusters = {}; + this._gridUnclustered = {}; + + //Set up DistanceGrids for each zoom + for (var zoom = maxZoom; zoom >= minZoom; zoom--) { + this._gridClusters[zoom] = new L.DistanceGrid(radiusFn(zoom)); + this._gridUnclustered[zoom] = new L.DistanceGrid(radiusFn(zoom)); + } + + // Instantiate the appropriate L.MarkerCluster class (animated or not). + this._topClusterLevel = new this._markerCluster(this, minZoom - 1); + }, + + //Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom) + _addLayer: function (layer, zoom) { + var gridClusters = this._gridClusters, + gridUnclustered = this._gridUnclustered, + minZoom = Math.floor(this._map.getMinZoom()), + markerPoint, z; + + if (this.options.singleMarkerMode) { + this._overrideMarkerIcon(layer); + } + + layer.on(this._childMarkerEventHandlers, this); + + //Find the lowest zoom level to slot this one in + for (; zoom >= minZoom; zoom--) { + markerPoint = this._map.project(layer.getLatLng(), zoom); // calculate pixel position + + //Try find a cluster close by + var closest = gridClusters[zoom].getNearObject(markerPoint); + if (closest) { + closest._addChild(layer); + layer.__parent = closest; + return; + } + + //Try find a marker close by to form a new cluster with + closest = gridUnclustered[zoom].getNearObject(markerPoint); + if (closest) { + var parent = closest.__parent; + if (parent) { + this._removeLayer(closest, false); + } + + //Create new cluster with these 2 in it + + var newCluster = new this._markerCluster(this, zoom, closest, layer); + gridClusters[zoom].addObject(newCluster, this._map.project(newCluster._cLatLng, zoom)); + closest.__parent = newCluster; + layer.__parent = newCluster; + + //First create any new intermediate parent clusters that don't exist + var lastParent = newCluster; + for (z = zoom - 1; z > parent._zoom; z--) { + lastParent = new this._markerCluster(this, z, lastParent); + gridClusters[z].addObject(lastParent, this._map.project(closest.getLatLng(), z)); + } + parent._addChild(lastParent); + + //Remove closest from this zoom level and any above that it is in, replace with newCluster + this._removeFromGridUnclustered(closest, zoom); + + return; + } + + //Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards + gridUnclustered[zoom].addObject(layer, markerPoint); + } + + //Didn't get in anything, add us to the top + this._topClusterLevel._addChild(layer); + layer.__parent = this._topClusterLevel; + return; + }, + + /** + * Refreshes the icon of all "dirty" visible clusters. + * Non-visible "dirty" clusters will be updated when they are added to the map. + * @private + */ + _refreshClustersIcons: function () { + this._featureGroup.eachLayer(function (c) { + if (c instanceof L.MarkerCluster && c._iconNeedsUpdate) { + c._updateIcon(); + } + }); + }, + + //Enqueue code to fire after the marker expand/contract has happened + _enqueue: function (fn) { + this._queue.push(fn); + if (!this._queueTimeout) { + this._queueTimeout = setTimeout(L.bind(this._processQueue, this), 300); + } + }, + _processQueue: function () { + for (var i = 0; i < this._queue.length; i++) { + this._queue[i].call(this); + } + this._queue.length = 0; + clearTimeout(this._queueTimeout); + this._queueTimeout = null; + }, + + //Merge and split any existing clusters that are too big or small + _mergeSplitClusters: function () { + var mapZoom = Math.round(this._map._zoom); + + //In case we are starting to split before the animation finished + this._processQueue(); + + if (this._zoom < mapZoom && this._currentShownBounds.intersects(this._getExpandedVisibleBounds())) { //Zoom in, split + this._animationStart(); + //Remove clusters now off screen + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), this._zoom, this._getExpandedVisibleBounds()); + + this._animationZoomIn(this._zoom, mapZoom); + + } else if (this._zoom > mapZoom) { //Zoom out, merge + this._animationStart(); + + this._animationZoomOut(this._zoom, mapZoom); + } else { + this._moveEnd(); + } + }, + + //Gets the maps visible bounds expanded in each direction by the size of the screen (so the user cannot see an area we do not cover in one pan) + _getExpandedVisibleBounds: function () { + if (!this.options.removeOutsideVisibleBounds) { + return this._mapBoundsInfinite; + } else if (L.Browser.mobile) { + return this._checkBoundsMaxLat(this._map.getBounds()); + } + + return this._checkBoundsMaxLat(this._map.getBounds().pad(1)); // Padding expands the bounds by its own dimensions but scaled with the given factor. + }, + + /** + * Expands the latitude to Infinity (or -Infinity) if the input bounds reach the map projection maximum defined latitude + * (in the case of Web/Spherical Mercator, it is 85.0511287798 / see https://en.wikipedia.org/wiki/Web_Mercator#Formulas). + * Otherwise, the removeOutsideVisibleBounds option will remove markers beyond that limit, whereas the same markers without + * this option (or outside MCG) will have their position floored (ceiled) by the projection and rendered at that limit, + * making the user think that MCG "eats" them and never displays them again. + * @param bounds L.LatLngBounds + * @returns {L.LatLngBounds} + * @private + */ + _checkBoundsMaxLat: function (bounds) { + var maxLat = this._maxLat; + + if (maxLat !== undefined) { + if (bounds.getNorth() >= maxLat) { + bounds._northEast.lat = Infinity; + } + if (bounds.getSouth() <= -maxLat) { + bounds._southWest.lat = -Infinity; + } + } + + return bounds; + }, + + //Shared animation code + _animationAddLayerNonAnimated: function (layer, newCluster) { + if (newCluster === layer) { + this._featureGroup.addLayer(layer); + } else if (newCluster._childCount === 2) { + newCluster._addToMap(); + + var markers = newCluster.getAllChildMarkers(); + this._featureGroup.removeLayer(markers[0]); + this._featureGroup.removeLayer(markers[1]); + } else { + newCluster._updateIcon(); + } + }, + + /** + * Extracts individual (i.e. non-group) layers from a Layer Group. + * @param group to extract layers from. + * @param output {Array} in which to store the extracted layers. + * @returns {*|Array} + * @private + */ + _extractNonGroupLayers: function (group, output) { + var layers = group.getLayers(), + i = 0, + layer; + + output = output || []; + + for (; i < layers.length; i++) { + layer = layers[i]; + + if (layer instanceof L.LayerGroup) { + this._extractNonGroupLayers(layer, output); + continue; + } + + output.push(layer); + } + + return output; + }, + + /** + * Implements the singleMarkerMode option. + * @param layer Marker to re-style using the Clusters iconCreateFunction. + * @returns {L.Icon} The newly created icon. + * @private + */ + _overrideMarkerIcon: function (layer) { + var icon = layer.options.icon = this.options.iconCreateFunction({ + getChildCount: function () { + return 1; + }, + getAllChildMarkers: function () { + return [layer]; + } + }); + + return icon; + } +}); + +// Constant bounds used in case option "removeOutsideVisibleBounds" is set to false. +L.MarkerClusterGroup.include({ + _mapBoundsInfinite: new L.LatLngBounds(new L.LatLng(-Infinity, -Infinity), new L.LatLng(Infinity, Infinity)) +}); + +L.MarkerClusterGroup.include({ + _noAnimation: { + //Non Animated versions of everything + _animationStart: function () { + //Do nothing... + }, + _animationZoomIn: function (previousZoomLevel, newZoomLevel) { + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), previousZoomLevel); + this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); + + //We didn't actually animate, but we use this event to mean "clustering animations have finished" + this.fire('animationend'); + }, + _animationZoomOut: function (previousZoomLevel, newZoomLevel) { + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), previousZoomLevel); + this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); + + //We didn't actually animate, but we use this event to mean "clustering animations have finished" + this.fire('animationend'); + }, + _animationAddLayer: function (layer, newCluster) { + this._animationAddLayerNonAnimated(layer, newCluster); + } + }, + + _withAnimation: { + //Animated versions here + _animationStart: function () { + this._map._mapPane.className += ' leaflet-cluster-anim'; + this._inZoomAnimation++; + }, + + _animationZoomIn: function (previousZoomLevel, newZoomLevel) { + var bounds = this._getExpandedVisibleBounds(), + fg = this._featureGroup, + minZoom = Math.floor(this._map.getMinZoom()), + i; + + this._ignoreMove = true; + + //Add all children of current clusters to map and remove those clusters from map + this._topClusterLevel._recursively(bounds, previousZoomLevel, minZoom, function (c) { + var startPos = c._latlng, + markers = c._markers, + m; + + if (!bounds.contains(startPos)) { + startPos = null; + } + + if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us + fg.removeLayer(c); + c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds); + } else { + //Fade out old cluster + c.clusterHide(); + c._recursivelyAddChildrenToMap(startPos, newZoomLevel, bounds); + } + + //Remove all markers that aren't visible any more + //TODO: Do we actually need to do this on the higher levels too? + for (i = markers.length - 1; i >= 0; i--) { + m = markers[i]; + if (!bounds.contains(m._latlng)) { + fg.removeLayer(m); + } + } + + }); + + this._forceLayout(); + + //Update opacities + this._topClusterLevel._recursivelyBecomeVisible(bounds, newZoomLevel); + //TODO Maybe? Update markers in _recursivelyBecomeVisible + fg.eachLayer(function (n) { + if (!(n instanceof L.MarkerCluster) && n._icon) { + n.clusterShow(); + } + }); + + //update the positions of the just added clusters/markers + this._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) { + c._recursivelyRestoreChildPositions(newZoomLevel); + }); + + this._ignoreMove = false; + + //Remove the old clusters and close the zoom animation + this._enqueue(function () { + //update the positions of the just added clusters/markers + this._topClusterLevel._recursively(bounds, previousZoomLevel, minZoom, function (c) { + fg.removeLayer(c); + c.clusterShow(); + }); + + this._animationEnd(); + }); + }, + + _animationZoomOut: function (previousZoomLevel, newZoomLevel) { + this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel - 1, newZoomLevel); + + //Need to add markers for those that weren't on the map before but are now + this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); + //Remove markers that were on the map before but won't be now + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), previousZoomLevel, this._getExpandedVisibleBounds()); + }, + + _animationAddLayer: function (layer, newCluster) { + var me = this, + fg = this._featureGroup; + + fg.addLayer(layer); + if (newCluster !== layer) { + if (newCluster._childCount > 2) { //Was already a cluster + + newCluster._updateIcon(); + this._forceLayout(); + this._animationStart(); + + layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng())); + layer.clusterHide(); + + this._enqueue(function () { + fg.removeLayer(layer); + layer.clusterShow(); + + me._animationEnd(); + }); + + } else { //Just became a cluster + this._forceLayout(); + + me._animationStart(); + me._animationZoomOutSingle(newCluster, this._map.getMaxZoom(), this._zoom); + } + } + } + }, + + // Private methods for animated versions. + _animationZoomOutSingle: function (cluster, previousZoomLevel, newZoomLevel) { + var bounds = this._getExpandedVisibleBounds(), + minZoom = Math.floor(this._map.getMinZoom()); + + //Animate all of the markers in the clusters to move to their cluster center point + cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, minZoom, previousZoomLevel + 1, newZoomLevel); + + var me = this; + + //Update the opacity (If we immediately set it they won't animate) + this._forceLayout(); + cluster._recursivelyBecomeVisible(bounds, newZoomLevel); + + //TODO: Maybe use the transition timing stuff to make this more reliable + //When the animations are done, tidy up + this._enqueue(function () { + + //This cluster stopped being a cluster before the timeout fired + if (cluster._childCount === 1) { + var m = cluster._markers[0]; + //If we were in a cluster animation at the time then the opacity and position of our child could be wrong now, so fix it + this._ignoreMove = true; + m.setLatLng(m.getLatLng()); + this._ignoreMove = false; + if (m.clusterShow) { + m.clusterShow(); + } + } else { + cluster._recursively(bounds, newZoomLevel, minZoom, function (c) { + c._recursivelyRemoveChildrenFromMap(bounds, minZoom, previousZoomLevel + 1); + }); + } + me._animationEnd(); + }); + }, + + _animationEnd: function () { + if (this._map) { + this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', ''); + } + this._inZoomAnimation--; + this.fire('animationend'); + }, + + //Force a browser layout of stuff in the map + // Should apply the current opacity and location to all elements so we can update them again for an animation + _forceLayout: function () { + //In my testing this works, infact offsetWidth of any element seems to work. + //Could loop all this._layers and do this for each _icon if it stops working + + L.Util.falseFn(document.body.offsetWidth); + } +}); + +L.markerClusterGroup = function (options) { + return new L.MarkerClusterGroup(options); +}; + + +L.MarkerCluster = L.Marker.extend({ + initialize: function (group, zoom, a, b) { + + L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0), + { icon: this, pane: group.options.clusterPane }); + + this._group = group; + this._zoom = zoom; + + this._markers = []; + this._childClusters = []; + this._childCount = 0; + this._iconNeedsUpdate = true; + this._boundsNeedUpdate = true; + + this._bounds = new L.LatLngBounds(); + + if (a) { + this._addChild(a); + } + if (b) { + this._addChild(b); + } + }, + + //Recursively retrieve all child markers of this cluster + getAllChildMarkers: function (storageArray) { + storageArray = storageArray || []; + + for (var i = this._childClusters.length - 1; i >= 0; i--) { + this._childClusters[i].getAllChildMarkers(storageArray); + } + + for (var j = this._markers.length - 1; j >= 0; j--) { + storageArray.push(this._markers[j]); + } + + return storageArray; + }, + + //Returns the count of how many child markers we have + getChildCount: function () { + return this._childCount; + }, + + //Zoom to the minimum of showing all of the child markers, or the extents of this cluster + zoomToBounds: function (fitBoundsOptions) { + var childClusters = this._childClusters.slice(), + map = this._group._map, + boundsZoom = map.getBoundsZoom(this._bounds), + zoom = this._zoom + 1, + mapZoom = map.getZoom(), + i; + + //calculate how far we need to zoom down to see all of the markers + while (childClusters.length > 0 && boundsZoom > zoom) { + zoom++; + var newClusters = []; + for (i = 0; i < childClusters.length; i++) { + newClusters = newClusters.concat(childClusters[i]._childClusters); + } + childClusters = newClusters; + } + + if (boundsZoom > zoom) { + this._group._map.setView(this._latlng, zoom); + } else if (boundsZoom <= mapZoom) { //If fitBounds wouldn't zoom us down, zoom us down instead + this._group._map.setView(this._latlng, mapZoom + 1); + } else { + this._group._map.fitBounds(this._bounds, fitBoundsOptions); + } + }, + + getBounds: function () { + var bounds = new L.LatLngBounds(); + bounds.extend(this._bounds); + return bounds; + }, + + _updateIcon: function () { + this._iconNeedsUpdate = true; + if (this._icon) { + this.setIcon(this); + } + }, + + //Cludge for Icon, we pretend to be an icon for performance + createIcon: function () { + if (this._iconNeedsUpdate) { + this._iconObj = this._group.options.iconCreateFunction(this); + this._iconNeedsUpdate = false; + } + return this._iconObj.createIcon(); + }, + createShadow: function () { + return this._iconObj.createShadow(); + }, + + + _addChild: function (new1, isNotificationFromChild) { + + this._iconNeedsUpdate = true; + + this._boundsNeedUpdate = true; + this._setClusterCenter(new1); + + if (new1 instanceof L.MarkerCluster) { + if (!isNotificationFromChild) { + this._childClusters.push(new1); + new1.__parent = this; + } + this._childCount += new1._childCount; + } else { + if (!isNotificationFromChild) { + this._markers.push(new1); + } + this._childCount++; + } + + if (this.__parent) { + this.__parent._addChild(new1, true); + } + }, + + /** + * Makes sure the cluster center is set. If not, uses the child center if it is a cluster, or the marker position. + * @param child L.MarkerCluster|L.Marker that will be used as cluster center if not defined yet. + * @private + */ + _setClusterCenter: function (child) { + if (!this._cLatLng) { + // when clustering, take position of the first point as the cluster center + this._cLatLng = child._cLatLng || child._latlng; + } + }, + + /** + * Assigns impossible bounding values so that the next extend entirely determines the new bounds. + * This method avoids having to trash the previous L.LatLngBounds object and to create a new one, which is much slower for this class. + * As long as the bounds are not extended, most other methods would probably fail, as they would with bounds initialized but not extended. + * @private + */ + _resetBounds: function () { + var bounds = this._bounds; + + if (bounds._southWest) { + bounds._southWest.lat = Infinity; + bounds._southWest.lng = Infinity; + } + if (bounds._northEast) { + bounds._northEast.lat = -Infinity; + bounds._northEast.lng = -Infinity; + } + }, + + _recalculateBounds: function () { + var markers = this._markers, + childClusters = this._childClusters, + latSum = 0, + lngSum = 0, + totalCount = this._childCount, + i, child, childLatLng, childCount; + + // Case where all markers are removed from the map and we are left with just an empty _topClusterLevel. + if (totalCount === 0) { + return; + } + + // Reset rather than creating a new object, for performance. + this._resetBounds(); + + // Child markers. + for (i = 0; i < markers.length; i++) { + childLatLng = markers[i]._latlng; + + this._bounds.extend(childLatLng); + + latSum += childLatLng.lat; + lngSum += childLatLng.lng; + } + + // Child clusters. + for (i = 0; i < childClusters.length; i++) { + child = childClusters[i]; + + // Re-compute child bounds and weighted position first if necessary. + if (child._boundsNeedUpdate) { + child._recalculateBounds(); + } + + this._bounds.extend(child._bounds); + + childLatLng = child._wLatLng; + childCount = child._childCount; + + latSum += childLatLng.lat * childCount; + lngSum += childLatLng.lng * childCount; + } + + this._latlng = this._wLatLng = new L.LatLng(latSum / totalCount, lngSum / totalCount); + + // Reset dirty flag. + this._boundsNeedUpdate = false; + }, + + //Set our markers position as given and add it to the map + _addToMap: function (startPos) { + if (startPos) { + this._backupLatlng = this._latlng; + this.setLatLng(startPos); + } + this._group._featureGroup.addLayer(this); + }, + + _recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) { + this._recursively(bounds, this._group._map.getMinZoom(), maxZoom - 1, + function (c) { + var markers = c._markers, + i, m; + for (i = markers.length - 1; i >= 0; i--) { + m = markers[i]; + + //Only do it if the icon is still on the map + if (m._icon) { + m._setPos(center); + m.clusterHide(); + } + } + }, + function (c) { + var childClusters = c._childClusters, + j, cm; + for (j = childClusters.length - 1; j >= 0; j--) { + cm = childClusters[j]; + if (cm._icon) { + cm._setPos(center); + cm.clusterHide(); + } + } + } + ); + }, + + _recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, mapMinZoom, previousZoomLevel, newZoomLevel) { + this._recursively(bounds, newZoomLevel, mapMinZoom, + function (c) { + c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel); + + //TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be. + //As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate + if (c._isSingleParent() && previousZoomLevel - 1 === newZoomLevel) { + c.clusterShow(); + c._recursivelyRemoveChildrenFromMap(bounds, mapMinZoom, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds + } else { + c.clusterHide(); + } + + c._addToMap(); + } + ); + }, + + _recursivelyBecomeVisible: function (bounds, zoomLevel) { + this._recursively(bounds, this._group._map.getMinZoom(), zoomLevel, null, function (c) { + c.clusterShow(); + }); + }, + + _recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) { + this._recursively(bounds, this._group._map.getMinZoom() - 1, zoomLevel, + function (c) { + if (zoomLevel === c._zoom) { + return; + } + + //Add our child markers at startPos (so they can be animated out) + for (var i = c._markers.length - 1; i >= 0; i--) { + var nm = c._markers[i]; + + if (!bounds.contains(nm._latlng)) { + continue; + } + + if (startPos) { + nm._backupLatlng = nm.getLatLng(); + + nm.setLatLng(startPos); + if (nm.clusterHide) { + nm.clusterHide(); + } + } + + c._group._featureGroup.addLayer(nm); + } + }, + function (c) { + c._addToMap(startPos); + } + ); + }, + + _recursivelyRestoreChildPositions: function (zoomLevel) { + //Fix positions of child markers + for (var i = this._markers.length - 1; i >= 0; i--) { + var nm = this._markers[i]; + if (nm._backupLatlng) { + nm.setLatLng(nm._backupLatlng); + delete nm._backupLatlng; + } + } + + if (zoomLevel - 1 === this._zoom) { + //Reposition child clusters + for (var j = this._childClusters.length - 1; j >= 0; j--) { + this._childClusters[j]._restorePosition(); + } + } else { + for (var k = this._childClusters.length - 1; k >= 0; k--) { + this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel); + } + } + }, + + _restorePosition: function () { + if (this._backupLatlng) { + this.setLatLng(this._backupLatlng); + delete this._backupLatlng; + } + }, + + //exceptBounds: If set, don't remove any markers/clusters in it + _recursivelyRemoveChildrenFromMap: function (previousBounds, mapMinZoom, zoomLevel, exceptBounds) { + var m, i; + this._recursively(previousBounds, mapMinZoom - 1, zoomLevel - 1, + function (c) { + //Remove markers at every level + for (i = c._markers.length - 1; i >= 0; i--) { + m = c._markers[i]; + if (!exceptBounds || !exceptBounds.contains(m._latlng)) { + c._group._featureGroup.removeLayer(m); + if (m.clusterShow) { + m.clusterShow(); + } + } + } + }, + function (c) { + //Remove child clusters at just the bottom level + for (i = c._childClusters.length - 1; i >= 0; i--) { + m = c._childClusters[i]; + if (!exceptBounds || !exceptBounds.contains(m._latlng)) { + c._group._featureGroup.removeLayer(m); + if (m.clusterShow) { + m.clusterShow(); + } + } + } + } + ); + }, + + //Run the given functions recursively to this and child clusters + // boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to + // zoomLevelToStart: zoom level to start running functions (inclusive) + // zoomLevelToStop: zoom level to stop running functions (inclusive) + // runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level + // runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level + _recursively: function (boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel) { + var childClusters = this._childClusters, + zoom = this._zoom, + i, c; + + if (zoomLevelToStart <= zoom) { + if (runAtEveryLevel) { + runAtEveryLevel(this); + } + if (runAtBottomLevel && zoom === zoomLevelToStop) { + runAtBottomLevel(this); + } + } + + if (zoom < zoomLevelToStart || zoom < zoomLevelToStop) { + for (i = childClusters.length - 1; i >= 0; i--) { + c = childClusters[i]; + if (boundsToApplyTo.intersects(c._bounds)) { + c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel); + } + } + } + }, + + //Returns true if we are the parent of only one cluster and that cluster is the same as us + _isSingleParent: function () { + //Don't need to check this._markers as the rest won't work if there are any + return this._childClusters.length > 0 && this._childClusters[0]._childCount === this._childCount; + } +}); + + + +/* +* Extends L.Marker to include two extra methods: clusterHide and clusterShow. +* +* They work as setOpacity(0) and setOpacity(1) respectively, but +* they will remember the marker's opacity when hiding and showing it again. +* +*/ + + +L.Marker.include({ + + clusterHide: function () { + this.options.opacityWhenUnclustered = this.options.opacity || 1; + return this.setOpacity(0); + }, + + clusterShow: function () { + var ret = this.setOpacity(this.options.opacity || this.options.opacityWhenUnclustered); + delete this.options.opacityWhenUnclustered; + return ret; + } + +}); + + + + + +L.DistanceGrid = function (cellSize) { + this._cellSize = cellSize; + this._sqCellSize = cellSize * cellSize; + this._grid = {}; + this._objectPoint = { }; +}; + +L.DistanceGrid.prototype = { + + addObject: function (obj, point) { + var x = this._getCoord(point.x), + y = this._getCoord(point.y), + grid = this._grid, + row = grid[y] = grid[y] || {}, + cell = row[x] = row[x] || [], + stamp = L.Util.stamp(obj); + + this._objectPoint[stamp] = point; + + cell.push(obj); + }, + + updateObject: function (obj, point) { + this.removeObject(obj); + this.addObject(obj, point); + }, + + //Returns true if the object was found + removeObject: function (obj, point) { + var x = this._getCoord(point.x), + y = this._getCoord(point.y), + grid = this._grid, + row = grid[y] = grid[y] || {}, + cell = row[x] = row[x] || [], + i, len; + + delete this._objectPoint[L.Util.stamp(obj)]; + + for (i = 0, len = cell.length; i < len; i++) { + if (cell[i] === obj) { + + cell.splice(i, 1); + + if (len === 1) { + delete row[x]; + } + + return true; + } + } + + }, + + eachObject: function (fn, context) { + var i, j, k, len, row, cell, removed, + grid = this._grid; + + for (i in grid) { + row = grid[i]; + + for (j in row) { + cell = row[j]; + + for (k = 0, len = cell.length; k < len; k++) { + removed = fn.call(context, cell[k]); + if (removed) { + k--; + len--; + } + } + } + } + }, + + getNearObject: function (point) { + var x = this._getCoord(point.x), + y = this._getCoord(point.y), + i, j, k, row, cell, len, obj, dist, + objectPoint = this._objectPoint, + closestDistSq = this._sqCellSize, + closest = null; + + for (i = y - 1; i <= y + 1; i++) { + row = this._grid[i]; + if (row) { + + for (j = x - 1; j <= x + 1; j++) { + cell = row[j]; + if (cell) { + + for (k = 0, len = cell.length; k < len; k++) { + obj = cell[k]; + dist = this._sqDist(objectPoint[L.Util.stamp(obj)], point); + if (dist < closestDistSq) { + closestDistSq = dist; + closest = obj; + } + } + } + } + } + } + return closest; + }, + + _getCoord: function (x) { + return Math.floor(x / this._cellSize); + }, + + _sqDist: function (p, p2) { + var dx = p2.x - p.x, + dy = p2.y - p.y; + return dx * dx + dy * dy; + } +}; + + +/* Copyright (c) 2012 the authors listed at the following URL, and/or +the authors of referenced articles or incorporated external code: +http://en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=20120410175256 + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=18434 +*/ + +(function () { + L.QuickHull = { + + /* + * @param {Object} cpt a point to be measured from the baseline + * @param {Array} bl the baseline, as represented by a two-element + * array of latlng objects. + * @returns {Number} an approximate distance measure + */ + getDistant: function (cpt, bl) { + var vY = bl[1].lat - bl[0].lat, + vX = bl[0].lng - bl[1].lng; + return (vX * (cpt.lat - bl[0].lat) + vY * (cpt.lng - bl[0].lng)); + }, + + /* + * @param {Array} baseLine a two-element array of latlng objects + * representing the baseline to project from + * @param {Array} latLngs an array of latlng objects + * @returns {Object} the maximum point and all new points to stay + * in consideration for the hull. + */ + findMostDistantPointFromBaseLine: function (baseLine, latLngs) { + var maxD = 0, + maxPt = null, + newPoints = [], + i, pt, d; + + for (i = latLngs.length - 1; i >= 0; i--) { + pt = latLngs[i]; + d = this.getDistant(pt, baseLine); + + if (d > 0) { + newPoints.push(pt); + } else { + continue; + } + + if (d > maxD) { + maxD = d; + maxPt = pt; + } + } + + return { maxPoint: maxPt, newPoints: newPoints }; + }, + + + /* + * Given a baseline, compute the convex hull of latLngs as an array + * of latLngs. + * + * @param {Array} latLngs + * @returns {Array} + */ + buildConvexHull: function (baseLine, latLngs) { + var convexHullBaseLines = [], + t = this.findMostDistantPointFromBaseLine(baseLine, latLngs); + + if (t.maxPoint) { // if there is still a point "outside" the base line + convexHullBaseLines = + convexHullBaseLines.concat( + this.buildConvexHull([baseLine[0], t.maxPoint], t.newPoints) + ); + convexHullBaseLines = + convexHullBaseLines.concat( + this.buildConvexHull([t.maxPoint, baseLine[1]], t.newPoints) + ); + return convexHullBaseLines; + } else { // if there is no more point "outside" the base line, the current base line is part of the convex hull + return [baseLine[0]]; + } + }, + + /* + * Given an array of latlngs, compute a convex hull as an array + * of latlngs + * + * @param {Array} latLngs + * @returns {Array} + */ + getConvexHull: function (latLngs) { + // find first baseline + var maxLat = false, minLat = false, + maxLng = false, minLng = false, + maxLatPt = null, minLatPt = null, + maxLngPt = null, minLngPt = null, + maxPt = null, minPt = null, + i; + + for (i = latLngs.length - 1; i >= 0; i--) { + var pt = latLngs[i]; + if (maxLat === false || pt.lat > maxLat) { + maxLatPt = pt; + maxLat = pt.lat; + } + if (minLat === false || pt.lat < minLat) { + minLatPt = pt; + minLat = pt.lat; + } + if (maxLng === false || pt.lng > maxLng) { + maxLngPt = pt; + maxLng = pt.lng; + } + if (minLng === false || pt.lng < minLng) { + minLngPt = pt; + minLng = pt.lng; + } + } + + if (minLat !== maxLat) { + minPt = minLatPt; + maxPt = maxLatPt; + } else { + minPt = minLngPt; + maxPt = maxLngPt; + } + + var ch = [].concat(this.buildConvexHull([minPt, maxPt], latLngs), + this.buildConvexHull([maxPt, minPt], latLngs)); + return ch; + } + }; +}()); + +L.MarkerCluster.include({ + getConvexHull: function () { + var childMarkers = this.getAllChildMarkers(), + points = [], + p, i; + + for (i = childMarkers.length - 1; i >= 0; i--) { + p = childMarkers[i].getLatLng(); + points.push(p); + } + + return L.QuickHull.getConvexHull(points); + } +}); + + +//This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet +//Huge thanks to jawj for implementing it first to make my job easy :-) + +L.MarkerCluster.include({ + + _2PI: Math.PI * 2, + _circleFootSeparation: 25, //related to circumference of circle + _circleStartAngle: Math.PI / 6, + + _spiralFootSeparation: 28, //related to size of spiral (experiment!) + _spiralLengthStart: 11, + _spiralLengthFactor: 5, + + _circleSpiralSwitchover: 9, //show spiral instead of circle from this marker count upwards. + // 0 -> always spiral; Infinity -> always circle + + spiderfy: function () { + if (this._group._spiderfied === this || this._group._inZoomAnimation) { + return; + } + + var childMarkers = this.getAllChildMarkers(), + group = this._group, + map = group._map, + center = map.latLngToLayerPoint(this._latlng), + positions; + + this._group._unspiderfy(); + this._group._spiderfied = this; + + //TODO Maybe: childMarkers order by distance to center + + if (childMarkers.length >= this._circleSpiralSwitchover) { + positions = this._generatePointsSpiral(childMarkers.length, center); + } else { + center.y += 10; // Otherwise circles look wrong => hack for standard blue icon, renders differently for other icons. + positions = this._generatePointsCircle(childMarkers.length, center); + } + + this._animationSpiderfy(childMarkers, positions); + }, + + unspiderfy: function (zoomDetails) { + /// Argument from zoomanim if being called in a zoom animation or null otherwise + if (this._group._inZoomAnimation) { + return; + } + this._animationUnspiderfy(zoomDetails); + + this._group._spiderfied = null; + }, + + _generatePointsCircle: function (count, centerPt) { + var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count), + legLength = circumference / this._2PI, //radius from circumference + angleStep = this._2PI / count, + res = [], + i, angle; + + res.length = count; + + for (i = count - 1; i >= 0; i--) { + angle = this._circleStartAngle + i * angleStep; + res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); + } + + return res; + }, + + _generatePointsSpiral: function (count, centerPt) { + var spiderfyDistanceMultiplier = this._group.options.spiderfyDistanceMultiplier, + legLength = spiderfyDistanceMultiplier * this._spiralLengthStart, + separation = spiderfyDistanceMultiplier * this._spiralFootSeparation, + lengthFactor = spiderfyDistanceMultiplier * this._spiralLengthFactor * this._2PI, + angle = 0, + res = [], + i; + + res.length = count; + + // Higher index, closer position to cluster center. + for (i = count - 1; i >= 0; i--) { + angle += separation / legLength + i * 0.0005; + res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); + legLength += lengthFactor / angle; + } + return res; + }, + + _noanimationUnspiderfy: function () { + var group = this._group, + map = group._map, + fg = group._featureGroup, + childMarkers = this.getAllChildMarkers(), + m, i; + + group._ignoreMove = true; + + this.setOpacity(1); + for (i = childMarkers.length - 1; i >= 0; i--) { + m = childMarkers[i]; + + fg.removeLayer(m); + + if (m._preSpiderfyLatlng) { + m.setLatLng(m._preSpiderfyLatlng); + delete m._preSpiderfyLatlng; + } + if (m.setZIndexOffset) { + m.setZIndexOffset(0); + } + + if (m._spiderLeg) { + map.removeLayer(m._spiderLeg); + delete m._spiderLeg; + } + } + + group.fire('unspiderfied', { + cluster: this, + markers: childMarkers + }); + group._ignoreMove = false; + group._spiderfied = null; + } +}); + +//Non Animated versions of everything +L.MarkerClusterNonAnimated = L.MarkerCluster.extend({ + _animationSpiderfy: function (childMarkers, positions) { + var group = this._group, + map = group._map, + fg = group._featureGroup, + legOptions = this._group.options.spiderLegPolylineOptions, + i, m, leg, newPos; + + group._ignoreMove = true; + + // Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition. + // The reverse order trick no longer improves performance on modern browsers. + for (i = 0; i < childMarkers.length; i++) { + newPos = map.layerPointToLatLng(positions[i]); + m = childMarkers[i]; + + // Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it. + leg = new L.Polyline([this._latlng, newPos], legOptions); + map.addLayer(leg); + m._spiderLeg = leg; + + // Now add the marker. + m._preSpiderfyLatlng = m._latlng; + m.setLatLng(newPos); + if (m.setZIndexOffset) { + m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING + } + + fg.addLayer(m); + } + this.setOpacity(0.3); + + group._ignoreMove = false; + group.fire('spiderfied', { + cluster: this, + markers: childMarkers + }); + }, + + _animationUnspiderfy: function () { + this._noanimationUnspiderfy(); + } +}); + +//Animated versions here +L.MarkerCluster.include({ + + _animationSpiderfy: function (childMarkers, positions) { + var me = this, + group = this._group, + map = group._map, + fg = group._featureGroup, + thisLayerLatLng = this._latlng, + thisLayerPos = map.latLngToLayerPoint(thisLayerLatLng), + svg = L.Path.SVG, + legOptions = L.extend({}, this._group.options.spiderLegPolylineOptions), // Copy the options so that we can modify them for animation. + finalLegOpacity = legOptions.opacity, + i, m, leg, legPath, legLength, newPos; + + if (finalLegOpacity === undefined) { + finalLegOpacity = L.MarkerClusterGroup.prototype.options.spiderLegPolylineOptions.opacity; + } + + if (svg) { + // If the initial opacity of the spider leg is not 0 then it appears before the animation starts. + legOptions.opacity = 0; + + // Add the class for CSS transitions. + legOptions.className = (legOptions.className || '') + ' leaflet-cluster-spider-leg'; + } else { + // Make sure we have a defined opacity. + legOptions.opacity = finalLegOpacity; + } + + group._ignoreMove = true; + + // Add markers and spider legs to map, hidden at our center point. + // Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition. + // The reverse order trick no longer improves performance on modern browsers. + for (i = 0; i < childMarkers.length; i++) { + m = childMarkers[i]; + + newPos = map.layerPointToLatLng(positions[i]); + + // Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it. + leg = new L.Polyline([thisLayerLatLng, newPos], legOptions); + map.addLayer(leg); + m._spiderLeg = leg; + + // Explanations: https://jakearchibald.com/2013/animated-line-drawing-svg/ + // In our case the transition property is declared in the CSS file. + if (svg) { + legPath = leg._path; + legLength = legPath.getTotalLength() + 0.1; // Need a small extra length to avoid remaining dot in Firefox. + legPath.style.strokeDasharray = legLength; // Just 1 length is enough, it will be duplicated. + legPath.style.strokeDashoffset = legLength; + } + + // If it is a marker, add it now and we'll animate it out + if (m.setZIndexOffset) { + m.setZIndexOffset(1000000); // Make normal markers appear on top of EVERYTHING + } + if (m.clusterHide) { + m.clusterHide(); + } + + // Vectors just get immediately added + fg.addLayer(m); + + if (m._setPos) { + m._setPos(thisLayerPos); + } + } + + group._forceLayout(); + group._animationStart(); + + // Reveal markers and spider legs. + for (i = childMarkers.length - 1; i >= 0; i--) { + newPos = map.layerPointToLatLng(positions[i]); + m = childMarkers[i]; + + //Move marker to new position + m._preSpiderfyLatlng = m._latlng; + m.setLatLng(newPos); + + if (m.clusterShow) { + m.clusterShow(); + } + + // Animate leg (animation is actually delegated to CSS transition). + if (svg) { + leg = m._spiderLeg; + legPath = leg._path; + legPath.style.strokeDashoffset = 0; + //legPath.style.strokeOpacity = finalLegOpacity; + leg.setStyle({opacity: finalLegOpacity}); + } + } + this.setOpacity(0.3); + + group._ignoreMove = false; + + setTimeout(function () { + group._animationEnd(); + group.fire('spiderfied', { + cluster: me, + markers: childMarkers + }); + }, 200); + }, + + _animationUnspiderfy: function (zoomDetails) { + var me = this, + group = this._group, + map = group._map, + fg = group._featureGroup, + thisLayerPos = zoomDetails ? map._latLngToNewLayerPoint(this._latlng, zoomDetails.zoom, zoomDetails.center) : map.latLngToLayerPoint(this._latlng), + childMarkers = this.getAllChildMarkers(), + svg = L.Path.SVG, + m, i, leg, legPath, legLength, nonAnimatable; + + group._ignoreMove = true; + group._animationStart(); + + //Make us visible and bring the child markers back in + this.setOpacity(1); + for (i = childMarkers.length - 1; i >= 0; i--) { + m = childMarkers[i]; + + //Marker was added to us after we were spiderfied + if (!m._preSpiderfyLatlng) { + continue; + } + + //Close any popup on the marker first, otherwise setting the location of the marker will make the map scroll + m.closePopup(); + + //Fix up the location to the real one + m.setLatLng(m._preSpiderfyLatlng); + delete m._preSpiderfyLatlng; + + //Hack override the location to be our center + nonAnimatable = true; + if (m._setPos) { + m._setPos(thisLayerPos); + nonAnimatable = false; + } + if (m.clusterHide) { + m.clusterHide(); + nonAnimatable = false; + } + if (nonAnimatable) { + fg.removeLayer(m); + } + + // Animate the spider leg back in (animation is actually delegated to CSS transition). + if (svg) { + leg = m._spiderLeg; + legPath = leg._path; + legLength = legPath.getTotalLength() + 0.1; + legPath.style.strokeDashoffset = legLength; + leg.setStyle({opacity: 0}); + } + } + + group._ignoreMove = false; + + setTimeout(function () { + //If we have only <= one child left then that marker will be shown on the map so don't remove it! + var stillThereChildCount = 0; + for (i = childMarkers.length - 1; i >= 0; i--) { + m = childMarkers[i]; + if (m._spiderLeg) { + stillThereChildCount++; + } + } + + + for (i = childMarkers.length - 1; i >= 0; i--) { + m = childMarkers[i]; + + if (!m._spiderLeg) { //Has already been unspiderfied + continue; + } + + if (m.clusterShow) { + m.clusterShow(); + } + if (m.setZIndexOffset) { + m.setZIndexOffset(0); + } + + if (stillThereChildCount > 1) { + fg.removeLayer(m); + } + + map.removeLayer(m._spiderLeg); + delete m._spiderLeg; + } + group._animationEnd(); + group.fire('unspiderfied', { + cluster: me, + markers: childMarkers + }); + }, 200); + } +}); + + +L.MarkerClusterGroup.include({ + //The MarkerCluster currently spiderfied (if any) + _spiderfied: null, + + unspiderfy: function () { + this._unspiderfy.apply(this, arguments); + }, + + _spiderfierOnAdd: function () { + this._map.on('click', this._unspiderfyWrapper, this); + + if (this._map.options.zoomAnimation) { + this._map.on('zoomstart', this._unspiderfyZoomStart, this); + } + //Browsers without zoomAnimation or a big zoom don't fire zoomstart + this._map.on('zoomend', this._noanimationUnspiderfy, this); + + if (!L.Browser.touch) { + this._map.getRenderer(this); + //Needs to happen in the pageload, not after, or animations don't work in webkit + // http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements + //Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable + } + }, + + _spiderfierOnRemove: function () { + this._map.off('click', this._unspiderfyWrapper, this); + this._map.off('zoomstart', this._unspiderfyZoomStart, this); + this._map.off('zoomanim', this._unspiderfyZoomAnim, this); + this._map.off('zoomend', this._noanimationUnspiderfy, this); + + //Ensure that markers are back where they should be + // Use no animation to avoid a sticky leaflet-cluster-anim class on mapPane + this._noanimationUnspiderfy(); + }, + + //On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated) + //This means we can define the animation they do rather than Markers doing an animation to their actual location + _unspiderfyZoomStart: function () { + if (!this._map) { //May have been removed from the map by a zoomEnd handler + return; + } + + this._map.on('zoomanim', this._unspiderfyZoomAnim, this); + }, + + _unspiderfyZoomAnim: function (zoomDetails) { + //Wait until the first zoomanim after the user has finished touch-zooming before running the animation + if (L.DomUtil.hasClass(this._map._mapPane, 'leaflet-touching')) { + return; + } + + this._map.off('zoomanim', this._unspiderfyZoomAnim, this); + this._unspiderfy(zoomDetails); + }, + + _unspiderfyWrapper: function () { + /// _unspiderfy but passes no arguments + this._unspiderfy(); + }, + + _unspiderfy: function (zoomDetails) { + if (this._spiderfied) { + this._spiderfied.unspiderfy(zoomDetails); + } + }, + + _noanimationUnspiderfy: function () { + if (this._spiderfied) { + this._spiderfied._noanimationUnspiderfy(); + } + }, + + //If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc + _unspiderfyLayer: function (layer) { + if (layer._spiderLeg) { + this._featureGroup.removeLayer(layer); + + if (layer.clusterShow) { + layer.clusterShow(); + } + //Position will be fixed up immediately in _animationUnspiderfy + if (layer.setZIndexOffset) { + layer.setZIndexOffset(0); + } + + this._map.removeLayer(layer._spiderLeg); + delete layer._spiderLeg; + } + } +}); + + +/** + * Adds 1 public method to MCG and 1 to L.Marker to facilitate changing + * markers' icon options and refreshing their icon and their parent clusters + * accordingly (case where their iconCreateFunction uses data of childMarkers + * to make up the cluster icon). + */ + + +L.MarkerClusterGroup.include({ + /** + * Updates the icon of all clusters which are parents of the given marker(s). + * In singleMarkerMode, also updates the given marker(s) icon. + * @param layers L.MarkerClusterGroup|L.LayerGroup|Array(L.Marker)|Map(L.Marker)| + * L.MarkerCluster|L.Marker (optional) list of markers (or single marker) whose parent + * clusters need to be updated. If not provided, retrieves all child markers of this. + * @returns {L.MarkerClusterGroup} + */ + refreshClusters: function (layers) { + if (!layers) { + layers = this._topClusterLevel.getAllChildMarkers(); + } else if (layers instanceof L.MarkerClusterGroup) { + layers = layers._topClusterLevel.getAllChildMarkers(); + } else if (layers instanceof L.LayerGroup) { + layers = layers._layers; + } else if (layers instanceof L.MarkerCluster) { + layers = layers.getAllChildMarkers(); + } else if (layers instanceof L.Marker) { + layers = [layers]; + } // else: must be an Array(L.Marker)|Map(L.Marker) + this._flagParentsIconsNeedUpdate(layers); + this._refreshClustersIcons(); + + // In case of singleMarkerMode, also re-draw the markers. + if (this.options.singleMarkerMode) { + this._refreshSingleMarkerModeMarkers(layers); + } + + return this; + }, + + /** + * Simply flags all parent clusters of the given markers as having a "dirty" icon. + * @param layers Array(L.Marker)|Map(L.Marker) list of markers. + * @private + */ + _flagParentsIconsNeedUpdate: function (layers) { + var id, parent; + + // Assumes layers is an Array or an Object whose prototype is non-enumerable. + for (id in layers) { + // Flag parent clusters' icon as "dirty", all the way up. + // Dumb process that flags multiple times upper parents, but still + // much more efficient than trying to be smart and make short lists, + // at least in the case of a hierarchy following a power law: + // http://jsperf.com/flag-nodes-in-power-hierarchy/2 + parent = layers[id].__parent; + while (parent) { + parent._iconNeedsUpdate = true; + parent = parent.__parent; + } + } + }, + + /** + * Re-draws the icon of the supplied markers. + * To be used in singleMarkerMode only. + * @param layers Array(L.Marker)|Map(L.Marker) list of markers. + * @private + */ + _refreshSingleMarkerModeMarkers: function (layers) { + var id, layer; + + for (id in layers) { + layer = layers[id]; + + // Make sure we do not override markers that do not belong to THIS group. + if (this.hasLayer(layer)) { + // Need to re-create the icon first, then re-draw the marker. + layer.setIcon(this._overrideMarkerIcon(layer)); + } + } + } +}); + +L.Marker.include({ + /** + * Updates the given options in the marker's icon and refreshes the marker. + * @param options map object of icon options. + * @param directlyRefreshClusters boolean (optional) true to trigger + * MCG.refreshClustersOf() right away with this single marker. + * @returns {L.Marker} + */ + refreshIconOptions: function (options, directlyRefreshClusters) { + var icon = this.options.icon; + + L.setOptions(icon, options); + + this.setIcon(icon); + + // Shortcut to refresh the associated MCG clusters right away. + // To be used when refreshing a single marker. + // Otherwise, better use MCG.refreshClusters() once at the end with + // the list of modified markers. + if (directlyRefreshClusters && this.__parent) { + this.__parent._group.refreshClusters(this); + } + + return this; + } +}); + + +}(window, document)); \ No newline at end of file diff --git a/simplemonitor/html/web_development/marker-aggregation-up.png b/simplemonitor/html/web_development/marker-aggregation-up.png new file mode 100644 index 0000000000000000000000000000000000000000..a5318be26365606c97b3e5a4f28ba6a14a35d327 GIT binary patch literal 19804 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 cw10s*ikL-;1;x<^TWy literal 0 HcmV?d00001 diff --git a/simplemonitor/html/web_development/marker-shadow.png b/simplemonitor/html/web_development/marker-shadow.png new file mode 100644 index 0000000000000000000000000000000000000000..d1e773c715a9b508ebea055c4bb4b0a2ad7f6e52 GIT binary patch literal 797 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 VTZv)2T+7Hd`Uqp~BDmAh{{alvRU7~S literal 0 HcmV?d00001 diff --git a/simplemonitor/html/web_development/marker-single-up.png b/simplemonitor/html/web_development/marker-single-up.png new file mode 100644 index 0000000000000000000000000000000000000000..df69a9ee3af558f2fb10e15fbd7f9ed28354be98 GIT binary patch literal 2244 zcmbVOX;c&E8ct+UqV(26@e;2vMnu+x2_zwr0D%Mv5Y|Wnf*6v4L`)_o0Roj(C@?#74QAw>5n^SmhXGN_j%s^oY@i_xXKuV z#h_3qW413d6#1GXkL^4|S5YO< zaUY!GbAkbHSOEbfytAXw2?PN$84tRU$Yjs~AUc6Wf)jF+9YGfwiA*C>fR7grv8E8k z(L$L%A8jE!I!>%o$!P>aa&j_0*%=Qj;t3#?O4V@?iH?YdqcTON5~v+z%4M?*Oh_qI zNaQLBECY0mf>=0FMaLnYeh5J-=Wsp|%ak7zg`|w27RU)8-iaWU>f(B@tyG0V|8?WD z+DdMU93q55N;pv=MCuW@Y!-~LW<)_w1HlKzEk|7hLKt`ju>iks{N`H#YWN_8L*YMVeqgI>V8Uu^1 zuKgBAGq++$OE2eP{Dh`cL5$OS{Dla(=F z`;G_47uN1xWqfN>uY1hdkjyDX4==zMFD{}-wU{q`z4G?c3$ze3{nHtJJzbU@lRY6} zZv5?yiLREtGhx{Wdl|f;7Hbb=yj4=#vX&I92Ul);e~kb)Ss`d znrxzqpBv?zm|G{(#+jAA!0vb3%UOJP{5JMxm4dU`HD}|zZ=Rg9N2P~hYpYZ}J*2Oq zAD%R+TWW(A-r+A6PiKBxAIUc7yffVR?<3QVw^rcLqTU4;)Rfa|qn`RLsQ)Ylx}VNqNmkA@01}h-yw6=P=HUnEzP zw;24^;^X6c$6rz%CT}Pc!>ud)19F$#t9aI`=i71pTkB)u^<{_66(#6f(xE#qkIV&v z7wKQm!8Q+-XJw4F3nr7IddmjHi%j(Pu}9R`TAhZ(eT`Q{UlpJ)&oBP*5mwaDb>TYR zAlJBYf8~+30!4$XrA?>R2#!)edNMI-VRi&{s7qT?X6HmK&o|XWq=(mBYPbZNXMUBB7Hn@qY!6T8WUDX3lPXpb#pxZ!}K4g zq0fOclufTXmj03O*S&(%r#o-iTO+A9!+-UTfG9SWq-Vne zceRIRG@<%teuGSQjG59;5TiF+jXBy6Y?;HroL|QGSPrK~5ZKv%HNmy9+uLt;7kCm^`Y>j=X^#7mh~*CoD>MVK3w_sWR^)!?v%1|U1vtARYcRd1Bxt(q?oV1)Kuy*@ z(%|D*LB!ICna&ehE_?f?Yp?UCw}(H%*m53hzL(GIh@w?k+BPwGLHkmr4!IAiZr4Vq z_(;qu-7DHF9DENf&tu#76=6mqOCxRg80?7!-gZmPfu7(K%g%fEpRqp8ue*0Y zku=;~lsJ5F?bhF|?DA!@L+|JUXPO^N}FMv(E1E{+umO6)9$0t z;n6*yndS4s#GOOk_UMIkG>v|F_b*Jeyz`rG^t)= currentZoom) { + visibleLayer = visibleLayer.__parent; + } + } + + if (this._currentShownBounds.contains(visibleLayer.getLatLng())) { + if (this.options.animateAddingMarkers) { + this._animationAddLayer(layer, visibleLayer); + } else { + this._animationAddLayerNonAnimated(layer, visibleLayer); + } + } + return this; + }, + + removeLayer: function (layer) { + + if (layer instanceof L.LayerGroup) { + return this.removeLayers([layer]); + } + + //Non point layers + if (!layer.getLatLng) { + this._nonPointGroup.removeLayer(layer); + this.fire('layerremove', { layer: layer }); + return this; + } + + if (!this._map) { + if (!this._arraySplice(this._needsClustering, layer) && this.hasLayer(layer)) { + this._needsRemoving.push({ layer: layer, latlng: layer._latlng }); + } + this.fire('layerremove', { layer: layer }); + return this; + } + + if (!layer.__parent) { + return this; + } + + if (this._unspiderfy) { + this._unspiderfy(); + this._unspiderfyLayer(layer); + } + + //Remove the marker from clusters + this._removeLayer(layer, true); + this.fire('layerremove', { layer: layer }); + + // Refresh bounds and weighted positions. + this._topClusterLevel._recalculateBounds(); + + this._refreshClustersIcons(); + + layer.off(this._childMarkerEventHandlers, this); + + if (this._featureGroup.hasLayer(layer)) { + this._featureGroup.removeLayer(layer); + if (layer.clusterShow) { + layer.clusterShow(); + } + } + + return this; + }, + + //Takes an array of markers and adds them in bulk + addLayers: function (layersArray, skipLayerAddEvent) { + if (!L.Util.isArray(layersArray)) { + return this.addLayer(layersArray); + } + + var fg = this._featureGroup, + npg = this._nonPointGroup, + chunked = this.options.chunkedLoading, + chunkInterval = this.options.chunkInterval, + chunkProgress = this.options.chunkProgress, + l = layersArray.length, + offset = 0, + originalArray = true, + m; + + if (this._map) { + var started = (new Date()).getTime(); + var process = L.bind(function () { + var start = (new Date()).getTime(); + for (; offset < l; offset++) { + if (chunked && offset % 200 === 0) { + // every couple hundred markers, instrument the time elapsed since processing started: + var elapsed = (new Date()).getTime() - start; + if (elapsed > chunkInterval) { + break; // been working too hard, time to take a break :-) + } + } + + m = layersArray[offset]; + + // Group of layers, append children to layersArray and skip. + // Side effects: + // - Total increases, so chunkProgress ratio jumps backward. + // - Groups are not included in this group, only their non-group child layers (hasLayer). + // Changing array length while looping does not affect performance in current browsers: + // http://jsperf.com/for-loop-changing-length/6 + if (m instanceof L.LayerGroup) { + if (originalArray) { + layersArray = layersArray.slice(); + originalArray = false; + } + this._extractNonGroupLayers(m, layersArray); + l = layersArray.length; + continue; + } + + //Not point data, can't be clustered + if (!m.getLatLng) { + npg.addLayer(m); + if (!skipLayerAddEvent) { + this.fire('layeradd', { layer: m }); + } + continue; + } + + if (this.hasLayer(m)) { + continue; + } + + this._addLayer(m, this._maxZoom); + if (!skipLayerAddEvent) { + this.fire('layeradd', { layer: m }); + } + + //If we just made a cluster of size 2 then we need to remove the other marker from the map (if it is) or we never will + if (m.__parent) { + if (m.__parent.getChildCount() === 2) { + var markers = m.__parent.getAllChildMarkers(), + otherMarker = markers[0] === m ? markers[1] : markers[0]; + fg.removeLayer(otherMarker); + } + } + } + + if (chunkProgress) { + // report progress and time elapsed: + chunkProgress(offset, l, (new Date()).getTime() - started); + } + + // Completed processing all markers. + if (offset === l) { + + // Refresh bounds and weighted positions. + this._topClusterLevel._recalculateBounds(); + + this._refreshClustersIcons(); + + this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); + } else { + setTimeout(process, this.options.chunkDelay); + } + }, this); + + process(); + } else { + var needsClustering = this._needsClustering; + + for (; offset < l; offset++) { + m = layersArray[offset]; + + // Group of layers, append children to layersArray and skip. + if (m instanceof L.LayerGroup) { + if (originalArray) { + layersArray = layersArray.slice(); + originalArray = false; + } + this._extractNonGroupLayers(m, layersArray); + l = layersArray.length; + continue; + } + + //Not point data, can't be clustered + if (!m.getLatLng) { + npg.addLayer(m); + continue; + } + + if (this.hasLayer(m)) { + continue; + } + + needsClustering.push(m); + } + } + return this; + }, + + //Takes an array of markers and removes them in bulk + removeLayers: function (layersArray) { + var i, m, + l = layersArray.length, + fg = this._featureGroup, + npg = this._nonPointGroup, + originalArray = true; + + if (!this._map) { + for (i = 0; i < l; i++) { + m = layersArray[i]; + + // Group of layers, append children to layersArray and skip. + if (m instanceof L.LayerGroup) { + if (originalArray) { + layersArray = layersArray.slice(); + originalArray = false; + } + this._extractNonGroupLayers(m, layersArray); + l = layersArray.length; + continue; + } + + this._arraySplice(this._needsClustering, m); + npg.removeLayer(m); + if (this.hasLayer(m)) { + this._needsRemoving.push({ layer: m, latlng: m._latlng }); + } + this.fire('layerremove', { layer: m }); + } + return this; + } + + if (this._unspiderfy) { + this._unspiderfy(); + + // Work on a copy of the array, so that next loop is not affected. + var layersArray2 = layersArray.slice(), + l2 = l; + for (i = 0; i < l2; i++) { + m = layersArray2[i]; + + // Group of layers, append children to layersArray and skip. + if (m instanceof L.LayerGroup) { + this._extractNonGroupLayers(m, layersArray2); + l2 = layersArray2.length; + continue; + } + + this._unspiderfyLayer(m); + } + } + + for (i = 0; i < l; i++) { + m = layersArray[i]; + + // Group of layers, append children to layersArray and skip. + if (m instanceof L.LayerGroup) { + if (originalArray) { + layersArray = layersArray.slice(); + originalArray = false; + } + this._extractNonGroupLayers(m, layersArray); + l = layersArray.length; + continue; + } + + if (!m.__parent) { + npg.removeLayer(m); + this.fire('layerremove', { layer: m }); + continue; + } + + this._removeLayer(m, true, true); + this.fire('layerremove', { layer: m }); + + if (fg.hasLayer(m)) { + fg.removeLayer(m); + if (m.clusterShow) { + m.clusterShow(); + } + } + } + + // Refresh bounds and weighted positions. + this._topClusterLevel._recalculateBounds(); + + this._refreshClustersIcons(); + + //Fix up the clusters and markers on the map + this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); + + return this; + }, + + //Removes all layers from the MarkerClusterGroup + clearLayers: function () { + //Need our own special implementation as the LayerGroup one doesn't work for us + + //If we aren't on the map (yet), blow away the markers we know of + if (!this._map) { + this._needsClustering = []; + delete this._gridClusters; + delete this._gridUnclustered; + } + + if (this._noanimationUnspiderfy) { + this._noanimationUnspiderfy(); + } + + //Remove all the visible layers + this._featureGroup.clearLayers(); + this._nonPointGroup.clearLayers(); + + this.eachLayer(function (marker) { + marker.off(this._childMarkerEventHandlers, this); + delete marker.__parent; + }, this); + + if (this._map) { + //Reset _topClusterLevel and the DistanceGrids + this._generateInitialClusters(); + } + + return this; + }, + + //Override FeatureGroup.getBounds as it doesn't work + getBounds: function () { + var bounds = new L.LatLngBounds(); + + if (this._topClusterLevel) { + bounds.extend(this._topClusterLevel._bounds); + } + + for (var i = this._needsClustering.length - 1; i >= 0; i--) { + bounds.extend(this._needsClustering[i].getLatLng()); + } + + bounds.extend(this._nonPointGroup.getBounds()); + + return bounds; + }, + + //Overrides LayerGroup.eachLayer + eachLayer: function (method, context) { + var markers = this._needsClustering.slice(), + needsRemoving = this._needsRemoving, + thisNeedsRemoving, i, j; + + if (this._topClusterLevel) { + this._topClusterLevel.getAllChildMarkers(markers); + } + + for (i = markers.length - 1; i >= 0; i--) { + thisNeedsRemoving = true; + + for (j = needsRemoving.length - 1; j >= 0; j--) { + if (needsRemoving[j].layer === markers[i]) { + thisNeedsRemoving = false; + break; + } + } + + if (thisNeedsRemoving) { + method.call(context, markers[i]); + } + } + + this._nonPointGroup.eachLayer(method, context); + }, + + //Overrides LayerGroup.getLayers + getLayers: function () { + var layers = []; + this.eachLayer(function (l) { + layers.push(l); + }); + return layers; + }, + + //Overrides LayerGroup.getLayer, WARNING: Really bad performance + getLayer: function (id) { + var result = null; + + id = parseInt(id, 10); + + this.eachLayer(function (l) { + if (L.stamp(l) === id) { + result = l; + } + }); + + return result; + }, + + //Returns true if the given layer is in this MarkerClusterGroup + hasLayer: function (layer) { + if (!layer) { + return false; + } + + var i, anArray = this._needsClustering; + + for (i = anArray.length - 1; i >= 0; i--) { + if (anArray[i] === layer) { + return true; + } + } + + anArray = this._needsRemoving; + for (i = anArray.length - 1; i >= 0; i--) { + if (anArray[i].layer === layer) { + return false; + } + } + + return !!(layer.__parent && layer.__parent._group === this) || this._nonPointGroup.hasLayer(layer); + }, + + //Zoom down to show the given layer (spiderfying if necessary) then calls the callback + zoomToShowLayer: function (layer, callback) { + + if (typeof callback !== 'function') { + callback = function () {}; + } + + var showMarker = function () { + if ((layer._icon || layer.__parent._icon) && !this._inZoomAnimation) { + this._map.off('moveend', showMarker, this); + this.off('animationend', showMarker, this); + + if (layer._icon) { + callback(); + } else if (layer.__parent._icon) { + this.once('spiderfied', callback, this); + layer.__parent.spiderfy(); + } + } + }; + + if (layer._icon && this._map.getBounds().contains(layer.getLatLng())) { + //Layer is visible ond on screen, immediate return + callback(); + } else if (layer.__parent._zoom < Math.round(this._map._zoom)) { + //Layer should be visible at this zoom level. It must not be on screen so just pan over to it + this._map.on('moveend', showMarker, this); + this._map.panTo(layer.getLatLng()); + } else { + this._map.on('moveend', showMarker, this); + this.on('animationend', showMarker, this); + layer.__parent.zoomToBounds(); + } + }, + + //Overrides FeatureGroup.onAdd + onAdd: function (map) { + this._map = map; + var i, l, layer; + + if (!isFinite(this._map.getMaxZoom())) { + throw "Map has no maxZoom specified"; + } + + this._featureGroup.addTo(map); + this._nonPointGroup.addTo(map); + + if (!this._gridClusters) { + this._generateInitialClusters(); + } + + this._maxLat = map.options.crs.projection.MAX_LATITUDE; + + //Restore all the positions as they are in the MCG before removing them + for (i = 0, l = this._needsRemoving.length; i < l; i++) { + layer = this._needsRemoving[i]; + layer.newlatlng = layer.layer._latlng; + layer.layer._latlng = layer.latlng; + } + //Remove them, then restore their new positions + for (i = 0, l = this._needsRemoving.length; i < l; i++) { + layer = this._needsRemoving[i]; + this._removeLayer(layer.layer, true); + layer.layer._latlng = layer.newlatlng; + } + this._needsRemoving = []; + + //Remember the current zoom level and bounds + this._zoom = Math.round(this._map._zoom); + this._currentShownBounds = this._getExpandedVisibleBounds(); + + this._map.on('zoomend', this._zoomEnd, this); + this._map.on('moveend', this._moveEnd, this); + + if (this._spiderfierOnAdd) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely + this._spiderfierOnAdd(); + } + + this._bindEvents(); + + //Actually add our markers to the map: + l = this._needsClustering; + this._needsClustering = []; + this.addLayers(l, true); + }, + + //Overrides FeatureGroup.onRemove + onRemove: function (map) { + map.off('zoomend', this._zoomEnd, this); + map.off('moveend', this._moveEnd, this); + + this._unbindEvents(); + + //In case we are in a cluster animation + this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', ''); + + if (this._spiderfierOnRemove) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely + this._spiderfierOnRemove(); + } + + delete this._maxLat; + + //Clean up all the layers we added to the map + this._hideCoverage(); + this._featureGroup.remove(); + this._nonPointGroup.remove(); + + this._featureGroup.clearLayers(); + + this._map = null; + }, + + getVisibleParent: function (marker) { + var vMarker = marker; + while (vMarker && !vMarker._icon) { + vMarker = vMarker.__parent; + } + return vMarker || null; + }, + + //Remove the given object from the given array + _arraySplice: function (anArray, obj) { + for (var i = anArray.length - 1; i >= 0; i--) { + if (anArray[i] === obj) { + anArray.splice(i, 1); + return true; + } + } + }, + + /** + * Removes a marker from all _gridUnclustered zoom levels, starting at the supplied zoom. + * @param marker to be removed from _gridUnclustered. + * @param z integer bottom start zoom level (included) + * @private + */ + _removeFromGridUnclustered: function (marker, z) { + var map = this._map, + gridUnclustered = this._gridUnclustered, + minZoom = Math.floor(this._map.getMinZoom()); + + for (; z >= minZoom; z--) { + if (!gridUnclustered[z].removeObject(marker, map.project(marker.getLatLng(), z))) { + break; + } + } + }, + + _childMarkerDragStart: function (e) { + e.target.__dragStart = e.target._latlng; + }, + + _childMarkerMoved: function (e) { + if (!this._ignoreMove && !e.target.__dragStart) { + var isPopupOpen = e.target._popup && e.target._popup.isOpen(); + + this._moveChild(e.target, e.oldLatLng, e.latlng); + + if (isPopupOpen) { + e.target.openPopup(); + } + } + }, + + _moveChild: function (layer, from, to) { + layer._latlng = from; + this.removeLayer(layer); + + layer._latlng = to; + this.addLayer(layer); + }, + + _childMarkerDragEnd: function (e) { + if (e.target.__dragStart) { + this._moveChild(e.target, e.target.__dragStart, e.target._latlng); + } + delete e.target.__dragStart; + }, + + + //Internal function for removing a marker from everything. + //dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions) + _removeLayer: function (marker, removeFromDistanceGrid, dontUpdateMap) { + var gridClusters = this._gridClusters, + gridUnclustered = this._gridUnclustered, + fg = this._featureGroup, + map = this._map, + minZoom = Math.floor(this._map.getMinZoom()); + + //Remove the marker from distance clusters it might be in + if (removeFromDistanceGrid) { + this._removeFromGridUnclustered(marker, this._maxZoom); + } + + //Work our way up the clusters removing them as we go if required + var cluster = marker.__parent, + markers = cluster._markers, + otherMarker; + + //Remove the marker from the immediate parents marker list + this._arraySplice(markers, marker); + + while (cluster) { + cluster._childCount--; + cluster._boundsNeedUpdate = true; + + if (cluster._zoom < minZoom) { + //Top level, do nothing + break; + } else if (removeFromDistanceGrid && cluster._childCount <= 1) { //Cluster no longer required + //We need to push the other marker up to the parent + otherMarker = cluster._markers[0] === marker ? cluster._markers[1] : cluster._markers[0]; + + //Update distance grid + gridClusters[cluster._zoom].removeObject(cluster, map.project(cluster._cLatLng, cluster._zoom)); + gridUnclustered[cluster._zoom].addObject(otherMarker, map.project(otherMarker.getLatLng(), cluster._zoom)); + + //Move otherMarker up to parent + this._arraySplice(cluster.__parent._childClusters, cluster); + cluster.__parent._markers.push(otherMarker); + otherMarker.__parent = cluster.__parent; + + if (cluster._icon) { + //Cluster is currently on the map, need to put the marker on the map instead + fg.removeLayer(cluster); + if (!dontUpdateMap) { + fg.addLayer(otherMarker); + } + } + } else { + cluster._iconNeedsUpdate = true; + } + + cluster = cluster.__parent; + } + + delete marker.__parent; + }, + + _isOrIsParent: function (el, oel) { + while (oel) { + if (el === oel) { + return true; + } + oel = oel.parentNode; + } + return false; + }, + + //Override L.Evented.fire + fire: function (type, data, propagate) { + if (data && data.layer instanceof L.MarkerCluster) { + //Prevent multiple clustermouseover/off events if the icon is made up of stacked divs (Doesn't work in ie <= 8, no relatedTarget) + if (data.originalEvent && this._isOrIsParent(data.layer._icon, data.originalEvent.relatedTarget)) { + return; + } + type = 'cluster' + type; + } + + L.FeatureGroup.prototype.fire.call(this, type, data, propagate); + }, + + //Override L.Evented.listens + listens: function (type, propagate) { + return L.FeatureGroup.prototype.listens.call(this, type, propagate) || L.FeatureGroup.prototype.listens.call(this, 'cluster' + type, propagate); + }, + + //Default functionality + _defaultIconCreateFunction: function (cluster) { + var childCount = cluster.getChildCount(); + + var c = ' marker-cluster-'; + if (childCount < 10) { + c += 'small'; + } else if (childCount < 100) { + c += 'medium'; + } else { + c += 'large'; + } + + return new L.DivIcon({ html: '
' + childCount + '
', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) }); + }, + + _bindEvents: function () { + var map = this._map, + spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom, + showCoverageOnHover = this.options.showCoverageOnHover, + zoomToBoundsOnClick = this.options.zoomToBoundsOnClick; + + //Zoom on cluster click or spiderfy if we are at the lowest level + if (spiderfyOnMaxZoom || zoomToBoundsOnClick) { + this.on('clusterclick', this._zoomOrSpiderfy, this); + } + + //Show convex hull (boundary) polygon on mouse over + if (showCoverageOnHover) { + this.on('clustermouseover', this._showCoverage, this); + this.on('clustermouseout', this._hideCoverage, this); + map.on('zoomend', this._hideCoverage, this); + } + }, + + _zoomOrSpiderfy: function (e) { + var cluster = e.layer, + bottomCluster = cluster; + + while (bottomCluster._childClusters.length === 1) { + bottomCluster = bottomCluster._childClusters[0]; + } + + if (bottomCluster._zoom === this._maxZoom && + bottomCluster._childCount === cluster._childCount && + this.options.spiderfyOnMaxZoom) { + + // All child markers are contained in a single cluster from this._maxZoom to this cluster. + cluster.spiderfy(); + } else if (this.options.zoomToBoundsOnClick) { + cluster.zoomToBounds(); + } + + // Focus the map again for keyboard users. + if (e.originalEvent && e.originalEvent.keyCode === 13) { + this._map._container.focus(); + } + }, + + _showCoverage: function (e) { + var map = this._map; + if (this._inZoomAnimation) { + return; + } + if (this._shownPolygon) { + map.removeLayer(this._shownPolygon); + } + if (e.layer.getChildCount() > 2 && e.layer !== this._spiderfied) { + this._shownPolygon = new L.Polygon(e.layer.getConvexHull(), this.options.polygonOptions); + map.addLayer(this._shownPolygon); + } + }, + + _hideCoverage: function () { + if (this._shownPolygon) { + this._map.removeLayer(this._shownPolygon); + this._shownPolygon = null; + } + }, + + _unbindEvents: function () { + var spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom, + showCoverageOnHover = this.options.showCoverageOnHover, + zoomToBoundsOnClick = this.options.zoomToBoundsOnClick, + map = this._map; + + if (spiderfyOnMaxZoom || zoomToBoundsOnClick) { + this.off('clusterclick', this._zoomOrSpiderfy, this); + } + if (showCoverageOnHover) { + this.off('clustermouseover', this._showCoverage, this); + this.off('clustermouseout', this._hideCoverage, this); + map.off('zoomend', this._hideCoverage, this); + } + }, + + _zoomEnd: function () { + if (!this._map) { //May have been removed from the map by a zoomEnd handler + return; + } + this._mergeSplitClusters(); + + this._zoom = Math.round(this._map._zoom); + this._currentShownBounds = this._getExpandedVisibleBounds(); + }, + + _moveEnd: function () { + if (this._inZoomAnimation) { + return; + } + + var newBounds = this._getExpandedVisibleBounds(); + + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), this._zoom, newBounds); + this._topClusterLevel._recursivelyAddChildrenToMap(null, Math.round(this._map._zoom), newBounds); + + this._currentShownBounds = newBounds; + return; + }, + + _generateInitialClusters: function () { + var maxZoom = Math.ceil(this._map.getMaxZoom()), + minZoom = Math.floor(this._map.getMinZoom()), + radius = this.options.maxClusterRadius, + radiusFn = radius; + + //If we just set maxClusterRadius to a single number, we need to create + //a simple function to return that number. Otherwise, we just have to + //use the function we've passed in. + if (typeof radius !== "function") { + radiusFn = function () { return radius; }; + } + + if (this.options.disableClusteringAtZoom !== null) { + maxZoom = this.options.disableClusteringAtZoom - 1; + } + this._maxZoom = maxZoom; + this._gridClusters = {}; + this._gridUnclustered = {}; + + //Set up DistanceGrids for each zoom + for (var zoom = maxZoom; zoom >= minZoom; zoom--) { + this._gridClusters[zoom] = new L.DistanceGrid(radiusFn(zoom)); + this._gridUnclustered[zoom] = new L.DistanceGrid(radiusFn(zoom)); + } + + // Instantiate the appropriate L.MarkerCluster class (animated or not). + this._topClusterLevel = new this._markerCluster(this, minZoom - 1); + }, + + //Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom) + _addLayer: function (layer, zoom) { + var gridClusters = this._gridClusters, + gridUnclustered = this._gridUnclustered, + minZoom = Math.floor(this._map.getMinZoom()), + markerPoint, z; + + if (this.options.singleMarkerMode) { + this._overrideMarkerIcon(layer); + } + + layer.on(this._childMarkerEventHandlers, this); + + //Find the lowest zoom level to slot this one in + for (; zoom >= minZoom; zoom--) { + markerPoint = this._map.project(layer.getLatLng(), zoom); // calculate pixel position + + //Try find a cluster close by + var closest = gridClusters[zoom].getNearObject(markerPoint); + if (closest) { + closest._addChild(layer); + layer.__parent = closest; + return; + } + + //Try find a marker close by to form a new cluster with + closest = gridUnclustered[zoom].getNearObject(markerPoint); + if (closest) { + var parent = closest.__parent; + if (parent) { + this._removeLayer(closest, false); + } + + //Create new cluster with these 2 in it + + var newCluster = new this._markerCluster(this, zoom, closest, layer); + gridClusters[zoom].addObject(newCluster, this._map.project(newCluster._cLatLng, zoom)); + closest.__parent = newCluster; + layer.__parent = newCluster; + + //First create any new intermediate parent clusters that don't exist + var lastParent = newCluster; + for (z = zoom - 1; z > parent._zoom; z--) { + lastParent = new this._markerCluster(this, z, lastParent); + gridClusters[z].addObject(lastParent, this._map.project(closest.getLatLng(), z)); + } + parent._addChild(lastParent); + + //Remove closest from this zoom level and any above that it is in, replace with newCluster + this._removeFromGridUnclustered(closest, zoom); + + return; + } + + //Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards + gridUnclustered[zoom].addObject(layer, markerPoint); + } + + //Didn't get in anything, add us to the top + this._topClusterLevel._addChild(layer); + layer.__parent = this._topClusterLevel; + return; + }, + + /** + * Refreshes the icon of all "dirty" visible clusters. + * Non-visible "dirty" clusters will be updated when they are added to the map. + * @private + */ + _refreshClustersIcons: function () { + this._featureGroup.eachLayer(function (c) { + if (c instanceof L.MarkerCluster && c._iconNeedsUpdate) { + c._updateIcon(); + } + }); + }, + + //Enqueue code to fire after the marker expand/contract has happened + _enqueue: function (fn) { + this._queue.push(fn); + if (!this._queueTimeout) { + this._queueTimeout = setTimeout(L.bind(this._processQueue, this), 300); + } + }, + _processQueue: function () { + for (var i = 0; i < this._queue.length; i++) { + this._queue[i].call(this); + } + this._queue.length = 0; + clearTimeout(this._queueTimeout); + this._queueTimeout = null; + }, + + //Merge and split any existing clusters that are too big or small + _mergeSplitClusters: function () { + var mapZoom = Math.round(this._map._zoom); + + //In case we are starting to split before the animation finished + this._processQueue(); + + if (this._zoom < mapZoom && this._currentShownBounds.intersects(this._getExpandedVisibleBounds())) { //Zoom in, split + this._animationStart(); + //Remove clusters now off screen + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), this._zoom, this._getExpandedVisibleBounds()); + + this._animationZoomIn(this._zoom, mapZoom); + + } else if (this._zoom > mapZoom) { //Zoom out, merge + this._animationStart(); + + this._animationZoomOut(this._zoom, mapZoom); + } else { + this._moveEnd(); + } + }, + + //Gets the maps visible bounds expanded in each direction by the size of the screen (so the user cannot see an area we do not cover in one pan) + _getExpandedVisibleBounds: function () { + if (!this.options.removeOutsideVisibleBounds) { + return this._mapBoundsInfinite; + } else if (L.Browser.mobile) { + return this._checkBoundsMaxLat(this._map.getBounds()); + } + + return this._checkBoundsMaxLat(this._map.getBounds().pad(1)); // Padding expands the bounds by its own dimensions but scaled with the given factor. + }, + + /** + * Expands the latitude to Infinity (or -Infinity) if the input bounds reach the map projection maximum defined latitude + * (in the case of Web/Spherical Mercator, it is 85.0511287798 / see https://en.wikipedia.org/wiki/Web_Mercator#Formulas). + * Otherwise, the removeOutsideVisibleBounds option will remove markers beyond that limit, whereas the same markers without + * this option (or outside MCG) will have their position floored (ceiled) by the projection and rendered at that limit, + * making the user think that MCG "eats" them and never displays them again. + * @param bounds L.LatLngBounds + * @returns {L.LatLngBounds} + * @private + */ + _checkBoundsMaxLat: function (bounds) { + var maxLat = this._maxLat; + + if (maxLat !== undefined) { + if (bounds.getNorth() >= maxLat) { + bounds._northEast.lat = Infinity; + } + if (bounds.getSouth() <= -maxLat) { + bounds._southWest.lat = -Infinity; + } + } + + return bounds; + }, + + //Shared animation code + _animationAddLayerNonAnimated: function (layer, newCluster) { + if (newCluster === layer) { + this._featureGroup.addLayer(layer); + } else if (newCluster._childCount === 2) { + newCluster._addToMap(); + + var markers = newCluster.getAllChildMarkers(); + this._featureGroup.removeLayer(markers[0]); + this._featureGroup.removeLayer(markers[1]); + } else { + newCluster._updateIcon(); + } + }, + + /** + * Extracts individual (i.e. non-group) layers from a Layer Group. + * @param group to extract layers from. + * @param output {Array} in which to store the extracted layers. + * @returns {*|Array} + * @private + */ + _extractNonGroupLayers: function (group, output) { + var layers = group.getLayers(), + i = 0, + layer; + + output = output || []; + + for (; i < layers.length; i++) { + layer = layers[i]; + + if (layer instanceof L.LayerGroup) { + this._extractNonGroupLayers(layer, output); + continue; + } + + output.push(layer); + } + + return output; + }, + + /** + * Implements the singleMarkerMode option. + * @param layer Marker to re-style using the Clusters iconCreateFunction. + * @returns {L.Icon} The newly created icon. + * @private + */ + _overrideMarkerIcon: function (layer) { + var icon = layer.options.icon = this.options.iconCreateFunction({ + getChildCount: function () { + return 1; + }, + getAllChildMarkers: function () { + return [layer]; + } + }); + + return icon; + } +}); + +// Constant bounds used in case option "removeOutsideVisibleBounds" is set to false. +L.MarkerClusterGroup.include({ + _mapBoundsInfinite: new L.LatLngBounds(new L.LatLng(-Infinity, -Infinity), new L.LatLng(Infinity, Infinity)) +}); + +L.MarkerClusterGroup.include({ + _noAnimation: { + //Non Animated versions of everything + _animationStart: function () { + //Do nothing... + }, + _animationZoomIn: function (previousZoomLevel, newZoomLevel) { + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), previousZoomLevel); + this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); + + //We didn't actually animate, but we use this event to mean "clustering animations have finished" + this.fire('animationend'); + }, + _animationZoomOut: function (previousZoomLevel, newZoomLevel) { + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), previousZoomLevel); + this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); + + //We didn't actually animate, but we use this event to mean "clustering animations have finished" + this.fire('animationend'); + }, + _animationAddLayer: function (layer, newCluster) { + this._animationAddLayerNonAnimated(layer, newCluster); + } + }, + + _withAnimation: { + //Animated versions here + _animationStart: function () { + this._map._mapPane.className += ' leaflet-cluster-anim'; + this._inZoomAnimation++; + }, + + _animationZoomIn: function (previousZoomLevel, newZoomLevel) { + var bounds = this._getExpandedVisibleBounds(), + fg = this._featureGroup, + minZoom = Math.floor(this._map.getMinZoom()), + i; + + this._ignoreMove = true; + + //Add all children of current clusters to map and remove those clusters from map + this._topClusterLevel._recursively(bounds, previousZoomLevel, minZoom, function (c) { + var startPos = c._latlng, + markers = c._markers, + m; + + if (!bounds.contains(startPos)) { + startPos = null; + } + + if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us + fg.removeLayer(c); + c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds); + } else { + //Fade out old cluster + c.clusterHide(); + c._recursivelyAddChildrenToMap(startPos, newZoomLevel, bounds); + } + + //Remove all markers that aren't visible any more + //TODO: Do we actually need to do this on the higher levels too? + for (i = markers.length - 1; i >= 0; i--) { + m = markers[i]; + if (!bounds.contains(m._latlng)) { + fg.removeLayer(m); + } + } + + }); + + this._forceLayout(); + + //Update opacities + this._topClusterLevel._recursivelyBecomeVisible(bounds, newZoomLevel); + //TODO Maybe? Update markers in _recursivelyBecomeVisible + fg.eachLayer(function (n) { + if (!(n instanceof L.MarkerCluster) && n._icon) { + n.clusterShow(); + } + }); + + //update the positions of the just added clusters/markers + this._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) { + c._recursivelyRestoreChildPositions(newZoomLevel); + }); + + this._ignoreMove = false; + + //Remove the old clusters and close the zoom animation + this._enqueue(function () { + //update the positions of the just added clusters/markers + this._topClusterLevel._recursively(bounds, previousZoomLevel, minZoom, function (c) { + fg.removeLayer(c); + c.clusterShow(); + }); + + this._animationEnd(); + }); + }, + + _animationZoomOut: function (previousZoomLevel, newZoomLevel) { + this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel - 1, newZoomLevel); + + //Need to add markers for those that weren't on the map before but are now + this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); + //Remove markers that were on the map before but won't be now + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, Math.floor(this._map.getMinZoom()), previousZoomLevel, this._getExpandedVisibleBounds()); + }, + + _animationAddLayer: function (layer, newCluster) { + var me = this, + fg = this._featureGroup; + + fg.addLayer(layer); + if (newCluster !== layer) { + if (newCluster._childCount > 2) { //Was already a cluster + + newCluster._updateIcon(); + this._forceLayout(); + this._animationStart(); + + layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng())); + layer.clusterHide(); + + this._enqueue(function () { + fg.removeLayer(layer); + layer.clusterShow(); + + me._animationEnd(); + }); + + } else { //Just became a cluster + this._forceLayout(); + + me._animationStart(); + me._animationZoomOutSingle(newCluster, this._map.getMaxZoom(), this._zoom); + } + } + } + }, + + // Private methods for animated versions. + _animationZoomOutSingle: function (cluster, previousZoomLevel, newZoomLevel) { + var bounds = this._getExpandedVisibleBounds(), + minZoom = Math.floor(this._map.getMinZoom()); + + //Animate all of the markers in the clusters to move to their cluster center point + cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, minZoom, previousZoomLevel + 1, newZoomLevel); + + var me = this; + + //Update the opacity (If we immediately set it they won't animate) + this._forceLayout(); + cluster._recursivelyBecomeVisible(bounds, newZoomLevel); + + //TODO: Maybe use the transition timing stuff to make this more reliable + //When the animations are done, tidy up + this._enqueue(function () { + + //This cluster stopped being a cluster before the timeout fired + if (cluster._childCount === 1) { + var m = cluster._markers[0]; + //If we were in a cluster animation at the time then the opacity and position of our child could be wrong now, so fix it + this._ignoreMove = true; + m.setLatLng(m.getLatLng()); + this._ignoreMove = false; + if (m.clusterShow) { + m.clusterShow(); + } + } else { + cluster._recursively(bounds, newZoomLevel, minZoom, function (c) { + c._recursivelyRemoveChildrenFromMap(bounds, minZoom, previousZoomLevel + 1); + }); + } + me._animationEnd(); + }); + }, + + _animationEnd: function () { + if (this._map) { + this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', ''); + } + this._inZoomAnimation--; + this.fire('animationend'); + }, + + //Force a browser layout of stuff in the map + // Should apply the current opacity and location to all elements so we can update them again for an animation + _forceLayout: function () { + //In my testing this works, infact offsetWidth of any element seems to work. + //Could loop all this._layers and do this for each _icon if it stops working + + L.Util.falseFn(document.body.offsetWidth); + } +}); + +L.markerClusterGroup = function (options) { + return new L.MarkerClusterGroup(options); +}; + + +L.MarkerCluster = L.Marker.extend({ + initialize: function (group, zoom, a, b) { + + L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0), + { icon: this, pane: group.options.clusterPane }); + + this._group = group; + this._zoom = zoom; + + this._markers = []; + this._childClusters = []; + this._childCount = 0; + this._iconNeedsUpdate = true; + this._boundsNeedUpdate = true; + + this._bounds = new L.LatLngBounds(); + + if (a) { + this._addChild(a); + } + if (b) { + this._addChild(b); + } + }, + + //Recursively retrieve all child markers of this cluster + getAllChildMarkers: function (storageArray) { + storageArray = storageArray || []; + + for (var i = this._childClusters.length - 1; i >= 0; i--) { + this._childClusters[i].getAllChildMarkers(storageArray); + } + + for (var j = this._markers.length - 1; j >= 0; j--) { + storageArray.push(this._markers[j]); + } + + return storageArray; + }, + + //Returns the count of how many child markers we have + getChildCount: function () { + return this._childCount; + }, + + //Zoom to the minimum of showing all of the child markers, or the extents of this cluster + zoomToBounds: function (fitBoundsOptions) { + var childClusters = this._childClusters.slice(), + map = this._group._map, + boundsZoom = map.getBoundsZoom(this._bounds), + zoom = this._zoom + 1, + mapZoom = map.getZoom(), + i; + + //calculate how far we need to zoom down to see all of the markers + while (childClusters.length > 0 && boundsZoom > zoom) { + zoom++; + var newClusters = []; + for (i = 0; i < childClusters.length; i++) { + newClusters = newClusters.concat(childClusters[i]._childClusters); + } + childClusters = newClusters; + } + + if (boundsZoom > zoom) { + this._group._map.setView(this._latlng, zoom); + } else if (boundsZoom <= mapZoom) { //If fitBounds wouldn't zoom us down, zoom us down instead + this._group._map.setView(this._latlng, mapZoom + 1); + } else { + this._group._map.fitBounds(this._bounds, fitBoundsOptions); + } + }, + + getBounds: function () { + var bounds = new L.LatLngBounds(); + bounds.extend(this._bounds); + return bounds; + }, + + _updateIcon: function () { + this._iconNeedsUpdate = true; + if (this._icon) { + this.setIcon(this); + } + }, + + //Cludge for Icon, we pretend to be an icon for performance + createIcon: function () { + if (this._iconNeedsUpdate) { + this._iconObj = this._group.options.iconCreateFunction(this); + this._iconNeedsUpdate = false; + } + return this._iconObj.createIcon(); + }, + createShadow: function () { + return this._iconObj.createShadow(); + }, + + + _addChild: function (new1, isNotificationFromChild) { + + this._iconNeedsUpdate = true; + + this._boundsNeedUpdate = true; + this._setClusterCenter(new1); + + if (new1 instanceof L.MarkerCluster) { + if (!isNotificationFromChild) { + this._childClusters.push(new1); + new1.__parent = this; + } + this._childCount += new1._childCount; + } else { + if (!isNotificationFromChild) { + this._markers.push(new1); + } + this._childCount++; + } + + if (this.__parent) { + this.__parent._addChild(new1, true); + } + }, + + /** + * Makes sure the cluster center is set. If not, uses the child center if it is a cluster, or the marker position. + * @param child L.MarkerCluster|L.Marker that will be used as cluster center if not defined yet. + * @private + */ + _setClusterCenter: function (child) { + if (!this._cLatLng) { + // when clustering, take position of the first point as the cluster center + this._cLatLng = child._cLatLng || child._latlng; + } + }, + + /** + * Assigns impossible bounding values so that the next extend entirely determines the new bounds. + * This method avoids having to trash the previous L.LatLngBounds object and to create a new one, which is much slower for this class. + * As long as the bounds are not extended, most other methods would probably fail, as they would with bounds initialized but not extended. + * @private + */ + _resetBounds: function () { + var bounds = this._bounds; + + if (bounds._southWest) { + bounds._southWest.lat = Infinity; + bounds._southWest.lng = Infinity; + } + if (bounds._northEast) { + bounds._northEast.lat = -Infinity; + bounds._northEast.lng = -Infinity; + } + }, + + _recalculateBounds: function () { + var markers = this._markers, + childClusters = this._childClusters, + latSum = 0, + lngSum = 0, + totalCount = this._childCount, + i, child, childLatLng, childCount; + + // Case where all markers are removed from the map and we are left with just an empty _topClusterLevel. + if (totalCount === 0) { + return; + } + + // Reset rather than creating a new object, for performance. + this._resetBounds(); + + // Child markers. + for (i = 0; i < markers.length; i++) { + childLatLng = markers[i]._latlng; + + this._bounds.extend(childLatLng); + + latSum += childLatLng.lat; + lngSum += childLatLng.lng; + } + + // Child clusters. + for (i = 0; i < childClusters.length; i++) { + child = childClusters[i]; + + // Re-compute child bounds and weighted position first if necessary. + if (child._boundsNeedUpdate) { + child._recalculateBounds(); + } + + this._bounds.extend(child._bounds); + + childLatLng = child._wLatLng; + childCount = child._childCount; + + latSum += childLatLng.lat * childCount; + lngSum += childLatLng.lng * childCount; + } + + this._latlng = this._wLatLng = new L.LatLng(latSum / totalCount, lngSum / totalCount); + + // Reset dirty flag. + this._boundsNeedUpdate = false; + }, + + //Set our markers position as given and add it to the map + _addToMap: function (startPos) { + if (startPos) { + this._backupLatlng = this._latlng; + this.setLatLng(startPos); + } + this._group._featureGroup.addLayer(this); + }, + + _recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) { + this._recursively(bounds, this._group._map.getMinZoom(), maxZoom - 1, + function (c) { + var markers = c._markers, + i, m; + for (i = markers.length - 1; i >= 0; i--) { + m = markers[i]; + + //Only do it if the icon is still on the map + if (m._icon) { + m._setPos(center); + m.clusterHide(); + } + } + }, + function (c) { + var childClusters = c._childClusters, + j, cm; + for (j = childClusters.length - 1; j >= 0; j--) { + cm = childClusters[j]; + if (cm._icon) { + cm._setPos(center); + cm.clusterHide(); + } + } + } + ); + }, + + _recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, mapMinZoom, previousZoomLevel, newZoomLevel) { + this._recursively(bounds, newZoomLevel, mapMinZoom, + function (c) { + c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel); + + //TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be. + //As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate + if (c._isSingleParent() && previousZoomLevel - 1 === newZoomLevel) { + c.clusterShow(); + c._recursivelyRemoveChildrenFromMap(bounds, mapMinZoom, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds + } else { + c.clusterHide(); + } + + c._addToMap(); + } + ); + }, + + _recursivelyBecomeVisible: function (bounds, zoomLevel) { + this._recursively(bounds, this._group._map.getMinZoom(), zoomLevel, null, function (c) { + c.clusterShow(); + }); + }, + + _recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) { + this._recursively(bounds, this._group._map.getMinZoom() - 1, zoomLevel, + function (c) { + if (zoomLevel === c._zoom) { + return; + } + + //Add our child markers at startPos (so they can be animated out) + for (var i = c._markers.length - 1; i >= 0; i--) { + var nm = c._markers[i]; + + if (!bounds.contains(nm._latlng)) { + continue; + } + + if (startPos) { + nm._backupLatlng = nm.getLatLng(); + + nm.setLatLng(startPos); + if (nm.clusterHide) { + nm.clusterHide(); + } + } + + c._group._featureGroup.addLayer(nm); + } + }, + function (c) { + c._addToMap(startPos); + } + ); + }, + + _recursivelyRestoreChildPositions: function (zoomLevel) { + //Fix positions of child markers + for (var i = this._markers.length - 1; i >= 0; i--) { + var nm = this._markers[i]; + if (nm._backupLatlng) { + nm.setLatLng(nm._backupLatlng); + delete nm._backupLatlng; + } + } + + if (zoomLevel - 1 === this._zoom) { + //Reposition child clusters + for (var j = this._childClusters.length - 1; j >= 0; j--) { + this._childClusters[j]._restorePosition(); + } + } else { + for (var k = this._childClusters.length - 1; k >= 0; k--) { + this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel); + } + } + }, + + _restorePosition: function () { + if (this._backupLatlng) { + this.setLatLng(this._backupLatlng); + delete this._backupLatlng; + } + }, + + //exceptBounds: If set, don't remove any markers/clusters in it + _recursivelyRemoveChildrenFromMap: function (previousBounds, mapMinZoom, zoomLevel, exceptBounds) { + var m, i; + this._recursively(previousBounds, mapMinZoom - 1, zoomLevel - 1, + function (c) { + //Remove markers at every level + for (i = c._markers.length - 1; i >= 0; i--) { + m = c._markers[i]; + if (!exceptBounds || !exceptBounds.contains(m._latlng)) { + c._group._featureGroup.removeLayer(m); + if (m.clusterShow) { + m.clusterShow(); + } + } + } + }, + function (c) { + //Remove child clusters at just the bottom level + for (i = c._childClusters.length - 1; i >= 0; i--) { + m = c._childClusters[i]; + if (!exceptBounds || !exceptBounds.contains(m._latlng)) { + c._group._featureGroup.removeLayer(m); + if (m.clusterShow) { + m.clusterShow(); + } + } + } + } + ); + }, + + //Run the given functions recursively to this and child clusters + // boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to + // zoomLevelToStart: zoom level to start running functions (inclusive) + // zoomLevelToStop: zoom level to stop running functions (inclusive) + // runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level + // runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level + _recursively: function (boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel) { + var childClusters = this._childClusters, + zoom = this._zoom, + i, c; + + if (zoomLevelToStart <= zoom) { + if (runAtEveryLevel) { + runAtEveryLevel(this); + } + if (runAtBottomLevel && zoom === zoomLevelToStop) { + runAtBottomLevel(this); + } + } + + if (zoom < zoomLevelToStart || zoom < zoomLevelToStop) { + for (i = childClusters.length - 1; i >= 0; i--) { + c = childClusters[i]; + if (boundsToApplyTo.intersects(c._bounds)) { + c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel); + } + } + } + }, + + //Returns true if we are the parent of only one cluster and that cluster is the same as us + _isSingleParent: function () { + //Don't need to check this._markers as the rest won't work if there are any + return this._childClusters.length > 0 && this._childClusters[0]._childCount === this._childCount; + } +}); + + + +/* +* Extends L.Marker to include two extra methods: clusterHide and clusterShow. +* +* They work as setOpacity(0) and setOpacity(1) respectively, but +* they will remember the marker's opacity when hiding and showing it again. +* +*/ + + +L.Marker.include({ + + clusterHide: function () { + this.options.opacityWhenUnclustered = this.options.opacity || 1; + return this.setOpacity(0); + }, + + clusterShow: function () { + var ret = this.setOpacity(this.options.opacity || this.options.opacityWhenUnclustered); + delete this.options.opacityWhenUnclustered; + return ret; + } + +}); + + + + + +L.DistanceGrid = function (cellSize) { + this._cellSize = cellSize; + this._sqCellSize = cellSize * cellSize; + this._grid = {}; + this._objectPoint = { }; +}; + +L.DistanceGrid.prototype = { + + addObject: function (obj, point) { + var x = this._getCoord(point.x), + y = this._getCoord(point.y), + grid = this._grid, + row = grid[y] = grid[y] || {}, + cell = row[x] = row[x] || [], + stamp = L.Util.stamp(obj); + + this._objectPoint[stamp] = point; + + cell.push(obj); + }, + + updateObject: function (obj, point) { + this.removeObject(obj); + this.addObject(obj, point); + }, + + //Returns true if the object was found + removeObject: function (obj, point) { + var x = this._getCoord(point.x), + y = this._getCoord(point.y), + grid = this._grid, + row = grid[y] = grid[y] || {}, + cell = row[x] = row[x] || [], + i, len; + + delete this._objectPoint[L.Util.stamp(obj)]; + + for (i = 0, len = cell.length; i < len; i++) { + if (cell[i] === obj) { + + cell.splice(i, 1); + + if (len === 1) { + delete row[x]; + } + + return true; + } + } + + }, + + eachObject: function (fn, context) { + var i, j, k, len, row, cell, removed, + grid = this._grid; + + for (i in grid) { + row = grid[i]; + + for (j in row) { + cell = row[j]; + + for (k = 0, len = cell.length; k < len; k++) { + removed = fn.call(context, cell[k]); + if (removed) { + k--; + len--; + } + } + } + } + }, + + getNearObject: function (point) { + var x = this._getCoord(point.x), + y = this._getCoord(point.y), + i, j, k, row, cell, len, obj, dist, + objectPoint = this._objectPoint, + closestDistSq = this._sqCellSize, + closest = null; + + for (i = y - 1; i <= y + 1; i++) { + row = this._grid[i]; + if (row) { + + for (j = x - 1; j <= x + 1; j++) { + cell = row[j]; + if (cell) { + + for (k = 0, len = cell.length; k < len; k++) { + obj = cell[k]; + dist = this._sqDist(objectPoint[L.Util.stamp(obj)], point); + if (dist < closestDistSq) { + closestDistSq = dist; + closest = obj; + } + } + } + } + } + } + return closest; + }, + + _getCoord: function (x) { + return Math.floor(x / this._cellSize); + }, + + _sqDist: function (p, p2) { + var dx = p2.x - p.x, + dy = p2.y - p.y; + return dx * dx + dy * dy; + } +}; + + +/* Copyright (c) 2012 the authors listed at the following URL, and/or +the authors of referenced articles or incorporated external code: +http://en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=20120410175256 + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=18434 +*/ + +(function () { + L.QuickHull = { + + /* + * @param {Object} cpt a point to be measured from the baseline + * @param {Array} bl the baseline, as represented by a two-element + * array of latlng objects. + * @returns {Number} an approximate distance measure + */ + getDistant: function (cpt, bl) { + var vY = bl[1].lat - bl[0].lat, + vX = bl[0].lng - bl[1].lng; + return (vX * (cpt.lat - bl[0].lat) + vY * (cpt.lng - bl[0].lng)); + }, + + /* + * @param {Array} baseLine a two-element array of latlng objects + * representing the baseline to project from + * @param {Array} latLngs an array of latlng objects + * @returns {Object} the maximum point and all new points to stay + * in consideration for the hull. + */ + findMostDistantPointFromBaseLine: function (baseLine, latLngs) { + var maxD = 0, + maxPt = null, + newPoints = [], + i, pt, d; + + for (i = latLngs.length - 1; i >= 0; i--) { + pt = latLngs[i]; + d = this.getDistant(pt, baseLine); + + if (d > 0) { + newPoints.push(pt); + } else { + continue; + } + + if (d > maxD) { + maxD = d; + maxPt = pt; + } + } + + return { maxPoint: maxPt, newPoints: newPoints }; + }, + + + /* + * Given a baseline, compute the convex hull of latLngs as an array + * of latLngs. + * + * @param {Array} latLngs + * @returns {Array} + */ + buildConvexHull: function (baseLine, latLngs) { + var convexHullBaseLines = [], + t = this.findMostDistantPointFromBaseLine(baseLine, latLngs); + + if (t.maxPoint) { // if there is still a point "outside" the base line + convexHullBaseLines = + convexHullBaseLines.concat( + this.buildConvexHull([baseLine[0], t.maxPoint], t.newPoints) + ); + convexHullBaseLines = + convexHullBaseLines.concat( + this.buildConvexHull([t.maxPoint, baseLine[1]], t.newPoints) + ); + return convexHullBaseLines; + } else { // if there is no more point "outside" the base line, the current base line is part of the convex hull + return [baseLine[0]]; + } + }, + + /* + * Given an array of latlngs, compute a convex hull as an array + * of latlngs + * + * @param {Array} latLngs + * @returns {Array} + */ + getConvexHull: function (latLngs) { + // find first baseline + var maxLat = false, minLat = false, + maxLng = false, minLng = false, + maxLatPt = null, minLatPt = null, + maxLngPt = null, minLngPt = null, + maxPt = null, minPt = null, + i; + + for (i = latLngs.length - 1; i >= 0; i--) { + var pt = latLngs[i]; + if (maxLat === false || pt.lat > maxLat) { + maxLatPt = pt; + maxLat = pt.lat; + } + if (minLat === false || pt.lat < minLat) { + minLatPt = pt; + minLat = pt.lat; + } + if (maxLng === false || pt.lng > maxLng) { + maxLngPt = pt; + maxLng = pt.lng; + } + if (minLng === false || pt.lng < minLng) { + minLngPt = pt; + minLng = pt.lng; + } + } + + if (minLat !== maxLat) { + minPt = minLatPt; + maxPt = maxLatPt; + } else { + minPt = minLngPt; + maxPt = maxLngPt; + } + + var ch = [].concat(this.buildConvexHull([minPt, maxPt], latLngs), + this.buildConvexHull([maxPt, minPt], latLngs)); + return ch; + } + }; +}()); + +L.MarkerCluster.include({ + getConvexHull: function () { + var childMarkers = this.getAllChildMarkers(), + points = [], + p, i; + + for (i = childMarkers.length - 1; i >= 0; i--) { + p = childMarkers[i].getLatLng(); + points.push(p); + } + + return L.QuickHull.getConvexHull(points); + } +}); + + +//This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet +//Huge thanks to jawj for implementing it first to make my job easy :-) + +L.MarkerCluster.include({ + + _2PI: Math.PI * 2, + _circleFootSeparation: 25, //related to circumference of circle + _circleStartAngle: Math.PI / 6, + + _spiralFootSeparation: 28, //related to size of spiral (experiment!) + _spiralLengthStart: 11, + _spiralLengthFactor: 5, + + _circleSpiralSwitchover: 9, //show spiral instead of circle from this marker count upwards. + // 0 -> always spiral; Infinity -> always circle + + spiderfy: function () { + if (this._group._spiderfied === this || this._group._inZoomAnimation) { + return; + } + + var childMarkers = this.getAllChildMarkers(), + group = this._group, + map = group._map, + center = map.latLngToLayerPoint(this._latlng), + positions; + + this._group._unspiderfy(); + this._group._spiderfied = this; + + //TODO Maybe: childMarkers order by distance to center + + if (childMarkers.length >= this._circleSpiralSwitchover) { + positions = this._generatePointsSpiral(childMarkers.length, center); + } else { + center.y += 10; // Otherwise circles look wrong => hack for standard blue icon, renders differently for other icons. + positions = this._generatePointsCircle(childMarkers.length, center); + } + + this._animationSpiderfy(childMarkers, positions); + }, + + unspiderfy: function (zoomDetails) { + /// Argument from zoomanim if being called in a zoom animation or null otherwise + if (this._group._inZoomAnimation) { + return; + } + this._animationUnspiderfy(zoomDetails); + + this._group._spiderfied = null; + }, + + _generatePointsCircle: function (count, centerPt) { + var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count), + legLength = circumference / this._2PI, //radius from circumference + angleStep = this._2PI / count, + res = [], + i, angle; + + res.length = count; + + for (i = count - 1; i >= 0; i--) { + angle = this._circleStartAngle + i * angleStep; + res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); + } + + return res; + }, + + _generatePointsSpiral: function (count, centerPt) { + var spiderfyDistanceMultiplier = this._group.options.spiderfyDistanceMultiplier, + legLength = spiderfyDistanceMultiplier * this._spiralLengthStart, + separation = spiderfyDistanceMultiplier * this._spiralFootSeparation, + lengthFactor = spiderfyDistanceMultiplier * this._spiralLengthFactor * this._2PI, + angle = 0, + res = [], + i; + + res.length = count; + + // Higher index, closer position to cluster center. + for (i = count - 1; i >= 0; i--) { + angle += separation / legLength + i * 0.0005; + res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); + legLength += lengthFactor / angle; + } + return res; + }, + + _noanimationUnspiderfy: function () { + var group = this._group, + map = group._map, + fg = group._featureGroup, + childMarkers = this.getAllChildMarkers(), + m, i; + + group._ignoreMove = true; + + this.setOpacity(1); + for (i = childMarkers.length - 1; i >= 0; i--) { + m = childMarkers[i]; + + fg.removeLayer(m); + + if (m._preSpiderfyLatlng) { + m.setLatLng(m._preSpiderfyLatlng); + delete m._preSpiderfyLatlng; + } + if (m.setZIndexOffset) { + m.setZIndexOffset(0); + } + + if (m._spiderLeg) { + map.removeLayer(m._spiderLeg); + delete m._spiderLeg; + } + } + + group.fire('unspiderfied', { + cluster: this, + markers: childMarkers + }); + group._ignoreMove = false; + group._spiderfied = null; + } +}); + +//Non Animated versions of everything +L.MarkerClusterNonAnimated = L.MarkerCluster.extend({ + _animationSpiderfy: function (childMarkers, positions) { + var group = this._group, + map = group._map, + fg = group._featureGroup, + legOptions = this._group.options.spiderLegPolylineOptions, + i, m, leg, newPos; + + group._ignoreMove = true; + + // Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition. + // The reverse order trick no longer improves performance on modern browsers. + for (i = 0; i < childMarkers.length; i++) { + newPos = map.layerPointToLatLng(positions[i]); + m = childMarkers[i]; + + // Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it. + leg = new L.Polyline([this._latlng, newPos], legOptions); + map.addLayer(leg); + m._spiderLeg = leg; + + // Now add the marker. + m._preSpiderfyLatlng = m._latlng; + m.setLatLng(newPos); + if (m.setZIndexOffset) { + m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING + } + + fg.addLayer(m); + } + this.setOpacity(0.3); + + group._ignoreMove = false; + group.fire('spiderfied', { + cluster: this, + markers: childMarkers + }); + }, + + _animationUnspiderfy: function () { + this._noanimationUnspiderfy(); + } +}); + +//Animated versions here +L.MarkerCluster.include({ + + _animationSpiderfy: function (childMarkers, positions) { + var me = this, + group = this._group, + map = group._map, + fg = group._featureGroup, + thisLayerLatLng = this._latlng, + thisLayerPos = map.latLngToLayerPoint(thisLayerLatLng), + svg = L.Path.SVG, + legOptions = L.extend({}, this._group.options.spiderLegPolylineOptions), // Copy the options so that we can modify them for animation. + finalLegOpacity = legOptions.opacity, + i, m, leg, legPath, legLength, newPos; + + if (finalLegOpacity === undefined) { + finalLegOpacity = L.MarkerClusterGroup.prototype.options.spiderLegPolylineOptions.opacity; + } + + if (svg) { + // If the initial opacity of the spider leg is not 0 then it appears before the animation starts. + legOptions.opacity = 0; + + // Add the class for CSS transitions. + legOptions.className = (legOptions.className || '') + ' leaflet-cluster-spider-leg'; + } else { + // Make sure we have a defined opacity. + legOptions.opacity = finalLegOpacity; + } + + group._ignoreMove = true; + + // Add markers and spider legs to map, hidden at our center point. + // Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition. + // The reverse order trick no longer improves performance on modern browsers. + for (i = 0; i < childMarkers.length; i++) { + m = childMarkers[i]; + + newPos = map.layerPointToLatLng(positions[i]); + + // Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it. + leg = new L.Polyline([thisLayerLatLng, newPos], legOptions); + map.addLayer(leg); + m._spiderLeg = leg; + + // Explanations: https://jakearchibald.com/2013/animated-line-drawing-svg/ + // In our case the transition property is declared in the CSS file. + if (svg) { + legPath = leg._path; + legLength = legPath.getTotalLength() + 0.1; // Need a small extra length to avoid remaining dot in Firefox. + legPath.style.strokeDasharray = legLength; // Just 1 length is enough, it will be duplicated. + legPath.style.strokeDashoffset = legLength; + } + + // If it is a marker, add it now and we'll animate it out + if (m.setZIndexOffset) { + m.setZIndexOffset(1000000); // Make normal markers appear on top of EVERYTHING + } + if (m.clusterHide) { + m.clusterHide(); + } + + // Vectors just get immediately added + fg.addLayer(m); + + if (m._setPos) { + m._setPos(thisLayerPos); + } + } + + group._forceLayout(); + group._animationStart(); + + // Reveal markers and spider legs. + for (i = childMarkers.length - 1; i >= 0; i--) { + newPos = map.layerPointToLatLng(positions[i]); + m = childMarkers[i]; + + //Move marker to new position + m._preSpiderfyLatlng = m._latlng; + m.setLatLng(newPos); + + if (m.clusterShow) { + m.clusterShow(); + } + + // Animate leg (animation is actually delegated to CSS transition). + if (svg) { + leg = m._spiderLeg; + legPath = leg._path; + legPath.style.strokeDashoffset = 0; + //legPath.style.strokeOpacity = finalLegOpacity; + leg.setStyle({opacity: finalLegOpacity}); + } + } + this.setOpacity(0.3); + + group._ignoreMove = false; + + setTimeout(function () { + group._animationEnd(); + group.fire('spiderfied', { + cluster: me, + markers: childMarkers + }); + }, 200); + }, + + _animationUnspiderfy: function (zoomDetails) { + var me = this, + group = this._group, + map = group._map, + fg = group._featureGroup, + thisLayerPos = zoomDetails ? map._latLngToNewLayerPoint(this._latlng, zoomDetails.zoom, zoomDetails.center) : map.latLngToLayerPoint(this._latlng), + childMarkers = this.getAllChildMarkers(), + svg = L.Path.SVG, + m, i, leg, legPath, legLength, nonAnimatable; + + group._ignoreMove = true; + group._animationStart(); + + //Make us visible and bring the child markers back in + this.setOpacity(1); + for (i = childMarkers.length - 1; i >= 0; i--) { + m = childMarkers[i]; + + //Marker was added to us after we were spiderfied + if (!m._preSpiderfyLatlng) { + continue; + } + + //Close any popup on the marker first, otherwise setting the location of the marker will make the map scroll + m.closePopup(); + + //Fix up the location to the real one + m.setLatLng(m._preSpiderfyLatlng); + delete m._preSpiderfyLatlng; + + //Hack override the location to be our center + nonAnimatable = true; + if (m._setPos) { + m._setPos(thisLayerPos); + nonAnimatable = false; + } + if (m.clusterHide) { + m.clusterHide(); + nonAnimatable = false; + } + if (nonAnimatable) { + fg.removeLayer(m); + } + + // Animate the spider leg back in (animation is actually delegated to CSS transition). + if (svg) { + leg = m._spiderLeg; + legPath = leg._path; + legLength = legPath.getTotalLength() + 0.1; + legPath.style.strokeDashoffset = legLength; + leg.setStyle({opacity: 0}); + } + } + + group._ignoreMove = false; + + setTimeout(function () { + //If we have only <= one child left then that marker will be shown on the map so don't remove it! + var stillThereChildCount = 0; + for (i = childMarkers.length - 1; i >= 0; i--) { + m = childMarkers[i]; + if (m._spiderLeg) { + stillThereChildCount++; + } + } + + + for (i = childMarkers.length - 1; i >= 0; i--) { + m = childMarkers[i]; + + if (!m._spiderLeg) { //Has already been unspiderfied + continue; + } + + if (m.clusterShow) { + m.clusterShow(); + } + if (m.setZIndexOffset) { + m.setZIndexOffset(0); + } + + if (stillThereChildCount > 1) { + fg.removeLayer(m); + } + + map.removeLayer(m._spiderLeg); + delete m._spiderLeg; + } + group._animationEnd(); + group.fire('unspiderfied', { + cluster: me, + markers: childMarkers + }); + }, 200); + } +}); + + +L.MarkerClusterGroup.include({ + //The MarkerCluster currently spiderfied (if any) + _spiderfied: null, + + unspiderfy: function () { + this._unspiderfy.apply(this, arguments); + }, + + _spiderfierOnAdd: function () { + this._map.on('click', this._unspiderfyWrapper, this); + + if (this._map.options.zoomAnimation) { + this._map.on('zoomstart', this._unspiderfyZoomStart, this); + } + //Browsers without zoomAnimation or a big zoom don't fire zoomstart + this._map.on('zoomend', this._noanimationUnspiderfy, this); + + if (!L.Browser.touch) { + this._map.getRenderer(this); + //Needs to happen in the pageload, not after, or animations don't work in webkit + // http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements + //Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable + } + }, + + _spiderfierOnRemove: function () { + this._map.off('click', this._unspiderfyWrapper, this); + this._map.off('zoomstart', this._unspiderfyZoomStart, this); + this._map.off('zoomanim', this._unspiderfyZoomAnim, this); + this._map.off('zoomend', this._noanimationUnspiderfy, this); + + //Ensure that markers are back where they should be + // Use no animation to avoid a sticky leaflet-cluster-anim class on mapPane + this._noanimationUnspiderfy(); + }, + + //On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated) + //This means we can define the animation they do rather than Markers doing an animation to their actual location + _unspiderfyZoomStart: function () { + if (!this._map) { //May have been removed from the map by a zoomEnd handler + return; + } + + this._map.on('zoomanim', this._unspiderfyZoomAnim, this); + }, + + _unspiderfyZoomAnim: function (zoomDetails) { + //Wait until the first zoomanim after the user has finished touch-zooming before running the animation + if (L.DomUtil.hasClass(this._map._mapPane, 'leaflet-touching')) { + return; + } + + this._map.off('zoomanim', this._unspiderfyZoomAnim, this); + this._unspiderfy(zoomDetails); + }, + + _unspiderfyWrapper: function () { + /// _unspiderfy but passes no arguments + this._unspiderfy(); + }, + + _unspiderfy: function (zoomDetails) { + if (this._spiderfied) { + this._spiderfied.unspiderfy(zoomDetails); + } + }, + + _noanimationUnspiderfy: function () { + if (this._spiderfied) { + this._spiderfied._noanimationUnspiderfy(); + } + }, + + //If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc + _unspiderfyLayer: function (layer) { + if (layer._spiderLeg) { + this._featureGroup.removeLayer(layer); + + if (layer.clusterShow) { + layer.clusterShow(); + } + //Position will be fixed up immediately in _animationUnspiderfy + if (layer.setZIndexOffset) { + layer.setZIndexOffset(0); + } + + this._map.removeLayer(layer._spiderLeg); + delete layer._spiderLeg; + } + } +}); + + +/** + * Adds 1 public method to MCG and 1 to L.Marker to facilitate changing + * markers' icon options and refreshing their icon and their parent clusters + * accordingly (case where their iconCreateFunction uses data of childMarkers + * to make up the cluster icon). + */ + + +L.MarkerClusterGroup.include({ + /** + * Updates the icon of all clusters which are parents of the given marker(s). + * In singleMarkerMode, also updates the given marker(s) icon. + * @param layers L.MarkerClusterGroup|L.LayerGroup|Array(L.Marker)|Map(L.Marker)| + * L.MarkerCluster|L.Marker (optional) list of markers (or single marker) whose parent + * clusters need to be updated. If not provided, retrieves all child markers of this. + * @returns {L.MarkerClusterGroup} + */ + refreshClusters: function (layers) { + if (!layers) { + layers = this._topClusterLevel.getAllChildMarkers(); + } else if (layers instanceof L.MarkerClusterGroup) { + layers = layers._topClusterLevel.getAllChildMarkers(); + } else if (layers instanceof L.LayerGroup) { + layers = layers._layers; + } else if (layers instanceof L.MarkerCluster) { + layers = layers.getAllChildMarkers(); + } else if (layers instanceof L.Marker) { + layers = [layers]; + } // else: must be an Array(L.Marker)|Map(L.Marker) + this._flagParentsIconsNeedUpdate(layers); + this._refreshClustersIcons(); + + // In case of singleMarkerMode, also re-draw the markers. + if (this.options.singleMarkerMode) { + this._refreshSingleMarkerModeMarkers(layers); + } + + return this; + }, + + /** + * Simply flags all parent clusters of the given markers as having a "dirty" icon. + * @param layers Array(L.Marker)|Map(L.Marker) list of markers. + * @private + */ + _flagParentsIconsNeedUpdate: function (layers) { + var id, parent; + + // Assumes layers is an Array or an Object whose prototype is non-enumerable. + for (id in layers) { + // Flag parent clusters' icon as "dirty", all the way up. + // Dumb process that flags multiple times upper parents, but still + // much more efficient than trying to be smart and make short lists, + // at least in the case of a hierarchy following a power law: + // http://jsperf.com/flag-nodes-in-power-hierarchy/2 + parent = layers[id].__parent; + while (parent) { + parent._iconNeedsUpdate = true; + parent = parent.__parent; + } + } + }, + + /** + * Re-draws the icon of the supplied markers. + * To be used in singleMarkerMode only. + * @param layers Array(L.Marker)|Map(L.Marker) list of markers. + * @private + */ + _refreshSingleMarkerModeMarkers: function (layers) { + var id, layer; + + for (id in layers) { + layer = layers[id]; + + // Make sure we do not override markers that do not belong to THIS group. + if (this.hasLayer(layer)) { + // Need to re-create the icon first, then re-draw the marker. + layer.setIcon(this._overrideMarkerIcon(layer)); + } + } + } +}); + +L.Marker.include({ + /** + * Updates the given options in the marker's icon and refreshes the marker. + * @param options map object of icon options. + * @param directlyRefreshClusters boolean (optional) true to trigger + * MCG.refreshClustersOf() right away with this single marker. + * @returns {L.Marker} + */ + refreshIconOptions: function (options, directlyRefreshClusters) { + var icon = this.options.icon; + + L.setOptions(icon, options); + + this.setIcon(icon); + + // Shortcut to refresh the associated MCG clusters right away. + // To be used when refreshing a single marker. + // Otherwise, better use MCG.refreshClusters() once at the end with + // the list of modified markers. + if (directlyRefreshClusters && this.__parent) { + this.__parent._group.refreshClusters(this); + } + + return this; + } +}); + + +}(window, document)); \ No newline at end of file diff --git a/simplemonitor/html/web_development/sites.js b/simplemonitor/html/web_development/sites.js new file mode 100644 index 00000000..2af73846 --- /dev/null +++ b/simplemonitor/html/web_development/sites.js @@ -0,0 +1,147 @@ +//An extract of address points from the LINZ bulk extract: http://www.linz.govt.nz/survey-titles/landonline-data/landonline-bde +//Should be this data set: http://data.linz.govt.nz/#/layer/779-nz-street-address-electoral/ +var addressPoints = [ + [45.564, -122.23, "mountpleasant-k12"], + [45.585, -122.328, "washougal-k12"], + [45.587, -122.402, "camas-lib"], + [45.591, -122.401, "camas-k12"], + [45.695, -121.283, "lyle-k12"], + [45.701, -121.889, "stevensoncarson-k12"], + [45.725, -122.633, "wsu-vancouver-bacc"], + [45.73, -121.488, "whitesalmon-k12"], + [45.738, -122.488, "hockinson-k12"], + [45.742, -120.218, "roosevelt-k12"], + [45.753, -120.9, "centerville-k12"], + [45.757, -121.65, "milla-k12"], + [45.784, -122.54, "battleground-k12"], + [45.797, -122.714, "ridgefield-k12"], + [45.815, -120.815, "goldendale-k12"], + [46.009, -122.839, "kalama-k12"], + [46.031, -118.388, "collegeplace-k12"], + [46.043, -118.672, "touchet-k12"], + [46.063, -118.344, "willow-k12"], + [46.065, -118.33, "wallawalla-k12"], + [46.08, -118.273, "wallawalla-ctc"], + [46.141, -122.907, "kelso-k12"], + [46.141, -122.938, "longview-lib"], + [46.142, -118.15, "dixie-k12"], + [46.142, -122.956, "longview-k12"], + [46.144, -122.935, "lowercolumbia-ctc"], + [46.154, -119.029, "finley-k12"], + [46.2, -119.01, "columbiawallawalla-k12"], + [46.201, -123.378, "wahkiakum-k12"], + [46.241, -119.143, "esd123-k12"], + [46.252, -119.124, "columbiabasin-ctc"], + [46.252, -119.738, "wsu-prosser-bacc"], + [46.267, -118.154, "waitsburg-k12"], + [46.271, -122.905, "castlerock-k12"], + [46.274, -119.294, "richland-k12"], + [46.28, -119.279, "richland-lib"], + [46.298, -118.32, "prescott-k12"], + [46.307, -124.038, "graysharbor-ilwaco-ctc "], + [46.309, -124.039, "oceanbeach-k12"], + [46.316, -117.975, "dayton-k12"], + [46.324, -122.741, "toutlelake-k12"], + [46.33, -119.263, "wsu-tricities-bacc"], + [46.339, -120.188, "granger-k12"], + [46.34, -117.053, "asotinanatone-k12"], + [46.376, -120.396, "heritage-pbacc"], + [46.378, -120.732, "mountadams-k12"], + [46.378, -123.802, "nasellegraysriver-k12"], + [46.412, -117.044, "asotincounty-lib"], + [46.413, -117.044, "clarkston-k12"], + [46.421, -117.068, "wallawalla-clarkston-ctc"], + [46.566, -117.131, "colton-k12"], + [46.576, -123.299, "peell-k12"], + [46.582, -122.718, "onalaska-k12"], + [46.585, -120.53, "cwu-yakima-bacc"], + [46.585, -120.53, "yakimavalley-ctc"], + [46.678, -123.741, "graysharbor-raymond-ctc"], + [46.686, -123.726, "raymond-k12"], + [46.714, -122.946, "centralia-ctc"], + [46.726, -122.983, "centralia-k12"], + [46.727, -117.189, "pullman-k12"], + [46.728, -120.695, "nachesvalley-k12"], + [46.729, -117.154, "wsu-puyallup-bacc"], + [46.73, -117.167, "spokane-pullman-ctc"], + [46.742, -119.893, "wahluke-k12"], + [46.983, -123.912, "hoquiam-k12"], + [46.984, -123.598, "montesano-k12"], + [46.986, -122.907, "washington-lib"], + [46.993, -120.524, "ellensburg-k12 "], + [46.995, -122.917, "esd113-k12"], + [47.0, -120.542, "cwu-bacc"], + [47.024, -122.93, "southpugetsound-ctc"], + [47.035, -122.895, "ospi-k12"], + [47.037, -122.888, "sbctc-olympia-ctc"], + [47.037, -122.888, "sbctc-sdc-1-ctc"], + [47.037, -122.888, "sbctc-sdc-2-ctc"], + [47.038, -122.817, "stmartins-pbacc"], + [47.038, -122.897, "councilofpresidents-bacc"], + [47.038, -122.897, "k20programoffice-other"], + [47.038, -122.897, "wsu-gov-bacc"], + [47.039, -122.9, "tvw-other"], + [47.041, -122.893, "wsu-energy-bacc"], + [47.044, -122.83, "southpugetsound-lacey-ctc"], + [47.046, -122.885, "olympia-k12"], + [47.048, -123.265, "mccleary-k12"], + [47.051, -122.824, "norththurston-k12"], + [47.067, -120.67, "thorp-k12"], + [47.18, -122.578, "steilacoomhistorical-k12"], + [47.185, -119.328, "bigbend-ctc"], + [47.185, -119.328, "cwu-moseslake-bacc"], + [47.186, -122.291, "puyallup-k12"], + [47.199, -123.056, "southside-k12"], + [47.201, -117.907, "lamont-k12"], + [47.245, -122.117, "muckleshoot-nwic"], + [47.245, -122.436, "wshm-other"], + [47.246, -122.525, "tacoma-ctc"], + [47.264, -122.481, "ups-pbacc"], + [47.297, -117.98, "sprague-k12"], + [47.312, -122.217, "auburn-k12"], + [47.395, -120.326, "wenatchee-k12"], + [47.474, -122.221, "renton-k12"], + [47.475, -122.28, "tukwila-k12"], + [47.476, -118.251, "harrington-k12"], + [47.479, -118.251, "enumclaw-k12"], + [47.489, -122.176, "renton-ctc"], + [47.49, -117.578, "cheney-k12"], + [47.571, -122.223, "mercerisland-k12"], + [47.573, -122.639, "bremerton-k12"], + [47.575, -122.635, "olympic-ctc"], + [47.576, -117.683, "medicallake-k12"], + [47.584, -122.148, "bellevue-ctc"], + [47.602, -120.654, "cascade-k12"], + [47.606, -122.332, "wsu-west-bacc"], + [47.675, -117.364, "spokane-ctc"], + [47.676, -117.465, "spokanefalls-ctc"], + [47.685, -117.239, "eastvalley-spk-k12"], + [47.686, -117.52, "greatnorthern-k12"], + [47.687, -119.108, "couleehartline-k12"], + [47.697, -122.904, "brinnon-k12"], + [47.707, -122.582, "chiefkitsap-k12"], + [47.823, -122.874, "quilcene-k12"], + [47.829, -117.601, "ninemilefalls-k12"], + [47.835, -120.02, "lakechelan-k12"], + [47.85, -121.989, "monroe-k12"], + [47.969, -117.35, "riverside-k12"], + [48.005, -122.197, "wsu-everett-bacc"], + [48.005, -122.41, "southwhidbey-k12"], + [48.534, -123.02, "sanjuanisland-k12"], + [48.535, -117.902, "spokane-colville-ctc"], + [48.539, -121.746, "upperskagit-lib"], + [48.543, -117.897, "colville-k12"], + [48.544, -117.904, "stevenscounty-lib"], + [48.546, -123.011, "uw-fhl-bacc"], + [48.572, -122.961, "shawisland-k12"], + [48.603, -118.056, "kettlefalls-k12"], + [48.608, -118.056, "kettlefalls-lib"], + [48.642, -118.727, "republic-k12"], + [48.648, -118.737, "spokane-republic-ctc"], + [48.696, -122.905, "orcasisland-lib "], + [48.912, -117.789, "northport-k12"], + [48.934, -119.438, "oroville-k12"], + [48.953, -122.423, "lynden-k12"], + [48.964, -122.308, "nooksackvalley-k12"], + [48.993371, -122.742043, "blaine-k12"], + ]; \ No newline at end of file diff --git a/simplemonitor/html/web_development/sites.txt b/simplemonitor/html/web_development/sites.txt new file mode 100644 index 00000000..e0fbca35 --- /dev/null +++ b/simplemonitor/html/web_development/sites.txt @@ -0,0 +1,382 @@ +[25.070.829,80.463.992,"quileute-k12"], +[45.564,-122.23,"mountpleasant-k12"], +[45.585,-122.328,"washougal-k12"], +[45.587,-122.402,"camas-lib"], +[45.591,-122.401,"camas-k12"], +[45.620.542,122.048.826,"skamania-k12"], +[45.625,-122.639,"waschoolfordeaf-k12"], +[45.63,-122.661,"fortvancouver-lib"], +[45.631,-122.648,"waschoolforblind-k12"], +[45.635,-122.653,"clark-ctc"], +[45.639,-122.606,"esd112-k12"], +[45.643,-122.626,"vancouver-k12"], +[45.644,-122.546,"evergreen-van-k12"], +[45.66,-120.963,"wishram-k12"], +[45.665.631,122.573.792,"glenwood-k12"], +[45.695,-121.283,"lyle-k12"], +[45.701,-121.889,"stevensoncarson-k12"], +[45.725,-122.633,"wsu-vancouver-bacc"], +[45.73,-121.488,"whitesalmon-k12"], +[45.738,-122.488,"hockinson-k12"], +[45.742,-120.218,"roosevelt-k12"], +[45.753,-120.9,"centerville-k12"], +[45.757,-121.65,"milla-k12"], +[45.784,-122.54,"battleground-k12"], +[45.797,-122.714,"ridgefield-k12"], +[45.815,-120.815,"goldendale-k12"], +[45.861,-122.664,"lacenter-k12"], +[45.862.121,120.805.837,"klickitat-k12"], +[45.904,-122.749,"woodland-k12"], +[45.939,-119.61,"paterson-k12"], +[45.948,-122.539,"greenmountain-k12"], +[45.989,-121.516,"troutlake-k12"], +[45.997854,-120.304794,"bickleton-k12"], +[46.009,-122.839,"kalama-k12"], +[46.031,-118.388,"collegeplace-k12"], +[46.043,-118.672,"touchet-k12"], +[46.063,-118.344,"willow-k12"], +[46.065,-118.33,"wallawalla-k12"], +[46.08,-118.273,"wallawalla-ctc"], +[46.141,-122.907,"kelso-k12"], +[46.141,-122.938,"longview-lib"], +[46.142,-118.15,"dixie-k12"], +[46.142,-122.956,"longview-k12"], +[46.144,-122.935,"lowercolumbia-ctc"], +[46.154,-119.029,"finley-k12"], +[46.2,-119.01,"columbiawallawalla-k12"], +[46.201,-123.378,"wahkiakum-k12"], +[46.241,-119.143,"esd123-k12"], +[46.252,-119.124,"columbiabasin-ctc"], +[46.252,-119.738,"wsu-prosser-bacc"], +[46.267,-118.154,"waitsburg-k12"], +[46.271,-122.905,"castlerock-k12"], +[46.274,-119.294,"richland-k12"], +[46.28,-119.279,"richland-lib"], +[46.298,-118.32,"prescott-k12"], +[46.307,-124.038,"graysharbor-ilwaco-ctc "], +[46.309,-124.039,"oceanbeach-k12"], +[46.316,-117.975,"dayton-k12"], +[46.324,-122.741,"toutlelake-k12"], +[46.33,-119.263,"wsu-tricities-bacc"], +[46.339,-120.188,"granger-k12"], +[46.34,-117.053,"asotinanatone-k12"], +[46.376,-120.396,"heritage-pbacc"], +[46.378,-120.732,"mountadams-k12"], +[46.378,-123.802,"nasellegraysriver-k12"], +[46.412,-117.044,"asotincounty-lib"], +[46.413,-117.044,"clarkston-k12"], +[46.421,-117.068,"wallawalla-clarkston-ctc"], +[46.437,-120.421,"wapato-k12"], +[46.442,-118.718,"star-k12"], +[46.444.144,108.544.659,"rainier-k12"], +[46.471,-117.6,"dennyashby-lib"], +[46.473,-117.597,"pomeroy-k12"], +[46.494.123,122.938.824,"evaline-k12"], +[46.517.867,11.812.364,"starbuck-k12"], +[46.533,-122.486,"mossyrock-k12"], +[46.536,-121.93,"whitepass-k12"], +[46.550166,-123.132762,"boistfort-k12"], +[46.553.364,122.282.008,"morton-k12"], +[46.566,-117.131,"colton-k12"], +[46.576,-123.299,"peell-k12"], +[46.582,-122.718,"onalaska-k12"], +[46.585,-120.53,"cwu-yakima-bacc"], +[46.585,-120.53,"yakimavalley-ctc"], +[46.6,-120.51,"esd105-k12"], +[46.624.826,123.653.091,"willapavalley-k12"], +[46.631243,-123.056327,"adna-k12"], +[46.643.937,118.552.545,"kahlotus-k12"], +[46.65,-122.949,"chehalis-k12"], +[46.662,-123.792,"southbend-k12"], +[46.675,-120.713,"highland-k12"], +[46.678,-123.741,"graysharbor-raymond-ctc"], +[46.686,-123.726,"raymond-k12"], +[46.714,-122.946,"centralia-ctc"], +[46.726,-122.983,"centralia-k12"], +[46.727,-117.189,"pullman-k12"], +[46.728,-120.695,"nachesvalley-k12"], +[46.729,-117.154,"wsu-puyallup-bacc"], +[46.73,-117.167,"spokane-pullman-ctc"], +[46.742,-119.893,"wahluke-k12"], +[46.753,-118.31,"washtucna-k12"], +[46.801,-123.045,"rochester-k12"], +[46.814,-117.882,"lacrosse-k12"], +[46.841,-123.241,"oakville-k12"], +[46.859.079,12.284.935,"tenino-k12"], +[46.861,-124.1,"ocosta-k12"], +[46.87069,-122.26844,"eatonville-k12"], +[46.892,-117.361,"colfax-k12 "], +[46.911,-119.627,"royal-k12"], +[46.913,-117.071,"palouse-k12"], +[46.914807,-118.0492,"benge-k12"], +[46.949,-122.612,"yelmcommunity-k12"], +[46.955.372,123.772.414,"northriver-k12"], +[46.956,-123.804,"graysharbor-ctc"], +[46.974,-118.617,"lind-k12"], +[46.978,-123.816,"aberdeen-k12"], +[46.983,-123.912,"hoquiam-k12"], +[46.984,-123.598,"montesano-k12"], +[46.986,-122.907,"washington-lib"], +[46.993,-120.524,"ellensburg-k12 "], +[46.995,-122.917,"esd113-k12"], +[47.0,-120.542,"cwu-bacc"], +[47.002,-122.674,"nisqually-nwic"], +[47.003,-123.48,"satsop-k12"], +[47.005.417,123.406.528,"elma-k12"], +[47.005.417,123.406.528,"marymknight-k12"], +[47.006,-117.351,"steptoe-k12"], +[47.007,-122.91,"tumwater-k12"], +[47.014,-117.14,"garfield-k12"], +[47.022,-124.159,"northbeach-k12"], +[47.024,-122.93,"southpugetsound-ctc"], +[47.035,-122.895,"ospi-k12"], +[47.037,-122.888,"sbctc-olympia-ctc"], +[47.037,-122.888,"sbctc-sdc-1-ctc"], +[47.037,-122.888,"sbctc-sdc-2-ctc"], +[47.038,-122.817,"stmartins-pbacc"], +[47.038,-122.897,"councilofpresidents-bacc"], +[47.038,-122.897,"k20programoffice-other"], +[47.038,-122.897,"wsu-gov-bacc"], +[47.039,-122.9,"tvw-other"], +[47.041,-122.893,"wsu-energy-bacc"], +[47.044,-122.83,"southpugetsound-lacey-ctc"], +[47.046,-122.885,"olympia-k12"], +[47.048,-123.265,"mccleary-k12"], +[47.051,-122.824,"norththurston-k12"], +[47.067,-120.67,"thorp-k12"], +[47.074,-122.974,"tesc-bacc"], +[47.081,-123.019,"griffin-k12 "], +[47.081.355,122.054.271,"carbonado-k12"], +[47.089.059,117.582.133,"stjohn-k12"], +[47.092.992,122.205.457,"orting-k12"], +[47.097,-122.425,"bethel-k12"], +[47.117,-123.77,"wishkahvalley-k12"], +[47.126,-117.241,"oakesdale-k12"], +[47.126,-118.371,"ritzville-k12"], +[47.145,-122.444,"plu-pbacc"], +[47.158.989,122.519.555,"cloverpark-k12"], +[47.165.263,122.025.522,"whiteriver-k12"], +[47.171,-122.571,"cwu-steilacoom-bacc "], +[47.171,-122.571,"pierce-fortsteilacoom-ctc"], +[47.171,-122.571,"pierce-puyallup-ctc"], +[47.175,-122.5,"cloverpark-ctc"], +[47.18,-122.578,"steilacoomhistorical-k12"], +[47.185,-119.328,"bigbend-ctc"], +[47.185,-119.328,"cwu-moseslake-bacc"], +[47.186,-122.291,"puyallup-k12"], +[47.199,-123.056,"southside-k12"], +[47.201,-117.907,"lamont-k12"], +[47.208,-123.101,"shelton-k12 "], +[47.209.691,122.217.072,"chiefleschi-k12"], +[47.219.499,122.236.127,"sumner-k12"], +[47.222,-122.552,"universityplace-k12"], +[47.224,-117.067,"tekoa-k12"], +[47.23,-119.848,"quincy-k12"], +[47.231,-117.369,"rosalia-k12"], +[47.237,-121.177,"easton-k12"], +[47.245,-122.117,"muckleshoot-nwic"], +[47.245,-122.436,"wshm-other"], +[47.246,-122.525,"tacoma-ctc"], +[47.246.453,122.263.803,"dieringer-k12"], +[47.248,-122.308,"fife-k12"], +[47.252418,-122.444926,"bates-ctc"], +[47.256.375,12.244.582,"tacoma-k12"], +[47.257,-122.446,"tacoma-k12"], +[47.264,-122.481,"ups-pbacc"], +[47.297,-117.98,"sprague-k12"], +[47.312,-122.217,"auburn-k12"], +[47.313,-122.18,"greenriver-ctc"], +[47.317.254,122.228.761,"muckleshoot-k12"], +[47.320.271,122.311.147,"federalway-k12"], +[47.326,-119.55,"ephrata-k12"], +[47.33,-118.688,"odessa-k12"], +[47.344.433,124.288.384,"taholah-k12"], +[47.349,-122.324,"highline-mast-ctc"], +[47.371.473,122.181.367,"kent-k12"], +[47.382,-117.319,"liberty-k12"], +[47.387,-119.497,"soaplake"], +[47.387,-122.62,"peninsula-k12"], +[47.388.803,122.099.999,"tahoma-k12"], +[47.39,-122.3,"cwu-desmoines-bacc"], +[47.39,-122.3,"highline-ctc"], +[47.395,-120.326,"wenatchee-k12"], +[47.415,-122.844,"northmason-k12"], +[47.416.407,119.916.071,"palisades-k12"], +[47.426,-119.122,"wilsoncreek-k12"], +[47.427581,122.452432,"vashonisland-k12"], +[47.431,-120.334,"cwu-wenatchee-bacc"], +[47.431,-120.334,"wenatcheevalley-ctc"], +[47.441,-120.353,"wsu-wenatchee-bacc"], +[47.46,-123.896,"lakequinault-k12"], +[47.462.276,122.337.957,"highline-k12"], +[47.463,-120.328,"esd171-k12"], +[47.473,-122.235,"esd121-k12"], +[47.474,-122.221,"renton-k12"], +[47.475,-122.28,"tukwila-k12"], +[47.476,-118.251,"harrington-k12"], +[47.479,-118.251,"enumclaw-k12"], +[47.489,-122.176,"renton-ctc"], +[47.49,-117.578,"cheney-k12"], +[47.491,-117.578,"ewu-bacc"], +[47.516,-120.477,"cashmere-k12"], +[47.526,-122.627,"southkitsap-k12"], +[47.528.629,121.828.463,"snoqualmievalley-k12"], +[47.536.868,122.042.401,"issaquah-k12"], +[47.553,-124.355,"queetsclearwater-k12"], +[47.553,-124.355,"quillayutevalley-k12"], +[47.566,-122.669,"esd114-k12"], +[47.571,-122.223,"mercerisland-k12"], +[47.573,-122.639,"bremerton-k12"], +[47.575,-122.635,"olympic-ctc"], +[47.576,-117.683,"medicallake-k12"], +[47.584,-122.148,"bellevue-ctc"], +[47.602,-120.654,"cascade-k12"], +[47.606,-122.332,"wsu-west-bacc"], +[47.609,-122.176,"bellevue-k12"], +[47.610.361,122.033.738,"cwu-sammamish-bacc"], +[47.615,-117.369,"esd101-k12"], +[47.619,-117.368,"ksps-other"], +[47.622,-122.256,"lakewashington-ctc"], +[47.624,-122.348,"kcts-other"], +[47.637,-122.525,"bainbridgeisland-k12"], +[47.641,-120.218,"orondo-k12"], +[47.645,-120.07,"waterville-k12"], +[47.649,-118.151,"davenport-k12"], +[47.649,-122.362,"spu-pbacc"], +[47.652,-122.699,"centralkitsap-k12"], +[47.659,-117.416,"spokane-k12"], +[47.661,-117.404,"wsu-riverpoint-bacc"], +[47.664,-120.226,"entiat-k12"], +[47.664.507,120.225.976,"entiat-k12"], +[47.667,-117.876,"reardanedwall-k12"], +[47.670.996,122.123.489,"lakewashington-k12"], +[47.674,-117.308,"westvalley-spk-k12"], +[47.674,-117.308,"westvalley-yak-k12"], +[47.675,-117.364,"spokane-ctc"], +[47.676,-117.465,"spokanefalls-ctc"], +[47.685,-117.239,"eastvalley-spk-k12"], +[47.686,-117.52,"greatnorthern-k12"], +[47.687,-119.108,"couleehartline-k12"], +[47.697,-122.904,"brinnon-k12"], +[47.707,-122.582,"chiefkitsap-k12"], +[47.708,-118.941,"almira-k12"], +[47.710.507,121.359.074,"skykomish-k12"], +[47.728,-117.314,"orchardprairie-k12"], +[47.729,-122.627,"northkitsap-k12"], +[47.735.639,121.952.554,"riverview-k12"], +[47.749,-122.359,"shoreline-ctc"], +[47.753,-118.705,"wilbur-k12"], +[47.755,-118.52,"creston-k12"], +[47.761,-122.191,"cascadia-ctc"], +[47.764.104,122.328.885,"shoreline-k12"], +[47.773,-117.376,"mead-k12"], +[47.777,-122.19,"northshore-k12"], +[47.812,-119.637,"mansfield-k12"], +[47.813.593,122.326.291,"edmonds-k12"], +[47.817,-122.327,"edmonds-ctc"], +[47.821,-121.555,"index-k12"], +[47.823,-122.874,"quilcene-k12"], +[47.829,-117.601,"ninemilefalls-k12"], +[47.835,-120.02,"lakechelan-k12"], +[47.85,-121.989,"monroe-k12"], +[47.853,-122.569,"portgamble-nwic"], +[47.866.348,121.815.116,"sultan-k12"], +[47.878,-122.224,"wsu-snohomish-bacc"], +[47.891,-120.153,"manson-k12"], +[47.898.735,117.964.874,"wellpinit-k12"], +[47.922,-122.263,"wsipc-k12"], +[47.928.314,122.312.824,"mukilteo-k12"], +[47.941.407,122.204.162,"everett-k12"], +[47.954,-117.463,"deerpark-k12"], +[47.962,-118.986,"grandcouleedam-k12"], +[47.969,-117.35,"riverside-k12"], +[48.005,-122.197,"wsu-everett-bacc"], +[48.005,-122.41,"southwhidbey-k12"], +[48.007,-119.675,"bridgeport-k12"], +[48.007,-122.205,"everett-ctc"], +[48.007,-122.205,"wwu-bacc"], +[48.012,-122.777,"chimacum-k12"], +[48.017.556,122.064.259,"lakestevens-k12"], +[48.033,-122.773,"jeffersoncounty-lib"], +[48.052,-119.907,"pateros-k12"], +[48.06,-117.746,"marywalker-k12"], +[48.061,-117.631,"loonlake-k12"], +[48.063,-117.633,"onioncreek-k12"], +[48.064,-122.187,"snoisle-lib"], +[48.066,-122.281,"tulalip-nwic"], +[48.068,-122.173,"marysville-k12"], +[48.078,-118.687,"keller-k12"], +[48.086,-121.964,"granitefalls-k12"], +[48.094,-119.787,"brewster-k12"], +[48.102,-123.431,"portangeles-k12"], +[48.106,-123.439,"northolympic-lib"], +[48.115.371,123.414.659,"uw-telemed-olympic-bacc"], +[48.117,-122.769,"porttownsend-k12"], +[48.134,-122.765,"peninsula-ctc"], +[48.134,-122.765,"peninsula-porttownsend-ctc"], +[48.136,-123.746,"crescent-k12"], +[48.149.256,122.207.884,"lakewood-k12"], +[48.167,-118.975,"nespelem-k12"], +[48.177,-117.042,"newport-k12"], +[48.177,-117.042,"spokane-newport-ctc"], +[48.195.301,122.548.945,"lopezisland-lib"], +[48.196,-122.122,"arlington-k12"], +[48.216.316,122.679.436,"coupeville-k12"], +[48.247.709,121.601.233,"darrington-k12"], +[48.253.248,124.256.933,"capeflattery-k12"], +[48.278,-117.699,"chewelah-lib"], +[48.28,-117.708,"chewelah-k12"], +[48.289,-122.634,"skagitvalley-oakharbor-ctc"], +[48.297.164,122.641.866,"oakharbor-k12"], +[48.301,-118.203,"spokane-inchelium-ctc"], +[48.304.301,118.040.678,"evergreen-stevensco-k12"], +[48.337,-117.297,"cusick-k12"], +[48.340.148,118.231.894,"inchelium-k12"], +[48.365,-119.585,"okanogan-k12"], +[48.392,-122.489,"laconner-lib"], +[48.394,-122.49,"laconner-k12"], +[48.397,-122.506,"swinomish-nwic"], +[48.411,-119.534,"omak-k12"], +[48.427.235,12.231.133,"conway-k12"], +[48.436,-122.311,"skagitvalley-ctc"], +[48.439,-122.387,"wsu-mountvernon-bacc"], +[48.441,-120.187,"methowvalley-k12"], +[48.478401,-122.336662,"burlingtonedison-k12"], +[48.503,-122.228,"sedrowoolley-k12"], +[48.504,-122.618,"anacortes-k12"], +[48.509,-122.607,"esd189-k12"], +[48.522,-122.909,"lopezisland-lib "], +[48.534,-123.02,"sanjuanisland-k12"], +[48.535,-117.902,"spokane-colville-ctc"], +[48.539,-121.746,"upperskagit-lib"], +[48.543,-117.897,"colville-k12"], +[48.544,-117.904,"stevenscounty-lib"], +[48.546,-123.011,"uw-fhl-bacc"], +[48.572,-122.961,"shawisland-k12"], +[48.603,-118.056,"kettlefalls-k12"], +[48.608,-118.056,"kettlefalls-lib"], +[48.642,-118.727,"republic-k12"], +[48.648,-118.737,"spokane-republic-ctc"], +[48.696,-122.905,"orcasisland-lib "], +[48.703,-119.441,"tonasket-k12"], +[48.739.138,117.422.605,"selkirk-k12"], +[48.76,-122.487,"bellingham-k12"], +[48.764.218,122.468.178,"meridian-k12"], +[48.766,-122.512,"bellingham-ctc"], +[48.781.684,122.327.696,"cwu-lynnwood-bacc"], +[48.792.435,122.094.207,"snohomish-k12"], +[48.795,-122.493,"whatcom-ctc"], +[48.795,-122.614,"lummi-nwic"], +[48.807.584,123.108.212,"sequim-k12"], +[48.825.696,122.220.796,"mountbaker-k12"], +[48.835.579,117.836.486,"summitvalley-k12"], +[48.841.063,122.339.034,"mountvernon-k12"], +[48.852,-122.593,"ferndale-k12"], +[48.853.166,121.757.264,"concrete-k12"], +[48.867.375,11.820.117,"orient-k12"], +[48.874,-118.602,"curlew-k12"], +[48.912,-117.789,"northport-k12"], +[48.934,-119.438,"oroville-k12"], +[48.953,-122.423,"lynden-k12"], +[48.964,-122.308,"nooksackvalley-k12"], +[48.993371,-122.742043,"blaine-k12"], \ No newline at end of file diff --git a/simplemonitor/html/web_development/style.css b/simplemonitor/html/web_development/style.css new file mode 100644 index 00000000..0f290b61 --- /dev/null +++ b/simplemonitor/html/web_development/style.css @@ -0,0 +1,28 @@ +#map { + width: auto; + height: 1100px; + border: 1px solid #ccc; +} + +#progress { + display: none; + position: absolute; + z-index: 1000; + left: 400px; + top: 300px; + width: 200px; + height: 20px; + margin-top: -20px; + margin-left: -100px; + background-color: #fff; + background-color: rgba(255, 255, 255, 0.7); + border-radius: 4px; + padding: 2px; +} + +#progress-bar { + width: 0; + height: 100%; + background-color: #76A6FC; + border-radius: 4px; +} \ No newline at end of file From 9dc316a5660e1b5810e723f88c20e8ccbb8daf73 Mon Sep 17 00:00:00 2001 From: danieldh206 Date: Thu, 25 Mar 2021 09:33:31 -0700 Subject: [PATCH 3/4] . --- simplemonitor/html/web_development/index_cluster.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simplemonitor/html/web_development/index_cluster.html b/simplemonitor/html/web_development/index_cluster.html index aa55a957..ba58442f 100644 --- a/simplemonitor/html/web_development/index_cluster.html +++ b/simplemonitor/html/web_development/index_cluster.html @@ -2,8 +2,8 @@ - - + + From 1fe22528e12eca9bde0b825a5e421bd3c4698b83 Mon Sep 17 00:00:00 2001 From: danieldh206 Date: Thu, 25 Mar 2021 15:19:39 -0700 Subject: [PATCH 4/4] green red pin --- .../web_development/MarkerCluster.Default.css | 8 +- .../html/web_development/index_cluster.html | 20 +++-- .../html/web_development/test_sites.js | 86 +++++++++++++++++++ 3 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 simplemonitor/html/web_development/test_sites.js diff --git a/simplemonitor/html/web_development/MarkerCluster.Default.css b/simplemonitor/html/web_development/MarkerCluster.Default.css index da330ca8..182f68b6 100644 --- a/simplemonitor/html/web_development/MarkerCluster.Default.css +++ b/simplemonitor/html/web_development/MarkerCluster.Default.css @@ -6,17 +6,17 @@ } .marker-cluster-medium { - background-color: rgba(241, 211, 87, 0.6); + background-color: rgba(181, 226, 140, 0.6); } .marker-cluster-medium div { - background-color: rgba(240, 194, 12, 0.6); + background-color: rgba(110, 204, 57, 0.6); } .marker-cluster-large { - background-color: rgba(253, 156, 115, 0.6); + background-color: rgba(181, 226, 140, 0.6); } .marker-cluster-large div { - background-color: rgba(241, 128, 23, 0.6); + background-color: rgba(110, 204, 57, 0.6); } /* IE 6-8 fallback colors */ diff --git a/simplemonitor/html/web_development/index_cluster.html b/simplemonitor/html/web_development/index_cluster.html index ba58442f..f288e869 100644 --- a/simplemonitor/html/web_development/index_cluster.html +++ b/simplemonitor/html/web_development/index_cluster.html @@ -2,15 +2,15 @@ - - + + - + @@ -44,10 +44,16 @@ for (var i = 0; i < addressPoints.length; i++) { var a = addressPoints[i]; - var title = a[2]; - var marker = L.marker(new L.LatLng(a[0], a[1]),{icon: markerIcon1}, { title: title }); - marker.bindPopup(title); - markers.addLayer(marker); + var title = a[3]; + if (a[2] == "up") { + var marker = L.marker(new L.LatLng(a[0], a[1]),{icon: markerIcon1}, { title: title }); + marker.bindPopup(title); + markers.addLayer(marker); + } else { + var marker = L.marker(new L.LatLng(a[0], a[1]),{icon: markerIcon2}, { title: title }); + marker.bindPopup(title); + markers.addLayer(marker); + } } map.addLayer(markers); diff --git a/simplemonitor/html/web_development/test_sites.js b/simplemonitor/html/web_development/test_sites.js new file mode 100644 index 00000000..1714a826 --- /dev/null +++ b/simplemonitor/html/web_development/test_sites.js @@ -0,0 +1,86 @@ +//An extract of address points from the LINZ bulk extract: http://www.linz.govt.nz/survey-titles/landonline-data/landonline-bde +//Should be this data set: http://data.linz.govt.nz/#/layer/779-nz-street-address-electoral/ +var addressPoints = [ + [46.9784, -123.816528, "up", "Aberdeen School District"], + [46.631243, -123.056327, "up", "Adna School District"], + [47.70752, -118.940926, "up", "Almira School District"], + [48.504402, -122.617639, "up", "Anacortes School District"], + [48.195549, -122.122384, "up", "Arlington School District"], + [46.412363, -117.043854, "up", "Asotin County Library"], + [46.34045, -117.052155, "up", "Asotin-Anatone School District"], + [47.311823, -122.217148, "up", "Auburn School District"], + [47.636244, -122.523954, "down", "Bainbridge Island School District"], + [47.251967, -122.446493, "down", "Bates Technical College"], + [47.188512, -122.466768, "down", "Bates Technical College South"], + [45.731551, -122.560421, "down", "Battle Ground School District"], + [47.584567, -122.148542, "down", "Bellevue College"], + [47.609185, -122.175862, "down", "Bellevue School District"], + [48.759351, -122.487003, "down", "Bellingham School District"], + [48.76628, -122.510107, "down", "Bellingham Technical College"], + [46.910246, -118.100453, "down", "Benge School District"], + [47.09463, -122.424469, "down", "Bethel School District"], + [45.997856, -120.304793, "down", "Bickleton School District"], + [47.186178, -119.328335, "down", "Big Bend Community College"], + [48.992293, -122.73828, "down", "Blaine School District"], + [46.550443, -123.131929, "down", "Boistfort School District"], + [47.573349, -122.638573, "down", "Bremerton School District"], + [48.094357, -119.787119, "down", "Brewster School District"], + [48.006785, -119.676184, "down", "Bridgeport School District"], + [47.696868, -122.904028, "down", "Brinnon School District"], + [48.477951, -122.338306, "down", "Burlington-Edison School District"], + [47.388419, -122.305146, "up", "CWU Des Moines (Highline CC)"], + [47.000023, -120.544309, "up", "CWU Ellensburg"], + [47.81684, -122.327696, "up", "CWU Lynnwood Center (Edmonds CC)"], + [47.186178, -119.328335, "up", "CWU Moses Lake (Big Bend CC)"], + [47.172695, -122.571471, "up", "CWU Pierce (Steilacoom)"], + [47.610295, -122.033849, "up", "CWU Sammamish"], + [47.430814, -120.333418, "up", "CWU Wenatchee (WWCC)"], + [46.585015, -120.530154, "up", "CWU Yakima (YVCC)"], + [45.587075, -122.402288, "up", "Camas Public Library"], + [45.591506, -122.40201, "up", "Camas School District"], + [48.265961, -124.332786, "up", "Cape Flattery School District"], + [47.081006, -122.054128, "up", "Carbonado School District"], + [47.602556, -120.654967, "up", "Cascade School District"], + [47.76087, -122.191349, "up", "Cascadia Community College"], + [47.514667, -120.476761, "up", "Cashmere School District"], + [46.281779, -122.917314, "up", "Castle Rock School District"], + [45.752851, -120.899691, "up", "Centerville School District"], + [47.651581, -122.699028, "up", "Central Kitsap School District"], + [46.714716, -122.961749, "up", "Centralia Community College"], + [46.725975, -122.983141, "up", "Centralia School District"], + [46.64909, -122.948924, "up", "Chehalis School District"], + [47.503829, -117.575742, "up", "Cheney School District"], + [48.277534, -117.704091, "up", "Chewelah Public Library"], + [48.280847, -117.708481, "up", "Chewelah School District"], + [47.706248, -122.580523, "up", "Chief Kitsap Academy"], + [47.211859, -122.354866, "up", "Chief Leschi School District"], + [48.013442, -122.778194, "up", "Chimacum School District"], + [45.634682, -122.652496, "up", "Clark College"], + [46.411157, -117.05812, "up", "Clarkston School District"], + [47.158989, -122.519556, "up", "Clover Park School District"], + [47.174874, -122.497828, "up", "Clover Park Technical College"], + [46.891895, -117.361433, "up", "Colfax School District"], + [46.030738, -118.386577, "up", "College Place School District"], + [46.566797, -117.130206, "up", "Colton School District"], + [46.252334, -119.121811, "up", "Columbia Basin Community College"], + [46.20035, -119.009915, "up", "Columbia Walla Walla School District"], + [48.159072, -118.074943, "up", "Columbia/Stevens School District"], + [48.547241, -117.875682, "up", "Colville School District"], + [48.532049, -121.757174, "up", "Concrete School District"], + [48.340215, -122.319458, "up", "Conway School District"], + [47.611018, -119.285007, "up", "Coulee-Hartline School District"], + [47.038593, -122.89687, "up", "Council Of Presidents"], + [48.207376, -122.685908, "up", "Coupeville School District"], + [48.136023, -123.745679, "up", "Crescent School District"], + [47.755021, -118.519973, "up", "Creston School District"], + [48.873944, -118.602107, "up", "Curlew School District"], + [48.337464, -117.299015, "up", "Cusick School District"], + [48.247709, -121.601233, "up", "Darrington School District"], + [47.651473, -118.149993, "up", "Davenport School District"], + [46.31676, -117.974656, "up", "Dayton School District"], + [47.954119, -117.462608, "up", "Deer Park School District"], + [46.470804, -117.599713, "up", "Denny Ashby Memorial Library"], + [47.248809, -122.162839, "up", "Dieringer School District"], + [46.141921, -118.149751, "up", "Dixie School District"], + [47.684665, -117.238363, "East Valley School District-Spokane"], + ]; \ No newline at end of file