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