ym4r 0.3.1 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README +54 -9
- data/lib/ym4r/google_maps/javascript/clusterer.js +444 -0
- data/lib/ym4r/google_maps/layer.rb +19 -12
- data/lib/ym4r/google_maps/map.rb +2 -1
- data/lib/ym4r/google_maps/overlay.rb +32 -9
- data/rakefile.rb +1 -1
- metadata +3 -2
data/README
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
=YM4R
|
2
|
-
This is YM4R 0.3.
|
2
|
+
This is YM4R 0.3.2. The goal of YM4R (Yellow Maps For Ruby) is to ease the use of the Google Maps (including the Geocoding API) and Yahoo! Maps API's (including the Geocoding, Map Image, Traffic and Local Search API's) from Ruby and Rails.
|
3
3
|
|
4
4
|
==Operations
|
5
5
|
===Google Maps
|
@@ -58,7 +58,7 @@ What is sent to the browser will be the fllowing JavaScript code:
|
|
58
58
|
A new type of GOverlay is available, called GMarkerGroup. To use it you would have to include in your HTML template the JavaScript file markerGroup.js found in the <tt>lib/ym4r/google_maps/javascript</tt> directory of the distribution.
|
59
59
|
|
60
60
|
It is useful in 2 situations:
|
61
|
-
- Display and undisplay a group of markers without referencing all of them. You just declare your marker group globally and call +activate+ and +deactivate+ on this group in response, for example, to clicks on links on your page
|
61
|
+
- Display and undisplay a group of markers without referencing all of them. You just declare your marker group globally and call +activate+ and +deactivate+ on this group in response, for example, to clicks on links on your page.
|
62
62
|
- Keeping an index of markers, for example, in order to show one of these markers in reponse to a click on a link (the way Google Local does with the results of the search).
|
63
63
|
|
64
64
|
Here is how you would use it from your Ruby code:
|
@@ -76,7 +76,32 @@ Then in your template, you could have that:
|
|
76
76
|
<%= @map.to_div %>
|
77
77
|
When you click on one of the links, the corresponding marker has its info window displayed.
|
78
78
|
|
79
|
-
You can call +activate+ and +deactivate+ to display or undisplay a group of markers. You can add new markers with
|
79
|
+
You can call +activate+ and +deactivate+ to display or undisplay a group of markers. You can add new markers with <tt>addMarker(marker,id)</tt>. Again if you don't care about referencing the marker, you don't need to pass an id. If the marker group is active, the newly added marker will be displayed immediately. Otherwise it will be displayed the next time the group is activated. Finally, since it is an overlay, the group will be removed when calling clearOverlays on the GMap object.
|
80
|
+
|
81
|
+
====Clusterer
|
82
|
+
A Clusterer is a type of overlay that contains a potentially very large group of markers. It is engineered so markers close to each other and undistinguishable from each other at some level of zoom, appear as only one marker on the map at that level of zoom. Therefore it is possible to have a very large number of markers on the map at the same time and not having the map crawl to a halt in order to display them. It has been slightly modified from Jef Poskanzer's original Clusterer2 code (see http://www.acme.com/javascript/ for the original version). The major difference with that version is that, in YM4R, the clusterer is a GOverlay and can therefore be added to the map with <tt>map.addOverlay(clusterer)</tt> and is cleared along with the rest of overlays when <tt>map.clearOverlays()</tt> is called.
|
83
|
+
|
84
|
+
<b>In order to use a clusterer, you should include in your template page the clusterer.js file that can be found in the <tt>lib/ym4r/gogle_maps/javascript/</tt> directory of the YM4R distribution.</b>
|
85
|
+
|
86
|
+
To create a clusterer, you should first create an array of all the GMarkers that you want to include in the clusterer (you can still add more after the clusterer has been added to the map though). When GMarkers close together are grouped in one cluster (represented by another marker on the map) and the user clicks on the cluster marker, a list of markers in the cluster is displayed in the info windo with a description: By default it is equal to the +title+ of the markers (which is also displayed as a tooltip when hovering on the marker with the mouse). If you don't want that, you can also pass to the GMarker constructor a key-value pair with key <tt>:description</tt> to have a description different from the title. For example, here is how you would create a clusterer:
|
87
|
+
markers = [GMarker.new([37.8,-90.4819],:info_window => "Hello",:title => "HOYOYO"),
|
88
|
+
GMarker.new([37.844,-90.47819],:info_window => "Namaste",:description => "Chopoto" , :title => "Ay"),
|
89
|
+
GMarker.new([37.83,-90.456619],:info_window => "Bonjour",:title => "Third"),
|
90
|
+
]
|
91
|
+
clusterer = Clusterer.new(markers,:max_visible_markers => 2)
|
92
|
+
Of course, with only 3 markers, the whole clusterer thing is totally useless. Usually, using the clusterer starts being interesting with hundreds of markers. The options to pass the clusterer are:
|
93
|
+
- <tt>:max_visible_markers</tt>: Below this number of markers, no clustering is performed. Defaults to 150.
|
94
|
+
- <tt>:grid_size</tt>: The clusterer divides the visible area into a grid of this size. Defaults to 5.
|
95
|
+
- <tt>:min_markers_per_cluster</tt>: Below this number of markers a cluster is not formed. Defaults to 5.
|
96
|
+
- <tt>:max_lines_per_info_box</tt>: Number of lines to display in the info window displayed when a cluster is clicked on by the user. Defaults to 10.
|
97
|
+
- <tt>:icon</tt>: Icon to be used to mark a cluster. Defaults to G_DEFAULT_ICON (the classic red marker).
|
98
|
+
|
99
|
+
Then to add the clusterer to the map, you proceed as with a classic overlay:
|
100
|
+
map.addOverlay(clusterer)
|
101
|
+
|
102
|
+
To add new markers in RJS, you should do this:
|
103
|
+
page << clusterer.add_marker(marker,description)
|
104
|
+
In this case, the <tt>:description</tt> passed to the GMarker contructor will not be taken into account. Only the +description+ passed in the call to +add_marker+ will.
|
80
105
|
|
81
106
|
====Adding new map types
|
82
107
|
It is now possible to easily add new map types, on top of the already existing ones, like G_SATELLITE_MAP or G_NORMAL_MAP. The imagery for these new map types can come from layers of the standard map types or can be taken either from a WMS server or from pretiled images on a server (that can be generated with a tool that comes with the library).
|
@@ -96,9 +121,26 @@ Here is how to define a pretiled layer:
|
|
96
121
|
layer = PreTiledLayer.new("http://localhost:3000/tiles",
|
97
122
|
{'prefix' => "Map C 2006", 'copyright_texts' => ["Open Atlas"]},
|
98
123
|
13..13,0.7,"gif")
|
99
|
-
I tell the PreTiledLayer constructor where I can find the tiles, setup the Copyright string, the valid zooms for the map, the opacity and the format. Tiles must have standardized names: <tt>tile_#{zoom}_#{x_tile}_#{y_tile}.#{format}</tt> (here the format is "gif"). You can use some tools (see below) to generate tiles in this format either from local maps or from WMS servers (useful to create tiles from geographic data files without having to run a map server or to cache images from slow servers).
|
124
|
+
I tell the PreTiledLayer constructor where I can find the tiles, setup the Copyright string, the valid zooms for the map, the opacity and the format. Tiles must have standardized names: <tt>tile_#{zoom}_#{x_tile}_#{y_tile}.#{format}</tt> (here the format is "gif"). You can use some tools (see below) to generate tiles in this format either from local maps or from WMS servers (useful to create tiles from geographic data files without having to run a map server or to cache images from slow servers).
|
100
125
|
|
101
|
-
You can
|
126
|
+
Instead of having the tiles requested directly, you can also decide to have an action on the server which takes care of it. You can used the class PreTiledLayerFromAction for this. In this case, the first argument of the constructor is an url of an action. The arguments +x+, +y+ and +z+ will be passed to it.
|
127
|
+
layer = PreTiledLayerFromAction.new(url_for(:action => :get_tile),
|
128
|
+
{'prefix' => "Map C 2006", 'copyright_texts' => ["Open Atlas"]},
|
129
|
+
13..14,0.7,"png")
|
130
|
+
The other constructor arguments have the same meaning as PreTiledLayer. Here is an uninteresting example of action that serves tiles:
|
131
|
+
def get_tile
|
132
|
+
x = @params[:x]
|
133
|
+
y = @params[:y]
|
134
|
+
z = @params[:z]
|
135
|
+
begin
|
136
|
+
send_file "#{RAILS_ROOT}/public/tiles/tile_#{z}_#{x}_#{y}.png" ,
|
137
|
+
:type => 'image/png', :disposition => 'inline'
|
138
|
+
rescue Exception
|
139
|
+
render :nothing => true
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
You can add a layer to a new map type the following way:
|
102
144
|
map_type = GMapType.new(layer,"My WMS")
|
103
145
|
This is actually the simplest configuration possible. Your map type has only one data layer and is called "My WMS". You can add more that one layer: Either one that you have created yourself or existing ones. For example:
|
104
146
|
map_type = GMapType.new([GMapType::G_SATELLITE_MAP.get_tile_layers[0],layer,GMapType::G_HYBRID_MAP.get_tile_layers[1]],
|
@@ -202,19 +244,22 @@ Here is an example of request:
|
|
202
244
|
Preliminary support for this API has been added. It works along the same lines as with the Google Maps API but it is not very polished currently.
|
203
245
|
|
204
246
|
==Changes since last version
|
205
|
-
- Addition of
|
247
|
+
- Addition of Clusterer class
|
248
|
+
- Addition of a PreTiledLayerFromAction class
|
206
249
|
|
207
250
|
==TODO
|
208
|
-
- Add support for easy manipulation of external Google Maps-related libraries: Advanced tooltip manipulation, GeoRSS,
|
209
|
-
- Add support to show and hide groups of overlays easily
|
251
|
+
- Add support for easy manipulation of external Google Maps-related libraries: Advanced tooltip manipulation, GeoRSS, KML,...
|
210
252
|
- Documentation! Documentation! Documentation!
|
211
253
|
- Tutorials
|
212
254
|
|
213
255
|
==Disclaimer
|
214
256
|
This software is not endorsed in any way by Yahoo! or Google.
|
215
257
|
|
258
|
+
==Acknowledgement
|
259
|
+
YM4R bundles JavaScript libraries from John Deck (WMS layers on Google Maps) and Jef Poskanzer (Clusterer on Google Maps).
|
260
|
+
|
216
261
|
==License
|
217
|
-
YM4R is released under the MIT license.
|
262
|
+
YM4R is released under the MIT license. The <tt>clusterer.js</tt> file is redistributed with a different license (but still compatible with the MIT license). Check the top of the file in <tt>lib/ym4r/google_maps/javascript</tt> to know more.
|
218
263
|
|
219
264
|
==Support
|
220
265
|
Any questions, enhancement proposals, bug notifications or corrections can be sent to mailto:guilhem.vellut+ym4r@gmail.com.
|
@@ -0,0 +1,444 @@
|
|
1
|
+
// Clusterer.js - marker clustering routines for Google Maps apps
|
2
|
+
//
|
3
|
+
// The original version of this code is available at:
|
4
|
+
// http://www.acme.com/javascript/
|
5
|
+
//
|
6
|
+
// Copyright � 2005,2006 by Jef Poskanzer <jef@mail.acme.com>.
|
7
|
+
// All rights reserved.
|
8
|
+
//
|
9
|
+
// Modified for inclusion into the YM4R library in accordance with the
|
10
|
+
// following license:
|
11
|
+
//
|
12
|
+
// Redistribution and use in source and binary forms, with or without
|
13
|
+
// modification, are permitted provided that the following conditions
|
14
|
+
// are met:
|
15
|
+
// 1. Redistributions of source code must retain the above copyright
|
16
|
+
// notice, this list of conditions and the following disclaimer.
|
17
|
+
// 2. Redistributions in binary form must reproduce the above copyright
|
18
|
+
// notice, this list of conditions and the following disclaimer in the
|
19
|
+
// documentation and/or other materials provided with the distribution.
|
20
|
+
//
|
21
|
+
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
22
|
+
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
23
|
+
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
24
|
+
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
25
|
+
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
26
|
+
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
27
|
+
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
28
|
+
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
29
|
+
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
30
|
+
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
31
|
+
// SUCH DAMAGE.
|
32
|
+
//
|
33
|
+
// For commentary on this license please see http://www.acme.com/license.html
|
34
|
+
|
35
|
+
|
36
|
+
// Constructor.
|
37
|
+
Clusterer = function(markers,icon,maxVisibleMarkers,gridSize,minMarkersPerCluster,maxLinesPerInfoBox) {
|
38
|
+
this.markers = [];
|
39
|
+
if(markers){
|
40
|
+
for(var i =0 ; i< markers.length ; i++){
|
41
|
+
this.addMarker(markers[i]);
|
42
|
+
}
|
43
|
+
}
|
44
|
+
this.clusters = [];
|
45
|
+
this.timeout = null;
|
46
|
+
|
47
|
+
this.maxVisibleMarkers = maxVisibleMarkers || 150;
|
48
|
+
this.gridSize = gridSize || 5;
|
49
|
+
this.minMarkersPerCluster = minMarkersPerCluster || 5;
|
50
|
+
this.maxLinesPerInfoBox = maxLinesPerInfoBox || 10;
|
51
|
+
|
52
|
+
this.icon = icon || G_DEFAULT_ICON;
|
53
|
+
}
|
54
|
+
|
55
|
+
Clusterer.prototype = new GOverlay();
|
56
|
+
|
57
|
+
Clusterer.prototype.initialize = function ( map ){
|
58
|
+
this.map = map;
|
59
|
+
this.currentZoomLevel = map.getZoom();
|
60
|
+
|
61
|
+
GEvent.addListener( map, 'zoomend', Clusterer.makeCaller( Clusterer.display, this ) );
|
62
|
+
GEvent.addListener( map, 'moveend', Clusterer.makeCaller( Clusterer.display, this ) );
|
63
|
+
GEvent.addListener( map, 'infowindowclose', Clusterer.makeCaller( Clusterer.popDown, this ) );
|
64
|
+
//Set map for each marker
|
65
|
+
for(var i = 0,len = this.markers.length ; i < len ; i++){
|
66
|
+
this.markers[i].setMap( map );
|
67
|
+
}
|
68
|
+
this.displayLater();
|
69
|
+
}
|
70
|
+
|
71
|
+
Clusterer.prototype.remove = function(){
|
72
|
+
for ( var i = 0; i < this.markers.length; ++i ){
|
73
|
+
this.removeMarker(this.markers[i]);
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
Clusterer.prototype.copy = function(){
|
78
|
+
return new Clusterer(this.markers,this.icon,this.maxVisibleMarkers,this.gridSize,this.minMarkersPerCluster,this.maxLinesPerInfoBox);
|
79
|
+
}
|
80
|
+
|
81
|
+
Clusterer.prototype.redraw = function(force){
|
82
|
+
this.displayLater();
|
83
|
+
}
|
84
|
+
|
85
|
+
// Call this to change the cluster icon.
|
86
|
+
Clusterer.prototype.setIcon = function ( icon ){
|
87
|
+
this.icon = icon;
|
88
|
+
}
|
89
|
+
|
90
|
+
// Call this to add a marker.
|
91
|
+
Clusterer.prototype.addMarker = function ( marker, description){
|
92
|
+
marker.onMap = false;
|
93
|
+
this.markers.push( marker );
|
94
|
+
marker.description = marker.description || description;
|
95
|
+
if(this.map != null){
|
96
|
+
marker.setMap(this.map);
|
97
|
+
this.displayLater();
|
98
|
+
}
|
99
|
+
};
|
100
|
+
|
101
|
+
|
102
|
+
// Call this to remove a marker.
|
103
|
+
Clusterer.prototype.removeMarker = function ( marker ){
|
104
|
+
for ( var i = 0; i < this.markers.length; ++i )
|
105
|
+
if ( this.markers[i] == marker ){
|
106
|
+
if ( marker.onMap )
|
107
|
+
this.map.removeOverlay( marker );
|
108
|
+
for ( var j = 0; j < this.clusters.length; ++j ){
|
109
|
+
var cluster = this.clusters[j];
|
110
|
+
if ( cluster != null ){
|
111
|
+
for ( var k = 0; k < cluster.markers.length; ++k )
|
112
|
+
if ( cluster.markers[k] == marker ){
|
113
|
+
cluster.markers[k] = null;
|
114
|
+
--cluster.markerCount;
|
115
|
+
break;
|
116
|
+
}
|
117
|
+
if ( cluster.markerCount == 0 ){
|
118
|
+
this.clearCluster( cluster );
|
119
|
+
this.clusters[j] = null;
|
120
|
+
}
|
121
|
+
else if ( cluster == this.poppedUpCluster )
|
122
|
+
Clusterer.rePop( this );
|
123
|
+
}
|
124
|
+
}
|
125
|
+
this.markers[i] = null;
|
126
|
+
break;
|
127
|
+
}
|
128
|
+
this.displayLater();
|
129
|
+
};
|
130
|
+
|
131
|
+
Clusterer.prototype.displayLater = function (){
|
132
|
+
if ( this.timeout != null )
|
133
|
+
clearTimeout( this.timeout );
|
134
|
+
this.timeout = setTimeout( Clusterer.makeCaller( Clusterer.display, this ), 50 );
|
135
|
+
};
|
136
|
+
|
137
|
+
Clusterer.display = function ( clusterer ){
|
138
|
+
var i, j, marker, cluster, len, len2;
|
139
|
+
|
140
|
+
clearTimeout( clusterer.timeout );
|
141
|
+
|
142
|
+
var newZoomLevel = clusterer.map.getZoom();
|
143
|
+
if ( newZoomLevel != clusterer.currentZoomLevel ){
|
144
|
+
// When the zoom level changes, we have to remove all the clusters.
|
145
|
+
for ( i = 0 , len = clusterer.clusters.length; i < len; ++i ){
|
146
|
+
if ( clusterer.clusters[i] != null ){
|
147
|
+
clusterer.clearCluster( clusterer.clusters[i] );
|
148
|
+
clusterer.clusters[i] = null;
|
149
|
+
}
|
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.lng() - sw.lng();
|
163
|
+
var dy = ne.lat() - sw.lat();
|
164
|
+
dx *= 0.10;
|
165
|
+
dy *= 0.10;
|
166
|
+
bounds = new GLatLngBounds(
|
167
|
+
new GLatLng( sw.lat() - dy, sw.lng() - dx ),
|
168
|
+
new GLatLng( ne.lat() + dy, ne.lng() + dx )
|
169
|
+
);
|
170
|
+
|
171
|
+
// Partition the markers into visible and non-visible lists.
|
172
|
+
var visibleMarkers = [];
|
173
|
+
var nonvisibleMarkers = [];
|
174
|
+
for ( i = 0, len = clusterer.markers.length ; i < len; ++i ){
|
175
|
+
marker = clusterer.markers[i];
|
176
|
+
if ( marker != null )
|
177
|
+
if ( bounds.contains( marker.getPoint() ) )
|
178
|
+
visibleMarkers.push( marker );
|
179
|
+
else
|
180
|
+
nonvisibleMarkers.push( marker );
|
181
|
+
}
|
182
|
+
|
183
|
+
// Take down the non-visible markers.
|
184
|
+
for ( i = 0, len = nonvisibleMarkers.length ; i < len; ++i ){
|
185
|
+
marker = nonvisibleMarkers[i];
|
186
|
+
if ( marker.onMap ){
|
187
|
+
clusterer.map.removeOverlay( marker );
|
188
|
+
marker.onMap = false;
|
189
|
+
}
|
190
|
+
}
|
191
|
+
|
192
|
+
// Take down the non-visible clusters.
|
193
|
+
for ( i = 0, len = clusterer.clusters.length ; i < len ; ++i ){
|
194
|
+
cluster = clusterer.clusters[i];
|
195
|
+
if ( cluster != null && ! bounds.contains( cluster.marker.getPoint() ) && cluster.onMap ){
|
196
|
+
clusterer.map.removeOverlay( cluster.marker );
|
197
|
+
cluster.onMap = false;
|
198
|
+
}
|
199
|
+
}
|
200
|
+
|
201
|
+
// Clustering! This is some complicated stuff. We have three goals
|
202
|
+
// here. One, limit the number of markers & clusters displayed, so the
|
203
|
+
// maps code doesn't slow to a crawl. Two, when possible keep existing
|
204
|
+
// clusters instead of replacing them with new ones, so that the app pans
|
205
|
+
// better. And three, of course, be CPU and memory efficient.
|
206
|
+
if ( visibleMarkers.length > clusterer.maxVisibleMarkers ){
|
207
|
+
// Add to the list of clusters by splitting up the current bounds
|
208
|
+
// into a grid.
|
209
|
+
var latRange = bounds.getNorthEast().lat() - bounds.getSouthWest().lat();
|
210
|
+
var latInc = latRange / clusterer.gridSize;
|
211
|
+
var lngInc = latInc / Math.cos( ( bounds.getNorthEast().lat() + bounds.getSouthWest().lat() ) / 2.0 * Math.PI / 180.0 );
|
212
|
+
for ( var lat = bounds.getSouthWest().lat(); lat <= bounds.getNorthEast().lat(); lat += latInc )
|
213
|
+
for ( var lng = bounds.getSouthWest().lng(); lng <= bounds.getNorthEast().lng(); lng += lngInc ){
|
214
|
+
cluster = new Object();
|
215
|
+
cluster.clusterer = clusterer;
|
216
|
+
cluster.bounds = new GLatLngBounds( new GLatLng( lat, lng ), new GLatLng( lat + latInc, lng + lngInc ) );
|
217
|
+
cluster.markers = [];
|
218
|
+
cluster.markerCount = 0;
|
219
|
+
cluster.onMap = false;
|
220
|
+
cluster.marker = null;
|
221
|
+
clusterer.clusters.push( cluster );
|
222
|
+
}
|
223
|
+
|
224
|
+
// Put all the unclustered visible markers into a cluster - the first
|
225
|
+
// one it fits in, which favors pre-existing clusters.
|
226
|
+
for ( i = 0, len = visibleMarkers.length ; i < len; ++i ){
|
227
|
+
marker = visibleMarkers[i];
|
228
|
+
if ( marker != null && ! marker.inCluster ){
|
229
|
+
for ( j = 0, len2 = clusterer.clusters.length ; j < len2 ; ++j ){
|
230
|
+
cluster = clusterer.clusters[j];
|
231
|
+
if ( cluster != null && cluster.bounds.contains( marker.getPoint() ) ){
|
232
|
+
cluster.markers.push( marker );
|
233
|
+
++cluster.markerCount;
|
234
|
+
marker.inCluster = true;
|
235
|
+
}
|
236
|
+
}
|
237
|
+
}
|
238
|
+
}
|
239
|
+
|
240
|
+
// Get rid of any clusters containing only a few markers.
|
241
|
+
for ( i = 0, len = clusterer.clusters.length ; i < len ; ++i )
|
242
|
+
if ( clusterer.clusters[i] != null && clusterer.clusters[i].markerCount < clusterer.minMarkersPerCluster ){
|
243
|
+
clusterer.clearCluster( clusterer.clusters[i] );
|
244
|
+
clusterer.clusters[i] = null;
|
245
|
+
}
|
246
|
+
|
247
|
+
// Shrink the clusters list.
|
248
|
+
for ( i = clusterer.clusters.length - 1; i >= 0; --i )
|
249
|
+
if ( clusterer.clusters[i] != null )
|
250
|
+
break;
|
251
|
+
else
|
252
|
+
--clusterer.clusters.length;
|
253
|
+
|
254
|
+
// Ok, we have our clusters. Go through the markers in each
|
255
|
+
// cluster and remove them from the map if they are currently up.
|
256
|
+
for ( i = 0, len = clusterer.clusters.length ; i < len; ++i ){
|
257
|
+
cluster = clusterer.clusters[i];
|
258
|
+
if ( cluster != null ){
|
259
|
+
for ( j = 0 , len2 = cluster.markers.length ; j < len2; ++j ){
|
260
|
+
marker = cluster.markers[j];
|
261
|
+
if ( marker != null && marker.onMap ){
|
262
|
+
clusterer.map.removeOverlay( marker );
|
263
|
+
marker.onMap = false;
|
264
|
+
}
|
265
|
+
}
|
266
|
+
}
|
267
|
+
}
|
268
|
+
|
269
|
+
// Now make cluster-markers for any clusters that need one.
|
270
|
+
for ( i = 0, len = clusterer.clusters.length; i < len; ++i ){
|
271
|
+
cluster = clusterer.clusters[i];
|
272
|
+
if ( cluster != null && cluster.marker == null ){
|
273
|
+
// Figure out the average coordinates of the markers in this
|
274
|
+
// cluster.
|
275
|
+
var xTotal = 0.0, yTotal = 0.0;
|
276
|
+
for ( j = 0, len2 = cluster.markers.length; j < len2 ; ++j ){
|
277
|
+
marker = cluster.markers[j];
|
278
|
+
if ( marker != null ){
|
279
|
+
xTotal += ( + marker.getPoint().lng() );
|
280
|
+
yTotal += ( + marker.getPoint().lat() );
|
281
|
+
}
|
282
|
+
}
|
283
|
+
var location = new GLatLng( yTotal / cluster.markerCount, xTotal / cluster.markerCount );
|
284
|
+
marker = new GMarker( location, { icon: clusterer.icon } );
|
285
|
+
cluster.marker = marker;
|
286
|
+
GEvent.addListener( marker, 'click', Clusterer.makeCaller( Clusterer.popUp, cluster ) );
|
287
|
+
}
|
288
|
+
}
|
289
|
+
}
|
290
|
+
|
291
|
+
// Display the visible markers not already up and not in clusters.
|
292
|
+
for ( i = 0, len = visibleMarkers.length; i < len; ++i ){
|
293
|
+
marker = visibleMarkers[i];
|
294
|
+
if ( marker != null && ! marker.onMap && ! marker.inCluster )
|
295
|
+
{
|
296
|
+
clusterer.map.addOverlay( marker );
|
297
|
+
marker.addedToMap();
|
298
|
+
marker.onMap = true;
|
299
|
+
}
|
300
|
+
}
|
301
|
+
|
302
|
+
// Display the visible clusters not already up.
|
303
|
+
for ( i = 0, len = clusterer.clusters.length ; i < len; ++i ){
|
304
|
+
cluster = clusterer.clusters[i];
|
305
|
+
if ( cluster != null && ! cluster.onMap && bounds.contains( cluster.marker.getPoint() )){
|
306
|
+
clusterer.map.addOverlay( cluster.marker );
|
307
|
+
cluster.onMap = true;
|
308
|
+
}
|
309
|
+
}
|
310
|
+
|
311
|
+
// In case a cluster is currently popped-up, re-pop to get any new
|
312
|
+
// markers into the infobox.
|
313
|
+
Clusterer.rePop( clusterer );
|
314
|
+
};
|
315
|
+
|
316
|
+
|
317
|
+
Clusterer.popUp = function ( cluster ){
|
318
|
+
var clusterer = cluster.clusterer;
|
319
|
+
var html = '<table width="300">';
|
320
|
+
var n = 0;
|
321
|
+
for ( var i = 0 , len = cluster.markers.length; i < len; ++i )
|
322
|
+
{
|
323
|
+
var marker = cluster.markers[i];
|
324
|
+
if ( marker != null )
|
325
|
+
{
|
326
|
+
++n;
|
327
|
+
html += '<tr><td>';
|
328
|
+
if ( marker.getIcon().smallImage != null )
|
329
|
+
html += '<img src="' + marker.getIcon().smallImage + '">';
|
330
|
+
else
|
331
|
+
html += '<img src="' + marker.getIcon().image + '" width="' + ( marker.getIcon().iconSize.width / 2 ) + '" height="' + ( marker.getIcon().iconSize.height / 2 ) + '">';
|
332
|
+
html += '</td><td>' + marker.description + '</td></tr>';
|
333
|
+
if ( n == clusterer.maxLinesPerInfoBox - 1 && cluster.markerCount > clusterer.maxLinesPerInfoBox )
|
334
|
+
{
|
335
|
+
html += '<tr><td colspan="2">...and ' + ( cluster.markerCount - n ) + ' more</td></tr>';
|
336
|
+
break;
|
337
|
+
}
|
338
|
+
}
|
339
|
+
}
|
340
|
+
html += '</table>';
|
341
|
+
clusterer.map.closeInfoWindow();
|
342
|
+
cluster.marker.openInfoWindowHtml( html );
|
343
|
+
clusterer.poppedUpCluster = cluster;
|
344
|
+
};
|
345
|
+
|
346
|
+
Clusterer.rePop = function ( clusterer ){
|
347
|
+
if ( clusterer.poppedUpCluster != null )
|
348
|
+
Clusterer.popUp( poppedUpCluster );
|
349
|
+
};
|
350
|
+
|
351
|
+
Clusterer.popDown = function ( clusterer ){
|
352
|
+
clusterer.poppedUpCluster = null;
|
353
|
+
};
|
354
|
+
|
355
|
+
Clusterer.prototype.clearCluster = function ( cluster ){
|
356
|
+
var i, marker;
|
357
|
+
|
358
|
+
for ( i = 0; i < cluster.markers.length; ++i ){
|
359
|
+
if ( cluster.markers[i] != null ){
|
360
|
+
cluster.markers[i].inCluster = false;
|
361
|
+
cluster.markers[i] = null;
|
362
|
+
}
|
363
|
+
}
|
364
|
+
|
365
|
+
cluster.markers.length = 0;
|
366
|
+
cluster.markerCount = 0;
|
367
|
+
|
368
|
+
if ( cluster == this.poppedUpCluster )
|
369
|
+
this.map.closeInfoWindow();
|
370
|
+
|
371
|
+
if ( cluster.onMap )
|
372
|
+
{
|
373
|
+
this.map.removeOverlay( cluster.marker );
|
374
|
+
cluster.onMap = false;
|
375
|
+
}
|
376
|
+
};
|
377
|
+
|
378
|
+
// This returns a function closure that calls the given routine with the
|
379
|
+
// specified arg.
|
380
|
+
Clusterer.makeCaller = function ( func, arg ){
|
381
|
+
return function () { func( arg ); };
|
382
|
+
};
|
383
|
+
|
384
|
+
|
385
|
+
// Augment GMarker so it handles markers that have been created but
|
386
|
+
// not yet addOverlayed.
|
387
|
+
GMarker.prototype.setMap = function ( map ){
|
388
|
+
this.map = map;
|
389
|
+
};
|
390
|
+
|
391
|
+
GMarker.prototype.getMap = function (){
|
392
|
+
return this.map;
|
393
|
+
}
|
394
|
+
|
395
|
+
GMarker.prototype.addedToMap = function (){
|
396
|
+
this.map = null;
|
397
|
+
};
|
398
|
+
|
399
|
+
|
400
|
+
GMarker.prototype.origOpenInfoWindow = GMarker.prototype.openInfoWindow;
|
401
|
+
GMarker.prototype.openInfoWindow = function ( node, opts ){
|
402
|
+
if ( this.map != null )
|
403
|
+
return this.map.openInfoWindow( this.getPoint(), node, opts );
|
404
|
+
else
|
405
|
+
return this.origOpenInfoWindow( node, opts );
|
406
|
+
};
|
407
|
+
|
408
|
+
GMarker.prototype.origOpenInfoWindowHtml = GMarker.prototype.openInfoWindowHtml;
|
409
|
+
GMarker.prototype.openInfoWindowHtml = function ( html, opts ){
|
410
|
+
if ( this.map != null )
|
411
|
+
return this.map.openInfoWindowHtml( this.getPoint(), html, opts );
|
412
|
+
else
|
413
|
+
return this.origOpenInfoWindowHtml( html, opts );
|
414
|
+
};
|
415
|
+
|
416
|
+
GMarker.prototype.origOpenInfoWindowTabs = GMarker.prototype.openInfoWindowTabs;
|
417
|
+
GMarker.prototype.openInfoWindowTabs = function ( tabNodes, opts ){
|
418
|
+
if ( this.map != null )
|
419
|
+
return this.map.openInfoWindowTabs( this.getPoint(), tabNodes, opts );
|
420
|
+
else
|
421
|
+
return this.origOpenInfoWindowTabs( tabNodes, opts );
|
422
|
+
};
|
423
|
+
|
424
|
+
GMarker.prototype.origOpenInfoWindowTabsHtml = GMarker.prototype.openInfoWindowTabsHtml;
|
425
|
+
GMarker.prototype.openInfoWindowTabsHtml = function ( tabHtmls, opts ){
|
426
|
+
if ( this.map != null )
|
427
|
+
return this.map.openInfoWindowTabsHtml( this.getPoint(), tabHtmls, opts );
|
428
|
+
else
|
429
|
+
return this.origOpenInfoWindowTabsHtml( tabHtmls, opts );
|
430
|
+
};
|
431
|
+
|
432
|
+
GMarker.prototype.origShowMapBlowup = GMarker.prototype.showMapBlowup;
|
433
|
+
GMarker.prototype.showMapBlowup = function ( opts ){
|
434
|
+
if ( this.map != null )
|
435
|
+
return this.map.showMapBlowup( this.getPoint(), opts );
|
436
|
+
else
|
437
|
+
return this.origShowMapBlowup( opts );
|
438
|
+
};
|
439
|
+
|
440
|
+
|
441
|
+
function addDescriptionToMarker(marker, description){
|
442
|
+
marker.description = description;
|
443
|
+
return marker;
|
444
|
+
}
|
@@ -43,16 +43,17 @@ module Ym4r
|
|
43
43
|
class GTileLayer
|
44
44
|
include MappingObject
|
45
45
|
|
46
|
-
attr_accessor :opacity, :zoom_inter, :copyright
|
46
|
+
attr_accessor :opacity, :zoom_inter, :copyright, :format
|
47
47
|
|
48
|
-
def initialize(zoom_inter = 0..17, copyright= {'prefix' => '', 'copyright_texts' => [""]}, opacity = 1.0)
|
48
|
+
def initialize(zoom_inter = 0..17, copyright= {'prefix' => '', 'copyright_texts' => [""]}, opacity = 1.0, format = "png")
|
49
49
|
@opacity = opacity
|
50
50
|
@zoom_inter = zoom_inter
|
51
51
|
@copyright = copyright
|
52
|
+
@format = format.to_s
|
52
53
|
end
|
53
54
|
|
54
55
|
def create
|
55
|
-
"addPropertiesToLayer(new GTileLayer(new GCopyrightCollection(\"\"),#{zoom_inter.begin},#{zoom_inter.end}),#{get_tile_url},function(a,b) {return #{MappingObject.javascriptify_variable(@copyright)};}\n,function() {return #{@opacity};})"
|
56
|
+
"addPropertiesToLayer(new GTileLayer(new GCopyrightCollection(\"\"),#{zoom_inter.begin},#{zoom_inter.end}),#{get_tile_url},function(a,b) {return #{MappingObject.javascriptify_variable(@copyright)};}\n,function() {return #{@opacity};},function(){return #{@format == "png"};})"
|
56
57
|
end
|
57
58
|
|
58
59
|
#for subclasses to implement
|
@@ -60,32 +61,38 @@ module Ym4r
|
|
60
61
|
end
|
61
62
|
end
|
62
63
|
|
64
|
+
#Represents a pre tiled layer, taking images directly from a server, without using a server script.
|
63
65
|
class PreTiledLayer < GTileLayer
|
64
|
-
attr_accessor :base_url
|
66
|
+
attr_accessor :base_url
|
65
67
|
|
66
|
-
def initialize(base_url = "
|
67
|
-
super(zoom_inter, copyright, opacity)
|
68
|
+
def initialize(base_url = "/public/tiles", copyright = {'prefix' => '', 'copyright_texts' => [""]}, zoom_inter = 0..17, opacity = 1.0,format = "png")
|
69
|
+
super(zoom_inter, copyright, opacity,format)
|
68
70
|
@base_url = base_url
|
69
|
-
@format = format
|
70
71
|
end
|
71
72
|
|
72
73
|
#returns the code to determine the url to fetch the tile. Follows the convention adopted by the tiler: {base_url}/tile_{b}_{a.x}_{a.y}.{format}
|
73
74
|
def get_tile_url
|
74
|
-
"function(a,b,c) { return '#{@base_url}/tile_' + b + '_' + a.x + '_' + a.y + '.#{
|
75
|
+
"function(a,b,c) { return '#{@base_url}/tile_' + b + '_' + a.x + '_' + a.y + '.#{format}';}"
|
75
76
|
end
|
76
77
|
end
|
78
|
+
|
79
|
+
#Represents a pretiled layer (it actually does not really matter where the tiles come from). Calls an action on the server to get back the tiles. It can be used, for example, to return default tiles when the requested tile is not present.
|
80
|
+
class PreTiledLayerFromAction < PreTiledLayer
|
81
|
+
def get_tile_url
|
82
|
+
"function(a,b,c) { return '#{base_url}?x=' + a.x + '&y=' + a.y + '&z=' + b;}"
|
83
|
+
end
|
84
|
+
end
|
77
85
|
|
78
86
|
#needs to include the JavaScript file wms-gs.js for this to work
|
79
87
|
#see http://docs.codehaus.org/display/GEOSDOC/Google+Maps
|
80
88
|
class WMSLayer < GTileLayer
|
81
|
-
attr_accessor :base_url, :layers, :styles, :
|
89
|
+
attr_accessor :base_url, :layers, :styles, :merc_proj, :use_geographic
|
82
90
|
|
83
91
|
def initialize(base_url, layers, styles = "", copyright = {'prefix' => '', 'copyright_texts' => [""]}, use_geographic = false, merc_proj = :mapserver, zoom_inter = 0..17, opacity = 1.0,format= "png")
|
84
|
-
super(zoom_inter, copyright, opacity)
|
92
|
+
super(zoom_inter, copyright, opacity,format)
|
85
93
|
@base_url = base_url
|
86
94
|
@layers = layers
|
87
95
|
@styles = styles
|
88
|
-
@format = format
|
89
96
|
@merc_proj = if merc_proj == :mapserver
|
90
97
|
"54004"
|
91
98
|
elsif merc_proj == :geoserver
|
@@ -101,7 +108,7 @@ module Ym4r
|
|
101
108
|
end
|
102
109
|
|
103
110
|
def create
|
104
|
-
"addWMSPropertiesToLayer(#{super},#{MappingObject.javascriptify_variable(@base_url)},#{MappingObject.javascriptify_variable(@layers)},#{MappingObject.javascriptify_variable(@styles)},#{MappingObject.javascriptify_variable(
|
111
|
+
"addWMSPropertiesToLayer(#{super},#{MappingObject.javascriptify_variable(@base_url)},#{MappingObject.javascriptify_variable(@layers)},#{MappingObject.javascriptify_variable(@styles)},#{MappingObject.javascriptify_variable(format)},#{MappingObject.javascriptify_variable(@merc_proj)},#{MappingObject.javascriptify_variable(@use_geographic)})"
|
105
112
|
end
|
106
113
|
end
|
107
114
|
end
|
data/lib/ym4r/google_maps/map.rb
CHANGED
@@ -117,7 +117,8 @@ module Ym4r
|
|
117
117
|
#put the functions in a separate javascript file to be included in the page
|
118
118
|
html << "function addInfoWindowToMarker(marker,info){\nGEvent.addListener(marker, \"click\", function() {\nmarker.openInfoWindowHtml(info);\n});\nreturn marker;\n}\n"
|
119
119
|
html << "function addInfoWindowTabsToMarker(marker,info){\nGEvent.addListener(marker, \"click\", function() {\nmarker.openInfoWindowTabsHtml(info);\n});\nreturn marker;\n}\n"
|
120
|
-
html << "function addPropertiesToLayer(layer,getTile,copyright,opacity){\nlayer.getTileUrl = getTile;\nlayer.getCopyright = copyright;\nlayer.getOpacity = opacity;\nreturn layer;\n}\n"
|
120
|
+
html << "function addPropertiesToLayer(layer,getTile,copyright,opacity,isPng){\nlayer.getTileUrl = getTile;\nlayer.getCopyright = copyright;\nlayer.getOpacity = opacity;\nlayer.isPng = isPng;\nreturn layer;\n}\n"
|
121
|
+
html << "function addOptionsToIcon(icon,options){\nfor(var k in options){\nicon[k] = options[k];\n}\nreturn icon;\n}\n"
|
121
122
|
html << @global_init * "\n"
|
122
123
|
html << "var #{@variable};\n" if !no_declare and !no_global
|
123
124
|
html << "window.onload = function() {\nif (GBrowserIsCompatible()) {\n" if !no_load
|
@@ -56,18 +56,15 @@ module Ym4r
|
|
56
56
|
#Creates a GIcon.
|
57
57
|
def create
|
58
58
|
if @copy_base
|
59
|
-
"new GIcon(#{MappingObject.javascriptify_variable(@copy_base)})"
|
59
|
+
c = "new GIcon(#{MappingObject.javascriptify_variable(@copy_base)})"
|
60
60
|
else
|
61
|
-
"new GIcon()"
|
61
|
+
c = "new GIcon()"
|
62
62
|
end
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
@options.each do |key,value|
|
68
|
-
decl << "#{to_javascript}.#{MappingObject.javascriptify_method(key.to_s)} = #{MappingObject.javascriptify_variable(value)};\n"
|
63
|
+
if !options.empty?
|
64
|
+
"addOptionsToIcon(#{c},#{MappingObject.javascriptify_variable(@options)})"
|
65
|
+
else
|
66
|
+
c
|
69
67
|
end
|
70
|
-
decl
|
71
68
|
end
|
72
69
|
end
|
73
70
|
|
@@ -96,6 +93,7 @@ module Ym4r
|
|
96
93
|
end
|
97
94
|
end
|
98
95
|
|
96
|
+
#A GOverlay representing a group of GMarkers. The GMarkers can be identified with an id, which can be used to show the info window of a specific marker, in reponse, for example, to a click on a link. The whole group can be shown on and off at once. It should be declared global at initialization time to be useful.
|
99
97
|
class GMarkerGroup
|
100
98
|
include MappingObject
|
101
99
|
attr_accessor :active, :markers, :markers_by_id
|
@@ -115,6 +113,31 @@ module Ym4r
|
|
115
113
|
"new GMarkerGroup(#{MappingObject.javascriptify_variable(@active)},#{MappingObject.javascriptify_variable(@markers)},#{MappingObject.javascriptify_variable(@markers_by_id)})"
|
116
114
|
end
|
117
115
|
end
|
116
|
+
|
117
|
+
#Makes the link with the Clusterer2 library by Jef Poskanzer (slightly modified though). Is a GOverlay making clusters out of its GMarkers, so that GMarkers very close to each other appear as one when the zoom is low. When the zoom gets higher, the individual markers are drawn.
|
118
|
+
class Clusterer
|
119
|
+
include MappingObject
|
120
|
+
attr_accessor :markers,:options
|
121
|
+
|
122
|
+
def initialize(markers = [], options = {})
|
123
|
+
@markers = markers
|
124
|
+
@options = options
|
125
|
+
end
|
126
|
+
|
127
|
+
def create
|
128
|
+
js_marker = '[' + @markers.collect do |marker|
|
129
|
+
add_description(marker)
|
130
|
+
end.join(",") + ']'
|
131
|
+
|
132
|
+
"new Clusterer(#{js_marker},#{MappingObject.javascriptify_variable(@options[:icon] || GIcon::DEFAULT)},#{@options[:max_visible_markers] || 150},#{@options[:grid_size] || 5},#{@options[:min_markers_per_cluster] || 5},#{@options[:max_lines_per_info_box] || 10})"
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
def add_description(marker)
|
137
|
+
"addDescriptionToMarker(#{MappingObject.javascriptify_variable(marker)},#{MappingObject.javascriptify_variable(marker.options[:description] || marker.options[:title] || '')})"
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
118
141
|
|
119
142
|
#A basic Latitude/longitude point.
|
120
143
|
class GLatLng
|
data/rakefile.rb
CHANGED
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.11
|
|
3
3
|
specification_version: 1
|
4
4
|
name: ym4r
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.3.
|
7
|
-
date: 2006-06-
|
6
|
+
version: 0.3.2
|
7
|
+
date: 2006-06-23 00:00:00 +05:00
|
8
8
|
summary: Using Google Maps and Yahoo! Maps from Ruby and Rails
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -59,6 +59,7 @@ files:
|
|
59
59
|
- lib/ym4r/yahoo_maps/flash/tool.rb
|
60
60
|
- lib/ym4r/yahoo_maps/flash/widget.rb
|
61
61
|
- lib/ym4r/google_maps/config/config.yml
|
62
|
+
- lib/ym4r/google_maps/javascript/clusterer.js
|
62
63
|
- lib/ym4r/google_maps/javascript/markerGroup.js
|
63
64
|
- lib/ym4r/google_maps/javascript/wms-gs.js
|
64
65
|
- tools/tile_image.rb
|