westdc-zf1/htdocs/js/timemap/timemap.js

1936 lines
64 KiB
JavaScript
Raw Normal View History

2009-12-20 10:56:42 +00:00
/*!
* 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:<pre>
* {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
* </pre>
*/
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; i<mapTypes.length; i++) {
map.addMapType(mapTypes[i]);
}
map.enableDoubleClickZoom();
map.enableScrollWheelZoom();
map.enableContinuousZoom();
// initialize map center and zoom
map.setCenter(mapCenter, mapZoom);
// must be called after setCenter, for reasons unclear
map.setMapType(mapType);
}
}
/**
* Current library version.
* @type String
*/
TimeMap.version = "1.5";
/**
* Intializes a TimeMap.
*
* <p>This 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.</p>
*
* <p>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.</p>
*
* <p>Call TimeMap.init() inside of an onLoad() function (or a jQuery
* $.(document).ready() function, or whatever you prefer). See the examples
* for usage.</p>
*
* @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:<pre>
* {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
* </pre>
*/
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:<pre>
* {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
* </pre>
*/
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:<pre>
* {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
* </pre>
*/
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:<pre>
* {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
* </pre>
*/
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:<pre>
* {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
* </pre>
*/
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:
* <ol>
* <li>Date.parse() (so Date.js should work here, if it works with Timeline...)</li>
* <li>Gregorian parser</li>
* <li>The Timeline ISO 8601 parser</li>
* </ol>
*
* @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: <pre>
* {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)
* </pre>
* @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<line.length; x++) {
point = new GLatLng(
parseFloat(line[x].lat),
parseFloat(line[x].lon)
);
points.push(point);
// add point to visible map bounds
if (tm.opts.centerOnItems) {
bounds.extend(point);
}
}
if ("polyline" in pdata) {
placemark = new GPolyline(points,
theme.lineColor,
theme.lineWeight,
theme.lineOpacity);
type = "polyline";
point = placemark.getVertex(Math.floor(placemark.getVertexCount()/2));
} else {
placemark = new GPolygon(points,
theme.polygonLineColor,
theme.polygonLineWeight,
theme.polygonLineOpacity,
theme.fillColor,
theme.fillOpacity);
type = "polygon";
point = placemark.getBounds().getCenter();
}
}
// ground overlay placemark
else if ("overlay" in pdata) {
var sw = new GLatLng(
parseFloat(pdata.overlay.south),
parseFloat(pdata.overlay.west)
);
var ne = new GLatLng(
parseFloat(pdata.overlay.north),
parseFloat(pdata.overlay.east)
);
// add to visible bounds
if (tm.opts.centerOnItems) {
bounds.extend(sw);
bounds.extend(ne);
}
// create overlay
var overlayBounds = new GLatLngBounds(sw, ne);
placemark = new GGroundOverlay(pdata.overlay.image, overlayBounds);
type = "overlay";
point = overlayBounds.getCenter();
}
return {
"placemark": placemark,
"type": type,
"point": point
};
};
// create placemark or placemarks
var placemark = [], pdataArr = [], pdata = null, type = "", point = null, i;
// array of placemark objects
if ("placemarks" in data) {
pdataArr = data.placemarks;
} else {
// we have one or more single placemarks
var types = ["point", "polyline", "polygon", "overlay"];
for (i=0; i<types.length; i++) {
if (types[i] in data) {
pdata = {};
pdata[types[i]] = data[types[i]];
pdataArr.push(pdata);
}
}
}
if (pdataArr) {
for (i=0; i<pdataArr.length; i++) {
// create the placemark
var p = createPlacemark(pdataArr[i]);
// take the first point and type as a default
point = point || p.point;
type = type || p.type;
placemark.push(p.placemark);
}
}
// override type for arrays
if (placemark.length > 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<placemark.length; i++) {
placemark[i].item = item;
// add listener to make placemark open when event is clicked
GEvent.addListener(placemark[i], "click", function() {
item.openInfoWindow();
});
// add placemark and event to map and timeline
tm.map.addOverlay(placemark[i]);
// hide placemarks until the next refresh
placemark[i].hide();
}
}
// add the item to the dataset
this.items.push(item);
// return the item object
return item;
};
/*----------------------------------------------------------------------------
* TimeMapTheme Class
*---------------------------------------------------------------------------*/
/**
* @class
* Predefined visual themes for datasets, defining colors and images for
* map markers and timeline events.
*
* @constructor
* @param {Object} options A container for optional arguments:<pre>
* {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)
* </pre>
*/
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:<pre>
* {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
* </pre>
*/
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<this.placemark.length; i++) {
this.placemark[i].show();
}
} else {
this.placemark.show();
}
this.placemarkVisible = true;
}
};
/**
* Hide the map placemark(s)
*/
TimeMapItem.prototype.hidePlacemark = function() {
if (this.placemark) {
if (this.getType() == "array") {
for (var i=0; i<this.placemark.length; i++) {
this.placemark[i].hide();
}
} else {
this.placemark.hide();
}
this.placemarkVisible = false;
}
this.closeInfoWindow();
};
/**
* Show the timeline event.
* NB: Will likely require calling timeline.layout()
*/
TimeMapItem.prototype.showEvent = function() {
if (this.event) {
if (this.eventVisible === false){
this.dataset.timemap.timeline.getBand(0)
.getEventSource()._events._events.add(this.event);
}
this.eventVisible = true;
}
};
/**
* Hide the timeline event.
* NB: Will likely require calling timeline.layout()
*/
TimeMapItem.prototype.hideEvent = function() {
if (this.event) {
if (this.eventVisible == true){
this.dataset.timemap.timeline.getBand(0)
.getEventSource()._events._events.remove(this.event);
}
this.eventVisible = false;
}
};
/**
* Standard open info window function, using static text in map window
*/
TimeMapItem.openInfoWindowBasic = function() {
var html = this.opts.infoHtml;
// create content for info window if none is provided
if (html === "") {
html = '<div class="infotitle">' + this.opts.title + '</div>';
if (this.opts.description !== "") {
html += '<div class="infodescription">' + this.opts.description + '</div>';
}
}
// 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<coordArr.length; x++) {
latlon = (coordArr[x].indexOf(',') > 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;
};