1 /*
  2 Geo Mashup - Adds a Google Maps mashup of geocoded blog posts.
  3 Copyright (c) 2005-2010 Dylan Kuhn
  4 
  5 This program is free software; you can redistribute it
  6 and/or modify it under the terms of the GNU General Public
  7 License as published by the Free Software Foundation;
  8 either version 2 of the License, or (at your option) any
  9 later version.
 10 
 11 This program is distributed in the hope that it will be
 12 useful, but WITHOUT ANY WARRANTY; without even the implied
 13 warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 14 PURPOSE. See the GNU General Public License for more
 15 details.
 16 */
 17 
 18 /**
 19  * @fileOverview 
 20  * The base Geo Mashup code that is independent of mapping API.
 21  */
 22 
 23 /*global jQuery, GeoMashup: true */
 24 // These globals are retained for backward custom javascript compatibility
 25 /*global customizeGeoMashup, customizeGeoMashupMap, customGeoMashupColorIcon, customGeoMashupCategoryIcon */
 26 /*global customGeoMashupSinglePostIcon, customGeoMashupMultiplePostImage */
 27 /*jslint browser: true, white: true, sloppy: true */
 28 
 29 var GeoMashup, customizeGeoMashup, customizeGeoMashupMap, customGeoMashupColorIcon, customGeoMashupCategoryIcon, 
 30 customGeoMashupSinglePostIcon, customGeoMashupMultiplePostImage;
 31 
 32 /** 
 33  * @name GeoMashupObject
 34  * @class This type represents an object Geo Mashup can place on the map.
 35  * It has no constructor, but is instantiated as an object literal.
 36  * Custom properties can be added, but some are present by default.
 37  * 
 38  * @property {String} object_name The type of object: post, user, comment, etc.
 39  * @property {String} object_id A unique identifier for the object
 40  * @property {String} title
 41  * @property {Number} lat Latitude
 42  * @property {Number} lng Longitude
 43  * @property {String} author_name The name of the object author
 44  * @property {Array} categories Deprecated, use terms. The object's category IDs if any.
 45  * @property {Object} terms The object's terms by taxonomy, e.g. { "tax1": [ "2", "5" ], "tax2": [ "1" ] }
 46  * @property {GeoMashupIcon} icon The default icon to use for the object
 47  */
 48  
 49 /**
 50  * @name GeoMashupIcon
 51  * @class This type represents an icon that can be used for a map marker.
 52  * It has no constructor, but is instantiated as an object literal.
 53  * @property {String} image URL of the icon image
 54  * @property {String} iconShadow URL of the icon shadow image
 55  * @property {Array} iconSize Pixel width and height of the icon
 56  * @property {Array} shadowSize Pixel width and height of the icon shadow 
 57  * @property {Array} iconAnchor Pixel offset from top left: [ right, down ]
 58  * @property {Array} infoWindowAnchor Pixel offset from top left: [ right, down ]
 59  */
 60 
 61 /** 
 62  * @name GeoMashupOptions
 63  * @class This type represents options used for a specific Geo Mashup map. 
 64  * It has no constructor, but is instantiated as an object literal.
 65  * Properties reflect the <a href="http://code.google.com/p/wordpress-geo-mashup/wiki/TagReference#Map">map tag parameters</a>.
 66  */
 67 
 68 /**
 69  * @name VisibilityFilter
 70  * @class This type represents objects used to filter object visibility.
 71  * It has no constructor, but is instantiated as an object literal.
 72  *
 73  * @name ContentFilter#visible
 74  * @property {Boolean} visible Whether the object is currently visible
 75  */
 76 
 77 /**
 78  * @namespace Used more as a singleton than a namespace for data and methods for a single Geo Mashup map.
 79  *
 80  * <p>Violates the convention that capitalized objects are designed to be used with the 
 81  * 'new' keyword - an artifact of the age of the project. :o</p>
 82  * 
 83  * <p><strong>Note: Events are Actions</strong></p>
 84  *
 85  * <p>Actions available for use with GeoMashup.addAction() are documented as events.
 86  * See the <a href="http://code.google.com/p/wordpress-geo-mashup/wiki/JavaScriptApi">javascript API documentation</a>
 87  * for an example.
 88  * </p>
 89  */
 90 GeoMashup = {
 91 	/**
 92 	 * Access to the options for this map.
 93 	 * Properties reflect the <a href="http://code.google.com/p/wordpress-geo-mashup/wiki/TagReference#Map">map tag parameters</a>.
 94 	 * @property {GeoMashupOptions}
 95 	 */
 96 	opts: {},
 97 	actions : {},
 98 	objects : {},
 99 	object_count : 0,
100 	locations : {},
101 	open_attachments : [],
102 	errors : [],
103 	color_names : ['red','lime','blue','orange','yellow','aqua','green','silver','maroon','olive','navy','purple','gray','teal','fuchsia','white','black'],
104 	colors : {
105 		'red':'#ff071e',
106 		'lime':'#62d962',
107 		'blue':'#9595ff',
108 		'orange':'#fe8f00',
109 		'yellow':'#f2f261',
110 		'aqua':'#8eeff0',
111 		'green':'#459234',
112 		'silver':'#c2c2c2',
113 		'maroon':'#ae1a40',
114 		'olive':'#9f9b46',
115 		'navy':'#30389d',
116 		'purple':'#a54086',
117 		'gray':'#9b9b9b',
118 		'teal':'#13957b',
119 		'fuchsia':'#e550e5',
120 		'white':'#ffffff',
121 		'black':'#000000'
122 	},
123 	firstLoad : true,
124 
125 	clone : function( obj ) {
126 		var ClonedObject = function(){};
127 		ClonedObject.prototype = obj;
128 		return new ClonedObject();
129 	},
130 
131 	forEach : function( obj, callback ) {
132 		var key;
133 		for( key in obj ) {
134 			if ( obj.hasOwnProperty( key ) && typeof obj[key] !== 'function' ) {
135 				callback.apply( this, [key, obj[key]] );
136 			}
137 		}
138 	},
139 
140 	locationCache : function( latlng, key ) {
141 		if ( !this.locations.hasOwnProperty( latlng ) ) {
142 			return false;
143 		}
144 		if ( !this.locations[latlng].cache ) {
145 			this.locations[latlng].cache = {};
146 		}
147 		if ( !this.locations[latlng].cache.hasOwnProperty( key ) ) {
148 			this.locations[latlng].cache[key] = {};
149 		}
150 		return this.locations[latlng].cache[key];
151 	},
152 
153 	/**
154 	 * Add an action callback to extend Geo Mashup functionality.
155 	 * 
156 	 * Essentially an event interface. Might make sense to convert to 
157 	 * Mapstraction events in the future.
158 	 *
159 	 * @param {String} name The name of the action (event).
160 	 * @param {Function} callback The function to call when the action occurs
161 	 */
162 	addAction : function ( name, callback ) {
163 		if ( typeof callback !== 'function' ) {
164 			return false;
165 		}
166 		if ( typeof this.actions[name] !== 'object' ) {
167 			this.actions[name] = [callback];
168 		} else {
169 			this.actions[name].push( callback );
170 		}
171 		return true;
172 	},
173 
174 	/**
175 	 * Fire all callbacks for an action.
176     * 
177 	 * Essentially an event interface. Might make sense to convert to 
178 	 * Mapstraction events in the future.
179 	 *
180 	 * @param {String} name The name of the action (event).
181 	 */
182 	doAction : function ( name ) {
183 		var args, i;
184 
185 		if ( typeof this.actions[name] !== 'object' ) {
186 			return false;
187 		}
188 		args = Array.prototype.slice.apply( arguments, [1] );
189 		for ( i = 0; i < this.actions[name].length; i += 1 ) {
190 			if ( typeof this.actions[name][i] === 'function' ) {
191 				this.actions[name][i].apply( null, args );
192 			}
193 		}
194 		return true;
195 	},
196 
197 	parentScrollToGeoPost : function () {
198 		var geo_post;
199 		if ( this.have_parent_access ) {
200 			geo_post = parent.document.getElementById('gm-post');
201 			if (geo_post) {
202 				parent.focus();
203 				parent.scrollTo(geo_post.offsetLeft, geo_post.offsetTop);
204 			}
205 		}
206 		return false;
207 	},
208 
209 	/**
210 	 * Get the DOM element where the full post content should be displayed, if any.
211 	 * @returns {DOMElement} The element, or undefined if none.
212 	 */
213 	getShowPostElement : function() {
214 		if ( this.have_parent_access && !this.show_post_element && this.opts.name) {
215 			this.show_post_element = parent.document.getElementById(this.opts.name + '-post');
216 		}
217 		if ( this.have_parent_access && !this.show_post_element) {
218 			this.show_post_element = parent.document.getElementById('gm-post');
219 		}
220 		return this.show_post_element;
221 	},
222 
223 	/**
224 	 * Change the target of links in HTML markup to target the parent frame.
225 	 * @param {String} markup
226 	 * @returns {String} Modified markup
227 	 */
228 	parentizeLinksMarkup : function( markup ) {
229 		var container = document.createElement( 'div' );
230 		container.innerHTML = markup;
231 		this.parentizeLinks( container );
232 		return container.innerHTML;
233 	},
234 
235 	/**
236 	 * Change the target of links in a DOM element to target the parent frame.
237 	 * @param {DOMElement} node The element to change
238 	 */
239 	parentizeLinks : function( node ) {
240 		var i, links = node.getElementsByTagName('a');
241 		if ( parent ) {
242 			for (i=0; i<links.length; i += 1) {
243 				if ( links[i].target.length === 0 || links[i].target === '_self' ) {
244 					links[i].target = "_parent";
245 				}
246 			}
247 		}
248 	},
249 
250 	/**
251 	 * Display a spinner icon for the map.
252 	 */
253 	showLoadingIcon : function() {
254 		if ( ! this.spinner_div.parentNode ) {
255 			this.container.appendChild( this.spinner_div );
256 		}
257 	},
258 
259 	/**
260 	 * Hide the spinner icon for the map.
261 	 */
262 	hideLoadingIcon : function() {
263 		if ( this.spinner_div.parentNode ) {
264 			this.spinner_div.parentNode.removeChild( this.spinner_div );
265 		}
266 	},
267 
268 	/**
269 	 * Get the objects at a specified location.
270 	 * @param {LatLonPoint} point The query location
271 	 * @returns {Array} The mapped objects at the query location
272 	 */
273 	getObjectsAtLocation : function( point ) {
274 		return this.locations[point].objects;
275 	},
276 
277 	/**
278 	 * Get the objects at the location of a specified marker.
279 	 * @param {Marker} marker 
280 	 * @returns {Array} The mapped objects at the marker location
281 	 */
282 	getMarkerObjects : function( marker ) {
283 		return this.getObjectsAtLocation( this.getMarkerLatLng( marker ) );
284 	},
285 
286 	/**
287 	 * Get the location coordinates for a marker.
288 	 * @param {Marker} marker 
289 	 * @returns {LatLonPoint} The marker coordinates
290 	 */
291 	getMarkerLatLng : function( marker ) {
292 	// Provider override
293 	},
294 
295 	/**
296 	 * Obscure an existing marker with the highlighted "glow" marker.
297 	 * @param {Marker} marker The existing marker
298 	 */
299 	addGlowMarker : function( marker ) {
300 	// Provider override
301 	},
302 
303 	/**
304 	 * Open the info bubble for a marker.
305 	 * @param {Marker} marker
306 	 */
307 	openInfoWindow : function( marker ) {
308 	// Provider override
309 	},
310 
311 	/**
312 	 * Close the info bubble for a marker.
313 	 * @param {Marker} marker
314 	 */
315 	closeInfoWindow : function( marker ) {
316 	// provider override
317 	},
318 
319 	/**
320 	 * Remove the highlighted "glow" marker from the map if it exists.
321 	 */
322 	removeGlowMarker : function() {
323 	// Provider override
324 	},
325 
326 	/**
327 	 * Hide any visible attachment layers on the map.
328 	 */
329 	hideAttachments : function() {
330 	// Provider override
331 	},
332 
333 	/**
334 	 * Show any attachment layers associated with the objects represented
335 	 * by a marker, loading the layer if necessary.
336 	 * @param {Marker} marker
337 	 */
338 	showMarkerAttachments : function( marker ) {
339 	// Provider override
340 	},
341 
342 	/** 
343 	 * Load full content for the objects/posts at a location into the 
344 	 * full post display element.
345 	 * @param {LatLonPoint} point
346 	 */
347 	loadFullPost : function( point ) {
348 	// jQuery or provider override
349 	},
350 
351 	/**
352 	 * Select a marker.
353 	 * @param {Marker} marker
354 	 */
355 	selectMarker : function( marker ) {
356 		var point = this.getMarkerLatLng( marker );
357 
358 		this.selected_marker = marker;
359 		if ( this.opts.marker_select_info_window ) {
360 			this.openInfoWindow( marker );
361 		}
362 		if ( this.opts.marker_select_attachments ) {
363 			this.showMarkerAttachments( marker );
364 		}
365 		if ( this.opts.marker_select_highlight ) {
366 			this.addGlowMarker( marker );
367 		}
368 		if ( this.opts.marker_select_center ) {
369 			this.centerMarker( marker );
370 		}
371 		if ('full-post' !== this.opts.template && this.getShowPostElement()) {
372 			this.loadFullPost( point );
373 		}
374 		/**
375 		 * A marker was selected.
376 		 * @name GeoMashup#selectedMarker
377 		 * @event
378 		 * @param {GeoMashupOptions} properties Geo Mashup configuration data
379 		 * @param {Marker} marker The selected marker
380 		 * @param {Map} map The map containing the marker
381 		 */
382 		this.doAction( 'selectedMarker', this.opts, this.selected_marker, this.map );
383 	},
384 
385 	/**
386 	 * Center and optionally zoom to a marker.
387 	 * @param {Marker} marker 
388 	 * @param {Number} zoom Optional zoom level
389 	 */
390 	centerMarker : function ( marker, zoom ) {
391 	// provider override
392 	},
393 
394 	/**
395 	 * De-select the currently selected marker if there is one.
396 	 */
397 	deselectMarker : function() {
398 		var i, post_element = GeoMashup.getShowPostElement();
399 		if ( post_element ) {
400 			post_element.innerHTML = '';
401 		}
402 		if ( this.glow_marker ) {
403 			this.removeGlowMarker();
404 		}
405 		if ( this.selected_marker ) {
406 			this.closeInfoWindow( this.selected_marker );
407 		}
408 		this.hideAttachments();
409 		this.selected_marker = null;
410 	},
411 
412 	addObjectIcon : function( obj ) {
413 	// provider override
414 	},
415 
416 	createMarker : function( point, obj ) {
417 		var marker;
418 		// provider override
419 		return marker;
420 	},
421 
422 	checkDependencies : function () {
423 	// provider override
424 	},
425 
426 	/**
427 	 * Simulate a user click on the marker that represents a specified object.
428 	 * @param {String} object_id The ID of the object.
429 	 * @param {Number} try_count Optional number of times to try (in case the object 
430 	 *   is still being loaded).
431 	 */
432 	clickObjectMarker : function(object_id, try_count) {
433 	// provider override
434 	},
435 
436 	/**
437 	 * Backward compatibility for clickObjectMarker().
438 	 * @deprecated
439 	 */
440 	clickMarker : function( object_id, try_count ) {
441 		this.clickObjectMarker( object_id, try_count );
442 	},
443 
444 	/**
445 	 * Get the name of a category, if loaded.
446 	 * @param {String} category_id
447 	 * @return {String} The ID or null if not available.
448 	 */
449 	getCategoryName : function (category_id) {
450 		if ( !this.opts.term_properties.hasOwnProperty( 'category' ) ) {
451 			return null;
452 		}
453 		return this.opts.term_properties.category[category_id];
454 	},
455 
456 	/**
457 	 * Hide a marker.
458 	 * @param {Marker} marker
459 	 */
460 	hideMarker : function( marker ) {
461 	// Provider override
462 	},
463 
464 	/**
465 	 * Show a marker.
466 	 * @param {Marker} marker
467 	 */
468 	showMarker : function( marker ) {
469 	// Provider override
470 	},
471 
472 	/**
473 	 * Hide a line.
474 	 * @param {Polyline} line
475 	 */
476 	hideLine : function( line ) {
477 	// Provider override
478 	},
479 
480 	/**
481 	 * Show a line.
482 	 * @param {Polyline} line
483 	 */
484 	showLine : function( line ) {
485 	// Provider override
486 	},
487 
488 	/**
489 	 * Get a line's current visibility.
490 	 * @param {Poloyline} line
491 	 */
492 	isLineVisible : function( line ) {
493 		// Provider override
494 	},
495 
496 	/**
497 	 * Create a new geo coordinate object.
498 	 * @param {Number} lat Latitude
499 	 * @param {Number} lng Longitude
500 	 * @returns {LatLonPoint} Coordinates
501 	 */
502 	newLatLng : function( lat, lng ) {
503 		var latlng;
504 		// Provider override
505 		return latlng;
506 	},
507 
508 	extendLocationBounds : function( ) {
509 	// Provider override
510 	},
511 
512 	addMarkers : function( ) {
513 	// Provider override
514 	},
515 
516 	makeMarkerMultiple : function( marker ) {
517 	// Provider override
518 	},
519 
520 	setMarkerImage : function( marker, image_url ) {
521 	// Provider override
522 	},
523 
524 	/**
525 	 * Zoom the map to loaded content.
526 	 */
527 	autoZoom : function( ) {
528 	// Provider override
529 	},
530 
531 	/**
532 	 * If clustering is active, refresh clusters.
533 	 */
534 	recluster : function( ) {
535 	// Provider override
536 	},
537 
538 	/**
539 	 * Show or hide markers according to current visibility criteria.
540 	 */
541 	updateMarkerVisibilities : function( ) {
542 		this.forEach( this.locations, function( point, loc ) {
543 			GeoMashup.updateMarkerVisibility( loc.marker, point );
544 		} );
545 		this.updateVisibleList();
546 	},
547 
548 	updateMarkerVisibility : function( marker ) {
549 		if ( this.isMarkerOn( marker ) ) {
550 			this.showMarker( marker );
551 		} else {
552 			this.hideMarker( marker );
553 		}
554 	},
555 
556 	isMarkerOn : function( marker ) {
557 		var i, objects, visible_object_indices = [], filter = {
558 			visible: false
559 		};
560 
561 		objects = this.getMarkerObjects( marker );
562 		for ( i = 0; i < objects.length; i += 1 ) {
563 			if ( this.isObjectOn( objects[i] ) ) {
564 				filter.visible = true;
565 				visible_object_indices.push( i );
566 			}
567 		}
568 
569 		// Adjust marker icon based on current visible contents
570 		if ( filter.visible ) {
571 
572 			if ( objects.length > 1 ) {
573 
574 				if ( visible_object_indices.length === 1 ) {
575 					GeoMashup.setMarkerImage( marker, objects[visible_object_indices[0]].icon.image );
576 				} else {
577 					GeoMashup.makeMarkerMultiple( marker );
578 				}
579 
580 			} else if ( objects[0].combined_term_count > 1 ) {
581 
582 				if ( objects[0].visible_term_count === 1 ) {
583 
584 					jQuery.each( objects[0].visible_terms, function( taxonomy, term_ids ) {
585 
586 						if ( term_ids.length === 1 ) {
587 							GeoMashup.setMarkerImage( marker, GeoMashup.term_manager.getTermData( taxonomy, term_ids[0], 'icon' ).image );
588 						}
589 
590 					} );
591 
592 				} else {
593 					GeoMashup.setMarkerImage( marker, objects[0].icon.image );
594 				}
595 			}
596 		}
597 		/**
598 		 * Visibility is being tested for a marker.
599 		 * @name GeoMashup#markerVisibilityOptions
600 		 * @event
601 		 * @param {GeoMashupOptions} properties Geo Mashup configuration data
602 		 * @param {VisibilityFilter} filter Test and set filter.visible
603 		 * @param {Marker} marker The marker being tested
604 		 * @param {Map} map The map for context
605 		 */
606 		this.doAction( 'markerVisibilityOptions', this.opts, filter, marker, this.map );
607 
608 		return filter.visible;
609 	},
610 
611 	isObjectOn : function( obj ) {
612 		var filter = {
613 			visible: false
614 		};
615 
616 		obj.visible_terms = {};
617 		obj.visible_term_count = 0;
618 
619 		if ( !GeoMashup.term_manager || 0 === obj.combined_term_count ) {
620 
621 			// Objects without terms are visible by default
622 			filter.visible = true;
623 
624 		} else {
625 
626 			// Check term visibility
627 			jQuery.each( obj.terms, function( taxonomy, term_ids ) {
628 				
629 				obj.visible_terms[taxonomy] = [];
630 
631 				jQuery.each( term_ids, function( i, term_id ) {
632 					
633 					if ( GeoMashup.term_manager.getTermData( taxonomy, term_id, 'visible' ) ) {
634 						obj.visible_terms[taxonomy].push( term_id );
635 						obj.visible_term_count += 1;
636 						filter.visible = true;
637 					}
638 
639 				});
640 
641 			});
642 
643 		}
644 
645 		/**
646 		 * Visibility is being tested for an object.
647 		 * @name GeoMashup#objectVisibilityOptions
648 		 * @event
649 		 * @param {GeoMashupOptions} properties Geo Mashup configuration data
650 		 * @param {VisibilityFilter} filter Test and set filter.visible
651 		 * @param {Object} object The object being tested
652 		 * @param {Map} map The map for context
653 		 */
654 		this.doAction( 'objectVisibilityOptions', this.opts, filter, obj, this.map );
655 
656 		return filter.visible;
657 	},
658 
659 	/**
660 	 * Extract the IDs of objects that are "on" (not filtered by a control).
661 	 * @since 1.4.2
662 	 * @param {Array} objects The objects to check
663 	 * @returns {Array} The IDs of the "on" objects
664 	 */
665 	getOnObjectIDs : function( objects ) {
666 		var i, object_ids = [];
667 		for( i = 0; i < objects.length; i += 1 ) {
668 			if ( this.isObjectOn( objects[i] ) ) {
669 				object_ids.push( objects[i].object_id );
670 			}
671 		}
672 		return object_ids;
673 	},
674 
675 	/**
676 	 * Add objects to the map.
677 	 * @param {Object} response_data Data returned by a geo query.
678 	 * @param {Boolean} add_term_info Whether to build and show term
679 	 *   data for these objects, for legend or other term controls.
680 	 */
681 	addObjects : function(response_data, add_term_info) {
682 		var i, k, object_id, point, taxonomy, term_ids, term_id, marker, plus_image,
683 			added_markers = [];
684 
685 		if ( add_term_info && this.term_manager ) {
686 			this.term_manager.reset();
687 		}
688 
689 		for (i = 0; i < response_data.length; i+=1) {
690 			// Make a marker for each new object location
691 			object_id = response_data[i].object_id;
692 			point = this.newLatLng(
693 				parseFloat(response_data[i].lat),
694 				parseFloat(response_data[i].lng)
695 			);
696 
697 			// Back compat for categories API
698 			response_data[i].categories = [];
699 
700 			response_data[i].combined_term_count = 0;
701 			if ( this.term_manager ) {
702 				// Add terms
703 				for( taxonomy in response_data[i].terms ) {
704 					if ( response_data[i].terms.hasOwnProperty( taxonomy ) && typeof taxonomy !== 'function' ) {
705 
706 						term_ids = response_data[i].terms[taxonomy];
707 						for (k = 0; k < term_ids.length; k+=1) {
708 							GeoMashup.term_manager.extendTerm( point, taxonomy, term_ids[k], response_data[i] );
709 						}
710 
711 						response_data[i].combined_term_count += term_ids.length;
712 
713 						if ( 'category' === taxonomy ) {
714 							response_data[i].categories = term_ids;
715 						}
716 					}
717 				}
718 			}
719 			
720 			if (this.opts.max_posts && this.object_count >= this.opts.max_posts) {
721 				break;
722 			}
723 
724 			if (!this.objects[object_id]) {
725 				// This object has not yet been loaded
726 				this.objects[object_id] = response_data[i];
727 				this.object_count += 1;
728 				if (!this.locations[point]) {
729 					// There are no other objects yet at this point, create a marker
730 					this.extendLocationBounds( point );
731 					this.locations[point] = {
732 						objects : [ response_data[i] ], 
733 						loaded_content: {}
734 					};
735 					marker = this.createMarker(point, response_data[i]);
736 					this.objects[object_id].marker = marker;
737 					this.locations[point].marker = marker;
738 					added_markers.push( marker );
739 				} else {
740 					// There is already a marker at this point, add the new object to it
741 					this.locations[point].objects.push( response_data[i] );
742 					marker = this.locations[point].marker;
743 					this.makeMarkerMultiple( marker );
744 					this.objects[object_id].marker = marker;
745 					this.addObjectIcon( this.objects[object_id] );
746 				}
747 			}
748 		} // end for each marker
749 
750 		// Openlayers at least only gets clicks on the top layer, so add markers last
751 		this.addMarkers( added_markers );
752 
753 		if ( this.term_manager ) {
754 			this.term_manager.populateTermElements();
755 		}
756 
757 		if (this.firstLoad) {
758 			this.firstLoad = false;
759 			if ( this.opts.auto_info_open && this.object_count > 0 ) {
760 				if ( !this.opts.open_object_id ) {
761 					if ( this.opts.context_object_id && this.objects[ this.opts.context_object_id ] ) {
762 						this.opts.open_object_id = this.opts.context_object_id;
763 					} else {
764 						this.opts.open_object_id = response_data[0].object_id;
765 					}
766 				}
767 				this.clickObjectMarker(this.opts.open_object_id);
768 			}
769 			if ( this.opts.zoom === 'auto' ) {
770 				this.autoZoom();
771 			} else {
772 				if ( this.opts.context_object_id && this.objects[ this.opts.context_object_id ] ) {
773 					this.centerMarker( this.objects[ this.opts.context_object_id ].marker, parseInt( this.opts.zoom, 10 ) );
774 				}
775 				this.updateVisibleList();
776 			}
777 		}
778 	},
779 
780 	requestObjects : function(use_bounds) {
781 	// provider override (maybe jQuery?)
782 	},
783 
784 	/**
785 	 * Hide all markers.
786 	 */
787 	hideMarkers : function() {
788 		var point;
789 
790 		for (point in this.locations) {
791 			if ( this.locations.hasOwnProperty( point ) && this.locations[point].marker ) {
792 				this.hideMarker( this.locations[point].marker );
793 			}
794 		}
795 		this.recluster();
796 		this.updateVisibleList();
797 	},
798 
799 	/**
800 	 * Show all unfiltered markers.
801 	 */
802 	showMarkers : function() {
803 
804 		jQuery.each( this.locations, function( point, location ) {
805 			if ( GeoMashup.isMarkerOn( location.marker ) ) {
806 				GeoMashup.showMarker( location.marker );
807 			}
808 		});
809 		this.recluster();
810 		this.updateVisibleList();
811 
812 	},
813 
814 	adjustZoom : function() {
815 		var old_level, new_level;
816 		new_level = this.map.getZoom();
817 		if ( typeof this.last_zoom_level === 'undefined' ) {
818 			this.last_zoom_level = new_level;
819 		}
820 		old_level = this.last_zoom_level;
821 
822 		if ( this.term_manager ) {
823 			this.term_manager.updateLineZoom( old_level, new_level );
824 		}
825 
826 		if ( this.clusterer && 'google' === this.opts.map_api ) {
827 			if ( old_level <= this.opts.cluster_max_zoom && 
828 				new_level > this.opts.cluster_max_zoom ) {
829 				this.clusterer.clusteringEnabled = false;
830 				this.clusterer.refresh( true );
831 			} else if ( old_level > this.opts.cluster_max_zoom &&
832 				new_level <= this.opts.cluster_max_zoom ) {
833 				this.clusterer.clusteringEnabled = true;
834 				this.clusterer.refresh( true );
835 			}
836 		}
837 		this.last_zoom_level = new_level;
838 	},
839 
840 	objectLinkHtml : function(object_id) {
841 		return ['<a href="#',
842 		this.opts.name,
843 		'" onclick="frames[\'',
844 		this.opts.name,
845 		'\'].GeoMashup.clickObjectMarker(',
846 		object_id,
847 		');">',
848 		this.objects[object_id].title,
849 		'</a>'].join('');
850 	},
851 
852 	/**
853 	 * Whether a marker is currently visible on the map.
854 	 * @param {Marker} marker
855 	 * @return {Boolean} False if the marker is hidden or outside the current viewport.
856 	 */
857 	isMarkerVisible : function( marker ) {
858 		// Provider override
859 		return false;
860 	},
861 
862 	/**
863 	 * Recompile the list of objects currently visible on the map.
864 	 */
865 	updateVisibleList : function() {
866 		var list_element, header_element, list_html, list_items, list_titles, list_count = 0;
867 
868 		if (this.have_parent_access && this.opts.name) {
869 			header_element = parent.document.getElementById(this.opts.name + "-visible-list-header");
870 			list_element = parent.document.getElementById(this.opts.name + "-visible-list");
871 		}
872 		if (header_element) {
873 			header_element.style.display = 'block';
874 		}
875 		if (list_element) {
876 			list_html = ['<ul class="gm-visible-list">'];
877 			list_items = [];
878 			list_titles = [];
879 			this.forEach( this.objects, function (object_id, obj) {
880 				if ( this.isObjectOn( obj ) && this.isMarkerVisible( obj.marker ) ) {
881 					var list_item = [];
882 					list_item.push('<li><img src="');
883 					list_item.push(obj.icon.image);
884 					list_item.push('" alt="');
885 					list_item.push(obj.title);
886 					list_item.push('" />');
887 					list_item.push(this.objectLinkHtml(object_id));
888 					list_item.push('</li>');
889 					list_count += 1;
890 					list_items[obj.title] = list_item.join('');
891 					list_titles.push(obj.title);
892 				}
893 			});
894 			list_titles.sort();
895 			var list_merged = '';
896 			list_titles.forEach( function(index, title){
897 				list_merged += list_items[index];
898 			});
899 			list_html.push(list_merged);
900 			list_html.push('</ul>');
901 			list_element.innerHTML = list_html.join('');
902 			/**
903 			 * The visible posts list was updated.
904 			 * @name GeoMashup#updatedVisibleList
905 			 * @event
906 			 * @param {GeoMashupOptions} properties Geo Mashup configuration data
907 			 * @param {Number} list_count The number of items in the list
908 			 */
909 			this.doAction( 'updatedVisibleList', this.opts, list_count );
910 		}
911 	},
912 
913 	adjustViewport : function() {
914 		this.updateVisibleList();
915 	},
916 
917 	createMap : function(container, opts) {
918 	// Provider override
919 	}
920 };
921