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 })();