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