From fc689a66f2347a1c1b20faa66830c4aaf9c6d8e6 Mon Sep 17 00:00:00 2001 From: wlx Date: Tue, 29 Jun 2010 08:58:09 +0000 Subject: [PATCH] update timemap to version 1.6 --- htdocs/js/timemap/loaders/flickr.js | 70 +- htdocs/js/timemap/loaders/georss.js | 65 +- .../js/timemap/loaders/google_spreadsheet.js | 154 +- htdocs/js/timemap/loaders/json.js | 139 +- htdocs/js/timemap/loaders/kml.js | 105 +- htdocs/js/timemap/loaders/metaweb.js | 90 +- htdocs/js/timemap/loaders/progressive.js | 197 ++ htdocs/js/timemap/loaders/xml.js | 71 + htdocs/js/timemap/param.js | 274 ++ htdocs/js/timemap/state.js | 314 ++ htdocs/js/timemap/timemap.js | 2699 +++++++++++------ 11 files changed, 3015 insertions(+), 1163 deletions(-) create mode 100644 htdocs/js/timemap/loaders/progressive.js create mode 100644 htdocs/js/timemap/loaders/xml.js create mode 100644 htdocs/js/timemap/param.js create mode 100644 htdocs/js/timemap/state.js diff --git a/htdocs/js/timemap/loaders/flickr.js b/htdocs/js/timemap/loaders/flickr.js index 1c33ddce..6a233853 100644 --- a/htdocs/js/timemap/loaders/flickr.js +++ b/htdocs/js/timemap/loaders/flickr.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,23 +9,24 @@ * * @author Nick Rabinowitz (www.nickrabinowitz.com) */ + +// for JSLint +/*global TimeMap */ /** * @class - * Flickr loader factory - inherits from jsonp loader + * Flickr loader: Load JSONP data from Flickr. * - *

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.

+ * + * @augments TimeMap.loaders.jsonp + * @requires loaders/json.js * - *

Depends on:

- * - * - * @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. * - *

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.

