From fc689a66f2347a1c1b20faa66830c4aaf9c6d8e6 Mon Sep 17 00:00:00 2001
From: wlx This is a loader for data from Flickr. You probably want to use it with a
+ * This is a loader for Flickr data. You probably want to use it with a
* URL for the Flickr Geo Feed API: http://www.flickr.com/services/feeds/geo/ The loader takes a full URL, minus the JSONP callback function. The loader takes a full URL, minus the JSONP callback function. Depends on: This is a loader class for GeoRSS feeds. Parsing is complicated by the
+ * Parsing is complicated by the
* diversity of GeoRSS formats; this parser handles: At the moment, this only supports points; polygons, polylines, and boxes
- * will be added at some later point. At the moment, this only supports points, polygons, polylines; boxes
+ * may be added at some later point. This is a loader for data from Google Spreadsheets. The constructor takes an optional map
* to indicate which columns contain which data elements; the default column
* names (case-insensitive) are: title, description, start, end, lat, lon
- *
- *
- * @example Usage in TimeMap.init():
-
+ * @example
+TimeMap.init({
datasets: [
{
title: "Flickr Dataset",
@@ -36,43 +37,56 @@
url: "http://www.flickr.com/services/feeds/geo/?format=json&jsoncallback="
}
}
- ]
+ ],
+ // etc...
+});
+ * @see Flickr Pathlines Example
*
- * @param {Object} options All options for the loader:
- * {String} url Full JSONP url of Flickr feed to load
- * {Function} preloadFunction Function to call on data before loading
- * {Function} transformFunction Function to call on individual items before loading
- *
- * @return {TimeMap.loaders.remote} Remote loader configured for Flickr
+ * @param {Object} options All options for the loader
+ * @param {String} options.url Full JSONP url of Flickr feed to load
+ * @param {mixed} [options[...]] Other options (see {@link TimeMap.loaders.jsonp})
*/
TimeMap.loaders.flickr = function(options) {
var loader = new TimeMap.loaders.jsonp(options);
- // preload function for Flickr feeds
+ /**
+ * Preload function for Flickr feeds
+ * @name TimeMap.loaders.flickr#preload
+ * @function
+ * @parameter {Object} data Data to preload
+ * @return {Array} data Array of item data
+ */
loader.preload = function(data) {
return data["items"];
};
-
- // transform function for Flickr feeds
+
+ /**
+ * Transform function for Flickr feeds
+ * @name TimeMap.loaders.flickr#transform
+ * @function
+ * @parameter {Object} data Data to transform
+ * @return {Object} data Transformed data for one item
+ */
loader.transform = function(data) {
var item = {
- title: data["title"],
- start: data["date_taken"],
+ title: data.title,
+ start: data.date_taken,
point: {
- lat: data["latitude"],
- lon: data["longitude"]
+ lat: data.latitude,
+ lon: data.longitude
},
options: {
- description: data["description"]
+ description: data.description
.replace(/>/g, ">")
.replace(/</g, "<")
.replace(/"/g, '"')
}
};
- if (options.transformFunction)
+ if (options.transformFunction) {
item = options.transformFunction(item);
+ }
return item;
};
return loader;
-}
+};
diff --git a/htdocs/js/timemap/loaders/georss.js b/htdocs/js/timemap/loaders/georss.js
index 30e82de1..490c2737 100644
--- a/htdocs/js/timemap/loaders/georss.js
+++ b/htdocs/js/timemap/loaders/georss.js
@@ -1,5 +1,5 @@
/*
- * Timemap.js Copyright 2008 Nick Rabinowitz.
+ * Timemap.js Copyright 2010 Nick Rabinowitz.
* Licensed under the MIT License (see LICENSE.txt)
*/
@@ -14,9 +14,9 @@
/**
* @class
- * GeoRSS loader factory - inherits from remote loader.
+ * GeoRSS loader: Load GeoRSS feeds.
*
- *
*
- *
- * {Array} url URL of GeoRSS file to load (NB: must be local address)
- * {Function} preloadFunction Function to call on data before loading
- * {Function} transformFunction Function to call on individual items before loading
- *
- * @return {TimeMap.loaders.remote} Remote loader configured for GeoRSS
+ * @param {Object} options All options for the loader:
+ * @param {String} options.url URL of GeoRSS file to load (NB: must be local address)
+ * @param {mixed} [options[...]] Other options (see {@link TimeMap.loaders.xml})
*/
TimeMap.loaders.georss = function(options) {
- var loader = new TimeMap.loaders.remote(options);
+ var loader = new TimeMap.loaders.xml(options);
loader.parse = TimeMap.loaders.georss.parse;
return loader;
-}
+};
/**
* Static function to parse GeoRSS
*
- * @param {XML text} rss GeoRSS to be parsed
- * @return {TimeMapItem Array} Array of TimeMapItems
+ * @param {String} rss GeoRSS string to be parsed
+ * @return {TimeMapItem[]} Array of TimeMapItems
*/
TimeMap.loaders.georss.parse = function(rss) {
- var items = [], data, node, placemarks, pm;
+ var items = [], data, node, placemarks, pm, i;
node = GXml.parse(rss);
// get TimeMap utilty functions
// assigning to variables should compress better
- var util = TimeMap.util;
- var getTagValue = util.getTagValue,
+ var util = TimeMap.util,
+ getTagValue = util.getTagValue,
getNodeList = util.getNodeList,
makePoint = util.makePoint,
makePoly = util.makePoly,
@@ -88,7 +93,7 @@ TimeMap.loaders.georss.parse = function(rss) {
// look for placemarks
var tName = (feedType == 'rss' ? "item" : "entry");
placemarks = getNodeList(node, tName);
- for (var i=0; i
The loader takes either a full URL, minus the JSONP callback function, or - * just the spreadsheet key. Note that the spreadsheet must be published.
+ * just the spreadsheet key. Note that the spreadsheet must be published. + * + * @augments TimeMap.loaders.jsonp + * @requires param.js + * @requires loaders/json.js * - *Depends on:
- *- * {String} key Key of spreadsheet to load, or - * {String} url Full JSONP url of spreadsheet to load - * {Function} preloadFunction Function to call on data before loading - * {Function} transformFunction Function to call on individual items before loading - *- * @return {TimeMap.loaders.remote} Remote loader configured for Google Spreadsheets + * @param {Object} options All options for the loader: + * @param {String} options.key Key of spreadsheet to load, or + * @param {String} options.url Full JSONP url of spreadsheet to load + * @param {Object} [options.paramMap] Map of paramName:columnName pairs for core parameters, + * if using non-standard column names; see keys in +* {@link TimeMap.loaders.base#params} for the standard param names + * @param {String[]} [options.extraColumns] Array of additional columns to load; all named columns will be + * loaded into the item.opts object. + * @param {mixed} [options[...]] Other options (see {@link TimeMap.loaders.jsonp}) */ TimeMap.loaders.gss = function(options) { - var loader = new TimeMap.loaders.jsonp(options); + var loader = new TimeMap.loaders.jsonp(options), + params = loader.params, paramName, x, + setParamField = TimeMap.loaders.gss.setParamField, + paramMap = options.paramMap || {}, + extraColumns = options.extraColumns || []; // use key if no URL was supplied if (!loader.url) { loader.url = "http://spreadsheets.google.com/feeds/list/" + options.key + "/1/public/values?alt=json-in-script&callback="; - } - - // column map - loader.map = options.map; + } - // preload function for spreadsheet data + // Set up additional columns + for (x=0; x < extraColumns.length; x++) { + paramName = extraColumns[x]; + params[paramName] = new TimeMap.params.OptionParam(paramName); + } + + // Set up parameters to work with Google Spreadsheets + for (paramName in params) { + if (params.hasOwnProperty(paramName)) { + fieldName = paramMap[paramName] || paramName; + setParamField(params[paramName], fieldName); + } + } + + /** + * Preload function for spreadsheet data + * @name TimeMap.loaders.gss#preload + * @function + * @parameter {Object} data Data to preload + * @return {Array} data Array of item data + */ loader.preload = function(data) { - return data["feed"]["entry"]; + return data.feed.entry; }; - - // transform function for spreadsheet data + + /** + * Transform function for spreadsheet data + * @name TimeMap.loaders.gss#transform + * @function + * @parameter {Object} data Data to transform + * @return {Object} data Transformed data for one item + */ loader.transform = function(data) { - // map spreadsheet column ids to corresponding TimeMap elements - var fieldMap = loader.map || TimeMap.loaders.gss.map; - var getField = function(f) { - if (f in fieldMap && fieldMap[f]) { - return data['gsx$' + fieldMap[f]]['$t']; - } else return false; - }; - var item = { - title: getField("title"), - start: getField("start"), - point: { - lat: getField("lat"), - lon: getField("lon") - }, - options: { - description: getField("description") + var item = {}, params = loader.params, paramName, + transform = options.transformFunction; + // run through parameters, loading each + for (paramName in params) { + if (params.hasOwnProperty(paramName)) { + params[paramName].setConfigGSS(item, data); } - }; + } // hook for further transformation - if (options.transformFunction) - item = options.transformFunction(item); + if (transform) { + item = transform(item); + } return item; }; return loader; -} - -/** - * 1:1 map of expected spreadsheet column ids. - */ -TimeMap.loaders.gss.map = { - 'title':'title', - 'description':'description', - 'start':'start', - 'end':'end', - 'lat':'lat', - 'lon':'lon' +}; + +/** + * Set a parameter to get its value from a given Google Spreadsheet field. + * + * @param {TimeMap.Param} param Param object + * @param {String} fieldName Name of the field + */ +TimeMap.loaders.gss.setParamField = function(param, fieldName) { + // internal function: Get the value of a Google Spreadsheet field + var getField = function(data, fieldName) { + // get element, converting field name to GSS format + var el = data['gsx$' + fieldName.toLowerCase().replace(" ", "")]; + if (el) { + return el.$t; + } + return null; + }; + // set the method on the parameter + param.setConfigGSS = function(config, data) { + this.setConfig(config, getField(data, fieldName)); + }; }; diff --git a/htdocs/js/timemap/loaders/json.js b/htdocs/js/timemap/loaders/json.js index 558f13dd..d4d5fdb3 100644 --- a/htdocs/js/timemap/loaders/json.js +++ b/htdocs/js/timemap/loaders/json.js @@ -1,5 +1,5 @@ /* - * Timemap.js Copyright 2008 Nick Rabinowitz. + * Timemap.js Copyright 2010 Nick Rabinowitz. * Licensed under the MIT License (see LICENSE.txt) */ @@ -9,107 +9,80 @@ * * @author Nick Rabinowitz (www.nickrabinowitz.com) */ + +// for JSLint +/*global TimeMap */ /** * @class - * JSONP loader class - expects a service that takes a callback function name as + * JSONP loader - expects a service that takes a callback function name as * the last URL parameter. * *
The jsonp loader assumes that the JSON can be loaded from a url to which a * callback function name can be appended, e.g. "http://www.test.com/getsomejson.php?callback=" * The loader then appends a nonce function name which the JSON should include. * This works for services like Google Spreadsheets, etc., and accepts remote URLs.
+ * + * @augments TimeMap.loaders.remote * - * @example Usage in TimeMap.init(): - + * @example +TimeMap.init({ datasets: [ { title: "JSONP Dataset", type: "jsonp", options: { - url: "http://www.test.com/getsomejson.php?callback=" + url: "http://www.example.com/getsomejson.php?callback=" } } - ] + ], + // etc... +}); * * @constructor - * @param {Object} options All options for the loader:- * {Array} url URL of JSON service to load, callback name left off - * {Function} preloadFunction Function to call on data before loading - * {Function} transformFunction Function to call on individual items before loading - *+ * @param {Object} options All options for the loader: + * @param {String} options.url URL of JSON service to load, callback name left off + * @param {mixed} [options[...]] Other options (see {@link TimeMap.loaders.remote}) */ TimeMap.loaders.jsonp = function(options) { - // get standard functions - TimeMap.loaders.mixin(this, options); - // get URL to load - this.url = options.url; -} - -/** - * JSONP load function. - * - * @param {TimeMapDataset} dataset Dataset to load data into - * @param {Function} callback Function to call once data is loaded - */ -TimeMap.loaders.jsonp.prototype.load = function(dataset, callback) { - var loader = this; - // get items - TimeMap.loaders.jsonp.read(this.url, function(result) { - // load - items = loader.preload(result); - dataset.loadItems(items, loader.transform); - // callback - callback(); - }); -} - -/** - * Static - for naming anonymous callback functions - * @type int - */ -TimeMap.loaders.jsonp.counter = 0; - -/** - * Static - reads JSON from a URL, assuming that the service is set up to apply - * a callback function specified in the URL parameters. - * - * @param {String} jsonUrl URL to load, missing the callback function name - * @param {function} f Callback function to apply to returned data - */ -TimeMap.loaders.jsonp.read = function(url, f) { - // Define a unique function name - var callbackName = "_" + TimeMap.loaders.jsonp.counter++; - - TimeMap.loaders.jsonp[callbackName] = function(result) { - // Pass result to user function - f(result); - // Delete the callback function - delete TimeMap.loaders.jsonp[callbackName]; - }; - - // Create a script tag, set its src attribute and add it to the document - // This triggers the HTTP request and submits the query - var script = document.createElement("script"); - script.src = url + "TimeMap.loaders.jsonp." + callbackName; - document.body.appendChild(script); + var loader = new TimeMap.loaders.remote(options); + + /** + * JSONP load function. Creates a callback function and adds a script tag + * with the appropriate URL to the document, triggering the HTTP request. + * @name TimeMap.loaders.jsonp#load + * @function + * + * @param {TimeMapDataset} dataset Dataset to load data into + * @param {Function} callback Function to call once data is loaded + */ + loader.load = function(dataset, callback) { + // get loader callback name + var callbackName = this.getCallbackName(dataset, callback), + // create a script tag + script = document.createElement("script"); + // set the src attribute and add to the document + script.src = this.url + "TimeMap.loaders.cb." + callbackName; + document.body.appendChild(script); + }; + + return loader; }; /** * @class * JSON string loader factory - expects a plain JSON array. - * Inherits from remote loader. * *
The json_string loader assumes an array of items in plain JSON, with no * callback function - this will require a local URL.
+ *Note that this loader requires lib/json2.pack.js.
* - *Depends on:
- *- * {Array} url URL of JSON service to load, callback name left off - * {Function} preloadFunction Function to call on data before loading - * {Function} transformFunction Function to call on individual items before loading - *- * @return {TimeMap.loaders.remote} Remote loader configured for JSON strings + * @param {Object} options All options for the loader + * @param {String} options.url URL of JSON file to load + * @param {mixed} [options[...]] Other options (see {@link TimeMap.loaders.remote}) */ TimeMap.loaders.json_string = function(options) { var loader = new TimeMap.loaders.remote(options); + + /** + * Parse a JSON string into a JavaScript object, using the json2.js library. + * @name TimeMap.loaders.json_string#parse + * @function + * @param {String} json JSON string to parse + * @returns {Object} Parsed JavaScript object + */ loader.parse = JSON.parse; + return loader; -} +}; // Probably the default json loader should be json_string, not // jsonp. I may change this in the future, so I'd encourage you to use diff --git a/htdocs/js/timemap/loaders/kml.js b/htdocs/js/timemap/loaders/kml.js index 55206ec3..74fa7d4a 100644 --- a/htdocs/js/timemap/loaders/kml.js +++ b/htdocs/js/timemap/loaders/kml.js @@ -1,5 +1,5 @@ /* - * Timemap.js Copyright 2008 Nick Rabinowitz. + * Timemap.js Copyright 2010 Nick Rabinowitz. * Licensed under the MIT License (see LICENSE.txt) */ @@ -14,13 +14,21 @@ /** * @class - * KML loader factory - inherits from remote loader + * KML loader: load KML files. * *
This is a loader class for KML files. Currently supports all geometry - * types (point, polyline, polygon, and overlay) and multiple geometries.
+ * types (point, polyline, polygon, and overlay) and multiple geometries. Supports loading + * ExtendedData + * through the extendedData parameter. + * + * + * @augments TimeMap.loaders.xml + * @requires loaders/xml.js + * @requires param.js + * @borrows TimeMap.loaders.kml.parse as #parse * - * @example Usage in TimeMap.init(): - + * @example +TimeMap.init({ datasets: [ { title: "KML Dataset", @@ -29,20 +37,36 @@ url: "mydata.kml" // Must be local } } - ] + ], + // etc... +}); + * @see KML Example + * @see KML ExtendedData Example * - * @param {Object} options All options for the loader:- * {Array} url URL of KML file to load (NB: must be local address) - * {Function} preloadFunction Function to call on data before loading - * {Function} transformFunction Function to call on individual items before loading - *- * @return {TimeMap.loaders.remote} Remote loader configured for KML + * @param {Object} options All options for the loader + * @param {String} options.url URL of KML file to load (NB: must be local address) + * @param {String[]} [options.extendedData] Array of names for ExtendedData data elements + * @param {mixed} [options[...]] Other options (see {@link TimeMap.loaders.xml}) + * @return {TimeMap.loaders.xml} Loader configured for KML */ TimeMap.loaders.kml = function(options) { - var loader = new TimeMap.loaders.remote(options); + var loader = new TimeMap.loaders.xml(options), + tagMap = options.tagMap || {}, + extendedData = options.extendedData || [], + tagName, x; + + // Add ExtendedData parameters to extra params + for (x=0; x < extendedData.length; x++) { + tagName = extendedData[x]; + loader.extraParams.push( + new TimeMap.params.ExtendedDataParam(tagMap[tagName] || tagName, tagName) + ); + } + + // set custom parser loader.parse = TimeMap.loaders.kml.parse; return loader; -} +}; /** * Static function to parse KML with time data. @@ -56,8 +80,8 @@ TimeMap.loaders.kml.parse = function(kml) { // get TimeMap utilty functions // assigning to variables should compress better - var util = TimeMap.util; - var getTagValue = util.getTagValue, + var util = TimeMap.util, + getTagValue = util.getTagValue, getNodeList = util.getNodeList, makePoint = util.makePoint, makePoly = util.makePoly, @@ -134,6 +158,9 @@ TimeMap.loaders.kml.parse = function(kml) { // XXX: worth closing unclosed polygons? data.placemarks.push(pmobj); } + // look for any extra tags and/or ExtendedData specified + this.parseExtra(data, pm); + items.push(data); } @@ -156,13 +183,51 @@ TimeMap.loaders.kml.parse = function(kml) { data.overlay.south = getTagValue(nList[0], "south"); data.overlay.east = getTagValue(nList[0], "east"); data.overlay.west = getTagValue(nList[0], "west"); + // look for any extra tags and/or ExtendedData specified + this.parseExtra(data, pm); items.push(data); } // clean up - kmlnode = null; - placemarks = null; - pm = null; - nList = null; + kmlnode = placemarks = pm = nList = null; + return items; }; + +/** + * @class + * Class for parameters loaded from KML ExtendedData elements + * + * @augments TimeMap.params.OptionParam + * + * @constructor + * @param {String} paramName String name of the parameter + * @param {String} [tagName] Tag name, if different + */ +TimeMap.params.ExtendedDataParam = function(paramName, tagName) { + return new TimeMap.params.OptionParam(paramName, { + + /** + * Set a config object based on an ExtendedData element + * @name TimeMap.params.ExtendedDataParam#setConfigKML + * @function + * + * @param {Object} config Config object to modify + * @param {XML NodeList} node Parent node to look for tags in + */ + setConfigXML: function(config, node) { + var util = TimeMap.util, + nList = util.getNodeList(node, "Data"), + i; + for (i=0; i
Depends on:
- *- * {Object} query MQL query to load - * {Function} preloadFunction Function to call on data before loading - * {Function} transformFunction Function to call on individual items before loading - *- * @return {TimeMap.loaders.remote} Remote loader configured for MetaWeb + * @param {Object} options All options for the loader + * @param {Object} options.query MQL query to load + * @param {Function} options.transformFunction Function to call on individual items before loading + * @param {String} [options.host=http://www.freebase.com] Host url of web service + * @param {String} [options.service=/api/service/mqlread] Path to web service on host + * @param {mixed} [options[...]] Other options (see {@link TimeMap.loaders.jsonp}) */ TimeMap.loaders.metaweb = function(options) { - var loader = new TimeMap.loaders.jsonp(options); + var loader = new TimeMap.loaders.jsonp(options), + q = options.query || {}, + // format the query URL for Metaweb + querytext = encodeURIComponent(JSON.stringify({qname: {query: q}})); - // Host and service - default to freebase.com + /** + * Host url - default to freebase.com + * @name TimeMap.loaders.metaweb#HOST + * @type {String} + */ loader.HOST = options.host || "http://www.freebase.com"; - loader.QUERY_SERVICE = options.service || "/api/service/mqlread"; + /** + * Service path - default to freebase.com default + * @name TimeMap.loaders.metaweb#QUERY_SERVICE + * @type {String} + */ + loader.QUERY_SERVICE = options.service || "/api/service/mqlread"; + + /** + * URL built using encoded query text and the callback name + * @name TimeMap.loaders.metaweb#url + * @type {String} + */ + loader.url = loader.HOST + loader.QUERY_SERVICE + "?queries=" + querytext + "&callback="; - // Metaweb preload functon + /** + * Preload function for Metaweb + * @name TimeMap.loaders.metaweb#preload + * @function + * @parameter {Object} data Data to preload + * @return {Array} data Array of item data + */ loader.preload = function(data) { // Open outer envelope var innerEnvelope = data.qname; // Make sure the query was successful - if (innerEnvelope.code.indexOf("/api/status/ok") != 0) { + if (innerEnvelope.code.indexOf("/api/status/ok") !== 0) { // uncomment for debugging /* // If error, get error message and throw @@ -80,16 +104,8 @@ TimeMap.loaders.metaweb = function(options) { return []; } // Get result from inner envelope - var result = innerEnvelope.result; - return result; + return innerEnvelope.result; }; - - // format the query URL for Metaweb - var q = options.query || {}; - var querytext = encodeURIComponent(JSON.stringify({qname: {query: q}})); - - // Build the URL using encoded query text and the callback name - loader.url = loader.HOST + loader.QUERY_SERVICE + "?queries=" + querytext + "&callback="; return loader; -} +}; diff --git a/htdocs/js/timemap/loaders/progressive.js b/htdocs/js/timemap/loaders/progressive.js new file mode 100644 index 00000000..de295895 --- /dev/null +++ b/htdocs/js/timemap/loaders/progressive.js @@ -0,0 +1,197 @@ + /* + * Timemap.js Copyright 2010 Nick Rabinowitz. + * Licensed under the MIT License (see LICENSE.txt) + */ + +/** + * @fileOverview + * Progressive loader + * + * @author Nick Rabinowitz (www.nickrabinowitz.com) + */ + +// for JSLint +/*global TimeMap */ + +/** + * @class + * Progressive loader class - basically a wrapper for another remote loader that can + * load data progressively by date range, depending on timeline position. + * + *
The progressive loader can take either another loader or parameters for + * another loader. It expects a loader with a "url" attribute including placeholder + * strings [start] and [end] for the start and end dates to retrieve. The assumption + * is that the data service can take start and end parameters and return the data for + * that date range.
+ * + * @example +TimeMap.init({ + datasets: [ + { + title: "Progressive JSONP Dataset", + type: "progressive", + options: { + type: "jsonp", + url: "http://www.test.com/getsomejson.php?start=[start]&end=[end]callback=" + } + } + ], + // etc... +}); + * + * @example +TimeMap.init({ + datasets: [ + { + title: "Progressive KML Dataset", + type: "progressive", + options: { + loader: new TimeMap.loaders.kml({ + url: "/mydata.kml?start=[start]&end=[end]" + }) + } + } + ], + // etc... +}); + * @see Progressive Loader Example + * + * @constructor + * @param {Object} options All options for the loader + * @param {TimeMap.loaders.remote} [options.loader] Instantiated loader class (overrides "type") + * @param {String} [options.type] Name of loader class to use + * @param {String|Date} options.start Start of initial date range, as date or string + * @param {Number} options.interval Size in milliseconds of date ranges to load at a time + * @param {String|Date} [options.dataMinDate] Minimum date available in data (optional, will avoid + * unnecessary service requests if supplied) + * @param {String|Date} [options.dataMaxDate] Maximum date available in data (optional, will avoid + * unnecessary service requests if supplied) + * @param {Function} [options.formatUrl] Function taking (urlTemplate, start, end) and returning + * a URL formatted as needed by the service + * @param {Function} [options.formatDate={@link TimeMap.util.formatDate}] + * Function to turn a date into a string formatted + * as needed by the service + * @param {mixed} [options[...]] Other options needed by the "type" loader + */ +TimeMap.loaders.progressive = function(options) { + // get loader + var loader = options.loader, + type = options.type; + if (!loader) { + // get loader class + var loaderClass = (typeof(type) == 'string') ? TimeMap.loaders[type] : type; + loader = new loaderClass(options); + } + + // quick string/date check + function cleanDate(d) { + if (typeof(d) == "string") { + d = TimeMapDataset.hybridParser(d); + } + return d; + } + + // save loader attributes + var baseUrl = loader.url, + baseLoadFunction = loader.load, + interval = options.interval, + formatDate = options.formatDate || TimeMap.util.formatDate, + formatUrl = options.formatUrl, + zeroDate = cleanDate(options.start), + dataMinDate = cleanDate(options.dataMinDate), + dataMaxDate = cleanDate(options.dataMaxDate), + loaded = {}; + + if (!formatUrl) { + formatUrl = function(url, start, end) { + return url + .replace('[start]', formatDate(start)) + .replace('[end]', formatDate(end)); + } + } + + // We don't start with a TimeMap reference, so we need + // to stick the listener in on the first load() call + var addListener = function(dataset) { + var band = dataset.timemap.timeline.getBand(0); + // add listener + band.addOnScrollListener(function() { + // determine relevant blocks + var now = band.getCenterVisibleDate(), + currBlock = Math.floor((now.getTime() - zeroDate.getTime()) / interval), + currBlockTime = zeroDate.getTime() + (interval * currBlock) + nextBlockTime = currBlockTime + interval, + prevBlockTime = currBlockTime - interval, + // no callback necessary? + callback = function() { + dataset.timemap.timeline.layout(); + }; + + // is the current block loaded? + if ((!dataMaxDate || currBlockTime < dataMaxDate.getTime()) && + (!dataMinDate || currBlockTime > dataMinDate.getTime()) && + !loaded[currBlock]) { + // load it + // console.log("loading current block (" + currBlock + ")"); + loader.load(dataset, callback, new Date(currBlockTime), currBlock); + } + // are we close enough to load the next block, and is it loaded? + if (nextBlockTime < band.getMaxDate().getTime() && + (!dataMaxDate || nextBlockTime < dataMaxDate.getTime()) && + !loaded[currBlock + 1]) { + // load next block + // console.log("loading next block (" + (currBlock + 1) + ")"); + loader.load(dataset, callback, new Date(nextBlockTime), currBlock + 1); + } + // are we close enough to load the previous block, and is it loaded? + if (prevBlockTime > band.getMinDate().getTime() && + (!dataMinDate || prevBlockTime > dataMinDate.getTime()) && + !loaded[currBlock - 1]) { + // load previous block + // console.log("loading prev block (" + (currBlock - 1) + ")"); + loader.load(dataset, callback, new Date(prevBlockTime), currBlock - 1); + } + }); + // kill this function so that listener is only added once + addListener = false; + }; + + /** + * Load data based on current time + * @name TimeMap.loaders.progressive#load + * @function + * @param {TimeMapDataset} dataset Dataset to load data into + * @param {Function} callback Callback to execute when data is loaded + * @param {Date} start Start date to load data from + * @param {Number} currBlock Index of the current time block + */ + loader.load = function(dataset, callback, start, currBlock) { + // set start date, defaulting to zero date + start = cleanDate(start) || zeroDate; + // set current block, defaulting to 0 + currBlock = currBlock || 0; + // set end by interval + var end = new Date(start.getTime() + interval); + + // set current block as loaded + // XXX: Failed loads will give a false positive here... + // but I'm not sure how else to avoid multiple loads :( + loaded[currBlock] = true; + + // put dates into URL + loader.url = formatUrl(baseUrl, start, end); + // console.log(loader.url); + + // load data + baseLoadFunction.call(loader, dataset, function() { + // add onscroll listener if not yet done + if (addListener) { + addListener(dataset); + } + // run callback + callback(); + }); + }; + + return loader; +}; diff --git a/htdocs/js/timemap/loaders/xml.js b/htdocs/js/timemap/loaders/xml.js new file mode 100644 index 00000000..9d3f7c31 --- /dev/null +++ b/htdocs/js/timemap/loaders/xml.js @@ -0,0 +1,71 @@ +/* + * Timemap.js Copyright 2010 Nick Rabinowitz. + * Licensed under the MIT License (see LICENSE.txt) + */ + +/** + * @fileOverview + * XML Loader + * + * @author Nick Rabinowitz (www.nickrabinowitz.com) + */ + +/*globals TimeMap */ + + /** + * @class + * This is a base loader class for XML files. + * + * @augments TimeMap.loaders.remote + * @requires param.js + * + * @param {Object} options All options for the loader + * @param {String} options.url URL of XML file to load (NB: must be local address) + * @parem {String[]} [options.extraTags] Array of names for extra tag elements to load + * @param {Object} [options.tagMap] Map of tagName:paramName pairs, if you want to load + * data into a differently-named elements + * @param {mixed} [options[...]] Other options (see {@link TimeMap.loaders.remote}) + * @return {TimeMap.loaders.remote} Remote loader configured for XML + */ +TimeMap.loaders.xml = function(options) { + var loader = new TimeMap.loaders.remote(options), + tagMap = options.tagMap || {}, + extraTags = options.extraTags || [], + params = loader.params, + paramName, tagName, x; + + /** + * Additional parameters to load + * @name TimeMap.loaders.xml#extraParams + * @type TimeMap.params.OptionParam[] + */ + loader.extraParams = []; + + // set up extra params + for (x=0; x < extraTags.length; x++) { + tagName = extraTags[x]; + loader.extraParams.push( + new TimeMap.params.OptionParam(tagMap[tagName] || tagName, { + sourceName: tagName + }) + ); + } + + /** + * Parse any extra tags that have been specified into the config object + * @name TimeMap.loaders.xml#parseExtra + * @function + * + * @param {Object} config Config object to modify + * @param {XML NodeList} node Parent node to look for tags in + */ + loader.parseExtra = function(config, node) { + var extraParams = loader.extraParams, x; + for (x=0; xTimemap.js is intended to sync a SIMILE Timeline with a Google Map. + * Depends on: 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. + * Timemap.js is licensed under the MIT License (see LICENSE.txt).
+ *- * {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 + * This will create the visible map, but not the timeline, which must be initialized separately. + * + * @param {DOM Element} tElement The timeline element. + * @param {DOM Element} mElement The map element. + * @param {Object} [options] A container for optional arguments + * @param {TimeMapTheme|String} [options.theme=red] Color theme for the timemap + * @param {Boolean} [options.syncBands=true] Whether to synchronize all bands in timeline + * @param {GLatLng} [options.mapCenter=0,0] Point for map center + * @param {Number} [options.mapZoom=0] Initial map zoom level + * @param {GMapType|String} [options.mapType=physical] The maptype for the map + * @param {Array} [options.mapTypes=normal,satellite,physical] The set of maptypes available for the map + * @param {Function|String} [options.mapFilter={@link TimeMap.filters.hidePastFuture}] + * How to hide/show map items depending on timeline state; + * options: keys in {@link TimeMap.filters} or function + * @param {Boolean} [options.showMapTypeCtrl=true] Whether to display the map type control + * @param {Boolean} [options.showMapCtrl=true] Whether to show map navigation control + * @param {Boolean} [options.centerMapOnItems=true] Whether to center and zoom the map based on loaded item + * @param {Boolean} [options.noEventLoad=false] Whether to skip loading events on the timeline + * @param {Boolean} [options.noPlacemarkLoad=false] Whether to skip loading placemarks on the map + * @param {String} [options.eventIconPath] Path for directory holding event icons; if set at the TimeMap + * level, will override dataset and item defaults + * @param {String} [options.infoTemplate] HTML for the info window content, with variable expressions + * (as "{{varname}}" by default) to be replaced by option data + * @param {String} [options.templatePattern] Regex pattern defining variable syntax in the infoTemplate + * @param {Function} [options.openInfoWindow={@link TimeMapItem.openInfoWindowBasic}] + * Function redefining how info window opens + * @param {Function} [options.closeInfoWindow={@link TimeMapItem.closeInfoWindowBasic}] + * Function redefining how info window closes + * @param {mixed} [options[...]] Any of the options for {@link TimeMapTheme} may be set here, + * to cascade to the entire TimeMap, though they can be overridden + * at lower levels **/ -function TimeMap(tElement, mElement, options) { - // save elements +TimeMap = function(tElement, mElement, options) { + var tm = this, + // set defaults for options + defaults = { + mapCenter: new GLatLng(0,0), + mapZoom: 0, + mapType: G_PHYSICAL_MAP, + mapTypes: [G_NORMAL_MAP, G_SATELLITE_MAP, G_PHYSICAL_MAP], + showMapTypeCtrl: true, + showMapCtrl: true, + syncBands: true, + mapFilter: 'hidePastFuture', + centerOnItems: true, + theme: 'red' + }; + // save DOM elements /** * Map element + * @name TimeMap#mElement * @type DOM Element */ - this.mElement = mElement; + tm.mElement = mElement; /** * Timeline element + * @name TimeMap#tElement * @type DOM Element */ - this.tElement = tElement; + tm.tElement = tElement; /** * Map of datasets + * @name TimeMap#datasets * @type Object */ - this.datasets = {}; + tm.datasets = {}; /** * Filter chains for this timemap + * @name TimeMap#chains * @type Object */ - this.filters = {}; - /** - * Bounds of the map - * @type GLatLngBounds - */ - this.mapBounds = new GLatLngBounds(); - - // set defaults for options + tm.chains = {}; /** * Container for optional settings passed in the "options" parameter + * @name TimeMap#opts * @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; + tm.opts = options = util.merge(options, defaults); - // 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); + // only these options will cascade to datasets and items + options.mergeOnly = ['mergeOnly', 'theme', 'eventIconPath', 'openInfoWindow', + 'closeInfoWindow', 'noPlacemarkLoad', 'noEventLoad', + 'infoTemplate', 'templatePattern'] + + // allow map types to be specified by key + options.mapType = util.lookup(options.mapType, TimeMap.mapTypes); + // allow map filters to be specified by key + options.mapFilter = util.lookup(options.mapFilter, TimeMap.filters); + // allow theme options to be specified in options + options.theme = TimeMapTheme.create(options.theme, options); // initialize map + tm.initMap(); +}; + +/** + * Initialize the map. + */ +TimeMap.prototype.initMap = function() { + var options = this.opts, map, i; if (GBrowserIsCompatible()) { + /** - * The associated map object + * The associated GMap 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()); - } + this.map = map = new GMap2(this.mElement); + // 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.addMapType(options.mapTypes[0]); map.removeMapType(G_DEFAULT_MAP_TYPES[0]); // add the rest of the new types - for (i=1; i
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.
- * *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 + * data format is outlined below, but if you leave elements out 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 + *
See the examples and the + * UsingTimeMapInit wiki page * 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 + * @param {Object} config Full set of configuration options. + * @param {String} config.mapId DOM id of the element to contain the map + * @param {String} config.timelineId DOM id of the element to contain the timeline + * @param {Object} [config.options] Options for the TimeMap object (see the {@link TimeMap} constructor) + * @param {Object[]} config.datasets Array of datasets to load + * @param {Object} config.datasets[x] Configuration options for a particular dataset + * @param {String|Class} config.datasets[x].type Loader type for this dataset (generally a sub-class + * of {@link TimeMap.loaders.base}) + * @param {Object} config.datasets[x].options Options for the loader. See the {@link TimeMap.loaders.base} + * constructor and the constructors for the various loaders for + * more details. + * @param {String} [config.datasets[x].id] Optional id for the dataset in the {@link TimeMap#datasets} + * object, for future reference; otherwise "ds"+x is used + * @param {String} [config.datasets[x][...]] Other options for the {@link TimeMapDataset} object + * @param {String|Array} [config.bandIntervals] Intervals for the two default timeline bands. Can either be an + * array of interval constants or a key in {@link TimeMap.intervals} + * @param {Object[]} [config.bandInfo] Array of configuration objects for Timeline bands, to be passed to + * Timeline.createBandInfo (see the Timeline Getting Started tutorial). + * This will override config.bandIntervals, if provided. + * @param {Timeline.Band[]} [config.bands] Array of instantiated Timeline Band objects. This will override + * config.bandIntervals and config.bandInfo, if provided. + * @param {Function} [config.dataLoadedFunction] Function to be run as soon as all datasets are loaded, but + * before they've been displayed on the map and timeline + * (this will override dataDisplayedFunction and scrollTo) + * @param {Function} [config.dataDisplayedFunction] Function to be run as soon as all datasets are loaded and + * displayed on the map and timeline + * @param {String|Date} [config.scrollTo] Date to scroll to once data is loaded - see + * {@link TimeMap.parseDate} for options; default is "earliest" + * @return {TimeMap} The initialized TimeMap object */ TimeMap.init = function(config) { + var err = "TimeMap.init: No id for ", + // set defaults + defaults = { + options: {}, + datasets: [], + bands: false, + bandInfo: false, + bandIntervals: "wk", + scrollTo: "earliest" + }, + state = TimeMap.state, + intervals, tm, + datasets = [], x, ds, dsOptions, topOptions, dsId, + bands = [], eventSource, bandInfo; // check required elements if (!('mapId' in config) || !config.mapId) { - throw "TimeMap.init: No id for map"; + throw err + "map"; } if (!('timelineId' in config) || !config.timelineId) { - throw "TimeMap.init: No id for timeline"; + throw err + "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"; + // get state from url hash if state functions are available + if (state) { + state.setConfigFromUrl(config); + } + // merge options and defaults + config = util.merge(config, defaults); + 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; + intervals = util.lookup(config.bandIntervals, TimeMap.intervals); // make default band info - config.bandInfo = [ - { + config.bandInfo = [ + { width: "80%", intervalUnit: intervals[0], intervalPixels: 70 @@ -211,7 +313,7 @@ TimeMap.init = function(config) { intervalUnit: intervals[1], intervalPixels: 100, showEventText: false, - overview: true, + overview: true, trackHeight: 0.4, trackGap: 0.2 } @@ -219,19 +321,21 @@ TimeMap.init = function(config) { } // create the TimeMap object - var tm = new TimeMap( + 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; + // put top-level data into options + topOptions = { + title: ds.title, + theme: ds.theme, + dateParser: ds.dateParser + }; + dsOptions = util.merge(ds.options, topOptions); dsId = ds.id || "ds" + x; datasets[x] = tm.createDataset(dsId, dsOptions); if (x > 0) { @@ -243,9 +347,8 @@ TimeMap.init = function(config) { 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(); + 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; @@ -261,7 +364,7 @@ TimeMap.init = function(config) { // otherwise, make bands from band info else { for (x=0; x < config.bandInfo.length; x++) { - var bandInfo = config.bandInfo[x]; + bandInfo = config.bandInfo[x]; // if eventSource is explicitly set to null or false, ignore if (!(('eventSource' in bandInfo) && !bandInfo.eventSource)) { bandInfo.eventSource = eventSource; @@ -270,7 +373,7 @@ TimeMap.init = function(config) { bandInfo.eventSource = null; } bands[x] = Timeline.createBandInfo(bandInfo); - if (x > 0 && TimeMap.util.TimelineVersion() == "1.2") { + if (x > 0 && util.TimelineVersion() == "1.2") { // set all to the same layout bands[x].eventPainter.setLayout(bands[0].eventPainter.getLayout()); } @@ -288,9 +391,9 @@ TimeMap.init = function(config) { (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 || {}; + options = data.data || data.options || {}; type = data.type || options.type; - callback = function() { loadManager.increment() }; + callback = function() { loadManager.increment(); }; // get loader class loaderClass = (typeof(type) == 'string') ? TimeMap.loaders[type] : type; // load with appropriate loader @@ -302,42 +405,46 @@ TimeMap.init = function(config) { return tm; }; -// for backwards compatibility -var timemapInit = TimeMap.init; - /** * @class Static singleton for managing multiple asynchronous loads */ TimeMap.loadManager = new function() { + var mgr = this; /** * Initialize (or reset) the load manager + * @name TimeMap.loadManager#init + * @function * * @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; + * @param {Number} target Number of datasets we're loading + * @param {Object} [options] Container for optional settings + * @param {Function} [options.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 + * @param {String|Date} [options.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; + * @param {Function} [options.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 || {}; + mgr.init = function(tm, target, config) { + mgr.count = 0; + mgr.tm = tm; + mgr.target = target; + mgr.opts = config || {}; }; /** * Increment the count of loaded datasets + * @name TimeMap.loadManager#increment + * @function */ - this.increment = function() { - this.count++; - if (this.count >= this.target) { - this.complete(); + mgr.increment = function() { + mgr.count++; + if (mgr.count >= mgr.target) { + mgr.complete(); } }; @@ -345,219 +452,131 @@ TimeMap.loadManager = new function() { * Function to fire when all loads are complete. * Default behavior is to scroll to a given date (if provided) and * layout the timeline. + * @name TimeMap.loadManager#complete + * @function */ - this.complete = function() { - var tm = this.tm; - // custom function including timeline scrolling and layout - var func = this.opts.dataLoadedFunction; + mgr.complete = function() { + var tm = mgr.tm, + opts = mgr.opts, + // custom function including timeline scrolling and layout + func = 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(); + } + else { + tm.scrollToDate(opts.scrollTo, true); + // check for state support + if (tm.initState) tm.initState(); // custom function to be called when data is loaded - func = this.opts.dataDisplayedFunction; - if (func) { - func(tm); - } + func = 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. + * Parse a date in the context of the timeline. Uses the standard parser + * ({@link TimeMapDataset.hybridParser}) but accepts "now", "earliest", + * "latest", "first", and "last" (referring to loaded events) * - * @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 - *+ * @param {String|Date} s String (or date) to parse + * @return {Date} Parsed date */ -TimeMap.loaders.basic = function(options) { - // get standard functions - TimeMap.loaders.mixin(this, options); - // allow "value" for backwards compatibility - this.data = options.items || options.value || []; +TimeMap.prototype.parseDate = function(s) { + var d = new Date(), + eventSource = this.eventSource, + parser = TimeMapDataset.hybridParser, + // make sure there are events to scroll to + hasEvents = eventSource.getCount() > 0 ? true : false; + switch (s) { + case "now": + break; + case "earliest": + case "first": + if (hasEvents) { + d = eventSource.getEarliestDate(); + } + break; + case "latest": + case "last": + if (hasEvents) { + d = eventSource.getLatestDate(); + } + break; + default: + // assume it's a date, try to parse + d = parser(s); + } + return d; } /** - * New loaders should implement a load function with the same parameters. + * Scroll the timeline to a given date. If lazyLayout is specified, this function + * will also call timeline.layout(), but only if it won't be called by the + * onScroll listener. This involves a certain amount of reverse engineering, + * and may not be future-proof. * - * @param {TimeMapDataset} dataset Dataset to load data into - * @param {Function} callback Function to call once data is loaded + * @param {String|Date} d Date to scroll to (either a date object, a + * date string, or one of the strings accepted + * by TimeMap#parseDate) + * @param {Boolean} [lazyLayout] Whether to call timeline.layout() if not + * required by the scroll. */ -TimeMap.loaders.basic.prototype.load = function(dataset, callback) { - // preload - var items = this.preload(this.data); - // load - dataset.loadItems(items, this.transform); - // run callback - callback(); +TimeMap.prototype.scrollToDate = function(d, lazyLayout) { + var d = this.parseDate(d), + timeline = this.timeline, x, + layouts = [], + band, minTime, maxTime; + if (d) { + // check which bands will need layout after scroll + for (x=0; x < timeline.getBandCount(); x++) { + band = timeline.getBand(x); + minTime = band.getMinDate().getTime(); + maxTime = band.getMaxDate().getTime(); + layouts[x] = (lazyLayout && d.getTime() > minTime && d.getTime() < maxTime); + } + // do scroll + timeline.getBand(0).setCenterVisibleDate(d); + // layout as necessary + for (x=0; x < layouts.length; x++) { + if (layouts[x]) { + timeline.getBand(x).layout(); + } + } + } + // layout if requested even if no date is found + else if (lazyLayout) { + timeline.layout(); + } } -/** - * @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 + * @param {Object} options A container for optional arguments for dataset constructor - + * see the options passed to {@link TimeMapDataset} * @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; + var tm = this, + dataset = new TimeMapDataset(tm, options); + tm.datasets[id] = dataset; // add event listener - if (this.opts.centerOnItems) { - var tm = this; + if (tm.opts.centerOnItems) { + var map = tm.map, + bounds = tm.mapBounds; 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()); + // determine the center and zoom level from the bounds + map.setCenter( + bounds.getCenter(), + map.getBoundsZoomLevel(bounds) + ); }); } 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. @@ -565,10 +584,12 @@ TimeMap.prototype.each = function(f) { * @param {BandInfo Array} bands Array of band information objects for timeline */ TimeMap.prototype.initTimeline = function(bands) { + var tm = this, + x, painter; // synchronize & highlight timeline bands - for (var x=1; x < bands.length; x++) { - if (this.opts.syncBands) { + for (x=1; x < bands.length; x++) { + if (tm.opts.syncBands) { bands[x].syncWith = (x-1); } bands[x].highlight = true; @@ -576,25 +597,28 @@ TimeMap.prototype.initTimeline = function(bands) { /** * The associated timeline object + * @name TimeMap#timeline * @type Timeline */ - this.timeline = Timeline.create(this.tElement, bands); + tm.timeline = Timeline.create(tm.tElement, bands); // set event listeners - var tm = this; + // update map on timeline scroll - this.timeline.getBand(0).addOnScrollListener(function() { + tm.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(); - }; + for (x=0; x < tm.timeline.getBandCount(); x++) { + painter = tm.timeline.getBand(x).getEventPainter().constructor; + painter.prototype._showBubble = function(xx, yy, evt) { + evt.item.openInfoWindow(); + }; + } // filter chain for map placemarks - this.addFilterChain("map", + tm.addFilterChain("map", function(item) { item.showPlacemark(); }, @@ -604,78 +628,514 @@ TimeMap.prototype.initTimeline = function(bands) { ); // filter: hide when item is hidden - this.addFilter("map", function(item) { + tm.addFilter("map", function(item) { return item.visible; }); // filter: hide when dataset is hidden - this.addFilter("map", function(item) { + tm.addFilter("map", function(item) { return item.dataset.visible; }); // filter: hide map items depending on timeline state - this.addFilter("map", this.opts.mapFilter); + tm.addFilter("map", tm.opts.mapFilter); // filter chain for timeline events - this.addFilterChain("timeline", + tm.addFilterChain("timeline", + // on function(item) { item.showEvent(); }, + // off function(item) { item.hideEvent(); + }, + // pre + null, + // post + function() { + var tm = this.timemap; + tm.eventSource._events._index(); + tm.timeline.layout(); } ); // filter: hide when item is hidden - this.addFilter("timeline", function(item) { + tm.addFilter("timeline", function(item) { return item.visible; }); // filter: hide when dataset is hidden - this.addFilter("timeline", function(item) { + tm.addFilter("timeline", function(item) { return item.dataset.visible; }); // add callback for window resize - var resizeTimerID = null; - var oTimeline = this.timeline; + var resizeTimerID = null, + timeline = tm.timeline; window.onresize = function() { if (resizeTimerID === null) { resizeTimerID = window.setTimeout(function() { resizeTimerID = null; - oTimeline.layout(); + timeline.layout(); }, 500); } }; }; +/** + * 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, taking one dataset as an argument + */ +TimeMap.prototype.each = function(f) { + var tm = this, + id; + for (id in tm.datasets) { + if (tm.datasets.hasOwnProperty(id)) { + f(tm.datasets[id]); + } + } +}; + +/** + * Run a function on each item in each dataset in the timemap. + * + * @param {Function} f The function to run, taking one item as an argument + */ +TimeMap.prototype.eachItem = function(f) { + this.each(function(ds) { + ds.each(function(item) { + f(item); + }); + }); +}; + +/** + * Get all items from all datasets. + * + * @return {TimeMapItem[]} Array of all items + */ +TimeMap.prototype.getItems = function() { + var items = []; + this.eachItem(function(item) { + items.push(item); + }); + return items; +}; + + +/*---------------------------------------------------------------------------- + * Loader namespace and base classes + *---------------------------------------------------------------------------*/ + +/** + * @namespace + * Namespace for different data loader functions. + * New loaders can add their factories or constructors to this object; loader + * functions are passed an object with parameters in TimeMap.init(). + * + * @example + TimeMap.init({ + datasets: [ + { + // name of class in TimeMap.loaders + type: "json_string", + options: { + url: "mydata.json" + }, + // etc... + } + ], + // etc... + }); + */ +TimeMap.loaders = { + + /** + * @namespace + * Namespace for storing callback functions + * @private + */ + cb: {}, + + /** + * Cancel all current load requests. In practice, this is really only + * applicable to remote asynchronous loads. Note that this doesn't cancel + * the download of data, just the callback that loads it. + */ + cancelAll: function() { + var namespace = TimeMap.loaders.cb, + callbackName; + for (callbackName in namespace) { + if (namespace.hasOwnProperty(callbackName)) { + // replace with self-cancellation function + namespace[callbackName] = function() { + delete namespace[callbackName]; + }; + } + } + }, + + /** + * Static counter for naming callback functions + * @private + * @type int + */ + counter: 0, + + /** + * @class + * Abstract loader class. All loaders should inherit from this class. + * + * @constructor + * @param {Object} options All options for the loader + * @param {Function} [options.parserFunction=Do nothing] + * Parser function to turn a string into a JavaScript array + * @param {Function} [options.preloadFunction=Do nothing] + * Function to call on data before loading + * @param {Function} [options.transformFunction=Do nothing] + * Function to call on individual items before loading + * @param {String|Date} [options.scrollTo=earliest] Date to scroll the timeline to in the default callback + * (see {@link TimeMap#parseDate} for accepted syntax) + */ + base: function(options) { + var dummy = function(data) { return data; }, + loader = this; + + /** + * Parser function to turn a string into a JavaScript array + * @name TimeMap.loaders.base#parse + * @function + * @parameter {String} s String to parse + * @return {Object[]} Array of item data + */ + loader.parse = options.parserFunction || dummy; + + /** + * Function to call on data object before loading + * @name TimeMap.loaders.base#preload + * @function + * @parameter {Object} data Data to preload + * @return {Object[]} Array of item data + */ + loader.preload = options.preloadFunction || dummy; + + /** + * Function to call on a single item data object before loading + * @name TimeMap.loaders.base#transform + * @function + * @parameter {Object} data Data to transform + * @return {Object} Transformed data for one item + */ + loader.transform = options.transformFunction || dummy; + + /** + * Date to scroll the timeline to on load + * @name TimeMap.loaders.base#scrollTo + * @default "earliest" + * @type String|Date + */ + loader.scrollTo = options.scrollTo || "earliest"; + + /** + * Get the name of a callback function that can be cancelled. This callback applies the parser, + * preload, and transform functions, loads the data, then calls the user callback + * @name TimeMap.loaders.base#getCallbackName + * @function + * + * @param {TimeMapDataset} dataset Dataset to load data into + * @param {Function} callback User-supplied callback function. If no function + * is supplied, the default callback will be used + * @return {String} The name of the callback function in TimeMap.loaders.cb + */ + loader.getCallbackName = function(dataset, callback) { + var callbacks = TimeMap.loaders.cb, + // Define a unique function name + callbackName = "_" + TimeMap.loaders.counter++, + // Define default callback + callback = callback || function() { + dataset.timemap.scrollToDate(loader.scrollTo, true); + }; + + // create callback + callbacks[callbackName] = function(result) { + // parse + var items = loader.parse(result); + // preload + items = loader.preload(items); + // load + dataset.loadItems(items, loader.transform); + // callback + callback(); + // delete the callback function + delete callbacks[callbackName]; + }; + + return callbackName; + }; + + /** + * Get a callback function that can be cancelled. This is a convenience function + * to be used if the callback name itself is not needed. + * @name TimeMap.loaders.base#getCallback + * @function + * @see TimeMap.loaders.base#getCallbackName + * + * @param {TimeMapDataset} dataset Dataset to load data into + * @param {Function} callback User-supplied callback function + * @return {Function} The configured callback function + */ + loader.getCallback = function(dataset, callback) { + // get loader callback name + var callbackName = loader.getCallbackName(dataset, callback); + // return the function + return TimeMap.loaders.cb[callbackName]; + }; + }, + + /** + * @class + * Basic loader class, for pre-loaded data. + * Other types of loaders should take the same parameter. + * + * @augments TimeMap.loaders.base + * @example +TimeMap.init({ + datasets: [ + { + type: "basic", + options: { + data: [ + // object literals for each item + { + title: "My Item", + start: "2009-10-06", + point: { + lat: 37.824, + lon: -122.256 + } + }, + // etc... + ] + } + } + ], + // etc... +}); + * @see Basic Example + * + * @constructor + * @param {Object} options All options for the loader + * @param {Array} options.data Array of items to load + * @param {mixed} [options[...]] Other options (see {@link TimeMap.loaders.base}) + */ + basic: function(options) { + var loader = new TimeMap.loaders.base(options); + + /** + * Array of item data to load. + * @name TimeMap.loaders.basic#data + * @default [] + * @type Object[] + */ + loader.data = options.items || + // allow "value" for backwards compatibility + options.value || []; + + /** + * Load javascript literal data. + * New loaders should implement a load function with the same signature. + * @name TimeMap.loaders.basic#load + * @function + * + * @param {TimeMapDataset} dataset Dataset to load data into + * @param {Function} callback Function to call once data is loaded + */ + loader.load = function(dataset, callback) { + // get callback function and call immediately on data + (this.getCallback(dataset, callback))(this.data); + }; + + return loader; + }, + + /** + * @class + * Generic class for loading remote data with a custom parser function + * + * @augments TimeMap.loaders.base + * + * @constructor + * @param {Object} options All options for the loader + * @param {String} options.url URL of file to load (NB: must be local address) + * @param {mixed} [options[...]] Other options (see {@link TimeMap.loaders.base}) + */ + remote: function(options) { + var loader = new TimeMap.loaders.base(options); + + /** + * URL to load + * @name TimeMap.loaders.remote#url + * @type String + */ + loader.url = options.url; + + /** + * Load function for remote files. + * @name TimeMap.loaders.remote#load + * @function + * + * @param {TimeMapDataset} dataset Dataset to load data into + * @param {Function} callback Function to call once data is loaded + */ + loader.load = function(dataset, callback) { + // download remote data and pass to callback + GDownloadUrl(this.url, this.getCallback(dataset, callback)); + }; + + return loader; + } + +}; + +/*---------------------------------------------------------------------------- + * TimeMapFilterChain Class + *---------------------------------------------------------------------------*/ + +/** + * @class + * TimeMapFilterChains hold a set of filters to apply to the map or timeline. + * + * @constructor + * @param {TimeMap} timemap Reference to the timemap object + * @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 + * @param {Function} [pre] Function to run before the filter runs + * @param {Function} [post] Function to run after the filter runs + */ +TimeMapFilterChain = function(timemap, fon, foff, pre, post) { + var fc = this, + dummy = function(item) {}; + /** + * Reference to parent TimeMap + * @name TimeMapFilterChain#timemap + * @type TimeMap + */ + fc.timemap = timemap; + + /** + * Chain of filter functions, each taking an item and returning a boolean + * @name TimeMapFilterChain#chain + * @type Function[] + */ + fc.chain = []; + + /** + * Function to run on an item if filter is true + * @name TimeMapFilterChain#on + * @function + */ + fc.on = fon || dummy; + + /** + * Function to run on an item if filter is false + * @name TimeMapFilterChain#off + * @function + */ + fc.off = foff || dummy; + + /** + * Function to run before the filter runs + * @name TimeMapFilterChain#pre + * @function + */ + fc.pre = pre || dummy; + + /** + * Function to run after the filter runs + * @name TimeMapFilterChain#post + * @function + */ + fc.post = post || dummy; +} + +/** + * Add a filter to the filter chain. + * + * @param {Function} f Function to add + */ +TimeMapFilterChain.prototype.add = function(f) { + this.chain.push(f); +} + +/** + * Remove a filter from the filter chain + * + * @param {Function} [f] Function to remove; if not supplied, the last filter + * added is removed + */ +TimeMapFilterChain.prototype.remove = function(f) { + var chain = this.chain, + i; + if (!f) { + // just remove the last filter added + chain.pop(); + } + else { + // look for the specific filter to remove + for(i=0; i < chain.length; i++){ + if(chain[i] == f){ + chain.splice(i, 1); + } + } + } +} + +/** + * Run filters on all items + */ +TimeMapFilterChain.prototype.run = function() { + var fc = this, + chain = fc.chain; + // early exit + if (!chain.length) { + return; + } + // pre-filter function + fc.pre(); + // run items through filter + fc.timemap.eachItem(function(item) { + var done = false; + F_LOOP: while (!done) { + for (var i = chain.length - 1; i >= 0; i--) { + if (!chain[i](item)) { + // false condition + fc.off(item); + break F_LOOP; + } + } + // true condition + fc.on(item); + done = true; + } + }); + // post-filter function + fc.post(); +} + +// TimeMap helper functions for dealing with filters + /** * 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; + var fc = this.chains[fid]; + if (fc) { + fc.run(); } - // 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; - } - }); - }); + }; /** @@ -684,13 +1144,11 @@ TimeMap.prototype.filter = function(fid) { * @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 + * @param {Function} [pre] Function to run before the filter runs + * @param {Function} [post] Function to run after the filter runs */ -TimeMap.prototype.addFilterChain = function(fid, fon, foff) { - this.filters[fid] = { - chain:[], - on: fon, - off: foff - }; +TimeMap.prototype.addFilterChain = function(fid, fon, foff, pre, post) { + this.chains[fid] = new TimeMapFilterChain(this, fon, foff, pre, post); }; /** @@ -699,7 +1157,7 @@ TimeMap.prototype.addFilterChain = function(fid, fon, foff) { * @param {String} fid Id of the filter chain */ TimeMap.prototype.removeFilterChain = function(fid) { - this.filters[fid] = null; + this.chains[fid] = null; }; /** @@ -709,8 +1167,9 @@ TimeMap.prototype.removeFilterChain = function(fid) { * @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); + var filterChain = this.chains[fid]; + if (filterChain) { + filterChain.add(f); } }; @@ -718,11 +1177,12 @@ TimeMap.prototype.addFilter = function(fid, f) { * Remove a function from a filter chain * * @param {String} fid Id of the filter chain - * XXX: Support index here + * @param {Function} [f] The function to remove */ -TimeMap.prototype.removeFilter = function(fid) { - if (this.filters[fid] && this.filters[fid].chain) { - this.filters[fid].chain.pop(); +TimeMap.prototype.removeFilter = function(fid, f) { + var filterChain = this.chains[fid]; + if (filterChain) { + filterChain.remove(f); } }; @@ -730,60 +1190,88 @@ TimeMap.prototype.removeFilter = function(fid) { * @namespace * Namespace for different filter functions. Adding new filters to this * object allows them to be specified by string name. + * @example + TimeMap.init({ + options: { + mapFilter: "hideFuture" + }, + // etc... + }); */ -TimeMap.filters = {}; +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; + /** + * 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 + */ + hidePastFuture: function(item) { + var topband = item.timeline.getBand(0), + maxVisibleDate = topband.getMaxVisibleDate().getTime(), + minVisibleDate = topband.getMinVisibleDate().getTime(), + itemStart = item.getStartTime(), + itemEnd = item.getEndTime(); + if (itemStart !== undefined) { + // hide items in the future + return itemStart < maxVisibleDate && + // hide items in the past + (itemEnd > minVisibleDate || itemStart > minVisibleDate); } - } - return true; -}; + 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; + /** + * Static filter function: Hide items later than the visible area of the timeline. + * + * @param {TimeMapItem} item Item to test for filter + * @return {Boolean} Whether to show the item + */ + hideFuture: function(item) { + var maxVisibleDate = item.timeline.getBand(0).getMaxVisibleDate().getTime(), + itemStart = item.getStartTime(); + if (itemStart !== undefined) { + // hide items in the future + return itemStart < maxVisibleDate; } + 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 + */ + showMomentOnly: function(item) { + var topband = item.timeline.getBand(0), + momentDate = topband.getCenterVisibleDate().getTime(), + itemStart = item.getStartTime(), + itemEnd = item.getEndTime(); + if (itemStart !== undefined) { + // hide items in the future + return itemStart < momentDate && + // hide items in the past + (itemEnd > momentDate || itemStart > momentDate); + } + return true; + }, + + /** + * Convenience function: Do nothing. Can be used as a setting for mapFilter + * in TimeMap.init() options, if you don't want map items to be hidden or + * shown based on the timeline position. + * + * @param {TimeMapItem} item Item to test for filter + * @return {Boolean} Whether to show the item + */ + none: function(item) { + return true; } - return true; -}; + +} + /*---------------------------------------------------------------------------- * TimeMapDataset Class @@ -796,85 +1284,95 @@ TimeMap.filters.showMomentOnly = function(item) { * * @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 - *+ * @param {Object} [options] Object holding optional arguments + * @param {String} [options.id] Key for this dataset in the datasets map + * @param {String} [options.title] Title of the dataset (for the legend) + * @param {String|TimeMapTheme} [options.theme] Theme settings. + * @param {String|Function} [options.dateParser] Function to replace default date parser. + * @param {String} [options.infoTemplate] HTML template for info window content + * @param {String} [options.templatePattern] Regex pattern defining variable syntax in the infoTemplate + * @param {Function} [options.openInfoWindow] Function redefining how info window opens + * @param {Function} [options.closeInfoWindow] Function redefining how info window closes + * @param {mixed} [options[...]] Any of the options for {@link TimeMapTheme} may be set here, + * to cascade to the dataset's objects, though they can be + * overridden at the TimeMapItem level */ -function TimeMapDataset(timemap, options) { +TimeMapDataset = function(timemap, options) { + var ds = this, + defaults = { + title: 'Untitled', + dateParser: TimeMapDataset.hybridParser + }; + /** * Reference to parent TimeMap + * @name TimeMapDataset#timemap * @type TimeMap */ - this.timemap = timemap; + ds.timemap = timemap; + /** * EventSource for timeline events + * @name TimeMapDataset#eventSource * @type Timeline.EventSource */ - this.eventSource = new Timeline.DefaultEventSource(); + ds.eventSource = new Timeline.DefaultEventSource(); + /** * Array of child TimeMapItems + * @name TimeMapDataset#items * @type Array */ - this.items = []; + ds.items = []; + /** * Whether the dataset is visible + * @name TimeMapDataset#visible * @type Boolean */ - this.visible = true; - - // set defaults for options - + ds.visible = true; + /** * Container for optional settings passed in the "options" parameter + * @name TimeMapDataset#opts * @type Object */ - this.opts = options || {}; // make sure the options object isn't null - this.opts.title = options.title || ""; + ds.opts = options = util.merge(options, defaults, timemap.opts); - // 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; + // allow date parser to be specified by key + options.dateParser = util.lookup(options.dateParser, TimeMap.dateParsers); + // allow theme options to be specified in options + options.theme = TimeMapTheme.create(options.theme, options); /** * Return an array of this dataset's items + * @name TimeMapDataset#getItems + * @function * - * @param {int} index Optional index of single item to return - * @return {TimeMapItem} Single item, or array of all items if no index was supplied + * @param {Number} [index] Index of single item to return + * @return {TimeMapItem[]} Single item, or array of all items if no index was supplied */ - this.getItems = function(index) { + ds.getItems = function(index) { if (index !== undefined) { - if (index < this.items.length) { - return this.items[index]; + if (index < ds.items.length) { + return ds.items[index]; } else { return null; } } - return this.items; + return ds.items; }; /** * Return the title of the dataset + * @name TimeMapDataset#getTitle + * @function * * @return {String} Dataset title */ - this.getTitle = function() { return this.opts.title; }; -} + ds.getTitle = function() { return ds.opts.title; }; +}; /** * Better Timeline Gregorian parser... shouldn't be necessary :(. @@ -884,21 +1382,22 @@ function TimeMapDataset(timemap, options) { * @return {Date} Parsed date or null */ TimeMapDataset.gregorianParser = function(s) { - if (!s) { + if (!s || typeof(s) != "string") { 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); + var bc = Boolean(s.match(/b\.?c\.?/i)), + // parse - parseInt will stop at non-number characters + year = parseInt(s, 10), + d; // look for success if (!isNaN(year)) { // deal with BC - if (bc) year = 1 - year; + if (bc) { + year = 1 - year; + } // make Date and return - var d = new Date(0); + d = new Date(0); d.setUTCFullYear(year); return d; } @@ -920,37 +1419,38 @@ TimeMapDataset.gregorianParser = function(s) { * @return {Date} Parsed date or null */ TimeMapDataset.hybridParser = function(s) { - // try native date parse - var d = new Date(Date.parse(s)); + // in case we don't know if this is a string or a date + if (s instanceof Date) { + return s; + } + // try native date parse and timestamp + var d = new Date(typeof(s) == "number" ? s : 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; + if (typeof(s) == "string") { + // 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 = DateTime.parseIso8601DateTime(s); + } catch(e) { + d = null; + } } + // look for timestamps + if (!d && s.match(/^\d{7,}$/)) { + d = new Date(parseInt(s)); + } + } else { + return 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. @@ -968,7 +1468,7 @@ TimeMapDataset.prototype.each = function(f) { * 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 + * @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) { @@ -982,25 +1482,25 @@ TimeMapDataset.prototype.loadItems = function(data, transform) { * 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) + * @param {Object} data Data to be loaded + * @param {String} [data.title] Title of the item (visible on timeline) + * @param {String|Date} [data.start] Start time of the event on the timeline + * @param {String|Date} [data.end] End time of the event on the timeline (duration events only) + * @param {Object} [data.point] Data for a single-point placemark: + * @param {Float} [data.point.lat] Latitude of map marker + * @param {Float} [data.point.lon] Longitude of map marker + * @param {Object[]} [data.polyline] Data for a polyline placemark, as an array in "point" format + * @param {Object[]} [data.polygon] Data for a polygon placemark, as an array "point" format + * @param {Object} [data.overlay] Data for a ground overlay: + * @param {String} [data.overlay.image] URL of image to overlay + * @param {Float} [data.overlay.north] Northern latitude of the overlay + * @param {Float} [data.overlay.south] Southern latitude of the overlay + * @param {Float} [data.overlay.east] Eastern longitude of the overlay + * @param {Float} [data.overlay.west] Western longitude of the overlay + * @param {Object[]} [data.placemarks] Array of placemarks, e.g. [{point:{...}}, {polyline:[...]}] + * @param {Object} [options] Optional arguments - see the {@link TimeMapItem} constructor for details + * @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 been added) * @see TimeMapItem */ TimeMapDataset.prototype.loadItem = function(data, transform) { @@ -1009,67 +1509,81 @@ TimeMapDataset.prototype.loadItem = function(data, transform) { data = transform(data); } // transform functions can return a null value to skip a datum in the set - if (data === null) { + if (!data) { 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, + var ds = this, + tm = ds.timemap, + // set defaults for options + options = util.merge(data.options, ds.opts), + // allow theme options to be specified in options + theme = options.theme = TimeMapTheme.create(options.theme, options), + parser = ds.opts.dateParser, + eventClass = Timeline.DefaultEventSource.Event, + // settings for timeline event + start = data.start, + end = data.end, + eventIcon = theme.eventIcon, + textColor = theme.eventTextColor, 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") { + event = null, + instant, + // settings for the placemark + markerIcon = theme.icon, + bounds = tm.mapBounds, + // empty containers + placemark = [], + pdataArr = [], + pdata = null, + type = "", + point = null, + i; + + // create timeline event + start = start ? parser(start) : null; + end = end ? parser(end) : null; + instant = !end; + if (start !== null) { + if (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 + 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; + var placemark = null, + type = "", + point = null; // point placemark - if ("point" in pdata) { + if (pdata.point) { + var lat = pdata.point.lat, + lon = pdata.point.lon; + if (lat === undefined || lon === undefined) { + // give up + return null; + } point = new GLatLng( parseFloat(pdata.point.lat), parseFloat(pdata.point.lon) @@ -1078,64 +1592,70 @@ TimeMapDataset.prototype.loadItem = function(data, transform) { if (tm.opts.centerOnItems) { bounds.extend(point); } - placemark = new GMarker(point, { icon: markerIcon }); + // create marker + placemark = new GMarker(point, { + icon: markerIcon, + title: pdata.title + }); type = "marker"; point = placemark.getLatLng(); } // polyline and polygon placemarks - else if ("polyline" in pdata || "polygon" in pdata) { + else if (pdata.polyline || pdata.polygon) { var points = [], line; - if ("polyline" in pdata) { + if (pdata.polyline) { line = pdata.polyline; } else { line = pdata.polygon; } - for (var x=0; x
- * {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) - *+ * @param {Object} [options] A container for optional arguments + * @param {GIcon} [options.icon=G_DEFAULT_ICON] Icon for marker placemarks. + * @param {String} [options.iconImage=red-dot.png] Icon image for marker placemarks + * (assumes G_MARKER_ICON for the rest of the icon settings) + * @param {String} [options.color=#FE766A] Default color in hex for events, polylines, polygons. + * @param {String} [options.lineColor=color] Color for polylines. + * @param {String} [options.polygonLineColor=lineColor] Color for polygon outlines. + * @param {Number} [options.lineOpacity=1] Opacity for polylines. + * @param {Number} [options.polgonLineOpacity=lineOpacity] Opacity for polygon outlines. + * @param {Number} [options.lineWeight=2] Line weight in pixels for polylines. + * @param {Number} [options.polygonLineWeight=lineWeight] Line weight for polygon outlines. + * @param {String} [options.fillColor=color] Color for polygon fill. + * @param {String} [options.fillOpacity=0.25] Opacity for polygon fill. + * @param {String} [options.eventColor=color] Background color for duration events. + * @param {String} [options.eventTextColor=null] Text color for events (null=Timeline default). + * @param {String} [options.eventIconPath=timemap/images/] Path to instant event icon directory. + * @param {String} [options.eventIconImage=red-circle.gif] Filename of instant event icon image. + * @param {URL} [options.eventIcon=eventIconPath+eventIconImage] URL for instant event icons. + * @param {Boolean} [options.classicTape=false] Whether to use the "classic" style timeline event tape + * (needs additional css to work - see examples/artists.html). */ -function TimeMapTheme(options) { +TimeMapTheme = function(options) { + // work out various defaults - the default theme is Google's reddish color - options = options || {}; + var defaults = { + /** Default color in hex + * @name TimeMapTheme#color + * @type String */ + color: "#FE766A", + /** Opacity for polylines + * @name TimeMapTheme#lineOpacity + * @type Number */ + lineOpacity: 1, + /** Line weight in pixels for polylines + * @name TimeMapTheme#lineWeight + * @type Number */ + lineWeight: 2, + /** Opacity for polygon fill + * @name TimeMapTheme#fillOpacity + * @type Number */ + fillOpacity: 0.25, + /** Text color for duration events + * @name TimeMapTheme#eventTextColor + * @type String */ + eventTextColor: null, + /** Path to instant event icon directory + * @name TimeMapTheme#eventIconPath + * @type String */ + eventIconPath: "timemap/images/", + /** Filename of instant event icon image + * @name TimeMapTheme#eventIconImage + * @type String */ + eventIconImage: "red-circle.png", + /** Whether to use the "classic" style timeline event tape + * @name TimeMapTheme#classicTape + * @type Boolean */ + classicTape: false, + /** Icon image for marker placemarks + * @name TimeMapTheme#iconImage + * @type String */ + iconImage: GIP + "red-dot.png" + }; - if (!options.icon) { + // merge defaults with options + var settings = util.merge(options, defaults); + + // kill mergeOnly if necessary + delete settings.mergeOnly; + + // make default map icon if not supplied + if (!settings.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.image = settings.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); - } + /** Marker icon for placemarks + * @name TimeMapTheme#icon + * @type GIcon */ + settings.icon = markerIcon; + } - 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; + // cascade some settings as defaults + defaults = { + /** Line color for polylines + * @name TimeMapTheme#lineColor + * @type String */ + lineColor: settings.color, + /** Line color for polygons + * @name TimeMapTheme#polygonLineColor + * @type String */ + polygonLineColor: settings.color, + /** Opacity for polygon outlines + * @name TimeMapTheme#polgonLineOpacity + * @type Number */ + polgonLineOpacity: settings.lineOpacity, + /** Line weight for polygon outlines + * @name TimeMapTheme#polygonLineWeight + * @type Number */ + polygonLineWeight: settings.lineWeight, + /** Fill color for polygons + * @name TimeMapTheme#fillColor + * @type String */ + fillColor: settings.color, + /** Background color for duration events + * @name TimeMapTheme#eventColor + * @type String */ + eventColor: settings.color, + /** Full URL for instant event icons + * @name TimeMapTheme#eventIcon + * @type String */ + eventIcon: settings.eventIconPath + settings.eventIconImage + }; + settings = util.merge(settings, defaults); - // whether to use the older "tape" event style for the newer Timeline versions - this.classicTape = ("classicTape" in options) ? options.classicTape : false; -} + // return configured options as theme + return settings; +}; /** * Create a theme, based on an optional new or pre-set theme * - * @param {Object} options Container for optional arguments - @see TimeMapTheme() + * @param {TimeMapTheme} [theme] Existing theme to clone + * @param {Object} [options] Optional settings to overwrite - see {@link 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) { + // test for string matches and missing themes + if (theme) { + theme = TimeMap.util.lookup(theme, TimeMap.themes); + } else { 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; + + // see if we need to clone - guessing fewer keys in options + var clone = false, key; + for (key in options) { + if (theme.hasOwnProperty(key)) { + clone = {}; + break; + } + } + // clone if necessary + if (clone) { + for (key in theme) { + if (theme.hasOwnProperty(key)) { + clone[key] = options[key] || theme[key]; } } + // fix event icon path, allowing full image path in options + clone.eventIcon = options.eventIcon || + clone.eventIconPath + clone.eventIconImage; + return clone; } - 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); + else { + return theme; } }; @@ -1439,39 +1935,71 @@ TimeMap.themes = { * @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 - *+ * @param {Object} [options] A container for optional arguments + * @param {String} [options.title=Untitled] Title of the item + * @param {String} [options.description] Plain-text description of the item + * @param {String} [options.type=none] Type of map placemark used (marker. polyline, polygon) + * @param {GLatLng} [options.infoPoint] Point indicating the center of this item + * @param {String} [options.infoHtml] Full HTML for the info window + * @param {String} [options.infoUrl] URL from which to retrieve full HTML for the info window + * @param {String} [options.infoTemplate] HTML for the info window content, with variable expressions + * (as "{{varname}}" by default) to be replaced by option data + * @param {String} [options.templatePattern=/{{([^}]+)}}/g] + * Regex pattern defining variable syntax in the infoTemplate + * @param {Function} [options.openInfoWindow={@link TimeMapItem.openInfoWindowBasic}] + * Function redefining how info window opens + * @param {Function} [options.closeInfoWindow={@link TimeMapItem.closeInfoWindowBasic}] + * Function redefining how info window closes + * @param {String|TimeMapTheme} [options.theme] Theme applying to this item, overriding dataset theme + * @param {mixed} [options[...]] Any of the options for {@link TimeMapTheme} may be set here */ -function TimeMapItem(placemark, event, dataset, options) { +TimeMapItem = function(placemark, event, dataset, options) { + // improve compression + var item = this, + // set defaults for options + defaults = { + type: 'none', + title: 'Untitled', + description: '', + infoPoint: null, + infoHtml: '', + infoUrl: '', + infoTemplate: '
+ * 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) { @@ -1871,17 +2485,21 @@ TimeMap.util.formatDate = function(d, precision) { precision = precision || 3; var str = ""; if (d) { + var yyyy = d.getUTCFullYear(), + mo = d.getUTCMonth(), + dd = d.getUTCDate(); + // deal with early dates + if (yyyy < 1000) { + return (yyyy < 1 ? (yyyy * -1 + "BC") : yyyy + ""); + } // check for date.js support - if (d.toISOString) { + if (d.toISOString && precision == 3) { 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) { @@ -1924,12 +2542,271 @@ TimeMap.util.TimelineVersion = function() { * @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; +TimeMap.util.getPlacemarkType = function(pm) { + return 'getIcon' in pm ? 'marker' : + 'getVertex' in pm ? + ('setFillStyle' in pm ? 'polygon' : 'polyline') : + false; }; + +/** + * Merge two or more objects, giving precendence to those + * first in the list (i.e. don't overwrite existing keys). + * Original objects will not be modified. + * + * @param {Object} obj1 Base object + * @param {Object} [objN] Objects to merge into base + * @return {Object} Merged object + */ +TimeMap.util.merge = function() { + var opts = {}, args = arguments, obj, key, x, y; + // must... make... subroutine... + var mergeKey = function(o1, o2, key) { + // note: existing keys w/undefined values will be overwritten + if (o1.hasOwnProperty(key) && o2[key] === undefined) { + o2[key] = o1[key]; + } + }; + for (x=0; x