├── .gitignore
├── CHANGELOG
├── LICENSE
├── README.markdown
├── Rakefile
├── jquery.jmapping.js
├── jquery.jmapping.min.js
├── spec
├── jmapping_spec.js
├── screw.css
├── screw.unit
│ ├── jquery.fn.js
│ ├── jquery.print.js
│ ├── screw.behaviors.js
│ ├── screw.builder.js
│ ├── screw.events.js
│ └── screw.matchers.js
├── smoke
│ ├── screw.mocking.js
│ ├── smoke.core.js
│ ├── smoke.mock.js
│ └── smoke.stub.js
├── spec_helper.js
└── suite.html
└── vendor
├── StyledMarker.js
├── jquery-1.4.4.js
├── jquery.metadata.js
├── jsmin.rb
└── markermanager.js
/.gitignore:
--------------------------------------------------------------------------------
1 | pkg/*
2 | pkg/**/*
3 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | == 2.1.0 / 2011-01-28
2 | * Fixed `force_zoom_level` & `default_zoom_level`
3 | * Add `always_show_markers` option (mokolabs)
4 |
5 | == 2.0.0 / 2010-11-24
6 | * Use version 3 of Google Maps API
7 | * Add `default_point` setting. (gsterndale)
8 | * Add `force_zoom_level` setting.
9 | * Use StyledMarker instead of MapIconMaker as MapIconMaker is incompatible with v3 of the Google Maps API.
10 |
11 | == 1.4.0 / 2010-07-11
12 |
13 | * Uncache metadata on update.
14 | * Allow creating many different types of markers based on categories.
15 |
16 | == 1.3.0 / 2010-07-11
17 |
18 | * Use data-jmapping attribute for metadata by default.
19 | * add `default_zoom_level` option
20 |
21 | == 1.2.1 / 2010-03-12
22 |
23 | * Also call `map.checkResize()` when updating the map.
24 |
25 | == 1.2 / 2010-03-12
26 |
27 | * Add new object and event API.
28 |
29 | == 1.1 / 2009-08-24
30 |
31 | * Allow updating the map.
32 |
33 | == 1.0 / 2009-07-15
34 |
35 | * Initial Release.
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | (The MIT License)
2 |
3 | Copyright (c) 2009-2010 Brian Landau of Viget Labs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | 'Software'), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | jMapping - Google Maps jQuery Plugin
2 | =====================================
3 |
4 | This plugin is designed for quick development of a page that implements
5 | a Google Map with a list of the locations that are specified within the HTML.
6 |
7 |
8 | ### Links
9 |
10 | * [Documentation](http://vigetlabs.github.com/jmapping/ "jMapping Documentation")
11 | * [Examples](http://vigetlabs.github.com/jmapping/examples/ "jMapping: Examples")
12 | * [Repository@GitHub](http://github.com/vigetlabs/jmapping)
13 | * [Downloads](http://wiki.github.com/vigetlabs/jmapping/downloads)
14 |
15 |
16 | Graceful degradation and Semantic Expectations
17 | -----------------------------------------------
18 |
19 | This plugin tries to allow as much graceful degradation as possible by having the HTML be as semantic as possible.
20 | The plugin expect the HTML for the locations to be grouped under a common element.
21 | Additionally, it expects the links and Map Info Window content to be grouped under the location elements.
22 | It also expects the necessary metadata to be on the location element.
23 | This way the HTML semantically reflects that all of those parts and information are associated with the specific location or place.
24 |
25 |
26 | Basic Usage
27 | ------------
28 |
29 | Download the necessary dependencies and jMapping.
30 |
31 | Make sure you include the necessary scripts in your page:
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | Make sure your HTML has a `div` element for the Google map, and there is a container element with some locations and their data. The data by default is expected to be on the "data-jmapping" attribute of the location (this can be configured):
41 |
42 |
58 |
59 | Then just call the `jMapping` function on the map element:
60 |
61 | $(document).ready(function(){
62 | $('#map').jMapping();
63 | });
64 |
65 |
66 | If you need to change the markers on the map, usually for some type of pagination, this can be done by simply updating the content of the "side-bar" container to contain new location data and then calling the update function:
67 |
68 | $('#map-side-bar').load('ajax/path/file.html', nil, function(){
69 | $('#map').jMapping('update');
70 | });
71 |
72 | *OR*
73 |
74 | $('#map-side-bar').load('ajax/path/file.html', nil, function(){
75 | $('#map').data('jMapping').update();
76 | });
77 |
78 |
79 | Options
80 | --------
81 |
82 | These are options that can be passed to the `jMapping` function to change specific settings.
83 |
84 |
85 | * `side_bar_selector`:
86 | * *Default*: `"#map-side-bar:first"`
87 | * This defines the selector where location items will be searched for within.
88 | * `location_selector`:
89 | * *Default*: `".map-location"`
90 | * This defines the selector for location items. This is the element that the metadata
91 | needs to be associated with. The plugin will look for items matching this selector *inside* of the side bar element.
92 | * `link_selector`:
93 | * *Default*: `a.map-link`
94 | * This selector defines the link that will be used to "activate" a marker on the map.
95 | If info window elements are provided the HTML inside of them will be loaded into
96 | the pop up window when these links are clicked. You should set this value to `false`
97 | if you do not wish to use this functionality.
98 | These links will be searched for inside of the location elements specified in the `location_selector`.
99 | * `info_window_selector`:
100 | * *Default*: `".info-box"`
101 | * This selector defines where the content for the Google Maps info window for each location marker is.
102 | This element will be searched for inside of the location elements specified in the `location_selector`.
103 | If no element is found no Info Window will be attached to the marker.
104 | * `default_point`:
105 | * *Default*: `{lat: 0.0, lng: 0.0}`
106 | * This point determines the Google Maps location if there are no location elements inside the specified `location_selector`.
107 | * `metadata_options`:
108 | * *Default*: `{type: "attr", name: "data"}`
109 | * This is the set of options passed to the jQuery metadata function. It defines how the necessary
110 | metadata for each location will be searched for.
111 | See the [metadata plugins docs](http://docs.jquery.com/Plugins/Metadata/metadata#toptions) for more info.
112 | * `info_window_max_width`:
113 | * *Default*: `425`
114 | * This specifies what the max width of the Google Maps Info Windows can be when a marker is activated.
115 | Otherwise it will expand to fit the width of the content.
116 | * `map_config`:
117 | * *Default*: *N/A*
118 | * This can be set to a [MapOptions object](http://code.google.com/apis/maps/documentation/javascript/reference.html#MapOptions). Which is just a normal object `{}` with specific properties that become the settings for the map.
119 | * `category_icon_options`:
120 | * *Default*: *N/A*
121 | * By default the plugin will use the default Google Maps marker icon. But you can use this option
122 | to specify what options to pass to the StyledMarker based on category data associated with the location.
123 | It accepts 2 types of values: an object or a function.
124 | If the setting is to an object it will take the category data on the location and look for a key on the object
125 | that matches and return that value. If there is no value for the supplied category, it will return
126 | the value specified in the "default" key.
127 | If the setting is set to a function it will call the function and pass the value for the
128 | category data to the function, returning the result. This can be used for more complicated logic and for
129 | using something other then just string data in the category, such as an object with multiple data
130 | attributes it's self.
131 | The object values for the associated category key or the function should return one of three data types:
132 | 1. A string, this will be used as the image source for the marker icon.
133 | 2. A [google.maps.MarkerImage](http://code.google.com/apis/maps/documentation/javascript/reference.html#MarkerImage), this will be used as the icon for the Marker object.
134 | 3. An object that has [valid options for a StyledIcon object](http://google-maps-utility-library-v3.googlecode.com/svn/trunk/styledmarker/docs/reference.html#StyledIconOptions).
135 | * `default_zoom_level`:
136 | * *Default*: *N/A*
137 | * Use this option to set the default zoom level for your map. Normally, zoom level is set dynamically based on the position of locations being mapped. But, in some cases, like viewing a single mapped location, you may wish to set a default zoom level. Zoom level values should be between 1 and 20. Neighborhood level is approximately 15.
138 | * `force_zoom_level`:
139 | * *Default*: *N/A*
140 | * This will force the map to **always** be rendered at this zoom level.
141 | * `always_show_markers`:
142 | * *Default*: `false`
143 | * Set this option to `true` if you wish to display markers on all zoom levels. (Normally, the markers may only be visible on certain zoom levels, depending on the normal bounds and zoom level of the marker data.)
144 |
145 | Object API
146 | -----------
147 |
148 | The jMapping API object is available from the "jMapping" data value on
149 | the selector passed to the original $().jMapping function.
150 |
151 | For example:
152 |
153 | $('#map').jMapping();
154 | $('#map').data('jMapping'); // returns the jMapping API object
155 |
156 | The API of that object:
157 |
158 | * `gmarkers`:
159 | * The google.maps.Marker objects that have been placed on the map.
160 | Stored in an object where the keys are the id's are those provided in the metadata
161 | * `settings`:
162 | * The settings for this jMapping instance.
163 | * `mapped`:
164 | * Did the plugin create the map and markers as expected or not.
165 | * `map`:
166 | * the google.maps.Map Google Map API object.
167 | * `markerManager`:
168 | * The Google Maps MarkerManager object for manipulating groups of markers, has control over all markers on the map.
169 | * `gmarkersArray`:
170 | * Returns an array of all the markers currently on the map.
171 | * `getBounds`:
172 | * The Google Maps google.maps.LatLngBounds bounds object for all the markers on the map.
173 | * `getPlaces`:
174 | * Returns the set of jQuery objects for the place DOM Elements.
175 | * `getPlacesData`:
176 | * Returns an array of all the metadata for each place returned by `getPlaces`
177 | * `update`:
178 | * Used to update the map if the HTML DOM for the locations has changed.
179 |
180 |
181 | Event API
182 | ----------
183 |
184 | There a number of events that fire as jMapping is used.
185 |
186 | * `beforeMapping.jMapping`
187 | * This fires immediately before the main functionality of the plugin begins and is passed the settings object.
188 | If it returns false the mapping will be canceled.
189 | * `afterMapping.jMapping`
190 | * This fires immediately after the plugins mapping has finished, passes in the jMapping API object.
191 | * `beforeUpdate.jMapping`
192 | * This fires right before the map is updated via the "update" method. The jMapping API object is passed to the callback.
193 | If the callback returns false the update will be canceled.
194 | * `afterUpdate.jMapping`
195 | * This fires immediately after the map is updated.
196 | * `markerCreated.jMapping`
197 | * This fires right after a map marker is created, the marker object is passed to the callback.
198 |
199 |
200 | Dependencies
201 | -------------
202 |
203 | * [jQuery 1.4.4](http://docs.jquery.com/Downloading_jQuery)
204 | * [jQuery Metadata plugin 2.1](http://plugins.jquery.com/project/metadata)
205 | * [MarkerManager v3 1.0](http://google-maps-utility-library-v3.googlecode.com/svn/tags/markermanager/1.0)
206 | * [StyledMarker 0.5](http://google-maps-utility-library-v3.googlecode.com/svn/trunk/styledmarker)
207 |
208 |
209 | License
210 | --------
211 |
212 | Copyright (c) 2009-2011 Brian Landau (Viget Labs)
213 | MIT License: [http://www.opensource.org/licenses/mit-license.php](http://www.opensource.org/licenses/mit-license.php)
214 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rake'
2 | require 'rake/clean'
3 | require 'rake/packagetask'
4 | require File.join(File.dirname(__FILE__), 'vendor/jsmin.rb')
5 |
6 | Object.send(:remove_const, :CLEAN) if defined?(CLEAN)
7 | CLEAN = FileList.new
8 | CLOBBER.add('jquery.jmapping.min.js')
9 |
10 | file 'jquery.jmapping.min.js' => ['jquery.jmapping.js'] do
11 | original_js = File.read('jquery.jmapping.js')
12 | print " - Minifing JS ... "
13 | minified_js = JSMin.minify(original_js)
14 | open('jquery.jmapping.min.js', 'w') do |file|
15 | file << minified_js
16 | end
17 | print "Done!\n"
18 | end
19 |
20 | task :minify => ['jquery.jmapping.min.js']
21 |
22 | Rake::PackageTask.new("jquery.jmapping", (ENV['VERSION'] || :noversion)) do |p|
23 | p.need_tar_gz = true
24 | p.need_zip = true
25 | p.package_files.add 'jquery.*.js', 'README.markdown', 'CHANGELOG', 'vendor/jquery.metadata.js',
26 | 'vendor/StyledMarker.js', 'vendor/markermanager.js'
27 | end
28 |
29 | if PLATFORM['darwin']
30 | task :test do
31 | sh 'open -a Firefox spec/suite.html' do |ok, status|
32 | unless ok
33 | sh 'open spec/suite.html'
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/jquery.jmapping.js:
--------------------------------------------------------------------------------
1 | /*
2 | * jMapping v2.1.0 - jQuery plugin for creating Google Maps
3 | *
4 | * Copyright (c) 2009-2010 Brian Landau (Viget Labs)
5 | * MIT License: http://www.opensource.org/licenses/mit-license.php
6 | *
7 | */
8 |
9 | (function($){
10 | $.jMapping = function(map_elm, options){
11 | var settings, gmarkers, mapped, map, markerManager, places, bounds, jMapper, info_windows;
12 | map_elm = (typeof map_elm == "string") ? $(map_elm).get(0) : map_elm;
13 |
14 | if (!($(map_elm).data('jMapping'))){ // TODO: Should we use a different test here?
15 | settings = $.extend(true, {}, $.jMapping.defaults);
16 | $.extend(true, settings, options);
17 | gmarkers = {};
18 | info_windows = [];
19 |
20 | var init = function(doUpdate){
21 | var info_window_selector, min_zoom, zoom_level;
22 |
23 | info_window_selector = [
24 | settings.side_bar_selector,
25 | settings.location_selector,
26 | settings.info_window_selector
27 | ].join(' ');
28 | $(info_window_selector).hide();
29 |
30 | places = getPlaces();
31 | bounds = getBounds(doUpdate);
32 |
33 | if (doUpdate){
34 | gmarkers = {};
35 | info_windows = [];
36 | markerManager.clearMarkers();
37 | google.maps.event.trigger(map, 'resize');
38 | map.fitBounds(bounds);
39 | if (settings.force_zoom_level){
40 | map.setZoom(settings.force_zoom_level);
41 | }
42 | } else {
43 | map = createMap();
44 | markerManager = new MarkerManager(map);
45 | }
46 |
47 | places.each(function(){
48 | var marker = createMarker(this);
49 | if (!(settings.link_selector === false)){
50 | setupLink(this);
51 | }
52 | $(document).trigger('markerCreated.jMapping', [marker]);
53 | });
54 |
55 | if (doUpdate){
56 | updateMarkerManager();
57 | } else {
58 | google.maps.event.addListener(markerManager, 'loaded', function(){
59 | updateMarkerManager();
60 | if (settings.default_zoom_level){
61 | map.setZoom(settings.default_zoom_level);
62 | }
63 | });
64 | }
65 |
66 | if (!(settings.link_selector === false) && !doUpdate){
67 | attachMapsEventToLinks();
68 | }
69 | };
70 |
71 | var createMap = function(){
72 | if (settings.map_config){
73 | map = new google.maps.Map(map_elm, settings.map_config);
74 | } else {
75 | map = new google.maps.Map(map_elm, {
76 | navigationControlOptions: {
77 | style: google.maps.NavigationControlStyle.SMALL
78 | },
79 | mapTypeControl: false,
80 | mapTypeId: google.maps.MapTypeId.ROADMAP,
81 | zoom: 9
82 | });
83 | }
84 | map.fitBounds(bounds);
85 | if (settings.force_zoom_level){
86 | map.setZoom(settings.force_zoom_level);
87 | }
88 | return map;
89 | };
90 |
91 | var getPlaces = function(){
92 | return $(settings.side_bar_selector+' '+settings.location_selector);
93 | };
94 |
95 | var getPlacesData = function(doUpdate){
96 | return places.map(function(){
97 | if (doUpdate){
98 | $(this).data('metadata', false);
99 | }
100 | return $(this).metadata(settings.metadata_options);
101 | });
102 | };
103 |
104 | var getBounds = function(doUpdate){
105 | var places_data = getPlacesData(doUpdate),
106 | newBounds, initialPoint;
107 |
108 | if (places_data.length){
109 | initialPoint = $.jMapping.makeGLatLng(places_data[0].point);
110 | }else{
111 | initialPoint = $.jMapping.makeGLatLng(settings.default_point);
112 | }
113 | newBounds = new google.maps.LatLngBounds(initialPoint, initialPoint);
114 |
115 | for (var i=1, len = places_data.length; i 0){
172 | info_window = new google.maps.InfoWindow({
173 | content: $info_window_elm.html(),
174 | maxWidth: settings.info_window_max_width
175 | });
176 | info_windows.push(info_window);
177 | google.maps.event.addListener(marker, 'click', function() {
178 | $.each(info_windows, function(index, iwindow){
179 | if (info_window != iwindow){
180 | iwindow.close();
181 | }
182 | });
183 | info_window.open(map, marker);
184 | });
185 | }
186 |
187 | gmarkers[parseInt(place_data.id, 10)] = marker;
188 | return marker;
189 | };
190 |
191 | var updateMarkerManager = function(){
192 | if (settings.always_show_markers === true) {
193 | min_zoom = 0;
194 | } else {
195 | zoom_level = map.getZoom();
196 | min_zoom = (zoom_level < 7) ? 0 : (zoom_level - 7);
197 | }
198 | markerManager.addMarkers(gmarkersArray(), min_zoom);
199 | markerManager.refresh();
200 | if (settings.force_zoom_level){
201 | map.setZoom(settings.force_zoom_level);
202 | }
203 | };
204 |
205 | var attachMapsEventToLinks = function(){
206 | var location_link_selector = [
207 | settings.side_bar_selector,
208 | settings.location_selector,
209 | settings.link_selector
210 | ].join(' ');
211 |
212 | $(location_link_selector).live('click', function(e){
213 | e.preventDefault();
214 | var marker_index = parseInt($(this).attr('href').split('#')[1], 10);
215 | google.maps.event.trigger(gmarkers[marker_index], "click");
216 | });
217 | };
218 |
219 | var gmarkersArray = function(){
220 | var marker_arr = [];
221 | $.each(gmarkers, function(key, value){
222 | marker_arr.push(value);
223 | });
224 | return marker_arr;
225 | };
226 |
227 | if ($(document).trigger('beforeMapping.jMapping', [settings]) != false){
228 | init();
229 | mapped = true;
230 | } else {
231 | mapped = false;
232 | }
233 | jMapper = {
234 | gmarkers: gmarkers,
235 | settings: settings,
236 | mapped: mapped,
237 | map: map,
238 | markerManager: markerManager,
239 | gmarkersArray: gmarkersArray,
240 | getBounds: getBounds,
241 | getPlacesData: getPlacesData,
242 | getPlaces: getPlaces,
243 | update: function(){
244 | if ($(document).trigger('beforeUpdate.jMapping', [this]) != false){
245 |
246 | init(true);
247 | this.map = map;
248 | this.gmarkers = gmarkers;
249 | this.markerManager = markerManager;
250 | $(document).trigger('afterUpdate.jMapping', [this]);
251 | }
252 | }
253 | };
254 | $(document).trigger('afterMapping.jMapping', [jMapper]);
255 | return jMapper;
256 | } else {
257 | return $(map_elm).data('jMapping');
258 | }
259 | };
260 |
261 | $.extend($.jMapping, {
262 | defaults: {
263 | side_bar_selector: '#map-side-bar:first',
264 | location_selector: '.map-location',
265 | link_selector: 'a.map-link',
266 | info_window_selector: '.info-box',
267 | info_window_max_width: 425,
268 | default_point: {lat: 0.0, lng: 0.0},
269 | metadata_options: {type: 'attr', name: 'data-jmapping'}
270 | },
271 | makeGLatLng: function(place_point){
272 | return new google.maps.LatLng(place_point.lat, place_point.lng);
273 | }
274 | });
275 |
276 | $.fn.jMapping = function(options){
277 | if ((options == 'update') && $(this[0]).data('jMapping')){
278 | $(this[0]).data('jMapping').update();
279 | } else {
280 | if (options == 'update') options = {};
281 | $(this[0]).data('jMapping', $.jMapping(this[0], options));
282 | }
283 | return this;
284 | };
285 | })(jQuery);
286 |
--------------------------------------------------------------------------------
/jquery.jmapping.min.js:
--------------------------------------------------------------------------------
1 |
2 | (function($){$.jMapping=function(map_elm,options){var settings,gmarkers,mapped,map,markerManager,places,bounds,jMapper,info_windows;map_elm=(typeof map_elm=="string")?$(map_elm).get(0):map_elm;if(!($(map_elm).data('jMapping'))){settings=$.extend(true,{},$.jMapping.defaults);$.extend(true,settings,options);gmarkers={};info_windows=[];var init=function(doUpdate){var info_window_selector,min_zoom,zoom_level;info_window_selector=[settings.side_bar_selector,settings.location_selector,settings.info_window_selector].join(' ');$(info_window_selector).hide();places=getPlaces();bounds=getBounds(doUpdate);if(doUpdate){gmarkers={};info_windows=[];markerManager.clearMarkers();google.maps.event.trigger(map,'resize');map.fitBounds(bounds);if(settings.force_zoom_level){map.setZoom(settings.force_zoom_level);}}else{map=createMap();markerManager=new MarkerManager(map);}
3 | places.each(function(){var marker=createMarker(this);if(!(settings.link_selector===false)){setupLink(this);}
4 | $(document).trigger('markerCreated.jMapping',[marker]);});if(doUpdate){updateMarkerManager();}else{google.maps.event.addListener(markerManager,'loaded',function(){updateMarkerManager();if(settings.default_zoom_level){map.setZoom(settings.default_zoom_level);}});}
5 | if(!(settings.link_selector===false)&&!doUpdate){attachMapsEventToLinks();}};var createMap=function(){if(settings.map_config){map=new google.maps.Map(map_elm,settings.map_config);}else{map=new google.maps.Map(map_elm,{navigationControlOptions:{style:google.maps.NavigationControlStyle.SMALL},mapTypeControl:false,mapTypeId:google.maps.MapTypeId.ROADMAP,zoom:9});}
6 | map.fitBounds(bounds);if(settings.force_zoom_level){map.setZoom(settings.force_zoom_level);}
7 | return map;};var getPlaces=function(){return $(settings.side_bar_selector+' '+settings.location_selector);};var getPlacesData=function(doUpdate){return places.map(function(){if(doUpdate){$(this).data('metadata',false);}
8 | return $(this).metadata(settings.metadata_options);});};var getBounds=function(doUpdate){var places_data=getPlacesData(doUpdate),newBounds,initialPoint;if(places_data.length){initialPoint=$.jMapping.makeGLatLng(places_data[0].point);}else{initialPoint=$.jMapping.makeGLatLng(settings.default_point);}
9 | newBounds=new google.maps.LatLngBounds(initialPoint,initialPoint);for(var i=1,len=places_data.length;i0){info_window=new google.maps.InfoWindow({content:$info_window_elm.html(),maxWidth:settings.info_window_max_width});info_windows.push(info_window);google.maps.event.addListener(marker,'click',function(){$.each(info_windows,function(index,iwindow){if(info_window!=iwindow){iwindow.close();}});info_window.open(map,marker);});}
12 | gmarkers[parseInt(place_data.id,10)]=marker;return marker;};var updateMarkerManager=function(){if(settings.always_show_markers===true){min_zoom=0;}else{zoom_level=map.getZoom();min_zoom=(zoom_level<7)?0:(zoom_level-7);}
13 | markerManager.addMarkers(gmarkersArray(),min_zoom);markerManager.refresh();if(settings.force_zoom_level){map.setZoom(settings.force_zoom_level);}};var attachMapsEventToLinks=function(){var location_link_selector=[settings.side_bar_selector,settings.location_selector,settings.link_selector].join(' ');$(location_link_selector).live('click',function(e){e.preventDefault();var marker_index=parseInt($(this).attr('href').split('#')[1],10);google.maps.event.trigger(gmarkers[marker_index],"click");});};var gmarkersArray=function(){var marker_arr=[];$.each(gmarkers,function(key,value){marker_arr.push(value);});return marker_arr;};if($(document).trigger('beforeMapping.jMapping',[settings])!=false){init();mapped=true;}else{mapped=false;}
14 | jMapper={gmarkers:gmarkers,settings:settings,mapped:mapped,map:map,markerManager:markerManager,gmarkersArray:gmarkersArray,getBounds:getBounds,getPlacesData:getPlacesData,getPlaces:getPlaces,update:function(){if($(document).trigger('beforeUpdate.jMapping',[this])!=false){init(true);this.map=map;this.gmarkers=gmarkers;this.markerManager=markerManager;$(document).trigger('afterUpdate.jMapping',[this]);}}};$(document).trigger('afterMapping.jMapping',[jMapper]);return jMapper;}else{return $(map_elm).data('jMapping');}};$.extend($.jMapping,{defaults:{side_bar_selector:'#map-side-bar:first',location_selector:'.map-location',link_selector:'a.map-link',info_window_selector:'.info-box',info_window_max_width:425,default_point:{lat:0.0,lng:0.0},metadata_options:{type:'attr',name:'data-jmapping'}},makeGLatLng:function(place_point){return new google.maps.LatLng(place_point.lat,place_point.lng);}});$.fn.jMapping=function(options){if((options=='update')&&$(this[0]).data('jMapping')){$(this[0]).data('jMapping').update();}else{if(options=='update')options={};$(this[0]).data('jMapping',$.jMapping(this[0],options));}
15 | return this;};})(jQuery);
--------------------------------------------------------------------------------
/spec/jmapping_spec.js:
--------------------------------------------------------------------------------
1 | Screw.Unit(function(){
2 |
3 | describe("makeGLatLng", function(){
4 | before(function(){
5 | google.maps.LatLng = mock_function(function(lat, lng){}, 'GLatLng');
6 | google.maps.LatLng.should_be_invoked().exactly('once').with_arguments(70, 50);
7 | });
8 |
9 | it("should be invoked", function(){
10 | expect($.jMapping.makeGLatLng({lat: 70, lng: 50})).to(be_true);
11 | });
12 | });
13 |
14 | describe("jMapping", function(){
15 | before(function(){
16 | // mock out Marker
17 | google.maps.InfoWindow = Smoke.MockFunction(function(options){}, 'InfoWindow');
18 |
19 | $('#map-side-bar .map-location .info-box').each(function(){
20 | google.maps.InfoWindow.should_be_invoked().exactly('once').with_arguments({
21 | content: $(this).html(),
22 | maxWidth: 425
23 | });
24 | });
25 |
26 | mockGMaps();
27 | mockMarkerManager();
28 | });
29 | after(function(){
30 | $('#map').data('jMapping', null);
31 | $('#map-side-bar .map-location a.map-link').attr('href', '#');
32 | $('#map-side-bar:first .map-location a.map-link').die('click');
33 | });
34 |
35 | it("should have 2 GMarkers", function(){
36 | $('#map').jMapping();
37 | var jmapper = $('#map').data('jMapping');
38 | expect(jmapper.gmarkers[5]).to(be_true);
39 | expect(jmapper.gmarkers[8]).to(be_true);
40 | });
41 |
42 | it("should hide the info html elements", function(){
43 | $('#map').jMapping();
44 | expect($('#map-side-bar .map-location .info-box')).to(match_selector, ':hidden');
45 | });
46 |
47 | it("should set the links to the correct URL", function(){
48 | $('#map').jMapping();
49 | expect($('#map-side-bar .map-location a.map-link#location5').attr('href')).to(equal, '#5');
50 | expect($('#map-side-bar .map-location a.map-link#location8').attr('href')).to(equal, '#8');
51 | });
52 |
53 | it("should only create the jMapping object once", function(){
54 | $('#map').jMapping();
55 | $('#map').jMapping();
56 | expect($('#map').data('jMapping')).to(be_true);
57 | });
58 |
59 | it('should fire a "beforeMapping.jMapping" event', function(){
60 | var callback = mock_function(function(){}, 'beforeMapping.jMapping');
61 | callback.should_be_invoked().exactly('once');
62 | $(document).bind('beforeMapping.jMapping', callback);
63 | $('#map').jMapping();
64 | });
65 |
66 | it('should fire a "afterMapping.jMapping" event', function(){
67 | var callback = mock_function(function(){}, 'afterMapping.jMapping');
68 | callback.should_be_invoked().exactly('once');
69 | $(document).bind('afterMapping.jMapping', callback);
70 | $('#map').jMapping();
71 | });
72 |
73 | it('should fire a "markerCreated.jMapping" event', function(){
74 | var callback = mock_function(function(){}, 'markerCreated.jMapping');
75 | callback.should_be_invoked().exactly('twice');
76 | $(document).bind('markerCreated.jMapping', callback);
77 | $('#map').jMapping();
78 | });
79 |
80 | describe("object created", function(){
81 | var jMapper;
82 | before(function(){
83 | $('#map').jMapping();
84 | jMapper = $('#map').data('jMapping');
85 | });
86 |
87 | it("should have a settings object", function(){
88 | expect(typeof jMapper.settings).to(equal, 'object');
89 | });
90 |
91 | it("should have a mapped boolean", function(){
92 | expect(typeof jMapper.mapped).to(equal, 'boolean');
93 | expect(jMapper.mapped).to(be_true);
94 | });
95 |
96 | it("should have a map GMap2 object", function(){
97 | expect(typeof jMapper.map).to(equal, 'object');
98 | expect(jMapper.map instanceof google.maps.Map).to(be_true);
99 | });
100 |
101 | it("should have a markerManager MarkerManager object", function(){
102 | expect(typeof jMapper.markerManager).to(equal, 'object');
103 | expect(jMapper.markerManager instanceof MarkerManager).to(be_true);
104 | });
105 |
106 | it("should have a gmarkersArray method that returns an array of markers", function(){
107 | expect($.isFunction(jMapper.gmarkersArray)).to(be_true);
108 | expect(jMapper.gmarkersArray()[0] instanceof google.maps.Marker).to(be_true);
109 | });
110 |
111 | it("should have a getBounds method", function(){
112 | expect($.isFunction(jMapper.getBounds)).to(be_true);
113 | });
114 |
115 | it("should have a getPlacesData method", function(){
116 | expect($.isFunction(jMapper.getPlacesData)).to(be_true);
117 | });
118 |
119 | it("should have a getPlaces method that returns a jQuery object", function(){
120 | expect($.isFunction(jMapper.getPlaces)).to(be_true);
121 | expect(jMapper.getPlaces() instanceof jQuery).to(be_true);
122 | });
123 |
124 | it("should have an update method", function(){
125 | expect($.isFunction(jMapper.update)).to(be_true);
126 | });
127 | });
128 |
129 | describe("setting link_selector to false", function(){
130 | before(function(){
131 | $('#map').jMapping({link_selector: false});
132 | });
133 | after(function(){
134 | delete google.maps.event._expectations;
135 | google.maps.event = {};
136 | });
137 |
138 | it("should not change the links url", function(){
139 | expect($('#map-side-bar .map-location a.map-link#location5').attr('href')).to_not(equal, '#5');
140 | expect($('#map-side-bar .map-location a.map-link#location8').attr('href')).to_not(equal, '#8');
141 | });
142 |
143 | it("should not trigger the GEvent function", function(){
144 | google.maps.event.should_receive('trigger').exactly(0, 'times')
145 | .with_arguments($('#map').data('jMapping').gmarkers[5], 'click');
146 |
147 | $('#map-side-bar .map-location a.map-link#location5').trigger('click');
148 | });
149 | });
150 |
151 | describe("click events for links", function(){
152 | before(function(){
153 | $('#map').jMapping();
154 | });
155 | after(function(){
156 | delete google.maps.event._expectations;
157 | google.maps.event = {};
158 | });
159 |
160 | it("should trigger the GEvent function", function(){
161 | google.maps.event.should_receive('trigger').exactly('once')
162 | .with_arguments($('#map').data('jMapping').gmarkers[5], 'click');
163 |
164 | $('#map-side-bar .map-location a.map-link#location5').trigger('click');
165 | });
166 | });
167 | });
168 |
169 | describe('jMapping with update', function(){
170 | var update_html = '
51 | * @before $.metadata.setType("html5")
52 | * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label"
53 | * @desc Reads metadata from a series of data-* attributes
54 | *
55 | * @param String type The encoding type
56 | * @param String name The name of the attribute to be used to get metadata (optional)
57 | * @cat Plugins/Metadata
58 | * @descr Sets the type of encoding to be used when loading metadata for the first time
59 | * @type undefined
60 | * @see metadata()
61 | */
62 |
63 | (function($) {
64 |
65 | $.extend({
66 | metadata : {
67 | defaults : {
68 | type: 'class',
69 | name: 'metadata',
70 | cre: /({.*})/,
71 | single: 'metadata'
72 | },
73 | setType: function( type, name ){
74 | this.defaults.type = type;
75 | this.defaults.name = name;
76 | },
77 | get: function( elem, opts ){
78 | var settings = $.extend({},this.defaults,opts);
79 | // check for empty string in single property
80 | if ( !settings.single.length ) settings.single = 'metadata';
81 |
82 | var data = $.data(elem, settings.single);
83 | // returned cached data if it already exists
84 | if ( data ) return data;
85 |
86 | data = "{}";
87 |
88 | var getData = function(data) {
89 | if(typeof data != "string") return data;
90 |
91 | if( data.indexOf('{') < 0 ) {
92 | data = eval("(" + data + ")");
93 | }
94 | }
95 |
96 | var getObject = function(data) {
97 | if(typeof data != "string") return data;
98 |
99 | data = eval("(" + data + ")");
100 | return data;
101 | }
102 |
103 | if ( settings.type == "html5" ) {
104 | var object = {};
105 | $( elem.attributes ).each(function() {
106 | var name = this.nodeName;
107 | if(name.match(/^data-/)) name = name.replace(/^data-/, '');
108 | else return true;
109 | object[name] = getObject(this.nodeValue);
110 | });
111 | } else {
112 | if ( settings.type == "class" ) {
113 | var m = settings.cre.exec( elem.className );
114 | if ( m )
115 | data = m[1];
116 | } else if ( settings.type == "elem" ) {
117 | if( !elem.getElementsByTagName ) return;
118 | var e = elem.getElementsByTagName(settings.name);
119 | if ( e.length )
120 | data = $.trim(e[0].innerHTML);
121 | } else if ( elem.getAttribute != undefined ) {
122 | var attr = elem.getAttribute( settings.name );
123 | if ( attr )
124 | data = attr;
125 | }
126 | object = getObject(data.indexOf("{") < 0 ? "{" + data + "}" : data);
127 | }
128 |
129 | $.data( elem, settings.single, object );
130 | return object;
131 | }
132 | }
133 | });
134 |
135 | /**
136 | * Returns the metadata object for the first member of the jQuery object.
137 | *
138 | * @name metadata
139 | * @descr Returns element's metadata object
140 | * @param Object opts An object contianing settings to override the defaults
141 | * @type jQuery
142 | * @cat Plugins/Metadata
143 | */
144 | $.fn.metadata = function( opts ){
145 | return $.metadata.get( this[0], opts );
146 | };
147 |
148 | })(jQuery);
--------------------------------------------------------------------------------
/vendor/jsmin.rb:
--------------------------------------------------------------------------------
1 | #--
2 | # jsmin.rb - Ruby implementation of Douglas Crockford's JSMin.
3 | #
4 | # This is a port of jsmin.c, and is distributed under the same terms, which are
5 | # as follows:
6 | #
7 | # Copyright (c) 2002 Douglas Crockford (www.crockford.com)
8 | #
9 | # Permission is hereby granted, free of charge, to any person obtaining a copy
10 | # of this software and associated documentation files (the "Software"), to deal
11 | # in the Software without restriction, including without limitation the rights
12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | # copies of the Software, and to permit persons to whom the Software is
14 | # furnished to do so, subject to the following conditions:
15 | #
16 | # The above copyright notice and this permission notice shall be included in all
17 | # copies or substantial portions of the Software.
18 | #
19 | # The Software shall be used for Good, not Evil.
20 | #
21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 | # SOFTWARE.
28 | #++
29 |
30 | require 'strscan'
31 |
32 | # = JSMin
33 | #
34 | # Ruby implementation of Douglas Crockford's JavaScript minifier, JSMin.
35 | #
36 | # *Author*:: Ryan Grove (mailto:ryan@wonko.com)
37 | # *Version*:: 1.0.1 (2008-11-10)
38 | # *Copyright*:: Copyright (c) 2008 Ryan Grove. All rights reserved.
39 | # *Website*:: http://github.com/rgrove/jsmin
40 | #
41 | # == Example
42 | #
43 | # require 'rubygems'
44 | # require 'jsmin'
45 | #
46 | # File.open('example.js', 'r') {|file| puts JSMin.minify(file) }
47 | #
48 | class JSMin
49 | CHR_APOS = "'".freeze
50 | CHR_ASTERISK = '*'.freeze
51 | CHR_BACKSLASH = '\\'.freeze
52 | CHR_CR = "\r".freeze
53 | CHR_FRONTSLASH = '/'.freeze
54 | CHR_LF = "\n".freeze
55 | CHR_QUOTE = '"'.freeze
56 | CHR_SPACE = ' '.freeze
57 |
58 | if RUBY_VERSION >= '1.9'
59 | ORD_LF = "\n".freeze
60 | ORD_SPACE = ' '.freeze
61 | ORD_TILDE = '~'.freeze
62 | else
63 | ORD_LF = "\n"[0].freeze
64 | ORD_SPACE = ' '[0].freeze
65 | ORD_TILDE = '~'[0].freeze
66 | end
67 |
68 | class << self
69 |
70 | # Reads JavaScript from _input_ (which can be a String or an IO object) and
71 | # returns a String containing minified JS.
72 | def minify(input)
73 | @js = StringScanner.new(input.is_a?(IO) ? input.read : input.to_s)
74 |
75 | @a = "\n"
76 | @b = nil
77 | @lookahead = nil
78 | @output = ''
79 |
80 | action_get
81 |
82 | while !@a.nil? do
83 | case @a
84 | when CHR_SPACE
85 | if alphanum?(@b)
86 | action_output
87 | else
88 | action_copy
89 | end
90 |
91 | when CHR_LF
92 | if @b == CHR_SPACE
93 | action_get
94 | elsif @b =~ /[{\[\(+-]/
95 | action_output
96 | else
97 | if alphanum?(@b)
98 | action_output
99 | else
100 | action_copy
101 | end
102 | end
103 |
104 | else
105 | if @b == CHR_SPACE
106 | if alphanum?(@a)
107 | action_output
108 | else
109 | action_get
110 | end
111 | elsif @b == CHR_LF
112 | if @a =~ /[}\]\)\\"+-]/
113 | action_output
114 | else
115 | if alphanum?(@a)
116 | action_output
117 | else
118 | action_get
119 | end
120 | end
121 | else
122 | action_output
123 | end
124 | end
125 | end
126 |
127 | @output
128 | end
129 |
130 | private
131 |
132 | # Corresponds to action(1) in jsmin.c.
133 | def action_output
134 | @output << @a
135 | action_copy
136 | end
137 |
138 | # Corresponds to action(2) in jsmin.c.
139 | def action_copy
140 | @a = @b
141 |
142 | if @a == CHR_APOS || @a == CHR_QUOTE
143 | loop do
144 | @output << @a
145 | @a = get
146 |
147 | break if @a == @b
148 |
149 | if @a[0] <= ORD_LF
150 | raise "JSMin parse error: unterminated string literal: #{@a}"
151 | end
152 |
153 | if @a == CHR_BACKSLASH
154 | @output << @a
155 | @a = get
156 |
157 | if @a[0] <= ORD_LF
158 | raise "JSMin parse error: unterminated string literal: #{@a}"
159 | end
160 | end
161 | end
162 | end
163 |
164 | action_get
165 | end
166 |
167 | # Corresponds to action(3) in jsmin.c.
168 | def action_get
169 | @b = nextchar
170 |
171 | if @b == CHR_FRONTSLASH && (@a == CHR_LF || @a =~ /[\(,=:\[!&|?{};]/)
172 | @output << @a
173 | @output << @b
174 |
175 | loop do
176 | @a = get
177 |
178 | if @a == CHR_FRONTSLASH
179 | break
180 | elsif @a == CHR_BACKSLASH
181 | @output << @a
182 | @a = get
183 | elsif @a[0] <= ORD_LF
184 | raise "JSMin parse error: unterminated regular expression " +
185 | "literal: #{@a}"
186 | end
187 |
188 | @output << @a
189 | end
190 |
191 | @b = nextchar
192 | end
193 | end
194 |
195 | # Returns true if +c+ is a letter, digit, underscore, dollar sign,
196 | # backslash, or non-ASCII character.
197 | def alphanum?(c)
198 | c.is_a?(String) && !c.empty? && (c[0] > ORD_TILDE || c =~ /[0-9a-z_$\\]/i)
199 | end
200 |
201 | # Returns the next character from the input. If the character is a control
202 | # character, it will be translated to a space or linefeed.
203 | def get
204 | c = @lookahead.nil? ? @js.getch : @lookahead
205 | @lookahead = nil
206 |
207 | return c if c.nil? || c == CHR_LF || c[0] >= ORD_SPACE
208 | return "\n" if c == CHR_CR
209 | return ' '
210 | end
211 |
212 | # Gets the next character, excluding comments.
213 | def nextchar
214 | c = get
215 | return c unless c == CHR_FRONTSLASH
216 |
217 | case peek
218 | when CHR_FRONTSLASH
219 | loop do
220 | c = get
221 | return c if c[0] <= ORD_LF
222 | end
223 |
224 | when CHR_ASTERISK
225 | get
226 | loop do
227 | case get
228 | when CHR_ASTERISK
229 | if peek == CHR_FRONTSLASH
230 | get
231 | return ' '
232 | end
233 |
234 | when nil
235 | raise 'JSMin parse error: unterminated comment'
236 | end
237 | end
238 |
239 | else
240 | return c
241 | end
242 | end
243 |
244 | # Gets the next character without getting it.
245 | def peek
246 | @lookahead = get
247 | end
248 | end
249 | end
250 |
--------------------------------------------------------------------------------
/vendor/markermanager.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @name MarkerManager v3
3 | * @version 1.0
4 | * @copyright (c) 2007 Google Inc.
5 | * @author Doug Ricket, Bjorn Brala (port to v3), others,
6 | *
7 | * @fileoverview Marker manager is an interface between the map and the user,
8 | * designed to manage adding and removing many points when the viewport changes.
9 | *
10 | * How it Works:
11 | * The MarkerManager places its markers onto a grid, similar to the map tiles.
12 | * When the user moves the viewport, it computes which grid cells have
13 | * entered or left the viewport, and shows or hides all the markers in those
14 | * cells.
15 | * (If the users scrolls the viewport beyond the markers that are loaded,
16 | * no markers will be visible until the EVENT_moveend
17 | * triggers an update.)
18 | * In practical consequences, this allows 10,000 markers to be distributed over
19 | * a large area, and as long as only 100-200 are visible in any given viewport,
20 | * the user will see good performance corresponding to the 100 visible markers,
21 | * rather than poor performance corresponding to the total 10,000 markers.
22 | * Note that some code is optimized for speed over space,
23 | * with the goal of accommodating thousands of markers.
24 | */
25 |
26 | /*
27 | * Licensed under the Apache License, Version 2.0 (the "License");
28 | * you may not use this file except in compliance with the License.
29 | * You may obtain a copy of the License at
30 | *
31 | * http://www.apache.org/licenses/LICENSE-2.0
32 | *
33 | * Unless required by applicable law or agreed to in writing, software
34 | * distributed under the License is distributed on an "AS IS" BASIS,
35 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
36 | * See the License for the specific language governing permissions and
37 | * limitations under the License.
38 | */
39 |
40 | /**
41 | * @name MarkerManagerOptions
42 | * @class This class represents optional arguments to the {@link MarkerManager}
43 | * constructor.
44 | * @property {Number} maxZoom Sets the maximum zoom level monitored by a
45 | * marker manager. If not given, the manager assumes the maximum map zoom
46 | * level. This value is also used when markers are added to the manager
47 | * without the optional {@link maxZoom} parameter.
48 | * @property {Number} borderPadding Specifies, in pixels, the extra padding
49 | * outside the map's current viewport monitored by a manager. Markers that
50 | * fall within this padding are added to the map, even if they are not fully
51 | * visible.
52 | * @property {Boolean} trackMarkers=false Indicates whether or not a marker
53 | * manager should track markers' movements. If you wish to move managed
54 | * markers using the {@link setPoint}/{@link setLatLng} methods,
55 | * this option should be set to {@link true}.
56 | */
57 |
58 | /**
59 | * Creates a new MarkerManager that will show/hide markers on a map.
60 | *
61 | * Events:
62 | * @event changed (Parameters: shown bounds, shown markers) Notify listeners when the state of what is displayed changes.
63 | * @event loaded MarkerManager has succesfully been initialized.
64 | *
65 | * @constructor
66 | * @param {Map} map The map to manage.
67 | * @param {Object} opt_opts A container for optional arguments:
68 | * {Number} maxZoom The maximum zoom level for which to create tiles.
69 | * {Number} borderPadding The width in pixels beyond the map border,
70 | * where markers should be display.
71 | * {Boolean} trackMarkers Whether or not this manager should track marker
72 | * movements.
73 | */
74 | function MarkerManager(map, opt_opts) {
75 | var me = this;
76 | me.map_ = map;
77 | me.mapZoom_ = map.getZoom();
78 |
79 | me.projectionHelper_ = new ProjectionHelperOverlay(map);
80 | google.maps.event.addListener(me.projectionHelper_, 'ready', function () {
81 | me.projection_ = this.getProjection();
82 | me.initialize(map, opt_opts);
83 | });
84 | }
85 |
86 |
87 | MarkerManager.prototype.initialize = function (map, opt_opts) {
88 | var me = this;
89 |
90 | opt_opts = opt_opts || {};
91 | me.tileSize_ = MarkerManager.DEFAULT_TILE_SIZE_;
92 |
93 | var mapTypes = map.mapTypes;
94 |
95 | // Find max zoom level
96 | var mapMaxZoom = 1;
97 | for (var sType in mapTypes ) {
98 | if (typeof map.mapTypes.get(sType) === 'object' && typeof map.mapTypes.get(sType).maxZoom === 'number') {
99 | var mapTypeMaxZoom = map.mapTypes.get(sType).maxZoom;
100 | if (mapTypeMaxZoom > mapMaxZoom) {
101 | mapMaxZoom = mapTypeMaxZoom;
102 | }
103 | }
104 | }
105 |
106 | me.maxZoom_ = opt_opts.maxZoom || 19;
107 |
108 | me.trackMarkers_ = opt_opts.trackMarkers;
109 | me.show_ = opt_opts.show || true;
110 |
111 | var padding;
112 | if (typeof opt_opts.borderPadding === 'number') {
113 | padding = opt_opts.borderPadding;
114 | } else {
115 | padding = MarkerManager.DEFAULT_BORDER_PADDING_;
116 | }
117 | // The padding in pixels beyond the viewport, where we will pre-load markers.
118 | me.swPadding_ = new google.maps.Size(-padding, padding);
119 | me.nePadding_ = new google.maps.Size(padding, -padding);
120 | me.borderPadding_ = padding;
121 |
122 | me.gridWidth_ = {};
123 |
124 | me.grid_ = {};
125 | me.grid_[me.maxZoom_] = {};
126 | me.numMarkers_ = {};
127 | me.numMarkers_[me.maxZoom_] = 0;
128 |
129 |
130 | google.maps.event.addListener(map, 'dragend', function () {
131 | me.onMapMoveEnd_();
132 | });
133 | google.maps.event.addListener(map, 'zoom_changed', function () {
134 | me.onMapMoveEnd_();
135 | });
136 |
137 |
138 |
139 | /**
140 | * This closure provide easy access to the map.
141 | * They are used as callbacks, not as methods.
142 | * @param GMarker marker Marker to be removed from the map
143 | * @private
144 | */
145 | me.removeOverlay_ = function (marker) {
146 | marker.setMap(null);
147 | me.shownMarkers_--;
148 | };
149 |
150 | /**
151 | * This closure provide easy access to the map.
152 | * They are used as callbacks, not as methods.
153 | * @param GMarker marker Marker to be added to the map
154 | * @private
155 | */
156 | me.addOverlay_ = function (marker) {
157 | if (me.show_) {
158 | marker.setMap(me.map_);
159 | me.shownMarkers_++;
160 | }
161 | };
162 |
163 | me.resetManager_();
164 | me.shownMarkers_ = 0;
165 |
166 | me.shownBounds_ = me.getMapGridBounds_();
167 |
168 | google.maps.event.trigger(me, 'loaded');
169 |
170 | };
171 |
172 | /**
173 | * Default tile size used for deviding the map into a grid.
174 | */
175 | MarkerManager.DEFAULT_TILE_SIZE_ = 1024;
176 |
177 | /*
178 | * How much extra space to show around the map border so
179 | * dragging doesn't result in an empty place.
180 | */
181 | MarkerManager.DEFAULT_BORDER_PADDING_ = 100;
182 |
183 | /**
184 | * Default tilesize of single tile world.
185 | */
186 | MarkerManager.MERCATOR_ZOOM_LEVEL_ZERO_RANGE = 256;
187 |
188 |
189 | /**
190 | * Initializes MarkerManager arrays for all zoom levels
191 | * Called by constructor and by clearAllMarkers
192 | */
193 | MarkerManager.prototype.resetManager_ = function () {
194 | var mapWidth = MarkerManager.MERCATOR_ZOOM_LEVEL_ZERO_RANGE;
195 | for (var zoom = 0; zoom <= this.maxZoom_; ++zoom) {
196 | this.grid_[zoom] = {};
197 | this.numMarkers_[zoom] = 0;
198 | this.gridWidth_[zoom] = Math.ceil(mapWidth / this.tileSize_);
199 | mapWidth <<= 1;
200 | }
201 |
202 | };
203 |
204 | /**
205 | * Removes all markers in the manager, and
206 | * removes any visible markers from the map.
207 | */
208 | MarkerManager.prototype.clearMarkers = function () {
209 | this.processAll_(this.shownBounds_, this.removeOverlay_);
210 | this.resetManager_();
211 | };
212 |
213 |
214 | /**
215 | * Gets the tile coordinate for a given latlng point.
216 | *
217 | * @param {LatLng} latlng The geographical point.
218 | * @param {Number} zoom The zoom level.
219 | * @param {google.maps.Size} padding The padding used to shift the pixel coordinate.
220 | * Used for expanding a bounds to include an extra padding
221 | * of pixels surrounding the bounds.
222 | * @return {GPoint} The point in tile coordinates.
223 | *
224 | */
225 | MarkerManager.prototype.getTilePoint_ = function (latlng, zoom, padding) {
226 |
227 | var pixelPoint = this.projectionHelper_.LatLngToPixel(latlng, zoom);
228 |
229 | var point = new google.maps.Point(
230 | Math.floor((pixelPoint.x + padding.width) / this.tileSize_),
231 | Math.floor((pixelPoint.y + padding.height) / this.tileSize_)
232 | );
233 |
234 | return point;
235 | };
236 |
237 |
238 | /**
239 | * Finds the appropriate place to add the marker to the grid.
240 | * Optimized for speed; does not actually add the marker to the map.
241 | * Designed for batch-processing thousands of markers.
242 | *
243 | * @param {Marker} marker The marker to add.
244 | * @param {Number} minZoom The minimum zoom for displaying the marker.
245 | * @param {Number} maxZoom The maximum zoom for displaying the marker.
246 | */
247 | MarkerManager.prototype.addMarkerBatch_ = function (marker, minZoom, maxZoom) {
248 | var me = this;
249 |
250 | var mPoint = marker.getPosition();
251 | marker.MarkerManager_minZoom = minZoom;
252 |
253 |
254 | // Tracking markers is expensive, so we do this only if the
255 | // user explicitly requested it when creating marker manager.
256 | if (this.trackMarkers_) {
257 | google.maps.event.addListener(marker, 'changed', function (a, b, c) {
258 | me.onMarkerMoved_(a, b, c);
259 | });
260 | }
261 |
262 | var gridPoint = this.getTilePoint_(mPoint, maxZoom, new google.maps.Size(0, 0, 0, 0));
263 |
264 | for (var zoom = maxZoom; zoom >= minZoom; zoom--) {
265 | var cell = this.getGridCellCreate_(gridPoint.x, gridPoint.y, zoom);
266 | cell.push(marker);
267 |
268 | gridPoint.x = gridPoint.x >> 1;
269 | gridPoint.y = gridPoint.y >> 1;
270 | }
271 | };
272 |
273 |
274 | /**
275 | * Returns whether or not the given point is visible in the shown bounds. This
276 | * is a helper method that takes care of the corner case, when shownBounds have
277 | * negative minX value.
278 | *
279 | * @param {Point} point a point on a grid.
280 | * @return {Boolean} Whether or not the given point is visible in the currently
281 | * shown bounds.
282 | */
283 | MarkerManager.prototype.isGridPointVisible_ = function (point) {
284 | var vertical = this.shownBounds_.minY <= point.y &&
285 | point.y <= this.shownBounds_.maxY;
286 | var minX = this.shownBounds_.minX;
287 | var horizontal = minX <= point.x && point.x <= this.shownBounds_.maxX;
288 | if (!horizontal && minX < 0) {
289 | // Shifts the negative part of the rectangle. As point.x is always less
290 | // than grid width, only test shifted minX .. 0 part of the shown bounds.
291 | var width = this.gridWidth_[this.shownBounds_.z];
292 | horizontal = minX + width <= point.x && point.x <= width - 1;
293 | }
294 | return vertical && horizontal;
295 | };
296 |
297 |
298 | /**
299 | * Reacts to a notification from a marker that it has moved to a new location.
300 | * It scans the grid all all zoom levels and moves the marker from the old grid
301 | * location to a new grid location.
302 | *
303 | * @param {Marker} marker The marker that moved.
304 | * @param {LatLng} oldPoint The old position of the marker.
305 | * @param {LatLng} newPoint The new position of the marker.
306 | */
307 | MarkerManager.prototype.onMarkerMoved_ = function (marker, oldPoint, newPoint) {
308 | // NOTE: We do not know the minimum or maximum zoom the marker was
309 | // added at, so we start at the absolute maximum. Whenever we successfully
310 | // remove a marker at a given zoom, we add it at the new grid coordinates.
311 | var zoom = this.maxZoom_;
312 | var changed = false;
313 | var oldGrid = this.getTilePoint_(oldPoint, zoom, new google.maps.Size(0, 0, 0, 0));
314 | var newGrid = this.getTilePoint_(newPoint, zoom, new google.maps.Size(0, 0, 0, 0));
315 | while (zoom >= 0 && (oldGrid.x !== newGrid.x || oldGrid.y !== newGrid.y)) {
316 | var cell = this.getGridCellNoCreate_(oldGrid.x, oldGrid.y, zoom);
317 | if (cell) {
318 | if (this.removeFromArray_(cell, marker)) {
319 | this.getGridCellCreate_(newGrid.x, newGrid.y, zoom).push(marker);
320 | }
321 | }
322 | // For the current zoom we also need to update the map. Markers that no
323 | // longer are visible are removed from the map. Markers that moved into
324 | // the shown bounds are added to the map. This also lets us keep the count
325 | // of visible markers up to date.
326 | if (zoom === this.mapZoom_) {
327 | if (this.isGridPointVisible_(oldGrid)) {
328 | if (!this.isGridPointVisible_(newGrid)) {
329 | this.removeOverlay_(marker);
330 | changed = true;
331 | }
332 | } else {
333 | if (this.isGridPointVisible_(newGrid)) {
334 | this.addOverlay_(marker);
335 | changed = true;
336 | }
337 | }
338 | }
339 | oldGrid.x = oldGrid.x >> 1;
340 | oldGrid.y = oldGrid.y >> 1;
341 | newGrid.x = newGrid.x >> 1;
342 | newGrid.y = newGrid.y >> 1;
343 | --zoom;
344 | }
345 | if (changed) {
346 | this.notifyListeners_();
347 | }
348 | };
349 |
350 |
351 | /**
352 | * Removes marker from the manager and from the map
353 | * (if it's currently visible).
354 | * @param {GMarker} marker The marker to delete.
355 | */
356 | MarkerManager.prototype.removeMarker = function (marker) {
357 | var zoom = this.maxZoom_;
358 | var changed = false;
359 | var point = marker.getPosition();
360 | var grid = this.getTilePoint_(point, zoom, new google.maps.Size(0, 0, 0, 0));
361 | while (zoom >= 0) {
362 | var cell = this.getGridCellNoCreate_(grid.x, grid.y, zoom);
363 |
364 | if (cell) {
365 | this.removeFromArray_(cell, marker);
366 | }
367 | // For the current zoom we also need to update the map. Markers that no
368 | // longer are visible are removed from the map. This also lets us keep the count
369 | // of visible markers up to date.
370 | if (zoom === this.mapZoom_) {
371 | if (this.isGridPointVisible_(grid)) {
372 | this.removeOverlay_(marker);
373 | changed = true;
374 | }
375 | }
376 | grid.x = grid.x >> 1;
377 | grid.y = grid.y >> 1;
378 | --zoom;
379 | }
380 | if (changed) {
381 | this.notifyListeners_();
382 | }
383 | this.numMarkers_[marker.MarkerManager_minZoom]--;
384 | };
385 |
386 |
387 | /**
388 | * Add many markers at once.
389 | * Does not actually update the map, just the internal grid.
390 | *
391 | * @param {Array of Marker} markers The markers to add.
392 | * @param {Number} minZoom The minimum zoom level to display the markers.
393 | * @param {Number} opt_maxZoom The maximum zoom level to display the markers.
394 | */
395 | MarkerManager.prototype.addMarkers = function (markers, minZoom, opt_maxZoom) {
396 | var maxZoom = this.getOptMaxZoom_(opt_maxZoom);
397 | for (var i = markers.length - 1; i >= 0; i--) {
398 | this.addMarkerBatch_(markers[i], minZoom, maxZoom);
399 | }
400 |
401 | this.numMarkers_[minZoom] += markers.length;
402 | };
403 |
404 |
405 | /**
406 | * Returns the value of the optional maximum zoom. This method is defined so
407 | * that we have just one place where optional maximum zoom is calculated.
408 | *
409 | * @param {Number} opt_maxZoom The optinal maximum zoom.
410 | * @return The maximum zoom.
411 | */
412 | MarkerManager.prototype.getOptMaxZoom_ = function (opt_maxZoom) {
413 | return opt_maxZoom || this.maxZoom_;
414 | };
415 |
416 |
417 | /**
418 | * Calculates the total number of markers potentially visible at a given
419 | * zoom level.
420 | *
421 | * @param {Number} zoom The zoom level to check.
422 | */
423 | MarkerManager.prototype.getMarkerCount = function (zoom) {
424 | var total = 0;
425 | for (var z = 0; z <= zoom; z++) {
426 | total += this.numMarkers_[z];
427 | }
428 | return total;
429 | };
430 |
431 | /**
432 | * Returns a marker given latitude, longitude and zoom. If the marker does not
433 | * exist, the method will return a new marker. If a new marker is created,
434 | * it will NOT be added to the manager.
435 | *
436 | * @param {Number} lat - the latitude of a marker.
437 | * @param {Number} lng - the longitude of a marker.
438 | * @param {Number} zoom - the zoom level
439 | * @return {GMarker} marker - the marker found at lat and lng
440 | */
441 | MarkerManager.prototype.getMarker = function (lat, lng, zoom) {
442 | var mPoint = new google.maps.LatLng(lat, lng);
443 | var gridPoint = this.getTilePoint_(mPoint, zoom, new google.maps.Size(0, 0, 0, 0));
444 |
445 | var marker = new google.maps.Marker({position: mPoint});
446 |
447 | var cellArray = this.getGridCellNoCreate_(gridPoint.x, gridPoint.y, zoom);
448 | if (cellArray !== undefined) {
449 | for (var i = 0; i < cellArray.length; i++)
450 | {
451 | if (lat === cellArray[i].getLatLng().lat() && lng === cellArray[i].getLatLng().lng()) {
452 | marker = cellArray[i];
453 | }
454 | }
455 | }
456 | return marker;
457 | };
458 |
459 | /**
460 | * Add a single marker to the map.
461 | *
462 | * @param {Marker} marker The marker to add.
463 | * @param {Number} minZoom The minimum zoom level to display the marker.
464 | * @param {Number} opt_maxZoom The maximum zoom level to display the marker.
465 | */
466 | MarkerManager.prototype.addMarker = function (marker, minZoom, opt_maxZoom) {
467 | var maxZoom = this.getOptMaxZoom_(opt_maxZoom);
468 | this.addMarkerBatch_(marker, minZoom, maxZoom);
469 | var gridPoint = this.getTilePoint_(marker.getPosition(), this.mapZoom_, new google.maps.Size(0, 0, 0, 0));
470 | if (this.isGridPointVisible_(gridPoint) &&
471 | minZoom <= this.shownBounds_.z &&
472 | this.shownBounds_.z <= maxZoom) {
473 | this.addOverlay_(marker);
474 | this.notifyListeners_();
475 | }
476 | this.numMarkers_[minZoom]++;
477 | };
478 |
479 |
480 | /**
481 | * Helper class to create a bounds of INT ranges.
482 | * @param bounds Array.> Bounds object.
483 | * @constructor
484 | */
485 | function GridBounds(bounds) {
486 | // [sw, ne]
487 |
488 | this.minX = Math.min(bounds[0].x, bounds[1].x);
489 | this.maxX = Math.max(bounds[0].x, bounds[1].x);
490 | this.minY = Math.min(bounds[0].y, bounds[1].y);
491 | this.maxY = Math.max(bounds[0].y, bounds[1].y);
492 |
493 | }
494 |
495 | /**
496 | * Returns true if this bounds equal the given bounds.
497 | * @param {GridBounds} gridBounds GridBounds The bounds to test.
498 | * @return {Boolean} This Bounds equals the given GridBounds.
499 | */
500 | GridBounds.prototype.equals = function (gridBounds) {
501 | if (this.maxX === gridBounds.maxX && this.maxY === gridBounds.maxY && this.minX === gridBounds.minX && this.minY === gridBounds.minY) {
502 | return true;
503 | } else {
504 | return false;
505 | }
506 | };
507 |
508 | /**
509 | * Returns true if this bounds (inclusively) contains the given point.
510 | * @param {Point} point The point to test.
511 | * @return {Boolean} This Bounds contains the given Point.
512 | */
513 | GridBounds.prototype.containsPoint = function (point) {
514 | var outer = this;
515 | return (outer.minX <= point.x && outer.maxX >= point.x && outer.minY <= point.y && outer.maxY >= point.y);
516 | };
517 |
518 | /**
519 | * Get a cell in the grid, creating it first if necessary.
520 | *
521 | * Optimization candidate
522 | *
523 | * @param {Number} x The x coordinate of the cell.
524 | * @param {Number} y The y coordinate of the cell.
525 | * @param {Number} z The z coordinate of the cell.
526 | * @return {Array} The cell in the array.
527 | */
528 | MarkerManager.prototype.getGridCellCreate_ = function (x, y, z) {
529 | var grid = this.grid_[z];
530 | if (x < 0) {
531 | x += this.gridWidth_[z];
532 | }
533 | var gridCol = grid[x];
534 | if (!gridCol) {
535 | gridCol = grid[x] = [];
536 | return (gridCol[y] = []);
537 | }
538 | var gridCell = gridCol[y];
539 | if (!gridCell) {
540 | return (gridCol[y] = []);
541 | }
542 | return gridCell;
543 | };
544 |
545 |
546 | /**
547 | * Get a cell in the grid, returning undefined if it does not exist.
548 | *
549 | * NOTE: Optimized for speed -- otherwise could combine with getGridCellCreate_.
550 | *
551 | * @param {Number} x The x coordinate of the cell.
552 | * @param {Number} y The y coordinate of the cell.
553 | * @param {Number} z The z coordinate of the cell.
554 | * @return {Array} The cell in the array.
555 | */
556 | MarkerManager.prototype.getGridCellNoCreate_ = function (x, y, z) {
557 | var grid = this.grid_[z];
558 |
559 | if (x < 0) {
560 | x += this.gridWidth_[z];
561 | }
562 | var gridCol = grid[x];
563 | return gridCol ? gridCol[y] : undefined;
564 | };
565 |
566 |
567 | /**
568 | * Turns at geographical bounds into a grid-space bounds.
569 | *
570 | * @param {LatLngBounds} bounds The geographical bounds.
571 | * @param {Number} zoom The zoom level of the bounds.
572 | * @param {google.maps.Size} swPadding The padding in pixels to extend beyond the
573 | * given bounds.
574 | * @param {google.maps.Size} nePadding The padding in pixels to extend beyond the
575 | * given bounds.
576 | * @return {GridBounds} The bounds in grid space.
577 | */
578 | MarkerManager.prototype.getGridBounds_ = function (bounds, zoom, swPadding, nePadding) {
579 | zoom = Math.min(zoom, this.maxZoom_);
580 |
581 | var bl = bounds.getSouthWest();
582 | var tr = bounds.getNorthEast();
583 | var sw = this.getTilePoint_(bl, zoom, swPadding);
584 |
585 | var ne = this.getTilePoint_(tr, zoom, nePadding);
586 | var gw = this.gridWidth_[zoom];
587 |
588 | // Crossing the prime meridian requires correction of bounds.
589 | if (tr.lng() < bl.lng() || ne.x < sw.x) {
590 | sw.x -= gw;
591 | }
592 | if (ne.x - sw.x + 1 >= gw) {
593 | // Computed grid bounds are larger than the world; truncate.
594 | sw.x = 0;
595 | ne.x = gw - 1;
596 | }
597 |
598 | var gridBounds = new GridBounds([sw, ne]);
599 | gridBounds.z = zoom;
600 |
601 | return gridBounds;
602 | };
603 |
604 |
605 | /**
606 | * Gets the grid-space bounds for the current map viewport.
607 | *
608 | * @return {Bounds} The bounds in grid space.
609 | */
610 | MarkerManager.prototype.getMapGridBounds_ = function () {
611 | return this.getGridBounds_(this.map_.getBounds(), this.mapZoom_, this.swPadding_, this.nePadding_);
612 | };
613 |
614 |
615 | /**
616 | * Event listener for map:movend.
617 | * NOTE: Use a timeout so that the user is not blocked
618 | * from moving the map.
619 | *
620 | * Removed this because a a lack of a scopy override/callback function on events.
621 | */
622 | MarkerManager.prototype.onMapMoveEnd_ = function () {
623 | this.objectSetTimeout_(this, this.updateMarkers_, 0);
624 | };
625 |
626 |
627 | /**
628 | * Call a function or evaluate an expression after a specified number of
629 | * milliseconds.
630 | *
631 | * Equivalent to the standard window.setTimeout function, but the given
632 | * function executes as a method of this instance. So the function passed to
633 | * objectSetTimeout can contain references to this.
634 | * objectSetTimeout(this, function () { alert(this.x) }, 1000);
635 | *
636 | * @param {Object} object The target object.
637 | * @param {Function} command The command to run.
638 | * @param {Number} milliseconds The delay.
639 | * @return {Boolean} Success.
640 | */
641 | MarkerManager.prototype.objectSetTimeout_ = function (object, command, milliseconds) {
642 | return window.setTimeout(function () {
643 | command.call(object);
644 | }, milliseconds);
645 | };
646 |
647 |
648 | /**
649 | * Is this layer visible?
650 | *
651 | * Returns visibility setting
652 | *
653 | * @return {Boolean} Visible
654 | */
655 | MarkerManager.prototype.visible = function () {
656 | return this.show_ ? true : false;
657 | };
658 |
659 |
660 | /**
661 | * Returns true if the manager is hidden.
662 | * Otherwise returns false.
663 | * @return {Boolean} Hidden
664 | */
665 | MarkerManager.prototype.isHidden = function () {
666 | return !this.show_;
667 | };
668 |
669 |
670 | /**
671 | * Shows the manager if it's currently hidden.
672 | */
673 | MarkerManager.prototype.show = function () {
674 | this.show_ = true;
675 | this.refresh();
676 | };
677 |
678 |
679 | /**
680 | * Hides the manager if it's currently visible
681 | */
682 | MarkerManager.prototype.hide = function () {
683 | this.show_ = false;
684 | this.refresh();
685 | };
686 |
687 |
688 | /**
689 | * Toggles the visibility of the manager.
690 | */
691 | MarkerManager.prototype.toggle = function () {
692 | this.show_ = !this.show_;
693 | this.refresh();
694 | };
695 |
696 |
697 | /**
698 | * Refresh forces the marker-manager into a good state.
699 | *
700 | *
If never before initialized, shows all the markers.
701 | *
If previously initialized, removes and re-adds all markers.
702 | *
703 | */
704 | MarkerManager.prototype.refresh = function () {
705 | if (this.shownMarkers_ > 0) {
706 | this.processAll_(this.shownBounds_, this.removeOverlay_);
707 | }
708 | // An extra check on this.show_ to increase performance (no need to processAll_)
709 | if (this.show_) {
710 | this.processAll_(this.shownBounds_, this.addOverlay_);
711 | }
712 | this.notifyListeners_();
713 | };
714 |
715 |
716 | /**
717 | * After the viewport may have changed, add or remove markers as needed.
718 | */
719 | MarkerManager.prototype.updateMarkers_ = function () {
720 | this.mapZoom_ = this.map_.getZoom();
721 | var newBounds = this.getMapGridBounds_();
722 |
723 | // If the move does not include new grid sections,
724 | // we have no work to do:
725 | if (newBounds.equals(this.shownBounds_) && newBounds.z === this.shownBounds_.z) {
726 | return;
727 | }
728 |
729 | if (newBounds.z !== this.shownBounds_.z) {
730 | this.processAll_(this.shownBounds_, this.removeOverlay_);
731 | if (this.show_) { // performance
732 | this.processAll_(newBounds, this.addOverlay_);
733 | }
734 | } else {
735 | // Remove markers:
736 | this.rectangleDiff_(this.shownBounds_, newBounds, this.removeCellMarkers_);
737 |
738 | // Add markers:
739 | if (this.show_) { // performance
740 | this.rectangleDiff_(newBounds, this.shownBounds_, this.addCellMarkers_);
741 | }
742 | }
743 | this.shownBounds_ = newBounds;
744 |
745 | this.notifyListeners_();
746 | };
747 |
748 |
749 | /**
750 | * Notify listeners when the state of what is displayed changes.
751 | */
752 | MarkerManager.prototype.notifyListeners_ = function () {
753 | google.maps.event.trigger(this, 'changed', this.shownBounds_, this.shownMarkers_);
754 | };
755 |
756 |
757 | /**
758 | * Process all markers in the bounds provided, using a callback.
759 | *
760 | * @param {Bounds} bounds The bounds in grid space.
761 | * @param {Function} callback The function to call for each marker.
762 | */
763 | MarkerManager.prototype.processAll_ = function (bounds, callback) {
764 | for (var x = bounds.minX; x <= bounds.maxX; x++) {
765 | for (var y = bounds.minY; y <= bounds.maxY; y++) {
766 | this.processCellMarkers_(x, y, bounds.z, callback);
767 | }
768 | }
769 | };
770 |
771 |
772 | /**
773 | * Process all markers in the grid cell, using a callback.
774 | *
775 | * @param {Number} x The x coordinate of the cell.
776 | * @param {Number} y The y coordinate of the cell.
777 | * @param {Number} z The z coordinate of the cell.
778 | * @param {Function} callback The function to call for each marker.
779 | */
780 | MarkerManager.prototype.processCellMarkers_ = function (x, y, z, callback) {
781 | var cell = this.getGridCellNoCreate_(x, y, z);
782 | if (cell) {
783 | for (var i = cell.length - 1; i >= 0; i--) {
784 | callback(cell[i]);
785 | }
786 | }
787 | };
788 |
789 |
790 | /**
791 | * Remove all markers in a grid cell.
792 | *
793 | * @param {Number} x The x coordinate of the cell.
794 | * @param {Number} y The y coordinate of the cell.
795 | * @param {Number} z The z coordinate of the cell.
796 | */
797 | MarkerManager.prototype.removeCellMarkers_ = function (x, y, z) {
798 | this.processCellMarkers_(x, y, z, this.removeOverlay_);
799 | };
800 |
801 |
802 | /**
803 | * Add all markers in a grid cell.
804 | *
805 | * @param {Number} x The x coordinate of the cell.
806 | * @param {Number} y The y coordinate of the cell.
807 | * @param {Number} z The z coordinate of the cell.
808 | */
809 | MarkerManager.prototype.addCellMarkers_ = function (x, y, z) {
810 | this.processCellMarkers_(x, y, z, this.addOverlay_);
811 | };
812 |
813 |
814 | /**
815 | * Use the rectangleDiffCoords_ function to process all grid cells
816 | * that are in bounds1 but not bounds2, using a callback, and using
817 | * the current MarkerManager object as the instance.
818 | *
819 | * Pass the z parameter to the callback in addition to x and y.
820 | *
821 | * @param {Bounds} bounds1 The bounds of all points we may process.
822 | * @param {Bounds} bounds2 The bounds of points to exclude.
823 | * @param {Function} callback The callback function to call
824 | * for each grid coordinate (x, y, z).
825 | */
826 | MarkerManager.prototype.rectangleDiff_ = function (bounds1, bounds2, callback) {
827 | var me = this;
828 | me.rectangleDiffCoords_(bounds1, bounds2, function (x, y) {
829 | callback.apply(me, [x, y, bounds1.z]);
830 | });
831 | };
832 |
833 |
834 | /**
835 | * Calls the function for all points in bounds1, not in bounds2
836 | *
837 | * @param {Bounds} bounds1 The bounds of all points we may process.
838 | * @param {Bounds} bounds2 The bounds of points to exclude.
839 | * @param {Function} callback The callback function to call
840 | * for each grid coordinate.
841 | */
842 | MarkerManager.prototype.rectangleDiffCoords_ = function (bounds1, bounds2, callback) {
843 | var minX1 = bounds1.minX;
844 | var minY1 = bounds1.minY;
845 | var maxX1 = bounds1.maxX;
846 | var maxY1 = bounds1.maxY;
847 | var minX2 = bounds2.minX;
848 | var minY2 = bounds2.minY;
849 | var maxX2 = bounds2.maxX;
850 | var maxY2 = bounds2.maxY;
851 |
852 | var x, y;
853 | for (x = minX1; x <= maxX1; x++) { // All x in R1
854 | // All above:
855 | for (y = minY1; y <= maxY1 && y < minY2; y++) { // y in R1 above R2
856 | callback(x, y);
857 | }
858 | // All below:
859 | for (y = Math.max(maxY2 + 1, minY1); // y in R1 below R2
860 | y <= maxY1; y++) {
861 | callback(x, y);
862 | }
863 | }
864 |
865 | for (y = Math.max(minY1, minY2);
866 | y <= Math.min(maxY1, maxY2); y++) { // All y in R2 and in R1
867 | // Strictly left:
868 | for (x = Math.min(maxX1 + 1, minX2) - 1;
869 | x >= minX1; x--) { // x in R1 left of R2
870 | callback(x, y);
871 | }
872 | // Strictly right:
873 | for (x = Math.max(minX1, maxX2 + 1); // x in R1 right of R2
874 | x <= maxX1; x++) {
875 | callback(x, y);
876 | }
877 | }
878 | };
879 |
880 |
881 | /**
882 | * Removes value from array. O(N).
883 | *
884 | * @param {Array} array The array to modify.
885 | * @param {any} value The value to remove.
886 | * @param {Boolean} opt_notype Flag to disable type checking in equality.
887 | * @return {Number} The number of instances of value that were removed.
888 | */
889 | MarkerManager.prototype.removeFromArray_ = function (array, value, opt_notype) {
890 | var shift = 0;
891 | for (var i = 0; i < array.length; ++i) {
892 | if (array[i] === value || (opt_notype && array[i] === value)) {
893 | array.splice(i--, 1);
894 | shift++;
895 | }
896 | }
897 | return shift;
898 | };
899 |
900 |
901 |
902 |
903 |
904 |
905 |
906 | /**
907 | * Projection overlay helper. Helps in calculating
908 | * that markers get into the right grid.
909 | * @constructor
910 | * @param {Map} map The map to manage.
911 | **/
912 | function ProjectionHelperOverlay(map) {
913 |
914 | this.setMap(map);
915 |
916 | var TILEFACTOR = 8;
917 | var TILESIDE = 1 << TILEFACTOR;
918 | var RADIUS = 7;
919 |
920 | this._map = map;
921 | this._zoom = -1;
922 | this._X0 =
923 | this._Y0 =
924 | this._X1 =
925 | this._Y1 = -1;
926 |
927 |
928 | }
929 | ProjectionHelperOverlay.prototype = new google.maps.OverlayView();
930 |
931 | /**
932 | * Helper function to convert Lng to X
933 | * @private
934 | * @param {float} lng
935 | **/
936 | ProjectionHelperOverlay.prototype.LngToX_ = function (lng) {
937 | return (1 + lng / 180);
938 | };
939 |
940 | /**
941 | * Helper function to convert Lat to Y
942 | * @private
943 | * @param {float} lat
944 | **/
945 | ProjectionHelperOverlay.prototype.LatToY_ = function (lat) {
946 | var sinofphi = Math.sin(lat * Math.PI / 180);
947 | return (1 - 0.5 / Math.PI * Math.log((1 + sinofphi) / (1 - sinofphi)));
948 | };
949 |
950 | /**
951 | * Old school LatLngToPixel
952 | * @param {LatLng} latlng google.maps.LatLng object
953 | * @param {Number} zoom Zoom level
954 | * @return {position} {x: pixelPositionX, y: pixelPositionY}
955 | **/
956 | ProjectionHelperOverlay.prototype.LatLngToPixel = function (latlng, zoom) {
957 | var map = this._map;
958 | var div = this.getProjection().fromLatLngToDivPixel(latlng);
959 | var abs = {x: ~~(0.5 + this.LngToX_(latlng.lng()) * (2 << (zoom + 6))), y: ~~(0.5 + this.LatToY_(latlng.lat()) * (2 << (zoom + 6)))};
960 | return abs;
961 | };
962 |
963 |
964 | /**
965 | * Draw function only triggers a ready event for
966 | * MarkerManager to know projection can proceed to
967 | * initialize.
968 | */
969 | ProjectionHelperOverlay.prototype.draw = function () {
970 | if (!this.ready) {
971 | this.ready = true;
972 | google.maps.event.trigger(this, 'ready');
973 | }
974 | };
--------------------------------------------------------------------------------