source: oceandb/jQuery_Prototype/script/timemap.js @ 122

Last change on this file since 122 was 122, checked in by rider, 15 years ago

KML file loaded & Timeline revised

File size: 68.4 KB
Line 
1/*!
2 * Timemap.js Copyright 2008 Nick Rabinowitz.
3 * Licensed under the MIT License (see LICENSE.txt)
4 */
5
6/**
7 * @overview
8 * Timemap.js is intended to sync a SIMILE Timeline with a Google Map.
9 * Dependencies: Google Maps API v2, SIMILE Timeline v1.2 - 2.3.1
10 * Thanks to Jorn Clausen (http://www.oe-files.de) for initial concept and code.
11 *
12 * @name timemap.js
13 * @author Nick Rabinowitz (www.nickrabinowitz.com)
14 * @version 1.6pre
15 */
16
17// globals - for JSLint
18/*global GBrowserIsCompatible, GLargeMapControl, GMap2, GIcon       */ 
19/*global GMapTypeControl, GDownloadUrl, GGroundOverlay              */
20/*global GMarker, GPolygon, GPolyline, GSize, G_DEFAULT_ICON        */
21/*global G_HYBRID_MAP, G_MOON_VISIBLE_MAP, G_SKY_VISIBLE_MAP        */
22
23(function(){
24
25// borrowing some space-saving devices from jquery
26var 
27  // Will speed up references to window, and allows munging its name.
28  window = this,
29  // Will speed up references to undefined, and allows munging its name.
30  undefined,
31    // aliases for Timeline objects
32    Timeline = window.Timeline, DateTime = Timeline.DateTime, 
33    // aliases for Google variables (anything that gets used more than once)
34    G_DEFAULT_MAP_TYPES = window.G_DEFAULT_MAP_TYPES, 
35    G_NORMAL_MAP = window.G_NORMAL_MAP, 
36    G_PHYSICAL_MAP = window.G_PHYSICAL_MAP, 
37    G_SATELLITE_MAP = window.G_SATELLITE_MAP, 
38    GLatLng = window.GLatLng, 
39    GLatLngBounds = window.GLatLngBounds, 
40    GEvent = window.GEvent,
41    // Google icon path
42    GIP = "http://www.google.com/intl/en_us/mapfiles/ms/icons/",
43    // aliases for class names, allowing munging
44    TimeMap, TimeMapDataset, TimeMapTheme, TimeMapItem;
45
46/*----------------------------------------------------------------------------
47 * TimeMap Class
48 *---------------------------------------------------------------------------*/
49 
50/**
51 * @class
52 * The TimeMap object holds references to timeline, map, and datasets.
53 * This will create the visible map, but not the timeline, which must be initialized separately.
54 *
55 * @constructor
56 * @param {element} tElement     The timeline element.
57 * @param {element} mElement     The map element.
58 * @param {Object} [options]       A container for optional arguments:<pre>
59 *   {Boolean} syncBands            Whether to synchronize all bands in timeline
60 *   {GLatLng} mapCenter            Point for map center
61 *   {Number} mapZoom               Intial map zoom level
62 *   {GMapType/String} mapType      The maptype for the map
63 *   {Array} mapTypes               The set of maptypes available for the map
64 *   {Function/String} mapFilter    How to hide/show map items depending on timeline state;
65                                    options: "hidePastFuture", "showMomentOnly", or function
66 *   {Boolean} showMapTypeCtrl      Whether to display the map type control
67 *   {Boolean} showMapCtrl          Whether to show map navigation control
68 *   {Boolean} centerMapOnItems     Whether to center and zoom the map based on loaded item positions
69 *   {Function} openInfoWindow      Function redefining how info window opens
70 *   {Function} closeInfoWindow     Function redefining how info window closes
71 * </pre>
72 */
73TimeMap = function(tElement, mElement, options) {
74   
75    // save DOM elements
76    /**
77     * Map element
78     * @type DOM Element
79     */
80    this.mElement = mElement;
81    /**
82     * Timeline element
83     * @type DOM Element
84     */
85    this.tElement = tElement;
86   
87    /**
88     * Map of datasets
89     * @type Object
90     */
91    this.datasets = {};
92    /**
93     * Filter chains for this timemap
94     * @type Object
95     */
96    this.chains = {};
97    /**
98     * Bounds of the map
99     * @type GLatLngBounds
100     */
101    this.mapBounds = new GLatLngBounds();
102   
103    // set defaults for options
104    var defaults = {
105        mapCenter:          new GLatLng(0,0),
106        mapZoom:            0,
107        mapType:            G_PHYSICAL_MAP,
108        mapTypes:           [G_NORMAL_MAP, G_SATELLITE_MAP, G_PHYSICAL_MAP],
109        showMapTypeCtrl:    true,
110        showMapCtrl:        true,
111        syncBands:          true,
112        mapFilter:          'hidePastFuture',
113        centerOnItems:      true,
114        theme:              'red'
115    };
116   
117    /**
118     * Container for optional settings passed in the "options" parameter
119     * @type Object
120     */
121    this.opts = options = util.merge(options, defaults);
122   
123    // only these options will cascade to datasets and items
124    options.mergeOnly = ['mergeOnly', 'theme', 'eventIconPath', 'openInfoWindow', 
125                         'closeInfoWindow', 'noPlacemarkLoad', 'noEventLoad']
126   
127    // allow map types to be specified by key
128    options.mapType = util.lookup(options.mapType, TimeMap.mapTypes);
129    // allow map filters to be specified by key
130    options.mapFilter = util.lookup(options.mapFilter, TimeMap.filters);
131    // allow theme options to be specified in options
132    options.theme = TimeMapTheme.create(options.theme, options);
133   
134    // initialize map
135    this.initMap();
136};
137
138/**
139 * Initialize the map.
140 */
141TimeMap.prototype.initMap = function() {
142    var options = this.opts, map, i;
143    if (GBrowserIsCompatible()) {
144   
145        /**
146         * The associated GMap object
147         * @type GMap2
148         */
149        this.map = map = new GMap2(this.mElement);
150       
151        // set controls
152        if (options.showMapCtrl) {
153            map.addControl(new GLargeMapControl());
154        }
155        if (options.showMapTypeCtrl) {
156            map.addControl(new GMapTypeControl());
157        }
158       
159        // drop all existing types
160        for (i=G_DEFAULT_MAP_TYPES.length-1; i>0; i--) {
161            map.removeMapType(G_DEFAULT_MAP_TYPES[i]);
162        }
163        // you can't remove the last maptype, so add a new one first
164        map.addMapType(options.mapTypes[0]);
165        map.removeMapType(G_DEFAULT_MAP_TYPES[0]);
166        // add the rest of the new types
167        for (i=1; i<options.mapTypes.length; i++) {
168            map.addMapType(options.mapTypes[i]);
169        }
170        // set basic parameters
171        map.enableDoubleClickZoom();
172        map.enableScrollWheelZoom();
173        map.enableContinuousZoom();
174        // initialize map center and zoom
175        map.setCenter(options.mapCenter, options.mapZoom);
176        // must be called after setCenter, for reasons unclear
177        map.setMapType(options.mapType);
178    }
179};
180
181/**
182 * Current library version.
183 * @type String
184 */
185TimeMap.version = "1.6pre";
186
187/**
188 * @name TimeMap.util
189 * @namespace
190 * Namespace for TimeMap utility functions.
191 */
192var util = TimeMap.util = {};
193
194/**
195 * Intializes a TimeMap.
196 *
197 * <p>This is an attempt to create a general initialization script that will
198 * work in most cases. If you need a more complex initialization, write your
199 * own script instead of using this one.</p>
200 *
201 * <p>The idea here is to throw all of the standard intialization settings into
202 * a large object and then pass it to the TimeMap.init() function. The full
203 * data format is outlined below, but if you leave elements off the script
204 * will use default settings instead.</p>
205 *
206 * <p>Call TimeMap.init() inside of an onLoad() function (or a jQuery
207 * $.(document).ready() function, or whatever you prefer). See the examples
208 * for usage.</p>
209 *
210 * @param {Object} config   Full set of configuration options.
211 *                          See examples/timemapinit_usage.js for format.
212 * @return {TimeMap}        The initialized TimeMap object, for future reference
213 */
214TimeMap.init = function(config) {
215   
216    // check required elements
217    var err = "TimeMap.init: No id for ";
218    if (!('mapId' in config) || !config.mapId) {
219        throw err + "map";
220    }
221    if (!('timelineId' in config) || !config.timelineId) {
222        throw err + "timeline";
223    }
224   
225    // set defaults
226    var defaults = {
227        options:        {},
228        datasets:       [],
229        bands:          false,
230        bandInfo:       false,
231        bandIntervals:  "wk",
232        scrollTo:       "earliest"
233    };
234    // merge options and defaults
235    config = util.merge(config, defaults);
236
237    if (!config.bandInfo && !config.bands) {
238        // allow intervals to be specified by key
239        var intervals = util.lookup(config.bandIntervals, TimeMap.intervals);
240        // make default band info
241        config.bandInfo = [
242        {
243                width:          "80%", 
244                intervalUnit:   intervals[0], 
245                intervalPixels: 70
246            },
247            {
248                width:          "20%", 
249                intervalUnit:   intervals[1], 
250                intervalPixels: 100,
251                showEventText:  false,
252                overview:       true,
253                trackHeight:    0.4,
254                trackGap:       0.2
255            }
256        ];
257    }
258   
259    // create the TimeMap object
260    var tm = new TimeMap(
261      document.getElementById(config.timelineId), 
262    document.getElementById(config.mapId),
263    config.options);
264   
265    // create the dataset objects
266    var datasets = [], x, ds, dsOptions, topOptions, dsId;
267    for (x=0; x < config.datasets.length; x++) {
268        ds = config.datasets[x];
269        // put top-level data into options
270        topOptions = {
271            title: ds.title,
272            theme: ds.theme,
273            dateParser: ds.dateParser
274        };
275        dsOptions = util.merge(ds.options, topOptions);
276        dsId = ds.id || "ds" + x;
277        datasets[x] = tm.createDataset(dsId, dsOptions);
278        if (x > 0) {
279            // set all to the same eventSource
280            datasets[x].eventSource = datasets[0].eventSource;
281        }
282    }
283    // add a pointer to the eventSource in the TimeMap
284    tm.eventSource = datasets[0].eventSource;
285   
286    // set up timeline bands
287    var bands = [];
288    // ensure there's at least an empty eventSource
289    var eventSource = (datasets[0] && datasets[0].eventSource) || new Timeline.DefaultEventSource();
290    // check for pre-initialized bands (manually created with Timeline.createBandInfo())
291    if (config.bands) {
292        bands = config.bands;
293        // substitute dataset event source
294        for (x=0; x < bands.length; x++) {
295            // assume that these have been set up like "normal" Timeline bands:
296            // with an empty event source if events are desired, and null otherwise
297            if (bands[x].eventSource !== null) {
298                bands[x].eventSource = eventSource;
299            }
300        }
301    }
302    // otherwise, make bands from band info
303    else {
304        for (x=0; x < config.bandInfo.length; x++) {
305            var bandInfo = config.bandInfo[x];
306            // if eventSource is explicitly set to null or false, ignore
307            if (!(('eventSource' in bandInfo) && !bandInfo.eventSource)) {
308                bandInfo.eventSource = eventSource;
309            }
310            else {
311                bandInfo.eventSource = null;
312            }
313            bands[x] = Timeline.createBandInfo(bandInfo);
314            if (x > 0 && util.TimelineVersion() == "1.2") {
315                // set all to the same layout
316                bands[x].eventPainter.setLayout(bands[0].eventPainter.getLayout()); 
317            }
318        }
319    }
320    // initialize timeline
321    tm.initTimeline(bands);
322   
323    // initialize load manager
324    var loadManager = TimeMap.loadManager;
325    loadManager.init(tm, config.datasets.length, config);
326   
327    // load data!
328    for (x=0; x < config.datasets.length; x++) {
329        (function(x) { // deal with closure issues
330            var data = config.datasets[x], options, type, callback, loaderClass, loader;
331            // support some older syntax
332            options = data.data || data.options || {};
333            type = data.type || options.type;
334            callback = function() { loadManager.increment(); };
335            // get loader class
336            loaderClass = (typeof(type) == 'string') ? TimeMap.loaders[type] : type;
337            // load with appropriate loader
338            loader = new loaderClass(options);
339            loader.load(datasets[x], callback);
340        })(x);
341    }
342    // return timemap object for later manipulation
343    return tm;
344};
345
346// for backwards compatibility
347var timemapInit = TimeMap.init;
348
349/**
350 * @class Static singleton for managing multiple asynchronous loads
351 */
352TimeMap.loadManager = new function() {
353   
354    /**
355     * Initialize (or reset) the load manager
356     *
357     * @param {TimeMap} tm          TimeMap instance
358     * @param {int} target     Number of datasets we're loading
359     * @param {Object} options      Container for optional settings:<pre>
360     *   {Function} dataLoadedFunction      Custom function replacing default completion function;
361     *                                      should take one parameter, the TimeMap object
362     *   {String/Date} scrollTo             Where to scroll the timeline when load is complete
363     *                                      Options: "earliest", "latest", "now", date string, Date
364     *   {Function} dataDisplayedFunction   Custom function to fire once data is loaded and displayed;
365     *                                      should take one parameter, the TimeMap object
366     * </pre>
367     */
368    this.init = function(tm, target, config) {
369        this.count = 0;
370        this.tm = tm;
371        this.target = target;
372        this.opts = config || {};
373    };
374   
375    /**
376     * Increment the count of loaded datasets
377     */
378    this.increment = function() {
379        this.count++;
380        if (this.count >= this.target) {
381            this.complete();
382        }
383    };
384   
385    /**
386     * Function to fire when all loads are complete.
387     * Default behavior is to scroll to a given date (if provided) and
388     * layout the timeline.
389     */
390    this.complete = function() {
391        var tm = this.tm;
392        // custom function including timeline scrolling and layout
393        var func = this.opts.dataLoadedFunction;
394        if (func) {
395            func(tm);
396        } else {
397            var d = new Date();
398            var eventSource = this.tm.eventSource;
399            var scrollTo = this.opts.scrollTo;
400            // make sure there are events to scroll to
401            if (scrollTo && eventSource.getCount() > 0) {
402                switch (scrollTo) {
403                    case "now":
404                        break;
405                    case "earliest":
406                        d = eventSource.getEarliestDate();
407                        break;
408                    case "latest":
409                        d = eventSource.getLatestDate();
410                        break;
411                    default:
412                        // assume it's a date, try to parse
413                        if (typeof(scrollTo) == 'string') {
414                            scrollTo = TimeMapDataset.hybridParser(scrollTo);
415                        }
416                        // either the parse worked, or it was a date to begin with
417                        if (scrollTo.constructor == Date) {
418                            d = scrollTo;
419                        }
420                }
421                tm.timeline.getBand(0).setCenterVisibleDate(d);
422            }
423            tm.timeline.layout();
424            // custom function to be called when data is loaded
425            func = this.opts.dataDisplayedFunction;
426            if (func) {
427                func(tm);
428            }
429        }
430    };
431};
432
433/**
434 * @namespace
435 * Namespace for different data loader functions.
436 * New loaders should add their factories or constructors to this object; loader
437 * functions are passed an object with parameters in TimeMap.init().
438 */
439TimeMap.loaders = {};
440
441/**
442 * @class
443 * Basic loader class, for pre-loaded data.
444 * Other types of loaders should take the same parameter.
445 *
446 * @constructor
447 * @param {Object} options          All options for the loader:<pre>
448 *   {Array} data                       Array of items to load
449 *   {Function} preloadFunction         Function to call on data before loading
450 *   {Function} transformFunction       Function to call on individual items before loading
451 * </pre>
452 */
453TimeMap.loaders.basic = function(options) {
454    // get standard functions and document
455    TimeMap.loaders.mixin(this, options);
456    /**
457     * Function to call on data object before loading
458     * @name TimeMap.loaders.basic#preload
459     * @function
460     * @parameter {Object} data     Data to preload
461     * @return {Object[]} data      Array of item data
462     */
463     
464    /**
465     * Function to call on a single item data object before loading
466     * @name TimeMap.loaders.basic#transform
467     * @function
468     * @parameter {Object} data     Data to transform
469     * @return {Object} data        Transformed data for one item
470     */
471   
472    /**
473     * Array of item data to load.
474     * @type Object[]
475     */
476    this.data = options.items || 
477        // allow "value" for backwards compatibility
478        options.value || [];
479};
480
481/**
482 * New loaders should implement a load function with the same parameters.
483 *
484 * @param {TimeMapDataset} dataset  Dataset to load data into
485 * @param {Function} callback       Function to call once data is loaded
486 */
487TimeMap.loaders.basic.prototype.load = function(dataset, callback) {
488    // preload
489    var items = this.preload(this.data);
490    // load
491    dataset.loadItems(items, this.transform);
492    // run callback
493    callback();
494};
495
496/**
497 * @class
498 * Generic class for loading remote data with a custom parser function
499 *
500 * @constructor
501 * @param {Object} options          All options for the loader:<pre>
502 *   {Array} url                        URL of file to load (NB: must be local address)
503 *   {Function} parserFunction          Parser function to turn a string into a JavaScript array
504 *   {Function} preloadFunction         Function to call on data before loading
505 *   {Function} transformFunction       Function to call on individual items before loading
506 * </pre>
507 */
508TimeMap.loaders.remote = function(options) {
509    // get standard functions and document
510    TimeMap.loaders.mixin(this, options);
511    /**
512     * Parser function to turn a string into a JavaScript array
513     * @name TimeMap.loaders.remote#parse
514     * @function
515     * @parameter {String} s    String to parse
516     * @return {Array} data     Array of item data
517     */
518     
519    /**
520     * Function to call on data object before loading
521     * @name TimeMap.loaders.remote#preload
522     * @function
523     * @parameter {Object} data     Data to preload
524     * @return {Object[]} data      Array of item data
525     */
526     
527    /**
528     * Function to call on a single item data object before loading
529     * @name TimeMap.loaders.remote#transform
530     * @function
531     * @parameter {Object} data     Data to transform
532     * @return {Object} data        Transformed data for one item
533     */
534   
535    /**
536     * URL to load
537     * @type String
538     */
539    this.url = options.url;
540};
541
542/**
543 * Remote load function.
544 *
545 * @param {TimeMapDataset} dataset  Dataset to load data into
546 * @param {Function} callback       Function to call once data is loaded
547 */
548TimeMap.loaders.remote.prototype.load = function(dataset, callback) {
549    var loader = this;
550   
551    // XXX: It would be nice to save the callback function here,
552    // and be able to cancel it (or cancel all) if needed
553   
554    // get items
555    GDownloadUrl(this.url, function(result) {
556        // parse
557        var items = loader.parse(result);
558        // load
559        items = loader.preload(items);
560        dataset.loadItems(items, loader.transform);
561        // callback
562        callback();
563    });
564};
565
566/**
567 * Save a few lines of code by adding standard functions
568 *
569 * @param {Function} loader         Loader to add functions to
570 * @param {Object} options          Options for the loader:<pre>
571 *   {Function} parserFunction          Parser function to turn data into JavaScript array
572 *   {Function} preloadFunction         Function to call on data before loading
573 *   {Function} transformFunction       Function to call on individual items before loading
574 * </pre>
575 */
576TimeMap.loaders.mixin = function(loader, options) {
577    // set preload and transform functions
578    var dummy = function(data) { return data; };
579    loader.parse = options.parserFunction || dummy;
580    loader.preload = options.preloadFunction || dummy;
581    loader.transform = options.transformFunction || dummy;
582};
583
584/**
585 * Create an empty dataset object and add it to the timemap
586 *
587 * @param {String} id           The id of the dataset
588 * @param {Object} options      A container for optional arguments for dataset constructor
589 * @return {TimeMapDataset}     The new dataset object   
590 */
591TimeMap.prototype.createDataset = function(id, options) {
592    var dataset = new TimeMapDataset(this, options);
593    this.datasets[id] = dataset;
594    // add event listener
595    if (this.opts.centerOnItems) {
596        var map = this.map, bounds = this.mapBounds;
597        GEvent.addListener(dataset, 'itemsloaded', function() {
598            // determine the center and zoom level from the bounds
599            map.setCenter(
600                bounds.getCenter(),
601                map.getBoundsZoomLevel(bounds)
602            );
603        });
604    }
605    return dataset;
606};
607
608/**
609 * Run a function on each dataset in the timemap. This is the preferred
610 * iteration method, as it allows for future iterator options.
611 *
612 * @param {Function} f    The function to run, taking one dataset as an argument
613 */
614TimeMap.prototype.each = function(f) {
615    for (var id in this.datasets) {
616        if (this.datasets.hasOwnProperty(id)) {
617            f(this.datasets[id]);
618        }
619    }
620};
621
622/**
623 * Run a function on each item in each dataset in the timemap.
624 *
625 * @param {Function} f    The function to run, taking one item as an argument
626 */
627TimeMap.prototype.eachItem = function(f) {
628    this.each(function(ds) {
629        ds.each(function(item) {
630            f(item);
631        });
632    });
633};
634
635/**
636 * Get all items from all datasets.
637 *
638 * @return {TimeMapItem[]}  Array of all items
639 */
640TimeMap.prototype.getItems = function(index) {
641    var items = [];
642    this.eachItem(function(item) {
643        items.push(item);
644    });
645    return items;
646};
647
648/**
649 * Initialize the timeline - this must happen separately to allow full control of
650 * timeline properties.
651 *
652 * @param {BandInfo Array} bands    Array of band information objects for timeline
653 */
654TimeMap.prototype.initTimeline = function(bands) {
655   
656    // synchronize & highlight timeline bands
657    for (var x=1; x < bands.length; x++) {
658        if (this.opts.syncBands) {
659            bands[x].syncWith = (x-1);
660        }
661        bands[x].highlight = true;
662    }
663   
664    /**
665     * The associated timeline object
666     * @type Timeline
667     */
668    this.timeline = Timeline.create(this.tElement, bands);
669   
670    // set event listeners
671    var tm = this;
672    // update map on timeline scroll
673    this.timeline.getBand(0).addOnScrollListener(function() {
674        tm.filter("map");
675    });
676
677    // hijack timeline popup window to open info window
678    var painter = this.timeline.getBand(0).getEventPainter().constructor;
679    painter.prototype._showBubble = function(x, y, evt) {
680        evt.item.openInfoWindow();
681    };
682   
683    // filter chain for map placemarks
684    this.addFilterChain("map", 
685        function(item) {
686            item.showPlacemark();
687        },
688        function(item) {
689            item.hidePlacemark();
690        }
691    );
692   
693    // filter: hide when item is hidden
694    this.addFilter("map", function(item) {
695        return item.visible;
696    });
697    // filter: hide when dataset is hidden
698    this.addFilter("map", function(item) {
699        return item.dataset.visible;
700    });
701   
702    // filter: hide map items depending on timeline state
703    this.addFilter("map", this.opts.mapFilter);
704   
705    // filter chain for timeline events
706    this.addFilterChain("timeline", 
707        // on
708        function(item) {
709            item.showEvent();
710        },
711        // off
712        function(item) {
713            item.hideEvent();
714        },
715        // pre
716        null,
717        // post
718        function(tm) {
719            tm.eventSource._events._index();
720            tm.timeline.layout();
721        }
722    );
723   
724    // filter: hide when item is hidden
725    this.addFilter("timeline", function(item) {
726        return item.visible;
727    });
728    // filter: hide when dataset is hidden
729    this.addFilter("timeline", function(item) {
730        return item.dataset.visible;
731    });
732   
733    // add callback for window resize
734    var resizeTimerID = null;
735    var oTimeline = this.timeline;
736    window.onresize = function() {
737        if (resizeTimerID === null) {
738            resizeTimerID = window.setTimeout(function() {
739                resizeTimerID = null;
740                oTimeline.layout();
741            }, 500);
742        }
743    };
744};
745
746/**
747 * Update items, hiding or showing according to filters
748 *
749 * @param {String} fid      Filter chain to update on
750 */
751TimeMap.prototype.filter = function(fid) {
752    var filterChain = this.chains[fid], chain;
753    // if no filters exist, forget it
754    if (!filterChain) {
755        return;
756    }
757    chain = filterChain.chain;
758    if (!chain || chain.length === 0) {
759        return;
760    }
761    // pre-filter function
762    if (filterChain.pre) {
763        filterChain.pre(this);
764    }
765    // run items through filter
766    this.each(function(ds) {
767        ds.each(function(item) {
768            var done = false;
769            F_LOOP: while (!done) { 
770                for (var i = chain.length - 1; i >= 0; i--) {
771                    if (!chain[i](item)) {
772                        // false condition
773                        filterChain.off(item);
774                        break F_LOOP;
775                    }
776                }
777                // true condition
778                filterChain.on(item);
779                done = true;
780            }
781        });
782    });
783    // post-filter function
784    if (filterChain.post) {
785        filterChain.post(this);
786    }
787};
788
789/**
790 * Add a new filter chain
791 *
792 * @param {String} fid      Id of the filter chain
793 * @param {Function} fon    Function to run on an item if filter is true
794 * @param {Function} foff   Function to run on an item if filter is false
795 * @param {Function} [pre]  Function to run before the filter runs
796 * @param {Function} [post] Function to run after the filter runs
797 */
798TimeMap.prototype.addFilterChain = function(fid, fon, foff, pre, post) {
799    this.chains[fid] = {
800        chain:[],
801        on: fon,
802        off: foff,
803        pre: pre,
804        post: post
805    };
806};
807
808/**
809 * Remove a filter chain
810 *
811 * @param {String} fid      Id of the filter chain
812 */
813TimeMap.prototype.removeFilterChain = function(fid) {
814    this.chains[fid] = null;
815};
816
817/**
818 * Add a function to a filter chain
819 *
820 * @param {String} fid      Id of the filter chain
821 * @param {Function} f      Function to add
822 */
823TimeMap.prototype.addFilter = function(fid, f) {
824    var filterChain = this.chains[fid];
825    if (filterChain && filterChain.chain) {
826        filterChain.chain.push(f);
827    }
828};
829
830/**
831 * Remove a function from a filter chain
832 *
833 * @param {String} fid      Id of the filter chain
834 * @param {Function} [f]    The function to remove
835 */
836TimeMap.prototype.removeFilter = function(fid, f) {
837    var filterChain = this.chains[fid];
838    if (filterChain && filterChain.chain) {
839        var chain = filterChain.chain;
840        if (!f) {
841            // just remove the last filter added
842            chain.pop();
843        }
844        else {
845            // look for the specific filter to remove
846            for(var i = 0; i < chain.length; i++){
847          if(chain[i] == f){
848            chain.splice(i, 1);
849          }
850        }
851        }
852    }
853};
854
855/**
856 * @namespace
857 * Namespace for different filter functions. Adding new filters to this
858 * object allows them to be specified by string name.
859 */
860TimeMap.filters = {};
861
862/**
863 * Static filter function: Hide items not in the visible area of the timeline.
864 *
865 * @param {TimeMapItem} item    Item to test for filter
866 * @return {Boolean}            Whether to show the item
867 */
868TimeMap.filters.hidePastFuture = function(item) {
869    var topband = item.dataset.timemap.timeline.getBand(0),
870        maxVisibleDate = topband.getMaxVisibleDate().getTime(),
871        minVisibleDate = topband.getMinVisibleDate().getTime();
872    if (item.event) {
873        var itemStart = item.event.getStart().getTime(),
874            itemEnd = item.event.getEnd().getTime();
875        // hide items in the future
876        if (itemStart > maxVisibleDate) {
877            return false;
878        } 
879        // hide items in the past
880        else if (itemEnd < minVisibleDate || 
881            (item.event.isInstant() && itemStart < minVisibleDate)) {
882            return false;
883        }
884    }
885    return true;
886};
887
888/**
889 * Static filter function: Hide items not present at the exact
890 * center date of the timeline (will only work for duration events).
891 *
892 * @param {TimeMapItem} item    Item to test for filter
893 * @return {Boolean}            Whether to show the item
894 */
895TimeMap.filters.showMomentOnly = function(item) {
896    var topband = item.dataset.timemap.timeline.getBand(0),
897        momentDate = topband.getCenterVisibleDate().getTime();
898    if (item.event) {
899        var itemStart = item.event.getStart().getTime(),
900            itemEnd = item.event.getEnd().getTime();
901        // hide items in the future
902        if (itemStart > momentDate) {
903            return false;
904        } 
905        // hide items in the past
906        else if (itemEnd < momentDate || 
907            (item.event.isInstant() && itemStart < momentDate)) {
908            return false;
909        }
910    }
911    return true;
912};
913
914/*----------------------------------------------------------------------------
915 * TimeMapDataset Class
916 *---------------------------------------------------------------------------*/
917
918/**
919 * @class
920 * The TimeMapDataset object holds an array of items and dataset-level
921 * options and settings, including visual themes.
922 *
923 * @constructor
924 * @param {TimeMap} timemap         Reference to the timemap object
925 * @param {Object} [options]        Object holding optional arguments:<pre>
926 *   {String} id                        Key for this dataset in the datasets map
927 *   {String} title                     Title of the dataset (for the legend)
928 *   {String or theme object} theme     Theme settings.
929 *   {String or Function} dateParser    Function to replace default date parser.
930 *   {Function} openInfoWindow          Function redefining how info window opens
931 *   {Function} closeInfoWindow         Function redefining how info window closes
932 * </pre>
933 */
934TimeMapDataset = function(timemap, options) {
935
936    /**
937     * Reference to parent TimeMap
938     * @type TimeMap
939     */
940    this.timemap = timemap;
941    /**
942     * EventSource for timeline events
943     * @type Timeline.EventSource
944     */
945    this.eventSource = new Timeline.DefaultEventSource();
946    /**
947     * Array of child TimeMapItems
948     * @type Array
949     */
950    this.items = [];
951    /**
952     * Whether the dataset is visible
953     * @type Boolean
954     */
955    this.visible = true;
956   
957    // set defaults for options
958    var defaults = {
959        title:          'Untitled',
960        dateParser:     TimeMapDataset.hybridParser
961    };
962       
963    /**
964     * Container for optional settings passed in the "options" parameter
965     * @type Object
966     */
967    this.opts = options = util.merge(options, defaults, timemap.opts);
968   
969    // allow date parser to be specified by key
970    options.dateParser = util.lookup(options.dateParser, TimeMap.dateParsers);
971    // allow theme options to be specified in options
972    options.theme = TimeMapTheme.create(options.theme, options);
973   
974    /**
975     * Return an array of this dataset's items
976     *
977     * @param {int} [index]     Index of single item to return
978     * @return {TimeMapItem[]}  Single item, or array of all items if no index was supplied
979     */
980    this.getItems = function(index) {
981        if (index !== undefined) {
982            if (index < this.items.length) {
983                return this.items[index];
984            }
985            else {
986                return null;
987            }
988        }
989        return this.items;
990    };
991   
992    /**
993     * Return the title of the dataset
994     *
995     * @return {String}     Dataset title
996     */
997    this.getTitle = function() { return this.opts.title; };
998};
999
1000/**
1001 * Better Timeline Gregorian parser... shouldn't be necessary :(.
1002 * Gregorian dates are years with "BC" or "AD"
1003 *
1004 * @param {String} s    String to parse into a Date object
1005 * @return {Date}       Parsed date or null
1006 */
1007TimeMapDataset.gregorianParser = function(s) {
1008    if (!s) {
1009        return null;
1010    } else if (s instanceof Date) {
1011        return s;
1012    }
1013    // look for BC
1014    var bc = Boolean(s.match(/b\.?c\.?/i));
1015    // parse - parseInt will stop at non-number characters
1016    var year = parseInt(s, 10);
1017    // look for success
1018    if (!isNaN(year)) {
1019        // deal with BC
1020        if (bc) {
1021            year = 1 - year;
1022        }
1023        // make Date and return
1024        var d = new Date(0);
1025        d.setUTCFullYear(year);
1026        return d;
1027    }
1028    else {
1029        return null;
1030    }
1031};
1032
1033/**
1034 * Parse date strings with a series of date parser functions, until one works.
1035 * In order:
1036 * <ol>
1037 *  <li>Date.parse() (so Date.js should work here, if it works with Timeline...)</li>
1038 *  <li>Gregorian parser</li>
1039 *  <li>The Timeline ISO 8601 parser</li>
1040 * </ol>
1041 *
1042 * @param {String} s    String to parse into a Date object
1043 * @return {Date}       Parsed date or null
1044 */
1045TimeMapDataset.hybridParser = function(s) {
1046    // try native date parse
1047    var d = new Date(Date.parse(s));
1048    if (isNaN(d)) {
1049        if (typeof(s) == "string") {
1050            // look for Gregorian dates
1051            if (s.match(/^-?\d{1,6} ?(a\.?d\.?|b\.?c\.?e?\.?|c\.?e\.?)?$/i)) {
1052                d = TimeMapDataset.gregorianParser(s);
1053            }
1054            // try ISO 8601 parse
1055            else {
1056                try {
1057                    d = DateTime.parseIso8601DateTime(s);
1058                } catch(e) {
1059                    d = null;
1060                }
1061            }
1062        }
1063        else {
1064            return null;
1065        }
1066    }
1067    // d should be a date or null
1068    return d;
1069};
1070
1071/**
1072 * Run a function on each item in the dataset. This is the preferred
1073 * iteration method, as it allows for future iterator options.
1074 *
1075 * @param {Function} f    The function to run
1076 */
1077TimeMapDataset.prototype.each = function(f) {
1078    for (var x=0; x < this.items.length; x++) {
1079        f(this.items[x]);
1080    }
1081};
1082
1083/**
1084 * Add an array of items to the map and timeline.
1085 * Each item has both a timeline event and a map placemark.
1086 *
1087 * @param {Object} data             Data to be loaded. See loadItem() for the format.
1088 * @param {Function} [transform]    If data is not in the above format, transformation function to make it so
1089 * @see TimeMapDataset#loadItem
1090 */
1091TimeMapDataset.prototype.loadItems = function(data, transform) {
1092    for (var x=0; x < data.length; x++) {
1093        this.loadItem(data[x], transform);
1094    }
1095    GEvent.trigger(this, 'itemsloaded');
1096};
1097
1098/**
1099 * Add one item to map and timeline.
1100 * Each item has both a timeline event and a map placemark.
1101 *
1102 * @param {Object} data         Data to be loaded, in the following format: <pre>
1103 *      {String} title              Title of the item (visible on timeline)
1104 *      {DateTime} start            Start time of the event on the timeline
1105 *      {DateTime} end              End time of the event on the timeline (duration events only)
1106 *      {Object} point              Data for a single-point placemark:
1107 *          {Float} lat                 Latitude of map marker
1108 *          {Float} lon                 Longitude of map marker
1109 *      {Array of points} polyline  Data for a polyline placemark, in format above
1110 *      {Array of points} polygon   Data for a polygon placemark, in format above
1111 *      {Object} overlay            Data for a ground overlay:
1112 *          {String} image              URL of image to overlay
1113 *          {Float} north               Northern latitude of the overlay
1114 *          {Float} south               Southern latitude of the overlay
1115 *          {Float} east                Eastern longitude of the overlay
1116 *          {Float} west                Western longitude of the overlay
1117 *      {Object} options            Optional arguments to be passed to the TimeMapItem (@see TimeMapItem)
1118 * </pre>
1119 * @param {Function} [transform]    If data is not in the above format, transformation function to make it so
1120 * @return {TimeMapItem}            The created item (for convenience, as it's already been added)
1121 * @see TimeMapItem
1122 */
1123TimeMapDataset.prototype.loadItem = function(data, transform) {
1124
1125    // apply transformation, if any
1126    if (transform !== undefined) {
1127        data = transform(data);
1128    }
1129    // transform functions can return a null value to skip a datum in the set
1130    if (!data) {
1131        return;
1132    }
1133   
1134    // set defaults for options
1135    options = util.merge(data.options, this.opts);
1136    // allow theme options to be specified in options
1137    var theme = options.theme = TimeMapTheme.create(options.theme, options);
1138   
1139    // create timeline event
1140    var parser = this.opts.dateParser, start = data.start, end = data.end, instant;
1141    start = start ? parser(start) : null;
1142    end = end ? parser(end) : null;
1143    instant = !end;
1144    var eventIcon = theme.eventIcon,
1145        title = data.title,
1146        // allow event-less placemarks - these will be always present on map
1147        event = null;
1148    if (start !== null) { 
1149        var eventClass = Timeline.DefaultEventSource.Event;
1150        if (util.TimelineVersion() == "1.2") {
1151            // attributes by parameter
1152            event = new eventClass(start, end, null, null,
1153                instant, title, null, null, null, eventIcon, theme.eventColor, 
1154                theme.eventTextColor);
1155        } else {
1156            var textColor = theme.eventTextColor;
1157            if (!textColor) {
1158                // tweak to show old-style events
1159                textColor = (theme.classicTape && !instant) ? '#FFFFFF' : '#000000';
1160            }
1161            // attributes in object
1162            event = new eventClass({
1163                start: start,
1164                end: end,
1165                instant: instant,
1166                text: title,
1167                icon: eventIcon,
1168                color: theme.eventColor,
1169                textColor: textColor
1170            });
1171        }
1172    }
1173   
1174    // set the icon, if any, outside the closure
1175    var markerIcon = theme.icon,
1176        tm = this.timemap,
1177        bounds = tm.mapBounds;
1178   
1179    // internal function: create map placemark
1180    // takes a data object (could be full data, could be just placemark)
1181    // returns an object with {placemark, type, point}
1182    var createPlacemark = function(pdata) {
1183        var placemark = null, type = "", point = null;
1184        // point placemark
1185        if (pdata.point) {
1186            var lat = pdata.point.lat, lon = pdata.point.lon;
1187            if (lat === undefined || lon === undefined) {
1188                // give up
1189                return null;
1190            }
1191            point = new GLatLng(
1192                parseFloat(pdata.point.lat), 
1193                parseFloat(pdata.point.lon)
1194            );
1195            // add point to visible map bounds
1196            if (tm.opts.centerOnItems) {
1197                bounds.extend(point);
1198            }
1199            // create marker
1200            placemark = new GMarker(point, {
1201                icon: markerIcon,
1202                title: pdata.title
1203            });
1204            type = "marker";
1205            point = placemark.getLatLng();
1206        }
1207        // polyline and polygon placemarks
1208        else if (pdata.polyline || pdata.polygon) {
1209            var points = [], line;
1210            if (pdata.polyline) {
1211                line = pdata.polyline;
1212            } else {
1213                line = pdata.polygon;
1214            }
1215            if (line && line.length) {
1216                for (var x=0; x<line.length; x++) {
1217                    point = new GLatLng(
1218                        parseFloat(line[x].lat), 
1219                        parseFloat(line[x].lon)
1220                    );
1221                    points.push(point);
1222                    // add point to visible map bounds
1223                    if (tm.opts.centerOnItems) {
1224                        bounds.extend(point);
1225                    }
1226                }
1227                if ("polyline" in pdata) {
1228                    placemark = new GPolyline(points, 
1229                                              theme.lineColor, 
1230                                              theme.lineWeight,
1231                                              theme.lineOpacity);
1232                    type = "polyline";
1233                    point = placemark.getVertex(Math.floor(placemark.getVertexCount()/2));
1234                } else {
1235                    placemark = new GPolygon(points, 
1236                                             theme.polygonLineColor, 
1237                                             theme.polygonLineWeight,
1238                                             theme.polygonLineOpacity,
1239                                             theme.fillColor,
1240                                             theme.fillOpacity);
1241                    type = "polygon";
1242                    point = placemark.getBounds().getCenter();
1243                }
1244            }
1245        } 
1246        // ground overlay placemark
1247        else if ("overlay" in pdata) {
1248            var sw = new GLatLng(
1249                parseFloat(pdata.overlay.south), 
1250                parseFloat(pdata.overlay.west)
1251            );
1252            var ne = new GLatLng(
1253                parseFloat(pdata.overlay.north), 
1254                parseFloat(pdata.overlay.east)
1255            );
1256            // add to visible bounds
1257            if (tm.opts.centerOnItems) {
1258                bounds.extend(sw);
1259                bounds.extend(ne);
1260            }
1261            // create overlay
1262            var overlayBounds = new GLatLngBounds(sw, ne);
1263            placemark = new GGroundOverlay(pdata.overlay.image, overlayBounds);
1264            type = "overlay";
1265            point = overlayBounds.getCenter();
1266        }
1267        return {
1268            "placemark": placemark,
1269            "type": type,
1270            "point": point
1271        };
1272    };
1273   
1274    // create placemark or placemarks
1275    var placemark = [], pdataArr = [], pdata = null, type = "", point = null, i;
1276    // array of placemark objects
1277    if ("placemarks" in data) {
1278        pdataArr = data.placemarks;
1279    } else {
1280        // we have one or more single placemarks
1281        var types = ["point", "polyline", "polygon", "overlay"];
1282        for (i=0; i<types.length; i++) {
1283            if (types[i] in data) {
1284                // put in title (only used for markers)
1285                pdata = {title: title};
1286                pdata[types[i]] = data[types[i]];
1287                pdataArr.push(pdata);
1288            }
1289        }
1290    }
1291    if (pdataArr) {
1292        for (i=0; i<pdataArr.length; i++) {
1293            // create the placemark
1294            var p = createPlacemark(pdataArr[i]);
1295            // check that the placemark was valid
1296            if (p && p.placemark) {
1297                // take the first point and type as a default
1298                point = point || p.point;
1299                type = type || p.type;
1300                placemark.push(p.placemark);
1301            }
1302        }
1303    }
1304    // override type for arrays
1305    if (placemark.length > 1) {
1306        type = "array";
1307    }
1308   
1309    options.title = title;
1310    options.type = type;
1311    // check for custom infoPoint and convert to GLatLng
1312    if (options.infoPoint) {
1313        options.infoPoint = new GLatLng(
1314            parseFloat(options.infoPoint.lat), 
1315            parseFloat(options.infoPoint.lon)
1316        );
1317    } else {
1318        options.infoPoint = point;
1319    }
1320   
1321    // create item and cross-references
1322    var item = new TimeMapItem(placemark, event, this, options);
1323    // add event if it exists
1324    if (event !== null) {
1325        event.item = item;
1326        // allow for custom event loading
1327        if (!this.opts.noEventLoad) {
1328            // add event to timeline
1329            this.eventSource.add(event);
1330        }
1331    }
1332    // add placemark(s) if any exist
1333    if (placemark.length > 0) {
1334        for (i=0; i<placemark.length; i++) {
1335            placemark[i].item = item;
1336            // add listener to make placemark open when event is clicked
1337            GEvent.addListener(placemark[i], "click", function() {
1338                item.openInfoWindow();
1339            });
1340            // allow for custom placemark loading
1341            if (!this.opts.noPlacemarkLoad) {
1342                // add placemark to map
1343                tm.map.addOverlay(placemark[i]);
1344            }
1345            // hide placemarks until the next refresh
1346            placemark[i].hide();
1347        }
1348    }
1349    // add the item to the dataset
1350    this.items.push(item);
1351    // return the item object
1352    return item;
1353};
1354
1355/*----------------------------------------------------------------------------
1356 * TimeMapTheme Class
1357 *---------------------------------------------------------------------------*/
1358
1359/**
1360 * @class
1361 * Predefined visual themes for datasets, defining colors and images for
1362 * map markers and timeline events.
1363 *
1364 * @constructor
1365 * @param {Object} [options]        A container for optional arguments:<pre>
1366 *      {GIcon} icon                    Icon for marker placemarks
1367 *      {String} color                  Default color in hex for events, polylines, polygons
1368 *      {String} lineColor              Color for polylines, defaults to options.color
1369 *      {String} polygonLineColor       Color for polygon outlines, defaults to lineColor
1370 *      {Number} lineOpacity            Opacity for polylines
1371 *      {Number} polgonLineOpacity      Opacity for polygon outlines, defaults to options.lineOpacity
1372 *      {Number} lineWeight             Line weight in pixels for polylines
1373 *      {Number} polygonLineWeight      Line weight for polygon outlines, defaults to options.lineWeight
1374 *      {String} fillColor              Color for polygon fill, defaults to options.color
1375 *      {String} fillOpacity            Opacity for polygon fill
1376 *      {String} eventColor             Background color for duration events
1377 *      {String} eventIconPath          Path to instant event icon directory
1378 *      {String} eventIconImage         Filename of instant event icon image
1379 *      {URL} eventIcon                 URL for instant event icons (overrides path + image)
1380 *      {Boolean} classicTape           Whether to use the "classic" style timeline event tape
1381 *                                      (NB: this needs additional css to work - see examples/artists.html)
1382 * </pre>
1383 */
1384TimeMapTheme = function(options) {
1385
1386    // work out various defaults - the default theme is Google's reddish color
1387    var defaults = {
1388        color:          "#FE766A",
1389        lineOpacity:    1,
1390        lineWeight:     2,
1391        fillOpacity:    0.25,
1392        eventTextColor: null,
1393        eventIconPath:  "timemap/images/",
1394        eventIconImage: "red-circle.png",
1395        classicTape:    false,
1396        iconImage:      GIP + "red-dot.png"
1397    };
1398   
1399    // merge defaults with options
1400    var settings = util.merge(options, defaults);
1401   
1402    // kill mergeOnly if necessary
1403    delete settings.mergeOnly;
1404   
1405    // make default map icon if not supplied
1406    if (!settings.icon) {
1407        // make new red icon
1408        var markerIcon = new GIcon(G_DEFAULT_ICON);
1409        markerIcon.image = settings.iconImage;
1410        markerIcon.iconSize = new GSize(32, 32);
1411        markerIcon.shadow = GIP + "msmarker.shadow.png";
1412        markerIcon.shadowSize = new GSize(59, 32);
1413        markerIcon.iconAnchor = new GPoint(16, 33);
1414        markerIcon.infoWindowAnchor = new GPoint(18, 3);
1415        settings.icon = markerIcon;
1416    } 
1417   
1418    // cascade some settings as defaults
1419    defaults = {
1420        lineColor:          settings.color,
1421        polygonLineColor:   settings.color,
1422        polgonLineOpacity:  settings.lineOpacity,
1423        polygonLineWeight:  settings.lineWeight,
1424        fillColor:          settings.color,
1425        eventColor:         settings.color,
1426        eventIcon:          settings.eventIconPath + settings.eventIconImage
1427    };
1428    settings = util.merge(settings, defaults);
1429   
1430    // return configured options as theme
1431    return settings;
1432};
1433
1434/**
1435 * Create a theme, based on an optional new or pre-set theme
1436 *
1437 * @param {TimeMapTheme} [theme]    Existing theme to clone
1438 * @param {Object} [options]        Container for optional arguments - @see TimeMapTheme()
1439 * @return {TimeMapTheme}           Configured theme
1440 */
1441TimeMapTheme.create = function(theme, options) {
1442    // test for string matches and missing themes
1443    if (theme) {
1444        theme = TimeMap.util.lookup(theme, TimeMap.themes);
1445    } else {
1446        return new TimeMapTheme(options);
1447    }
1448   
1449    // see if we need to clone - guessing fewer keys in options
1450    var clone = false, key;
1451    for (key in options) {
1452        if (theme.hasOwnProperty(key)) {
1453            clone = {};
1454            break;
1455        }
1456    }
1457    // clone if necessary
1458    if (clone) {
1459        for (key in theme) {
1460            if (theme.hasOwnProperty(key)) {
1461                clone[key] = options[key] || theme[key];
1462            }
1463        }
1464        // fix event icon path, allowing full image path in options
1465        clone.eventIcon = options.eventIcon || 
1466            clone.eventIconPath + clone.eventIconImage;
1467        return clone;
1468    }
1469    else {
1470        return theme;
1471    }
1472};
1473
1474
1475/*----------------------------------------------------------------------------
1476 * TimeMapItem Class
1477 *---------------------------------------------------------------------------*/
1478
1479/**
1480 * @class
1481 * The TimeMapItem object holds references to one or more map placemarks and
1482 * an associated timeline event.
1483 *
1484 * @constructor
1485 * @param {placemark} placemark     Placemark or array of placemarks (GMarker, GPolyline, etc)
1486 * @param {Event} event             The timeline event
1487 * @param {TimeMapDataset} dataset  Reference to the parent dataset object
1488 * @param {Object} [options]        A container for optional arguments:<pre>
1489 *   {String} title                     Title of the item
1490 *   {String} description               Plain-text description of the item
1491 *   {String} type                      Type of map placemark used (marker. polyline, polygon)
1492 *   {GLatLng} infoPoint                Point indicating the center of this item
1493 *   {String} infoHtml                  Full HTML for the info window
1494 *   {String} infoUrl                   URL from which to retrieve full HTML for the info window
1495 *   {Function} openInfoWindow          Function redefining how info window opens
1496 *   {Function} closeInfoWindow         Function redefining how info window closes
1497 *   {String/TimeMapTheme} theme        Theme applying to this item, overriding dataset theme
1498 * </pre>
1499 */
1500TimeMapItem = function(placemark, event, dataset, options) {
1501
1502    /**
1503     * This item's timeline event
1504     * @type Timeline.Event
1505     */
1506    this.event = event;
1507   
1508    /**
1509     * This item's parent dataset
1510     * @type TimeMapDataset
1511     */
1512    this.dataset = dataset;
1513   
1514    /**
1515     * The timemap's map object
1516     * @type GMap2
1517     */
1518    this.map = dataset.timemap.map;
1519   
1520    // initialize placemark(s) with some type juggling
1521    if (placemark && util.isArray(placemark) && placemark.length === 0) {
1522        placemark = null;
1523    }
1524    if (placemark && placemark.length == 1) {
1525        placemark = placemark[0];
1526    }
1527    /**
1528     * This item's placemark(s)
1529     * @type GMarker/GPolyline/GPolygon/GOverlay/Array
1530     */
1531    this.placemark = placemark;
1532   
1533    // set defaults for options
1534    var defaults = {
1535        type: 'none',
1536        title: 'Untitled',
1537        description: '',
1538        infoPoint: null,
1539        infoHtml: '',
1540        infoUrl: '',
1541        closeInfoWindow: TimeMapItem.closeInfoWindowBasic
1542    };
1543    this.opts = options = util.merge(options, defaults, dataset.opts);
1544   
1545    // select default open function
1546    if (!options.openInfoWindow) {
1547        if (options.infoUrl !== "") {
1548            // load via AJAX if URL is provided
1549            options.openInfoWindow = TimeMapItem.openInfoWindowAjax;
1550        } else {
1551            // otherwise default to basic window
1552            options.openInfoWindow = TimeMapItem.openInfoWindowBasic;
1553        }
1554    }
1555   
1556    // getter functions
1557   
1558    /**
1559     * Return the placemark type for this item
1560     *
1561     * @return {String}     Placemark type
1562     */
1563    this.getType = function() { return this.opts.type; };
1564   
1565    /**
1566     * Return the title for this item
1567     *
1568     * @return {String}     Item title
1569     */
1570    this.getTitle = function() { return this.opts.title; };
1571   
1572    /**
1573     * Return the item's "info point" (the anchor for the map info window)
1574     *
1575     * @return {GLatLng}    Info point
1576     */
1577    this.getInfoPoint = function() { 
1578        // default to map center if placemark not set
1579        return this.opts.infoPoint || this.map.getCenter(); 
1580    };
1581   
1582    /**
1583     * Whether the item is visible
1584     * @type Boolean
1585     */
1586    this.visible = true;
1587   
1588    /**
1589     * Whether the item's placemark is visible
1590     * @type Boolean
1591     */
1592    this.placemarkVisible = false;
1593   
1594    /**
1595     * Whether the item's event is visible
1596     * @type Boolean
1597     */
1598    this.eventVisible = true;
1599   
1600    /**
1601     * Open the info window for this item.
1602     * By default this is the map infoWindow, but you can set custom functions
1603     * for whatever behavior you want when the event or placemark is clicked
1604     * @function
1605     */
1606    this.openInfoWindow = options.openInfoWindow;
1607   
1608    /**
1609     * Close the info window for this item.
1610     * By default this is the map infoWindow, but you can set custom functions
1611     * for whatever behavior you want.
1612     * @function
1613     */
1614    this.closeInfoWindow = options.closeInfoWindow;
1615};
1616
1617/**
1618 * Show the map placemark(s)
1619 */
1620TimeMapItem.prototype.showPlacemark = function() {
1621    if (this.placemark) {
1622        if (this.getType() == "array") {
1623            for (var i=0; i<this.placemark.length; i++) {
1624                this.placemark[i].show();
1625            }
1626        } else {
1627            this.placemark.show();
1628        }
1629        this.placemarkVisible = true;
1630    }
1631};
1632
1633/**
1634 * Hide the map placemark(s)
1635 */
1636TimeMapItem.prototype.hidePlacemark = function() {
1637    if (this.placemark) {
1638        if (this.getType() == "array") {
1639            for (var i=0; i<this.placemark.length; i++) {
1640                this.placemark[i].hide();
1641            }
1642        } else {
1643            this.placemark.hide();
1644        }
1645        this.placemarkVisible = false;
1646    }
1647    this.closeInfoWindow();
1648};
1649
1650/**
1651 * Show the timeline event.
1652 * NB: Will likely require calling timeline.layout()
1653 */
1654TimeMapItem.prototype.showEvent = function() {
1655    if (this.event) {
1656        if (this.eventVisible === false){
1657            this.dataset.timemap.timeline.getBand(0)
1658                .getEventSource()._events._events.add(this.event);
1659        }
1660        this.eventVisible = true;
1661    }
1662};
1663
1664/**
1665 * Hide the timeline event.
1666 * NB: Will likely require calling timeline.layout(),
1667 * AND calling eventSource._events._index()  (ugh)
1668 */
1669TimeMapItem.prototype.hideEvent = function() {
1670    if (this.event) {
1671        if (this.eventVisible){
1672            this.dataset.timemap.timeline.getBand(0)
1673                .getEventSource()._events._events.remove(this.event);
1674        }
1675        this.eventVisible = false;
1676    }
1677};
1678
1679/**
1680 * Standard open info window function, using static text in map window
1681 */
1682TimeMapItem.openInfoWindowBasic = function() {
1683    var html = this.opts.infoHtml;
1684    // create content for info window if none is provided
1685    if (html === "") {
1686        html = '<div class="infotitle">' + this.opts.title + '</div>';
1687        if (this.opts.description !== "") {
1688            html += '<div class="infodescription">' + this.opts.description + '</div>';
1689        }
1690    }
1691    // scroll timeline if necessary
1692    if (this.placemark && !this.visible && this.event) {
1693        var topband = this.dataset.timemap.timeline.getBand(0);
1694        topband.setCenterVisibleDate(this.event.getStart());
1695    }
1696    // open window
1697    if (this.getType() == "marker") {
1698        this.placemark.openInfoWindowHtml(html);
1699    } else {
1700        this.map.openInfoWindowHtml(this.getInfoPoint(), html);
1701    }
1702    // custom functions will need to set this as well
1703    this.selected = true;
1704};
1705
1706/**
1707 * Open info window function using ajax-loaded text in map window
1708 */
1709TimeMapItem.openInfoWindowAjax = function() {
1710    if (this.opts.infoHtml !== "") { // already loaded - change to static
1711        this.openInfoWindow = TimeMapItem.openInfoWindowBasic;
1712        this.openInfoWindow();
1713    } else { // load content via AJAX
1714        if (this.opts.infoUrl !== "") {
1715            var item = this;
1716            GDownloadUrl(this.opts.infoUrl, function(result) {
1717                    item.opts.infoHtml = result;
1718                    item.openInfoWindow();
1719            });
1720        } else { // fall back on basic function
1721            this.openInfoWindow = TimeMapItem.openInfoWindowBasic;
1722            this.openInfoWindow();
1723        }
1724    }
1725};
1726
1727/**
1728 * Standard close window function, using the map window
1729 */
1730TimeMapItem.closeInfoWindowBasic = function() {
1731    if (this.getType() == "marker") {
1732        this.placemark.closeInfoWindow();
1733    } else {
1734        var infoWindow = this.map.getInfoWindow();
1735        // close info window if its point is the same as this item's point
1736        if (infoWindow.getPoint() == this.getInfoPoint() && !infoWindow.isHidden()) {
1737            this.map.closeInfoWindow();
1738        }
1739    }
1740    // custom functions will need to set this as well
1741    this.selected = false;
1742};
1743
1744/*----------------------------------------------------------------------------
1745 * Utility functions
1746 *---------------------------------------------------------------------------*/
1747
1748/**
1749 * Convenience trim function
1750 *
1751 * @param {String} str      String to trim
1752 * @return {String}         Trimmed string
1753 */
1754TimeMap.util.trim = function(str) {
1755    str = str && String(str) || '';
1756    return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
1757};
1758
1759/**
1760 * Convenience array tester
1761 *
1762 * @param {Object} o        Object to test
1763 * @return {Boolean}        Whether the object is an array
1764 */
1765TimeMap.util.isArray = function(o) {   
1766    return o && !(o.propertyIsEnumerable('length')) && 
1767        typeof o === 'object' && typeof o.length === 'number';
1768};
1769
1770/**
1771 * Get XML tag value as a string
1772 *
1773 * @param {XML Node} n      Node in which to look for tag
1774 * @param {String} tag      Name of tag to look for
1775 * @param {String} [ns]     XML namespace to look in
1776 * @return {String}         Tag value as string
1777 */
1778TimeMap.util.getTagValue = function(n, tag, ns) {
1779    var str = "";
1780    var nList = TimeMap.util.getNodeList(n, tag, ns);
1781    if (nList.length > 0) {
1782        n = nList[0].firstChild;
1783        // fix for extra-long nodes
1784        // see http://code.google.com/p/timemap/issues/detail?id=36
1785        while(n !== null) {
1786            str += n.nodeValue;
1787            n = n.nextSibling;
1788        }
1789    }
1790    return str;
1791};
1792
1793/**
1794 * Empty container for mapping XML namespaces to URLs
1795 */
1796TimeMap.util.nsMap = {};
1797
1798/**
1799 * Cross-browser implementation of getElementsByTagNameNS.
1800 * Note: Expects any applicable namespaces to be mapped in
1801 * TimeMap.util.nsMap. XXX: There may be better ways to do this.
1802 *
1803 * @param {XML Node} n      Node in which to look for tag
1804 * @param {String} tag      Name of tag to look for
1805 * @param {String} [ns]     XML namespace to look in
1806 * @return {XML Node List}  List of nodes with the specified tag name
1807 */
1808TimeMap.util.getNodeList = function(n, tag, ns) {
1809    var nsMap = TimeMap.util.nsMap;
1810    if (ns === undefined) {
1811        // no namespace
1812        return n.getElementsByTagName(tag);
1813    }
1814    if (n.getElementsByTagNameNS && nsMap[ns]) {
1815        // function and namespace both exist
1816        return n.getElementsByTagNameNS(nsMap[ns], tag);
1817    }
1818    // no function, try the colon tag name
1819    return n.getElementsByTagName(ns + ':' + tag);
1820};
1821
1822/**
1823 * Make TimeMap.init()-style points from a GLatLng, array, or string
1824 *
1825 * @param {Object} coords       GLatLng, array, or string to convert
1826 * @param {Boolean} [reversed]  Whether the points are KML-style lon/lat, rather than lat/lon
1827 * @return {Object}             TimeMap.init()-style point
1828 */
1829TimeMap.util.makePoint = function(coords, reversed) {
1830    var latlon = null, 
1831        trim = TimeMap.util.trim;
1832    // GLatLng
1833    if (coords.lat && coords.lng) {
1834        latlon = [coords.lat(), coords.lng()];
1835    }
1836    // array of coordinates
1837    if (TimeMap.util.isArray(coords)) {
1838        latlon = coords;
1839    }
1840    // string
1841    if (!latlon) {
1842        // trim extra whitespace
1843        coords = trim(coords);
1844        if (coords.indexOf(',') > -1) {
1845            // split on commas
1846            latlon = coords.split(",");
1847        } else {
1848            // split on whitespace
1849            latlon = coords.split(/[\r\n\f ]+/);
1850        }
1851    }
1852    // deal with extra coordinates (i.e. KML altitude)
1853    if (latlon.length > 2) {
1854        latlon = latlon.slice(0, 2);
1855    }
1856    // deal with backwards (i.e. KML-style) coordinates
1857    if (reversed) {
1858        latlon.reverse();
1859    }
1860    return {
1861        "lat": trim(latlon[0]),
1862        "lon": trim(latlon[1])
1863    };
1864};
1865
1866/**
1867 * Make TimeMap.init()-style polyline/polygons from a whitespace-delimited
1868 * string of coordinates (such as those in GeoRSS and KML).
1869 *
1870 * @param {Object} coords       String to convert
1871 * @param {Boolean} [reversed]  Whether the points are KML-style lon/lat, rather than lat/lon
1872 * @return {Object}             Formated coordinate array
1873 */
1874TimeMap.util.makePoly = function(coords, reversed) {
1875    var poly = [], latlon;
1876    var coordArr = TimeMap.util.trim(coords).split(/[\r\n\f ]+/);
1877    if (coordArr.length === 0) return [];
1878    // loop through coordinates
1879    for (var x=0; x<coordArr.length; x++) {
1880        latlon = (coordArr[x].indexOf(',') > 0) ?
1881            // comma-separated coordinates (KML-style lon/lat)
1882            coordArr[x].split(",") :
1883            // space-separated coordinates - increment to step by 2s
1884            [coordArr[x], coordArr[++x]];
1885        // deal with extra coordinates (i.e. KML altitude)
1886        if (latlon.length > 2) {
1887            latlon = latlon.slice(0, 2);
1888        }
1889        // deal with backwards (i.e. KML-style) coordinates
1890        if (reversed) {
1891            latlon.reverse();
1892        }
1893        poly.push({
1894            "lat": latlon[0],
1895            "lon": latlon[1]
1896        });
1897    }
1898    return poly;
1899}
1900
1901/**
1902 * Format a date as an ISO 8601 string
1903 *
1904 * @param {Date} d          Date to format
1905 * @param {int} [precision] Precision indicator:<pre>
1906 *                              3 (default): Show full date and time
1907 *                              2: Show full date and time, omitting seconds
1908 *                              1: Show date only
1909 *</pre>
1910 * @return {String}         Formatted string
1911 */
1912TimeMap.util.formatDate = function(d, precision) {
1913    // default to high precision
1914    precision = precision || 3;
1915    var str = "";
1916    if (d) {
1917        // check for date.js support
1918        if (d.toISOString && precision == 3) {
1919            return d.toISOString();
1920        }
1921        // otherwise, build ISO 8601 string
1922        var pad = function(num) {
1923            return ((num < 10) ? "0" : "") + num;
1924        };
1925        var yyyy = d.getUTCFullYear(),
1926            mo = d.getUTCMonth(),
1927            dd = d.getUTCDate();
1928        str += yyyy + '-' + pad(mo + 1 ) + '-' + pad(dd);
1929        // show time if top interval less than a week
1930        if (precision > 1) {
1931            var hh = d.getUTCHours(),
1932                mm = d.getUTCMinutes(),
1933                ss = d.getUTCSeconds();
1934            str += 'T' + pad(hh) + ':' + pad(mm);
1935            // show seconds if the interval is less than a day
1936            if (precision > 2) {
1937                str += pad(ss);
1938            }
1939            str += 'Z';
1940        }
1941    }
1942    return str;
1943};
1944
1945/**
1946 * Determine the SIMILE Timeline version.
1947 *
1948 * @return {String}     At the moment, only "1.2", "2.2.0", or what Timeline provides
1949 */
1950TimeMap.util.TimelineVersion = function() {
1951    // check for Timeline.version support - added in 2.3.0
1952    if (Timeline.version) {
1953        return Timeline.version;
1954    }
1955    if (Timeline.DurationEventPainter) {
1956        return "1.2";
1957    } else {
1958        return "2.2.0";
1959    }
1960};
1961
1962
1963/**
1964 * Identify the placemark type.
1965 * XXX: Not 100% happy with this implementation, which relies heavily on duck-typing.
1966 *
1967 * @param {Object} pm       Placemark to identify
1968 * @return {String}         Type of placemark, or false if none found
1969 */
1970TimeMap.util.getPlacemarkType = function(pm) {
1971    if ('getIcon' in pm) {
1972        return 'marker';
1973    }
1974    if ('getVertex' in pm) {
1975        return 'setFillStyle' in pm ? 'polygon' : 'polyline';
1976    }
1977    return false;
1978};
1979
1980/**
1981 * Merge two or more objects, giving precendence to those
1982 * first in the list (i.e. don't overwrite existing keys).
1983 * Original objects will not be modified.
1984 *
1985 * @param {Object} obj1     Base object
1986 * @param {Object} [objN]   Objects to merge into base
1987 * @return {Object}         Merged object
1988 */
1989TimeMap.util.merge = function() {
1990    var opts = {}, args = arguments, obj, key, x, y;
1991    // must... make... subroutine...
1992    var mergeKey = function(o1, o2, key) {
1993        // note: existing keys w/undefined values will be overwritten
1994        if (o1.hasOwnProperty(key) && o2[key] === undefined) {
1995            o2[key] = o1[key];
1996        }
1997    };
1998    for (x=0; x<args.length; x++) {
1999        obj = args[x];
2000        if (obj) {
2001            // allow non-base objects to constrain what will be merged
2002            if (x > 0 && 'mergeOnly' in obj) {
2003                for (y=0; y<obj.mergeOnly.length; y++) {
2004                    key = obj.mergeOnly[y];
2005                    mergeKey(obj, opts, key);
2006                }
2007            }
2008            // otherwise, just merge everything
2009            else {
2010                for (key in obj) {
2011                    mergeKey(obj, opts, key);
2012                }
2013            }
2014        }
2015    }
2016    return opts;
2017};
2018
2019/**
2020 * Attempt look up a key in an object, returning either the value,
2021 * undefined if the key is a string but not found, or the key if not a string
2022 *
2023 * @param {String|Object} key   Key to look up
2024 * @param {Object} map          Object in which to look
2025 * @return {Object}             Value, undefined, or key
2026 */
2027TimeMap.util.lookup = function(key, map) {
2028    if (typeof(key) == 'string') {
2029        return map[key];
2030    }
2031    else {
2032        return key;
2033    }
2034};
2035
2036
2037/*----------------------------------------------------------------------------
2038 * Lookup maps
2039 * (need to be at end because some call util functions on initialization)
2040 *---------------------------------------------------------------------------*/
2041
2042/**
2043 * Lookup map of common timeline intervals. 
2044 * Add custom intervals here if you want to refer to them by key rather
2045 * than as a function name.
2046 * @type Object
2047 */
2048TimeMap.intervals = {
2049    sec: [DateTime.SECOND, DateTime.MINUTE],
2050    min: [DateTime.MINUTE, DateTime.HOUR],
2051    hr: [DateTime.HOUR, DateTime.DAY],
2052    day: [DateTime.DAY, DateTime.WEEK],
2053    wk: [DateTime.WEEK, DateTime.MONTH],
2054    mon: [DateTime.MONTH, DateTime.YEAR],
2055    yr: [DateTime.YEAR, DateTime.DECADE],
2056    dec: [DateTime.DECADE, DateTime.CENTURY]
2057};
2058
2059/**
2060 * Lookup map of Google map types.
2061 * @type Object
2062 */
2063TimeMap.mapTypes = {
2064    normal: G_NORMAL_MAP, 
2065    satellite: G_SATELLITE_MAP, 
2066    hybrid: G_HYBRID_MAP, 
2067    physical: G_PHYSICAL_MAP, 
2068    moon: G_MOON_VISIBLE_MAP, 
2069    sky: G_SKY_VISIBLE_MAP
2070};
2071
2072/**
2073 * Lookup map of supported date parser functions.
2074 * Add custom date parsers here if you want to refer to them by key rather
2075 * than as a function name.
2076 * @type Object
2077 */
2078TimeMap.dateParsers = {
2079    hybrid: TimeMapDataset.hybridParser,
2080    iso8601: DateTime.parseIso8601DateTime,
2081    gregorian: TimeMapDataset.gregorianParser
2082};
2083 
2084/**
2085 * @namespace
2086 * Pre-set event/placemark themes in a variety of colors.
2087 * Add custom themes here if you want to refer to them by key rather
2088 * than as a function name.
2089 */
2090TimeMap.themes = {
2091
2092    /**
2093     * Red theme: #FE766A
2094     * This is the default.
2095     *
2096     * @type TimeMapTheme
2097     */
2098    red: new TimeMapTheme(),
2099   
2100    /**
2101     * Blue theme: #5A7ACF
2102     *
2103     * @type TimeMapTheme
2104     */
2105    blue: new TimeMapTheme({
2106        iconImage: GIP + "blue-dot.png",
2107        color: "#5A7ACF",
2108        eventIconImage: "blue-circle.png"
2109    }),
2110
2111    /**
2112     * Green theme: #19CF54
2113     *
2114     * @type TimeMapTheme
2115     */
2116    green: new TimeMapTheme({
2117        iconImage: GIP + "green-dot.png",
2118        color: "#19CF54",
2119        eventIconImage: "green-circle.png"
2120    }),
2121
2122    /**
2123     * Light blue theme: #5ACFCF
2124     *
2125     * @type TimeMapTheme
2126     */
2127    ltblue: new TimeMapTheme({
2128        iconImage: GIP + "ltblue-dot.png",
2129        color: "#5ACFCF",
2130        eventIconImage: "ltblue-circle.png"
2131    }),
2132
2133    /**
2134     * Purple theme: #8E67FD
2135     *
2136     * @type TimeMapTheme
2137     */
2138    purple: new TimeMapTheme({
2139        iconImage: GIP + "purple-dot.png",
2140        color: "#8E67FD",
2141        eventIconImage: "purple-circle.png"
2142    }),
2143
2144    /**
2145     * Orange theme: #FF9900
2146     *
2147     * @type TimeMapTheme
2148     */
2149    orange: new TimeMapTheme({
2150        iconImage: GIP + "orange-dot.png",
2151        color: "#FF9900",
2152        eventIconImage: "orange-circle.png"
2153    }),
2154
2155    /**
2156     * Yellow theme: #ECE64A
2157     *
2158     * @type TimeMapTheme
2159     */
2160    yellow: new TimeMapTheme({
2161        iconImage: GIP + "yellow-dot.png",
2162        color: "#ECE64A",
2163        eventIconImage: "yellow-circle.png"
2164    })
2165};
2166
2167// save to window
2168window.TimeMap = TimeMap;
2169window.TimeMapDataset = TimeMapDataset;
2170window.TimeMapTheme = TimeMapTheme;
2171window.TimeMapItem = TimeMapItem;
2172
2173})();
Note: See TracBrowser for help on using the repository browser.