diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..99559db --- /dev/null +++ b/AUTHORS @@ -0,0 +1,6 @@ +# Names should be added to this file as: +# Name or Organization +# The email address is not required for organizations. + +Google Inc. +Nelson Antunes diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..9edd92c --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,6 @@ +# Names should be added to this file as: +# Name + +Luke Mahé +Brendan Kenny +Nelson Antunes diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5c304d1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c9c5fb5 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +Data Layer Clusterer – A Google Maps JavaScript API utility library +============== + +A Google Maps JavaScript API v3 library to create and manage per-zoom-level clusters for large amounts of markers. + +Based on [Marker Clusterer – A Google Maps JavaScript API utility library](https://github.com/googlemaps/js-marker-clusterer) by Luke Mehe (Google Inc.). + +## License + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..5134e9a --- /dev/null +++ b/bower.json @@ -0,0 +1,27 @@ +{ + "name": "data-layer-clusterer", + "version": "0.7.0", + "homepage": "https://github.com/nantunes/data-layer-clusterer", + "authors": [ + "Nelson Antunes" + ], + "description": "The library creates and manages per-zoom-level clusters large amounts of data layer features. Google API v3.", + "main": "src/datalayerclusterer.js", + "keywords": [ + "google", + "maps", + "data", + "layer", + "marker", + "cluster", + "clusterer", + "javascript", + "js", + "api", + "v3" + ], + "license": "Apache 2.0", + "ignore": [ + "**/.*" + ] +} diff --git a/src/datalayerclusterer.js b/src/datalayerclusterer.js new file mode 100755 index 0000000..6fe7af6 --- /dev/null +++ b/src/datalayerclusterer.js @@ -0,0 +1,1092 @@ +/* globals google */ +/* exports DataLayerClusterer */ +'use strict'; + +/** + * @name DataLayerClusterer for Google Maps v3 + * @version version 0.7 + * @author Nelson Antunes + * + * The library creates and manages per-zoom-level clusters for large amounts of + * data layer features. + * + * Based on MarkerClusterer by Luke Mehe. + */ + +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * A Data Layer Clusterer that clusters point features. + * + * @param {google.maps.Map} map The Google map to attach to. + * @param {Object=} opt_options support the following options: + * 'map': (google.maps.Map) The Google map to attach to. + * 'gridSize': (number) The grid size of a cluster in pixels. + * 'maxZoom': (number) The maximum zoom level that a feature can be part of a + * cluster. + * 'zoomOnClick': (boolean) Whether the default behaviour of clicking on a + * cluster is to zoom into it. + * 'averageCenter': (boolean) Wether the center of each cluster should be + * the average of all features in the cluster. + * 'minimumClusterSize': (number) The minimum number of features to be in a + * cluster before the features are hidden and a count + * is shown. + * 'styles': (object) An object that has style properties: + * 'url': (string) The image url. + * 'height': (number) The image height. + * 'width': (number) The image width. + * 'anchor': (Array) The anchor position of the label text. + * 'textColor': (string) The text color. + * 'textSize': (number) The text size. + * 'backgroundPosition': (string) The position of the backgound x, y. + * @constructor + * @extends google.maps.OverlayView + */ +function DataLayerClusterer(opt_options) { + DataLayerClusterer.extend(DataLayerClusterer, google.maps.OverlayView); + + /** + * @type {Array.} + */ + this.clusters_ = []; + + this.sizes = [53, 56, 66, 78, 90]; + + /** + * @private + */ + this.styles_ = []; + + /** + * @type {boolean} + * @private + */ + this.ready_ = false; + + var options = opt_options || {}; + + var map = options.map || null; + + this.gridSize_ = options.gridSize || 60; + this.minClusterSize_ = options.minimumClusterSize || 2; + this.maxZoom_ = options.maxZoom || null; + + this.styles_ = options.styles || []; + + this.imagePath_ = options.imagePath || DataLayerClusterer.MARKER_CLUSTER_IMAGE_PATH_; + this.imageExtension_ = options.imageExtension || DataLayerClusterer.MARKER_CLUSTER_IMAGE_EXTENSION_; + + this.zoomOnClick_ = true; + if (options.zoomOnClick !== undefined) { + this.zoomOnClick_ = options.zoomOnClick; + } + + this.averageCenter_ = true; + if (options.averageCenter !== undefined) { + this.averageCenter_ = options.averageCenter; + } + + this.setupStyles_(); + + this._data_layer = new google.maps.Data(); + this._data_layer.setStyle(DataLayerClusterer.HIDDEN_FEATURE); + + if(map !== null) { + this.setMap(this.map_); + } +} + +/* ---- Constants ---- */ + +DataLayerClusterer.VISIBLE_FEATURE = { + visible: true +}; + +DataLayerClusterer.HIDDEN_FEATURE = { + visible: false +}; + + +/* ---- Public methods ---- */ + +/** + * Returns the number of clusters in the clusterer. + * + * @return {number} The number of clusters. + */ +DataLayerClusterer.prototype.getTotalClusters = function() { + return this.clusters_.length; +}; + +/** + * Extends a bounds object by the grid size. + * + * @param {google.maps.LatLngBounds} bounds The bounds to extend. + * @return {google.maps.LatLngBounds} The extended bounds. + */ +DataLayerClusterer.prototype.getExtendedBounds = function(bounds) { + var projection = this.getProjection(); + + // Turn the bounds into latlng. + var tr = new google.maps.LatLng(bounds.getNorthEast().lat(), + bounds.getNorthEast().lng()); + var bl = new google.maps.LatLng(bounds.getSouthWest().lat(), + bounds.getSouthWest().lng()); + + // Convert the points to pixels and the extend out by the grid size. + var trPix = projection.fromLatLngToDivPixel(tr); + trPix.x += this.gridSize_; + trPix.y -= this.gridSize_; + + var blPix = projection.fromLatLngToDivPixel(bl); + blPix.x -= this.gridSize_; + blPix.y += this.gridSize_; + + // Convert the pixel points back to LatLng + var ne = projection.fromDivPixelToLatLng(trPix); + var sw = projection.fromDivPixelToLatLng(blPix); + + // Extend the bounds to contain the new bounds. + bounds.extend(ne); + bounds.extend(sw); + + return bounds; +}; + +/** + * Redraws the clusters. + */ +DataLayerClusterer.prototype.redraw = function() { + var oldClusters = this.clusters_.slice(); + this.clusters_.length = 0; + + this.createClusters_(); + + // Remove the old clusters. + // Do it in a timeout so the other clusters have been drawn first. + window.requestAnimationFrame(function() { + var old_size = oldClusters.length; + for (var i = 0; i !== old_size; ++i) { + oldClusters[i].remove(); + } + }); +}; + + +/* ---- Options GET & SET ---- */ + +/** + * Whether zoom on click is set. + * + * @return {boolean} True if zoomOnClick_ is set. + */ +DataLayerClusterer.prototype.isZoomOnClick = function() { + return this.zoomOnClick_; +}; + +/** + * Whether average center is set. + * + * @return {boolean} True if averageCenter_ is set. + */ +DataLayerClusterer.prototype.isAverageCenter = function() { + return this.averageCenter_; +}; + +/** + * Sets the max zoom for the clusterer. + * + * @param {number} maxZoom The max zoom level. + */ +DataLayerClusterer.prototype.setMaxZoom = function(maxZoom) { + this.maxZoom_ = maxZoom; +}; + +/** + * Gets the max zoom for the clusterer. + * + * @return {number} The max zoom level. + */ +DataLayerClusterer.prototype.getMaxZoom = function() { + return this.maxZoom_; +}; + +/** + * Returns the size of the grid. + * + * @return {number} The grid size. + */ +DataLayerClusterer.prototype.getGridSize = function() { + return this.gridSize_; +}; + +/** + * Sets the size of the grid. + * + * @param {number} size The grid size. + */ +DataLayerClusterer.prototype.setGridSize = function(size) { + this.gridSize_ = size; +}; + +/** + * Returns the min cluster size. + * + * @return {number} The grid size. + */ +DataLayerClusterer.prototype.getMinClusterSize = function() { + return this.minClusterSize_; +}; + +/** + * Sets the min cluster size. + * + * @param {number} size The grid size. + */ +DataLayerClusterer.prototype.setMinClusterSize = function(size) { + this.minClusterSize_ = size; +}; + + +/* ---- google.maps.Data interface ---- */ + +DataLayerClusterer.prototype.add = function(feature) { + return this._data_layer.add(feature); +}; + +DataLayerClusterer.prototype.addGeoJson = function(geoJson, options) { + return this._data_layer.addGeoJson(geoJson, options); +}; + +DataLayerClusterer.prototype.contains = function(feature) { + return this._data_layer.contains(feature); +}; + +DataLayerClusterer.prototype.forEach = function(callback) { + return this._data_layer.forEach(callback); +}; + +DataLayerClusterer.prototype.getControlPosition = function() { + return this._data_layer.getControlPosition(); +}; + +DataLayerClusterer.prototype.getControls = function() { + return this._data_layer.getControls(); +}; + +DataLayerClusterer.prototype.getDrawingMode = function() { + return this._data_layer.getDrawingMode(); +}; + +DataLayerClusterer.prototype.getFeatureById = function(id) { + return this._data_layer.getFeatureById(id); +}; + +DataLayerClusterer.prototype.getStyle = function() { + return this._data_layer.getStyle(); +}; + +DataLayerClusterer.prototype.loadGeoJson = function(url, options, callback) { + return this._data_layer.loadGeoJson(url, options, callback); +}; + +DataLayerClusterer.prototype.overrideStyle = function(feature, style) { + return this._data_layer.overrideStyle(feature, style); +}; + +DataLayerClusterer.prototype.remove = function(feature) { + return this._data_layer.remove(feature); +}; + +DataLayerClusterer.prototype.revertStyle = function(feature) { + return this._data_layer.revertStyle(feature); +}; + +DataLayerClusterer.prototype.setControlPosition = function(controlPosition) { + return this._data_layer.setControlPosition(controlPosition); +}; + +DataLayerClusterer.prototype.setControls = function(controls) { + return this._data_layer.setControls(controls); +}; + +DataLayerClusterer.prototype.setDrawingMode = function(drawingMode) { + return this._data_layer.setDrawingMode(drawingMode); +}; + +DataLayerClusterer.prototype.setStyle = function(style) { + return this._data_layer.setStyle(style); +}; + +DataLayerClusterer.prototype.toGeoJson = function(callback) { + return this._data_layer.toGeoJson(callback); +}; + + +/* ---- Private methods ---- */ + +DataLayerClusterer.prototype.resetViewport = function() { + // Remove all the clusters + var c_size = this.clusters_.length; + for (var i = 0; i !== c_size; ++i) { + this.clusters_[i].remove(); + } + + this.clusters_ = []; +}; + +/** + * Sets the clusterer's ready state. + * + * @param {boolean} ready The state. + * @private + */ +DataLayerClusterer.prototype.setReady_ = function(ready) { + if (!this.ready_) { + this.ready_ = ready; + this.createClusters_(); + } +}; + +/** + * Determines if a feature is contained in a bounds. + * + * @param {google.maps.Data.Feature} feature The feature to check. + * @param {google.maps.LatLngBounds} bounds The bounds to check against. + * @return {boolean} True if the feature is in the bounds. + * @private + */ +DataLayerClusterer.prototype.isFeatureInBounds_ = function(f, bounds) { + return bounds.contains(f.getGeometry().get()); +}; + +/** + * Calculates the distance between two latlng locations in km. + * @see http://www.movable-type.co.uk/scripts/latlong.html + * + * @param {google.maps.LatLng} p1 The first lat lng point. + * @param {google.maps.LatLng} p2 The second lat lng point. + * @return {number} The distance between the two points in km. + * @private + */ +DataLayerClusterer.prototype.distanceBetweenPoints_ = function(p1, p2) { + if (!p1 || !p2) { + return 0; + } + + var R = 6371; // Radius of the Earth in km + var dLat = (p2.lat() - p1.lat()) * Math.PI / 180; + var dLon = (p2.lng() - p1.lng()) * Math.PI / 180; + var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + var d = R * c; + return d; +}; + +/** + * Add a feature to a cluster, or creates a new cluster. + * + * @param {google.maps.Data.Feature} feature The feature to add. + * @private + */ +DataLayerClusterer.prototype.addToClosestCluster_ = function(feature) { + var distance = 40000; // Some large number + + var pos = feature.getGeometry().get(); + + var cluster; + + var c_size = this.clusters_.length; + for (var i = 0; i !== c_size; ++i) { + var center = this.clusters_[i].getCenter(); + + if (center) { + var d = this.distanceBetweenPoints_(center, pos); + if (d < distance) { + distance = d; + cluster = this.clusters_[i]; + } + } + } + + if (cluster && cluster.isFeatureInClusterBounds(feature)) { + cluster.addFeature(feature); + } else { + cluster = new FeatureCluster(this); + cluster.addFeature(feature); + this.clusters_.push(cluster); + } +}; + +/** + * Creates the clusters. + * + * @private + */ +DataLayerClusterer.prototype.createClusters_ = function() { + if (!this.ready_) { + return; + } + + var mapBounds = new google.maps.LatLngBounds(this.map_.getBounds().getSouthWest(), + this.map_.getBounds().getNorthEast()); + var bounds = this.getExtendedBounds(mapBounds); + + var self = this; + this.forEach(function(feature) { + if (self.isFeatureInBounds_(feature, bounds)) { + self.addToClosestCluster_(feature); + } + }); +}; + + +/* ---- google.maps.OverlayView interface methods ---- */ + +/** + * Method called once after setMap() is called with a valid map. + * + * Adds the data layer to the map and setup the events listeners. + */ +DataLayerClusterer.prototype.onAdd = function() { + var map = this.getMap(); + + if (this.map_ !== map) { + this.onRemove(); + } + + if (map !== null) { + this._data_layer.setMap(this.map_); + + this.prevZoom_ = this.map_.getZoom(); + + // Add the map event listeners + var self = this; + this._zoom_changed = google.maps.event.addListener(this.map_, 'zoom_changed', function() { + var zoom = self.map_.getZoom(); + + if (self.prevZoom_ !== zoom) { + self.prevZoom_ = zoom; + self.resetViewport(); + } + }); + + this._idle = google.maps.event.addListener(this.map_, 'idle', function() { + self.redraw(); + }); + + this.setReady_(true); + } else { + this.setReady_(false); + } +}; + +/** + * Method called once following a call to setMap(null). + * + * Removes the data layer from the map and cleans the events listeners. + */ +DataLayerClusterer.prototype.onRemove = function() { + if (this.map_ !== null) { + if (this._zoom_changed !== null) { + try { + this.map_.removeListener(this._zoom_changed); + } catch (e) {} + } + + if (this._idle !== null) { + try { + this.map_.removeListener(this._idle); + } catch (e) {} + } + } + + this._data_layer.setMap(null); + + this.map_ = null; + + this.setReady_(false); +}; + +/** + * Empty implementation of the interface method. + */ +DataLayerClusterer.prototype.draw = function() {}; + + +/* ---- Utils ---- */ + +/** + * Extends a objects prototype by anothers. + * + * @param {Object} obj1 The object to be extended. + * @param {Object} obj2 The object to extend with. + * @return {Object} The new extended object. + */ +DataLayerClusterer.extend = function(obj1, obj2) { + return (function(object) { + for (var property in object.prototype) { + this.prototype[property] = object.prototype[property]; + } + return this; + }).apply(obj1, [obj2]); +}; + + +/** + * A cluster that contains features. + * + * @param {DataLayerClusterer} featureClusterer The featureclusterer that this + * cluster is associated with. + * @constructor + * @ignore + */ +function FeatureCluster(featureClusterer) { + this.featureClusterer_ = featureClusterer; + this.map_ = featureClusterer.getMap(); + + this.minClusterSize_ = featureClusterer.getMinClusterSize(); + this.averageCenter_ = featureClusterer.isAverageCenter(); + + this.center_ = null; + this.features_ = []; + + this.bounds_ = null; + + this.clusterIcon_ = new ClusterIcon(this, featureClusterer.getStyles(), + featureClusterer.getGridSize()); +} + +/** + * Determins if a feature is already added to the cluster. + * + * @param {google.maps.Data.Feature} feature The feature to check. + * @return {boolean} True if the feature is already added. + */ +FeatureCluster.prototype.isFeatureAlreadyAdded = function(feature) { + if (this.features_.indexOf) { + return this.features_.indexOf(feature) !== -1; + } else { + var f_size = this.features_.length; + for (var i = 0; i !== f_size; ++i) { + if (this.features_[i] === feature) { + return true; + } + } + } + + return false; +}; + + +/** + * Add a feature the cluster. + * + * @param {google.maps.Data.Feature} feature The feature to add. + * @return {boolean} True if the feature was added. + */ +FeatureCluster.prototype.addFeature = function(feature) { + if (this.isFeatureAlreadyAdded(feature)) { + return false; + } + + if (!this.center_) { + this.center_ = feature.getGeometry().get(); + this.calculateBounds_(); + } else { + if (this.averageCenter_) { + var l = this.features_.length + 1; + var lat = (this.center_.lat() * (l - 1) + feature.getGeometry().get().lat()) / l; + var lng = (this.center_.lng() * (l - 1) + feature.getGeometry().get().lng()) / l; + this.center_ = new google.maps.LatLng(lat, lng); + this.calculateBounds_(); + } + } + + this.features_.push(feature); + + var len = this.features_.length; + if (len < this.minClusterSize_) { + // Min cluster size not reached so show the feature. + this.featureClusterer_.overrideStyle(feature, DataLayerClusterer.VISIBLE_FEATURE); + } + + if (len === this.minClusterSize_) { + // Hide the features that were showing. + for (var i = 0; i < len; i++) { + this.featureClusterer_.revertStyle(this.features_[i]); + } + } + + if (len >= this.minClusterSize_) { + this.featureClusterer_.revertStyle(feature); + } + + this.updateIcon(); + return true; +}; + +/** + * Returns the feature clusterer that the cluster is associated with. + * + * @return {DataLayerClusterer} The associated feature clusterer. + */ +FeatureCluster.prototype.getDataLayerClusterer = function() { + return this.featureClusterer_; +}; + +/** + * Returns the bounds of the cluster. + * + * @return {google.maps.LatLngBounds} the cluster bounds. + */ +FeatureCluster.prototype.getBounds = function() { + var bounds = new google.maps.LatLngBounds(this.center_, this.center_); + + var f_size = this.features_.length; + for (var i = 0; i !== f_size; ++i) { + bounds.extend(this.features_[i].getGeometry().get()); + } + + return bounds; +}; + +/** + * Removes the cluster + */ +FeatureCluster.prototype.remove = function() { + this.clusterIcon_.remove(); + this.features_.length = 0; + delete this.features_; +}; + +/** + * Returns the size of the cluster. + * + * @return {number} The cluster size. + */ +FeatureCluster.prototype.getSize = function() { + return this.features_.length; +}; + +/** + * Returns the features of the cluster. + * + * @return {Array.} The cluster's features. + */ +FeatureCluster.prototype.getFeatures = function() { + return this.features_; +}; + +/** + * Returns the center of the cluster. + * + * @return {google.maps.LatLng} The cluster center. + */ +FeatureCluster.prototype.getCenter = function() { + return this.center_; +}; + + +/** + * Calculated the extended bounds of the cluster with the grid. + * + * @private + */ +FeatureCluster.prototype.calculateBounds_ = function() { + var bounds = new google.maps.LatLngBounds(this.center_, this.center_); + this.bounds_ = this.featureClusterer_.getExtendedBounds(bounds); +}; + +/** + * Determines if a feature lies in the clusters bounds. + * + * @param {google.maps.Data.Feature} feature The feature to check. + * @return {boolean} True if the feature lies in the bounds. + */ +FeatureCluster.prototype.isFeatureInClusterBounds = function(feature) { + return this.bounds_.contains(feature.getGeometry().get()); +}; + +/** + * Returns the map that the cluster is associated with. + * + * @return {google.maps.Map} The map. + */ +FeatureCluster.prototype.getMap = function() { + return this.map_; +}; + +/** + * Updates the cluster icon + */ +FeatureCluster.prototype.updateIcon = function() { + var zoom = this.map_.getZoom(); + var mz = this.featureClusterer_.getMaxZoom(); + + if (mz && zoom > mz) { + // The zoom is greater than our max zoom so show all the features in cluster. + var f_size = this.features_.length; + for (var i = 0; i !== f_size; ++i) { + this.featureClusterer_.overrideStyle(this.features_[i], DataLayerClusterer.VISIBLE_FEATURE); + } + + return; + } + + if (this.features_.length < this.minClusterSize_) { + // Min cluster size not yet reached. + this.clusterIcon_.hide(); + return; + } + + var numStyles = this.featureClusterer_.getStyles().length; + var sums = this.featureClusterer_.getCalculator()(this.features_, numStyles); + + this.clusterIcon_.setSums(sums); + + this.clusterIcon_.setCenter(this.center_); + this.clusterIcon_.show(); +}; + + +/** + * A cluster icon + * + * @param {Cluster} cluster The cluster to be associated with. + * @param {Object} styles An object that has style properties: + * 'url': (string) The image url. + * 'height': (number) The image height. + * 'width': (number) The image width. + * 'anchor': (Array) The anchor position of the label text. + * 'textColor': (string) The text color. + * 'textSize': (number) The text size. + * 'backgroundPosition: (string) The background postition x, y. + * @param {number=} opt_padding Optional padding to apply to the cluster icon. + * @constructor + * @extends google.maps.OverlayView + */ +function ClusterIcon(cluster, styles, opt_padding) { + DataLayerClusterer.extend(ClusterIcon, google.maps.OverlayView); + + this.styles_ = styles; + this.padding_ = opt_padding || 0; + this.cluster_ = cluster; + this.center_ = null; + this.map_ = cluster.getMap(); + this.div_ = null; + this.sums_ = null; + this.visible_ = false; + + this.setMap(this.map_); +} + + +/* ---- Public methods ---- */ + +/** + * Hide the icon. + */ +ClusterIcon.prototype.hide = function() { + if (this.div_) { + this.div_.style.display = 'none'; + } + this.visible_ = false; +}; + +/** + * Position and show the icon. + */ +ClusterIcon.prototype.show = function() { + if (this.div_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.cssText = this.createCss(pos); + this.div_.style.display = ''; + } + this.visible_ = true; +}; + +/** + * Remove the icon from the map + */ +ClusterIcon.prototype.remove = function() { + this.setMap(null); +}; + +/** + * Sets the center of the icon. + * + * @param {google.maps.LatLng} center The latlng to set as the center. + */ +ClusterIcon.prototype.setCenter = function(center) { + this.center_ = center; +}; + + +/* ---- google.maps.OverlayView interface methods ---- */ + +/** + * Adding the cluster icon to the dom. + * @ignore + */ +ClusterIcon.prototype.onAdd = function() { + this.div_ = document.createElement('DIV'); + if (this.visible_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.cssText = this.createCss(pos); + this.div_.innerHTML = this.sums_.text; + } + + var panes = this.getPanes(); + panes.overlayMouseTarget.appendChild(this.div_); + + var self = this; + google.maps.event.addDomListener(this.div_, 'click', function() { + self.triggerClusterClick(); + }); +}; + +/** + * Draw the icon. + * @ignore + */ +ClusterIcon.prototype.draw = function() { + if (this.visible_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.top = pos.y + 'px'; + this.div_.style.left = pos.x + 'px'; + } +}; + +/** + * Implementation of the onRemove interface. + * @ignore + */ +ClusterIcon.prototype.onRemove = function() { + if (this.div_ && this.div_.parentNode) { + this.hide(); + this.div_.parentNode.removeChild(this.div_); + this.div_ = null; + } +}; + + +/* ---- Private methods ---- */ + +/** + * Triggers the clusterclick event and zoom's if the option is set. + */ +ClusterIcon.prototype.triggerClusterClick = function() { + var featureClusterer = this.cluster_.getDataLayerClusterer(); + + // Trigger the clusterclick event. + google.maps.event.trigger(featureClusterer, 'clusterclick', this.cluster_); + + if (featureClusterer.isZoomOnClick()) { + // Zoom into the cluster. + this.map_.fitBounds(this.cluster_.getBounds()); + } +}; + +/** + * Returns the position to place the div dending on the latlng. + * + * @param {google.maps.LatLng} latlng The position in latlng. + * @return {google.maps.Point} The position in pixels. + * @private + */ +ClusterIcon.prototype.getPosFromLatLng_ = function(latlng) { + var pos = this.getProjection().fromLatLngToDivPixel(latlng); + pos.x -= parseInt(this.width_ / 2, 10); + pos.y -= parseInt(this.height_ / 2, 10); + return pos; +}; + +/** + * Create the css text based on the position of the icon. + * + * @param {google.maps.Point} pos The position. + * @return {string} The css style text. + */ +ClusterIcon.prototype.createCss = function(pos) { + var style = []; + style.push('background-image:url(' + this.url_ + ');'); + var backgroundPosition = this.backgroundPosition_ ? this.backgroundPosition_ : '0 0'; + style.push('background-position:' + backgroundPosition + ';'); + + if (typeof this.anchor_ === 'object') { + if (typeof this.anchor_[0] === 'number' && this.anchor_[0] > 0 && + this.anchor_[0] < this.height_) { + style.push('height:' + (this.height_ - this.anchor_[0]) + + 'px; padding-top:' + this.anchor_[0] + 'px;'); + } else { + style.push('height:' + this.height_ + 'px; line-height:' + this.height_ + + 'px;'); + } + if (typeof this.anchor_[1] === 'number' && this.anchor_[1] > 0 && + this.anchor_[1] < this.width_) { + style.push('width:' + (this.width_ - this.anchor_[1]) + + 'px; padding-left:' + this.anchor_[1] + 'px;'); + } else { + style.push('width:' + this.width_ + 'px; text-align:center;'); + } + } else { + style.push('height:' + this.height_ + 'px; line-height:' + + this.height_ + 'px; width:' + this.width_ + 'px; text-align:center;'); + } + + var txtColor = this.textColor_ ? this.textColor_ : 'black'; + var txtSize = this.textSize_ ? this.textSize_ : 11; + + style.push('cursor:pointer; top:' + pos.y + 'px; left:' + + pos.x + 'px; color:' + txtColor + '; position:absolute; font-size:' + + txtSize + 'px; font-family:Arial,sans-serif; font-weight:bold'); + return style.join(''); +}; + +/** + * Sets the icon to the the styles. + */ +ClusterIcon.prototype.useStyle = function() { + var index = Math.max(0, this.sums_.index - 1); + index = Math.min(this.styles_.length - 1, index); + var style = this.styles_[index]; + this.url_ = style.url; + this.height_ = style.height; + this.width_ = style.width; + this.textColor_ = style.textColor; + this.anchor_ = style.anchor; + this.textSize_ = style.textSize; + this.backgroundPosition_ = style.backgroundPosition; +}; + +/** + * Set the sums of the icon. + * + * @param {Object} sums The sums containing: + * 'text': (string) The text to display in the icon. + * 'index': (number) The style index of the icon. + */ +ClusterIcon.prototype.setSums = function(sums) { + this.sums_ = sums; + this.text_ = sums.text; + this.index_ = sums.index; + if (this.div_) { + this.div_.innerHTML = sums.text; + } + + this.useStyle(); +}; + + +/* ---- To remove soon ---- */ +/* + * TODO: Allow the styling using a similar interface than google.map.Data. + * Use SVG icon by default, remove dependency of google-maps-utility-library-v3.googlecode.com. + */ + +/** + * The feature cluster image path. + * + * @type {string} + */ +DataLayerClusterer.MARKER_CLUSTER_IMAGE_PATH_ = 'http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclusterer/images/m'; +DataLayerClusterer.MARKER_CLUSTER_IMAGE_EXTENSION_ = 'png'; + +/** + * Sets up the styles object. + * + * @private + */ +DataLayerClusterer.prototype.setupStyles_ = function() { + if (this.styles_.length) { + return; + } + + var s_sizes = this.sizes.length; + for (var i = 0; i !== s_sizes; ++i) { + this.styles_.push({ + url: this.imagePath_ + (i + 1) + '.' + this.imageExtension_, + height: this.sizes[i], + width: this.sizes[i] + }); + } +}; + +/** + * Sets the styles. + * + * @param {Object} styles The style to set. + */ +DataLayerClusterer.prototype.setStyles = function(styles) { + this.styles_ = styles; +}; + +/** + * Gets the styles. + * + * @return {Object} The styles object. + */ +DataLayerClusterer.prototype.getStyles = function() { + return this.styles_; +}; + +/** + * Set the calculator function. + * + * @param {function(Array, number)} calculator The function to set as the + * calculator. The function should return a object properties: + * 'text' (string) and 'index' (number). + * + */ +DataLayerClusterer.prototype.setCalculator = function(calculator) { + this.calculator_ = calculator; +}; + +/** + * Get the calculator function. + * + * @return {function(Array, number)} the calculator function. + */ +DataLayerClusterer.prototype.getCalculator = function() { + return this.calculator_; +}; + +/** + * The function for calculating the cluster icon image. + * + * @param {Array.} features The features in the clusterer. + * @param {number} numStyles The number of styles available. + * @return {Object} A object properties: 'text' (string) and 'index' (number). + * @private + */ +DataLayerClusterer.prototype.calculator_ = function(features, numStyles) { + var index = 0; + var count = features.length; + var dv = count; + while (dv !== 0) { + dv = parseInt(dv / 10, 10); + index++; + } + + index = Math.min(index, numStyles); + return { + text: count, + index: index + }; +};