/*! * Timemap.js Copyright 2008 Nick Rabinowitz. * Licensed under the MIT License (see LICENSE.txt) */ /** * @fileOverview * Core functions for the timemap.js library. * Timemap.js is intended to sync a SIMILE Timeline with a Google Map. * Dependencies: Google Maps API v2, SIMILE Timeline v1.2 - 2.3.1 * Thanks to Jorn Clausen (http://www.oe-files.de) for initial concept and code. * * @author Nick Rabinowitz (www.nickrabinowitz.com) */ // globals - for JSLint /*global GBrowserIsCompatible, GLargeMapControl, GLatLngBounds, GMap2 */ /*global GMapTypeControl, GDownloadUrl, GEvent, GGroundOverlay, GIcon */ /*global GMarker, GPolygon, GPolyline, GSize, GLatLng, G_DEFAULT_ICON */ /*global G_DEFAULT_MAP_TYPES, G_NORMAL_MAP, G_PHYSICAL_MAP, G_HYBRID_MAP */ /*global G_MOON_VISIBLE_MAP, G_SKY_VISIBLE_MAP, G_SATELLITE_MAP, Timeline */ // A couple of aliases to save a few bytes var DT = Timeline.DateTime, // Google icon path GIP = "http://www.google.com/intl/en_us/mapfiles/ms/icons/"; /*---------------------------------------------------------------------------- * TimeMap Class *---------------------------------------------------------------------------*/ /** * @class * The TimeMap object holds references to timeline, map, and datasets. * This will create the visible map, but not the timeline, which must be initialized separately. * * @constructor * @param {element} tElement The timeline element. * @param {element} mElement The map element. * @param {Object} options A container for optional arguments:
 *   {Boolean} syncBands            Whether to synchronize all bands in timeline
 *   {GLatLng} mapCenter            Point for map center
 *   {Number} mapZoom               Intial map zoom level
 *   {GMapType/String} mapType      The maptype for the map
 *   {Array} mapTypes               The set of maptypes available for the map
 *   {Function/String} mapFilter    How to hide/show map items depending on timeline state;
                                    options: "hidePastFuture", "showMomentOnly", or function
 *   {Boolean} showMapTypeCtrl      Whether to display the map type control
 *   {Boolean} showMapCtrl          Whether to show map navigation control
 *   {Boolean} centerMapOnItems     Whether to center and zoom the map based on loaded item positions
 *   {Function} openInfoWindow      Function redefining how info window opens
 *   {Function} closeInfoWindow     Function redefining how info window closes
 * 
