ym4r-mapstraction 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.DS_Store ADDED
Binary file
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ *.sample
2
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Luke van der Hoeven
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,158 @@
1
+ =YM4R/Mapstraction plugin for Rails
2
+
3
+ This is the YM4R/Mapstraction plugin for Rails. It is aimed at facilitating the use of the Mapstraction library from Rails application.
4
+
5
+ ==Getting Started
6
+ I present here the most common operations you would want to do with YM4R/Mapstraction. Read the rest of the documents if you want more details.
7
+
8
+ If you are using the GoogleMaps, Map24, MapQuest, or MultiMap APIs, you will need to create a file housing the API keys as follows:
9
+
10
+ ===GoogleMaps
11
+ For development and test, we have only one possible host (localhost:3000), so there is only a single key associated with the mode.
12
+ In production, the app can be accessed through 2 different hosts: test.com and exmaple.com. There then needs a 2-key hash. If you deployed to one host, only the API key would be needed (as in development and test).
13
+
14
+ development:
15
+ ABQIAAAAzMUFFnT9uH0xq39J0Y4kbhTJQa0g3IQ9GZqIMmInSLzwtGDKaBR6j135zrztfTGVOm2QlWnkaidDIQ
16
+
17
+ test:
18
+ ABQIAAAAzMUFFnT9uH0xq39J0Y4kbhTJQa0g3IQ9GZqIMmInSLzwtGDKaBR6j135zrztfTGVOm2QlWnkaidDIQ
19
+
20
+ production:
21
+ test.com: ABQIAAAAzMUFFnT9uH0Sfg76Y4kbhTJQa0g3IQ9GZqIMmInSLzwtGDmlRT6e90j135zat56yhJKQlWnkaidDIQ
22
+ example.com: ABQIAAAAzMUFFnT9uH0Sfg98Y4kbhGFJQa0g3IQ9GZqIMmInSLrthJKGDmlRT98f4j135zat56yjRKQlWnkmod3TB
23
+
24
+ ===24Maps, MapQuest, MultiMap
25
+ For development and test, we have only one possible host (localhost:3000), so there is only a single key associated with the mode.
26
+ In production, the app can be accessed through 2 different hosts: test.com and exmaple.com. There then needs a 2-key hash. If you deployed to one host, only the API key would be needed (as in development and test).
27
+
28
+ development:
29
+ localhost: xxx
30
+
31
+ test:
32
+ localhost: xxx
33
+
34
+ production:
35
+ exmaple.org: xxx
36
+ example.com: xxx
37
+
38
+ ==Examples
39
+ In your controller, here is a typical initialization sequence in action +index+:
40
+ def index
41
+ @map = Mapstraction.new("map_div",:yahoo)
42
+ @map.control_init(:small => true)
43
+ @map.center_zoom_init([75.5,-42.56],4)
44
+ @map.marker_init(Marker.new([75.6,-42.467],:label => "Hello", :info_bubble => "Info! Info!",
45
+ :icon => "/images/icon02.png"))
46
+ end
47
+
48
+ Here I create a Mapstraction map (which will be placed inside a DIV of id +map_div+) which will use Yahoo! Maps, add a small control (it is the only available control currently), set the center and the zoom and add a marker. The 3 possible options to pass to the +Marker+ constructor are all shown above. Of these 4 steps only the creation of the map and the setting of the center and the zoom are absolutely necessary. Apart from Yahoo! Maps, I could have created a map of type <tt>:google</tt> (Google Maps) or <tt>:microsoft</tt> (Virtual Earth).
49
+
50
+ In your view, here is what you would have:
51
+ <html><head><title>Test</title>
52
+ <%= Mapstraction.header(:yahoo) %>
53
+ <%= @map.to_html %>
54
+ </head><body>
55
+ <%= @map.div(:width => 600, :height => 400) %>
56
+ </body></html>
57
+
58
+ First you must output the header, used by all the maps of the page: It includes the Mapstraction JS code and the JS helper functions of YM4R/Mapstraction. It also includes the API files, as determined according to the parameter of the method. Usually, if you only have one map, there will be a single symbol, identical to the one used to create the map. But you can also pass an array of symbols, in which case all the corresponding API's will be included. Then we initialize the map by calling <tt>@map.to_html</tt>. In the body, we need a DIV whose +id+ is the same as the one passed to the Mapstraction constructor in the controller. The call to <tt>@map.div</tt> outputs such a DIV. We pass to it options to set the width and height of the map in the style attribute of the DIV.
59
+
60
+ <b>Note that you need to set a size for the map DIV element at some point or the map will not be displayed.</b> You have a few ways to do this:
61
+ - You define it yourself, wherever you want. Usually as part of the layout definition in an external CSS.
62
+ - In the head of the document, through a CSS instruction output by <tt>@map.header_width_height</tt>, to which you pass 2 arguments (width and height).
63
+ - When outputting the DIV with <tt>@map.div</tt>, you can also pass an option hash, with keys <tt>:width</tt> and <tt>:height</tt> and a style attribute for the DIV element will be output.
64
+
65
+ You can update the map trough RJS. Here is an action you can call from a <tt>link_remote_tag</tt> which would do this:
66
+ def update
67
+ @map = Variable.new("map")
68
+ @marker = Marker.new([75.89,-42.767],:label => "Update", :info_bubble => "I have been placed through RJS")
69
+ end
70
+
71
+ Here, I first bind the Ruby <tt>@map</tt> variable to the JS variable <tt>map</tt>, which already exists in the client browser. +map+ is by default the name given to a map created from YM4R/Mapstraction (this could be overriden by passing a second argument to the Mapstraction constructor). Then I create a marker.
72
+
73
+ And you would have inside the RJS template for the action:
74
+ page << @map.remove_all_markers
75
+ page << @map.add_marker(@marker)
76
+
77
+ Here I first clear the map of all markers. Then I add the marker. Note that the +marker_init+ is not used anymore since, as its name indicates, this method is only for the initialization of the map.
78
+
79
+ ==Installation
80
+
81
+ gem install ym4r-mapstraction
82
+
83
+ As part of the installation procedure, the JavaScript files found in the <tt>PLUGIN_ROOT/javascript</tt> directory will be copied to the <tt>RAILS_ROOT/public/javascripts/</tt> directory.
84
+
85
+ Moreover a <tt>gmaps_api_key.yml</tt> file will also be created in the <tt>RAILS_ROOT/config/</tt> folder. This is in order to setup the Google Maps API in case you want to use it with Mapstraction. You don't need to do anything special if you use only Yahoo! Maps or Virtual Earth. If this file already exists (installed for example by a previous version of the plugin), nothing will be done to it, so you can keep your configuration data even after updating the plugin. This file is a YAML representation of a hash, similar to the <tt>database.yml</tt> file in that the primary keys in the file are the different environments (development, test, production), since you will probably need to have different Google Maps API keys depending on that criteria (for example: a key for localhost for development and test; a key for your host for production). If you don't have any special need, there will be only one key associated with each environment. If however, you need to have multiple keys (for example if your app will be accessible from multiple URLs, for which you need different keys), you can also associate a hash to the environment, whose keys will be the different hosts. In this case, you will need to pass a value to the <tt>:host</tt> key when calling the method <tt>Mapstraction.header</tt> (usually <tt>@request.host</tt>).
86
+
87
+ ==Operations
88
+ ===Static switching between mapping API's
89
+ It is the goal of the Mapstraction API to make it trivial to switch API (for example, in case the currently used API changes its term of service). It is equally easy to do this with YM4R/Mapstraction. You need to do 2 things:
90
+ - Change the provider when creating a Mapstraction map
91
+ - Change the provider when calling the <tt>Mapstraction.header</tt>
92
+
93
+ ===Dynamic switching between mapping API's
94
+ The Mapstraction API provides a method +swap+ to be called on a Mapstraction JS object, with the name of the new API as argument, that will switch the mapping API on the fly. The zoom levels and elements will be copied over to the new API.
95
+ ===Markers
96
+ Markers are constructed with either a LatLonPoint or a 2D-array of coordinates in the first argument, followed by an option hash. The keys to the hash can be <tt>:info_bubble</tt> (text to display when the marker is clicked), <tt>:label</tt> (summary text of the marker; The way it is displayed depends on the mapping service used) or <tt>:icon</tt> (to indicate the URL of an icon to use for the marker). Support for icons is a bit flaky since the different services have different conventions for their origins, so you should probably have different sets of icons depending on the service to make it less confusing for the user.
97
+
98
+ ===MarkerGroup
99
+ Surprisingly, MarkerGroups are groups of markers. They can be useful if you want to be able to show and hide a group of markers at the same time in response to a user-generated event (for example, as a response to a click on a link in the HTML page) without having to declare all the markers as global variables. Instead you declare only the group as global and give it the markers. Then you can call +show+ and +hide+ on the group, to act on all the markers of the group at the same time.
100
+
101
+ ===Polylines
102
+ Polylines are constructed with an array of LatLonPoints, as well as with options like the opacity (<tt>:opacity</tt>), the width (<tt>:width</tt>) or the color (<tt>:color</tt> and with a value in the form "#RRGGBB", with RR, GG and BB numbers between 0 and 255 in hexadecimal form). You add the marker to the map at initialization time with the method +polyline_init+:
103
+ @map.polyline_init(Polyline.new([LatLonPoint.new([45.12,22.3]),
104
+ LatLonPoint.new([34.12,56.1])],:width => 5, :color => "#FF00AB",
105
+ :opacity => 0.7))
106
+
107
+ ===Clusterer
108
+ A clusterer contains a large number of markers, which are dynamically grouped in clusters if needed. This allows to have a lot of markers on a map without being slowed down too much. It is an adaptation to Mapstraction of Jef Poskanzer's Clusterer2 library for Google Maps. It works for all 3 Mapstraction backends. To use it from your Rails code, do the following:
109
+ marker_array = [.....] #large number of markers
110
+ clusterer = Clusterer.new(marker_array,:max_visible_markers => 20)
111
+ @map.clusterer_init(clusterer)
112
+ By default the markers start being clustered when there are more than 150 in the clusterer. Nothing special happens before this. This setting can be changed with the <tt>:max_visible_markers</tt> option. Other options are possible. Look inside the JS code inside the <tt>clusterer.js</tt> to know what they are.
113
+
114
+ ===Routing
115
+ Currently supported only by Mapquest.
116
+ Must be initialized at the same time as the map. In your controller, here is a typical initialization sequence in action +index+:
117
+ def index
118
+ @router = Router.new(:display_route, 'mapquest', :error_route)
119
+ end
120
+
121
+ The router is created with 3 possible options to pass to the +Router+ constructor and all are necessary. First one is the name of the callback function that will show the route on the map. You can change the name of the callback function but if you do not pass :no_callback option in +to_html+ the method will be implemented as shown in routing.rb. The same goes for :error_route callback (gets called if an error occures). If you leave them as they are in the example, all will work fine. The middle parameter is the name of the routing provider and for now it is allways 'mapquest'. Additionally, you can pass two more parameters, the variable name for the router (default = "router") and variable name for the map (default = "map").
122
+
123
+ In your view, xou have to output the header after map header (@map.to_html):
124
+ <%= @router.to_html %>
125
+
126
+ For usage, you can choose weather you will use the routing option with geocoding, as in mapstraction examples or you can prepare point in some other fashion and just use the router to connect them. So, when you have the points, in your rjs you can do the following:
127
+ page << @router.routePoints(@points)
128
+
129
+ Be carefull here because routePoints is an additional mastraction library method that is not in the main trunk yet. But you can use +route+ too.
130
+
131
+ ==Recent changes
132
+ - Support for latest Mapstraction version
133
+ - Support for polylines
134
+ - Some additional documentation
135
+
136
+ ==TODO
137
+ - Update to newer versions of Mapstraction as they come along
138
+ - More documentation
139
+
140
+ ==License
141
+ The YM4R/Mapstraction plugin is released under the MIT license.
142
+
143
+ ==Support
144
+ Any questions, enhancement proposals, bug notifications or corrections can be sent to mailto:hungerandthirst@gmail.com.
145
+
146
+ == Note on Patches/Pull Requests
147
+
148
+ * Fork the project.
149
+ * Make your feature addition or bug fix.
150
+ * Add tests for it. This is important so I don't break it in a
151
+ future version unintentionally.
152
+ * Commit, do not mess with rakefile, version, or history.
153
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
154
+ * Send me a pull request. Bonus points for topic branches.
155
+
156
+ == Copyright
157
+
158
+ Copyright (c) 2010 Luke van der Hoeven. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "ym4r-mapstraction"
8
+ gem.summary = %Q{ym4r-mapstraction: Mapstraction library for ruby applications.}
9
+ gem.description = %Q{This is an updated/gemmed version of the ym4r-mapstraction plugin by nofxx}
10
+ gem.email = "hungerandthirst@gmail.com"
11
+ gem.homepage = "http://github.com/plukevdh/ym4r_mapstraction"
12
+ gem.authors = ["Luke van der Hoeven"]
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/test_*.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+ task :test => :check_dependencies
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
47
+
48
+ rdoc.rdoc_dir = 'rdoc'
49
+ rdoc.title = "ym4r-mapstraction #{version}"
50
+ rdoc.rdoc_files.include('README*')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,410 @@
1
+ // Clusterer for Mapstraction: To display a large number of markers
2
+ //
3
+ // Usage:
4
+ //
5
+ // When you have a Mapstraction object (for example, of name mapstraction), do:
6
+ //
7
+ // var clusterer = new Clusterer(mapstraction);
8
+ //
9
+ // Then you must add markers to the clusterer.
10
+ //
11
+ // clusterer.addMarker(marker);
12
+ //
13
+ // By default, the clustering will start when there are more than 150 markers in the Clusterer object.
14
+ // You can pass options to the Clusterer constructor to change this behaviour.
15
+ //
16
+ // Copyright � 2005,2006 by Jef Poskanzer <jef@mail.acme.com>.
17
+ // All rights reserved.
18
+ //
19
+ // Redistribution and use in source and binary forms, with or without
20
+ // modification, are permitted provided that the following conditions
21
+ // are met:
22
+ // 1. Redistributions of source code must retain the above copyright
23
+ // notice, this list of conditions and the following disclaimer.
24
+ // 2. Redistributions in binary form must reproduce the above copyright
25
+ // notice, this list of conditions and the following disclaimer in the
26
+ // documentation and/or other materials provided with the distribution.
27
+ //
28
+ // THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
29
+ // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
30
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
31
+ // ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
32
+ // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
33
+ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
34
+ // OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
35
+ // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
36
+ // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
37
+ // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
38
+ // SUCH DAMAGE.
39
+ //
40
+ // For commentary on this license please see http://www.acme.com/license.html
41
+ //
42
+ // Modified for use with Mapstraction by Guilhem Vellut. Bugs and comments: guilhem.vellut@gmail.com
43
+
44
+ //Add a method to the Mapstraction class to add a clusterer
45
+ Mapstraction.prototype.addClusterer = function(clusterer){
46
+ clusterer.initialize(this);
47
+ }
48
+
49
+ // Constructor.
50
+ Clusterer = function (markers, options){
51
+ this.markers = markers || [];
52
+ for(var i = 0, len = markers.length ; i < len ; ++i){
53
+ markers[i].onMap = false;
54
+ }
55
+
56
+ this.clusters = [];
57
+ this.timeout = null;
58
+
59
+ options = options || new Object();
60
+ this.maxVisibleMarkers = options.maxVisibleMarkers || 150;
61
+ this.gridSize = options.gridSize || 5;
62
+ this.minMarkersPerCluster = options.minMarkersPerCluster ||5;
63
+ this.maxLinesPerInfoBox = options.maxLinesPerInfoBox ||10;
64
+ this.icon = options.icon;
65
+
66
+ map.addEventListener("moveend",Clusterer.makeCaller( Clusterer.display, this ) );
67
+
68
+ };
69
+
70
+ // Call this to add a marker.
71
+ Clusterer.prototype.addMarker = function ( marker){
72
+ marker.onMap = false;
73
+ this.markers.push( marker );
74
+
75
+ if(this.map != null){
76
+ if ( marker.setMap != null )
77
+ marker.setMap( this.map );
78
+ this.displayLater();
79
+ }
80
+ };
81
+
82
+ Clusterer.prototype.initialize = function(mapstraction){
83
+ this.map = mapstraction;
84
+ this.currentZoomLevel = mapstraction.getZoom();
85
+ for(var i = 0, len = this.markers.length; i < len; ++i ){
86
+ this.markers[i].setMap(mapstraction);
87
+ }
88
+ this.displayLater();
89
+ }
90
+
91
+
92
+ // Call this to remove a marker.
93
+ Clusterer.prototype.removeMarker = function ( marker )
94
+ {
95
+ for ( var i = 0; i < this.markers.length; ++i )
96
+ if ( this.markers[i] == marker )
97
+ {
98
+ if ( marker.onMap )
99
+ this.map.removeMarker( marker );
100
+ for ( var j = 0; j < this.clusters.length; ++j )
101
+ {
102
+ var cluster = this.clusters[j];
103
+ if ( cluster != null )
104
+ {
105
+ for ( var k = 0; k < cluster.markers.length; ++k )
106
+ if ( cluster.markers[k] == marker )
107
+ {
108
+ cluster.markers[k] = null;
109
+ --cluster.markerCount;
110
+ break;
111
+ }
112
+ if ( cluster.markerCount == 0 )
113
+ {
114
+ this.clearCluster( cluster );
115
+ this.clusters[j] = null;
116
+ }
117
+ }
118
+ }
119
+ this.markers[i] = null;
120
+ break;
121
+ }
122
+ this.displayLater();
123
+
124
+ };
125
+
126
+
127
+ Clusterer.prototype.displayLater = function ()
128
+ {
129
+ if ( this.timeout != null )
130
+ clearTimeout( this.timeout );
131
+ this.timeout = setTimeout( Clusterer.makeCaller( Clusterer.display, this ), 50 );
132
+ };
133
+
134
+
135
+ Clusterer.display = function ( clusterer )
136
+ {
137
+ var i, j, marker, cluster;
138
+
139
+ clearTimeout( clusterer.timeout );
140
+
141
+ var newZoomLevel = clusterer.map.getZoom();
142
+ if ( newZoomLevel != clusterer.currentZoomLevel )
143
+ {
144
+ // When the zoom level changes, we have to remove all the clusters.
145
+ for ( i = 0; i < clusterer.clusters.length; ++i )
146
+ if ( clusterer.clusters[i] != null )
147
+ {
148
+ clusterer.clearCluster( clusterer.clusters[i] );
149
+ clusterer.clusters[i] = null;
150
+ }
151
+ clusterer.clusters.length = 0;
152
+ clusterer.currentZoomLevel = newZoomLevel;
153
+ }
154
+
155
+ // Get the current bounds of the visible area.
156
+ var bounds = clusterer.map.getBounds();
157
+
158
+ // Expand the bounds a little, so things look smoother when scrolling
159
+ // by small amounts.
160
+ var sw = bounds.getSouthWest();
161
+ var ne = bounds.getNorthEast();
162
+ var dx = ne.lon - sw.lon;
163
+ var dy = ne.lat - sw.lat;
164
+
165
+ dx *= 0.10;
166
+ dy *= 0.10;
167
+ bounds = new BoundingBox(sw.lat - dy, sw.lon - dx,ne.lat + dy, ne.lon + dx);
168
+
169
+
170
+ // Partition the markers into visible and non-visible lists.
171
+ var visibleMarkers = [];
172
+ var nonvisibleMarkers = [];
173
+ for ( i = 0; i < clusterer.markers.length; ++i )
174
+ {
175
+ marker = clusterer.markers[i];
176
+ if ( marker != null )
177
+ if ( bounds.contains( marker.location ) )
178
+ visibleMarkers.push( marker );
179
+ else
180
+ nonvisibleMarkers.push( marker );
181
+ }
182
+
183
+ // Take down the non-visible markers.
184
+ for ( i = 0; i < nonvisibleMarkers.length; ++i )
185
+ {
186
+ marker = nonvisibleMarkers[i];
187
+ if ( marker.onMap )
188
+ {
189
+ clusterer.map.removeMarker( marker );
190
+ marker.onMap = false;
191
+ }
192
+ }
193
+
194
+ // Take down the non-visible clusters.
195
+ for ( i = 0; i < clusterer.clusters.length; ++i )
196
+ {
197
+ cluster = clusterer.clusters[i];
198
+ if ( cluster != null && ! bounds.contains( cluster.marker.location ) && cluster.onMap )
199
+ {
200
+ clusterer.map.removeMarker( cluster.marker );
201
+ cluster.onMap = false;
202
+ }
203
+ }
204
+
205
+ // Clustering! This is some complicated stuff. We have three goals
206
+ // here. One, limit the number of markers & clusters displayed, so the
207
+ // maps code doesn't slow to a crawl. Two, when possible keep existing
208
+ // clusters instead of replacing them with new ones, so that the app pans
209
+ // better. And three, of course, be CPU and memory efficient.
210
+ if ( visibleMarkers.length > clusterer.maxVisibleMarkers )
211
+ {
212
+ // Add to the list of clusters by splitting up the current bounds
213
+ // into a grid.
214
+ var latRange = bounds.getNorthEast().lat - bounds.getSouthWest().lat;
215
+ var latInc = latRange / clusterer.gridSize;
216
+ var lngInc = latInc / Math.cos( ( bounds.getNorthEast().lat + bounds.getSouthWest().lat ) / 2.0 * Math.PI / 180.0 );
217
+ for ( var lat = bounds.getSouthWest().lat; lat <= bounds.getNorthEast().lat; lat += latInc )
218
+ for ( var lng = bounds.getSouthWest().lng; lng <= bounds.getNorthEast().lng; lng += lngInc )
219
+ {
220
+ cluster = new Object();
221
+ cluster.clusterer = clusterer;
222
+ cluster.bounds = new BoundingBox( lat, lng , lat + latInc, lng + lngInc );
223
+ cluster.markers = [];
224
+ cluster.markerCount = 0;
225
+ cluster.onMap = false;
226
+ cluster.marker = null;
227
+ clusterer.clusters.push( cluster );
228
+ }
229
+
230
+ // Put all the unclustered visible markers into a cluster - the first
231
+ // one it fits in, which favors pre-existing clusters.
232
+ for ( i = 0; i < visibleMarkers.length; ++i )
233
+ {
234
+ marker = visibleMarkers[i];
235
+ if ( marker != null && ! marker.inCluster )
236
+ {
237
+ for ( j = 0; j < clusterer.clusters.length; ++j )
238
+ {
239
+ cluster = clusterer.clusters[j];
240
+ if ( cluster != null && cluster.bounds.contains( marker.location ) )
241
+ {
242
+ cluster.markers.push( marker );
243
+ ++cluster.markerCount;
244
+ marker.inCluster = true;
245
+ }
246
+ }
247
+ }
248
+ }
249
+
250
+ // Get rid of any clusters containing only a few markers.
251
+ for ( i = 0; i < clusterer.clusters.length; ++i )
252
+ if ( clusterer.clusters[i] != null && clusterer.clusters[i].markerCount < clusterer.minMarkersPerCluster )
253
+ {
254
+ clusterer.clearCluster( clusterer.clusters[i] );
255
+ clusterer.clusters[i] = null;
256
+ }
257
+
258
+ // Shrink the clusters list.
259
+ for ( i = clusterer.clusters.length - 1; i >= 0; --i )
260
+ if ( clusterer.clusters[i] != null )
261
+ break;
262
+ else
263
+ --clusterer.clusters.length;
264
+
265
+ // Ok, we have our clusters. Go through the markers in each
266
+ // cluster and remove them from the map if they are currently up.
267
+ for ( i = 0; i < clusterer.clusters.length; ++i )
268
+ {
269
+ cluster = clusterer.clusters[i];
270
+ if ( cluster != null )
271
+ {
272
+ for ( j = 0; j < cluster.markers.length; ++j )
273
+ {
274
+ marker = cluster.markers[j];
275
+ if ( marker != null && marker.onMap )
276
+ {
277
+ clusterer.map.removeMarker( marker );
278
+ marker.onMap = false;
279
+ }
280
+ }
281
+ }
282
+ }
283
+
284
+ // Now make cluster-markers for any clusters that need one.
285
+ for ( i = 0; i < clusterer.clusters.length; ++i )
286
+ {
287
+ cluster = clusterer.clusters[i];
288
+ if ( cluster != null && cluster.marker == null )
289
+ {
290
+ // Figure out the average coordinates of the markers in this
291
+ // cluster.
292
+ var xTotal = 0.0, yTotal = 0.0;
293
+ for ( j = 0; j < cluster.markers.length; ++j )
294
+ {
295
+ marker = cluster.markers[j];
296
+ if ( marker != null )
297
+ {
298
+ xTotal += ( + marker.location.lng );
299
+ yTotal += ( + marker.location.lat );
300
+ }
301
+ }
302
+ var location = new LatLonPoint( yTotal / cluster.markerCount, xTotal / cluster.markerCount );
303
+ marker = new Marker( location);
304
+ if(clusterer.icon)
305
+ marker.setIcon(clusterer.icon);
306
+ marker.setInfoBubble(Clusterer.popUpText(cluster));
307
+ cluster.marker = marker;
308
+ }
309
+ }
310
+ }
311
+
312
+ // Display the visible markers not already up and not in clusters.
313
+ for ( i = 0; i < visibleMarkers.length; ++i )
314
+ {
315
+ marker = visibleMarkers[i];
316
+ if ( marker != null && ! marker.onMap && ! marker.inCluster )
317
+ {
318
+ clusterer.map.addMarker( marker );
319
+ if ( marker.addedToMap != null )
320
+ marker.addedToMap();
321
+ marker.onMap = true;
322
+ }
323
+ }
324
+
325
+ // Display the visible clusters not already up.
326
+ for ( i = 0; i < clusterer.clusters.length; ++i )
327
+ {
328
+ cluster = clusterer.clusters[i];
329
+ if ( cluster != null && ! cluster.onMap && bounds.contains( cluster.marker.location ) )
330
+ {
331
+ clusterer.map.addMarker( cluster.marker );
332
+ cluster.onMap = true;
333
+ }
334
+ }
335
+ };
336
+
337
+
338
+ Clusterer.popUpText = function (cluster)
339
+ {
340
+ var clusterer = cluster.clusterer;
341
+ var html = '<table width="300">';
342
+ var n = 0;
343
+ for ( var i = 0; i < cluster.markers.length; ++i )
344
+ {
345
+ var marker = cluster.markers[i];
346
+ if ( marker != null )
347
+ {
348
+ ++n;
349
+ html += '<tr>';
350
+ html += '<td>' ;
351
+ if( marker.labelText != null)
352
+ html += marker.labelText;
353
+ else
354
+ html += 'marker #' + i;
355
+ if(marker.iconUrl != null){
356
+ html += '<td><img src="' + marker.iconUrl + '" /></td>';
357
+ }
358
+ html += '</td></tr>';
359
+ if ( n == clusterer.maxLinesPerInfoBox - 1 && cluster.markerCount > clusterer.maxLinesPerInfoBox )
360
+ {
361
+ html += '<tr><td colspan="2">...and ' + ( cluster.markerCount - n ) + ' more</td></tr>';
362
+ break;
363
+ }
364
+ }
365
+ }
366
+ html += '</table>';
367
+
368
+ return html;
369
+ };
370
+
371
+ Clusterer.prototype.clearCluster = function ( cluster )
372
+ {
373
+ var i, marker;
374
+
375
+ for ( i = 0; i < cluster.markers.length; ++i )
376
+ if ( cluster.markers[i] != null )
377
+ {
378
+ cluster.markers[i].inCluster = false;
379
+ cluster.markers[i] = null;
380
+ }
381
+ cluster.markers.length = 0;
382
+ cluster.markerCount = 0;
383
+ if ( cluster.onMap )
384
+ {
385
+ this.map.removeMarker( cluster.marker );
386
+ cluster.onMap = false;
387
+ }
388
+ };
389
+
390
+
391
+ // This returns a function closure that calls the given routine with the
392
+ // specified arg.
393
+ Clusterer.makeCaller = function ( func, arg )
394
+ {
395
+ return function () { func( arg ); };
396
+ };
397
+
398
+
399
+ // Augment GMarker so it handles markers that have been created but
400
+ // not yet addOverlayed.
401
+
402
+ Marker.prototype.setMap = function ( map )
403
+ {
404
+ this.map = map;
405
+ };
406
+
407
+ Marker.prototype.addedToMap = function ()
408
+ {
409
+ this.map = null;
410
+ };