1 /**
  2  * @fileOverview 
  3  * Additions to GeoMashup for handling taxonomy terms.
  4  */
  5 
  6 /*global GeoMashup, jQuery */
  7 /*global customGeoMashupCategoryIcon, customGeoMashupColorIcon */
  8 
  9 /*jslint browser: true, white: true, sloppy: true */
 10 
 11 jQuery.extend( GeoMashup, {
 12 
 13 	/**
 14 	 * An object to manage taxonomy terms.
 15 	 * @since 1.5
 16 	 * @memberOf GeoMashup
 17 	 */
 18 	term_manager : (function() {
 19 		var $ = jQuery, 
 20 
 21 			// Public interface
 22 			term_manager = {},
 23 
 24 			// Loaded terms as { "taxonomy1": { term_count: 0, terms: { 
 25 			// "3" : {
 26 			//    icon : icon,
 27 			//    points : [point],
 28 			//    posts : [object_id],
 29 			//    color : color_rgb,
 30 			//    visible : true,
 31 			//    max_line_zoom : max_line_zoom
 32 			// } } }
 33 			loaded_terms = {},
 34 		
 35 			// Structure and settings for terms in included taxonomies
 36 			// e.g. { "taxonomy1": { "label": "Taxonomy One", "terms": { 
 37 			//   "3": { name: "Term 3 Name", parent_id: "1", color: "red", line_zoom: "7" } 
 38 			// } } }
 39 			term_properties,
 40 			
 41 			// Each taxonomy gets a tree of term ids with null leaves
 42 			// { "taxonomy1": { "1": { "3": null }, "2": null } }
 43 			hierarchies = {}, 
 44 			
 45 			// { "taxonomy1": Element, "taxonomy2": Element }
 46 			legend_elements = [];
 47 
 48 		/**
 49 		 * Search for a legend element for a taxonomy.
 50 		 * 
 51 		 * Without a context, the current document is searched first, 
 52 		 * then the parent if available.
 53 		 *
 54 		 * @private
 55 		 *
 56 		 * @param {String} taxonomy 
 57 		 * @param {String} widget_type 'legend' or 'tabbed-index'
 58 		 * @param {Document} context
 59 		 * @return {Element} The legend element or null if not found.
 60 		 */
 61 		function getWidgetElement( taxonomy, widget_type, context ) {
 62 			var element = null;
 63 
 64 			if ( typeof context === 'undefined' ) {
 65 
 66 				element = getWidgetElement( taxonomy, widget_type, document );
 67 
 68 				if ( !element && GeoMashup.have_parent_access ) {
 69 					element = getWidgetElement( taxonomy, widget_type, parent.document );
 70 				}
 71 
 72 			} else {
 73 
 74 				if ( GeoMashup.opts.name ) {
 75 
 76 					element = context.getElementById( GeoMashup.opts.name + '-' + taxonomy + '-' + widget_type );
 77 
 78 					if ( !element ) {
 79 						element = context.getElementById( GeoMashup.opts.name + '-' + widget_type );
 80 					}
 81 
 82 				}
 83 
 84 				if ( !element ) {
 85 					element = context.getElementById( 'gm-term-' + widget_type );
 86 				}
 87 
 88 				// Back compat names
 89 				if ( !element && 'category' === taxonomy ) {
 90 					if ( 'legend' === widget_type ) {
 91 						element = context.getElementById( 'gm-cat-legend' );
 92 					} else {
 93 						element = context.getElementById( 'gm-' + widget_type );
 94 					}
 95 				}
 96 
 97 			}
 98 
 99 			return element;
100 		}
101 
102 		function buildTermHierarchy(taxonomy, term_id) {
103 			var children, child_count, top_id, child_id, properties = term_properties[taxonomy];
104 
105 			if (term_id) { // This is a recursive call
106 
107 				// Find children of this term and return them
108 				term_id = String( term_id );
109 				children = {};
110 				child_count = 0;
111 
112 				$.each( properties.terms, function( child_id, term_data ) {
113 					if ( term_data.parent_id && term_data.parent_id === term_id ) {
114 						children[child_id] = buildTermHierarchy( taxonomy, child_id );
115 						child_count += 1;
116 					}
117 				});
118 
119 				return (child_count > 0) ? children : null;
120 
121 			} else { // Top-level call
122 
123 				// Build a tree for each taxonomy's top level (no parent) terms
124 				hierarchies[taxonomy] = {};
125 
126 				$.each( properties.terms, function( top_id, term_data ) {
127 					if ( !term_data.parent_id) {
128 						hierarchies[taxonomy][top_id] = buildTermHierarchy( taxonomy, top_id );
129 					}
130 				} );
131 
132 			}
133 		}
134 
135 		function createTermLines() {
136 			
137 			$.each( loaded_terms, function( taxonomy, tax_data ) {
138 				$.each( tax_data.terms, function ( term_id, term_data ) {
139 						
140 					if ( term_data.max_line_zoom >= 0 ) {
141 						GeoMashup.createTermLine( term_data );
142 					}
143 
144 				} );
145 			} );
146 		}
147 
148 		function sortTermLegendData( taxonomy, tax_data ) {
149 			var ordered_terms = [];
150 
151 			$.each( tax_data.terms, function ( term_id, term_data ) {
152 				var order, sort_term = term_data;
153 
154 				sort_term.term_id = term_id;
155 				sort_term.name = term_properties[taxonomy].terms[term_id].name;
156 
157 				// Check for an explicit order field, otherwise use name
158 				if ( term_properties[taxonomy].terms[term_id].hasOwnProperty( 'order' ) ) {
159 					sort_term.order = term_properties[taxonomy].terms[term_id].order;
160 				} else {
161 					sort_term.order = sort_term.name.toLowerCase();
162 				}
163 
164 				ordered_terms.push( sort_term );
165 			} );
166 
167 			ordered_terms.sort( function( a, b ) {
168 				return ((a.order < b.order) ? -1 : ((a.order > b.order) ? 1 : 0));
169 			} );
170 			
171 			return ordered_terms;
172 		}
173 
174 		function createTermLegends() {
175 			
176 			$.each( loaded_terms, function( taxonomy, tax_data ) {
177 				var $legend, list_tag, row_tag, term_tag, definition_tag, 
178 					$element, $title, format, format_match, interactive,
179 					add_check_all, $check_all, default_off,
180 					element = getWidgetElement( taxonomy, 'legend' );
181 
182 				if ( !element ) {
183 					return;
184 				}
185 				$element = $( element );
186 
187 				if ( $element.hasClass( 'noninteractive' ) ) {
188 					interactive = false;
189 				} else if ( typeof GeoMashup.opts.interactive_legend === 'undefined' ) {
190 					interactive = true;
191 				} else {
192 					interactive = GeoMashup.opts.interactive_legend;
193 				}
194 
195 				if ( $element.hasClass( 'check-all-off' ) ) {
196 					default_off = false;
197 				} else if ( interactive ) {
198 					add_check_all = true;
199 				}
200 
201 				if ( $element.hasClass( 'default-off' ) ) {
202 					default_off = true;
203 				} else {
204 					default_off = false;
205 				}
206 
207 				format_match = /format-(\w+)/.exec( $element.attr( 'class' ) );
208 				if ( format_match ) {
209 					format = format_match[1];
210 				} else if ( GeoMashup.opts.legend_format ) {
211 					format = GeoMashup.opts.legend_format;
212 				} else {
213 					format = 'table';
214 				}
215 				if ( format === 'dl' ) { 
216 					list_tag = 'dl'; 
217 					row_tag = ''; 
218 					term_tag = 'dt'; 
219 					definition_tag = 'dd'; 
220 				} else if ( format === 'ul' ) { 
221 					list_tag = 'ul'; 
222 					row_tag = 'li'; 
223 					term_tag = 'span'; 
224 					definition_tag = 'span'; 
225 				} else { 
226 					list_tag = 'table'; 
227 					row_tag = 'tr'; 
228 					term_tag = 'td'; 
229 					definition_tag = 'td'; 
230 				} 
231 				
232 				if ( $element.hasClass( 'titles-on' ) || ( !$element.hasClass( 'titles-off' ) && GeoMashup.opts.include_taxonomies.length > 1 ) ) {
233 
234 					$title = $( '<h2></h2>' )
235 						.addClass( 'gm-legend-title' )
236 						.addClass( taxonomy + '-legend-title' )
237 						.text( term_properties[taxonomy].label );
238 					/**
239 					 * A taxonomy legend title is being created
240 					 * @name GeoMashup#taxonomyLegendTitle
241 					 * @event
242 					 * @since 1.5
243 					 * @param {jQuery} $title Empty legend element with classes
244 					 * @param {String} taxonomy 
245 					 */
246 					GeoMashup.doAction( 'taxonomyLegendTitle', $title, taxonomy );
247 
248 					$element.append( $title );
249 
250 				}
251 				$legend = $( '<' + list_tag + ' class="gm-legend ' + taxonomy + '" />' );
252 
253 				/**
254 				 * A taxonomy legend is being created
255 				 * @name GeoMashup#taxonomyLegend
256 				 * @event
257 				 * @since 1.5
258 				 * @param {jQuery} $legend Empty legend element with classes
259 				 * @param {String} taxonomy 
260 				 */
261 				GeoMashup.doAction( 'taxonomyLegend', $legend, taxonomy );
262 
263 				if ( GeoMashup.opts.name && add_check_all ) {
264 
265 					// Add check/uncheck all
266 					$check_all = $( '<label></label>' )
267 						.text( GeoMashup.opts.check_all_label )
268 						.attr( 'for', 'gm-' + taxonomy + '-check-all' )
269 						.prepend(
270 							$( '<input type="checkbox" />' ).attr( 'id', 'gm-' + taxonomy + '-check-all' )
271 								.attr( 'checked', ( default_off ? false : 'checked' ) )
272 								.change( function() {
273 									if ( $( this ).is( ':checked' ) ) {
274 										$legend.find( 'input.gm-' + taxonomy + '-checkbox:not(:checked)' ).click();
275 									} else {
276 										$legend.find( 'input.gm-' + taxonomy + '-checkbox:checked' ).click();
277 									}
278 								} )
279 						);	
280 					if ( row_tag ) {
281 						$legend.append( 
282 							$( '<' + row_tag + '/>' ).append( $( '<' + term_tag + '/>' ) )
283 								.append( $( '<' + definition_tag + '/>' ).append( $check_all ) )
284 						);
285 					} else {
286 						$legend.append( $( '<' + term_tag + '/>' ) ).append( $( '<' + definition_tag + '/>' )
287 							.append( $check_all ) );
288 					}
289 				}
290 
291 				$.each( sortTermLegendData( taxonomy, tax_data ), function ( i, sort_term ) {
292 					var term_id, id, name, $entry, $key, $def, $label, $checkbox;
293 
294 					term_id = sort_term.term_id;
295 					name = sort_term.name;
296 
297 					if ( GeoMashup.opts.name && interactive ) {
298 
299 						id = 'gm-' + taxonomy + '-checkbox-' + term_id;
300 
301 						$checkbox = $( '<input type="checkbox" name="term_checkbox" />' )
302 							.attr( 'id', id )
303 							.addClass( 'gm-' + taxonomy + '-checkbox' )
304 							.change( function() {
305 								GeoMashup.term_manager.setTermVisibility( term_id, taxonomy, $( this ).is( ':checked' ) ); 
306 							});
307 
308 						if ( default_off ) {
309 							GeoMashup.term_manager.setTermVisibility( term_id, taxonomy, false ); 
310 						} else {
311 							$checkbox.attr( 'checked', 'checked' );
312 						}
313 
314 						$label = $( '<label/>' )
315 							.attr( 'for', id )
316 							.text( name )
317 							.prepend( $checkbox );
318 
319 					} else {
320 
321 						$label = $( '<span/>' ).text( name ); 
322 
323 					}
324 
325 					$key = $( '<' + term_tag + ' class="symbol"/>').append(
326 						$( '<img/>' )
327 							.attr( 'src', sort_term.icon.image )
328 							.attr( 'alt', term_id )
329 							.click( function() {
330 								// Pass clicks to the checkbox
331 								$label.click();
332 								return false;
333 							} )
334 					);
335 
336 					$def = $( '<' + definition_tag + ' class="term"/>' ).append( $label );
337 
338 					/**
339 					 * A taxonomy legend entry is being created
340 					 * @name GeoMashup#termLegendEntry
341 					 * @event
342 					 * @since 1.5
343 					 * @param {jQuery} $key Legend key node (td or dt)
344 					 * @param {jQuery} $def Legend definition node (td or dd)
345 					 * @param {String} taxonomy 
346 					 * @param {String} term_id 
347 					 */
348 					GeoMashup.doAction( 'taxonomyLegendEntry', $key, $def, taxonomy, term_id );
349 
350 					if (row_tag) {
351 
352 						$entry = $( '<' + row_tag + '/>' )
353 							.addClass( 'term-' + term_id )
354 							.append( $key )
355 							.append( $def );
356 						/**
357 						 * A taxonomy legend table row is being created
358 						 * @name GeoMashup#termLegendRow
359 						 * @event
360 						 * @since 1.5
361 						 * @param {jQuery} $entry Table row
362 						 * @param {String} taxonomy 
363 						 * @param {String} term_id 
364 						 */
365 						GeoMashup.doAction( 'taxonomyLegendRow', $entry, taxonomy, term_id );
366 
367 						$legend.append( $entry );
368 
369 					} else {
370 
371 						$legend.append( $key ).append( $def );
372 
373 					}
374 
375 				}); // end each term
376 
377 				$( element ).append( $legend );
378 
379 			} );
380 		}
381 
382 		term_manager.load = function() {
383 
384 			term_properties = GeoMashup.opts.term_properties;
385 
386 			if ( !term_properties ) {
387 				return;
388 			}
389 
390 			$.each( term_properties, function( taxonomy ) {
391 				buildTermHierarchy( taxonomy );
392 			} );
393 
394 		};
395 
396 		term_manager.extendTerm = function(point, taxonomy, term_id, object) {
397 			var loaded_taxonomy,
398 				icon, 
399 				color_rgb, 
400 				color_name, 
401 				max_line_zoom;
402 
403 			term_id = String( term_id );
404 
405 			if ( !loaded_terms.hasOwnProperty( taxonomy ) ) {
406 				loaded_terms[taxonomy] = {terms: {}, term_count: 0};
407 			}
408 
409 			loaded_taxonomy = loaded_terms[taxonomy];
410 
411 			if ( !loaded_taxonomy.terms.hasOwnProperty( term_id ) ) {
412 
413 				if ( term_properties[taxonomy].terms[term_id].color ) {
414 					color_name = term_properties[taxonomy].terms[term_id].color;
415 				} else {
416 					color_name = GeoMashup.color_names[ loaded_taxonomy.term_count % GeoMashup.color_names.length ];
417 				}
418 				color_rgb = GeoMashup.colors[color_name];
419 
420 				// Back compat callbacks
421 				if ( typeof customGeoMashupCategoryIcon === 'function' ) {
422 					icon = customGeoMashupCategoryIcon( GeoMashup.opts, [term_id] );
423 				}
424 				if ( !icon && typeof customGeoMashupColorIcon === 'function' ) {
425 					icon = customGeoMashupColorIcon( GeoMashup.opts, color_name );
426 				}
427 				if (!icon) {
428 					icon = GeoMashup.colorIcon( color_name );
429 				}
430 
431 				if ( 'category' === taxonomy ) {
432 					/**
433 					 * A category icon is being assigned.
434 					 * @name GeoMashup#categoryIcon
435 					 * @deprecated Use GeoMashup#termIcon
436 					 * @event
437 					 * @since 1.5
438 					 * @param {GeoMashupOptions} properties Geo Mashup configuration data
439 					 * @param {GeoMashupIcon} icon
440 					 * @param {String} term_id
441 					 */
442 					GeoMashup.doAction( 'categoryIcon', GeoMashup.opts, icon, term_id );
443 				}
444 
445 				/**
446 				 * A term icon is being assigned.
447 				 * @name GeoMashup#termIcon
448 				 * @event
449 				 * @since 1.5
450 				 * @param {GeoMashupIcon} icon
451 				 * @param {String} taxonomy
452 				 * @param {String} term_id
453 				 */
454 				GeoMashup.doAction( 'termIcon', icon, taxonomy, term_id );
455 
456 				/**
457 				 * A category icon is being assigned by color.
458 				 * @name GeoMashup#colorIcon
459 				 * @event
460 				 * @since 1.5
461 				 * @param {GeoMashupOptions} properties Geo Mashup configuration data
462 				 * @param {GeoMashupIcon} icon
463 				 * @param {String} color_name
464 				 */
465 				GeoMashup.doAction( 'colorIcon', GeoMashup.opts, icon, color_name );
466 
467 				max_line_zoom = -1;
468 				if ( term_properties[taxonomy].terms[term_id].line_zoom ) {
469 					max_line_zoom = term_properties[taxonomy].terms[term_id].line_zoom;
470 				}
471 
472 				loaded_taxonomy.terms[term_id] = {
473 					icon : icon,
474 					points : [point],
475 					objects : [object],
476 					color : color_rgb,
477 					visible : true,
478 					max_line_zoom : max_line_zoom
479 				};
480 
481 				loaded_taxonomy.term_count += 1;
482 
483 			} else { // taxonomy term exists
484 
485 				loaded_taxonomy.terms[term_id].points.push( point );
486 				loaded_taxonomy.terms[term_id].objects.push( object );
487 
488 			}
489 		};
490 
491 		/**
492 		 * Enable more objects to be loaded.
493 		 * Consider it alpha - probably needs work.
494 		 * @methodOf GeoMashup
495 		 * @since 1.5
496 		 */
497 		term_manager.reset = function() {
498 
499 			$.each( loaded_terms, function( taxonomy, tax_data ) {
500 				$.each( tax_data.terms, function( term_id, term_data ) {
501 					term_data.points.length = 0;
502 					if ( term_data.line ) {
503 						GeoMashup.hideLine( term_data.line );
504 					}
505 				} );
506 			} );
507 
508 		};
509 
510 		/**
511 		 * Get a property of a loaded term.
512 		 * @methodOf GeoMashup
513 		 * @since 1.5
514 		 * @param taxonomy
515 		 * @param term_id
516 		 * @param property Property name
517 		 * @returns {*} Property value
518 		 */
519 		term_manager.getTermData = function( taxonomy, term_id, property ) {
520 			return loaded_terms[taxonomy].terms[term_id][property];
521 		};
522 
523 		/**
524 		 * Find a term in a hierarchy.
525 		 *
526 		 * @methodOf GeoMashup
527 		 * @since 1.5
528 		 * @param search_id
529 		 * @param {object|string} hierarchy Optional hierarchy structure or taxonomy name, defaults to 'category'.
530 		 * @returns {object} Hierarchy tree rooted at the search term or null if not found.
531 		 */
532 		term_manager.searchTermHierarchy = function( search_id, hierarchy ) {
533 			var child_search, term_id;
534 
535 			if ( !hierarchy ) {
536 				hierarchy = hierarchies.category;
537 			} else if ( typeof hierarchy === 'string' ) {
538 				hierarchy = hierarchies[hierarchy];
539 			}
540 			// Use a regular loop, so it can return a value for this function
541 			for( term_id in hierarchy ) {
542 				if ( hierarchy.hasOwnProperty( term_id ) && typeof hierarchy[term_id] !== 'function' ) {
543 					if ( term_id === search_id ) {
544 						return hierarchy[term_id];
545 					} else if ( hierarchy[term_id] ) {
546 						child_search = term_manager.searchTermHierarchy( search_id, hierarchy[term_id] );
547 						if (child_search) {
548 							return child_search;
549 						}
550 					}
551 				}
552 			}
553 			return null;
554 		};
555 
556 		term_manager.populateTermElements = function() {
557 
558 			createTermLines();
559 			createTermLegends();
560 			// The tabbed index may hide markers, so it's created last
561 			term_manager.tabbed_index.create();
562 
563 		};
564 
565 		/**
566 		 * The tabbed index control object.
567 		 * @memberOf GeoMashup
568 		 * @since 1.5
569 		 */
570 		term_manager.tabbed_index = (function() {
571 			var tabbed_index = {},
572 				tab_term_ids = [],
573 				tab_hierarchy,
574 				tab_index_group_size,
575 				show_inactive_tab_markers, 
576 				$index;
577 
578 			function hasLocatedChildren( term_id, taxonomy, hierarchy ) {
579 				var child_id;
580 
581 				if ( loaded_terms[taxonomy].terms[term_id] ) {
582 					return true;
583 				}
584 
585 				for ( child_id in hierarchy ) {
586 					if ( hierarchy.hasOwnProperty( child_id ) && hasLocatedChildren( child_id, taxonomy, hierarchy[child_id] ) ) {
587 						return true;
588 					}
589 				}
590 				return false;
591 			}
592 
593 			function buildTermIndex( term_id, taxonomy, children, top_level) {
594 				var $term_index, $list, $sub_list, group_count, 
595 					// Back compat tax name
596 					tax = taxonomy.replace( 'category', 'cat' );  
597 
598 				if ( typeof top_level === 'undefined' ) {
599 					top_level = true;
600 				}
601 
602 				$term_index = $( '<div></div>' )
603 					.attr( 'id', tabbed_index.getTermIndexId( term_id, taxonomy ) )
604 					.addClass( 'gm-tabs-panel' )
605 					.addClass( 'gm-tabs-panel-' + term_id );
606 				if ( top_level ) {
607 					$term_index.addClass( 'gm-hidden' );
608 				}
609 
610 				$list = $( '<ul></ul>' ).addClass( 'gm-index-posts' );
611 
612 				if ( loaded_terms[taxonomy].terms[term_id] ) {
613 
614 					loaded_terms[taxonomy].terms[term_id].objects.sort( function( a, b ) {
615 						var a_name = a.title,
616 							b_name = b.title;
617 
618 						if (a_name === b_name) {
619 							return 0;
620 						} else {
621 							return a_name < b_name ? -1 : 1;
622 						}
623 					});
624 
625 					$.each( loaded_terms[taxonomy].terms[term_id].objects, function( i, object ) {
626 						$list.append( 
627 							$( '<li></li>' ).append( 
628 								$( '<a></a>' )
629 									.attr( 'href', '#' + GeoMashup.opts.name )
630 									.text( object.title )
631 									.click( function() {
632 										GeoMashup.clickObjectMarker( object.object_id );
633 									})
634 							)
635 						);
636 					});
637 				}
638 				
639 				if ( children ) {
640 
641 					group_count = 0;
642 					$sub_list = $( '<ul></ul>' ).addClass( 'gm-sub-' + tax + '-index');
643 
644 					$.each( children, function( child_id, grandchildren ) {
645 						var $li = $( '<li></li>' ),
646 							loaded_term = loaded_terms[taxonomy].terms[child_id];
647 
648 						if ( loaded_term ) {
649 							$li.append( $( '<img />' ).attr( 'src', loaded_term.icon.image ) )
650 								.append( $( '<span></span>' ).addClass( 'gm-sub-' + tax + '-title' ).text( loaded_term.name ) );
651 						}
652 
653 						$li.append( buildTermIndex( child_id, taxonomy, grandchildren, false ) );
654 						
655 						group_count += 1;
656 						if ( tab_index_group_size && group_count%tab_index_group_size === 0) {
657 							$list.append( $sub_list );
658 							$sub_list = $( '<ul></ul>' ).addClass( 'gm-sub-' + tax + '-index');
659 						}
660 					});
661 					$list.append( $sub_list );
662 
663 				}
664 
665 				$term_index.append( $list );
666 
667 				return $term_index;
668 			}
669 
670 			function buildTabbedIndex( hierarchy, taxonomy ) {
671 				var $list;
672 
673 				$index = $( '<div></div>' ).attr( 'id', GeoMashup.opts.name + '-tab-index' );
674 				$list = $( '<ul class="gm-tabs-nav"></ul>' );
675 
676 				$.each( hierarchy, function( term_id, children ) {
677 					if ( hasLocatedChildren( term_id, taxonomy, children ) ) {
678 						tab_term_ids.push( term_id );
679 					}
680 				} );
681 
682 				tab_term_ids.sort( function( a, b ) {
683 					var a_name = term_properties[taxonomy].terms[a].name,
684 						b_name = term_properties[taxonomy].terms[b].name;
685 
686 					if ( a_name === b_name ) {
687 						return 0;
688 					} else {
689 						return a_name < b_name ? -1 : 1;
690 					}
691 				} );
692 				
693 				$.each( tab_term_ids, function( i, term_id ) {
694 					var children = hierarchy[term_id],
695 						$li = $( '<li></li>' ).addClass( 'gm-tab-inactive' ).addClass( 'gm-tab-inactive-' + term_id ),
696 						$a = $( '<a></a>' ).attr( 'href', '#' + GeoMashup.opts.name ).click( function() {
697 							tabbed_index.selectTab( term_id, taxonomy );
698 							return false;
699 						});
700 
701 					if ( loaded_terms[taxonomy] && loaded_terms[taxonomy].terms[term_id] ) {
702 						$a.append( $( '<img />' ).attr( 'src', loaded_terms[taxonomy].terms[term_id].icon.image ) );
703 					}
704 					$a.append( $( '<span></span>' ).text( term_properties[taxonomy].terms[term_id].name ) );
705 					$li.append( $a );
706 					$list.append( $li );
707 
708 					if ( !show_inactive_tab_markers ) {
709 						term_manager.setHierarchyVisibility( term_id, children, taxonomy, false );
710 					}
711 				});
712 
713 				$index.append( $list ); 
714 
715 				$.each( hierarchy, function( term_id, children ) {
716 					$index.append( buildTermIndex( term_id, taxonomy, children ) );
717 				});
718 
719 				return $index;
720 			}
721 
722 			/**
723 			 * Get the DOM id of term index element
724 			 * @since 1.5
725 			 * @methodOf GeoMashup.term_manager
726 			 * @param term_id
727 			 * @param taxonomy
728 			 * @returns {string}
729 			 */
730 			tabbed_index.getTermIndexId = function( term_id, taxonomy ) {
731 				var tax = taxonomy.replace( 'category', 'cat' );
732 				return 'gm-' + tax + '-index-' + term_id;
733 			};
734 
735 			tabbed_index.create = function() {
736 				var start_tab_term_id, start_tab_term_id_match, group_size_match, taxonomy,
737 					disable_tab_auto_select = false,
738 					$element = [];
739 
740 				// Determine a taxonomy to use
741 				$.each( loaded_terms, function( check_taxonomy ) {
742 					var element = getWidgetElement( check_taxonomy, 'tabbed-index' );
743 					if ( element ) {
744 						taxonomy = check_taxonomy;
745 						$element = $( element );
746 						return true; // break
747 					}
748 				} );
749 				if ( $element.length === 0 ) {
750 					return;
751 				}
752 				tab_hierarchy = hierarchies[taxonomy];
753 				
754 				start_tab_term_id_match = /start-tab-term-(\d+)/.exec( $element.attr( 'class' ) );
755 				if ( start_tab_term_id_match ) {
756 					start_tab_term_id = start_tab_term_id_match[1];
757 				} else if ( 'category' === taxonomy ) {
758 					start_tab_term_id = GeoMashup.opts.start_tab_category_id;
759 				}
760 
761 				group_size_match = /tab-index-group-size-(\d+)/.exec( $element.attr( 'class' ) );
762 				if ( group_size_match ) {
763 					tab_index_group_size = group_size_match[1];
764 				} else {
765 					tab_index_group_size = GeoMashup.opts.tab_index_group_size;
766 				}
767 
768 				if ( $element.hasClass( 'show-inactive-tab-markers' ) ) {
769 					show_inactive_tab_markers = true;
770 				} else {
771 					show_inactive_tab_markers = GeoMashup.opts.show_inactive_tab_markers;
772 				}
773 
774 				if ( $element.hasClass( 'disable-tab-auto-select' ) ) {
775 					disable_tab_auto_select = true;
776 				} else if ( 'category' === taxonomy ) {
777 					disable_tab_auto_select = GeoMashup.opts.disable_tab_auto_select;
778 				}
779 				
780 				if ( start_tab_term_id ) {
781 					tab_hierarchy = term_manager.searchTermHierarchy( start_tab_term_id, tab_hierarchy );
782 				}
783 
784 				$element.append( buildTabbedIndex( tab_hierarchy, taxonomy ) );
785 
786 				if ( !disable_tab_auto_select ) {
787 					// Select the first tab
788 					$.each( tab_term_ids, function( i, term_id ) {
789 						if ( hasLocatedChildren( term_id, taxonomy, tab_hierarchy ) ) {
790 							tabbed_index.selectTab( term_id, taxonomy );
791 							return false;
792 						}
793 					});
794 				}
795 			};
796 
797 			/**
798 			 * Make a term tab active.
799 			 * @methodOf GeoMashup.term_manager
800 			 * @since 1.5
801 			 * @param term_id
802 			 * @param taxonomy
803 			 * @returns {boolean} success
804 			 */
805 			tabbed_index.selectTab = function( term_id, taxonomy ) {
806 				var $active_tab, hide_term_classes, hide_term_match, hide_term_id;
807 
808 				if ( !$index ) {
809 					return false;
810 				}
811 
812 				if ( $index.find( '.gm-tab-active-' + term_id ).length > 0 ) {
813 					// Requested tab is already selected
814 					return true;
815 				}
816 				
817 				$active_tab = $index.find( '.gm-tabs-nav .gm-tab-active' );
818 				if ( $active_tab.length > 0 ) {
819 					hide_term_match = /gm-tab-active-(\d+)/.exec( $active_tab.attr( 'class' ) );
820 					if ( hide_term_match ) {
821 						hide_term_id = hide_term_match[1];
822 						$active_tab.attr( 'class', 'gm-tab-inactive gm-tab-inactive-' + hide_term_id );
823 					}
824 				}
825 				$index.find( '.gm-tabs-nav .gm-tab-inactive-' + term_id )
826 					.attr( 'class', 'gm-tab-active gm-tab-active-' + term_id );
827 
828 				// Hide previous active panel
829 				$index.find( '.gm-tabs-panel.gm-active' )
830 					.removeClass( 'gm-active' )
831 					.addClass( 'gm-hidden' );
832 
833 				// Show selected panel
834 				$index.find( '.gm-tabs-panel-' + term_id ).removeClass( 'gm-hidden' ).addClass( 'gm-active' );
835 
836 				if ( !show_inactive_tab_markers ) {
837 					// Hide previous active markers
838 					if ( hide_term_id ) {
839 						term_manager.setHierarchyVisibility( hide_term_id, tab_hierarchy[hide_term_id], taxonomy, false );
840 					}
841 					// Show selected markers second so none get re-hidden
842 					term_manager.setHierarchyVisibility( term_id, tab_hierarchy[term_id], taxonomy, true );
843 				}
844 
845 			};
846 
847 			return tabbed_index;
848 		}());
849 
850 		/**
851 		 * Show or hide a term.
852 		 * @methodOf GeoMashup
853 		 * @since 1.5
854 		 * @param term_id
855 		 * @param taxonomy
856 		 * @param visible
857 		 * @returns {boolean} Whether visibility was set.
858 		 */
859 		term_manager.setTermVisibility = function( term_id, taxonomy, visible ) {
860 			var term_data;
861 
862 			if ( !loaded_terms[taxonomy] || !loaded_terms[taxonomy].terms[term_id] ) {
863 				return false;
864 			}
865 
866 			term_data = loaded_terms[taxonomy].terms[term_id];
867 
868 			if ( GeoMashup.map.closeInfoWindow ) {
869 				GeoMashup.map.closeInfoWindow();
870 			}
871 
872 			if ( term_data.line ) {
873 				if ( visible && GeoMashup.map.getZoom() <= term_data.max_line_zoom ) {
874 					GeoMashup.showLine( term_data.line );
875 				} else {
876 					GeoMashup.hideLine( term_data.line );
877 				}
878 			}
879 
880 			// Check for other visible terms at this location
881 			loaded_terms[taxonomy].terms[term_id].visible = visible;
882 
883 			$.each( loaded_terms[taxonomy].terms[term_id].points, function( i, point ) {
884 				GeoMashup.updateMarkerVisibility( GeoMashup.locations[point].marker );
885 			});
886 			
887 			GeoMashup.recluster();
888 			GeoMashup.updateVisibleList();
889 
890 			return true;
891 		};
892 
893 		/**
894 		 * Get the display name of a term.
895 		 * @methodOf GeoMashup
896 		 * @since 1.5
897 		 *
898 		 * @param taxonomy
899 		 * @param term_id
900 		 * @returns {String} The term name.
901 		 */
902 		term_manager.getTermName = function( taxonomy, term_id ) {
903 			return term_properties[taxonomy].terms[term_id].name;
904 		};
905 
906 		/**
907 		 * Determine whether a term ID is an ancestor of another.
908 		 * 
909 		 * Works on the loadedMap action and after, when the term hierarchy has been
910 		 * determined.
911 		 *
912 		 * @methodOf GeoMashup
913 		 * @since 1.5
914 		 * @param {String} ancestor_id The term ID of the potential ancestor
915 		 * @param {String} child_id The term ID of the potential child
916 		 * @param {String} taxonomy The taxonomy of the terms.
917 		 * @returns {boolean}
918 		 */
919 		term_manager.isTermAncestor = function( ancestor_id, child_id, taxonomy ) {
920 
921 			if ( !term_properties[taxonomy] ) {
922 				return false;
923 			}
924 
925 			ancestor_id = ancestor_id.toString();
926 			child_id = child_id.toString();
927 
928 			if ( term_properties[taxonomy].terms[child_id].parent_id ) {
929 				if ( term_properties[taxonomy].terms[child_id].parent_id === ancestor_id ) {
930 					return true;
931 				} else {
932 					return term_manager.isTermAncestor( ancestor_id, term_properties[taxonomy].terms[child_id].parent_id, taxonomy );
933 				}
934 			} else {
935 				return false;
936 			}
937 		};
938 
939 		/**
940 		 * Show or hide category lines according to their max_line_zoom setting.
941 		 *
942 		 * @methodOf GeoMashup
943 		 * @since 1.5
944 		 *
945 		 * @param {number} old_zoom Previous zoom level.
946 		 * @param {number} new_zoom New zoom level.
947 		 */
948 		term_manager.updateLineZoom = function( old_zoom, new_zoom ) {
949 			
950 			$.each( loaded_terms, function( taxonomy, tax_data ) {
951 
952 				$.each( tax_data.terms, function( term_id, term_data ) {
953 
954 					if ( term_data.visible && term_data.line ) {
955 
956 						if ( old_zoom <= term_data.max_line_zoom && new_zoom > term_data.max_line_zoom ) {
957 
958 							GeoMashup.hideLine( term_data.line );
959 
960 						} else if ( old_zoom > term_data.max_line_zoom && new_zoom <= term_data.max_line_zoom ) {
961 
962 							GeoMashup.showLine( term_data.line );
963 
964 						}
965 					}
966 
967 				});
968 
969 			});
970 
971 		};
972 
973 		/**
974 		 * Show or hide a tree of terms.
975 		 * @methodOf GeoMashup
976 		 * @since 1.5
977 		 *
978 		 * @param term_id
979 		 * @param hierarchy The term hierarchy ID tree.
980 		 * @param taxonomy
981 		 * @param visible
982 		 */
983 		term_manager.setHierarchyVisibility = function( term_id, hierarchy, taxonomy, visible ) {
984 
985 			term_manager.setTermVisibility( term_id, taxonomy, visible );
986 
987 			hierarchy = hierarchy || {};
988 			$.each( hierarchy, function( child_id, grandchildren ) {
989 				term_manager.setHierarchyVisibility( child_id, grandchildren, taxonomy, visible );
990 			} );
991 		};
992 
993 		return term_manager;
994 	}()),
995 
996 	/**
997 	 * Determine whether a category ID is an ancestor of another.
998 	 * 
999 	 * Works on the loadedMap action and after, when the category hierarchy has been
1000 	 * determined.
1001 	 *
1002 	 * @methodOf GeoMashup
1003 	 * @deprecated 1.5
1004 	 * @see GeoMashup.term_manager.isTermAncestor()
1005 	 *
1006 	 * @param {String} ancestor_id The category ID of the potential ancestor
1007 	 * @param {String} child_id The category ID of the potential child
1008 	 */
1009 	isCategoryAncestor : function(ancestor_id, child_id) {
1010 		return this.term_manager.isTermAncestor( ancestor_id, child_id, 'category' );
1011 	},
1012 
1013 	/**
1014 	 * Hide markers and line for a category.
1015 	 * @methodOf GeoMashup
1016 	 * @deprecated 1.5
1017 	 * @see GeoMashup.term_manager.setTermVisibility()
1018 	 * @param {String} category_id
1019 	 */
1020 	hideCategory : function(category_id) {
1021 		GeoMashup.term_manager.setTermVisibility( category_id, 'category', false );
1022 	},
1023 
1024 	/**
1025 	 * Show markers for a category. Also show line if consistent with configuration.
1026 	 * @methodOf GeoMashup
1027 	 * @deprecated 1.5
1028 	 * @see GeoMashup.term_manager.setTermVisibility()
1029 	 * @param {String} category_id
1030 	 */
1031 	showCategory : function(category_id) {
1032 		GeoMashup.term_manager.setTermVisibility( category_id, 'category', true );
1033 	},
1034 
1035 	/**
1036 	 * Hide a category and all its child categories.
1037 	 * @methodOf GeoMashup
1038 	 * @deprecated 1.5
1039 	 * @see GeoMashup.term_manager.searchTermHierarchy()
1040 	 * @see GeoMashup.term_manager.setHierarchyVisibility()
1041 	 * @param {String} category_id The ID of the category to hide
1042 	 */
1043 	hideCategoryHierarchy : function(category_id) {
1044 		var hierarchy = GeoMashup.term_manager.searchTermHierarchy( category_id, 'category' );
1045 		GeoMashup.term_manager.setHierarchyVisibility( category_id, hierarchy, 'category', false );
1046 	},
1047 
1048 	/**
1049 	 * Show a category and all its child categories.
1050 	 * @methodof GeoMashup
1051 	 * @deprecated 1.5
1052 	 * @see GeoMashup.term_manager.searchTermHierarchy()
1053 	 * @see GeoMashup.term_manager.setHierarchyVisibility()
1054 	 * @param {String} category_id The ID of the category to show
1055 	 */
1056 	showCategoryHierarchy : function(category_id) {
1057 		var hierarchy = GeoMashup.term_manager.searchTermHierarchy( category_id, 'category' );
1058 		GeoMashup.term_manager.setHierarchyVisibility( category_id, hierarchy, 'category', true );
1059 	},
1060 
1061 	/**
1062 	 * Select a tab of the tabbed category index control.
1063 	 * @methodOf GeoMashup
1064 	 * @deprecated 1.5
1065 	 * @see GeoMashup.term_manager.tabbed_index.selectTab()
1066 	 * @param {String} select_category_id The ID of the category tab to select
1067 	 */
1068 	categoryTabSelect : function(select_category_id) {
1069 		this.term_manager.tabbed_index.selectTab( select_category_id, 'category' );
1070 	},
1071 
1072 	/**
1073 	 * Get the DOM ID of the element containing a category index in the 
1074 	 * tabbed category index control.
1075 	 * @methodOf GeoMashup
1076 	 * @deprecated 1.5
1077 	 * @see GeoMashup.term_manager.tabbed_index.getTermIndexId()
1078 	 * @param {String} category_id The category ID
1079 	 * @return {String} DOM ID
1080 	 */
1081 	categoryIndexId : function(category_id) {
1082 		return 'gm-cat-index-' + category_id;
1083 	},
1084 
1085 	createTermLine: function( term_data ) {
1086 		//provider override
1087 	}
1088 
1089 } );
1090 
1091