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 GeoMashup */
 24 // These globals are retained for backward custom javascript compatibility
 25 /*global customizeGeoMashup, customizeGeoMashupMap, customGeoMashupColorIcon, customGeoMashupCategoryIcon */
 26 /*global customGeoMashupSinglePostIcon, customGeoMashupMultiplePostImage */
 27 
 28 var GeoMashup, customizeGeoMashup, customizeGeoMashupMap, customGeoMashupColorIcon, customGeoMashupCategoryIcon, 
 29 	customGeoMashupSinglePostIcon, customGeoMashupMultiplePostImage;
 30 
 31 /** 
 32  * @name GeoMashupObject
 33  * @class This type represents an object Geo Mashup can place on the map.
 34  * It has no constructor, but is instantiated as an object literal.
 35  * Custom properties can be added, but some are present by default.
 36  * 
 37  * @property {String} object_name The type of object: post, user, comment, etc.
 38  * @property {String} object_id A unique identifier for the object
 39  * @property {String} title
 40  * @property {Number} lat Latitude
 41  * @property {Number} lng Longitude
 42  * @property {String} author_name The name of the object author
 43  * @property {Array} categories The IDs of the categories this object belongs to
 44  * @property {GeoMashupIcon} icon The default icon to use for the object
 45  */
 46  
 47 /**
 48  * @name GeoMashupIcon
 49  * @class This type represents an icon that can be used for a map marker.
 50  * It has no constructor, but is instantiated as an object literal.
 51  * @property {String} image URL of the icon image
 52  * @property {String} iconShadow URL of the icon shadow image
 53  * @property {Array} iconSize Pixel width and height of the icon
 54  * @property {Array} shadowSize Pixel width and height of the icon shadow 
 55  * @property {Array} iconAnchor Pixel offset from top left: [ right, down ]
 56  * @property {Array} infoWindowAnchor Pixel offset from top left: [ right, down ]
 57  */
 58 
 59 /** 
 60  * @name GeoMashupOptions
 61  * @class This type represents options used for a specific Geo Mashup map. 
 62  * It has no constructor, but is instantiated as an object literal.
 63  * Properties reflect the <a href="http://code.google.com/p/wordpress-geo-mashup/wiki/TagReference#Map">map tag parameters</a>.
 64  */
 65 
 66 /**
 67  * @namespace Used more as a singleton than a namespace for data and methods for a single Geo Mashup map.
 68  *
 69  * <p>Violates the convention that capitalized objects are designed to be used with the 
 70  * 'new' keyword - an artifact of the age of the project. :o</p>
 71  * 
 72  * <p><strong>Note: Events are Actions</strong></p>
 73  *
 74  * <p>Actions available for use with GeoMashup.addAction() are documented as events.
 75  * See the <a href="http://code.google.com/p/wordpress-geo-mashup/wiki/Documentation#Custom_JavaScript">custom javascript documentation</a>
 76  * for an example.
 77  * </p>
 78  */
 79 GeoMashup = {
 80 	actions : {},
 81 	objects : {},
 82 	object_count : 0,
 83 	locations : {},
 84 	categories : {}, // only categories on the map here
 85 	category_count : 0,
 86 	open_attachments : [],
 87 	errors : [],
 88 	color_names : ['red','lime','blue','orange','yellow','aqua','green','silver','maroon','olive','navy','purple','gray','teal','fuchsia','white','black'],
 89 	colors : {
 90 		'red':'#ff0000',
 91 		'lime':'#00ff00',
 92 		'blue':'#0000ff',
 93 		'orange':'#ffa500',
 94 		'yellow':'#ffff00',
 95 		'aqua':'#00ffff',
 96 		'green':'#008000',
 97 		'silver':'#c0c0c0',
 98 		'maroon':'#800000',
 99 		'olive':'#808000',
100 		'navy':'#000080',
101 		'purple':'#800080',
102 		'gray':'#808080',
103 		'teal':'#008080',
104 		'fuchsia':'#ff00ff',
105 		'white':'#ffffff',
106 		'black':'#000000'},
107 	firstLoad : true,
108 
109 	clone : function( obj ) {
110 		var ClonedObject = function(){};
111 		ClonedObject.prototype = obj;
112 		return new ClonedObject();
113 	},
114 
115 	forEach : function( obj, callback ) {
116 		var key;
117 		for( key in obj ) {
118 			if ( obj.hasOwnProperty( key ) && typeof obj[key] !== 'function' ) {
119 				callback.apply( this, [key, obj[key]] );
120 			}
121 		}
122 	},
123 
124 	/**
125 	 * Add an action callback to extend Geo Mashup functionality.
126 	 * 
127 	 * Essentially an event interface. Might make sense to convert to 
128 	 * Mapstraction events in the future.
129 	 *
130 	 * @param {String} name The name of the action (event).
131 	 * @param {Function} callback The function to call when the action occurs
132 	 */
133 	addAction : function ( name, callback ) {
134 		if ( typeof callback !== 'function' ) {
135 			return false;
136 		}
137 		if ( typeof this.actions[name] !== 'object' ) {
138 			this.actions[name] = [callback];
139 		} else {
140 			this.actions[name].push( callback );
141 		}
142 		return true;
143 	},
144 
145 	/**
146 	 * Fire all callbacks for an action.
147     * 
148 	 * Essentially an event interface. Might make sense to convert to 
149 	 * Mapstraction events in the future.
150 	 *
151 	 * @param {String} name The name of the action (event).
152 	 */
153 	doAction : function ( name ) {
154 		var args, i;
155 
156 		if ( typeof this.actions[name] !== 'object' ) {
157 			return false;
158 		}
159 		args = Array.prototype.slice.apply( arguments, [1] );
160 		for ( i = 0; i < this.actions[name].length; i += 1 ) {
161 			if ( typeof this.actions[name][i] === 'function' ) {
162 				this.actions[name][i].apply( null, args );
163 			}
164 		}
165 		return true;
166 	},
167 
168 	parentScrollToGeoPost : function () {
169 		var geo_post;
170 		if ( this.have_parent_access ) {
171 			geo_post = parent.document.getElementById('gm-post');
172 			if (geo_post) {
173 				parent.focus();
174 				parent.scrollTo(geo_post.offsetLeft, geo_post.offsetTop);
175 			}
176 		}
177 		return false;
178 	},
179 
180 	buildCategoryHierarchy : function(category_id) {
181 		var children, child_count, cat_id, child_id;
182 		if (category_id) {
183 			children = {};
184 			child_count = 0;
185 			for (child_id in this.opts.category_opts) {
186 				if (this.opts.category_opts[child_id].parent_id && 
187 					this.opts.category_opts[child_id].parent_id == category_id) {
188 						children[child_id] = this.buildCategoryHierarchy(child_id);
189 						child_count += 1;
190 				}
191 			}
192 			return (child_count > 0) ? children : null;
193 		} else {
194 			this.category_hierarchy = {};
195 			for (cat_id in this.opts.category_opts) {
196 				if (!this.opts.category_opts[cat_id].parent_id) {
197 					this.category_hierarchy[cat_id] = this.buildCategoryHierarchy(cat_id);
198 				}
199 			}
200 		}
201 	},
202 
203 	/**
204 	 * Determine whether a category ID is an ancestor of another.
205 	 * 
206 	 * Works on the loadedMap action and after, when the category hierarchy has been
207 	 * determined.
208 	 * 
209 	 * @param {String} ancestor_id The category ID of the potential ancestor
210 	 * @param {String} child_id The category ID of the potential child
211 	 */
212 	isCategoryAncestor : function(ancestor_id, child_id) {
213 		if (this.opts.category_opts[child_id].parent_id) {
214 			if (this.opts.category_opts[child_id].parent_id == ancestor_id) {
215 				return true;
216 			} else {
217 				return this.isCategoryAncestor(ancestor_id, this.opts.category_opts[child_id].parent_id);
218 			}
219 		} else {
220 			return false;
221 		}
222 	},
223 
224 	hasLocatedChildren : function(category_id, hierarchy) {
225 		var child_id;
226 
227 		if (this.categories[category_id]) {
228 			return true;
229 		}
230 		for (child_id in hierarchy) {
231 			if (this.hasLocatedChildren(child_id, hierarchy[child_id])) {
232 				return true;
233 			}
234 		}
235 		return false;
236 	},
237 
238 	searchCategoryHierarchy : function(search_id, hierarchy) {
239 		var child_search, category_id;
240 
241 		if (!hierarchy) {
242 			hierarchy = this.category_hierarchy;
243 		}
244 		// Use a regular loop, so it can return a value for this function
245 		for( category_id in hierarchy ) {
246 			if ( hierarchy.hasOwnProperty( category_id ) && typeof hierarchy[category_id] !== 'function' ) {
247 				if (category_id === search_id) {
248 					return hierarchy[category_id];
249 				}else if (hierarchy[category_id]) {
250 					child_search = this.searchCategoryHierarchy(search_id, hierarchy[category_id]);
251 					if (child_search) {
252 						return child_search;
253 					}
254 				}
255 			}
256 		}
257 		return null;
258 	},
259 
260 	/**
261 	 * Hide a category and all its child categories.
262 	 * @param {String} category_id The ID of the category to hide
263 	 */
264 	hideCategoryHierarchy : function(category_id) {
265 		var child_id;
266 
267 		this.hideCategory(category_id);
268 		this.forEach( this.tab_hierarchy[category_id], function (child_id) {
269 			this.hideCategoryHierarchy(child_id);
270 		});
271   },
272 
273 	/**
274 	 * Show a category and all its child categories.
275 	 * @param {String} category_id The ID of the category to show
276 	 */
277 	showCategoryHierarchy : function(category_id) {
278 		var child_id;
279 
280 		this.showCategory(category_id);
281 		this.forEach( this.tab_hierarchy[category_id], function (child_id) {
282 			this.showCategoryHierarchy(child_id);
283 		});
284   },
285 
286 	/**
287 	 * Select a tab of the tabbed category index control.
288 	 * @param {String} select_category_id The ID of the category tab to select
289 	 */
290 	categoryTabSelect : function(select_category_id) {
291 		var i, tab_div, tab_list_element, tab_element, id_match, category_id, index_div;
292 
293 		if ( !this.have_parent_access ) {
294 			return false;
295 		}
296 		tab_div = parent.document.getElementById(this.opts.name + '-tab-index');
297 		if (!tab_div) {
298 			return false;
299 		}
300 		tab_list_element = tab_div.childNodes[0];
301 		for (i=0; i<tab_list_element.childNodes.length; i += 1) {
302 			tab_element = tab_list_element.childNodes[i];
303 			id_match = tab_element.childNodes[0].href.match(/\d+$/);
304 			category_id = id_match && id_match[0];
305 			if (category_id === select_category_id) {
306 				tab_element.className = 'gm-tab-active gm-tab-active-' + select_category_id;
307 			}else {
308 				tab_element.className = 'gm-tab-inactive gm-tab-inactive-' + category_id;
309 			}
310 		}
311 		this.forEach( this.tab_hierarchy, function (category_id) {
312 			index_div = parent.document.getElementById(this.categoryIndexId(category_id));
313 			if (index_div) {
314 				if (category_id === select_category_id) {
315 					index_div.className = 'gm-tabs-panel';
316 				} else {
317 					index_div.className = 'gm-tabs-panel gm-hidden';
318 					if (!this.opts.show_inactive_tab_markers) {
319 						this.hideCategoryHierarchy(category_id);
320 					}
321 				}
322 			}
323 		});
324 		if (!this.opts.show_inactive_tab_markers) {
325 			// Done last so none of the markers get re-hidden
326 			this.showCategoryHierarchy(select_category_id);
327 		}
328 	},
329 
330 	/**
331 	 * Get the DOM ID of the element containing a category index in the 
332 	 * tabbed category index control.
333 	 * @param {String} category_id The category ID
334 	 */
335 	categoryIndexId : function(category_id) {
336 		return 'gm-cat-index-' + category_id;
337 	},
338 
339 	categoryTabIndexHtml : function(hierarchy) {
340 		var html_array = [], category_id;
341 
342 		html_array.push('<div id="');
343 		html_array.push(this.opts.name);
344 		html_array.push('-tab-index"><ul class="gm-tabs-nav">');
345 		for (category_id in hierarchy) {
346 			if (this.hasLocatedChildren(category_id, hierarchy[category_id])) {
347 				html_array = html_array.concat([
348 					'<li class="gm-tab-inactive gm-tab-inactive-',
349 					category_id,
350 					'"><a href="#',
351 					this.categoryIndexId(category_id),
352 					'" onclick="frames[\'',
353 					this.opts.name,
354 					'\'].GeoMashup.categoryTabSelect(\'',
355 					category_id,
356 					'\'); return false;">']);
357 				if (this.categories[category_id]) {
358 					html_array.push('<img src="');
359 					html_array.push(this.categories[category_id].icon.image);
360 					html_array.push('" />');
361 				}
362 				html_array.push('<span>');
363 				html_array.push(this.opts.category_opts[category_id].name);
364 				html_array.push('</span></a></li>');
365 			}
366 		} 
367 		html_array.push('</ul></div>');
368 		this.forEach( hierarchy, function (category_id) {
369 			html_array.push(this.categoryIndexHtml(category_id, hierarchy[category_id]));
370 		});
371 		return html_array.join('');
372 	},
373 
374 	categoryIndexHtml : function(category_id, children, top_level) {
375 		var i, a_name, b_name, html_array = [], group_count, ul_open_tag, child_id;
376 		if ( typeof top_level === 'undefined' ) {
377 			top_level = true;
378 		}
379 		html_array.push('<div id="');
380 		html_array.push(this.categoryIndexId(category_id));
381 		html_array.push('" class="gm-tabs-panel');
382 		if ( top_level ) {
383 			html_array.push(' gm-hidden');
384 		}
385 		html_array.push('"><ul class="gm-index-posts">');
386 		if (this.categories[category_id]) {
387 			this.categories[category_id].posts.sort(function (a, b) {
388 				a_name = GeoMashup.objects[a].title;
389 				b_name = GeoMashup.objects[b].title;
390 				if (a_name === b_name) {
391 					return 0;
392 				} else {
393 					return a_name < b_name ? -1 : 1;
394 				}
395 			});
396 			for (i=0; i<this.categories[category_id].posts.length; i += 1) {
397 				html_array.push('<li>');
398 				html_array.push(this.objectLinkHtml(this.categories[category_id].posts[i]));
399 				html_array.push('</li>');
400 			}
401 		}
402 		html_array.push('</ul>');
403 		group_count = 0;
404 		ul_open_tag = '<ul class="gm-sub-cat-index">';
405 		html_array.push(ul_open_tag);
406 		this.forEach( children, function (child_id) {
407 			html_array.push('<li>');
408 			if (this.categories[child_id]) {
409 				html_array.push('<img src="');
410 				html_array.push(this.categories[child_id].icon.image);
411 				html_array.push('" />');
412 				html_array.push('<span class="gm-sub-cat-title">');
413 				html_array.push(this.opts.category_opts[child_id].name);
414 				html_array.push('</span>');
415 			}
416 			html_array.push(this.categoryIndexHtml(child_id, children[child_id], false));
417 			html_array.push('</li>');
418 			group_count += 1;
419 			if (this.opts.tab_index_group_size && group_count%this.opts.tab_index_group_size === 0) {
420 				html_array.push('</ul>');
421 				html_array.push(ul_open_tag);
422 			}
423 		});
424 		html_array.push('</ul></div>');
425 		return html_array.join('');
426 	},
427 
428 	createCategoryLine : function( category ) {
429 		// Provider override
430 	},
431 
432 	showCategoryInfo : function() {
433 		var legend_element = null,
434 			legend_html,
435 			category_id,
436 			label,
437 			list_tag,
438 			row_tag,
439 			term_tag,
440 			definition_tag,
441 			id;
442 		if (this.opts.name && this.have_parent_access ) {
443 			legend_element = parent.document.getElementById(this.opts.name + "-legend");
444 		}
445 		if (!legend_element && this.have_parent_access ) {
446 			legend_element = parent.document.getElementById("gm-cat-legend");
447 		}
448 		if (this.opts.legend_format && 'dl' === this.opts.legend_format) {
449 			list_tag = 'dl';
450 			row_tag = '';
451 			term_tag = 'dt';
452 			definition_tag = 'dd';
453 		} else {
454 			list_tag = 'table';
455 			row_tag = 'tr';
456 			term_tag = 'td';
457 			definition_tag = 'td';
458 		}
459 
460 		legend_html = ['<', list_tag, ' class="gm-legend">'];
461 		this.forEach(this.categories, function (category_id, category) {
462 			this.createCategoryLine( category );
463 			if (legend_element) {
464 				// Default is interactive
465 				if (typeof this.opts.interactive_legend === 'undefined') {
466 					this.opts.interactive_legend = true;
467 				}
468 				if (this.opts.name && this.opts.interactive_legend) {
469 					id = 'gm-cat-checkbox-' + category_id;
470 					label = [
471 						'<label for="',
472 						id,
473 						'"><input type="checkbox" name="category_checkbox" id="',
474 						id,
475 						'" onclick="if (this.checked) { frames[\'',
476 						this.opts.name,
477 						'\'].GeoMashup.showCategory(\'',
478 						category_id,
479 						'\'); } else { frames[\'',
480 						this.opts.name,
481 						'\'].GeoMashup.hideCategory(\'',
482 						category_id,
483 						'\'); }" checked="true" />',
484 						this.opts.category_opts[category_id].name,
485 						'</label>'].join('');
486 				} else {
487 					label = this.opts.category_opts[category_id].name;
488 				}
489 				if (row_tag) {
490 					legend_html.push('<' + row_tag + '>');
491 				}
492 				legend_html = legend_html.concat([
493 					'<',
494 					term_tag,
495 					'><img src="',
496 					category.icon.image,
497 					'" alt="',
498 					category_id,
499 					'"></',
500 					term_tag,
501 					'><',
502 					definition_tag,
503 					'>',
504 					label,
505 					'</',
506 					definition_tag,
507 					'>']);
508 				if (row_tag) {
509 					legend_html.push('</' + row_tag + '>');
510 				}
511 			}
512 		}); // end forEach this.categories
513 		legend_html.push('</' + list_tag + '>');
514 		if (legend_element) {
515 			legend_element.innerHTML = legend_html.join('');
516 		}
517 	},
518 
519 	initializeTabbedIndex : function() {
520 		var category_id,
521 			index_element;
522 		if ( !this.have_parent_access ) {
523 			return false;
524 		}
525 		index_element = parent.document.getElementById(this.opts.name + "-tabbed-index");
526 		if (!index_element) {
527 			index_element = parent.document.getElementById("gm-tabbed-index");
528 		}
529 		if (index_element) {
530 			if (this.opts.start_tab_category_id) {
531 				this.tab_hierarchy = this.searchCategoryHierarchy(this.opts.start_tab_category_id);
532 			} else {
533 				this.tab_hierarchy = this.category_hierarchy;
534 			}
535 			index_element.innerHTML = this.categoryTabIndexHtml(this.tab_hierarchy);
536 			if (!this.opts.disable_tab_auto_select) {
537 				// Select the first tab
538 				for (category_id in this.tab_hierarchy) {
539 					if (this.tab_hierarchy.hasOwnProperty(category_id) && typeof category_id !== 'function') {
540 						if (this.hasLocatedChildren(category_id, this.tab_hierarchy[category_id])) {
541 							this.categoryTabSelect(category_id);
542 							break;
543 						}
544 					}
545 				}
546 			}
547 		}
548 	}, 
549 
550 	/**
551 	 * Get the DOM element where the full post content should be displayed, if any.
552 	 * @returns {DOMElement} The element, or undefined if none.
553 	 */
554 	getShowPostElement : function() {
555 	  if ( this.have_parent_access && !this.show_post_element && this.opts.name) {
556 			this.show_post_element = parent.document.getElementById(this.opts.name + '-post');
557 		}
558 	  if ( this.have_parent_access && !this.show_post_element) {
559 			this.show_post_element = parent.document.getElementById('gm-post');
560 		}
561 		return this.show_post_element;
562 	},
563 
564 	/**
565 	 * Change the target of links in HTML markup to target the parent frame.
566 	 * @param {String} markup
567 	 * @returns {String} Modified markup
568 	 */
569 	parentizeLinksMarkup : function( markup ) {
570 		var container = document.createElement( 'div' );
571 		container.innerHTML = markup;
572 		this.parentizeLinks( container );
573 		return container.innerHTML;
574 	},
575 
576 	/**
577 	 * Change the target of links in a DOM element to target the parent frame.
578 	 * @param {DOMElement} node The element to change
579 	 */
580 	parentizeLinks : function( node ) {
581 		var i, links = node.getElementsByTagName('a');
582 		if ( parent ) {
583 			for (i=0; i<links.length; i += 1) {
584 				if ( links[i].target.length === 0 || links[i].target === '_self' ) {
585 					links[i].target = "_parent";
586 				}
587 			}
588 		}
589 	},
590 
591 	/**
592 	 * Display a spinner icon for the map.
593 	 */
594 	showLoadingIcon : function() {
595 		if ( ! this.spinner_div.parentNode ) {
596 			this.container.appendChild( this.spinner_div );
597 		}
598 	},
599 
600 	/**
601 	 * Hide the spinner icon for the map.
602 	 */
603 	hideLoadingIcon : function() {
604 		if ( this.spinner_div.parentNode ) {
605 			this.spinner_div.parentNode.removeChild( this.spinner_div );
606 		}
607 	},
608 
609 	/**
610 	 * Get the objects at a specified location.
611 	 * @param {LatLonPoint} point The query location
612 	 * @returns {Array} The mapped objects at the query location
613 	 */
614 	getObjectsAtLocation : function( point ) {
615 		return this.locations[point].objects;
616 	},
617 
618 	/**
619 	 * Get the objects at the location of a specified marker.
620 	 * @param {Marker} marker 
621 	 * @returns {Array} The mapped objects at the marker location
622 	 */
623 	getMarkerObjects : function( marker ) {
624 		return this.getObjectsAtLocation( this.getMarkerLatLng( marker ) );
625 	},
626 
627 	/**
628 	 * Get the location coordinates for a marker.
629 	 * @param {Marker} marker 
630 	 * @returns {LatLonPoint} The marker coordinates
631 	 */
632 	getMarkerLatLng : function( marker ) {
633 		// Provider override
634 	},
635 
636 	/**
637 	 * Obscure an existing marker with the highlighted "glow" marker.
638 	 * @param {Marker} marker The existing marker
639 	 */
640 	addGlowMarker : function( marker ) {
641 		// Provider override
642 	},
643 
644 	/**
645 	 * Open the info bubble for a marker.
646 	 * @param {Marker} marker
647 	 */
648 	openInfoWindow : function( marker ) {
649 		// Provider override
650 	},
651 
652 	/**
653 	 * Close the info bubble for a marker.
654 	 * @param {Marker} marker
655 	 */
656 	closeInfoWindow : function( marker ) {
657 		// provider override
658 	},
659 
660 	/**
661 	 * Remove the highlighted "glow" marker from the map if it exists.
662 	 */
663 	removeGlowMarker : function() {
664 		// Provider override
665 	},
666 
667 	/**
668 	 * Hide any visible attachment layers on the map.
669 	 */
670 	hideAttachments : function() {
671 		// Provider override
672 	},
673 
674 	/**
675 	 * Show any attachment layers associated with the objects represented
676 	 * by a marker, loading the layer if necessary.
677 	 * @param {Marker} marker
678 	 */
679 	showMarkerAttachments : function( marker ) {
680 		// Provider override
681 	},
682 
683 	/** 
684 	 * Load full content for the objects/posts at a location into the 
685 	 * full post display element.
686 	 * @param {LatLonPoint} point
687 	 */
688 	loadFullPost : function( point ) {
689 		// jQuery or provider override
690 	},
691 
692 	/**
693 	 * Select a marker.
694 	 * @param {Marker} marker
695 	 */
696 	selectMarker : function( marker ) {
697 		var point = this.getMarkerLatLng( marker );
698 
699 		this.selected_marker = marker;
700 		if ( this.opts.marker_select_info_window ) {
701 			this.openInfoWindow( marker );
702 		}
703 		if ( this.opts.marker_select_attachments ) {
704 			this.showMarkerAttachments( marker );
705 		}
706 		if ( this.opts.marker_select_highlight ) {
707 			this.addGlowMarker( marker );
708 		}
709 		if ( this.opts.marker_select_center ) {
710 			this.centerMarker( marker );
711 		}
712 		if ('full-post' !== this.opts.template && this.getShowPostElement()) {
713 			if ( this.locations[point].post_html ) {
714 				this.getShowPostElement().innerHTML = this.locations[point].post_html;
715 			} else {
716 				this.loadFullPost( point );
717 			}
718 		}
719 		/**
720 		 * A marker was selected.
721 		 * @name GeoMashup#selectedMarker
722 		 * @event
723 		 * @param {GeoMashupOptions} properties Geo Mashup configuration data
724 		 * @param {Marker} marker The selected marker
725 		 * @param {Map} map The map containing the marker
726 		 */
727 		this.doAction( 'selectedMarker', this.opts, this.selected_marker, this.map );
728 	},
729 
730 	/**
731 	 * Center and optionally zoom to a marker.
732 	 * @param {Marker} marker 
733 	 * @param {Number} zoom Optional zoom level
734 	 */
735 	centerMarker : function ( marker, zoom ) {
736 		// provider override
737 	},
738 
739 	/**
740 	 * De-select the currently selected marker if there is one.
741 	 */
742 	deselectMarker : function() {
743 		var i, post_element = GeoMashup.getShowPostElement();
744 		if ( post_element ) {
745 			post_element.innerHTML = '';
746 		}
747 		if ( this.glow_marker ) {
748 			this.removeGlowMarker();
749 		}
750 		if ( this.selected_marker ) {
751 			this.closeInfoWindow( this.selected_marker );
752 		}
753 		this.hideAttachments();
754 		this.selected_marker = null;
755 	},
756 
757 	addObjectIcon : function( obj ) {
758 		// provider override
759 	},
760 
761 	createMarker : function( point, obj ) {
762 		var marker;
763 		// provider override
764 		return marker;
765 	},
766 
767 	checkDependencies : function () {
768 		// provider override
769 	},
770 
771 	/**
772 	 * Simulate a user click on the marker that represents a specified object.
773 	 * @param {String} object_id The ID of the object.
774 	 * @param {Number} try_count Optional number of times to try (in case the object 
775 	 *   is still being loaded).
776 	 */
777 	clickObjectMarker : function(object_id, try_count) {
778 		// provider override
779 	},
780 
781 	/**
782 	 * Backward compatibility for clickObjectMarker().
783 	 * @deprecated
784 	 */
785 	clickMarker : function( object_id, try_count ) {
786 		this.clickObjectMarker( object_id, try_count );
787 	},
788 
789 	/**
790 	 * Get the name of a category.
791 	 * @param {String} category_id
792 	 */
793 	getCategoryName : function (category_id) {
794 		return this.category_opts[category_id].name;
795 	},
796 
797 	/**
798 	 * Hide a marker.
799 	 * @param {Marker} marker
800 	 */
801 	hideMarker : function( marker ) {
802 		// Provider override
803 	},
804 
805 	/**
806 	 * Show a marker.
807 	 * @param {Marker} marker
808 	 */
809 	showMarker : function( marker ) {
810 		// Provider override
811 	},
812 
813 	/**
814 	 * Hide a line.
815 	 * @param {Polyline} line
816 	 */
817 	hideLine : function( line ) {
818 		// Provider override
819 	},
820 
821 	/**
822 	 * Show a line.
823 	 * @param {Polyline} line
824 	 */
825 	showLine : function( line ) {
826 		// Provider override
827 	},
828 
829 	/**
830 	 * Create a new geo coordinate object.
831 	 * @param {Number} lat Latitude
832 	 * @param {Number} lng Longitude
833 	 * @returns {LatLonPoint} Coordinates
834 	 */
835 	newLatLng : function( lat, lng ) {
836 		var latlng;
837 		// Provider override
838 		return latlng;
839 	},
840 
841 	extendLocationBounds : function( ) {
842 		// Provider override
843 	},
844 
845 	addMarkers : function( ) {
846 		// Provider override
847 	},
848 
849 	makeMarkerMultiple : function( ) {
850 		// Provider override
851 	},
852 
853 	/**
854 	 * Zoom the map to loaded content.
855 	 */
856 	autoZoom : function( ) {
857 		// Provider override
858 	},
859 
860 	/**
861 	 * If clustering is active, refresh clusters.
862 	 */
863 	recluster : function( ) {
864 		// Provider override
865 	},
866 
867 	extendCategory : function(point, category_id, post_id) {
868 		var icon, color, color_name, max_line_zoom;
869 
870 		if (!this.categories[category_id]) {
871 			if (this.opts.category_opts[category_id].color_name) {
872 				color_name = this.opts.category_opts[category_id].color_name;
873 			} else {
874 				color_name = this.color_names[this.category_count%this.color_names.length];
875 			}
876 			color = this.colors[color_name];
877 			// Custom callbacks
878 			if (!icon && typeof customGeoMashupCategoryIcon === 'function') {
879 				icon = customGeoMashupCategoryIcon(this.opts, [category_id]);
880 			}
881 			if (!icon && typeof customGeoMashupColorIcon === 'function') {
882 				icon = customGeoMashupColorIcon(this.opts, color_name);
883 			}
884 			if (!icon) {
885 				icon = this.colorIcon( color_name );
886 			}
887 			/**
888 			 * A category icon is being assigned.
889 			 * @name GeoMashup#categoryIcon
890 			 * @event
891 			 * @param {GeoMashupOptions} properties Geo Mashup configuration data
892 			 * @param {GeoMashupIcon} icon
893 			 * @param {String} category_id
894 			 */
895 			this.doAction( 'categoryIcon', this.opts, icon, category_id );
896 			/**
897 			 * A category icon is being assigned by color.
898 			 * @name GeoMashup#colorIcon
899 			 * @event
900 			 * @param {GeoMashupOptions} properties Geo Mashup configuration data
901 			 * @param {GeoMashupIcon} icon
902 			 * @param {String} color_name
903 			 */
904 			this.doAction( 'colorIcon', this.opts, icon, color_name );
905 
906 			max_line_zoom = -1;
907 			if (this.opts.category_opts[category_id].max_line_zoom) {
908 				max_line_zoom = this.opts.category_opts[category_id].max_line_zoom;
909 			}
910 			this.categories[category_id] = {
911 				icon : icon,
912 				points : [point],
913 				posts : [post_id],
914 				color : color,
915 				visible : true,
916 				max_line_zoom : max_line_zoom
917 			};
918 			this.category_count += 1;
919 		} else {
920 			this.categories[category_id].points.push(point);
921 			this.categories[category_id].posts.push(post_id);
922 		}
923 	},
924 
925 	/**
926 	 * Show or hide markers according to current visibility criteria.
927 	 */
928 	updateMarkerVisibilities : function( ) {
929 		this.forEach( this.locations, function( point, loc ) {
930 			GeoMashup.updateMarkerVisibility( loc.marker, point );
931 		} );
932 		this.updateVisibleList();
933 	},
934 
935 	updateMarkerVisibility : function( marker, point ) {
936 		var i, j, loc, obj, check_cat_id, options = {visible: false};
937 
938 		loc = this.locations[ point ];
939 		for ( i=0; i<loc.objects.length; i+=1 ) {
940 			obj = loc.objects[i];
941 			for ( j=0; j<obj.categories.length; j+=1 ) {
942 				check_cat_id = obj.categories[j];
943 				if ( this.categories[check_cat_id] && this.categories[check_cat_id].visible ) {
944 					options.visible = true;
945 				}
946 			}
947 			/**
948 			 * Visbility is being tested for an object.
949 			 * @name GeoMashup#objectVisibilityOptions
950 			 * @event
951 			 * @param {GeoMashupOptions} properties Geo Mashup configuration data
952 			 * @param {Object} options A container allowing the boolean options.visiblity to be updated
953 			 * @param {Object} object The object being tested
954 			 * @param {Map} map The map for context
955 			 */
956 			this.doAction( 'objectVisibilityOptions', this.opts, options, obj, this.map );
957 		}
958 		/**
959 		 * Visbility is being tested for a marker.
960 		 * @name GeoMashup#markerVisibilityOptions
961 		 * @event
962 		 * @param {GeoMashupOptions} properties Geo Mashup configuration data
963 		 * @param {Object} options A container allowing the boolean options.visiblity to be updated
964 		 * @param {Marker} marker The marker being tested
965 		 * @param {Map} map The map for context
966 		 */
967 		this.doAction( 'markerVisibilityOptions', this.opts, options, loc.marker, this.map );
968 
969 		if ( options.visible ) {
970 			this.showMarker( marker );
971 		} else {
972 			this.hideMarker( marker );
973 		}
974 	},
975 
976 	/**
977 	 * Hide markers and line for a category.
978 	 * @param {String} category_id
979 	 */
980 	hideCategory : function(category_id) {
981 		var i, loc;
982 
983 		if (!this.categories[category_id]) {
984 			return false;
985 		}
986 		if ( this.map.closeInfoWindow ) {
987 			this.map.closeInfoWindow();
988 		}
989 		if (this.categories[category_id].line) {
990 			this.hideLine( this.categories[category_id].line );
991 		}
992 		// A somewhat involved check for other visible categories at this location
993 		this.categories[category_id].visible = false;
994 		for (i=0; i<this.categories[category_id].points.length; i+=1) {
995 			loc = this.locations[ this.categories[category_id].points[i] ];
996 			this.updateMarkerVisibility( loc.marker, this.categories[category_id].points[i] );
997 		}
998 		this.recluster();
999 		this.updateVisibleList();
1000 	},
1001 
1002 	/**
1003 	 * Show markers for a category. Also show line if consistent with configuration.
1004 	 * @param {String} category_id
1005 	 */
1006 	showCategory : function(category_id) {
1007 		var i, point;
1008 
1009 		if (!this.categories[category_id]) {
1010 			return false;
1011 		}
1012 		if (this.categories[category_id].line && this.map.getZoom() <= this.categories[category_id].max_line_zoom) {
1013 			this.showLine( this.categories[category_id].line );
1014 		}
1015 		this.categories[category_id].visible = true;
1016 		for (i=0; i<this.categories[category_id].points.length; i+=1) {
1017 			point = this.categories[category_id].points[i];
1018 			this.updateMarkerVisibility( this.locations[point].marker, point );
1019 		}
1020 		this.recluster();
1021 		this.updateVisibleList();
1022 	},
1023 
1024 	/**
1025 	 * Add objects to the map.
1026 	 * @param {Object} response_data Data returned by a geo query.
1027 	 * @param {Boolean} add_category_info Whether to build and show category
1028 	 *   data for these objects, for legend or other category controls.
1029 	 */
1030 	addObjects : function(response_data, add_category_info) {
1031 		var i, j, object_id, point, category_id, marker, plus_image,
1032 			added_markers = [];
1033 
1034 		if (add_category_info) {
1035 			this.forEach( this.categories, function (category_id, category) {
1036 				category.points.length = 0;
1037 				if (category.line) {
1038 					this.hideLine( category.line );
1039 				}
1040 			});
1041 		}
1042 		for (i = 0; i < response_data.length; i+=1) {
1043 			// Make a marker for each new object location
1044 			object_id = response_data[i].object_id;
1045 			point = this.newLatLng(
1046 				parseFloat(response_data[i].lat),
1047 				parseFloat(response_data[i].lng)
1048 			);
1049 			// Update categories
1050 			for (j = 0; j < response_data[i].categories.length; j+=1) {
1051 				category_id = response_data[i].categories[j];
1052 				this.extendCategory(point, category_id, object_id);
1053 			}
1054 			if (this.opts.max_posts && this.object_count >= this.opts.max_posts) {
1055 				break;
1056 			}
1057 			if (!this.objects[object_id]) {
1058 				// This object has not yet been loaded
1059 				this.objects[object_id] = response_data[i];
1060 				this.object_count += 1;
1061 				if (!this.locations[point]) {
1062 					// There are no other objects yet at this point, create a marker
1063 					this.extendLocationBounds( point );
1064 					this.locations[point] = {objects : [ response_data[i] ]};
1065 					this.locations[point].loaded = false;
1066 					marker = this.createMarker(point, response_data[i]);
1067 					this.objects[object_id].marker = marker;
1068 					this.locations[point].marker = marker;
1069 					added_markers.push( marker );
1070 				} else {
1071 					// There is already a marker at this point, add the new object to it
1072 					this.locations[point].objects.push( response_data[i] );
1073 					marker = this.locations[point].marker;
1074 					this.makeMarkerMultiple( marker );
1075 					this.objects[object_id].marker = marker;
1076 					this.addObjectIcon( this.objects[object_id] );
1077 				}
1078 			}
1079 		} // end for each marker
1080 
1081 		// Add category lines
1082 		if (add_category_info) {
1083 			this.showCategoryInfo();
1084 		}
1085 
1086 		// Openlayers at least only gets clicks on the top layer, so add markers last
1087 		this.addMarkers( added_markers );
1088 
1089 		// Tabbed index may hide markers
1090 		this.initializeTabbedIndex();
1091 
1092 		if (this.firstLoad) {
1093 			this.firstLoad = false;
1094 			if ( this.opts.auto_info_open && this.object_count > 0 ) {
1095 				if ( !this.opts.open_object_id ) {
1096 					if ( this.opts.context_object_id && this.objects[ this.opts.context_object_id ] ) {
1097 						this.opts.open_object_id = this.opts.context_object_id;
1098 					} else {
1099 						this.opts.open_object_id = response_data[0].object_id;
1100 					}
1101 				}
1102 				this.clickObjectMarker(this.opts.open_object_id);
1103 			}
1104 			if ( this.opts.zoom === 'auto' ) {
1105 				this.autoZoom();
1106 			} else {
1107 				if ( this.opts.context_object_id && this.objects[ this.opts.context_object_id ] ) {
1108 					this.centerMarker( this.objects[ this.opts.context_object_id ].marker, parseInt( this.opts.zoom, 10 ) );
1109 				}
1110 				this.updateVisibleList();
1111 			}
1112 		}
1113 	},
1114 
1115 	requestObjects : function(use_bounds) {
1116 		// provider override (maybe jQuery?)
1117 	},
1118 
1119 	/**
1120 	 * Hide all markers.
1121 	 */
1122 	hideMarkers : function() {
1123 		var point;
1124 
1125 		for (point in this.locations) {
1126 			if ( this.locations[point].marker ) {
1127 				this.hideMarker( this.locations[point].marker );
1128 			}
1129 		}
1130 		this.recluster();
1131 		this.updateVisibleList();
1132 	},
1133 
1134 	/**
1135  	 * Show all markers.
1136 	 */
1137 	showMarkers : function() {
1138 		var i, category_id, point;
1139 
1140 		for (category_id in this.categories) {
1141 			if (this.categories[category_id].visible) {
1142 				for (i=0; i<this.categories[category_id].points.length; i++) {
1143 					point = this.categories[category_id].points[i];
1144 					this.showMarker( this.locations[point].marker );
1145 				}
1146 			}
1147 		}
1148 	},
1149 
1150 	adjustZoom : function() {
1151 		var category_id, old_level, new_level;
1152 		new_level = this.map.getZoom();
1153 		if ( typeof this.last_zoom_level === 'undefined' ) {
1154 			this.last_zoom_level = new_level;
1155 		}
1156 		old_level = this.last_zoom_level;
1157 
1158 		for (category_id in this.categories) {
1159 			if (old_level <= this.categories[category_id].max_line_zoom &&
1160 			  new_level > this.categories[category_id].max_line_zoom) {
1161 				this.hideLine( this.categories[category_id].line );
1162 			} else if (this.categories[category_id].visible &&
1163 				old_level > this.categories[category_id].max_line_zoom &&
1164 			  new_level <= this.categories[category_id].max_line_zoom) {
1165 				this.showLine( this.categories[category_id].line );
1166 			}
1167 		}
1168 
1169 		if ( this.clusterer && 'markercluster' == this.opts.cluster_lib ) {
1170 			if ( old_level <= this.opts.cluster_max_zoom && 
1171 					new_level > this.opts.cluster_max_zoom ) {
1172 				this.clusterer.clusteringEnabled = false;
1173 				this.clusterer.refresh( true );
1174 			} else if ( old_level > this.opts.cluster_max_zoom &&
1175 					new_level <= this.opts.cluster_max_zoom ) {
1176 				this.clusterer.clusteringEnabled = true;
1177 				this.clusterer.refresh( true );
1178 			}
1179 		}
1180 		this.last_zoom_level = new_level;
1181 	},
1182 
1183 	objectLinkHtml : function(object_id) {
1184 		return ['<a href="#',
1185 			this.opts.name,
1186 			'" onclick="frames[\'',
1187 			this.opts.name,
1188 			'\'].GeoMashup.clickObjectMarker(',
1189 			object_id,
1190 			');">',
1191 			this.objects[object_id].title,
1192 			'</a>'].join('');
1193 	},
1194 
1195 	/**
1196 	 * Whether a marker is currently visible on the map.
1197 	 * @param {Marker} marker
1198 	 * @param {Boolean} False if the marker is hidden or outside the current viewport.
1199 	 */
1200 	isMarkerVisible : function( marker ) {
1201 		// Provider override
1202 		return false;
1203 	},
1204 
1205 	/**
1206 	 * Recompile the list of objects currently visible on the map.
1207 	 */
1208 	updateVisibleList : function() {
1209 		var list_element, header_element, list_html, list_count = 0;
1210 
1211 		if (this.have_parent_access && this.opts.name) {
1212 			header_element = parent.document.getElementById(this.opts.name + "-visible-list-header");
1213 			list_element = parent.document.getElementById(this.opts.name + "-visible-list");
1214 		}
1215 		if (header_element) {
1216 			header_element.style.display = 'block';
1217 		}
1218 		if (list_element) {
1219 			list_html = ['<ul class="gm-visible-list">'];
1220 			this.forEach( this.objects, function (object_id, obj) {
1221 				if ( this.isMarkerVisible( obj.marker ) ) {
1222 					list_html.push('<li><img src="');
1223 					list_html.push(obj.icon.image);
1224 					list_html.push('" alt="');
1225 					list_html.push(obj.title);
1226 					list_html.push('" />');
1227 					list_html.push(this.objectLinkHtml(object_id));
1228 					list_html.push('</li>');
1229 					list_count++;
1230 				}
1231 			});
1232 			list_html.push('</ul>');
1233 			list_element.innerHTML = list_html.join('');
1234 			/** 
1235 			 * The visible posts list was updated.
1236 			 * @name GeoMashup#updatedVisibleList
1237 			 * @event
1238 			 * @param {GeoMashupOptions} properties Geo Mashup configuration data
1239 			 * @param {Number} list_count The number of items in the list
1240 			 */
1241 			this.doAction( 'updatedVisibleList', this.opts, list_count );
1242 		}
1243 	},
1244 
1245 	adjustViewport : function() {
1246 		this.updateVisibleList();
1247 	},
1248 
1249 	createMap : function(container, opts) {
1250 		// Provider override
1251 	}
1252 };
1253