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