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 	/**
 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 	 * Create a new geo coordinate object.
490 	 * @param {Number} lat Latitude
491 	 * @param {Number} lng Longitude
492 	 * @returns {LatLonPoint} Coordinates
493 	 */
494 	newLatLng : function( lat, lng ) {
495 		var latlng;
496 		// Provider override
497 		return latlng;
498 	},
499 
500 	extendLocationBounds : function( ) {
501 	// Provider override
502 	},
503 
504 	addMarkers : function( ) {
505 	// Provider override
506 	},
507 
508 	makeMarkerMultiple : function( marker ) {
509 	// Provider override
510 	},
511 
512 	setMarkerImage : function( marker, image_url ) {
513 	// Provider override
514 	},
515 
516 	/**
517 	 * Zoom the map to loaded content.
518 	 */
519 	autoZoom : function( ) {
520 	// Provider override
521 	},
522 
523 	/**
524 	 * If clustering is active, refresh clusters.
525 	 */
526 	recluster : function( ) {
527 	// Provider override
528 	},
529 
530 	/**
531 	 * Show or hide markers according to current visibility criteria.
532 	 */
533 	updateMarkerVisibilities : function( ) {
534 		this.forEach( this.locations, function( point, loc ) {
535 			GeoMashup.updateMarkerVisibility( loc.marker, point );
536 		} );
537 		this.updateVisibleList();
538 	},
539 
540 	updateMarkerVisibility : function( marker ) {
541 		if ( this.isMarkerOn( marker ) ) {
542 			this.showMarker( marker );
543 		} else {
544 			this.hideMarker( marker );
545 		}
546 	},
547 
548 	isMarkerOn : function( marker ) {
549 		var i, objects, visible_object_indices = [], filter = {
550 			visible: false
551 		};
552 
553 		objects = this.getMarkerObjects( marker );
554 		for ( i = 0; i < objects.length; i += 1 ) {
555 			if ( this.isObjectOn( objects[i] ) ) {
556 				filter.visible = true;
557 				visible_object_indices.push( i );
558 			}
559 		}
560 
561 		// Adjust marker icon based on current visible contents
562 		if ( filter.visible ) {
563 
564 			if ( objects.length > 1 ) {
565 
566 				if ( visible_object_indices.length === 1 ) {
567 					GeoMashup.setMarkerImage( marker, objects[visible_object_indices[0]].icon.image );
568 				} else {
569 					GeoMashup.makeMarkerMultiple( marker );
570 				}
571 
572 			} else if ( objects[0].combined_term_count > 1 ) {
573 
574 				if ( objects[0].visible_term_count === 1 ) {
575 
576 					jQuery.each( objects[0].visible_terms, function( taxonomy, term_ids ) {
577 
578 						if ( term_ids.length === 1 ) {
579 							GeoMashup.setMarkerImage( marker, GeoMashup.term_manager.getTermData( taxonomy, term_ids[0], 'icon' ).image );
580 						}
581 
582 					} );
583 
584 				} else {
585 					GeoMashup.setMarkerImage( marker, objects[0].icon.image );
586 				}
587 			}
588 		}
589 		/**
590 		 * Visibility is being tested for a marker.
591 		 * @name GeoMashup#markerVisibilityOptions
592 		 * @event
593 		 * @param {GeoMashupOptions} properties Geo Mashup configuration data
594 		 * @param {VisibilityFilter} filter Test and set filter.visible
595 		 * @param {Marker} marker The marker being tested
596 		 * @param {Map} map The map for context
597 		 */
598 		this.doAction( 'markerVisibilityOptions', this.opts, filter, marker, this.map );
599 
600 		return filter.visible;
601 	},
602 
603 	isObjectOn : function( obj ) {
604 		var filter = {
605 			visible: false
606 		};
607 
608 		obj.visible_terms = {};
609 		obj.visible_term_count = 0;
610 
611 		if ( !GeoMashup.term_manager || 0 === obj.combined_term_count ) {
612 
613 			// Objects without terms are visible by default
614 			filter.visible = true;
615 
616 		} else {
617 
618 			// Check term visibility
619 			jQuery.each( obj.terms, function( taxonomy, term_ids ) {
620 				
621 				obj.visible_terms[taxonomy] = [];
622 
623 				jQuery.each( term_ids, function( i, term_id ) {
624 					
625 					if ( GeoMashup.term_manager.getTermData( taxonomy, term_id, 'visible' ) ) {
626 						obj.visible_terms[taxonomy].push( term_id );
627 						obj.visible_term_count += 1;
628 						filter.visible = true;
629 					}
630 
631 				});
632 
633 			});
634 
635 		}
636 
637 		/**
638 		 * Visibility is being tested for an object.
639 		 * @name GeoMashup#objectVisibilityOptions
640 		 * @event
641 		 * @param {GeoMashupOptions} properties Geo Mashup configuration data
642 		 * @param {VisibilityFilter} filter Test and set filter.visible
643 		 * @param {Object} object The object being tested
644 		 * @param {Map} map The map for context
645 		 */
646 		this.doAction( 'objectVisibilityOptions', this.opts, filter, obj, this.map );
647 
648 		return filter.visible;
649 	},
650 
651 	/**
652 	 * Extract the IDs of objects that are "on" (not filtered by a control).
653 	 * @since 1.4.2
654 	 * @param {Array} objects The objects to check
655 	 * @returns {Array} The IDs of the "on" objects
656 	 */
657 	getOnObjectIDs : function( objects ) {
658 		var i, object_ids = [];
659 		for( i = 0; i < objects.length; i += 1 ) {
660 			if ( this.isObjectOn( objects[i] ) ) {
661 				object_ids.push( objects[i].object_id );
662 			}
663 		}
664 		return object_ids;
665 	},
666 
667 	/**
668 	 * Add objects to the map.
669 	 * @param {Object} response_data Data returned by a geo query.
670 	 * @param {Boolean} add_term_info Whether to build and show term
671 	 *   data for these objects, for legend or other term controls.
672 	 */
673 	addObjects : function(response_data, add_term_info) {
674 		var i, k, object_id, point, taxonomy, term_ids, term_id, marker, plus_image,
675 			added_markers = [];
676 
677 		if ( add_term_info && this.term_manager ) {
678 			this.term_manager.reset();
679 		}
680 
681 		for (i = 0; i < response_data.length; i+=1) {
682 			// Make a marker for each new object location
683 			object_id = response_data[i].object_id;
684 			point = this.newLatLng(
685 				parseFloat(response_data[i].lat),
686 				parseFloat(response_data[i].lng)
687 			);
688 
689 			// Back compat for categories API
690 			response_data[i].categories = [];
691 
692 			response_data[i].combined_term_count = 0;
693 			if ( this.term_manager ) {
694 				// Add terms
695 				for( taxonomy in response_data[i].terms ) {
696 					if ( response_data[i].terms.hasOwnProperty( taxonomy ) && typeof taxonomy !== 'function' ) {
697 
698 						term_ids = response_data[i].terms[taxonomy];
699 						for (k = 0; k < term_ids.length; k+=1) {
700 							GeoMashup.term_manager.extendTerm( point, taxonomy, term_ids[k], response_data[i] );
701 						}
702 
703 						response_data[i].combined_term_count += term_ids.length;
704 
705 						if ( 'category' === taxonomy ) {
706 							response_data[i].categories = term_ids;
707 						}
708 					}
709 				}
710 			}
711 			
712 			if (this.opts.max_posts && this.object_count >= this.opts.max_posts) {
713 				break;
714 			}
715 
716 			if (!this.objects[object_id]) {
717 				// This object has not yet been loaded
718 				this.objects[object_id] = response_data[i];
719 				this.object_count += 1;
720 				if (!this.locations[point]) {
721 					// There are no other objects yet at this point, create a marker
722 					this.extendLocationBounds( point );
723 					this.locations[point] = {
724 						objects : [ response_data[i] ], 
725 						loaded_content: {}
726 					};
727 					marker = this.createMarker(point, response_data[i]);
728 					this.objects[object_id].marker = marker;
729 					this.locations[point].marker = marker;
730 					added_markers.push( marker );
731 				} else {
732 					// There is already a marker at this point, add the new object to it
733 					this.locations[point].objects.push( response_data[i] );
734 					marker = this.locations[point].marker;
735 					this.makeMarkerMultiple( marker );
736 					this.objects[object_id].marker = marker;
737 					this.addObjectIcon( this.objects[object_id] );
738 				}
739 			}
740 		} // end for each marker
741 
742 		// Openlayers at least only gets clicks on the top layer, so add markers last
743 		this.addMarkers( added_markers );
744 
745 		if ( this.term_manager ) {
746 			this.term_manager.populateTermElements();
747 		}
748 
749 		if (this.firstLoad) {
750 			this.firstLoad = false;
751 			if ( this.opts.auto_info_open && this.object_count > 0 ) {
752 				if ( !this.opts.open_object_id ) {
753 					if ( this.opts.context_object_id && this.objects[ this.opts.context_object_id ] ) {
754 						this.opts.open_object_id = this.opts.context_object_id;
755 					} else {
756 						this.opts.open_object_id = response_data[0].object_id;
757 					}
758 				}
759 				this.clickObjectMarker(this.opts.open_object_id);
760 			}
761 			if ( this.opts.zoom === 'auto' ) {
762 				this.autoZoom();
763 			} else {
764 				if ( this.opts.context_object_id && this.objects[ this.opts.context_object_id ] ) {
765 					this.centerMarker( this.objects[ this.opts.context_object_id ].marker, parseInt( this.opts.zoom, 10 ) );
766 				}
767 				this.updateVisibleList();
768 			}
769 		}
770 	},
771 
772 	requestObjects : function(use_bounds) {
773 	// provider override (maybe jQuery?)
774 	},
775 
776 	/**
777 	 * Hide all markers.
778 	 */
779 	hideMarkers : function() {
780 		var point;
781 
782 		for (point in this.locations) {
783 			if ( this.locations.hasOwnProperty( point ) && this.locations[point].marker ) {
784 				this.hideMarker( this.locations[point].marker );
785 			}
786 		}
787 		this.recluster();
788 		this.updateVisibleList();
789 	},
790 
791 	/**
792 	 * Show all unfiltered markers.
793 	 */
794 	showMarkers : function() {
795 
796 		jQuery.each( this.locations, function( point, location ) {
797 			if ( GeoMashup.isMarkerOn( location.marker ) ) {
798 				GeoMashup.showMarker( location.marker );
799 			}
800 		});
801 		this.recluster();
802 		this.updateVisibleList();
803 
804 	},
805 
806 	adjustZoom : function() {
807 		var old_level, new_level;
808 		new_level = this.map.getZoom();
809 		if ( typeof this.last_zoom_level === 'undefined' ) {
810 			this.last_zoom_level = new_level;
811 		}
812 		old_level = this.last_zoom_level;
813 
814 		if ( this.term_manager ) {
815 			this.term_manager.updateLineZoom( old_level, new_level );
816 		}
817 
818 		if ( this.clusterer && 'google' === this.opts.map_api ) {
819 			if ( old_level <= this.opts.cluster_max_zoom && 
820 				new_level > this.opts.cluster_max_zoom ) {
821 				this.clusterer.clusteringEnabled = false;
822 				this.clusterer.refresh( true );
823 			} else if ( old_level > this.opts.cluster_max_zoom &&
824 				new_level <= this.opts.cluster_max_zoom ) {
825 				this.clusterer.clusteringEnabled = true;
826 				this.clusterer.refresh( true );
827 			}
828 		}
829 		this.last_zoom_level = new_level;
830 	},
831 
832 	objectLinkHtml : function(object_id) {
833 		return ['<a href="#',
834 		this.opts.name,
835 		'" onclick="frames[\'',
836 		this.opts.name,
837 		'\'].GeoMashup.clickObjectMarker(',
838 		object_id,
839 		');">',
840 		this.objects[object_id].title,
841 		'</a>'].join('');
842 	},
843 
844 	/**
845 	 * Whether a marker is currently visible on the map.
846 	 * @param {Marker} marker
847 	 * @return {Boolean} False if the marker is hidden or outside the current viewport.
848 	 */
849 	isMarkerVisible : function( marker ) {
850 		// Provider override
851 		return false;
852 	},
853 
854 	/**
855 	 * Recompile the list of objects currently visible on the map.
856 	 */
857 	updateVisibleList : function() {
858 		var list_element, header_element, list_html, list_count = 0;
859 
860 		if (this.have_parent_access && this.opts.name) {
861 			header_element = parent.document.getElementById(this.opts.name + "-visible-list-header");
862 			list_element = parent.document.getElementById(this.opts.name + "-visible-list");
863 		}
864 		if (header_element) {
865 			header_element.style.display = 'block';
866 		}
867 		if (list_element) {
868 			list_html = ['<ul class="gm-visible-list">'];
869 			this.forEach( this.objects, function (object_id, obj) {
870 				if ( this.isObjectOn( obj ) && this.isMarkerVisible( obj.marker ) ) {
871 					list_html.push('<li><img src="');
872 					list_html.push(obj.icon.image);
873 					list_html.push('" alt="');
874 					list_html.push(obj.title);
875 					list_html.push('" />');
876 					list_html.push(this.objectLinkHtml(object_id));
877 					list_html.push('</li>');
878 					list_count += 1;
879 				}
880 			});
881 			list_html.push('</ul>');
882 			list_element.innerHTML = list_html.join('');
883 			/** 
884 			 * The visible posts list was updated.
885 			 * @name GeoMashup#updatedVisibleList
886 			 * @event
887 			 * @param {GeoMashupOptions} properties Geo Mashup configuration data
888 			 * @param {Number} list_count The number of items in the list
889 			 */
890 			this.doAction( 'updatedVisibleList', this.opts, list_count );
891 		}
892 	},
893 
894 	adjustViewport : function() {
895 		this.updateVisibleList();
896 	},
897 
898 	createMap : function(container, opts) {
899 	// Provider override
900 	}
901 };
902