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