* - * @example Usage in TimeMap.init(): - + * @augments TimeMap.loaders.xml + * @requires loaders/xml.js + * @requires param.js + * @borrows TimeMap.loaders.georss.parse as #parse + * + * @example +TimeMap.init({ datasets: [ { title: "GeoRSS Dataset", @@ -41,35 +46,35 @@ url: "mydata.rss" // GeoRSS file to load - must be a local URL } } - ] + ], + // etc... +}); + * @see GeoRSS Example * - * @param {Object} options All options for the loader:
- *   {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; iThis is a loader for data from Google Spreadsheets. Takes an optional map + *

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

* @@ -24,15 +27,14 @@ * in the spreadsheet (Google's automatic date formatting won't work).

* *

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:

- * - * - * @example Usage in TimeMap.init(): - + * @example +TimeMap.init({ datasets: [ { title: "Google Spreadsheet by key", @@ -48,70 +50,104 @@ url: "http://spreadsheets.google.com/feeds/list/pjUcDAp-oNIOjmx3LCxT4XA/1/public/values?alt=json-in-script&callback=" } } - ] + ], + // etc... +}); + * @see Google Spreadsheet Example + * @see Google Spreadsheet Example, Arbitrary Columns * - * @param {Object} options All options for the loader:
- *   {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:

- * + * @augments TimeMap.loaders.remote + * + * @requires lib/json2.pack.js * - * @example Usage in TimeMap.init(): - + * @example +TimeMap.init({ datasets: [ { title: "JSON String Dataset", @@ -118,20 +91,28 @@ TimeMap.loaders.jsonp.read = function(url, f) { url: "mydata.json" // Must be a local URL } } - ] + ], + // etc... +}); * - * @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
- * 
- * @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; iThis is a loader for data from the Metaweb service at freebase.com. See - * the API documentation at http://www.freebase.com/view/en/documentation for + * the API documentation at http://www.freebase.com/docs/mql/ch01.html for * a description of how to write MQL queries. This code is based on code from * the API site.

* - *

Depends on:

- *
    - *
  • lib/json2.pack.js
  • - *
  • loaders/jsonp.js
  • - *
+ * @augments TimeMap.loaders.jsonp + * @requires lib/json2.pack.js + * @requires loaders/jsonp.js * - * @example Usage in TimeMap.init(): - + * @example +TimeMap.init({ datasets: [ { title: "Freebase Dataset", @@ -49,28 +45,56 @@ } } } - ] + ], + // etc... +}); + * @see Metaweb Example * - * @param {Object} options All options for the loader:
- *   {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; x 1) { + tagName = nameParts[1]; + ns = nameParts[0]; + } + // set to config + param.setConfig(config, TimeMap.util.getTagValue(node, tagName, ns)); + }; + }, + + /** + * @class + * A convenience class for those parameters which deal with a value + * in the options of a TimeMap or TimeMapItem object, setting some + * additional default functions. + * + * @augments TimeMap.params.Param + * + * @constructor + * @param {String} paramName String name of the option parameter + * @param {Object} [options] Container for named arguments (see {@link TimeMap.params.Param}) + */ + OptionParam: function(paramName, options) { + options = options || {}; + var defaults = { + + /** + * Get the current state value from the opts object of a TimeMap or TimeMapItem + * @name TimeMap.params.OptionParam#get + * @function + * + * @param {TimeMap|TimeMapItem} o Object to inspect + * @return {mixed} Current state value + */ + get: function(o) { + return o.opts[paramName]; + }, + + /** + * Set the state value in the opts object of a TimeMap or TimeMapItem + * @name TimeMap.params.OptionParam#set + * + * @param {TimeMap|TimeMapItem} o Object to modify + * @param {mixed} value Value to set + */ + set: function(o, value) { + o.opts[paramName] = value; + }, + + /** + * Set a new value on a config object for TimeMap.init() or a particular item + * @name TimeMap.params.OptionParam#setConfig + * @function + * + * @param {Object} config Config object to modify + * @param {mixed} value Value to set + */ + setConfig: function(config, value) { + config.options = config.options || {}; + config.options[paramName] = value; + } + + }; + options = TimeMap.util.merge(options, defaults); + return new params.Param(paramName, options); + } +}; + + +/*---------------------------------------------------------------------------- + * TimeMapItem params + *---------------------------------------------------------------------------*/ + +/** + * @namespace Namespace for parameters used for loading data into a TimeMapItem + * object. Because these are intended for loading, only setConfig is defined. + */ +TimeMap.loaders.base.prototype.params = { + /** + * Item title + * @type TimeMap.params.Param + */ + title: new params.Param("title"), + + /** + * Item start date + * @type TimeMap.params.Param + */ + start: new params.Param("start"), + + /** + * Item end date + * @type TimeMap.params.Param + */ + end: new params.Param("end"), + + /** + * Item description + * @type TimeMap.params.OptionParam + */ + description: new params.OptionParam("description"), + + /** + * Item latitude + * @type TimeMap.params.Param + */ + lat: new params.Param("lat", { + setConfig: function(config, value) { + config.point = config.point || {}; + config.point.lat = value; + } + }), + + /** + * Item longitude + * @type TimeMap.params.Param + */ + lon: new params.Param("lon", { + setConfig: function(config, value) { + config.point = config.point || {}; + config.point.lon = value; + } + }) +}; + +})(); diff --git a/htdocs/js/timemap/state.js b/htdocs/js/timemap/state.js new file mode 100644 index 00000000..017c290c --- /dev/null +++ b/htdocs/js/timemap/state.js @@ -0,0 +1,314 @@ +/* + * Timemap.js Copyright 2010 Nick Rabinowitz. + * Licensed under the MIT License (see LICENSE.txt) + */ + +/** + * @fileOverview + * Functions in this file are used to set the timemap state programmatically, + * either in a script or from the url hash. + * + * @requires param.js + * + * @author Nick Rabinowitz (www.nickrabinowitz.com) + */ + +// save a few bytes +(function() { + +/*---------------------------------------------------------------------------- + * State namespace, with setters, serializers, and url functions + *---------------------------------------------------------------------------*/ + +var paramNS = TimeMap.params, + + /** + * @name TimeMap.state + * @namespace Namespace for static state functions used to + * set the timemap state programmatically, either in a script or + * from the url hash. + * @see State Example + */ + stateNS = TimeMap.state = { + + /** + * Get the state parameters from the URL, returning as a config object + * + * @return {Object} Object with state config settings + */ + fromUrl: function() { + var pairs = location.hash.substring(1).split('&'), + params = stateNS.params, + state = {}, x, pair, key; + for (x=0; x < pairs.length; x++) { + if (pairs[x] != "") { + pair = pairs[x].split('='); + key = pair[0]; + if (key && key in params) { + state[key] = params[key].fromString(decodeURI(pair[1])); + } + } + } + return state; + }, + + /** + * Make a parameter string from a state object + * + * @param {Object} state Object with state config settings + * @return {String} Parameter string in URL param format + */ + toParamString: function(state) { + var params = stateNS.params, + paramArray = [], + key; + // go through each key in state + for (key in state) { + if (state.hasOwnProperty(key)) { + if (key in params) { + paramArray.push(key + "=" + encodeURI(params[key].toString(state[key]))); + } + } + } + return paramArray.join("&"); + }, + + /** + * Make a full URL from a state object + * + * @param {Object} state Object with state config settings + * @return {String} Full URL with parameters + */ + toUrl: function(state) { + var paramString = stateNS.toParamString(state), + url = location.href.split("#")[0]; + return url + "#" + paramString; + }, + + /** + * Set state settings on a config object for TimeMap.init() + * @see TimeMap.init + * + * @param {Object} config Config object for TimeMap.init(), modified in place + * @param {Object} state Object with state config settings + */ + setConfig: function(config, state) { + var params = stateNS.params, + key; + for (key in state) { + if (state.hasOwnProperty(key)) { + if (key in params) { + params[key].setConfig(config, state[key]); + } + } + } + }, + + /** + * Set state settings on a config object for TimeMap.init() using + * parameters in the URL. Note that as of Timemap.js v.1.6, this + * will run automatically if state functions are present. + * @see TimeMap.init + * @example + // set up the config object + var config = { + // various settings, as usual for TimeMap.init() + }; + + // get state settings from the URL, e.g.: + // http://www.example.com/mytimemap.html#zoom=4&selected=1 + TimeMap.state.setConfigFromUrl(config); + + // initialize TimeMap object + var tm = TimeMap.init(config); + * + * @param {Object} config Config object for TimeMap.init() + */ + setConfigFromUrl: function(config) { + stateNS.setConfig(config, stateNS.fromUrl()); + } + +}; + +/*---------------------------------------------------------------------------- + * TimeMap object methods + *---------------------------------------------------------------------------*/ + +/** + * Set the timemap state with a set of configuration options. + * + * @param {Object} state Object with state config settings + */ +TimeMap.prototype.setState = function(state) { + var params = stateNS.params, + key; + // go through each key in state + for (key in state) { + if (state.hasOwnProperty(key)) { + if (key in params) { + // run setter function with config value + params[key].set(this, state[key]); + } + } + } +}; + +/** + * Get a configuration object of state variables + * + * @return {Object} Object with state config settings + */ +TimeMap.prototype.getState = function() { + var state = {}, + params = stateNS.params, + key; + // run through params, adding values to state + for (key in params) { + if (params.hasOwnProperty(key)) { + // get state value + state[key] = params[key].get(this); + } + } + return state; +}; + +/** + * Initialize state tracking based on URL. + * Note: continuous tracking will only work + * on browsers that support the "onhashchange" event. + */ +TimeMap.prototype.initState = function() { + var tm = this; + tm.setStateFromUrl(); + window.onhashchange = function() { + tm.setStateFromUrl(); + }; +}; + +/** + * Set the timemap state with parameters in the URL + */ +TimeMap.prototype.setStateFromUrl = function() { + this.setState(stateNS.fromUrl()); +}; + +/** + * Get current state parameters serialized as a hash string + * + * @return {String} State parameters serialized as a hash string + */ +TimeMap.prototype.getStateParamString = function() { + return stateNS.toParamString(this.getState()); +}; + +/** + * Get URL with current state parameters in hash + * + * @return {String} URL with state parameters + */ +TimeMap.prototype.getStateUrl = function() { + return stateNS.toUrl(this.getState()); +}; + + +/*---------------------------------------------------------------------------- + * State parameters + *---------------------------------------------------------------------------*/ + +/** + * @namespace + * Namespace for state parameters, each with a set of functions to set and serialize values. + * Add your own Param objects to this namespace to get and set additional state variables. + */ +TimeMap.state.params = { + + /** + * Map zoom level + * @type TimeMap.params.Param + */ + zoom: new paramNS.OptionParam("mapZoom", { + get: function(tm) { + return tm.map.getZoom(); + }, + set: function(tm, value) { + tm.map.setZoom(value); + }, + fromStr: function(s) { + return parseInt(s); + } + }), + + /** + * Map center + * @type TimeMap.params.Param + */ + center: new paramNS.OptionParam("mapCenter", { + get: function(tm) { + return tm.map.getCenter(); + }, + set: function(tm, value) { + tm.map.setCenter(value); + }, + fromStr: function(s) { + var params = s.split(","); + if (params.length < 2) { + // give up + return null; + } + return new GLatLng( + parseFloat(params[0]), + parseFloat(params[1]) + ); + }, + toStr: function(value) { + return value.lat() + "," + value.lng(); + } + }), + + /** + * Timeline center date + * @type TimeMap.params.Param + */ + date: new paramNS.Param("scrollTo", { + get: function(tm) { + return tm.timeline.getBand(0).getCenterVisibleDate(); + }, + set: function(tm, value) { + tm.scrollToDate(value); + }, + fromStr: function(s) { + return TimeMapDataset.hybridParser(s); + }, + toStr: function(value) { + return TimeMap.util.formatDate(value); + } + }), + + /** + * Index of selected/open item, if any + * @type TimeMap.params.Param + */ + selected: new paramNS.Param("selected", { + get: function(tm) { + var items = tm.getItems(), + i = items.length-1; + while (i >= 0 && i--) { + if (items[i].selected) break; + } + return i; + }, + set: function(tm, value) { + if (value >= 0) { + var item = tm.getItems()[value]; + if (item) { + item.openInfoWindow(); + } + } + }, + fromStr: function(s) { + return parseInt(s); + } + }) +}; + +})(); diff --git a/htdocs/js/timemap/timemap.js b/htdocs/js/timemap/timemap.js index d16d7cb0..51790119 100644 --- a/htdocs/js/timemap/timemap.js +++ b/htdocs/js/timemap/timemap.js @@ -4,26 +4,51 @@ */ /** - * @fileOverview - * Core functions for the timemap.js library. - * Timemap.js is intended to sync a SIMILE Timeline with a Google Map. - * Dependencies: Google Maps API v2, SIMILE Timeline v1.2 - 2.3.1 - * Thanks to Jorn Clausen (http://www.oe-files.de) for initial concept and code. + * @overview * + *

Timemap.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).

+ * + * + * @name timemap.js * @author Nick Rabinowitz (www.nickrabinowitz.com) + * @version 1.6 */ // globals - for JSLint -/*global GBrowserIsCompatible, GLargeMapControl, GLatLngBounds, GMap2 */ -/*global GMapTypeControl, GDownloadUrl, GEvent, GGroundOverlay, GIcon */ -/*global GMarker, GPolygon, GPolyline, GSize, GLatLng, G_DEFAULT_ICON */ -/*global G_DEFAULT_MAP_TYPES, G_NORMAL_MAP, G_PHYSICAL_MAP, G_HYBRID_MAP */ -/*global G_MOON_VISIBLE_MAP, G_SKY_VISIBLE_MAP, G_SATELLITE_MAP, Timeline */ +/*global GBrowserIsCompatible, GLargeMapControl, GMap2, GIcon */ +/*global GMapTypeControl, GDownloadUrl, GGroundOverlay */ +/*global GMarker, GPolygon, GPolyline, GSize, G_DEFAULT_ICON */ +/*global G_HYBRID_MAP, G_MOON_VISIBLE_MAP, G_SKY_VISIBLE_MAP */ -// A couple of aliases to save a few bytes -var DT = Timeline.DateTime, -// Google icon path -GIP = "http://www.google.com/intl/en_us/mapfiles/ms/icons/"; +(function(){ + +// borrowing some space-saving devices from jquery +var + // Will speed up references to window, and allows munging its name. + window = this, + // Will speed up references to undefined, and allows munging its name. + undefined, + // aliases for Timeline objects + Timeline = window.Timeline, DateTime = Timeline.DateTime, + // aliases for Google variables (anything that gets used more than once) + G_DEFAULT_MAP_TYPES = window.G_DEFAULT_MAP_TYPES, + G_NORMAL_MAP = window.G_NORMAL_MAP, + G_PHYSICAL_MAP = window.G_PHYSICAL_MAP, + G_SATELLITE_MAP = window.G_SATELLITE_MAP, + GLatLng = window.GLatLng, + GLatLngBounds = window.GLatLngBounds, + GEvent = window.GEvent, + // Google icon path + GIP = "http://www.google.com/intl/en_us/mapfiles/ms/icons/", + // aliases for class names, allowing munging + TimeMap, TimeMapFilterChain, TimeMapDataset, TimeMapTheme, TimeMapItem; /*---------------------------------------------------------------------------- * TimeMap Class @@ -32,176 +57,253 @@ GIP = "http://www.google.com/intl/en_us/mapfiles/ms/icons/"; /** * @class * The TimeMap object holds references to timeline, map, and datasets. - * This will create the visible map, but not the timeline, which must be initialized separately. * * @constructor - * @param {element} tElement The timeline element. - * @param {element} mElement The map element. - * @param {Object} options A container for optional arguments:
- *   {Boolean} syncBands            Whether to synchronize all bands in timeline
- *   {GLatLng} mapCenter            Point for map center
- *   {Number} mapZoom               Intial map zoom level
- *   {GMapType/String} mapType      The maptype for the map
- *   {Array} mapTypes               The set of maptypes available for the map
- *   {Function/String} mapFilter    How to hide/show map items depending on timeline state;
-                                    options: "hidePastFuture", "showMomentOnly", or function
- *   {Boolean} showMapTypeCtrl      Whether to display the map type control
- *   {Boolean} showMapCtrl          Whether to show map navigation control
- *   {Boolean} centerMapOnItems     Whether to center and zoom the map based on loaded item positions
- *   {Function} openInfoWindow      Function redefining how info window opens
- *   {Function} closeInfoWindow     Function redefining how info window closes
+ * 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 0 ? + // if the zoom has been set, use the map bounds + map.getBounds() : + // otherwise, start from scratch + new GLatLngBounds(); } -} +}; /** * Current library version. + * @constant * @type String */ -TimeMap.version = "1.5"; +TimeMap.version = "1.6"; + +/** + * @name TimeMap.util + * @namespace + * Namespace for TimeMap utility functions. + */ +var util = TimeMap.util = {}; /** * Intializes a TimeMap. * - *

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 0) { @@ -1206,14 +1733,17 @@ TimeMapDataset.prototype.loadItem = function(data, transform) { GEvent.addListener(placemark[i], "click", function() { item.openInfoWindow(); }); - // add placemark and event to map and timeline - tm.map.addOverlay(placemark[i]); + // allow for custom placemark loading + if (!ds.opts.noPlacemarkLoad) { + // add placemark to map + tm.map.addOverlay(placemark[i]); + } // hide placemarks until the next refresh placemark[i].hide(); } } // add the item to the dataset - this.items.push(item); + ds.items.push(item); // return the item object return item; }; @@ -1225,203 +1755,169 @@ TimeMapDataset.prototype.loadItem = function(data, transform) { /** * @class * Predefined visual themes for datasets, defining colors and images for - * map markers and timeline events. + * map markers and timeline events. Note that theme is only used at creation + * time - updating the theme of an existing object won't do anything. * * @constructor - * @param {Object} options A container for optional arguments:
- *      {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: '
{{title}}
' + + '
{{description}}
', + templatePattern: /{{([^}]+)}}/g, + closeInfoWindow: TimeMapItem.closeInfoWindowBasic + }; + /** * This item's timeline event + * @name TimeMapItem#event * @type Timeline.Event */ - this.event = event; + item.event = event; /** * This item's parent dataset + * @name TimeMapItem#dataset * @type TimeMapDataset */ - this.dataset = dataset; + item.dataset = dataset; /** * The timemap's map object + * @name TimeMapItem#map * @type GMap2 */ - this.map = dataset.timemap.map; + item.map = dataset.timemap.map; + + /** + * The timemap's timeline object + * @name TimeMapItem#timeline + * @type Timeline + */ + item.timeline = dataset.timemap.timeline; // initialize placemark(s) with some type juggling - if (placemark && TimeMap.util.isArray(placemark) && placemark.length === 0) { + if (placemark && util.isArray(placemark) && placemark.length === 0) { placemark = null; } if (placemark && placemark.length == 1) { @@ -1479,114 +2007,182 @@ function TimeMapItem(placemark, event, dataset, options) { } /** * This item's placemark(s) - * @type GMarker/GPolyline/GPolygon/GOverlay/Array + * @name TimeMapItem#placemark + * @type GMarker|GPolyline|GPolygon|GOverlay|Array */ - this.placemark = placemark; + item.placemark = placemark; - // set defaults for options - this.opts = options || {}; - this.opts.type = options.type || ''; - this.opts.title = options.title || ''; - this.opts.description = options.description || ''; - this.opts.infoPoint = options.infoPoint || null; - this.opts.infoHtml = options.infoHtml || ''; - this.opts.infoUrl = options.infoUrl || ''; + /** + * Container for optional settings passed in through the "options" parameter + * @name TimeMapItem#opts + * @type Object + */ + item.opts = options = util.merge(options, defaults, dataset.opts); - // get functions + // select default open function + if (!options.openInfoWindow) { + if (options.infoUrl !== "") { + // load via AJAX if URL is provided + options.openInfoWindow = TimeMapItem.openInfoWindowAjax; + } else { + // otherwise default to basic window + options.openInfoWindow = TimeMapItem.openInfoWindowBasic; + } + } + + // getter functions /** * Return the placemark type for this item + * @name TimeMapItem#getType + * @function * * @return {String} Placemark type */ - this.getType = function() { return this.opts.type; }; + item.getType = function() { return item.opts.type; }; /** * Return the title for this item + * @name TimeMapItem#getTitle + * @function * * @return {String} Item title */ - this.getTitle = function() { return this.opts.title; }; + item.getTitle = function() { return item.opts.title; }; /** * Return the item's "info point" (the anchor for the map info window) + * @name TimeMapItem#getInfoPoint + * @function * * @return {GLatLng} Info point */ - this.getInfoPoint = function() { + item.getInfoPoint = function() { // default to map center if placemark not set - return this.opts.infoPoint || this.map.getCenter(); + return item.opts.infoPoint || item.map.getCenter(); }; /** - * Whether the item is visible + * Return the start date of the item's event, if any + * @name TimeMapItem#getStart + * @function + * + * @return {Date} Item start date or undefined + */ + item.getStart = function() { + if (item.event) { + return item.event.getStart(); + } + }; + + /** + * Return the end date of the item's event, if any + * @name TimeMapItem#getEnd + * @function + * + * @return {Date} Item end dateor undefined + */ + item.getEnd = function() { + if (item.event) { + return item.event.getEnd(); + } + }; + + /** + * Return the timestamp of the start date of the item's event, if any + * @name TimeMapItem#getStartTime + * @function + * + * @return {Number} Item start date timestamp or undefined + */ + item.getStartTime = function() { + var start = item.getStart(); + if (start) { + return start.getTime(); + } + }; + + /** + * Return the timestamp of the end date of the item's event, if any + * @name TimeMapItem#getEndTime + * @function + * + * @return {Number} Item end date timestamp or undefined + */ + item.getEndTime = function() { + var end = item.getEnd(); + if (end) { + return end.getTime(); + } + }; + + /** + * Whether the item is currently selected + * @name TimeMapItem#selected * @type Boolean */ - this.visible = true; + item.selected = false; + + /** + * Whether the item is visible + * @name TimeMapItem#visible + * @type Boolean + */ + item.visible = true; /** * Whether the item's placemark is visible + * @name TimeMapItem#placemarkVisible * @type Boolean */ - this.placemarkVisible = false; + item.placemarkVisible = false; /** * Whether the item's event is visible + * @name TimeMapItem#eventVisible * @type Boolean */ - this.eventVisible = true; - - // allow for custom open/close functions, set at item, dataset, or timemap level - var openFunction, dopts = dataset.opts, - tmopts = dataset.timemap.opts; - // set open function - openFunction = options.openInfoWindow || - dopts.openInfoWindow || - tmopts.openInfoWindow || - false; - if (!openFunction) { - if (this.opts.infoUrl !== "") { - // load via AJAX if URL is provided - openFunction = TimeMapItem.openInfoWindowAjax; - } else { - // otherwise default to basic window - openFunction = TimeMapItem.openInfoWindowBasic; - } - } + item.eventVisible = true; /** * Open the info window for this item. * By default this is the map infoWindow, but you can set custom functions * for whatever behavior you want when the event or placemark is clicked + * @name TimeMapItem#openInfoWindow * @function */ - this.openInfoWindow = openFunction; + item.openInfoWindow = function() { + options.openInfoWindow.call(item); + item.selected = true; + }; /** * Close the info window for this item. * By default this is the map infoWindow, but you can set custom functions * for whatever behavior you want. + * @name TimeMapItem#closeInfoWindow * @function */ - this.closeInfoWindow = options.closeInfoWindow || - dopts.closeInfoWindow || - tmopts.closeInfoWindow || - TimeMapItem.closeInfoWindowBasic; -} + item.closeInfoWindow = function() { + options.closeInfoWindow.call(item); + item.selected = false; + }; +}; /** * Show the map placemark(s) */ TimeMapItem.prototype.showPlacemark = function() { - if (this.placemark) { - if (this.getType() == "array") { - for (var i=0; i'; - if (this.opts.description !== "") { - html += '
' + this.opts.description + '
'; + if (!html) { + // fill in template + html = opts.infoTemplate; + match = opts.templatePattern.exec(html); + while (match) { + html = html.replace(match[0], opts[match[1]]); + match = opts.templatePattern.exec(html); } } // scroll timeline if necessary - if (this.placemark && !this.visible && this.event) { - var topband = this.dataset.timemap.timeline.getBand(0); - topband.setCenterVisibleDate(this.event.getStart()); + if (item.placemark && !item.placemarkVisible && item.event) { + item.dataset.timemap.scrollToDate(item.event.getStart()); } // open window - if (this.getType() == "marker") { - this.placemark.openInfoWindowHtml(html); + if (item.getType() == "marker") { + item.placemark.openInfoWindowHtml(html); } else { - this.map.openInfoWindowHtml(this.getInfoPoint(), html); + item.map.openInfoWindowHtml(item.getInfoPoint(), html); } - // custom functions will need to set this as well - this.selected = true; + // deselect when window is closed + item.closeListener = GEvent.addListener(item.map, "infowindowclose", function() { + // deselect + item.selected = false; + // kill self + GEvent.removeListener(item.closeListener); + }); }; /** * Open info window function using ajax-loaded text in map window */ TimeMapItem.openInfoWindowAjax = function() { - if (this.opts.infoHtml !== "") { // already loaded - change to static - this.openInfoWindow = TimeMapItem.openInfoWindowBasic; - this.openInfoWindow(); - } else { // load content via AJAX - if (this.opts.infoUrl !== "") { - var item = this; - GDownloadUrl(this.opts.infoUrl, function(result) { + var item = this; + if (!item.opts.infoHtml) { // load content via AJAX + if (item.opts.infoUrl) { + GDownloadUrl(item.opts.infoUrl, function(result) { item.opts.infoHtml = result; item.openInfoWindow(); }); - } else { // fall back on basic function - this.openInfoWindow = TimeMapItem.openInfoWindowBasic; - this.openInfoWindow(); + return; } } + // fall back on basic function if content is loaded or URL is missing + item.openInfoWindow = function() { + TimeMapItem.openInfoWindowBasic.call(item); + item.selected = true; + }; + item.openInfoWindow(); }; /** @@ -1696,20 +2305,12 @@ TimeMapItem.closeInfoWindowBasic = function() { this.map.closeInfoWindow(); } } - // custom functions will need to set this as well - this.selected = false; }; /*---------------------------------------------------------------------------- * Utility functions *---------------------------------------------------------------------------*/ -/** - * @namespace - * Namespace for TimeMap utility functions. - */ -TimeMap.util = {}; - /** * Convenience trim function * @@ -1737,12 +2338,12 @@ TimeMap.util.isArray = function(o) { * * @param {XML Node} n Node in which to look for tag * @param {String} tag Name of tag to look for - * @param {String} ns Optional namespace + * @param {String} [ns] XML namespace to look in * @return {String} Tag value as string */ TimeMap.util.getTagValue = function(n, tag, ns) { - var str = ""; - var nList = TimeMap.util.getNodeList(n, tag, ns); + var str = "", + nList = TimeMap.util.getNodeList(n, tag, ns); if (nList.length > 0) { n = nList[0].firstChild; // fix for extra-long nodes @@ -1757,17 +2358,21 @@ TimeMap.util.getTagValue = function(n, tag, ns) { /** * Empty container for mapping XML namespaces to URLs + * @example + TimeMap.util.nsMap['georss'] = 'http://www.georss.org/georss'; + // find georss:point + TimeMap.util.getNodeList(node, 'point', 'georss') */ TimeMap.util.nsMap = {}; /** * Cross-browser implementation of getElementsByTagNameNS. * Note: Expects any applicable namespaces to be mapped in - * TimeMap.util.nsMap. XXX: There may be better ways to do this. + * {@link TimeMap.util.nsMap}. * * @param {XML Node} n Node in which to look for tag * @param {String} tag Name of tag to look for - * @param {String} ns Optional namespace + * @param {String} [ns] XML namespace to look in * @return {XML Node List} List of nodes with the specified tag name */ TimeMap.util.getNodeList = function(n, tag, ns) { @@ -1788,7 +2393,7 @@ TimeMap.util.getNodeList = function(n, tag, ns) { * Make TimeMap.init()-style points from a GLatLng, array, or string * * @param {Object} coords GLatLng, array, or string to convert - * @param {Boolean} reversed Whether the points are KML-style lon/lat, rather than lat/lon + * @param {Boolean} [reversed] Whether the points are KML-style lon/lat, rather than lat/lon * @return {Object} TimeMap.init()-style point */ TimeMap.util.makePoint = function(coords, reversed) { @@ -1803,7 +2408,7 @@ TimeMap.util.makePoint = function(coords, reversed) { latlon = coords; } // string - if (latlon === null) { + if (!latlon) { // trim extra whitespace coords = trim(coords); if (coords.indexOf(',') > -1) { @@ -1815,9 +2420,13 @@ TimeMap.util.makePoint = function(coords, reversed) { } } // deal with extra coordinates (i.e. KML altitude) - if (latlon.length > 2) latlon = latlon.slice(0, 2); + if (latlon.length > 2) { + latlon = latlon.slice(0, 2); + } // deal with backwards (i.e. KML-style) coordinates - if (reversed) latlon.reverse(); + if (reversed) { + latlon.reverse(); + } return { "lat": trim(latlon[0]), "lon": trim(latlon[1]) @@ -1827,16 +2436,16 @@ TimeMap.util.makePoint = function(coords, reversed) { /** * Make TimeMap.init()-style polyline/polygons from a whitespace-delimited * string of coordinates (such as those in GeoRSS and KML). - * XXX: Any reason for this to take arrays of GLatLngs as well? * * @param {Object} coords String to convert - * @param {Boolean} reversed Whether the points are KML-style lon/lat, rather than lat/lon + * @param {Boolean} [reversed] Whether the points are KML-style lon/lat, rather than lat/lon * @return {Object} Formated coordinate array */ TimeMap.util.makePoly = function(coords, reversed) { - var poly = [], latlon; - var coordArr = TimeMap.util.trim(coords).split(/[\r\n\f ]+/); - if (coordArr.length == 0) return []; + var poly = [], + latlon, + coordArr = TimeMap.util.trim(coords).split(/[\r\n\f ]+/); + if (coordArr.length === 0) return []; // loop through coordinates for (var x=0; x 0) ? @@ -1845,9 +2454,13 @@ TimeMap.util.makePoly = function(coords, reversed) { // space-separated coordinates - increment to step by 2s [coordArr[x], coordArr[++x]]; // deal with extra coordinates (i.e. KML altitude) - if (latlon.length > 2) latlon = latlon.slice(0, 2); + if (latlon.length > 2) { + latlon = latlon.slice(0, 2); + } // deal with backwards (i.e. KML-style) coordinates - if (reversed) latlon.reverse(); + if (reversed) { + latlon.reverse(); + } poly.push({ "lat": latlon[0], "lon": latlon[1] @@ -1860,10 +2473,11 @@ TimeMap.util.makePoly = function(coords, reversed) { * Format a date as an ISO 8601 string * * @param {Date} d Date to format - * @param {int} precision Optional precision indicator: - * 3 (default): Show full date and time - * 2: Show full date and time, omitting seconds - * 1: Show date only + * @param {Number} [precision] Precision indicator:
+ *      3 (default): Show full date and time
+ *      2: Show full date and time, omitting seconds
+ *      1: Show date only
+ *
* @return {String} Formatted string */ TimeMap.util.formatDate = function(d, precision) { @@ -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 0 && 'mergeOnly' in obj) { + for (y=0; y#FE766A + * This is the default. + * + * @type TimeMapTheme + */ + red: new TimeMapTheme(), + + /** + * Blue theme: #5A7ACF + * + * @type TimeMapTheme + */ + blue: new TimeMapTheme({ + iconImage: GIP + "blue-dot.png", + color: "#5A7ACF", + eventIconImage: "blue-circle.png" + }), + + /** + * Green theme: #19CF54 + * + * @type TimeMapTheme + */ + green: new TimeMapTheme({ + iconImage: GIP + "green-dot.png", + color: "#19CF54", + eventIconImage: "green-circle.png" + }), + + /** + * Light blue theme: #5ACFCF + * + * @type TimeMapTheme + */ + ltblue: new TimeMapTheme({ + iconImage: GIP + "ltblue-dot.png", + color: "#5ACFCF", + eventIconImage: "ltblue-circle.png" + }), + + /** + * Purple theme: #8E67FD + * + * @type TimeMapTheme + */ + purple: new TimeMapTheme({ + iconImage: GIP + "purple-dot.png", + color: "#8E67FD", + eventIconImage: "purple-circle.png" + }), + + /** + * Orange theme: #FF9900 + * + * @type TimeMapTheme + */ + orange: new TimeMapTheme({ + iconImage: GIP + "orange-dot.png", + color: "#FF9900", + eventIconImage: "orange-circle.png" + }), + + /** + * Yellow theme: #ECE64A + * + * @type TimeMapTheme + */ + yellow: new TimeMapTheme({ + iconImage: GIP + "yellow-dot.png", + color: "#ECE64A", + eventIconImage: "yellow-circle.png" + }), + + /** + * Pink theme: #E14E9D + * + * @type TimeMapTheme + */ + pink: new TimeMapTheme({ + iconImage: GIP + "pink-dot.png", + color: "#E14E9D", + eventIconImage: "pink-circle.png" + }) +}; + +// save to window +window.TimeMap = TimeMap; +window.TimeMapFilterChain = TimeMapFilterChain; +window.TimeMapDataset = TimeMapDataset; +window.TimeMapTheme = TimeMapTheme; +window.TimeMapItem = TimeMapItem; + +})();