*/ function TimeMap(tElement, mElement, options) { // save elements /** * Map element * @type DOM Element */ this.mElement = mElement; /** * Timeline element * @type DOM Element */ this.tElement = tElement; /** * Map of datasets * @type Object */ this.datasets = {}; /** * Filter chains for this timemap * @type Object */ this.filters = {}; /** * Bounds of the map * @type GLatLngBounds */ this.mapBounds = new GLatLngBounds(); // set defaults for options /** * Container for optional settings passed in the "options" parameter * @type Object */ this.opts = options || {}; // make sure the options object isn't null // allow map types to be specified by key if (typeof(options.mapType) == 'string') { options.mapType = TimeMap.mapTypes[options.mapType]; } // allow map filters to be specified by key if (typeof(options.mapFilter) == 'string') { options.mapFilter = TimeMap.filters[options.mapFilter]; } // these options only needed for map initialization var mapCenter = options.mapCenter || new GLatLng(0,0), mapZoom = options.mapZoom || 0, mapType = options.mapType || G_PHYSICAL_MAP, mapTypes = options.mapTypes || [G_NORMAL_MAP, G_SATELLITE_MAP, G_PHYSICAL_MAP], showMapTypeCtrl = ('showMapTypeCtrl' in options) ? options.showMapTypeCtrl : true, showMapCtrl = ('showMapCtrl' in options) ? options.showMapCtrl : true; // these options need to be saved for later this.opts.syncBands = ('syncBands' in options) ? options.syncBands : true; this.opts.mapFilter = options.mapFilter || TimeMap.filters.hidePastFuture; this.opts.centerOnItems = ('centerMapOnItems' in options) ? options.centerMapOnItems : true; this.opts.theme = TimeMapTheme.create(options.theme, options); // initialize map if (GBrowserIsCompatible()) { /** * The associated map object * @type GMap2 */ this.map = new GMap2(this.mElement); var map = this.map; if (showMapCtrl) { map.addControl(new GLargeMapControl()); } if (showMapTypeCtrl) { map.addControl(new GMapTypeControl()); } // drop all existing types var i; for (i=G_DEFAULT_MAP_TYPES.length-1; i>0; i--) { map.removeMapType(G_DEFAULT_MAP_TYPES[i]); } // you can't remove the last maptype, so add a new one first map.addMapType(mapTypes[0]); map.removeMapType(G_DEFAULT_MAP_TYPES[0]); // add the rest of the new types for (i=1; iThis is an attempt to create a general initialization script that will * work in most cases. If you need a more complex initialization, write your * own script instead of using this one.

* *

The idea here is to throw all of the standard intialization settings into * a large object and then pass it to the TimeMap.init() function. The full * data format is outlined below, but if you leave elements off the script * will use default settings instead.

* *

Call TimeMap.init() inside of an onLoad() function (or a jQuery * $.(document).ready() function, or whatever you prefer). See the examples * for usage.

* * @param {Object} config Full set of configuration options. * See examples/timemapinit_usage.js for format. * @return {TimeMap} The initialized TimeMap object, for future reference */ TimeMap.init = function(config) { // check required elements if (!('mapId' in config) || !config.mapId) { throw "TimeMap.init: No id for map"; } if (!('timelineId' in config) || !config.timelineId) { throw "TimeMap.init: No id for timeline"; } // set defaults config = config || {}; // make sure the config object isn't null config.options = config.options || {}; config.datasets = config.datasets || []; config.bandInfo = config.bandInfo || false; config.scrollTo = config.scrollTo || "earliest"; if (!config.bandInfo && !config.bands) { var intervals = config.bandIntervals || config.options.bandIntervals || [DT.WEEK, DT.MONTH]; // allow intervals to be specified by key if (typeof(intervals) == 'string') { intervals = TimeMap.intervals[intervals]; } // save for later reference config.options.bandIntervals = intervals; // make default band info config.bandInfo = [ { width: "80%", intervalUnit: intervals[0], intervalPixels: 70 }, { width: "20%", intervalUnit: intervals[1], intervalPixels: 100, showEventText: false, overview: true, trackHeight: 0.4, trackGap: 0.2 } ]; } // create the TimeMap object var tm = new TimeMap( document.getElementById(config.timelineId), document.getElementById(config.mapId), config.options); // create the dataset objects var datasets = [], x, ds, dsOptions, dsId; for (x=0; x < config.datasets.length; x++) { ds = config.datasets[x]; dsOptions = ds.options || {}; dsOptions.title = ds.title || ''; dsOptions.theme = ds.theme; dsOptions.dateParser = ds.dateParser; dsId = ds.id || "ds" + x; datasets[x] = tm.createDataset(dsId, dsOptions); if (x > 0) { // set all to the same eventSource datasets[x].eventSource = datasets[0].eventSource; } } // add a pointer to the eventSource in the TimeMap tm.eventSource = datasets[0].eventSource; // set up timeline bands var bands = []; // ensure there's at least an empty eventSource var eventSource = (datasets[0] && datasets[0].eventSource) || new Timeline.DefaultEventSource(); // check for pre-initialized bands (manually created with Timeline.createBandInfo()) if (config.bands) { bands = config.bands; // substitute dataset event source for (x=0; x < bands.length; x++) { // assume that these have been set up like "normal" Timeline bands: // with an empty event source if events are desired, and null otherwise if (bands[x].eventSource !== null) { bands[x].eventSource = eventSource; } } } // otherwise, make bands from band info else { for (x=0; x < config.bandInfo.length; x++) { var bandInfo = config.bandInfo[x]; // if eventSource is explicitly set to null or false, ignore if (!(('eventSource' in bandInfo) && !bandInfo.eventSource)) { bandInfo.eventSource = eventSource; } else { bandInfo.eventSource = null; } bands[x] = Timeline.createBandInfo(bandInfo); if (x > 0 && TimeMap.util.TimelineVersion() == "1.2") { // set all to the same layout bands[x].eventPainter.setLayout(bands[0].eventPainter.getLayout()); } } } // initialize timeline tm.initTimeline(bands); // initialize load manager var loadManager = TimeMap.loadManager; loadManager.init(tm, config.datasets.length, config); // load data! for (x=0; x < config.datasets.length; x++) { (function(x) { // deal with closure issues var data = config.datasets[x], options, type, callback, loaderClass, loader; // support some older syntax options = data.options || data.data || {}; type = data.type || options.type; callback = function() { loadManager.increment() }; // get loader class loaderClass = (typeof(type) == 'string') ? TimeMap.loaders[type] : type; // load with appropriate loader loader = new loaderClass(options); loader.load(datasets[x], callback); })(x); } // return timemap object for later manipulation return tm; }; // for backwards compatibility var timemapInit = TimeMap.init; /** * @class Static singleton for managing multiple asynchronous loads */ TimeMap.loadManager = new function() { /** * Initialize (or reset) the load manager * * @param {TimeMap} tm TimeMap instance * @param {int} target Number of datasets we're loading * @param {Object} options Container for optional settings:
     *   {Function} dataLoadedFunction      Custom function replacing default completion function;
     *                                      should take one parameter, the TimeMap object
     *   {String/Date} scrollTo             Where to scroll the timeline when load is complete
     *                                      Options: "earliest", "latest", "now", date string, Date
     *   {Function} dataDisplayedFunction   Custom function to fire once data is loaded and displayed;
     *                                      should take one parameter, the TimeMap object
     * 
*/ this.init = function(tm, target, config) { this.count = 0; this.tm = tm; this.target = target; this.opts = config || {}; }; /** * Increment the count of loaded datasets */ this.increment = function() { this.count++; if (this.count >= this.target) { this.complete(); } }; /** * Function to fire when all loads are complete. * Default behavior is to scroll to a given date (if provided) and * layout the timeline. */ this.complete = function() { var tm = this.tm; // custom function including timeline scrolling and layout var func = this.opts.dataLoadedFunction; if (func) { func(tm); } else { var d = new Date(); var eventSource = this.tm.eventSource; var scrollTo = this.opts.scrollTo; // make sure there are events to scroll to if (scrollTo && eventSource.getCount() > 0) { switch (scrollTo) { case "now": break; case "earliest": d = eventSource.getEarliestDate(); break; case "latest": d = eventSource.getLatestDate(); break; default: // assume it's a date, try to parse if (typeof(scrollTo) == 'string') { scrollTo = TimeMapDataset.hybridParser(scrollTo); } // either the parse worked, or it was a date to begin with if (scrollTo.constructor == Date) d = scrollTo; } this.tm.timeline.getBand(0).setCenterVisibleDate(d); } this.tm.timeline.layout(); // custom function to be called when data is loaded func = this.opts.dataDisplayedFunction; if (func) { func(tm); } } }; }; /** * @namespace * Namespace for different data loader functions. * New loaders should add their factories or constructors to this object; loader * functions are passed an object with parameters in TimeMap.init(). */ TimeMap.loaders = {}; /** * @class * Basic loader class, for pre-loaded data. * Other types of loaders should take the same parameter. * * @constructor * @param {Object} options All options for the loader:
 *   {Array} data                       Array of items to load
 *   {Function} preloadFunction         Function to call on data before loading
 *   {Function} transformFunction       Function to call on individual items before loading
 * 
*/ TimeMap.loaders.basic = function(options) { // get standard functions TimeMap.loaders.mixin(this, options); // allow "value" for backwards compatibility this.data = options.items || options.value || []; } /** * New loaders should implement a load function with the same parameters. * * @param {TimeMapDataset} dataset Dataset to load data into * @param {Function} callback Function to call once data is loaded */ TimeMap.loaders.basic.prototype.load = function(dataset, callback) { // preload var items = this.preload(this.data); // load dataset.loadItems(items, this.transform); // run callback callback(); } /** * @class * Generic class for loading remote data with a custom parser function * * @constructor * @param {Object} options All options for the loader:
 *   {Array} url                        URL of file to load (NB: must be local address)
 *   {Function} parserFunction          Parser function to turn data into JavaScript array
 *   {Function} preloadFunction         Function to call on data before loading
 *   {Function} transformFunction       Function to call on individual items before loading
 * 
*/ TimeMap.loaders.remote = function(options) { // get standard functions TimeMap.loaders.mixin(this, options); // get URL to load this.url = options.url; } /** * Remote load function. * * @param {TimeMapDataset} dataset Dataset to load data into * @param {Function} callback Function to call once data is loaded */ TimeMap.loaders.remote.prototype.load = function(dataset, callback) { var loader = this; // get items GDownloadUrl(this.url, function(result) { // parse var items = loader.parse(result); // load items = loader.preload(items); dataset.loadItems(items, loader.transform); // callback callback(); }); } /** * Save a few lines of code by adding standard functions * * @param {Function} loader Loader to add functions to * @param {Object} options Options for the loader:
 *   {Function} parserFunction          Parser function to turn data into JavaScript array
 *   {Function} preloadFunction         Function to call on data before loading
 *   {Function} transformFunction       Function to call on individual items before loading
 * 
*/ TimeMap.loaders.mixin = function(loader, options) { // set preload and transform functions var dummy = function(data) { return data; }; loader.parse = options.parserFunction || dummy; loader.preload = options.preloadFunction || dummy; loader.transform = options.transformFunction || dummy; } /** * Map of common timeline intervals. Add custom intervals here if you * want to refer to them by key rather than as literals. * @type Object */ TimeMap.intervals = { 'sec': [DT.SECOND, DT.MINUTE], 'min': [DT.MINUTE, DT.HOUR], 'hr': [DT.HOUR, DT.DAY], 'day': [DT.DAY, DT.WEEK], 'wk': [DT.WEEK, DT.MONTH], 'mon': [DT.MONTH, DT.YEAR], 'yr': [DT.YEAR, DT.DECADE], 'dec': [DT.DECADE, DT.CENTURY] }; /** * Map of Google map types. Using keys rather than literals allows * for serialization of the map type. * @type Object */ TimeMap.mapTypes = { 'normal':G_NORMAL_MAP, 'satellite':G_SATELLITE_MAP, 'hybrid':G_HYBRID_MAP, 'physical':G_PHYSICAL_MAP, 'moon':G_MOON_VISIBLE_MAP, 'sky':G_SKY_VISIBLE_MAP }; /** * Create an empty dataset object and add it to the timemap * * @param {String} id The id of the dataset * @param {Object} options A container for optional arguments for dataset constructor * @return {TimeMapDataset} The new dataset object */ TimeMap.prototype.createDataset = function(id, options) { options = options || {}; // make sure the options object isn't null if (!("title" in options)) { options.title = id; } var dataset = new TimeMapDataset(this, options); this.datasets[id] = dataset; // add event listener if (this.opts.centerOnItems) { var tm = this; GEvent.addListener(dataset, 'itemsloaded', function() { var map = tm.map, bounds = tm.mapBounds; // determine the zoom level from the bounds map.setZoom(map.getBoundsZoomLevel(bounds)); // determine the center from the bounds map.setCenter(bounds.getCenter()); }); } return dataset; }; /** * Run a function on each dataset in the timemap. This is the preferred * iteration method, as it allows for future iterator options. * * @param {Function} f The function to run */ TimeMap.prototype.each = function(f) { for (var id in this.datasets) { if (this.datasets.hasOwnProperty(id)) { f(this.datasets[id]); } } }; /** * Initialize the timeline - this must happen separately to allow full control of * timeline properties. * * @param {BandInfo Array} bands Array of band information objects for timeline */ TimeMap.prototype.initTimeline = function(bands) { // synchronize & highlight timeline bands for (var x=1; x < bands.length; x++) { if (this.opts.syncBands) { bands[x].syncWith = (x-1); } bands[x].highlight = true; } /** * The associated timeline object * @type Timeline */ this.timeline = Timeline.create(this.tElement, bands); // set event listeners var tm = this; // update map on timeline scroll this.timeline.getBand(0).addOnScrollListener(function() { tm.filter("map"); }); // hijack timeline popup window to open info window var painter = this.timeline.getBand(0).getEventPainter().constructor; painter.prototype._showBubble = function(x, y, evt) { evt.item.openInfoWindow(); }; // filter chain for map placemarks this.addFilterChain("map", function(item) { item.showPlacemark(); }, function(item) { item.hidePlacemark(); } ); // filter: hide when item is hidden this.addFilter("map", function(item) { return item.visible; }); // filter: hide when dataset is hidden this.addFilter("map", function(item) { return item.dataset.visible; }); // filter: hide map items depending on timeline state this.addFilter("map", this.opts.mapFilter); // filter chain for timeline events this.addFilterChain("timeline", function(item) { item.showEvent(); }, function(item) { item.hideEvent(); } ); // filter: hide when item is hidden this.addFilter("timeline", function(item) { return item.visible; }); // filter: hide when dataset is hidden this.addFilter("timeline", function(item) { return item.dataset.visible; }); // add callback for window resize var resizeTimerID = null; var oTimeline = this.timeline; window.onresize = function() { if (resizeTimerID === null) { resizeTimerID = window.setTimeout(function() { resizeTimerID = null; oTimeline.layout(); }, 500); } }; }; /** * Update items, hiding or showing according to filters * * @param {String} fid Filter chain to update on */ TimeMap.prototype.filter = function(fid) { var filters = this.filters[fid]; // if no filters exist, forget it if (!filters || !filters.chain || filters.chain.length === 0) { return; } // run items through filter this.each(function(ds) { ds.each(function(item) { var done = false; F_LOOP: while (!done) { for (var i = filters.chain.length - 1; i >= 0; i--) { if (!filters.chain[i](item)) { // false condition filters.off(item); break F_LOOP; } } // true condition filters.on(item); done = true; } }); }); }; /** * Add a new filter chain * * @param {String} fid Id of the filter chain * @param {Function} fon Function to run on an item if filter is true * @param {Function} foff Function to run on an item if filter is false */ TimeMap.prototype.addFilterChain = function(fid, fon, foff) { this.filters[fid] = { chain:[], on: fon, off: foff }; }; /** * Remove a filter chain * * @param {String} fid Id of the filter chain */ TimeMap.prototype.removeFilterChain = function(fid) { this.filters[fid] = null; }; /** * Add a function to a filter chain * * @param {String} fid Id of the filter chain * @param {Function} f Function to add */ TimeMap.prototype.addFilter = function(fid, f) { if (this.filters[fid] && this.filters[fid].chain) { this.filters[fid].chain.push(f); } }; /** * Remove a function from a filter chain * * @param {String} fid Id of the filter chain * XXX: Support index here */ TimeMap.prototype.removeFilter = function(fid) { if (this.filters[fid] && this.filters[fid].chain) { this.filters[fid].chain.pop(); } }; /** * @namespace * Namespace for different filter functions. Adding new filters to this * object allows them to be specified by string name. */ TimeMap.filters = {}; /** * Static filter function: Hide items not in the visible area of the timeline. * * @param {TimeMapItem} item Item to test for filter * @return {Boolean} Whether to show the item */ TimeMap.filters.hidePastFuture = function(item) { var topband = item.dataset.timemap.timeline.getBand(0); var maxVisibleDate = topband.getMaxVisibleDate().getTime(); var minVisibleDate = topband.getMinVisibleDate().getTime(); if (item.event !== null) { var itemStart = item.event.getStart().getTime(); var itemEnd = item.event.getEnd().getTime(); // hide items in the future if (itemStart > maxVisibleDate) { return false; } // hide items in the past else if (itemEnd < minVisibleDate || (item.event.isInstant() && itemStart < minVisibleDate)) { return false; } } return true; }; /** * Static filter function: Hide items not present at the exact * center date of the timeline (will only work for duration events). * * @param {TimeMapItem} item Item to test for filter * @return {Boolean} Whether to show the item */ TimeMap.filters.showMomentOnly = function(item) { var topband = item.dataset.timemap.timeline.getBand(0); var momentDate = topband.getCenterVisibleDate().getTime(); if (item.event !== null) { var itemStart = item.event.getStart().getTime(); var itemEnd = item.event.getEnd().getTime(); // hide items in the future if (itemStart > momentDate) { return false; } // hide items in the past else if (itemEnd < momentDate || (item.event.isInstant() && itemStart < momentDate)) { return false; } } return true; }; /*---------------------------------------------------------------------------- * TimeMapDataset Class *---------------------------------------------------------------------------*/ /** * @class * The TimeMapDataset object holds an array of items and dataset-level * options and settings, including visual themes. * * @constructor * @param {TimeMap} timemap Reference to the timemap object * @param {Object} options Object holding optional arguments:
 *   {String} id                        Key for this dataset in the datasets map
 *   {String} title                     Title of the dataset (for the legend)
 *   {String or theme object} theme     Theme settings.
 *   {String or Function} dateParser    Function to replace default date parser.
 *   {Function} openInfoWindow          Function redefining how info window opens
 *   {Function} closeInfoWindow         Function redefining how info window closes
 * 
*/ function TimeMapDataset(timemap, options) { /** * Reference to parent TimeMap * @type TimeMap */ this.timemap = timemap; /** * EventSource for timeline events * @type Timeline.EventSource */ this.eventSource = new Timeline.DefaultEventSource(); /** * Array of child TimeMapItems * @type Array */ this.items = []; /** * Whether the dataset is visible * @type Boolean */ this.visible = true; // set defaults for options /** * Container for optional settings passed in the "options" parameter * @type Object */ this.opts = options || {}; // make sure the options object isn't null this.opts.title = options.title || ""; // get theme var tmtheme = this.timemap.opts.theme, theme = options.theme || tmtheme; // event icon path overrides custom themes options.eventIconPath = options.eventIconPath || tmtheme.eventIconPath; // configure theme this.opts.theme = TimeMapTheme.create(theme, options); // allow for other data parsers (e.g. Gregorgian) by key or function if (typeof(options.dateParser) == "string") { options.dateParser = TimeMapDataset.dateParsers[options.dateParser]; } this.opts.dateParser = options.dateParser || TimeMapDataset.hybridParser; /** * Return an array of this dataset's items * * @param {int} index Optional index of single item to return * @return {TimeMapItem} Single item, or array of all items if no index was supplied */ this.getItems = function(index) { if (index !== undefined) { if (index < this.items.length) { return this.items[index]; } else { return null; } } return this.items; }; /** * Return the title of the dataset * * @return {String} Dataset title */ this.getTitle = function() { return this.opts.title; }; } /** * Better Timeline Gregorian parser... shouldn't be necessary :(. * Gregorian dates are years with "BC" or "AD" * * @param {String} s String to parse into a Date object * @return {Date} Parsed date or null */ TimeMapDataset.gregorianParser = function(s) { if (!s) { return null; } else if (s instanceof Date) { return s; } // look for BC var bc = Boolean(s.match(/b\.?c\.?/i)); // parse - parseInt will stop at non-number characters var year = parseInt(s); // look for success if (!isNaN(year)) { // deal with BC if (bc) year = 1 - year; // make Date and return var d = new Date(0); d.setUTCFullYear(year); return d; } else { return null; } }; /** * Parse date strings with a series of date parser functions, until one works. * In order: *
    *
  1. Date.parse() (so Date.js should work here, if it works with Timeline...)
  2. *
  3. Gregorian parser
  4. *
  5. The Timeline ISO 8601 parser
  6. *
* * @param {String} s String to parse into a Date object * @return {Date} Parsed date or null */ TimeMapDataset.hybridParser = function(s) { // try native date parse var d = new Date(Date.parse(s)); if (isNaN(d)) { // look for Gregorian dates if (s.match(/^-?\d{1,6} ?(a\.?d\.?|b\.?c\.?e?\.?|c\.?e\.?)?$/i)) { d = TimeMapDataset.gregorianParser(s); } // try ISO 8601 parse else { try { d = DT.parseIso8601DateTime(s); } catch(e) { d = null; } } } // d should be a date or null return d; }; /** * Map of supported date parser functions. Add custom date parsers here if you * want to refer to them by key rather than as a function name. * @type Object */ TimeMapDataset.dateParsers = { 'hybrid': TimeMapDataset.hybridParser, 'iso8601': DT.parseIso8601DateTime, 'gregorian': TimeMapDataset.gregorianParser }; /** * Run a function on each item in the dataset. This is the preferred * iteration method, as it allows for future iterator options. * * @param {Function} f The function to run */ TimeMapDataset.prototype.each = function(f) { for (var x=0; x < this.items.length; x++) { f(this.items[x]); } }; /** * Add an array of items to the map and timeline. * Each item has both a timeline event and a map placemark. * * @param {Object} data Data to be loaded. See loadItem() for the format. * @param {Function} transform If data is not in the above format, transformation function to make it so * @see TimeMapDataset#loadItem */ TimeMapDataset.prototype.loadItems = function(data, transform) { for (var x=0; x < data.length; x++) { this.loadItem(data[x], transform); } GEvent.trigger(this, 'itemsloaded'); }; /** * Add one item to map and timeline. * Each item has both a timeline event and a map placemark. * * @param {Object} data Data to be loaded, in the following format:
 *      {String} title              Title of the item (visible on timeline)
 *      {DateTime} start            Start time of the event on the timeline
 *      {DateTime} end              End time of the event on the timeline (duration events only)
 *      {Object} point              Data for a single-point placemark: 
 *          {Float} lat                 Latitude of map marker
 *          {Float} lon                 Longitude of map marker
 *      {Array of points} polyline  Data for a polyline placemark, in format above
 *      {Array of points} polygon   Data for a polygon placemark, in format above
 *      {Object} overlay            Data for a ground overlay:
 *          {String} image              URL of image to overlay
 *          {Float} north               Northern latitude of the overlay
 *          {Float} south               Southern latitude of the overlay
 *          {Float} east                Eastern longitude of the overlay
 *          {Float} west                Western longitude of the overlay
 *      {Object} options            Optional arguments to be passed to the TimeMapItem (@see TimeMapItem)
 * 
* @param {Function} transform If data is not in the above format, transformation function to make it so * @return {TimeMapItem} The created item (for convenience, as it's already be added) * @see TimeMapItem */ TimeMapDataset.prototype.loadItem = function(data, transform) { // apply transformation, if any if (transform !== undefined) { data = transform(data); } // transform functions can return a null value to skip a datum in the set if (data === null) { return; } var options = data.options || {}, tm = this.timemap, dstheme = this.opts.theme, theme = options.theme || dstheme; // event icon path overrides custom themes options.eventIconPath = options.eventIconPath || dstheme.eventIconPath; // get configured theme theme = TimeMapTheme.create(theme, options); // create timeline event var parser = this.opts.dateParser, start = data.start, end = data.end, instant; start = (start === undefined||start === "") ? null : parser(start); end = (end === undefined||end === "") ? null : parser(end); instant = (end === undefined); var eventIcon = theme.eventIcon, title = data.title, // allow event-less placemarks - these will be always present on map event = null; if (start !== null) { var eventClass = Timeline.DefaultEventSource.Event; if (TimeMap.util.TimelineVersion() == "1.2") { // attributes by parameter event = new eventClass(start, end, null, null, instant, title, null, null, null, eventIcon, theme.eventColor, theme.eventTextColor); } else { var textColor = theme.eventTextColor; if (!textColor) { // tweak to show old-style events textColor = (theme.classicTape && !instant) ? '#FFFFFF' : '#000000'; } // attributes in object event = new eventClass({ "start": start, "end": end, "instant": instant, "text": title, "icon": eventIcon, "color": theme.eventColor, "textColor": textColor }); } } // set the icon, if any, outside the closure var markerIcon = theme.icon, bounds = tm.mapBounds; // save some bytes // internal function: create map placemark // takes a data object (could be full data, could be just placemark) // returns an object with {placemark, type, point} var createPlacemark = function(pdata) { var placemark = null, type = "", point = null; // point placemark if ("point" in pdata) { point = new GLatLng( parseFloat(pdata.point.lat), parseFloat(pdata.point.lon) ); // add point to visible map bounds if (tm.opts.centerOnItems) { bounds.extend(point); } placemark = new GMarker(point, { icon: markerIcon }); type = "marker"; point = placemark.getLatLng(); } // polyline and polygon placemarks else if ("polyline" in pdata || "polygon" in pdata) { var points = [], line; if ("polyline" in pdata) { line = pdata.polyline; } else { line = pdata.polygon; } for (var x=0; x 1) { type = "array"; } options.title = title; options.type = type || "none"; options.theme = theme; // check for custom infoPoint and convert to GLatLng if (options.infoPoint) { options.infoPoint = new GLatLng( parseFloat(options.infoPoint.lat), parseFloat(options.infoPoint.lon) ); } else { options.infoPoint = point; } // create item and cross-references var item = new TimeMapItem(placemark, event, this, options); // add event if it exists if (event !== null) { event.item = item; this.eventSource.add(event); } // add placemark(s) if any exist if (placemark.length > 0) { for (i=0; i * {GIcon} icon Icon for marker placemarks * {String} color Default color in hex for events, polylines, polygons * {String} lineColor Color for polylines, defaults to options.color * {String} polygonLineColor Color for polygon outlines, defaults to lineColor * {Number} lineOpacity Opacity for polylines * {Number} polgonLineOpacity Opacity for polygon outlines, defaults to options.lineOpacity * {Number} lineWeight Line weight in pixels for polylines * {Number} polygonLineWeight Line weight for polygon outlines, defaults to options.lineWeight * {String} fillColor Color for polygon fill, defaults to options.color * {String} fillOpacity Opacity for polygon fill * {String} eventColor Background color for duration events * {URL} eventIcon Icon URL for instant events * {Boolean} classicTape Whether to use the "classic" style timeline event tape * (NB: this needs additional css to work - see examples/artists.html) * */ function TimeMapTheme(options) { // work out various defaults - the default theme is Google's reddish color options = options || {}; if (!options.icon) { // make new red icon var markerIcon = new GIcon(G_DEFAULT_ICON); this.iconImage = options.iconImage || GIP + "red-dot.png"; markerIcon.image = this.iconImage; markerIcon.iconSize = new GSize(32, 32); markerIcon.shadow = GIP + "msmarker.shadow.png"; markerIcon.shadowSize = new GSize(59, 32); markerIcon.iconAnchor = new GPoint(16, 33); markerIcon.infoWindowAnchor = new GPoint(18, 3); } this.icon = options.icon || markerIcon; this.color = options.color || "#FE766A"; this.lineColor = options.lineColor || this.color; this.polygonLineColor = options.polygonLineColor || this.lineColor; this.lineOpacity = options.lineOpacity || 1; this.polgonLineOpacity = options.polgonLineOpacity || this.lineOpacity; this.lineWeight = options.lineWeight || 2; this.polygonLineWeight = options.polygonLineWeight || this.lineWeight; this.fillColor = options.fillColor || this.color; this.fillOpacity = options.fillOpacity || 0.25; this.eventColor = options.eventColor || this.color; this.eventTextColor = options.eventTextColor || null; this.eventIconPath = options.eventIconPath || "timemap/images/"; this.eventIconImage = options.eventIconImage || "red-circle.png"; this.eventIcon = options.eventIcon || this.eventIconPath + this.eventIconImage; // whether to use the older "tape" event style for the newer Timeline versions this.classicTape = ("classicTape" in options) ? options.classicTape : false; } /** * Create a theme, based on an optional new or pre-set theme * * @param {Object} options Container for optional arguments - @see TimeMapTheme() * @return {TimeMapTheme} Configured theme */ TimeMapTheme.create = function(theme, options) { // test for string matches if (typeof(theme) == "string") { if (theme in TimeMap.themes) { // get theme by key return new TimeMap.themes[theme](options) } else { theme = null; } } // no theme supplied, or bad string if (!theme) { return new TimeMapTheme(options); } // theme supplied - clone, overriding with options as necessary var clone = new TimeMapTheme(), prop; for (prop in theme) { if (theme.hasOwnProperty(prop)) { clone[prop] = options[prop] || theme[prop]; // special case for event icon path if (prop == "eventIconPath") { clone.eventIconImage = clone[prop] + theme.eventIconImage; } } } return clone; } /** * @namespace * Pre-set event/placemark themes in a variety of colors. */ TimeMap.themes = { /** * Red theme: #FE766A * This is the default. * * @param {Object} options Container for optional settings * @return {TimeMapTheme} Pre-set theme * @see TimeMapTheme */ red: function(options) { return new TimeMapTheme(options); }, /** * Blue theme: #5A7ACF * * @param {Object} options Container for optional settings * @return {TimeMapTheme} Pre-set theme * @see TimeMapTheme */ blue: function(options) { options = options || {}; options.iconImage = GIP + "blue-dot.png"; options.color = "#5A7ACF"; options.eventIconImage = "blue-circle.png"; return new TimeMapTheme(options); }, /** * Green theme: #19CF54 * * @param {Object} options Container for optional settings * @return {TimeMapTheme} Pre-set theme * @see TimeMapTheme */ green: function(options) { options = options || {}; options.iconImage = GIP + "green-dot.png"; options.color = "#19CF54"; options.eventIconImage = "green-circle.png"; return new TimeMapTheme(options); }, /** * Light blue theme: #5ACFCF * * @param {Object} options Container for optional settings * @return {TimeMapTheme} Pre-set theme * @see TimeMapTheme */ ltblue: function(options) { options = options || {}; options.iconImage = GIP + "ltblue-dot.png"; options.color = "#5ACFCF"; options.eventIconImage = "ltblue-circle.png"; return new TimeMapTheme(options); }, /** * Purple theme: #8E67FD * * @param {Object} options Container for optional settings * @return {TimeMapTheme} Pre-set theme * @see TimeMapTheme */ purple: function(options) { options = options || {}; options.iconImage = GIP + "purple-dot.png"; options.color = "#8E67FD"; options.eventIconImage = "purple-circle.png"; return new TimeMapTheme(options); }, /** * Orange theme: #FF9900 * * @param {Object} options Container for optional settings * @return {TimeMapTheme} Pre-set theme * @see TimeMapTheme */ orange: function(options) { options = options || {}; options.iconImage = GIP + "orange-dot.png"; options.color = "#FF9900"; options.eventIconImage = "orange-circle.png"; return new TimeMapTheme(options); }, /** * Yellow theme: #ECE64A * * @param {Object} options Container for optional settings * @return {TimeMapTheme} Pre-set theme * @see TimeMapTheme */ yellow: function(options) { options = options || {}; options.iconImage = GIP + "yellow-dot.png"; options.color = "#ECE64A"; options.eventIconImage = "yellow-circle.png"; return new TimeMapTheme(options); } }; /*---------------------------------------------------------------------------- * TimeMapItem Class *---------------------------------------------------------------------------*/ /** * @class * The TimeMapItem object holds references to one or more map placemarks and * an associated timeline event. * * @constructor * @param {placemark} placemark Placemark or array of placemarks (GMarker, GPolyline, etc) * @param {Event} event The timeline event * @param {TimeMapDataset} dataset Reference to the parent dataset object * @param {Object} options A container for optional arguments:
 *   {String} title                     Title of the item
 *   {String} description               Plain-text description of the item
 *   {String} type                      Type of map placemark used (marker. polyline, polygon)
 *   {GLatLng} infoPoint                Point indicating the center of this item
 *   {String} infoHtml                  Full HTML for the info window
 *   {String} infoUrl                   URL from which to retrieve full HTML for the info window
 *   {Function} openInfoWindow          Function redefining how info window opens
 *   {Function} closeInfoWindow         Function redefining how info window closes
 *   {String/TimeMapTheme} theme        Theme applying to this item, overriding dataset theme
 * 
*/ function TimeMapItem(placemark, event, dataset, options) { /** * This item's timeline event * @type Timeline.Event */ this.event = event; /** * This item's parent dataset * @type TimeMapDataset */ this.dataset = dataset; /** * The timemap's map object * @type GMap2 */ this.map = dataset.timemap.map; // initialize placemark(s) with some type juggling if (placemark && TimeMap.util.isArray(placemark) && placemark.length === 0) { placemark = null; } if (placemark && placemark.length == 1) { placemark = placemark[0]; } /** * This item's placemark(s) * @type GMarker/GPolyline/GPolygon/GOverlay/Array */ this.placemark = placemark; // set defaults for options this.opts = options || {}; this.opts.type = options.type || ''; this.opts.title = options.title || ''; this.opts.description = options.description || ''; this.opts.infoPoint = options.infoPoint || null; this.opts.infoHtml = options.infoHtml || ''; this.opts.infoUrl = options.infoUrl || ''; // get functions /** * Return the placemark type for this item * * @return {String} Placemark type */ this.getType = function() { return this.opts.type; }; /** * Return the title for this item * * @return {String} Item title */ this.getTitle = function() { return this.opts.title; }; /** * Return the item's "info point" (the anchor for the map info window) * * @return {GLatLng} Info point */ this.getInfoPoint = function() { // default to map center if placemark not set return this.opts.infoPoint || this.map.getCenter(); }; /** * Whether the item is visible * @type Boolean */ this.visible = true; /** * Whether the item's placemark is visible * @type Boolean */ this.placemarkVisible = false; /** * Whether the item's event is visible * @type Boolean */ this.eventVisible = true; // allow for custom open/close functions, set at item, dataset, or timemap level var openFunction, dopts = dataset.opts, tmopts = dataset.timemap.opts; // set open function openFunction = options.openInfoWindow || dopts.openInfoWindow || tmopts.openInfoWindow || false; if (!openFunction) { if (this.opts.infoUrl !== "") { // load via AJAX if URL is provided openFunction = TimeMapItem.openInfoWindowAjax; } else { // otherwise default to basic window openFunction = TimeMapItem.openInfoWindowBasic; } } /** * Open the info window for this item. * By default this is the map infoWindow, but you can set custom functions * for whatever behavior you want when the event or placemark is clicked * @function */ this.openInfoWindow = openFunction; /** * Close the info window for this item. * By default this is the map infoWindow, but you can set custom functions * for whatever behavior you want. * @function */ this.closeInfoWindow = options.closeInfoWindow || dopts.closeInfoWindow || tmopts.closeInfoWindow || TimeMapItem.closeInfoWindowBasic; } /** * Show the map placemark(s) */ TimeMapItem.prototype.showPlacemark = function() { if (this.placemark) { if (this.getType() == "array") { for (var i=0; i'; if (this.opts.description !== "") { html += '
' + this.opts.description + '
'; } } // scroll timeline if necessary if (this.placemark && !this.visible && this.event) { var topband = this.dataset.timemap.timeline.getBand(0); topband.setCenterVisibleDate(this.event.getStart()); } // open window if (this.getType() == "marker") { this.placemark.openInfoWindowHtml(html); } else { this.map.openInfoWindowHtml(this.getInfoPoint(), html); } // custom functions will need to set this as well this.selected = true; }; /** * Open info window function using ajax-loaded text in map window */ TimeMapItem.openInfoWindowAjax = function() { if (this.opts.infoHtml !== "") { // already loaded - change to static this.openInfoWindow = TimeMapItem.openInfoWindowBasic; this.openInfoWindow(); } else { // load content via AJAX if (this.opts.infoUrl !== "") { var item = this; GDownloadUrl(this.opts.infoUrl, function(result) { item.opts.infoHtml = result; item.openInfoWindow(); }); } else { // fall back on basic function this.openInfoWindow = TimeMapItem.openInfoWindowBasic; this.openInfoWindow(); } } }; /** * Standard close window function, using the map window */ TimeMapItem.closeInfoWindowBasic = function() { if (this.getType() == "marker") { this.placemark.closeInfoWindow(); } else { var infoWindow = this.map.getInfoWindow(); // close info window if its point is the same as this item's point if (infoWindow.getPoint() == this.getInfoPoint() && !infoWindow.isHidden()) { this.map.closeInfoWindow(); } } // custom functions will need to set this as well this.selected = false; }; /*---------------------------------------------------------------------------- * Utility functions *---------------------------------------------------------------------------*/ /** * @namespace * Namespace for TimeMap utility functions. */ TimeMap.util = {}; /** * Convenience trim function * * @param {String} str String to trim * @return {String} Trimmed string */ TimeMap.util.trim = function(str) { str = str && String(str) || ''; return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); }; /** * Convenience array tester * * @param {Object} o Object to test * @return {Boolean} Whether the object is an array */ TimeMap.util.isArray = function(o) { return o && !(o.propertyIsEnumerable('length')) && typeof o === 'object' && typeof o.length === 'number'; }; /** * Get XML tag value as a string * * @param {XML Node} n Node in which to look for tag * @param {String} tag Name of tag to look for * @param {String} ns Optional namespace * @return {String} Tag value as string */ TimeMap.util.getTagValue = function(n, tag, ns) { var str = ""; var nList = TimeMap.util.getNodeList(n, tag, ns); if (nList.length > 0) { n = nList[0].firstChild; // fix for extra-long nodes // see http://code.google.com/p/timemap/issues/detail?id=36 while(n !== null) { str += n.nodeValue; n = n.nextSibling; } } return str; }; /** * Empty container for mapping XML namespaces to URLs */ TimeMap.util.nsMap = {}; /** * Cross-browser implementation of getElementsByTagNameNS. * Note: Expects any applicable namespaces to be mapped in * TimeMap.util.nsMap. XXX: There may be better ways to do this. * * @param {XML Node} n Node in which to look for tag * @param {String} tag Name of tag to look for * @param {String} ns Optional namespace * @return {XML Node List} List of nodes with the specified tag name */ TimeMap.util.getNodeList = function(n, tag, ns) { var nsMap = TimeMap.util.nsMap; if (ns === undefined) { // no namespace return n.getElementsByTagName(tag); } if (n.getElementsByTagNameNS && nsMap[ns]) { // function and namespace both exist return n.getElementsByTagNameNS(nsMap[ns], tag); } // no function, try the colon tag name return n.getElementsByTagName(ns + ':' + tag); }; /** * Make TimeMap.init()-style points from a GLatLng, array, or string * * @param {Object} coords GLatLng, array, or string to convert * @param {Boolean} reversed Whether the points are KML-style lon/lat, rather than lat/lon * @return {Object} TimeMap.init()-style point */ TimeMap.util.makePoint = function(coords, reversed) { var latlon = null, trim = TimeMap.util.trim; // GLatLng if (coords.lat && coords.lng) { latlon = [coords.lat(), coords.lng()]; } // array of coordinates if (TimeMap.util.isArray(coords)) { latlon = coords; } // string if (latlon === null) { // trim extra whitespace coords = trim(coords); if (coords.indexOf(',') > -1) { // split on commas latlon = coords.split(","); } else { // split on whitespace latlon = coords.split(/[\r\n\f ]+/); } } // deal with extra coordinates (i.e. KML altitude) if (latlon.length > 2) latlon = latlon.slice(0, 2); // deal with backwards (i.e. KML-style) coordinates if (reversed) latlon.reverse(); return { "lat": trim(latlon[0]), "lon": trim(latlon[1]) }; }; /** * Make TimeMap.init()-style polyline/polygons from a whitespace-delimited * string of coordinates (such as those in GeoRSS and KML). * XXX: Any reason for this to take arrays of GLatLngs as well? * * @param {Object} coords String to convert * @param {Boolean} reversed Whether the points are KML-style lon/lat, rather than lat/lon * @return {Object} Formated coordinate array */ TimeMap.util.makePoly = function(coords, reversed) { var poly = [], latlon; var coordArr = TimeMap.util.trim(coords).split(/[\r\n\f ]+/); if (coordArr.length == 0) return []; // loop through coordinates for (var x=0; x 0) ? // comma-separated coordinates (KML-style lon/lat) coordArr[x].split(",") : // space-separated coordinates - increment to step by 2s [coordArr[x], coordArr[++x]]; // deal with extra coordinates (i.e. KML altitude) if (latlon.length > 2) latlon = latlon.slice(0, 2); // deal with backwards (i.e. KML-style) coordinates if (reversed) latlon.reverse(); poly.push({ "lat": latlon[0], "lon": latlon[1] }); } return poly; } /** * Format a date as an ISO 8601 string * * @param {Date} d Date to format * @param {int} precision Optional precision indicator: * 3 (default): Show full date and time * 2: Show full date and time, omitting seconds * 1: Show date only * @return {String} Formatted string */ TimeMap.util.formatDate = function(d, precision) { // default to high precision precision = precision || 3; var str = ""; if (d) { // check for date.js support if (d.toISOString) { return d.toISOString(); } // otherwise, build ISO 8601 string var pad = function(num) { return ((num < 10) ? "0" : "") + num; }; var yyyy = d.getUTCFullYear(), mo = d.getUTCMonth(), dd = d.getUTCDate(); str += yyyy + '-' + pad(mo + 1 ) + '-' + pad(dd); // show time if top interval less than a week if (precision > 1) { var hh = d.getUTCHours(), mm = d.getUTCMinutes(), ss = d.getUTCSeconds(); str += 'T' + pad(hh) + ':' + pad(mm); // show seconds if the interval is less than a day if (precision > 2) { str += pad(ss); } str += 'Z'; } } return str; }; /** * Determine the SIMILE Timeline version. * * @return {String} At the moment, only "1.2", "2.2.0", or what Timeline provides */ TimeMap.util.TimelineVersion = function() { // check for Timeline.version support - added in 2.3.0 if (Timeline.version) { return Timeline.version; } if (Timeline.DurationEventPainter) { return "1.2"; } else { return "2.2.0"; } }; /** * Identify the placemark type. * XXX: Not 100% happy with this implementation, which relies heavily on duck-typing. * * @param {Object} pm Placemark to identify * @return {String} Type of placemark, or false if none found */ TimeMap.util.getPlacemarkType = function(pm) { if ('getIcon' in pm) { return 'marker'; } if ('getVertex' in pm) { return 'setFillStyle' in pm ? 'polygon' : 'polyline'; } return false; };