1 /*! 2 * Timemap.js Copyright 2008 Nick Rabinowitz. 3 * Licensed under the MIT License (see LICENSE.txt) 4 */ 5 6 /** 7 * @overview 8 * 9 * <p>Timemap.js is intended to sync a SIMILE Timeline with a Google Map. 10 * Depends on: Google Maps API v2, SIMILE Timeline v1.2 - 2.3.1. 11 * Thanks to Jorn Clausen (http://www.oe-files.de) for initial concept and code. 12 * Timemap.js is licensed under the MIT License (see <a href="../LICENSE.txt">LICENSE.txt</a>).</p> 13 * <ul> 14 * <li><a href="http://code.google.com/p/timemap/">Project Homepage</a></li> 15 * <li><a href="http://groups.google.com/group/timemap-development">Discussion Group</a></li> 16 * <li><a href="../examples/index.html">Working Examples</a></li> 17 * </ul> 18 * 19 * @name timemap.js 20 * @author Nick Rabinowitz (www.nickrabinowitz.com) 21 * @version 1.6 22 */ 23 24 // globals - for JSLint 25 /*global GBrowserIsCompatible, GLargeMapControl, GMap2, GIcon */ 26 /*global GMapTypeControl, GDownloadUrl, GGroundOverlay */ 27 /*global GMarker, GPolygon, GPolyline, GSize, G_DEFAULT_ICON */ 28 /*global G_HYBRID_MAP, G_MOON_VISIBLE_MAP, G_SKY_VISIBLE_MAP */ 29 30 (function(){ 31 32 // borrowing some space-saving devices from jquery 33 var 34 // Will speed up references to window, and allows munging its name. 35 window = this, 36 // Will speed up references to undefined, and allows munging its name. 37 undefined, 38 // aliases for Timeline objects 39 Timeline = window.Timeline, DateTime = Timeline.DateTime, 40 // aliases for Google variables (anything that gets used more than once) 41 G_DEFAULT_MAP_TYPES = window.G_DEFAULT_MAP_TYPES, 42 G_NORMAL_MAP = window.G_NORMAL_MAP, 43 G_PHYSICAL_MAP = window.G_PHYSICAL_MAP, 44 G_SATELLITE_MAP = window.G_SATELLITE_MAP, 45 GLatLng = window.GLatLng, 46 GLatLngBounds = window.GLatLngBounds, 47 GEvent = window.GEvent, 48 // Google icon path 49 GIP = "http://www.google.com/intl/en_us/mapfiles/ms/icons/", 50 // aliases for class names, allowing munging 51 TimeMap, TimeMapFilterChain, TimeMapDataset, TimeMapTheme, TimeMapItem; 52 53 /*---------------------------------------------------------------------------- 54 * TimeMap Class 55 *---------------------------------------------------------------------------*/ 56 57 /** 58 * @class 59 * The TimeMap object holds references to timeline, map, and datasets. 60 * 61 * @constructor 62 * This will create the visible map, but not the timeline, which must be initialized separately. 63 * 64 * @param {DOM Element} tElement The timeline element. 65 * @param {DOM Element} mElement The map element. 66 * @param {Object} [options] A container for optional arguments 67 * @param {TimeMapTheme|String} [options.theme=red] Color theme for the timemap 68 * @param {Boolean} [options.syncBands=true] Whether to synchronize all bands in timeline 69 * @param {GLatLng} [options.mapCenter=0,0] Point for map center 70 * @param {Number} [options.mapZoom=0] Initial map zoom level 71 * @param {GMapType|String} [options.mapType=physical] The maptype for the map 72 * @param {Array} [options.mapTypes=normal,satellite,physical] The set of maptypes available for the map 73 * @param {Function|String} [options.mapFilter={@link TimeMap.filters.hidePastFuture}] 74 * How to hide/show map items depending on timeline state; 75 * options: keys in {@link TimeMap.filters} or function 76 * @param {Boolean} [options.showMapTypeCtrl=true] Whether to display the map type control 77 * @param {Boolean} [options.showMapCtrl=true] Whether to show map navigation control 78 * @param {Boolean} [options.centerMapOnItems=true] Whether to center and zoom the map based on loaded item 79 * @param {Boolean} [options.noEventLoad=false] Whether to skip loading events on the timeline 80 * @param {Boolean} [options.noPlacemarkLoad=false] Whether to skip loading placemarks on the map 81 * @param {String} [options.eventIconPath] Path for directory holding event icons; if set at the TimeMap 82 * level, will override dataset and item defaults 83 * @param {String} [options.infoTemplate] HTML for the info window content, with variable expressions 84 * (as "{{varname}}" by default) to be replaced by option data 85 * @param {String} [options.templatePattern] Regex pattern defining variable syntax in the infoTemplate 86 * @param {Function} [options.openInfoWindow={@link TimeMapItem.openInfoWindowBasic}] 87 * Function redefining how info window opens 88 * @param {Function} [options.closeInfoWindow={@link TimeMapItem.closeInfoWindowBasic}] 89 * Function redefining how info window closes 90 * @param {mixed} [options[...]] Any of the options for {@link TimeMapTheme} may be set here, 91 * to cascade to the entire TimeMap, though they can be overridden 92 * at lower levels 93 * </pre> 94 */ 95 TimeMap = function(tElement, mElement, options) { 96 var tm = this, 97 // set defaults for options 98 defaults = { 99 mapCenter: new GLatLng(0,0), 100 mapZoom: 0, 101 mapType: G_PHYSICAL_MAP, 102 mapTypes: [G_NORMAL_MAP, G_SATELLITE_MAP, G_PHYSICAL_MAP], 103 showMapTypeCtrl: true, 104 showMapCtrl: true, 105 syncBands: true, 106 mapFilter: 'hidePastFuture', 107 centerOnItems: true, 108 theme: 'red' 109 }; 110 111 // save DOM elements 112 /** 113 * Map element 114 * @name TimeMap#mElement 115 * @type DOM Element 116 */ 117 tm.mElement = mElement; 118 /** 119 * Timeline element 120 * @name TimeMap#tElement 121 * @type DOM Element 122 */ 123 tm.tElement = tElement; 124 125 /** 126 * Map of datasets 127 * @name TimeMap#datasets 128 * @type Object 129 */ 130 tm.datasets = {}; 131 /** 132 * Filter chains for this timemap 133 * @name TimeMap#chains 134 * @type Object 135 */ 136 tm.chains = {}; 137 138 /** 139 * Container for optional settings passed in the "options" parameter 140 * @name TimeMap#opts 141 * @type Object 142 */ 143 tm.opts = options = util.merge(options, defaults); 144 145 // only these options will cascade to datasets and items 146 options.mergeOnly = ['mergeOnly', 'theme', 'eventIconPath', 'openInfoWindow', 147 'closeInfoWindow', 'noPlacemarkLoad', 'noEventLoad', 148 'infoTemplate', 'templatePattern'] 149 150 // allow map types to be specified by key 151 options.mapType = util.lookup(options.mapType, TimeMap.mapTypes); 152 // allow map filters to be specified by key 153 options.mapFilter = util.lookup(options.mapFilter, TimeMap.filters); 154 // allow theme options to be specified in options 155 options.theme = TimeMapTheme.create(options.theme, options); 156 157 // initialize map 158 tm.initMap(); 159 }; 160 161 /** 162 * Initialize the map. 163 */ 164 TimeMap.prototype.initMap = function() { 165 var options = this.opts, map, i; 166 if (GBrowserIsCompatible()) { 167 168 /** 169 * The associated GMap object 170 * @type GMap2 171 */ 172 this.map = map = new GMap2(this.mElement); 173 174 // drop all existing types 175 for (i=G_DEFAULT_MAP_TYPES.length-1; i>0; i--) { 176 map.removeMapType(G_DEFAULT_MAP_TYPES[i]); 177 } 178 // you can't remove the last maptype, so add a new one first 179 map.addMapType(options.mapTypes[0]); 180 map.removeMapType(G_DEFAULT_MAP_TYPES[0]); 181 // add the rest of the new types 182 for (i=1; i<options.mapTypes.length; i++) { 183 map.addMapType(options.mapTypes[i]); 184 } 185 186 // initialize map center, zoom, and map type 187 map.setCenter(options.mapCenter, options.mapZoom, options.mapType); 188 189 // set basic parameters 190 map.enableDoubleClickZoom(); 191 map.enableScrollWheelZoom(); 192 map.enableContinuousZoom(); 193 194 // set controls 195 if (options.showMapCtrl) { 196 map.addControl(new GLargeMapControl()); 197 } 198 if (options.showMapTypeCtrl) { 199 map.addControl(new GMapTypeControl()); 200 } 201 202 /** 203 * Bounds of the map 204 * @type GLatLngBounds 205 */ 206 this.mapBounds = options.mapZoom > 0 ? 207 // if the zoom has been set, use the map bounds 208 map.getBounds() : 209 // otherwise, start from scratch 210 new GLatLngBounds(); 211 } 212 }; 213 214 /** 215 * Current library version. 216 * @constant 217 * @type String 218 */ 219 TimeMap.version = "1.6"; 220 221 /** 222 * @name TimeMap.util 223 * @namespace 224 * Namespace for TimeMap utility functions. 225 */ 226 var util = TimeMap.util = {}; 227 228 /** 229 * Intializes a TimeMap. 230 * 231 * <p>The idea here is to throw all of the standard intialization settings into 232 * a large object and then pass it to the TimeMap.init() function. The full 233 * data format is outlined below, but if you leave elements out the script 234 * will use default settings instead.</p> 235 * 236 * <p>See the examples and the 237 * <a href="http://code.google.com/p/timemap/wiki/UsingTimeMapInit">UsingTimeMapInit wiki page</a> 238 * for usage.</p> 239 * 240 * @param {Object} config Full set of configuration options. 241 * @param {String} config.mapId DOM id of the element to contain the map 242 * @param {String} config.timelineId DOM id of the element to contain the timeline 243 * @param {Object} [config.options] Options for the TimeMap object (see the {@link TimeMap} constructor) 244 * @param {Object[]} config.datasets Array of datasets to load 245 * @param {Object} config.datasets[x] Configuration options for a particular dataset 246 * @param {String|Class} config.datasets[x].type Loader type for this dataset (generally a sub-class 247 * of {@link TimeMap.loaders.base}) 248 * @param {Object} config.datasets[x].options Options for the loader. See the {@link TimeMap.loaders.base} 249 * constructor and the constructors for the various loaders for 250 * more details. 251 * @param {String} [config.datasets[x].id] Optional id for the dataset in the {@link TimeMap#datasets} 252 * object, for future reference; otherwise "ds"+x is used 253 * @param {String} [config.datasets[x][...]] Other options for the {@link TimeMapDataset} object 254 * @param {String|Array} [config.bandIntervals] Intervals for the two default timeline bands. Can either be an 255 * array of interval constants or a key in {@link TimeMap.intervals} 256 * @param {Object[]} [config.bandInfo] Array of configuration objects for Timeline bands, to be passed to 257 * Timeline.createBandInfo (see the <a href="http://code.google.com/p/simile-widgets/wiki/Timeline_GettingStarted">Timeline Getting Started tutorial</a>). 258 * This will override config.bandIntervals, if provided. 259 * @param {Timeline.Band[]} [config.bands] Array of instantiated Timeline Band objects. This will override 260 * config.bandIntervals and config.bandInfo, if provided. 261 * @param {Function} [config.dataLoadedFunction] Function to be run as soon as all datasets are loaded, but 262 * before they've been displayed on the map and timeline 263 * (this will override dataDisplayedFunction and scrollTo) 264 * @param {Function} [config.dataDisplayedFunction] Function to be run as soon as all datasets are loaded and 265 * displayed on the map and timeline 266 * @param {String|Date} [config.scrollTo] Date to scroll to once data is loaded - see 267 * {@link TimeMap.parseDate} for options; default is "earliest" 268 * @return {TimeMap} The initialized TimeMap object 269 */ 270 TimeMap.init = function(config) { 271 var err = "TimeMap.init: No id for ", 272 // set defaults 273 defaults = { 274 options: {}, 275 datasets: [], 276 bands: false, 277 bandInfo: false, 278 bandIntervals: "wk", 279 scrollTo: "earliest" 280 }, 281 state = TimeMap.state, 282 intervals, tm, 283 datasets = [], x, ds, dsOptions, topOptions, dsId, 284 bands = [], eventSource, bandInfo; 285 286 // check required elements 287 if (!('mapId' in config) || !config.mapId) { 288 throw err + "map"; 289 } 290 if (!('timelineId' in config) || !config.timelineId) { 291 throw err + "timeline"; 292 } 293 294 // get state from url hash if state functions are available 295 if (state) { 296 state.setConfigFromUrl(config); 297 } 298 // merge options and defaults 299 config = util.merge(config, defaults); 300 301 if (!config.bandInfo && !config.bands) { 302 // allow intervals to be specified by key 303 intervals = util.lookup(config.bandIntervals, TimeMap.intervals); 304 // make default band info 305 config.bandInfo = [ 306 { 307 width: "80%", 308 intervalUnit: intervals[0], 309 intervalPixels: 70 310 }, 311 { 312 width: "20%", 313 intervalUnit: intervals[1], 314 intervalPixels: 100, 315 showEventText: false, 316 overview: true, 317 trackHeight: 0.4, 318 trackGap: 0.2 319 } 320 ]; 321 } 322 323 // create the TimeMap object 324 tm = new TimeMap( 325 document.getElementById(config.timelineId), 326 document.getElementById(config.mapId), 327 config.options); 328 329 // create the dataset objects 330 for (x=0; x < config.datasets.length; x++) { 331 ds = config.datasets[x]; 332 // put top-level data into options 333 topOptions = { 334 title: ds.title, 335 theme: ds.theme, 336 dateParser: ds.dateParser 337 }; 338 dsOptions = util.merge(ds.options, topOptions); 339 dsId = ds.id || "ds" + x; 340 datasets[x] = tm.createDataset(dsId, dsOptions); 341 if (x > 0) { 342 // set all to the same eventSource 343 datasets[x].eventSource = datasets[0].eventSource; 344 } 345 } 346 // add a pointer to the eventSource in the TimeMap 347 tm.eventSource = datasets[0].eventSource; 348 349 // set up timeline bands 350 // ensure there's at least an empty eventSource 351 eventSource = (datasets[0] && datasets[0].eventSource) || new Timeline.DefaultEventSource(); 352 // check for pre-initialized bands (manually created with Timeline.createBandInfo()) 353 if (config.bands) { 354 bands = config.bands; 355 // substitute dataset event source 356 for (x=0; x < bands.length; x++) { 357 // assume that these have been set up like "normal" Timeline bands: 358 // with an empty event source if events are desired, and null otherwise 359 if (bands[x].eventSource !== null) { 360 bands[x].eventSource = eventSource; 361 } 362 } 363 } 364 // otherwise, make bands from band info 365 else { 366 for (x=0; x < config.bandInfo.length; x++) { 367 bandInfo = config.bandInfo[x]; 368 // if eventSource is explicitly set to null or false, ignore 369 if (!(('eventSource' in bandInfo) && !bandInfo.eventSource)) { 370 bandInfo.eventSource = eventSource; 371 } 372 else { 373 bandInfo.eventSource = null; 374 } 375 bands[x] = Timeline.createBandInfo(bandInfo); 376 if (x > 0 && util.TimelineVersion() == "1.2") { 377 // set all to the same layout 378 bands[x].eventPainter.setLayout(bands[0].eventPainter.getLayout()); 379 } 380 } 381 } 382 // initialize timeline 383 tm.initTimeline(bands); 384 385 // initialize load manager 386 var loadManager = TimeMap.loadManager; 387 loadManager.init(tm, config.datasets.length, config); 388 389 // load data! 390 for (x=0; x < config.datasets.length; x++) { 391 (function(x) { // deal with closure issues 392 var data = config.datasets[x], options, type, callback, loaderClass, loader; 393 // support some older syntax 394 options = data.data || data.options || {}; 395 type = data.type || options.type; 396 callback = function() { loadManager.increment(); }; 397 // get loader class 398 loaderClass = (typeof(type) == 'string') ? TimeMap.loaders[type] : type; 399 // load with appropriate loader 400 loader = new loaderClass(options); 401 loader.load(datasets[x], callback); 402 })(x); 403 } 404 // return timemap object for later manipulation 405 return tm; 406 }; 407 408 /** 409 * @class Static singleton for managing multiple asynchronous loads 410 */ 411 TimeMap.loadManager = new function() { 412 var mgr = this; 413 414 /** 415 * Initialize (or reset) the load manager 416 * @name TimeMap.loadManager#init 417 * @function 418 * 419 * @param {TimeMap} tm TimeMap instance 420 * @param {Number} target Number of datasets we're loading 421 * @param {Object} [options] Container for optional settings 422 * @param {Function} [options.dataLoadedFunction] 423 * Custom function replacing default completion function; 424 * should take one parameter, the TimeMap object 425 * @param {String|Date} [options.scrollTo] 426 * Where to scroll the timeline when load is complete 427 * Options: "earliest", "latest", "now", date string, Date 428 * @param {Function} [options.dataDisplayedFunction] 429 * Custom function to fire once data is loaded and displayed; 430 * should take one parameter, the TimeMap object 431 */ 432 mgr.init = function(tm, target, config) { 433 mgr.count = 0; 434 mgr.tm = tm; 435 mgr.target = target; 436 mgr.opts = config || {}; 437 }; 438 439 /** 440 * Increment the count of loaded datasets 441 * @name TimeMap.loadManager#increment 442 * @function 443 */ 444 mgr.increment = function() { 445 mgr.count++; 446 if (mgr.count >= mgr.target) { 447 mgr.complete(); 448 } 449 }; 450 451 /** 452 * Function to fire when all loads are complete. 453 * Default behavior is to scroll to a given date (if provided) and 454 * layout the timeline. 455 * @name TimeMap.loadManager#complete 456 * @function 457 */ 458 mgr.complete = function() { 459 var tm = mgr.tm, 460 opts = mgr.opts, 461 // custom function including timeline scrolling and layout 462 func = opts.dataLoadedFunction; 463 if (func) { 464 func(tm); 465 } 466 else { 467 tm.scrollToDate(opts.scrollTo, true); 468 // check for state support 469 if (tm.initState) tm.initState(); 470 // custom function to be called when data is loaded 471 func = opts.dataDisplayedFunction; 472 if (func) func(tm); 473 } 474 }; 475 }; 476 477 /** 478 * Parse a date in the context of the timeline. Uses the standard parser 479 * ({@link TimeMapDataset.hybridParser}) but accepts "now", "earliest", 480 * "latest", "first", and "last" (referring to loaded events) 481 * 482 * @param {String|Date} s String (or date) to parse 483 * @return {Date} Parsed date 484 */ 485 TimeMap.prototype.parseDate = function(s) { 486 var d = new Date(), 487 eventSource = this.eventSource, 488 parser = TimeMapDataset.hybridParser, 489 // make sure there are events to scroll to 490 hasEvents = eventSource.getCount() > 0 ? true : false; 491 switch (s) { 492 case "now": 493 break; 494 case "earliest": 495 case "first": 496 if (hasEvents) { 497 d = eventSource.getEarliestDate(); 498 } 499 break; 500 case "latest": 501 case "last": 502 if (hasEvents) { 503 d = eventSource.getLatestDate(); 504 } 505 break; 506 default: 507 // assume it's a date, try to parse 508 d = parser(s); 509 } 510 return d; 511 } 512 513 /** 514 * Scroll the timeline to a given date. If lazyLayout is specified, this function 515 * will also call timeline.layout(), but only if it won't be called by the 516 * onScroll listener. This involves a certain amount of reverse engineering, 517 * and may not be future-proof. 518 * 519 * @param {String|Date} d Date to scroll to (either a date object, a 520 * date string, or one of the strings accepted 521 * by TimeMap#parseDate) 522 * @param {Boolean} [lazyLayout] Whether to call timeline.layout() if not 523 * required by the scroll. 524 */ 525 TimeMap.prototype.scrollToDate = function(d, lazyLayout) { 526 var d = this.parseDate(d), 527 timeline = this.timeline, x, 528 layouts = [], 529 band, minTime, maxTime; 530 if (d) { 531 // check which bands will need layout after scroll 532 for (x=0; x < timeline.getBandCount(); x++) { 533 band = timeline.getBand(x); 534 minTime = band.getMinDate().getTime(); 535 maxTime = band.getMaxDate().getTime(); 536 layouts[x] = (lazyLayout && d.getTime() > minTime && d.getTime() < maxTime); 537 } 538 // do scroll 539 timeline.getBand(0).setCenterVisibleDate(d); 540 // layout as necessary 541 for (x=0; x < layouts.length; x++) { 542 if (layouts[x]) { 543 timeline.getBand(x).layout(); 544 } 545 } 546 } 547 // layout if requested even if no date is found 548 else if (lazyLayout) { 549 timeline.layout(); 550 } 551 } 552 553 /** 554 * Create an empty dataset object and add it to the timemap 555 * 556 * @param {String} id The id of the dataset 557 * @param {Object} options A container for optional arguments for dataset constructor - 558 * see the options passed to {@link TimeMapDataset} 559 * @return {TimeMapDataset} The new dataset object 560 */ 561 TimeMap.prototype.createDataset = function(id, options) { 562 var tm = this, 563 dataset = new TimeMapDataset(tm, options); 564 tm.datasets[id] = dataset; 565 // add event listener 566 if (tm.opts.centerOnItems) { 567 var map = tm.map, 568 bounds = tm.mapBounds; 569 GEvent.addListener(dataset, 'itemsloaded', function() { 570 // determine the center and zoom level from the bounds 571 map.setCenter( 572 bounds.getCenter(), 573 map.getBoundsZoomLevel(bounds) 574 ); 575 }); 576 } 577 return dataset; 578 }; 579 580 /** 581 * Initialize the timeline - this must happen separately to allow full control of 582 * timeline properties. 583 * 584 * @param {BandInfo Array} bands Array of band information objects for timeline 585 */ 586 TimeMap.prototype.initTimeline = function(bands) { 587 var tm = this, 588 x, painter; 589 590 // synchronize & highlight timeline bands 591 for (x=1; x < bands.length; x++) { 592 if (tm.opts.syncBands) { 593 bands[x].syncWith = (x-1); 594 } 595 bands[x].highlight = true; 596 } 597 598 /** 599 * The associated timeline object 600 * @name TimeMap#timeline 601 * @type Timeline 602 */ 603 tm.timeline = Timeline.create(tm.tElement, bands); 604 605 // set event listeners 606 607 // update map on timeline scroll 608 tm.timeline.getBand(0).addOnScrollListener(function() { 609 tm.filter("map"); 610 }); 611 612 // hijack timeline popup window to open info window 613 for (x=0; x < tm.timeline.getBandCount(); x++) { 614 painter = tm.timeline.getBand(x).getEventPainter().constructor; 615 painter.prototype._showBubble = function(xx, yy, evt) { 616 evt.item.openInfoWindow(); 617 }; 618 } 619 620 // filter chain for map placemarks 621 tm.addFilterChain("map", 622 function(item) { 623 item.showPlacemark(); 624 }, 625 function(item) { 626 item.hidePlacemark(); 627 } 628 ); 629 630 // filter: hide when item is hidden 631 tm.addFilter("map", function(item) { 632 return item.visible; 633 }); 634 // filter: hide when dataset is hidden 635 tm.addFilter("map", function(item) { 636 return item.dataset.visible; 637 }); 638 639 // filter: hide map items depending on timeline state 640 tm.addFilter("map", tm.opts.mapFilter); 641 642 // filter chain for timeline events 643 tm.addFilterChain("timeline", 644 // on 645 function(item) { 646 item.showEvent(); 647 }, 648 // off 649 function(item) { 650 item.hideEvent(); 651 }, 652 // pre 653 null, 654 // post 655 function() { 656 var tm = this.timemap; 657 tm.eventSource._events._index(); 658 tm.timeline.layout(); 659 } 660 ); 661 662 // filter: hide when item is hidden 663 tm.addFilter("timeline", function(item) { 664 return item.visible; 665 }); 666 // filter: hide when dataset is hidden 667 tm.addFilter("timeline", function(item) { 668 return item.dataset.visible; 669 }); 670 671 // add callback for window resize 672 var resizeTimerID = null, 673 timeline = tm.timeline; 674 window.onresize = function() { 675 if (resizeTimerID === null) { 676 resizeTimerID = window.setTimeout(function() { 677 resizeTimerID = null; 678 timeline.layout(); 679 }, 500); 680 } 681 }; 682 }; 683 684 /** 685 * Run a function on each dataset in the timemap. This is the preferred 686 * iteration method, as it allows for future iterator options. 687 * 688 * @param {Function} f The function to run, taking one dataset as an argument 689 */ 690 TimeMap.prototype.each = function(f) { 691 var tm = this, 692 id; 693 for (id in tm.datasets) { 694 if (tm.datasets.hasOwnProperty(id)) { 695 f(tm.datasets[id]); 696 } 697 } 698 }; 699 700 /** 701 * Run a function on each item in each dataset in the timemap. 702 * 703 * @param {Function} f The function to run, taking one item as an argument 704 */ 705 TimeMap.prototype.eachItem = function(f) { 706 this.each(function(ds) { 707 ds.each(function(item) { 708 f(item); 709 }); 710 }); 711 }; 712 713 /** 714 * Get all items from all datasets. 715 * 716 * @return {TimeMapItem[]} Array of all items 717 */ 718 TimeMap.prototype.getItems = function() { 719 var items = []; 720 this.eachItem(function(item) { 721 items.push(item); 722 }); 723 return items; 724 }; 725 726 727 /*---------------------------------------------------------------------------- 728 * Loader namespace and base classes 729 *---------------------------------------------------------------------------*/ 730 731 /** 732 * @namespace 733 * Namespace for different data loader functions. 734 * New loaders can add their factories or constructors to this object; loader 735 * functions are passed an object with parameters in TimeMap.init(). 736 * 737 * @example 738 TimeMap.init({ 739 datasets: [ 740 { 741 // name of class in TimeMap.loaders 742 type: "json_string", 743 options: { 744 url: "mydata.json" 745 }, 746 // etc... 747 } 748 ], 749 // etc... 750 }); 751 */ 752 TimeMap.loaders = { 753 754 /** 755 * @namespace 756 * Namespace for storing callback functions 757 * @private 758 */ 759 cb: {}, 760 761 /** 762 * Cancel all current load requests. In practice, this is really only 763 * applicable to remote asynchronous loads. Note that this doesn't cancel 764 * the download of data, just the callback that loads it. 765 */ 766 cancelAll: function() { 767 var namespace = TimeMap.loaders.cb, 768 callbackName; 769 for (callbackName in namespace) { 770 if (namespace.hasOwnProperty(callbackName)) { 771 // replace with self-cancellation function 772 namespace[callbackName] = function() { 773 delete namespace[callbackName]; 774 }; 775 } 776 } 777 }, 778 779 /** 780 * Static counter for naming callback functions 781 * @private 782 * @type int 783 */ 784 counter: 0, 785 786 /** 787 * @class 788 * Abstract loader class. All loaders should inherit from this class. 789 * 790 * @constructor 791 * @param {Object} options All options for the loader 792 * @param {Function} [options.parserFunction=Do nothing] 793 * Parser function to turn a string into a JavaScript array 794 * @param {Function} [options.preloadFunction=Do nothing] 795 * Function to call on data before loading 796 * @param {Function} [options.transformFunction=Do nothing] 797 * Function to call on individual items before loading 798 * @param {String|Date} [options.scrollTo=earliest] Date to scroll the timeline to in the default callback 799 * (see {@link TimeMap#parseDate} for accepted syntax) 800 */ 801 base: function(options) { 802 var dummy = function(data) { return data; }, 803 loader = this; 804 805 /** 806 * Parser function to turn a string into a JavaScript array 807 * @name TimeMap.loaders.base#parse 808 * @function 809 * @parameter {String} s String to parse 810 * @return {Object[]} Array of item data 811 */ 812 loader.parse = options.parserFunction || dummy; 813 814 /** 815 * Function to call on data object before loading 816 * @name TimeMap.loaders.base#preload 817 * @function 818 * @parameter {Object} data Data to preload 819 * @return {Object[]} Array of item data 820 */ 821 loader.preload = options.preloadFunction || dummy; 822 823 /** 824 * Function to call on a single item data object before loading 825 * @name TimeMap.loaders.base#transform 826 * @function 827 * @parameter {Object} data Data to transform 828 * @return {Object} Transformed data for one item 829 */ 830 loader.transform = options.transformFunction || dummy; 831 832 /** 833 * Date to scroll the timeline to on load 834 * @name TimeMap.loaders.base#scrollTo 835 * @default "earliest" 836 * @type String|Date 837 */ 838 loader.scrollTo = options.scrollTo || "earliest"; 839 840 /** 841 * Get the name of a callback function that can be cancelled. This callback applies the parser, 842 * preload, and transform functions, loads the data, then calls the user callback 843 * @name TimeMap.loaders.base#getCallbackName 844 * @function 845 * 846 * @param {TimeMapDataset} dataset Dataset to load data into 847 * @param {Function} callback User-supplied callback function. If no function 848 * is supplied, the default callback will be used 849 * @return {String} The name of the callback function in TimeMap.loaders.cb 850 */ 851 loader.getCallbackName = function(dataset, callback) { 852 var callbacks = TimeMap.loaders.cb, 853 // Define a unique function name 854 callbackName = "_" + TimeMap.loaders.counter++, 855 // Define default callback 856 callback = callback || function() { 857 dataset.timemap.scrollToDate(loader.scrollTo, true); 858 }; 859 860 // create callback 861 callbacks[callbackName] = function(result) { 862 // parse 863 var items = loader.parse(result); 864 // preload 865 items = loader.preload(items); 866 // load 867 dataset.loadItems(items, loader.transform); 868 // callback 869 callback(); 870 // delete the callback function 871 delete callbacks[callbackName]; 872 }; 873 874 return callbackName; 875 }; 876 877 /** 878 * Get a callback function that can be cancelled. This is a convenience function 879 * to be used if the callback name itself is not needed. 880 * @name TimeMap.loaders.base#getCallback 881 * @function 882 * @see TimeMap.loaders.base#getCallbackName 883 * 884 * @param {TimeMapDataset} dataset Dataset to load data into 885 * @param {Function} callback User-supplied callback function 886 * @return {Function} The configured callback function 887 */ 888 loader.getCallback = function(dataset, callback) { 889 // get loader callback name 890 var callbackName = loader.getCallbackName(dataset, callback); 891 // return the function 892 return TimeMap.loaders.cb[callbackName]; 893 }; 894 }, 895 896 /** 897 * @class 898 * Basic loader class, for pre-loaded data. 899 * Other types of loaders should take the same parameter. 900 * 901 * @augments TimeMap.loaders.base 902 * @example 903 TimeMap.init({ 904 datasets: [ 905 { 906 type: "basic", 907 options: { 908 data: [ 909 // object literals for each item 910 { 911 title: "My Item", 912 start: "2009-10-06", 913 point: { 914 lat: 37.824, 915 lon: -122.256 916 } 917 }, 918 // etc... 919 ] 920 } 921 } 922 ], 923 // etc... 924 }); 925 * @see <a href="../../examples/basic.html">Basic Example</a> 926 * 927 * @constructor 928 * @param {Object} options All options for the loader 929 * @param {Array} options.data Array of items to load 930 * @param {mixed} [options[...]] Other options (see {@link TimeMap.loaders.base}) 931 */ 932 basic: function(options) { 933 var loader = new TimeMap.loaders.base(options); 934 935 /** 936 * Array of item data to load. 937 * @name TimeMap.loaders.basic#data 938 * @default [] 939 * @type Object[] 940 */ 941 loader.data = options.items || 942 // allow "value" for backwards compatibility 943 options.value || []; 944 945 /** 946 * Load javascript literal data. 947 * New loaders should implement a load function with the same signature. 948 * @name TimeMap.loaders.basic#load 949 * @function 950 * 951 * @param {TimeMapDataset} dataset Dataset to load data into 952 * @param {Function} callback Function to call once data is loaded 953 */ 954 loader.load = function(dataset, callback) { 955 // get callback function and call immediately on data 956 (this.getCallback(dataset, callback))(this.data); 957 }; 958 959 return loader; 960 }, 961 962 /** 963 * @class 964 * Generic class for loading remote data with a custom parser function 965 * 966 * @augments TimeMap.loaders.base 967 * 968 * @constructor 969 * @param {Object} options All options for the loader 970 * @param {String} options.url URL of file to load (NB: must be local address) 971 * @param {mixed} [options[...]] Other options (see {@link TimeMap.loaders.base}) 972 */ 973 remote: function(options) { 974 var loader = new TimeMap.loaders.base(options); 975 976 /** 977 * URL to load 978 * @name TimeMap.loaders.remote#url 979 * @type String 980 */ 981 loader.url = options.url; 982 983 /** 984 * Load function for remote files. 985 * @name TimeMap.loaders.remote#load 986 * @function 987 * 988 * @param {TimeMapDataset} dataset Dataset to load data into 989 * @param {Function} callback Function to call once data is loaded 990 */ 991 loader.load = function(dataset, callback) { 992 // download remote data and pass to callback 993 GDownloadUrl(this.url, this.getCallback(dataset, callback)); 994 }; 995 996 return loader; 997 } 998 999 }; 1000 1001 /*---------------------------------------------------------------------------- 1002 * TimeMapFilterChain Class 1003 *---------------------------------------------------------------------------*/ 1004 1005 /** 1006 * @class 1007 * TimeMapFilterChains hold a set of filters to apply to the map or timeline. 1008 * 1009 * @constructor 1010 * @param {TimeMap} timemap Reference to the timemap object 1011 * @param {Function} fon Function to run on an item if filter is true 1012 * @param {Function} foff Function to run on an item if filter is false 1013 * @param {Function} [pre] Function to run before the filter runs 1014 * @param {Function} [post] Function to run after the filter runs 1015 */ 1016 TimeMapFilterChain = function(timemap, fon, foff, pre, post) { 1017 var fc = this, 1018 dummy = function(item) {}; 1019 /** 1020 * Reference to parent TimeMap 1021 * @name TimeMapFilterChain#timemap 1022 * @type TimeMap 1023 */ 1024 fc.timemap = timemap; 1025 1026 /** 1027 * Chain of filter functions, each taking an item and returning a boolean 1028 * @name TimeMapFilterChain#chain 1029 * @type Function[] 1030 */ 1031 fc.chain = []; 1032 1033 /** 1034 * Function to run on an item if filter is true 1035 * @name TimeMapFilterChain#on 1036 * @function 1037 */ 1038 fc.on = fon || dummy; 1039 1040 /** 1041 * Function to run on an item if filter is false 1042 * @name TimeMapFilterChain#off 1043 * @function 1044 */ 1045 fc.off = foff || dummy; 1046 1047 /** 1048 * Function to run before the filter runs 1049 * @name TimeMapFilterChain#pre 1050 * @function 1051 */ 1052 fc.pre = pre || dummy; 1053 1054 /** 1055 * Function to run after the filter runs 1056 * @name TimeMapFilterChain#post 1057 * @function 1058 */ 1059 fc.post = post || dummy; 1060 } 1061 1062 /** 1063 * Add a filter to the filter chain. 1064 * 1065 * @param {Function} f Function to add 1066 */ 1067 TimeMapFilterChain.prototype.add = function(f) { 1068 this.chain.push(f); 1069 } 1070 1071 /** 1072 * Remove a filter from the filter chain 1073 * 1074 * @param {Function} [f] Function to remove; if not supplied, the last filter 1075 * added is removed 1076 */ 1077 TimeMapFilterChain.prototype.remove = function(f) { 1078 var chain = this.chain, 1079 i; 1080 if (!f) { 1081 // just remove the last filter added 1082 chain.pop(); 1083 } 1084 else { 1085 // look for the specific filter to remove 1086 for(i=0; i < chain.length; i++){ 1087 if(chain[i] == f){ 1088 chain.splice(i, 1); 1089 } 1090 } 1091 } 1092 } 1093 1094 /** 1095 * Run filters on all items 1096 */ 1097 TimeMapFilterChain.prototype.run = function() { 1098 var fc = this, 1099 chain = fc.chain; 1100 // early exit 1101 if (!chain.length) { 1102 return; 1103 } 1104 // pre-filter function 1105 fc.pre(); 1106 // run items through filter 1107 fc.timemap.eachItem(function(item) { 1108 var done = false; 1109 F_LOOP: while (!done) { 1110 for (var i = chain.length - 1; i >= 0; i--) { 1111 if (!chain[i](item)) { 1112 // false condition 1113 fc.off(item); 1114 break F_LOOP; 1115 } 1116 } 1117 // true condition 1118 fc.on(item); 1119 done = true; 1120 } 1121 }); 1122 // post-filter function 1123 fc.post(); 1124 } 1125 1126 // TimeMap helper functions for dealing with filters 1127 1128 /** 1129 * Update items, hiding or showing according to filters 1130 * 1131 * @param {String} fid Filter chain to update on 1132 */ 1133 TimeMap.prototype.filter = function(fid) { 1134 var fc = this.chains[fid]; 1135 if (fc) { 1136 fc.run(); 1137 } 1138 1139 }; 1140 1141 /** 1142 * Add a new filter chain 1143 * 1144 * @param {String} fid Id of the filter chain 1145 * @param {Function} fon Function to run on an item if filter is true 1146 * @param {Function} foff Function to run on an item if filter is false 1147 * @param {Function} [pre] Function to run before the filter runs 1148 * @param {Function} [post] Function to run after the filter runs 1149 */ 1150 TimeMap.prototype.addFilterChain = function(fid, fon, foff, pre, post) { 1151 this.chains[fid] = new TimeMapFilterChain(this, fon, foff, pre, post); 1152 }; 1153 1154 /** 1155 * Remove a filter chain 1156 * 1157 * @param {String} fid Id of the filter chain 1158 */ 1159 TimeMap.prototype.removeFilterChain = function(fid) { 1160 this.chains[fid] = null; 1161 }; 1162 1163 /** 1164 * Add a function to a filter chain 1165 * 1166 * @param {String} fid Id of the filter chain 1167 * @param {Function} f Function to add 1168 */ 1169 TimeMap.prototype.addFilter = function(fid, f) { 1170 var filterChain = this.chains[fid]; 1171 if (filterChain) { 1172 filterChain.add(f); 1173 } 1174 }; 1175 1176 /** 1177 * Remove a function from a filter chain 1178 * 1179 * @param {String} fid Id of the filter chain 1180 * @param {Function} [f] The function to remove 1181 */ 1182 TimeMap.prototype.removeFilter = function(fid, f) { 1183 var filterChain = this.chains[fid]; 1184 if (filterChain) { 1185 filterChain.remove(f); 1186 } 1187 }; 1188 1189 /** 1190 * @namespace 1191 * Namespace for different filter functions. Adding new filters to this 1192 * object allows them to be specified by string name. 1193 * @example 1194 TimeMap.init({ 1195 options: { 1196 mapFilter: "hideFuture" 1197 }, 1198 // etc... 1199 }); 1200 */ 1201 TimeMap.filters = { 1202 1203 /** 1204 * Static filter function: Hide items not in the visible area of the timeline. 1205 * 1206 * @param {TimeMapItem} item Item to test for filter 1207 * @return {Boolean} Whether to show the item 1208 */ 1209 hidePastFuture: function(item) { 1210 var topband = item.timeline.getBand(0), 1211 maxVisibleDate = topband.getMaxVisibleDate().getTime(), 1212 minVisibleDate = topband.getMinVisibleDate().getTime(), 1213 itemStart = item.getStartTime(), 1214 itemEnd = item.getEndTime(); 1215 if (itemStart !== undefined) { 1216 // hide items in the future 1217 return itemStart < maxVisibleDate && 1218 // hide items in the past 1219 (itemEnd > minVisibleDate || itemStart > minVisibleDate); 1220 } 1221 return true; 1222 }, 1223 1224 /** 1225 * Static filter function: Hide items later than the visible area of the timeline. 1226 * 1227 * @param {TimeMapItem} item Item to test for filter 1228 * @return {Boolean} Whether to show the item 1229 */ 1230 hideFuture: function(item) { 1231 var maxVisibleDate = item.timeline.getBand(0).getMaxVisibleDate().getTime(), 1232 itemStart = item.getStartTime(); 1233 if (itemStart !== undefined) { 1234 // hide items in the future 1235 return itemStart < maxVisibleDate; 1236 } 1237 return true; 1238 }, 1239 1240 /** 1241 * Static filter function: Hide items not present at the exact 1242 * center date of the timeline (will only work for duration events). 1243 * 1244 * @param {TimeMapItem} item Item to test for filter 1245 * @return {Boolean} Whether to show the item 1246 */ 1247 showMomentOnly: function(item) { 1248 var topband = item.timeline.getBand(0), 1249 momentDate = topband.getCenterVisibleDate().getTime(), 1250 itemStart = item.getStartTime(), 1251 itemEnd = item.getEndTime(); 1252 if (itemStart !== undefined) { 1253 // hide items in the future 1254 return itemStart < momentDate && 1255 // hide items in the past 1256 (itemEnd > momentDate || itemStart > momentDate); 1257 } 1258 return true; 1259 }, 1260 1261 /** 1262 * Convenience function: Do nothing. Can be used as a setting for mapFilter 1263 * in TimeMap.init() options, if you don't want map items to be hidden or 1264 * shown based on the timeline position. 1265 * 1266 * @param {TimeMapItem} item Item to test for filter 1267 * @return {Boolean} Whether to show the item 1268 */ 1269 none: function(item) { 1270 return true; 1271 } 1272 1273 } 1274 1275 1276 /*---------------------------------------------------------------------------- 1277 * TimeMapDataset Class 1278 *---------------------------------------------------------------------------*/ 1279 1280 /** 1281 * @class 1282 * The TimeMapDataset object holds an array of items and dataset-level 1283 * options and settings, including visual themes. 1284 * 1285 * @constructor 1286 * @param {TimeMap} timemap Reference to the timemap object 1287 * @param {Object} [options] Object holding optional arguments 1288 * @param {String} [options.id] Key for this dataset in the datasets map 1289 * @param {String} [options.title] Title of the dataset (for the legend) 1290 * @param {String|TimeMapTheme} [options.theme] Theme settings. 1291 * @param {String|Function} [options.dateParser] Function to replace default date parser. 1292 * @param {String} [options.infoTemplate] HTML template for info window content 1293 * @param {String} [options.templatePattern] Regex pattern defining variable syntax in the infoTemplate 1294 * @param {Function} [options.openInfoWindow] Function redefining how info window opens 1295 * @param {Function} [options.closeInfoWindow] Function redefining how info window closes 1296 * @param {mixed} [options[...]] Any of the options for {@link TimeMapTheme} may be set here, 1297 * to cascade to the dataset's objects, though they can be 1298 * overridden at the TimeMapItem level 1299 */ 1300 TimeMapDataset = function(timemap, options) { 1301 var ds = this, 1302 defaults = { 1303 title: 'Untitled', 1304 dateParser: TimeMapDataset.hybridParser 1305 }; 1306 1307 /** 1308 * Reference to parent TimeMap 1309 * @name TimeMapDataset#timemap 1310 * @type TimeMap 1311 */ 1312 ds.timemap = timemap; 1313 1314 /** 1315 * EventSource for timeline events 1316 * @name TimeMapDataset#eventSource 1317 * @type Timeline.EventSource 1318 */ 1319 ds.eventSource = new Timeline.DefaultEventSource(); 1320 1321 /** 1322 * Array of child TimeMapItems 1323 * @name TimeMapDataset#items 1324 * @type Array 1325 */ 1326 ds.items = []; 1327 1328 /** 1329 * Whether the dataset is visible 1330 * @name TimeMapDataset#visible 1331 * @type Boolean 1332 */ 1333 ds.visible = true; 1334 1335 /** 1336 * Container for optional settings passed in the "options" parameter 1337 * @name TimeMapDataset#opts 1338 * @type Object 1339 */ 1340 ds.opts = options = util.merge(options, defaults, timemap.opts); 1341 1342 // allow date parser to be specified by key 1343 options.dateParser = util.lookup(options.dateParser, TimeMap.dateParsers); 1344 // allow theme options to be specified in options 1345 options.theme = TimeMapTheme.create(options.theme, options); 1346 1347 /** 1348 * Return an array of this dataset's items 1349 * @name TimeMapDataset#getItems 1350 * @function 1351 * 1352 * @param {Number} [index] Index of single item to return 1353 * @return {TimeMapItem[]} Single item, or array of all items if no index was supplied 1354 */ 1355 ds.getItems = function(index) { 1356 if (index !== undefined) { 1357 if (index < ds.items.length) { 1358 return ds.items[index]; 1359 } 1360 else { 1361 return null; 1362 } 1363 } 1364 return ds.items; 1365 }; 1366 1367 /** 1368 * Return the title of the dataset 1369 * @name TimeMapDataset#getTitle 1370 * @function 1371 * 1372 * @return {String} Dataset title 1373 */ 1374 ds.getTitle = function() { return ds.opts.title; }; 1375 }; 1376 1377 /** 1378 * Better Timeline Gregorian parser... shouldn't be necessary :(. 1379 * Gregorian dates are years with "BC" or "AD" 1380 * 1381 * @param {String} s String to parse into a Date object 1382 * @return {Date} Parsed date or null 1383 */ 1384 TimeMapDataset.gregorianParser = function(s) { 1385 if (!s || typeof(s) != "string") { 1386 return null; 1387 } 1388 // look for BC 1389 var bc = Boolean(s.match(/b\.?c\.?/i)), 1390 // parse - parseInt will stop at non-number characters 1391 year = parseInt(s, 10), 1392 d; 1393 // look for success 1394 if (!isNaN(year)) { 1395 // deal with BC 1396 if (bc) { 1397 year = 1 - year; 1398 } 1399 // make Date and return 1400 d = new Date(0); 1401 d.setUTCFullYear(year); 1402 return d; 1403 } 1404 else { 1405 return null; 1406 } 1407 }; 1408 1409 /** 1410 * Parse date strings with a series of date parser functions, until one works. 1411 * In order: 1412 * <ol> 1413 * <li>Date.parse() (so Date.js should work here, if it works with Timeline...)</li> 1414 * <li>Gregorian parser</li> 1415 * <li>The Timeline ISO 8601 parser</li> 1416 * </ol> 1417 * 1418 * @param {String} s String to parse into a Date object 1419 * @return {Date} Parsed date or null 1420 */ 1421 TimeMapDataset.hybridParser = function(s) { 1422 // in case we don't know if this is a string or a date 1423 if (s instanceof Date) { 1424 return s; 1425 } 1426 // try native date parse and timestamp 1427 var d = new Date(typeof(s) == "number" ? s : Date.parse(s)); 1428 if (isNaN(d)) { 1429 if (typeof(s) == "string") { 1430 // look for Gregorian dates 1431 if (s.match(/^-?\d{1,6} ?(a\.?d\.?|b\.?c\.?e?\.?|c\.?e\.?)?$/i)) { 1432 d = TimeMapDataset.gregorianParser(s); 1433 } 1434 // try ISO 8601 parse 1435 else { 1436 try { 1437 d = DateTime.parseIso8601DateTime(s); 1438 } catch(e) { 1439 d = null; 1440 } 1441 } 1442 // look for timestamps 1443 if (!d && s.match(/^\d{7,}$/)) { 1444 d = new Date(parseInt(s)); 1445 } 1446 } else { 1447 return null; 1448 } 1449 } 1450 // d should be a date or null 1451 return d; 1452 }; 1453 1454 /** 1455 * Run a function on each item in the dataset. This is the preferred 1456 * iteration method, as it allows for future iterator options. 1457 * 1458 * @param {Function} f The function to run 1459 */ 1460 TimeMapDataset.prototype.each = function(f) { 1461 for (var x=0; x < this.items.length; x++) { 1462 f(this.items[x]); 1463 } 1464 }; 1465 1466 /** 1467 * Add an array of items to the map and timeline. 1468 * Each item has both a timeline event and a map placemark. 1469 * 1470 * @param {Object} data Data to be loaded. See loadItem() for the format. 1471 * @param {Function} [transform] If data is not in the above format, transformation function to make it so 1472 * @see TimeMapDataset#loadItem 1473 */ 1474 TimeMapDataset.prototype.loadItems = function(data, transform) { 1475 for (var x=0; x < data.length; x++) { 1476 this.loadItem(data[x], transform); 1477 } 1478 GEvent.trigger(this, 'itemsloaded'); 1479 }; 1480 1481 /** 1482 * Add one item to map and timeline. 1483 * Each item has both a timeline event and a map placemark. 1484 * 1485 * @param {Object} data Data to be loaded 1486 * @param {String} [data.title] Title of the item (visible on timeline) 1487 * @param {String|Date} [data.start] Start time of the event on the timeline 1488 * @param {String|Date} [data.end] End time of the event on the timeline (duration events only) 1489 * @param {Object} [data.point] Data for a single-point placemark: 1490 * @param {Float} [data.point.lat] Latitude of map marker 1491 * @param {Float} [data.point.lon] Longitude of map marker 1492 * @param {Object[]} [data.polyline] Data for a polyline placemark, as an array in "point" format 1493 * @param {Object[]} [data.polygon] Data for a polygon placemark, as an array "point" format 1494 * @param {Object} [data.overlay] Data for a ground overlay: 1495 * @param {String} [data.overlay.image] URL of image to overlay 1496 * @param {Float} [data.overlay.north] Northern latitude of the overlay 1497 * @param {Float} [data.overlay.south] Southern latitude of the overlay 1498 * @param {Float} [data.overlay.east] Eastern longitude of the overlay 1499 * @param {Float} [data.overlay.west] Western longitude of the overlay 1500 * @param {Object[]} [data.placemarks] Array of placemarks, e.g. [{point:{...}}, {polyline:[...]}] 1501 * @param {Object} [options] Optional arguments - see the {@link TimeMapItem} constructor for details 1502 * @param {Function} [transform] If data is not in the above format, transformation function to make it so 1503 * @return {TimeMapItem} The created item (for convenience, as it's already been added) 1504 * @see TimeMapItem 1505 */ 1506 TimeMapDataset.prototype.loadItem = function(data, transform) { 1507 // apply transformation, if any 1508 if (transform !== undefined) { 1509 data = transform(data); 1510 } 1511 // transform functions can return a null value to skip a datum in the set 1512 if (!data) { 1513 return; 1514 } 1515 1516 var ds = this, 1517 tm = ds.timemap, 1518 // set defaults for options 1519 options = util.merge(data.options, ds.opts), 1520 // allow theme options to be specified in options 1521 theme = options.theme = TimeMapTheme.create(options.theme, options), 1522 parser = ds.opts.dateParser, 1523 eventClass = Timeline.DefaultEventSource.Event, 1524 // settings for timeline event 1525 start = data.start, 1526 end = data.end, 1527 eventIcon = theme.eventIcon, 1528 textColor = theme.eventTextColor, 1529 title = data.title, 1530 // allow event-less placemarks - these will be always present on map 1531 event = null, 1532 instant, 1533 // settings for the placemark 1534 markerIcon = theme.icon, 1535 bounds = tm.mapBounds, 1536 // empty containers 1537 placemark = [], 1538 pdataArr = [], 1539 pdata = null, 1540 type = "", 1541 point = null, 1542 i; 1543 1544 // create timeline event 1545 start = start ? parser(start) : null; 1546 end = end ? parser(end) : null; 1547 instant = !end; 1548 if (start !== null) { 1549 if (util.TimelineVersion() == "1.2") { 1550 // attributes by parameter 1551 event = new eventClass(start, end, null, null, 1552 instant, title, null, null, null, eventIcon, theme.eventColor, 1553 theme.eventTextColor); 1554 } else { 1555 if (!textColor) { 1556 // tweak to show old-style events 1557 textColor = (theme.classicTape && !instant) ? '#FFFFFF' : '#000000'; 1558 } 1559 // attributes in object 1560 event = new eventClass({ 1561 start: start, 1562 end: end, 1563 instant: instant, 1564 text: title, 1565 icon: eventIcon, 1566 color: theme.eventColor, 1567 textColor: textColor 1568 }); 1569 } 1570 } 1571 1572 // internal function: create map placemark 1573 // takes a data object (could be full data, could be just placemark) 1574 // returns an object with {placemark, type, point} 1575 var createPlacemark = function(pdata) { 1576 var placemark = null, 1577 type = "", 1578 point = null; 1579 // point placemark 1580 if (pdata.point) { 1581 var lat = pdata.point.lat, 1582 lon = pdata.point.lon; 1583 if (lat === undefined || lon === undefined) { 1584 // give up 1585 return null; 1586 } 1587 point = new GLatLng( 1588 parseFloat(pdata.point.lat), 1589 parseFloat(pdata.point.lon) 1590 ); 1591 // add point to visible map bounds 1592 if (tm.opts.centerOnItems) { 1593 bounds.extend(point); 1594 } 1595 // create marker 1596 placemark = new GMarker(point, { 1597 icon: markerIcon, 1598 title: pdata.title 1599 }); 1600 type = "marker"; 1601 point = placemark.getLatLng(); 1602 } 1603 // polyline and polygon placemarks 1604 else if (pdata.polyline || pdata.polygon) { 1605 var points = [], line; 1606 if (pdata.polyline) { 1607 line = pdata.polyline; 1608 } else { 1609 line = pdata.polygon; 1610 } 1611 if (line && line.length) { 1612 for (var x=0; x<line.length; x++) { 1613 point = new GLatLng( 1614 parseFloat(line[x].lat), 1615 parseFloat(line[x].lon) 1616 ); 1617 points.push(point); 1618 // add point to visible map bounds 1619 if (tm.opts.centerOnItems) { 1620 bounds.extend(point); 1621 } 1622 } 1623 if ("polyline" in pdata) { 1624 placemark = new GPolyline(points, 1625 theme.lineColor, 1626 theme.lineWeight, 1627 theme.lineOpacity); 1628 type = "polyline"; 1629 point = placemark.getVertex(Math.floor(placemark.getVertexCount()/2)); 1630 } else { 1631 placemark = new GPolygon(points, 1632 theme.polygonLineColor, 1633 theme.polygonLineWeight, 1634 theme.polygonLineOpacity, 1635 theme.fillColor, 1636 theme.fillOpacity); 1637 type = "polygon"; 1638 point = placemark.getBounds().getCenter(); 1639 } 1640 } 1641 } 1642 // ground overlay placemark 1643 else if ("overlay" in pdata) { 1644 var sw = new GLatLng( 1645 parseFloat(pdata.overlay.south), 1646 parseFloat(pdata.overlay.west) 1647 ), 1648 ne = new GLatLng( 1649 parseFloat(pdata.overlay.north), 1650 parseFloat(pdata.overlay.east) 1651 ), 1652 // create overlay 1653 overlayBounds = new GLatLngBounds(sw, ne); 1654 // add to visible bounds 1655 if (tm.opts.centerOnItems) { 1656 bounds.extend(sw); 1657 bounds.extend(ne); 1658 } 1659 placemark = new GGroundOverlay(pdata.overlay.image, overlayBounds); 1660 type = "overlay"; 1661 point = overlayBounds.getCenter(); 1662 } 1663 return { 1664 "placemark": placemark, 1665 "type": type, 1666 "point": point 1667 }; 1668 }; 1669 1670 // create placemark or placemarks 1671 1672 // array of placemark objects 1673 if ("placemarks" in data) { 1674 pdataArr = data.placemarks; 1675 } else { 1676 // we have one or more single placemarks 1677 var types = ["point", "polyline", "polygon", "overlay"]; 1678 for (i=0; i<types.length; i++) { 1679 if (types[i] in data) { 1680 // put in title (only used for markers) 1681 pdata = {title: title}; 1682 pdata[types[i]] = data[types[i]]; 1683 pdataArr.push(pdata); 1684 } 1685 } 1686 } 1687 if (pdataArr) { 1688 for (i=0; i<pdataArr.length; i++) { 1689 // create the placemark 1690 var p = createPlacemark(pdataArr[i]); 1691 // check that the placemark was valid 1692 if (p && p.placemark) { 1693 // take the first point and type as a default 1694 point = point || p.point; 1695 type = type || p.type; 1696 placemark.push(p.placemark); 1697 } 1698 } 1699 } 1700 // override type for arrays 1701 if (placemark.length > 1) { 1702 type = "array"; 1703 } 1704 1705 options.title = title; 1706 options.type = type; 1707 // check for custom infoPoint and convert to GLatLng 1708 if (options.infoPoint) { 1709 options.infoPoint = new GLatLng( 1710 parseFloat(options.infoPoint.lat), 1711 parseFloat(options.infoPoint.lon) 1712 ); 1713 } else { 1714 options.infoPoint = point; 1715 } 1716 1717 // create item and cross-references 1718 var item = new TimeMapItem(placemark, event, ds, options); 1719 // add event if it exists 1720 if (event !== null) { 1721 event.item = item; 1722 // allow for custom event loading 1723 if (!ds.opts.noEventLoad) { 1724 // add event to timeline 1725 ds.eventSource.add(event); 1726 } 1727 } 1728 // add placemark(s) if any exist 1729 if (placemark.length > 0) { 1730 for (i=0; i<placemark.length; i++) { 1731 placemark[i].item = item; 1732 // add listener to make placemark open when event is clicked 1733 GEvent.addListener(placemark[i], "click", function() { 1734 item.openInfoWindow(); 1735 }); 1736 // allow for custom placemark loading 1737 if (!ds.opts.noPlacemarkLoad) { 1738 // add placemark to map 1739 tm.map.addOverlay(placemark[i]); 1740 } 1741 // hide placemarks until the next refresh 1742 placemark[i].hide(); 1743 } 1744 } 1745 // add the item to the dataset 1746 ds.items.push(item); 1747 // return the item object 1748 return item; 1749 }; 1750 1751 /*---------------------------------------------------------------------------- 1752 * TimeMapTheme Class 1753 *---------------------------------------------------------------------------*/ 1754 1755 /** 1756 * @class 1757 * Predefined visual themes for datasets, defining colors and images for 1758 * map markers and timeline events. Note that theme is only used at creation 1759 * time - updating the theme of an existing object won't do anything. 1760 * 1761 * @constructor 1762 * @param {Object} [options] A container for optional arguments 1763 * @param {GIcon} [options.icon=G_DEFAULT_ICON] Icon for marker placemarks. 1764 * @param {String} [options.iconImage=red-dot.png] Icon image for marker placemarks 1765 * (assumes G_MARKER_ICON for the rest of the icon settings) 1766 * @param {String} [options.color=#FE766A] Default color in hex for events, polylines, polygons. 1767 * @param {String} [options.lineColor=color] Color for polylines. 1768 * @param {String} [options.polygonLineColor=lineColor] Color for polygon outlines. 1769 * @param {Number} [options.lineOpacity=1] Opacity for polylines. 1770 * @param {Number} [options.polgonLineOpacity=lineOpacity] Opacity for polygon outlines. 1771 * @param {Number} [options.lineWeight=2] Line weight in pixels for polylines. 1772 * @param {Number} [options.polygonLineWeight=lineWeight] Line weight for polygon outlines. 1773 * @param {String} [options.fillColor=color] Color for polygon fill. 1774 * @param {String} [options.fillOpacity=0.25] Opacity for polygon fill. 1775 * @param {String} [options.eventColor=color] Background color for duration events. 1776 * @param {String} [options.eventTextColor=null] Text color for events (null=Timeline default). 1777 * @param {String} [options.eventIconPath=timemap/images/] Path to instant event icon directory. 1778 * @param {String} [options.eventIconImage=red-circle.gif] Filename of instant event icon image. 1779 * @param {URL} [options.eventIcon=eventIconPath+eventIconImage] URL for instant event icons. 1780 * @param {Boolean} [options.classicTape=false] Whether to use the "classic" style timeline event tape 1781 * (needs additional css to work - see examples/artists.html). 1782 */ 1783 TimeMapTheme = function(options) { 1784 1785 // work out various defaults - the default theme is Google's reddish color 1786 var defaults = { 1787 /** Default color in hex 1788 * @name TimeMapTheme#color 1789 * @type String */ 1790 color: "#FE766A", 1791 /** Opacity for polylines 1792 * @name TimeMapTheme#lineOpacity 1793 * @type Number */ 1794 lineOpacity: 1, 1795 /** Line weight in pixels for polylines 1796 * @name TimeMapTheme#lineWeight 1797 * @type Number */ 1798 lineWeight: 2, 1799 /** Opacity for polygon fill 1800 * @name TimeMapTheme#fillOpacity 1801 * @type Number */ 1802 fillOpacity: 0.25, 1803 /** Text color for duration events 1804 * @name TimeMapTheme#eventTextColor 1805 * @type String */ 1806 eventTextColor: null, 1807 /** Path to instant event icon directory 1808 * @name TimeMapTheme#eventIconPath 1809 * @type String */ 1810 eventIconPath: "timemap/images/", 1811 /** Filename of instant event icon image 1812 * @name TimeMapTheme#eventIconImage 1813 * @type String */ 1814 eventIconImage: "red-circle.png", 1815 /** Whether to use the "classic" style timeline event tape 1816 * @name TimeMapTheme#classicTape 1817 * @type Boolean */ 1818 classicTape: false, 1819 /** Icon image for marker placemarks 1820 * @name TimeMapTheme#iconImage 1821 * @type String */ 1822 iconImage: GIP + "red-dot.png" 1823 }; 1824 1825 // merge defaults with options 1826 var settings = util.merge(options, defaults); 1827 1828 // kill mergeOnly if necessary 1829 delete settings.mergeOnly; 1830 1831 // make default map icon if not supplied 1832 if (!settings.icon) { 1833 // make new red icon 1834 var markerIcon = new GIcon(G_DEFAULT_ICON); 1835 markerIcon.image = settings.iconImage; 1836 markerIcon.iconSize = new GSize(32, 32); 1837 markerIcon.shadow = GIP + "msmarker.shadow.png"; 1838 markerIcon.shadowSize = new GSize(59, 32); 1839 markerIcon.iconAnchor = new GPoint(16, 33); 1840 markerIcon.infoWindowAnchor = new GPoint(18, 3); 1841 /** Marker icon for placemarks 1842 * @name TimeMapTheme#icon 1843 * @type GIcon */ 1844 settings.icon = markerIcon; 1845 } 1846 1847 // cascade some settings as defaults 1848 defaults = { 1849 /** Line color for polylines 1850 * @name TimeMapTheme#lineColor 1851 * @type String */ 1852 lineColor: settings.color, 1853 /** Line color for polygons 1854 * @name TimeMapTheme#polygonLineColor 1855 * @type String */ 1856 polygonLineColor: settings.color, 1857 /** Opacity for polygon outlines 1858 * @name TimeMapTheme#polgonLineOpacity 1859 * @type Number */ 1860 polgonLineOpacity: settings.lineOpacity, 1861 /** Line weight for polygon outlines 1862 * @name TimeMapTheme#polygonLineWeight 1863 * @type Number */ 1864 polygonLineWeight: settings.lineWeight, 1865 /** Fill color for polygons 1866 * @name TimeMapTheme#fillColor 1867 * @type String */ 1868 fillColor: settings.color, 1869 /** Background color for duration events 1870 * @name TimeMapTheme#eventColor 1871 * @type String */ 1872 eventColor: settings.color, 1873 /** Full URL for instant event icons 1874 * @name TimeMapTheme#eventIcon 1875 * @type String */ 1876 eventIcon: settings.eventIconPath + settings.eventIconImage 1877 }; 1878 settings = util.merge(settings, defaults); 1879 1880 // return configured options as theme 1881 return settings; 1882 }; 1883 1884 /** 1885 * Create a theme, based on an optional new or pre-set theme 1886 * 1887 * @param {TimeMapTheme} [theme] Existing theme to clone 1888 * @param {Object} [options] Optional settings to overwrite - see {@link TimeMapTheme} 1889 * @return {TimeMapTheme} Configured theme 1890 */ 1891 TimeMapTheme.create = function(theme, options) { 1892 // test for string matches and missing themes 1893 if (theme) { 1894 theme = TimeMap.util.lookup(theme, TimeMap.themes); 1895 } else { 1896 return new TimeMapTheme(options); 1897 } 1898 1899 // see if we need to clone - guessing fewer keys in options 1900 var clone = false, key; 1901 for (key in options) { 1902 if (theme.hasOwnProperty(key)) { 1903 clone = {}; 1904 break; 1905 } 1906 } 1907 // clone if necessary 1908 if (clone) { 1909 for (key in theme) { 1910 if (theme.hasOwnProperty(key)) { 1911 clone[key] = options[key] || theme[key]; 1912 } 1913 } 1914 // fix event icon path, allowing full image path in options 1915 clone.eventIcon = options.eventIcon || 1916 clone.eventIconPath + clone.eventIconImage; 1917 return clone; 1918 } 1919 else { 1920 return theme; 1921 } 1922 }; 1923 1924 1925 /*---------------------------------------------------------------------------- 1926 * TimeMapItem Class 1927 *---------------------------------------------------------------------------*/ 1928 1929 /** 1930 * @class 1931 * The TimeMapItem object holds references to one or more map placemarks and 1932 * an associated timeline event. 1933 * 1934 * @constructor 1935 * @param {placemark} placemark Placemark or array of placemarks (GMarker, GPolyline, etc) 1936 * @param {Event} event The timeline event 1937 * @param {TimeMapDataset} dataset Reference to the parent dataset object 1938 * @param {Object} [options] A container for optional arguments 1939 * @param {String} [options.title=Untitled] Title of the item 1940 * @param {String} [options.description] Plain-text description of the item 1941 * @param {String} [options.type=none] Type of map placemark used (marker. polyline, polygon) 1942 * @param {GLatLng} [options.infoPoint] Point indicating the center of this item 1943 * @param {String} [options.infoHtml] Full HTML for the info window 1944 * @param {String} [options.infoUrl] URL from which to retrieve full HTML for the info window 1945 * @param {String} [options.infoTemplate] HTML for the info window content, with variable expressions 1946 * (as "{{varname}}" by default) to be replaced by option data 1947 * @param {String} [options.templatePattern=/{{([^}]+)}}/g] 1948 * Regex pattern defining variable syntax in the infoTemplate 1949 * @param {Function} [options.openInfoWindow={@link TimeMapItem.openInfoWindowBasic}] 1950 * Function redefining how info window opens 1951 * @param {Function} [options.closeInfoWindow={@link TimeMapItem.closeInfoWindowBasic}] 1952 * Function redefining how info window closes 1953 * @param {String|TimeMapTheme} [options.theme] Theme applying to this item, overriding dataset theme 1954 * @param {mixed} [options[...]] Any of the options for {@link TimeMapTheme} may be set here 1955 */ 1956 TimeMapItem = function(placemark, event, dataset, options) { 1957 // improve compression 1958 var item = this, 1959 // set defaults for options 1960 defaults = { 1961 type: 'none', 1962 title: 'Untitled', 1963 description: '', 1964 infoPoint: null, 1965 infoHtml: '', 1966 infoUrl: '', 1967 infoTemplate: '<div class="infotitle">{{title}}</div>' + 1968 '<div class="infodescription">{{description}}</div>', 1969 templatePattern: /{{([^}]+)}}/g, 1970 closeInfoWindow: TimeMapItem.closeInfoWindowBasic 1971 }; 1972 1973 /** 1974 * This item's timeline event 1975 * @name TimeMapItem#event 1976 * @type Timeline.Event 1977 */ 1978 item.event = event; 1979 1980 /** 1981 * This item's parent dataset 1982 * @name TimeMapItem#dataset 1983 * @type TimeMapDataset 1984 */ 1985 item.dataset = dataset; 1986 1987 /** 1988 * The timemap's map object 1989 * @name TimeMapItem#map 1990 * @type GMap2 1991 */ 1992 item.map = dataset.timemap.map; 1993 1994 /** 1995 * The timemap's timeline object 1996 * @name TimeMapItem#timeline 1997 * @type Timeline 1998 */ 1999 item.timeline = dataset.timemap.timeline; 2000 2001 // initialize placemark(s) with some type juggling 2002 if (placemark && util.isArray(placemark) && placemark.length === 0) { 2003 placemark = null; 2004 } 2005 if (placemark && placemark.length == 1) { 2006 placemark = placemark[0]; 2007 } 2008 /** 2009 * This item's placemark(s) 2010 * @name TimeMapItem#placemark 2011 * @type GMarker|GPolyline|GPolygon|GOverlay|Array 2012 */ 2013 item.placemark = placemark; 2014 2015 /** 2016 * Container for optional settings passed in through the "options" parameter 2017 * @name TimeMapItem#opts 2018 * @type Object 2019 */ 2020 item.opts = options = util.merge(options, defaults, dataset.opts); 2021 2022 // select default open function 2023 if (!options.openInfoWindow) { 2024 if (options.infoUrl !== "") { 2025 // load via AJAX if URL is provided 2026 options.openInfoWindow = TimeMapItem.openInfoWindowAjax; 2027 } else { 2028 // otherwise default to basic window 2029 options.openInfoWindow = TimeMapItem.openInfoWindowBasic; 2030 } 2031 } 2032 2033 // getter functions 2034 2035 /** 2036 * Return the placemark type for this item 2037 * @name TimeMapItem#getType 2038 * @function 2039 * 2040 * @return {String} Placemark type 2041 */ 2042 item.getType = function() { return item.opts.type; }; 2043 2044 /** 2045 * Return the title for this item 2046 * @name TimeMapItem#getTitle 2047 * @function 2048 * 2049 * @return {String} Item title 2050 */ 2051 item.getTitle = function() { return item.opts.title; }; 2052 2053 /** 2054 * Return the item's "info point" (the anchor for the map info window) 2055 * @name TimeMapItem#getInfoPoint 2056 * @function 2057 * 2058 * @return {GLatLng} Info point 2059 */ 2060 item.getInfoPoint = function() { 2061 // default to map center if placemark not set 2062 return item.opts.infoPoint || item.map.getCenter(); 2063 }; 2064 2065 /** 2066 * Return the start date of the item's event, if any 2067 * @name TimeMapItem#getStart 2068 * @function 2069 * 2070 * @return {Date} Item start date or undefined 2071 */ 2072 item.getStart = function() { 2073 if (item.event) { 2074 return item.event.getStart(); 2075 } 2076 }; 2077 2078 /** 2079 * Return the end date of the item's event, if any 2080 * @name TimeMapItem#getEnd 2081 * @function 2082 * 2083 * @return {Date} Item end dateor undefined 2084 */ 2085 item.getEnd = function() { 2086 if (item.event) { 2087 return item.event.getEnd(); 2088 } 2089 }; 2090 2091 /** 2092 * Return the timestamp of the start date of the item's event, if any 2093 * @name TimeMapItem#getStartTime 2094 * @function 2095 * 2096 * @return {Number} Item start date timestamp or undefined 2097 */ 2098 item.getStartTime = function() { 2099 var start = item.getStart(); 2100 if (start) { 2101 return start.getTime(); 2102 } 2103 }; 2104 2105 /** 2106 * Return the timestamp of the end date of the item's event, if any 2107 * @name TimeMapItem#getEndTime 2108 * @function 2109 * 2110 * @return {Number} Item end date timestamp or undefined 2111 */ 2112 item.getEndTime = function() { 2113 var end = item.getEnd(); 2114 if (end) { 2115 return end.getTime(); 2116 } 2117 }; 2118 2119 /** 2120 * Whether the item is currently selected 2121 * @name TimeMapItem#selected 2122 * @type Boolean 2123 */ 2124 item.selected = false; 2125 2126 /** 2127 * Whether the item is visible 2128 * @name TimeMapItem#visible 2129 * @type Boolean 2130 */ 2131 item.visible = true; 2132 2133 /** 2134 * Whether the item's placemark is visible 2135 * @name TimeMapItem#placemarkVisible 2136 * @type Boolean 2137 */ 2138 item.placemarkVisible = false; 2139 2140 /** 2141 * Whether the item's event is visible 2142 * @name TimeMapItem#eventVisible 2143 * @type Boolean 2144 */ 2145 item.eventVisible = true; 2146 2147 /** 2148 * Open the info window for this item. 2149 * By default this is the map infoWindow, but you can set custom functions 2150 * for whatever behavior you want when the event or placemark is clicked 2151 * @name TimeMapItem#openInfoWindow 2152 * @function 2153 */ 2154 item.openInfoWindow = function() { 2155 options.openInfoWindow.call(item); 2156 item.selected = true; 2157 }; 2158 2159 /** 2160 * Close the info window for this item. 2161 * By default this is the map infoWindow, but you can set custom functions 2162 * for whatever behavior you want. 2163 * @name TimeMapItem#closeInfoWindow 2164 * @function 2165 */ 2166 item.closeInfoWindow = function() { 2167 options.closeInfoWindow.call(item); 2168 item.selected = false; 2169 }; 2170 }; 2171 2172 /** 2173 * Show the map placemark(s) 2174 */ 2175 TimeMapItem.prototype.showPlacemark = function() { 2176 var item = this, i; 2177 if (item.placemark) { 2178 if (item.getType() == "array") { 2179 for (i=0; i<item.placemark.length; i++) { 2180 item.placemark[i].show(); 2181 } 2182 } else { 2183 item.placemark.show(); 2184 } 2185 item.placemarkVisible = true; 2186 } 2187 }; 2188 2189 /** 2190 * Hide the map placemark(s) 2191 */ 2192 TimeMapItem.prototype.hidePlacemark = function() { 2193 var item = this, i; 2194 if (item.placemark) { 2195 if (item.getType() == "array") { 2196 for (i=0; i<item.placemark.length; i++) { 2197 item.placemark[i].hide(); 2198 } 2199 } else { 2200 item.placemark.hide(); 2201 } 2202 item.placemarkVisible = false; 2203 } 2204 item.closeInfoWindow(); 2205 }; 2206 2207 /** 2208 * Show the timeline event. 2209 * NB: Will likely require calling timeline.layout() 2210 */ 2211 TimeMapItem.prototype.showEvent = function() { 2212 if (this.event) { 2213 if (this.eventVisible === false){ 2214 this.dataset.timemap.timeline.getBand(0) 2215 .getEventSource()._events._events.add(this.event); 2216 } 2217 this.eventVisible = true; 2218 } 2219 }; 2220 2221 /** 2222 * Hide the timeline event. 2223 * NB: Will likely require calling timeline.layout(), 2224 * AND calling eventSource._events._index() (ugh) 2225 */ 2226 TimeMapItem.prototype.hideEvent = function() { 2227 if (this.event) { 2228 if (this.eventVisible){ 2229 this.dataset.timemap.timeline.getBand(0) 2230 .getEventSource()._events._events.remove(this.event); 2231 } 2232 this.eventVisible = false; 2233 } 2234 }; 2235 2236 /** 2237 * Standard open info window function, using static text in map window 2238 */ 2239 TimeMapItem.openInfoWindowBasic = function() { 2240 var item = this, 2241 opts = item.opts, 2242 html = opts.infoHtml, 2243 match; 2244 // create content for info window if none is provided 2245 if (!html) { 2246 // fill in template 2247 html = opts.infoTemplate; 2248 match = opts.templatePattern.exec(html); 2249 while (match) { 2250 html = html.replace(match[0], opts[match[1]]); 2251 match = opts.templatePattern.exec(html); 2252 } 2253 } 2254 // scroll timeline if necessary 2255 if (item.placemark && !item.placemarkVisible && item.event) { 2256 item.dataset.timemap.scrollToDate(item.event.getStart()); 2257 } 2258 // open window 2259 if (item.getType() == "marker") { 2260 item.placemark.openInfoWindowHtml(html); 2261 } else { 2262 item.map.openInfoWindowHtml(item.getInfoPoint(), html); 2263 } 2264 // deselect when window is closed 2265 item.closeListener = GEvent.addListener(item.map, "infowindowclose", function() { 2266 // deselect 2267 item.selected = false; 2268 // kill self 2269 GEvent.removeListener(item.closeListener); 2270 }); 2271 }; 2272 2273 /** 2274 * Open info window function using ajax-loaded text in map window 2275 */ 2276 TimeMapItem.openInfoWindowAjax = function() { 2277 var item = this; 2278 if (!item.opts.infoHtml) { // load content via AJAX 2279 if (item.opts.infoUrl) { 2280 GDownloadUrl(item.opts.infoUrl, function(result) { 2281 item.opts.infoHtml = result; 2282 item.openInfoWindow(); 2283 }); 2284 return; 2285 } 2286 } 2287 // fall back on basic function if content is loaded or URL is missing 2288 item.openInfoWindow = function() { 2289 TimeMapItem.openInfoWindowBasic.call(item); 2290 item.selected = true; 2291 }; 2292 item.openInfoWindow(); 2293 }; 2294 2295 /** 2296 * Standard close window function, using the map window 2297 */ 2298 TimeMapItem.closeInfoWindowBasic = function() { 2299 if (this.getType() == "marker") { 2300 this.placemark.closeInfoWindow(); 2301 } else { 2302 var infoWindow = this.map.getInfoWindow(); 2303 // close info window if its point is the same as this item's point 2304 if (infoWindow.getPoint() == this.getInfoPoint() && !infoWindow.isHidden()) { 2305 this.map.closeInfoWindow(); 2306 } 2307 } 2308 }; 2309 2310 /*---------------------------------------------------------------------------- 2311 * Utility functions 2312 *---------------------------------------------------------------------------*/ 2313 2314 /** 2315 * Convenience trim function 2316 * 2317 * @param {String} str String to trim 2318 * @return {String} Trimmed string 2319 */ 2320 TimeMap.util.trim = function(str) { 2321 str = str && String(str) || ''; 2322 return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); 2323 }; 2324 2325 /** 2326 * Convenience array tester 2327 * 2328 * @param {Object} o Object to test 2329 * @return {Boolean} Whether the object is an array 2330 */ 2331 TimeMap.util.isArray = function(o) { 2332 return o && !(o.propertyIsEnumerable('length')) && 2333 typeof o === 'object' && typeof o.length === 'number'; 2334 }; 2335 2336 /** 2337 * Get XML tag value as a string 2338 * 2339 * @param {XML Node} n Node in which to look for tag 2340 * @param {String} tag Name of tag to look for 2341 * @param {String} [ns] XML namespace to look in 2342 * @return {String} Tag value as string 2343 */ 2344 TimeMap.util.getTagValue = function(n, tag, ns) { 2345 var str = "", 2346 nList = TimeMap.util.getNodeList(n, tag, ns); 2347 if (nList.length > 0) { 2348 n = nList[0].firstChild; 2349 // fix for extra-long nodes 2350 // see http://code.google.com/p/timemap/issues/detail?id=36 2351 while(n !== null) { 2352 str += n.nodeValue; 2353 n = n.nextSibling; 2354 } 2355 } 2356 return str; 2357 }; 2358 2359 /** 2360 * Empty container for mapping XML namespaces to URLs 2361 * @example 2362 TimeMap.util.nsMap['georss'] = 'http://www.georss.org/georss'; 2363 // find georss:point 2364 TimeMap.util.getNodeList(node, 'point', 'georss') 2365 */ 2366 TimeMap.util.nsMap = {}; 2367 2368 /** 2369 * Cross-browser implementation of getElementsByTagNameNS. 2370 * Note: Expects any applicable namespaces to be mapped in 2371 * {@link TimeMap.util.nsMap}. 2372 * 2373 * @param {XML Node} n Node in which to look for tag 2374 * @param {String} tag Name of tag to look for 2375 * @param {String} [ns] XML namespace to look in 2376 * @return {XML Node List} List of nodes with the specified tag name 2377 */ 2378 TimeMap.util.getNodeList = function(n, tag, ns) { 2379 var nsMap = TimeMap.util.nsMap; 2380 if (ns === undefined) { 2381 // no namespace 2382 return n.getElementsByTagName(tag); 2383 } 2384 if (n.getElementsByTagNameNS && nsMap[ns]) { 2385 // function and namespace both exist 2386 return n.getElementsByTagNameNS(nsMap[ns], tag); 2387 } 2388 // no function, try the colon tag name 2389 return n.getElementsByTagName(ns + ':' + tag); 2390 }; 2391 2392 /** 2393 * Make TimeMap.init()-style points from a GLatLng, array, or string 2394 * 2395 * @param {Object} coords GLatLng, array, or string to convert 2396 * @param {Boolean} [reversed] Whether the points are KML-style lon/lat, rather than lat/lon 2397 * @return {Object} TimeMap.init()-style point 2398 */ 2399 TimeMap.util.makePoint = function(coords, reversed) { 2400 var latlon = null, 2401 trim = TimeMap.util.trim; 2402 // GLatLng 2403 if (coords.lat && coords.lng) { 2404 latlon = [coords.lat(), coords.lng()]; 2405 } 2406 // array of coordinates 2407 if (TimeMap.util.isArray(coords)) { 2408 latlon = coords; 2409 } 2410 // string 2411 if (!latlon) { 2412 // trim extra whitespace 2413 coords = trim(coords); 2414 if (coords.indexOf(',') > -1) { 2415 // split on commas 2416 latlon = coords.split(","); 2417 } else { 2418 // split on whitespace 2419 latlon = coords.split(/[\r\n\f ]+/); 2420 } 2421 } 2422 // deal with extra coordinates (i.e. KML altitude) 2423 if (latlon.length > 2) { 2424 latlon = latlon.slice(0, 2); 2425 } 2426 // deal with backwards (i.e. KML-style) coordinates 2427 if (reversed) { 2428 latlon.reverse(); 2429 } 2430 return { 2431 "lat": trim(latlon[0]), 2432 "lon": trim(latlon[1]) 2433 }; 2434 }; 2435 2436 /** 2437 * Make TimeMap.init()-style polyline/polygons from a whitespace-delimited 2438 * string of coordinates (such as those in GeoRSS and KML). 2439 * 2440 * @param {Object} coords String to convert 2441 * @param {Boolean} [reversed] Whether the points are KML-style lon/lat, rather than lat/lon 2442 * @return {Object} Formated coordinate array 2443 */ 2444 TimeMap.util.makePoly = function(coords, reversed) { 2445 var poly = [], 2446 latlon, 2447 coordArr = TimeMap.util.trim(coords).split(/[\r\n\f ]+/); 2448 if (coordArr.length === 0) return []; 2449 // loop through coordinates 2450 for (var x=0; x<coordArr.length; x++) { 2451 latlon = (coordArr[x].indexOf(',') > 0) ? 2452 // comma-separated coordinates (KML-style lon/lat) 2453 coordArr[x].split(",") : 2454 // space-separated coordinates - increment to step by 2s 2455 [coordArr[x], coordArr[++x]]; 2456 // deal with extra coordinates (i.e. KML altitude) 2457 if (latlon.length > 2) { 2458 latlon = latlon.slice(0, 2); 2459 } 2460 // deal with backwards (i.e. KML-style) coordinates 2461 if (reversed) { 2462 latlon.reverse(); 2463 } 2464 poly.push({ 2465 "lat": latlon[0], 2466 "lon": latlon[1] 2467 }); 2468 } 2469 return poly; 2470 } 2471 2472 /** 2473 * Format a date as an ISO 8601 string 2474 * 2475 * @param {Date} d Date to format 2476 * @param {Number} [precision] Precision indicator:<pre> 2477 * 3 (default): Show full date and time 2478 * 2: Show full date and time, omitting seconds 2479 * 1: Show date only 2480 *</pre> 2481 * @return {String} Formatted string 2482 */ 2483 TimeMap.util.formatDate = function(d, precision) { 2484 // default to high precision 2485 precision = precision || 3; 2486 var str = ""; 2487 if (d) { 2488 var yyyy = d.getUTCFullYear(), 2489 mo = d.getUTCMonth(), 2490 dd = d.getUTCDate(); 2491 // deal with early dates 2492 if (yyyy < 1000) { 2493 return (yyyy < 1 ? (yyyy * -1 + "BC") : yyyy + ""); 2494 } 2495 // check for date.js support 2496 if (d.toISOString && precision == 3) { 2497 return d.toISOString(); 2498 } 2499 // otherwise, build ISO 8601 string 2500 var pad = function(num) { 2501 return ((num < 10) ? "0" : "") + num; 2502 }; 2503 str += yyyy + '-' + pad(mo + 1 ) + '-' + pad(dd); 2504 // show time if top interval less than a week 2505 if (precision > 1) { 2506 var hh = d.getUTCHours(), 2507 mm = d.getUTCMinutes(), 2508 ss = d.getUTCSeconds(); 2509 str += 'T' + pad(hh) + ':' + pad(mm); 2510 // show seconds if the interval is less than a day 2511 if (precision > 2) { 2512 str += pad(ss); 2513 } 2514 str += 'Z'; 2515 } 2516 } 2517 return str; 2518 }; 2519 2520 /** 2521 * Determine the SIMILE Timeline version. 2522 * 2523 * @return {String} At the moment, only "1.2", "2.2.0", or what Timeline provides 2524 */ 2525 TimeMap.util.TimelineVersion = function() { 2526 // check for Timeline.version support - added in 2.3.0 2527 if (Timeline.version) { 2528 return Timeline.version; 2529 } 2530 if (Timeline.DurationEventPainter) { 2531 return "1.2"; 2532 } else { 2533 return "2.2.0"; 2534 } 2535 }; 2536 2537 2538 /** 2539 * Identify the placemark type. 2540 * XXX: Not 100% happy with this implementation, which relies heavily on duck-typing. 2541 * 2542 * @param {Object} pm Placemark to identify 2543 * @return {String} Type of placemark, or false if none found 2544 */ 2545 TimeMap.util.getPlacemarkType = function(pm) { 2546 return 'getIcon' in pm ? 'marker' : 2547 'getVertex' in pm ? 2548 ('setFillStyle' in pm ? 'polygon' : 'polyline') : 2549 false; 2550 }; 2551 2552 /** 2553 * Merge two or more objects, giving precendence to those 2554 * first in the list (i.e. don't overwrite existing keys). 2555 * Original objects will not be modified. 2556 * 2557 * @param {Object} obj1 Base object 2558 * @param {Object} [objN] Objects to merge into base 2559 * @return {Object} Merged object 2560 */ 2561 TimeMap.util.merge = function() { 2562 var opts = {}, args = arguments, obj, key, x, y; 2563 // must... make... subroutine... 2564 var mergeKey = function(o1, o2, key) { 2565 // note: existing keys w/undefined values will be overwritten 2566 if (o1.hasOwnProperty(key) && o2[key] === undefined) { 2567 o2[key] = o1[key]; 2568 } 2569 }; 2570 for (x=0; x<args.length; x++) { 2571 obj = args[x]; 2572 if (obj) { 2573 // allow non-base objects to constrain what will be merged 2574 if (x > 0 && 'mergeOnly' in obj) { 2575 for (y=0; y<obj.mergeOnly.length; y++) { 2576 key = obj.mergeOnly[y]; 2577 mergeKey(obj, opts, key); 2578 } 2579 } 2580 // otherwise, just merge everything 2581 else { 2582 for (key in obj) { 2583 mergeKey(obj, opts, key); 2584 } 2585 } 2586 } 2587 } 2588 return opts; 2589 }; 2590 2591 /** 2592 * Attempt look up a key in an object, returning either the value, 2593 * undefined if the key is a string but not found, or the key if not a string 2594 * 2595 * @param {String|Object} key Key to look up 2596 * @param {Object} map Object in which to look 2597 * @return {Object} Value, undefined, or key 2598 */ 2599 TimeMap.util.lookup = function(key, map) { 2600 if (typeof(key) == 'string') { 2601 return map[key]; 2602 } 2603 else { 2604 return key; 2605 } 2606 }; 2607 2608 2609 /*---------------------------------------------------------------------------- 2610 * Lookup maps 2611 * (need to be at end because some call util functions on initialization) 2612 *---------------------------------------------------------------------------*/ 2613 2614 /** 2615 * @namespace 2616 * Lookup map of common timeline intervals. 2617 * Add custom intervals here if you want to refer to them by key rather 2618 * than as a function name. 2619 * @example 2620 TimeMap.init({ 2621 bandIntervals: "hr", 2622 // etc... 2623 }); 2624 * 2625 */ 2626 TimeMap.intervals = { 2627 /** second / minute */ 2628 sec: [DateTime.SECOND, DateTime.MINUTE], 2629 /** minute / hour */ 2630 min: [DateTime.MINUTE, DateTime.HOUR], 2631 /** hour / day */ 2632 hr: [DateTime.HOUR, DateTime.DAY], 2633 /** day / week */ 2634 day: [DateTime.DAY, DateTime.WEEK], 2635 /** week / month */ 2636 wk: [DateTime.WEEK, DateTime.MONTH], 2637 /** month / year */ 2638 mon: [DateTime.MONTH, DateTime.YEAR], 2639 /** year / decade */ 2640 yr: [DateTime.YEAR, DateTime.DECADE], 2641 /** decade / century */ 2642 dec: [DateTime.DECADE, DateTime.CENTURY] 2643 }; 2644 2645 /** 2646 * @namespace 2647 * Lookup map of Google map types. You could add 2648 * G_MOON_VISIBLE_MAP, G_SKY_VISIBLE_MAP, or G_MARS_VISIBLE_MAP 2649 * if you really needed them. 2650 * @example 2651 TimeMap.init({ 2652 options: { 2653 mapType: "satellite" 2654 }, 2655 // etc... 2656 }); 2657 */ 2658 TimeMap.mapTypes = { 2659 /** Normal map */ 2660 normal: G_NORMAL_MAP, 2661 /** Satellite map */ 2662 satellite: G_SATELLITE_MAP, 2663 /** Hybrid map */ 2664 hybrid: G_HYBRID_MAP, 2665 /** Physical (terrain) map */ 2666 physical: G_PHYSICAL_MAP 2667 }; 2668 2669 /** 2670 * @namespace 2671 * Lookup map of supported date parser functions. 2672 * Add custom date parsers here if you want to refer to them by key rather 2673 * than as a function name. 2674 * @example 2675 TimeMap.init({ 2676 datasets: [ 2677 { 2678 options: { 2679 dateParser: "gregorian" 2680 }, 2681 // etc... 2682 } 2683 ], 2684 // etc... 2685 }); 2686 */ 2687 TimeMap.dateParsers = { 2688 /** Hybrid parser: see {@link TimeMapDataset.hybridParser} */ 2689 hybrid: TimeMapDataset.hybridParser, 2690 /** ISO8601 parser: parse ISO8601 datetime strings */ 2691 iso8601: DateTime.parseIso8601DateTime, 2692 /** Gregorian parser: see {@link TimeMapDataset.gregorianParser} */ 2693 gregorian: TimeMapDataset.gregorianParser 2694 }; 2695 2696 /** 2697 * @namespace 2698 * Pre-set event/placemark themes in a variety of colors. 2699 * Add custom themes here if you want to refer to them by key rather 2700 * than as a function name. 2701 * @example 2702 TimeMap.init({ 2703 options: { 2704 theme: "orange" 2705 }, 2706 datasets: [ 2707 { 2708 options: { 2709 theme: "yellow" 2710 }, 2711 // etc... 2712 } 2713 ], 2714 // etc... 2715 }); 2716 */ 2717 TimeMap.themes = { 2718 2719 /** 2720 * Red theme: <span style="background:#FE766A">#FE766A</span> 2721 * This is the default. 2722 * 2723 * @type TimeMapTheme 2724 */ 2725 red: new TimeMapTheme(), 2726 2727 /** 2728 * Blue theme: <span style="background:#5A7ACF">#5A7ACF</span> 2729 * 2730 * @type TimeMapTheme 2731 */ 2732 blue: new TimeMapTheme({ 2733 iconImage: GIP + "blue-dot.png", 2734 color: "#5A7ACF", 2735 eventIconImage: "blue-circle.png" 2736 }), 2737 2738 /** 2739 * Green theme: <span style="background:#19CF54">#19CF54</span> 2740 * 2741 * @type TimeMapTheme 2742 */ 2743 green: new TimeMapTheme({ 2744 iconImage: GIP + "green-dot.png", 2745 color: "#19CF54", 2746 eventIconImage: "green-circle.png" 2747 }), 2748 2749 /** 2750 * Light blue theme: <span style="background:#5ACFCF">#5ACFCF</span> 2751 * 2752 * @type TimeMapTheme 2753 */ 2754 ltblue: new TimeMapTheme({ 2755 iconImage: GIP + "ltblue-dot.png", 2756 color: "#5ACFCF", 2757 eventIconImage: "ltblue-circle.png" 2758 }), 2759 2760 /** 2761 * Purple theme: <span style="background:#8E67FD">#8E67FD</span> 2762 * 2763 * @type TimeMapTheme 2764 */ 2765 purple: new TimeMapTheme({ 2766 iconImage: GIP + "purple-dot.png", 2767 color: "#8E67FD", 2768 eventIconImage: "purple-circle.png" 2769 }), 2770 2771 /** 2772 * Orange theme: <span style="background:#FF9900">#FF9900</span> 2773 * 2774 * @type TimeMapTheme 2775 */ 2776 orange: new TimeMapTheme({ 2777 iconImage: GIP + "orange-dot.png", 2778 color: "#FF9900", 2779 eventIconImage: "orange-circle.png" 2780 }), 2781 2782 /** 2783 * Yellow theme: <span style="background:#FF9900">#ECE64A</span> 2784 * 2785 * @type TimeMapTheme 2786 */ 2787 yellow: new TimeMapTheme({ 2788 iconImage: GIP + "yellow-dot.png", 2789 color: "#ECE64A", 2790 eventIconImage: "yellow-circle.png" 2791 }), 2792 2793 /** 2794 * Pink theme: <span style="background:#E14E9D">#E14E9D</span> 2795 * 2796 * @type TimeMapTheme 2797 */ 2798 pink: new TimeMapTheme({ 2799 iconImage: GIP + "pink-dot.png", 2800 color: "#E14E9D", 2801 eventIconImage: "pink-circle.png" 2802 }) 2803 }; 2804 2805 // save to window 2806 window.TimeMap = TimeMap; 2807 window.TimeMapFilterChain = TimeMapFilterChain; 2808 window.TimeMapDataset = TimeMapDataset; 2809 window.TimeMapTheme = TimeMapTheme; 2810 window.TimeMapItem = TimeMapItem; 2811 2812 })(); 2813