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

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

Javascript library: timeline

File size: 56.0 KB
Line 
1/*!
2 * TimeMap Copyright 2008 Nick Rabinowitz.
3 * Licensed under the MIT License (see LICENSE.txt)
4 */
5
6/**---------------------------------------------------------------------------
7 * TimeMap
8 *
9 * @author Nick Rabinowitz (www.nickrabinowitz.com)
10 * The TimeMap object is intended to sync a SIMILE Timeline with a Google Map.
11 * Dependencies: Google Maps API v2, SIMILE Timeline v1.2 or v2.2.0
12 * Thanks to Jörn Clausen (http://www.oe-files.de) for initial concept and code.
13 *---------------------------------------------------------------------------*/
14
15// globals - for JSLint
16/*global GBrowserIsCompatible, GLargeMapControl, GLatLngBounds, GMap2       */ 
17/*global GMapTypeControl, GDownloadUrl, GEvent, GGroundOverlay, GIcon       */
18/*global GMarker, GPolygon, GPolyline, GSize, GLatLng, G_DEFAULT_ICON       */
19/*global G_DEFAULT_MAP_TYPES, G_NORMAL_MAP, G_PHYSICAL_MAP, G_HYBRID_MAP    */
20/*global G_MOON_VISIBLE_MAP, G_SKY_VISIBLE_MAP, G_SATELLITE_MAP, Timeline   */
21
22// A couple of aliases to save a few bytes
23
24
25var DT = Timeline.DateTime, 
26// Google icon path
27GIP = "http://www.google.com/intl/en_us/mapfiles/ms/icons/";
28
29/*----------------------------------------------------------------------------
30 * TimeMap Class - holds references to timeline, map, and datasets
31 *---------------------------------------------------------------------------*/
32 
33/**
34 * Creates a new TimeMap with map placemarks synched to timeline events
35 * This will create the visible map, but not the timeline, which must be initialized separately.
36 *
37 * @constructor
38 * @param {element} tElement     The timeline element.
39 * @param {element} mElement     The map element.
40 * @param {Object} options       A container for optional arguments:
41 *   {Boolean} syncBands            Whether to synchronize all bands in timeline
42 *   {GLatLng} mapCenter            Point for map center
43 *   {Number} mapZoom               Intial map zoom level
44 *   {GMapType/String} mapType      The maptype for the map
45 *   {Array} mapTypes               The set of maptypes available for the map
46 *   {Function/String} mapFilter    How to hide/show map items depending on timeline state;
47                                    options: "hidePastFuture", "showMomentOnly"
48 *   {Boolean} showMapTypeCtrl      Whether to display the map type control
49 *   {Boolean} showMapCtrl          Whether to show map navigation control
50 *   {Boolean} centerMapOnItems     Whether to center and zoom the map based on loaded item positions
51 *   {Function} openInfoWindow      Function redefining how info window opens
52 *   {Function} closeInfoWindow     Function redefining how info window closes
53 */
54function TimeMap(tElement, mElement, options) {
55    // save elements
56    this.mElement = mElement;
57    this.tElement = tElement;
58    // initialize array of datasets
59    this.datasets = {};
60    // initialize filters
61    this.filters = {};
62    // initialize map bounds
63    this.mapBounds = new GLatLngBounds();
64   
65    // set defaults for options
66    // other options can be set directly on the map or timeline
67    this.opts = options || {};   // make sure the options object isn't null
68    // allow map types to be specified by key
69    if (typeof(options.mapType) == 'string') {
70        options.mapType = TimeMap.mapTypes[options.mapType];
71    }
72    // allow map filters to be specified by key
73    if (typeof(options.mapFilter) == 'string') {
74        options.mapFilter = TimeMap.filters[options.mapFilter];
75    }
76    // these options only needed for map initialization
77    var mapCenter =        options.mapCenter || new GLatLng(0,0),
78        mapZoom =          options.mapZoom || 0,
79        mapType =          options.mapType || G_PHYSICAL_MAP,
80        mapTypes =         options.mapTypes || [G_NORMAL_MAP, G_SATELLITE_MAP, G_PHYSICAL_MAP],
81        showMapTypeCtrl =  ('showMapTypeCtrl' in options) ? options.showMapTypeCtrl : true,
82        showMapCtrl =      ('showMapCtrl' in options) ? options.showMapCtrl : true;
83   
84    // these options need to be saved for later
85    this.opts.syncBands =        ('syncBands' in options) ? options.syncBands : true;
86    this.opts.mapFilter =        options.mapFilter || TimeMap.filters.hidePastFuture;
87    this.opts.centerOnItems =    ('centerMapOnItems' in options) ? options.centerMapOnItems : true;
88   
89    // initialize map
90    if (GBrowserIsCompatible()) {
91        var map = this.map = new GMap2(this.mElement);
92        if (showMapCtrl) {
93            map.addControl(new GLargeMapControl());
94        }
95        if (showMapTypeCtrl) {
96            map.addControl(new GMapTypeControl());
97        }
98        // drop all existing types
99        var i;
100        for (i=G_DEFAULT_MAP_TYPES.length-1; i>0; i--) {
101            map.removeMapType(G_DEFAULT_MAP_TYPES[i]);
102        }
103        // you can't remove the last maptype, so add a new one first
104        map.addMapType(mapTypes[0]);
105        map.removeMapType(G_DEFAULT_MAP_TYPES[0]);
106        // add the rest of the new types
107        for (i=1; i<mapTypes.length; i++) {
108            map.addMapType(mapTypes[i]);
109        }
110        map.enableDoubleClickZoom();
111        map.enableScrollWheelZoom();
112        map.enableContinuousZoom();
113        // initialize map center and zoom
114        map.setCenter(mapCenter, mapZoom);
115        // must be called after setCenter, for reasons unclear
116        map.setMapType(mapType);
117    }
118}
119
120/**
121 * Current library version.
122 */
123TimeMap.version = "1.5pre";
124
125/**
126 * Intializes a TimeMap.
127 *
128 * This is an attempt to create a general initialization script that will
129 * work in most cases. If you need a more complex initialization, write your
130 * own script instead of using this one.
131 *
132 * The idea here is to throw all of the standard intialization settings into
133 * a large object and then pass it to the TimeMap.init() function. The full
134 * data format is outlined below, but if you leave elements off the script
135 * will use default settings instead.
136 *
137 * Call TimeMap.init() inside of an onLoad() function (or a jQuery
138 * $.(document).ready() function, or whatever you prefer). See the examples
139 * for usage.
140 *
141 * @param {Object} config   Full set of configuration options.
142 *                          See examples/timemapinit_usage.js for format.
143 */
144TimeMap.init = function(config) {
145   
146    // check required elements
147    if (!('mapId' in config) || !config.mapId) {
148        throw "TimeMap.init: No id for map";
149    }
150    if (!('timelineId' in config) || !config.timelineId) {
151        throw "TimeMap.init: No id for timeline";
152    }
153   
154    // set defaults
155    config = config || {}; // make sure the config object isn't null
156    config.options = config.options || {};
157    config.datasets = config.datasets || [];
158    config.bandInfo = config.bandInfo || false;
159    config.scrollTo = config.scrollTo || "earliest";
160    if (!config.bandInfo && !config.bands) {
161        var intervals = config.bandIntervals || 
162            config.options.bandIntervals ||
163            [DT.WEEK, DT.MONTH];
164        // allow intervals to be specified by key
165        if (typeof(intervals) == 'string') {
166            intervals = TimeMap.intervals[intervals];
167        }
168        // save for later reference
169        config.options.bandIntervals = intervals;
170        // make default band info
171        config.bandInfo = [
172        {
173                width:          "80%", 
174                intervalUnit:   intervals[0], 
175                intervalPixels: 70
176            },
177            {
178                width:          "20%", 
179                intervalUnit:   intervals[1], 
180                intervalPixels: 100,
181                showEventText:  false,
182                overview: true,
183                trackHeight:    0.4,
184                trackGap:       0.2
185            }
186        ];
187    }
188   
189    // create the TimeMap object
190    var tm = new TimeMap(
191      document.getElementById(config.timelineId), 
192    document.getElementById(config.mapId),
193    config.options);
194   
195    // create the dataset objects
196    var datasets = [], x, ds, dsOptions, dsId;
197    for (x=0; x < config.datasets.length; x++) {
198        ds = config.datasets[x];
199        dsOptions = ds.options || {};
200        dsOptions.title = ds.title || '';
201        dsOptions.theme = ds.theme;
202        dsOptions.dateParser = ds.dateParser;
203        dsId = ds.id || "ds" + x;
204        datasets[x] = tm.createDataset(dsId, dsOptions);
205        if (x > 0) {
206            // set all to the same eventSource
207            datasets[x].eventSource = datasets[0].eventSource;
208        }
209    }
210    // add a pointer to the eventSource in the TimeMap
211    tm.eventSource = datasets[0].eventSource;
212   
213    // set up timeline bands
214    var bands = [];
215    // ensure there's at least an empty eventSource
216    var eventSource = (datasets[0] && datasets[0].eventSource) || new Timeline.DefaultEventSource();
217    // check for pre-initialized bands (manually created with Timeline.createBandInfo())
218    if (config.bands) {
219        bands = config.bands;
220        // substitute dataset event source
221        for (x=0; x < bands.length; x++) {
222            // assume that these have been set up like "normal" Timeline bands:
223            // with an empty event source if events are desired, and null otherwise
224            if (bands[x].eventSource !== null) {
225                bands[x].eventSource = eventSource;
226            }
227        }
228    }
229    // otherwise, make bands from band info
230    else {
231        for (x=0; x < config.bandInfo.length; x++) {
232            var bandInfo = config.bandInfo[x];
233            // if eventSource is explicitly set to null or false, ignore
234            if (!(('eventSource' in bandInfo) && !bandInfo.eventSource)) {
235                bandInfo.eventSource = eventSource;
236            }
237            else {
238                bandInfo.eventSource = null;
239            }
240            bands[x] = Timeline.createBandInfo(bandInfo);
241            if (x > 0 && TimeMap.TimelineVersion() == "1.2") {
242                // set all to the same layout
243                bands[x].eventPainter.setLayout(bands[0].eventPainter.getLayout()); 
244            }
245        }
246    }
247    // initialize timeline
248    tm.initTimeline(bands);
249   
250    // initialize load manager
251    var loadManager = TimeMap.loadManager;
252    loadManager.init(tm, config.datasets.length, config);
253   
254    // load data!
255    for (x=0; x < config.datasets.length; x++) {
256        (function(x) { // deal with closure issues
257            var data = config.datasets[x], options, type, callback, loaderClass, loader;
258            // support some older syntax
259            options = data.options || data.data || {};
260            type = data.type || options.type;
261            callback = function() { loadManager.increment() };
262            // get loader class
263            loaderClass = (typeof(type) == 'string') ? TimeMap.loaders[type] : type;
264            // load with appropriate loader
265            loader = new loaderClass(options);
266            loader.load(datasets[x], callback);
267        })(x);
268    }
269    // return timemap object for later manipulation
270    return tm;
271};
272
273// for backwards compatibility
274var timemapInit = TimeMap.init;
275
276/**
277 * Load manager - static singleton for managing multiple asynchronous loads
278 */
279TimeMap.loadManager = new function() {
280   
281    /**
282     * Initialize (or reset) the load manager
283     *
284     * @param {TimeMap} tm          TimeMap instance
285     * @param {int} target     Number of datasets we're loading
286     * @param {Object} options      Container for optional functions
287     */
288    this.init = function(tm, target, config) {
289        this.count = 0;
290        this.tm = tm;
291        this.target = target;
292        this.opts = config || {};
293    };
294   
295    /**
296     * Increment the count of loaded datasets
297     */
298    this.increment = function() {
299        this.count++;
300        if (this.count >= this.target) {
301            this.complete();
302        }
303    };
304   
305    /**
306     * Function to fire when all loads are complete
307     */
308    this.complete = function() {
309        // custom function including timeline scrolling and layout
310        var func = this.opts.dataLoadedFunction;
311        if (func) {
312            func(tm);
313        } else {
314            var d = new Date();
315            var eventSource = this.tm.eventSource;
316            var scrollTo = this.opts.scrollTo;
317            // make sure there are events to scroll to
318            if (scrollTo && eventSource.getCount() > 0) {
319                switch (scrollTo) {
320                    case "now":
321                        break;
322                    case "earliest":
323                        d = eventSource.getEarliestDate();
324                        break;
325                    case "latest":
326                        d = eventSource.getLatestDate();
327                        break;
328                    default:
329                        // assume it's a date, try to parse
330                        if (typeof(scrollTo) == 'string') {
331                            scrollTo = TimeMapDataset.hybridParser(scrollTo);
332                        }
333                        // either the parse worked, or it was a date to begin with
334                        if (scrollTo.constructor == Date) d = scrollTo;
335                }
336                this.tm.timeline.getBand(0).setCenterVisibleDate(d);
337            }
338            this.tm.timeline.layout();
339            // custom function to be called when data is loaded
340            func = this.opts.dataDisplayedFunction;
341            if (func) {
342                func(tm);
343            }
344        }
345    };
346};
347
348/**
349 * Map of different data loader functions.
350 * New loaders should add their loader function to this map; loader
351 * functions are passed an object with parameters in TimeMap.init().
352 */
353TimeMap.loaders = {};
354
355/**
356 * Basic loader class, for pre-loaded data.
357 * Other types of loaders should take the same parameter.
358 *
359 * @param {Object} options          All options for the loader:
360 *   {Array} data                       Array of items to load
361 *   {Function} preloadFunction         Function to call on data before loading
362 *   {Function} transformFunction       Function to call on individual items before loading
363 */
364TimeMap.loaders.basic = function(options) {
365    // get standard functions
366    TimeMap.loaders.mixin(this, options);
367    // allow "value" for backwards compatibility
368    this.data = options.items || options.value || [];
369}
370
371/**
372 * New loaders should implement a load function with the same parameters.
373 *
374 * @param {TimeMapDataset} dataset  Dataset to load data into
375 * @param {Function} callback       Function to call once data is loaded
376 */
377TimeMap.loaders.basic.prototype.load = function(dataset, callback) {
378    // preload
379    var items = this.preload(this.data);
380    // load
381    dataset.loadItems(items, this.transform);
382    // run callback
383    callback();
384}
385
386/**
387 * Generic class for loading remote data with a custom parser function
388 *
389 * @param {Object} options          All options for the loader:
390 *   {Array} url                        URL of file to load (NB: must be local address)
391 *   {Function} parserFunction          Parser function to turn data into JavaScript array
392 *   {Function} preloadFunction         Function to call on data before loading
393 *   {Function} transformFunction       Function to call on individual items before loading
394 */
395TimeMap.loaders.remote = function(options) {
396    // get standard functions
397    TimeMap.loaders.mixin(this, options);
398    // get URL to load
399    this.url = options.url;
400}
401
402/**
403 * KML load function.
404 *
405 * @param {TimeMapDataset} dataset  Dataset to load data into
406 * @param {Function} callback       Function to call once data is loaded
407 */
408TimeMap.loaders.remote.prototype.load = function(dataset, callback) {
409    var loader = this;
410    // get items
411    GDownloadUrl(this.url, function(result) {
412        // parse
413        var items = loader.parse(result);
414        // load
415        items = loader.preload(items);
416        dataset.loadItems(items, loader.transform);
417        // callback
418        callback();
419    });
420}
421
422/**
423 * Save a few lines of code by adding standard functions
424 *
425 * @param {Function} loader         Loader to add functions to
426 * @param {Object} options          Options for the loader:
427 *   {Function} parserFunction          Parser function to turn data into JavaScript array
428 *   {Function} preloadFunction         Function to call on data before loading
429 *   {Function} transformFunction       Function to call on individual items before loading
430 */
431TimeMap.loaders.mixin = function(loader, options) {
432    // set preload and transform functions
433    var dummy = function(data) { return data; };
434    loader.parse = options.parserFunction || dummy;
435    loader.preload = options.preloadFunction || dummy;
436    loader.transform = options.transformFunction || dummy;
437} 
438
439/**
440 * Map of common timeline intervals. Add custom intervals here if you
441 * want to refer to them by key rather than as literals.
442 */
443TimeMap.intervals = {
444    'sec': [DT.SECOND, DT.MINUTE],
445    'min': [DT.MINUTE, DT.HOUR],
446    'hr': [DT.HOUR, DT.DAY],
447    'day': [DT.DAY, DT.WEEK],
448    'wk': [DT.WEEK, DT.MONTH],
449    'mon': [DT.MONTH, DT.YEAR],
450    'yr': [DT.YEAR, DT.DECADE],
451    'dec': [DT.DECADE, DT.CENTURY]
452};
453
454/**
455 * Map of Google map types. Using keys rather than literals allows
456 * for serialization of the map type.
457 */
458TimeMap.mapTypes = {
459    'normal':G_NORMAL_MAP, 
460    'satellite':G_SATELLITE_MAP, 
461    'hybrid':G_HYBRID_MAP, 
462    'physical':G_PHYSICAL_MAP, 
463    'moon':G_MOON_VISIBLE_MAP, 
464    'sky':G_SKY_VISIBLE_MAP
465};
466
467/**
468 * Create an empty dataset object and add it to the timemap
469 *
470 * @param {String} id           The id of the dataset
471 * @param {Object} options      A container for optional arguments for dataset constructor
472 * @return {TimeMapDataset}     The new dataset object   
473 */
474TimeMap.prototype.createDataset = function(id, options) {
475    options = options || {}; // make sure the options object isn't null
476    if (!("title" in options)) {
477        options.title = id;
478    }
479    var dataset = new TimeMapDataset(this, options);
480    this.datasets[id] = dataset;
481    // add event listener
482    if (this.opts.centerOnItems) {
483        var tm = this;
484        GEvent.addListener(dataset, 'itemsloaded', function() {
485            var map = tm.map, bounds = tm.mapBounds;
486            // determine the zoom level from the bounds
487            map.setZoom(map.getBoundsZoomLevel(bounds));
488            // determine the center from the bounds
489            map.setCenter(bounds.getCenter());
490        });
491    }
492    return dataset;
493};
494
495/**
496 * Run a function on each dataset in the timemap. This is the preferred
497 * iteration method, as it allows for future iterator options.
498 *
499 * @param {Function} f    The function to run
500 */
501TimeMap.prototype.each = function(f) {
502    for (var id in this.datasets) {
503        if (this.datasets.hasOwnProperty(id)) {
504            f(this.datasets[id]);
505        }
506    }
507};
508
509/**
510 * Initialize the timeline - this must happen separately to allow full control of
511 * timeline properties.
512 *
513 * @param {BandInfo Array} bands    Array of band information objects for timeline
514 */
515TimeMap.prototype.initTimeline = function(bands) {
516   
517    // synchronize & highlight timeline bands
518    for (var x=1; x < bands.length; x++) {
519        if (this.opts.syncBands) {
520            bands[x].syncWith = (x-1);
521        }
522        bands[x].highlight = true;
523    }
524   
525    // initialize timeline
526    this.timeline = Timeline.create(this.tElement, bands);
527   
528    // set event listeners
529    var tm = this;
530    // update map on timeline scroll
531    this.timeline.getBand(0).addOnScrollListener(function() {
532        tm.filter("map");
533    });
534
535    // hijack timeline popup window to open info window
536    var painter = this.timeline.getBand(0).getEventPainter().constructor;
537    painter.prototype._showBubble = function(x, y, evt) {
538        evt.item.openInfoWindow();
539    };
540   
541    // filter chain for map placemarks
542    this.addFilterChain("map", 
543        function(item) {
544            item.showPlacemark();
545        },
546        function(item) {
547            item.hidePlacemark();
548        }
549    );
550   
551    // filter: hide when item is hidden
552    this.addFilter("map", function(item) {
553        return item.visible;
554    });
555    // filter: hide when dataset is hidden
556    this.addFilter("map", function(item) {
557        return item.dataset.visible;
558    });
559   
560    // filter: hide map items depending on timeline state
561    this.addFilter("map", this.opts.mapFilter);
562   
563    // filter chain for timeline events
564    this.addFilterChain("timeline", 
565        function(item) {
566            item.showEvent();
567        },
568        function(item) {
569            item.hideEvent();
570        }
571    );
572   
573    // filter: hide when item is hidden
574    this.addFilter("timeline", function(item) {
575        return item.visible;
576    });
577    // filter: hide when dataset is hidden
578    this.addFilter("timeline", function(item) {
579        return item.dataset.visible;
580    });
581   
582    // add callback for window resize
583    var resizeTimerID = null;
584    var oTimeline = this.timeline;
585    window.onresize = function() {
586        if (resizeTimerID === null) {
587            resizeTimerID = window.setTimeout(function() {
588                resizeTimerID = null;
589                oTimeline.layout();
590            }, 500);
591        }
592    };
593};
594
595/**
596 * Update items, hiding or showing according to filters
597 *
598 * @param {String} fid      Filter chain to update on
599 */
600TimeMap.prototype.filter = function(fid) {
601    var filters = this.filters[fid];
602    // if no filters exist, forget it
603    if (!filters || !filters.chain || filters.chain.length === 0) {
604        return;
605    }
606    // run items through filter
607    this.each(function(ds) {
608        ds.each(function(item) {
609            F_LOOP: { 
610                for (var i = filters.chain.length - 1; i >= 0; i--) {
611                    if (!filters.chain[i](item)) {
612                        // false condition
613                        filters.off(item);
614                        break F_LOOP;
615                    }
616                }
617                // true condition
618                filters.on(item);
619            }
620        });
621    });
622};
623
624/**
625 * Add a new filter chain
626 *
627 * @param {String} fid      Id of the filter chain
628 * @param {Function} fon    Function to run on an item if filter is true
629 * @param {Function} foff   Function to run on an item if filter is false
630 */
631TimeMap.prototype.addFilterChain = function(fid, fon, foff) {
632    this.filters[fid] = {
633        chain:[],
634        on: fon,
635        off: foff
636    };
637};
638
639/**
640 * Remove a filter chain
641 *
642 * @param {String} fid      Id of the filter chain
643 */
644TimeMap.prototype.removeFilterChain = function(fid, on, off) {
645    this.filters[fid] = null;
646};
647
648/**
649 * Add a function to a filter chain
650 *
651 * @param {String} fid      Id of the filter chain
652 * @param {Function} f      Function to add
653 */
654TimeMap.prototype.addFilter = function(fid, f) {
655    if (this.filters[fid] && this.filters[fid].chain) {
656        this.filters[fid].chain.push(f);
657    }
658};
659
660/**
661 * Remove a function from a filter chain
662 *
663 * @param {String} fid      Id of the filter chain
664 * XXX: Support index here
665 */
666TimeMap.prototype.removeFilter = function(fid) {
667    if (this.filters[fid] && this.filters[fid].chain) {
668        this.filters[fid].chain.pop();
669    }
670};
671
672/**
673 * Map of different filter functions. Adding new filters to this
674 * map allows them to be specified by string name.
675 */
676TimeMap.filters = {};
677
678/**
679 * Static filter function: Hide items not shown on the timeline
680 *
681 * @param {TimeMapItem} item    Item to test for filter
682 * @return {Boolean}            Whether to show the item
683 */
684TimeMap.filters.hidePastFuture = function(item) {
685    var topband = item.dataset.timemap.timeline.getBand(0);
686    var maxVisibleDate = topband.getMaxVisibleDate().getTime();
687    var minVisibleDate = topband.getMinVisibleDate().getTime();
688    if (item.event !== null) {
689        var itemStart = item.event.getStart().getTime();
690        var itemEnd = item.event.getEnd().getTime();
691        // hide items in the future
692        if (itemStart > maxVisibleDate) {
693            return false;
694        } 
695        // hide items in the past
696        else if (itemEnd < minVisibleDate || 
697            (item.event.isInstant() && itemStart < minVisibleDate)) {
698            return false;
699        }
700    }
701    return true;
702};
703
704/**
705 * Static filter function: Hide items not shown on the timeline
706 *
707 * @param {TimeMapItem} item    Item to test for filter
708 * @return {Boolean}            Whether to show the item
709 */
710TimeMap.filters.showMomentOnly = function(item) {
711    var topband = item.dataset.timemap.timeline.getBand(0);
712    var momentDate = topband.getCenterVisibleDate().getTime();
713    if (item.event !== null) {
714        var itemStart = item.event.getStart().getTime();
715        var itemEnd = item.event.getEnd().getTime();
716        // hide items in the future
717        if (itemStart > momentDate) {
718            return false;
719        } 
720        // hide items in the past
721        else if (itemEnd < momentDate || 
722            (item.event.isInstant() && itemStart < momentDate)) {
723            return false;
724        }
725    }
726    return true;
727};
728
729/*----------------------------------------------------------------------------
730 * TimeMapDataset Class - holds references to items and visual themes
731 *---------------------------------------------------------------------------*/
732
733/**
734 * Create a new TimeMap dataset to hold a set of items
735 *
736 * @constructor
737 * @param {TimeMap} timemap         Reference to the timemap object
738 * @param {Object} options          Object holding optional arguments:
739 *   {String} title                     Title of the dataset (for the legend)
740 *   {String or theme object} theme     Theme settings.
741 *   {String or Function} dateParser    Function to replace default date parser.
742 *   {Function} openInfoWindow          Function redefining how info window opens
743 *   {Function} closeInfoWindow         Function redefining how info window closes
744 */
745function TimeMapDataset(timemap, options) {
746    // hold reference to timemap
747    this.timemap = timemap;
748    // initialize timeline event source
749    this.eventSource = new Timeline.DefaultEventSource();
750    // initialize array of items
751    this.items = [];
752    // for show/hide functions
753    this.visible = true;
754   
755    // set defaults for options
756    this.opts = options || {}; // make sure the options object isn't null
757    this.opts.title = options.title || "";
758   
759    // get theme by key or object
760    if (typeof(options.theme) == "string") {
761        options.theme = TimeMapDataset.themes[options.theme];
762    }
763    this.opts.theme = options.theme || this.timemap.opts.theme || new TimeMapDatasetTheme({});
764    // allow icon path override in options or timemap options
765    this.opts.theme.eventIconPath = options.eventIconPath || 
766        this.timemap.opts.eventIconPath || this.opts.theme.eventIconPath;
767    this.opts.theme.eventIcon = options.eventIconPath + this.opts.theme.eventIconImage;
768   
769    // allow for other data parsers (e.g. Gregorgian) by key or function
770    if (typeof(options.dateParser) == "string") { 
771        options.dateParser = TimeMapDataset.dateParsers[options.dateParser];
772    }
773    this.opts.dateParser = options.dateParser || TimeMapDataset.hybridParser;
774   
775    // get functions
776    this.getItems = function() { return this.items; };
777    this.getTitle = function() { return this.opts.title; };
778}
779
780/**
781 * Wrapper to fix Timeline Gregorian parser for invalid strings
782 *
783 * @param {String} s    String to parse into a Date object
784 * @return {Date}       Parsed date or null
785 */
786TimeMapDataset.gregorianParser = function(s) {
787    d = DT.parseGregorianDateTime(s);
788    // check for invalid dates
789    if (!d.getFullYear()) d = null;
790    return d;
791};
792
793/**
794 * Parse dates with the ISO 8601 parser, then fall back on the Gregorian
795 * parser if the first parse fails
796 *
797 * @param {String} s    String to parse into a Date object
798 * @return {Date}       Parsed date or null
799 */
800TimeMapDataset.hybridParser = function(s) {
801    var d = DT.parseIso8601DateTime(s);
802    if (!d) {
803        d = TimeMapDataset.gregorianParser(s);
804    }
805    return d;
806};
807
808/**
809 * Map of supported date parsers. Add custom date parsers here if you
810 * want to refer to them by key rather than as a function name.
811 */
812TimeMapDataset.dateParsers = {
813    'hybrid': TimeMapDataset.hybridParser,
814    'iso8601': DT.parseIso8601DateTime,
815    'gregorian': TimeMapDataset.gregorianParser
816};
817
818/**
819 * Run a function on each item in the dataset. This is the preferred
820 * iteration method, as it allows for future iterator options.
821 *
822 * @param {Function} f    The function to run
823 */
824TimeMapDataset.prototype.each = function(f) {
825    for (var x=0; x < this.items.length; x++) {
826        f(this.items[x]);
827    }
828};
829
830/**
831 * Add items to map and timeline.
832 * Each item has both a timeline event and a map placemark.
833 *
834 * @param {Object} data             Data to be loaded. See loadItem() below for the format.
835 * @param {Function} transform      If data is not in the above format, transformation function to make it so
836 */
837TimeMapDataset.prototype.loadItems = function(data, transform) {
838    for (var x=0; x < data.length; x++) {
839        this.loadItem(data[x], transform);
840    }
841    GEvent.trigger(this, 'itemsloaded');
842};
843
844/*
845 * Add one item to map and timeline.
846 * Each item has both a timeline event and a map placemark.
847 *
848 * @param {Object} data         Data to be loaded, in the following format:
849 *      {String} title              Title of the item (visible on timeline)
850 *      {DateTime} start            Start time of the event on the timeline
851 *      {DateTime} end              End time of the event on the timeline (duration events only)
852 *      {Object} point              Data for a single-point placemark:
853 *          {Float} lat                 Latitude of map marker
854 *          {Float} lon                 Longitude of map marker
855 *      {Array of points} polyline  Data for a polyline placemark, in format above
856 *      {Array of points} polygon   Data for a polygon placemark, in format above
857 *      {Object} overlay            Data for a ground overlay:
858 *          {String} image              URL of image to overlay
859 *          {Float} north               Northern latitude of the overlay
860 *          {Float} south               Southern latitude of the overlay
861 *          {Float} east                Eastern longitude of the overlay
862 *          {Float} west                Western longitude of the overlay
863 *      {Object} options            Optional arguments to be passed to the TimeMapItem (@see TimeMapItem)
864 * @param {Function} transform  If data is not in the above format, transformation function to make it so
865 */
866TimeMapDataset.prototype.loadItem = function(data, transform) {
867    // apply transformation, if any
868    if (transform !== undefined) {
869        data = transform(data);
870    }
871    // transform functions can return a null value to skip a datum in the set
872    if (data === null) {
873        return;
874    }
875   
876    // use item theme if provided, defaulting to dataset theme
877    var options = data.options || {};
878    if (typeof(options.theme) == "string") {
879        options.theme = TimeMapDataset.themes[options.theme];
880    }
881    var theme = options.theme || this.opts.theme;
882    theme.eventIconPath = options.eventIconPath || this.opts.theme.eventIconPath;
883    theme.eventIcon = theme.eventIconPath + theme.eventIconImage;
884   
885    var tm = this.timemap;
886   
887    // create timeline event
888    var parser = this.opts.dateParser, start = data.start, end = data.end, instant;
889    start = (start === undefined||start === "") ? null : parser(start);
890    end = (end === undefined||end === "") ? null : parser(end);
891    instant = (end === undefined);
892    var eventIcon = theme.eventIcon,
893        title = data.title,
894        // allow event-less placemarks - these will be always present on map
895        event = null;
896    if (start !== null) {
897        var eventClass = Timeline.DefaultEventSource.Event;
898        if (TimeMap.TimelineVersion() == "1.2") {
899            // attributes by parameter
900            event = new eventClass(start, end, null, null,
901                instant, title, null, null, null, eventIcon, theme.eventColor, 
902                theme.eventTextColor);
903        } else {
904            var textColor = theme.eventTextColor;
905            if (!textColor) {
906                // tweak to show old-style events
907                textColor = (theme.classicTape && !instant) ? '#FFFFFF' : '#000000';
908            }
909            // attributes in object
910            event = new eventClass({
911                "start": start,
912                "end": end,
913                "instant": instant,
914                "text": title,
915                "icon": eventIcon,
916                "color": theme.eventColor,
917                "textColor": textColor
918            });
919        }
920    }
921   
922    // set the icon, if any, outside the closure
923    var markerIcon = ("icon" in data) ? data.icon : theme.icon,
924        bounds = tm.mapBounds; // save some bytes
925   
926   
927    // internal function: create map placemark
928    // takes a data object (could be full data, could be just placemark)
929    // returns an object with {placemark, type, point}
930    var createPlacemark = function(pdata) {
931        var placemark = null, type = "", point = null;
932        // point placemark
933        if ("point" in pdata) {
934            point = new GLatLng(
935                parseFloat(pdata.point.lat), 
936                parseFloat(pdata.point.lon)
937            );
938            // add point to visible map bounds
939            if (tm.opts.centerOnItems) {
940                bounds.extend(point);
941            }
942            placemark = new GMarker(point, { icon: markerIcon });
943            type = "marker";
944            point = placemark.getLatLng();
945        }
946        // polyline and polygon placemarks
947        else if ("polyline" in pdata || "polygon" in pdata) {
948            var points = [], line;
949            if ("polyline" in pdata) {
950                line = pdata.polyline;
951            } else {
952                line = pdata.polygon;
953            }
954            for (var x=0; x<line.length; x++) {
955                point = new GLatLng(
956                    parseFloat(line[x].lat), 
957                    parseFloat(line[x].lon)
958                );
959                points.push(point);
960                // add point to visible map bounds
961                if (tm.opts.centerOnItems) {
962                    bounds.extend(point);
963                }
964            }
965            if ("polyline" in pdata) {
966                placemark = new GPolyline(points, 
967                                          theme.lineColor, 
968                                          theme.lineWeight,
969                                          theme.lineOpacity);
970                type = "polyline";
971                point = placemark.getVertex(Math.floor(placemark.getVertexCount()/2));
972            } else {
973                placemark = new GPolygon(points, 
974                                         theme.polygonLineColor, 
975                                         theme.polygonLineWeight,
976                                         theme.polygonLineOpacity,
977                                         theme.fillColor,
978                                         theme.fillOpacity);
979                type = "polygon";
980                point = placemark.getBounds().getCenter();
981            }
982        } 
983        // ground overlay placemark
984        else if ("overlay" in pdata) {
985            var sw = new GLatLng(
986                parseFloat(pdata.overlay.south), 
987                parseFloat(pdata.overlay.west)
988            );
989            var ne = new GLatLng(
990                parseFloat(pdata.overlay.north), 
991                parseFloat(pdata.overlay.east)
992            );
993            // add to visible bounds
994            if (tm.opts.centerOnItems) {
995                bounds.extend(sw);
996                bounds.extend(ne);
997            }
998            // create overlay
999            var overlayBounds = new GLatLngBounds(sw, ne);
1000            placemark = new GGroundOverlay(pdata.overlay.image, overlayBounds);
1001            type = "overlay";
1002            point = overlayBounds.getCenter();
1003        }
1004        return {
1005            "placemark": placemark,
1006            "type": type,
1007            "point": point
1008        };
1009    };
1010   
1011    // create placemark or placemarks
1012    var placemark = [], pdataArr = [], pdata = null, type = "", point = null, i;
1013    // array of placemark objects
1014    if ("placemarks" in data) {
1015        pdataArr = data.placemarks;
1016    } else {
1017        // we have one or more single placemarks
1018        var types = ["point", "polyline", "polygon", "overlay"];
1019        for (i=0; i<types.length; i++) {
1020            if (types[i] in data) {
1021                pdata = {};
1022                pdata[types[i]] = data[types[i]];
1023                pdataArr.push(pdata);
1024            }
1025        }
1026    }
1027    if (pdataArr) {
1028        for (i=0; i<pdataArr.length; i++) {
1029            // create the placemark
1030            var p = createPlacemark(pdataArr[i]);
1031            // take the first point and type as a default
1032            point = point || p.point;
1033            type = type || p.type;
1034            placemark.push(p.placemark);
1035        }
1036    }
1037    // override type for arrays
1038    if (placemark.length > 1) {
1039        type = "array";
1040    }
1041   
1042    options.title = title;
1043    options.type = type || "none";
1044    options.theme = theme;
1045    // check for custom infoPoint and convert to GLatLng
1046    if (options.infoPoint) {
1047        options.infoPoint = new GLatLng(
1048            parseFloat(options.infoPoint.lat), 
1049            parseFloat(options.infoPoint.lon)
1050        );
1051    } else {
1052        options.infoPoint = point;
1053    }
1054   
1055    // create item and cross-references
1056    var item = new TimeMapItem(placemark, event, this, options);
1057    // add event if it exists
1058    if (event !== null) {
1059        event.item = item;
1060        this.eventSource.add(event);
1061    }
1062    // add placemark(s) if any exist
1063    if (placemark.length > 0) {
1064        for (i=0; i<placemark.length; i++) {
1065            placemark[i].item = item;
1066            // add listener to make placemark open when event is clicked
1067            GEvent.addListener(placemark[i], "click", function() {
1068                item.openInfoWindow();
1069            });
1070            // add placemark and event to map and timeline
1071            tm.map.addOverlay(placemark[i]);
1072            // hide placemarks until the next refresh
1073            placemark[i].hide();
1074        }
1075    }
1076    // add the item to the dataset
1077    this.items.push(item);
1078    // return the item object
1079    return item;
1080};
1081
1082/*----------------------------------------------------------------------------
1083 * Predefined visual themes for datasets, based on Google markers
1084 *---------------------------------------------------------------------------*/
1085
1086/**
1087 * Create a new theme for a TimeMap dataset, defining colors and images
1088 *
1089 * @constructor
1090 * @param {Object} options          A container for optional arguments:
1091 *      {GIcon} icon                    Icon for marker placemarks
1092 *      {String} color                  Default color in hex for events, polylines, polygons
1093 *      {String} lineColor              Color for polylines, defaults to options.color
1094 *      {String} polygonLineColor       Color for polygon outlines, defaults to lineColor
1095 *      {Number} lineOpacity            Opacity for polylines
1096 *      {Number} polgonLineOpacity      Opacity for polygon outlines, defaults to options.lineOpacity
1097 *      {Number} lineWeight             Line weight in pixels for polylines
1098 *      {Number} polygonLineWeight      Line weight for polygon outlines, defaults to options.lineWeight
1099 *      {String} fillColor              Color for polygon fill, defaults to options.color
1100 *      {String} fillOpacity            Opacity for polygon fill
1101 *      {String} eventColor             Background color for duration events
1102 *      {URL} eventIcon                 Icon URL for instant events
1103 */
1104function TimeMapDatasetTheme(options) {
1105    // work out various defaults - the default theme is Google's reddish color
1106    options = options || {};
1107   
1108    if (!options.icon) {
1109        // make new red icon
1110        var markerIcon = new GIcon(G_DEFAULT_ICON);
1111        this.iconImage = options.iconImage || GIP + "red-dot.png";
1112        markerIcon.image = this.iconImage;
1113        markerIcon.iconSize = new GSize(32, 32);
1114        markerIcon.shadow = GIP + "msmarker.shadow.png";
1115        markerIcon.shadowSize = new GSize(59, 32);
1116        markerIcon.iconAnchor = new GPoint(16, 33);
1117        markerIcon.infoWindowAnchor = new GPoint(18, 3);
1118    }
1119   
1120    this.icon =              options.icon || markerIcon;
1121    this.color =             options.color || "#FE766A";
1122    this.lineColor =         options.lineColor || this.color;
1123    this.polygonLineColor =  options.polygonLineColor || this.lineColor;
1124    this.lineOpacity =       options.lineOpacity || 1;
1125    this.polgonLineOpacity = options.polgonLineOpacity || this.lineOpacity;
1126    this.lineWeight =        options.lineWeight || 2;
1127    this.polygonLineWeight = options.polygonLineWeight || this.lineWeight;
1128    this.fillColor =         options.fillColor || this.color;
1129    this.fillOpacity =       options.fillOpacity || 0.25;
1130    this.eventColor =        options.eventColor || this.color;
1131    this.eventTextColor =    options.eventTextColor || null;
1132    this.eventIconPath =     options.eventIconPath || "timemap/images/";
1133    this.eventIconImage =    options.eventIconImage || "red-circle.png";
1134    this.eventIcon =         options.eventIcon || this.eventIconPath + this.eventIconImage;
1135   
1136    // whether to use the older "tape" event style for the newer Timeline versions
1137    // NB: this needs additional css to work - see examples/artists.html
1138    this.classicTape = ("classicTape" in options) ? options.classicTape : false;
1139}
1140
1141TimeMapDataset.redTheme = function(options) {
1142    return new TimeMapDatasetTheme(options);
1143};
1144
1145TimeMapDataset.blueTheme = function(options) {
1146    options = options || {};
1147    options.iconImage = GIP + "blue-dot.png";
1148    options.color = "#5A7ACF";
1149    options.eventIconImage = "blue-circle.png";
1150    return new TimeMapDatasetTheme(options);
1151};
1152
1153TimeMapDataset.greenTheme = function(options) {
1154    options = options || {};
1155    options.iconImage = GIP + "green-dot.png";
1156    options.color = "#19CF54";
1157    options.eventIconImage = "green-circle.png";
1158    return new TimeMapDatasetTheme(options);
1159};
1160
1161TimeMapDataset.ltblueTheme = function(options) {
1162    options = options || {};
1163    options.iconImage = GIP + "ltblue-dot.png";
1164    options.color = "#5ACFCF";
1165    options.eventIconImage = "ltblue-circle.png";
1166    return new TimeMapDatasetTheme(options);
1167};
1168
1169TimeMapDataset.purpleTheme = function(options) {
1170    options = options || {};
1171    options.iconImage = GIP + "purple-dot.png";
1172    options.color = "#8E67FD";
1173    options.eventIconImage = "purple-circle.png";
1174    return new TimeMapDatasetTheme(options);
1175};
1176
1177TimeMapDataset.orangeTheme = function(options) {
1178    options = options || {};
1179    options.iconImage = GIP + "orange-dot.png";
1180    options.color = "#FF9900";
1181    options.eventIconImage = "orange-circle.png";
1182    return new TimeMapDatasetTheme(options);
1183};
1184
1185TimeMapDataset.yellowTheme = function(options) {
1186    options = options || {};
1187    options.iconImage = GIP + "yellow-dot.png";
1188    options.color = "#ECE64A";
1189    options.eventIconImage = "yellow-circle.png";
1190    return new TimeMapDatasetTheme(options);
1191};
1192
1193/**
1194 * Map of themes. Add custom themes to this map if you want
1195 * to load them by key rather than as an object.
1196 */
1197TimeMapDataset.themes = {
1198    'red': TimeMapDataset.redTheme(),
1199    'blue': TimeMapDataset.blueTheme(),
1200    'green': TimeMapDataset.greenTheme(),
1201    'ltblue': TimeMapDataset.ltblueTheme(),
1202    'orange': TimeMapDataset.orangeTheme(),
1203    'yellow': TimeMapDataset.yellowTheme(),
1204    'purple': TimeMapDataset.purpleTheme()
1205};
1206
1207
1208/*----------------------------------------------------------------------------
1209 * TimeMapItem Class - holds references to map placemark and timeline event
1210 *---------------------------------------------------------------------------*/
1211
1212/**
1213 * Create a new TimeMap item with a map placemark and a timeline event
1214 *
1215 * @constructor
1216 * @param {placemark} placemark     Placemark or array of placemarks (GMarker, GPolyline, etc)
1217 * @param {Event} event             The timeline event
1218 * @param {TimeMapDataset} dataset  Reference to the parent dataset object
1219 * @param {Object} options          A container for optional arguments:
1220 *   {String} title                     Title of the item
1221 *   {String} description               Plain-text description of the item
1222 *   {String} type                      Type of map placemark used (marker. polyline, polygon)
1223 *   {GLatLng} infoPoint                Point indicating the center of this item
1224 *   {String} infoHtml                  Full HTML for the info window
1225 *   {String} infoUrl                   URL from which to retrieve full HTML for the info window
1226 *   {Function} openInfoWindow          Function redefining how info window opens
1227 *   {Function} closeInfoWindow         Function redefining how info window closes
1228 */
1229function TimeMapItem(placemark, event, dataset, options) {
1230    // initialize vars
1231    this.event =     event;
1232    this.dataset =   dataset;
1233    this.map =       dataset.timemap.map;
1234   
1235    // initialize placemark(s) with some type juggling
1236    if (placemark && TimeMap.isArray(placemark) && placemark.length === 0) {
1237        placemark = null;
1238    }
1239    if (placemark && placemark.length == 1) {
1240        placemark = placemark[0];
1241    }
1242    this.placemark = placemark;
1243   
1244    // set defaults for options
1245    this.opts = options || {};
1246    this.opts.type =        options.type || '';
1247    this.opts.title =       options.title || '';
1248    this.opts.description = options.description || '';
1249    this.opts.infoPoint =   options.infoPoint || null;
1250    this.opts.infoHtml =    options.infoHtml || '';
1251    this.opts.infoUrl =     options.infoUrl || '';
1252   
1253    // get functions
1254    this.getType = function() { return this.opts.type; };
1255    this.getTitle = function() { return this.opts.title; };
1256    this.getInfoPoint = function() { 
1257        // default to map center if placemark not set
1258        return this.opts.infoPoint || this.map.getCenter(); 
1259    };
1260   
1261    // items initialize visible
1262    this.visible = true;
1263    // placemarks initialize hidden
1264    this.placemarkVisible = false;
1265    // events initialize visible
1266    this.eventVisible = true;
1267   
1268    // allow for custom open/close functions, set at item, dataset, or timemap level
1269    this.openInfoWindow =   options.openInfoWindow ||
1270        dataset.opts.openInfoWindow ||
1271        dataset.timemap.opts.openInfoWindow ||
1272        false;
1273    if (!this.openInfoWindow) {
1274        if (this.opts.infoUrl !== "") {
1275            // load via AJAX if URL is provided
1276            this.openInfoWindow = TimeMapItem.openInfoWindowAjax;
1277        } else {
1278            // otherwise default to basic window
1279            this.openInfoWindow = TimeMapItem.openInfoWindowBasic;
1280        }
1281    }
1282    this.closeInfoWindow = options.closeInfoWindow ||
1283        dataset.opts.closeInfoWindow ||
1284        dataset.timemap.opts.closeInfoWindow ||
1285        TimeMapItem.closeInfoWindowBasic;
1286}
1287
1288/**
1289 * Show the map placemark
1290 */
1291TimeMapItem.prototype.showPlacemark = function() {
1292    if (this.placemark) {
1293        if (this.getType() == "array") {
1294            for (var i=0; i<this.placemark.length; i++) {
1295                this.placemark[i].show();
1296            }
1297        } else {
1298            this.placemark.show();
1299        }
1300        this.placemarkVisible = true;
1301    }
1302};
1303
1304/**
1305 * Hide the map placemark
1306 */
1307TimeMapItem.prototype.hidePlacemark = function() {
1308    if (this.placemark) {
1309        if (this.getType() == "array") {
1310            for (var i=0; i<this.placemark.length; i++) {
1311                this.placemark[i].hide();
1312            }
1313        } else {
1314            this.placemark.hide();
1315        }
1316        this.placemarkVisible = false;
1317    }
1318    this.closeInfoWindow();
1319};
1320
1321/**
1322 * Show the timeline event
1323 * NB: Will likely require calling timeline.layout()
1324 */
1325TimeMapItem.prototype.showEvent = function() {
1326    if (this.event) {
1327        if (this.eventVisible === false){
1328            this.dataset.timemap.timeline.getBand(0)
1329                .getEventSource()._events._events.add(this.event);
1330        }
1331        this.eventVisible = true;
1332    }
1333};
1334
1335/**
1336 * Show the timeline event
1337 * NB: Will likely require calling timeline.layout()
1338 */
1339TimeMapItem.prototype.hideEvent = function() {
1340    if (this.event) {
1341        if (this.eventVisible == true){
1342            this.dataset.timemap.timeline.getBand(0)
1343                .getEventSource()._events._events.remove(this.event);
1344        }
1345        this.eventVisible = false;
1346    }
1347};
1348
1349/**
1350 * Standard open info window function, using static text in map window
1351 */
1352TimeMapItem.openInfoWindowBasic = function() {
1353    var html = this.opts.infoHtml;
1354    // create content for info window if none is provided
1355    if (html === "") {
1356        html = '<div class="infotitle">' + this.opts.title + '</div>';
1357        if (this.opts.description !== "") {
1358            html += '<div class="infodescription">' + this.opts.description + '</div>';
1359        }
1360    }
1361    // scroll timeline if necessary
1362    if (this.placemark && !this.visible && this.event) {
1363        var topband = this.dataset.timemap.timeline.getBand(0);
1364        topband.setCenterVisibleDate(this.event.getStart());
1365    }
1366    // open window
1367    if (this.getType() == "marker") {
1368        this.placemark.openInfoWindowHtml(html);
1369    } else {
1370        this.map.openInfoWindowHtml(this.getInfoPoint(), html);
1371    }
1372    // custom functions will need to set this as well
1373    this.selected = true;
1374};
1375
1376/**
1377 * Open info window function using ajax-loaded text in map window
1378 */
1379TimeMapItem.openInfoWindowAjax = function() {
1380    if (this.opts.infoHtml !== "") { // already loaded - change to static
1381        this.openInfoWindow = TimeMapItem.openInfoWindowBasic;
1382        this.openInfoWindow();
1383    } else { // load content via AJAX
1384        if (this.opts.infoUrl !== "") {
1385            var item = this;
1386            GDownloadUrl(this.opts.infoUrl, function(result) {
1387                    item.opts.infoHtml = result;
1388                    item.openInfoWindow();
1389            });
1390        } else { // fall back on basic function
1391            this.openInfoWindow = TimeMapItem.openInfoWindowBasic;
1392            this.openInfoWindow();
1393        }
1394    }
1395};
1396
1397/**
1398 * Standard close window function, using the map window
1399 */
1400TimeMapItem.closeInfoWindowBasic = function() {
1401    if (this.getType() == "marker") {
1402        this.placemark.closeInfoWindow();
1403    } else {
1404        var infoWindow = this.map.getInfoWindow();
1405        // close info window if its point is the same as this item's point
1406        if (infoWindow.getPoint() == this.getInfoPoint() && !infoWindow.isHidden()) {
1407            this.map.closeInfoWindow();
1408        }
1409    }
1410    // custom functions will need to set this as well
1411    this.selected = false;
1412};
1413
1414/*----------------------------------------------------------------------------
1415 * Utility functions, attached to TimeMap to avoid namespace issues
1416 *---------------------------------------------------------------------------*/
1417
1418/**
1419 * Convenience trim function
1420 *
1421 * @param {String} str      String to trim
1422 * @return {String}         Trimmed string
1423 */
1424TimeMap.trim = function(str) {
1425    str = str && String(str) || '';
1426    return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
1427};
1428
1429/**
1430 * Convenience array tester
1431 *
1432 * @param {Object} o        Object to test
1433 * @return {Boolean}        Whether the object is an array
1434 */
1435TimeMap.isArray = function(o) {   
1436    return o && !(o.propertyIsEnumerable('length')) && 
1437        typeof o === 'object' && typeof o.length === 'number';
1438};
1439
1440/**
1441 * Get XML tag value as a string
1442 *
1443 * @param {XML Node} n      Node in which to look for tag
1444 * @param {String} tag      Name of tag to look for
1445 * @param {String} ns       Optional namespace
1446 * @return {String}         Tag value as string
1447 */
1448TimeMap.getTagValue = function(n, tag, ns) {
1449    var str = "";
1450    var nList = TimeMap.getNodeList(n, tag, ns);
1451    if (nList.length > 0) {
1452        n = nList[0].firstChild;
1453        // fix for extra-long nodes
1454        // see http://code.google.com/p/timemap/issues/detail?id=36
1455        while(n !== null) {
1456            str += n.nodeValue;
1457            n = n.nextSibling;
1458        }
1459    }
1460    return str;
1461};
1462
1463/**
1464 * Empty container for mapping XML namespaces to URLs
1465 */
1466TimeMap.nsMap = {};
1467
1468/**
1469 * Cross-browser implementation of getElementsByTagNameNS
1470 * Note: Expects any applicable namespaces to be mapped in
1471 * TimeMap.nsMap. XXX: There may be better ways to do this.
1472 *
1473 * @param {XML Node} n      Node in which to look for tag
1474 * @param {String} tag      Name of tag to look for
1475 * @param {String} ns       Optional namespace
1476 * @return {XML Node List}  List of nodes with the specified tag name
1477 */
1478TimeMap.getNodeList = function(n, tag, ns) {
1479    if (ns === undefined) {
1480        // no namespace
1481        return n.getElementsByTagName(tag);
1482    }
1483    if (n.getElementsByTagNameNS && TimeMap.nsMap[ns]) {
1484        // function and namespace both exist
1485        return n.getElementsByTagNameNS(TimeMap.nsMap[ns], tag);
1486    }
1487    // no function, try the colon tag name
1488    return n.getElementsByTagName(ns + ':' + tag);
1489};
1490
1491/**
1492 * Make TimeMap.init()-style points from a GLatLng, array, or string
1493 *
1494 * @param {Object} coords       GLatLng, array, or string to convert
1495 * @param {Boolean} reversed    Whether the points are KML-style lon/lat, rather than lat/lon
1496 * @return {Object}             TimeMap.init()-style point
1497 */
1498TimeMap.makePoint = function(coords, reversed) {
1499    var latlon = null;
1500    // GLatLng
1501    if (coords.lat && coords.lng) {
1502        latlon = [coords.lat(), coords.lng()];
1503    }
1504    // array of coordinates
1505    if (TimeMap.isArray(coords)) {
1506        latlon = coords;
1507    }
1508    // string
1509    if (latlon === null) {
1510        // trim extra whitespace
1511        coords = TimeMap.trim(coords);
1512        if (coords.indexOf(',') > -1) {
1513            // split on commas
1514            latlon = coords.split(",");
1515        } else {
1516            // split on whitespace
1517            latlon = coords.split(/[\r\n\f ]+/);
1518        }
1519    }
1520    if (reversed) latlon.reverse();
1521    return {
1522        "lat": TimeMap.trim(latlon[0]),
1523        "lon": TimeMap.trim(latlon[1])
1524    };
1525};
1526
1527/**
1528 * Make TimeMap.init()-style polyline/polygons from a whitespace-delimited
1529 * string of coordinates (such as those in GeoRSS and KML)
1530 * XXX: Any reason for this to take arrays of GLatLngs as well?
1531 *
1532 * @param {Object} coords       String to convert
1533 * @param {Boolean} reversed    Whether the points are KML-style lon/lat, rather than lat/lon
1534 * @return {Object}             Formated coordinate array
1535 */
1536TimeMap.makePoly = function(coords, reversed) {
1537    var poly = [], latlon;
1538    var coordArr = TimeMap.trim(coords).split(/[\r\n\f ]+/);
1539    if (coordArr.length == 0) return [];
1540    // loop through coordinates
1541    for (var x=0; x<coordArr.length; x++) {
1542        latlon = (coordArr[x].indexOf(',')) ?
1543            // comma-separated coordinates (KML-style lon/lat)
1544            latlon = coordArr[x].split(",") :
1545            // space-separated coordinates - increment to step by 2s
1546            latlon = [coordArr[x], coordArr[++x]];
1547        if (reversed) latlon.reverse();
1548        poly.push({
1549            "lat": latlon[0],
1550            "lon": latlon[1]
1551        });
1552    }
1553    return poly;
1554}
1555
1556/**
1557 * Format a date as an ISO 8601 string
1558 *
1559 * @param {Date} d          Date to format
1560 * @param {int} precision   Optional precision indicator:
1561 *                              3 (default): Show full date and time
1562 *                              2: Show full date and time, omitting seconds
1563 *                              1: Show date only
1564 * @return {String}         Formatted string
1565 */
1566TimeMap.formatDate = function(d, precision) {
1567    // default to high precision
1568    precision = precision || 3;
1569    var str = "";
1570    if (d) {
1571        // check for date.js support
1572        if (d.toISOString) {
1573            return d.toISOString();
1574        }
1575        // otherwise, build ISO 8601 string
1576        var pad = function(num) {
1577            return ((num < 10) ? "0" : "") + num;
1578        };
1579        var yyyy = d.getUTCFullYear(),
1580            mo = d.getUTCMonth(),
1581            dd = d.getUTCDate();
1582        str += yyyy + '-' + pad(mo + 1 ) + '-' + pad(dd);
1583        // show time if top interval less than a week
1584        if (precision > 1) {
1585            var hh = d.getUTCHours(),
1586                mm = d.getUTCMinutes(),
1587                ss = d.getUTCSeconds();
1588            str += 'T' + pad(hh) + ':' + pad(mm);
1589            // show seconds if the interval is less than a day
1590            if (precision > 2) {
1591                str += pad(ss);
1592            }
1593            str += 'Z';
1594        }
1595    }
1596    return str;
1597};
1598
1599/**
1600 * Determine the SIMILE Timeline version
1601 * XXX: quite rough at the moment
1602 *
1603 * @return {String}     At the moment, only "1.2", "2.2.0", or what Timeline provides
1604 */
1605TimeMap.TimelineVersion = function() {
1606    // check for Timeline.version support - added in 2.3.0
1607    if (Timeline.version) {
1608        return Timeline.version;
1609    }
1610    if (Timeline.DurationEventPainter) {
1611        return "1.2";
1612    } else {
1613        return "2.2.0";
1614    }
1615};
Note: See TracBrowser for help on using the repository browser.