diff --git a/application/default/controllers/DataController.php b/application/default/controllers/DataController.php index 5854883d..d549bf77 100755 --- a/application/default/controllers/DataController.php +++ b/application/default/controllers/DataController.php @@ -273,7 +273,7 @@ class DataController extends Zend_Controller_Action $dateformat="D M j Y G:i:s O"; $md = new MetadataTable(); $db=$md->getAdapter(); - $state=$db->query('select id,uuid,title,description,timebegin,timeend from metadata where timebegin is not null'); + $state=$db->query('select id,uuid,title,timebegin,timeend from metadata where timebegin is not null'); $rows=$state->fetchAll(); $timexml=''; foreach($rows as $row) { @@ -331,6 +331,14 @@ class DataController extends Zend_Controller_Action $this->_helper->json($geomd); } /* + * 时空动态浏览 + */ + function timemapAction() + { + $sql='select id,uuid,west,south,north,east,title,timebegin,timeend,description from metadata where timebegin is not null'; + $this->view->rows=$this->db->fetchAll($sql); + } + /* * 返回XML源文件 */ function xmlAction() diff --git a/application/default/views/scripts/data/timemap.phtml b/application/default/views/scripts/data/timemap.phtml new file mode 100644 index 00000000..da88ba8e --- /dev/null +++ b/application/default/views/scripts/data/timemap.phtml @@ -0,0 +1,83 @@ +headTitle($this->config->title->site); +$this->headTitle($this->config->title->data); +$this->headTitle('时空导航'); +$this->headTitle()->setSeparator(' - '); +$this->headLink()->appendStylesheet('/css/metadata.css'); +$this->breadcrumb('首页'); +$this->breadcrumb(''.$this->config->title->data.''); +$this->breadcrumb('时空导航'); +$this->breadcrumb()->setSeparator(' > '); +$this->headScript()->appendFile('http://maps.google.com/maps?file=api&v=2&key=ABQIAAAACD-MqkkoOm60o_dvwdcKVhThiRESR0xRCe9JKd36EL3glTk0OxTsRzifkUWmTTrYWaE7dY1lYUlGxA'); +$this->headScript()->appendFile('/js/timeline_var.js'); +$this->headScript()->appendFile('/js/timeline_js/timeline-api.js'); +$this->headScript()->appendFile('/js/timemap/timemap.js'); +$this->headScript()->captureStart(); +?> +var tm; +window.onload=function() { + tm = TimeMap.init({ + mapId: "map", // Id of map div element (required) + timelineId: "timeline", // Id of timeline div element (required) + options: { + eventIconPath: "../images/" + }, + datasets: [ + { + id: "artists", + title: "Artists", + theme: "orange", + // note that the lines below are now the preferred syntax + type: "basic", + options: { + items: [ + rows as $row) : ?> + { + "start" : "", + + "end" : "", + + "polygon" : [ + { + "lat" : , + "lon" : + },{ + "lat" : , + "lon" : + },{ + "lat" : , + "lon" : + },{ + "lat" : , + "lon" : + },{ + "lat" : , + "lon" : + } + ], + "title" : "", + "options" : { + // set the full HTML for the info window + "infoHtml": ">
/>" + } + }, + + ] + } + } + ], + bandIntervals: [ + Timeline.DateTime.MONTH, + Timeline.DateTime.DECADE + ] + }); + // manipulate the timemap further here if you like +} +window.onunload=GUnload(); +headScript()->captureEnd(); ?> +
partial('data/tools.phtml'); ?>
+
+
+
+
diff --git a/htdocs/js/timemap/loaders/flickr.js b/htdocs/js/timemap/loaders/flickr.js new file mode 100644 index 00000000..1c33ddce --- /dev/null +++ b/htdocs/js/timemap/loaders/flickr.js @@ -0,0 +1,78 @@ +/* + * Timemap.js Copyright 2008 Nick Rabinowitz. + * Licensed under the MIT License (see LICENSE.txt) + */ + +/** + * @fileOverview + * Flickr Loader + * + * @author Nick Rabinowitz (www.nickrabinowitz.com) + */ + +/** + * @class + * Flickr loader factory - inherits from jsonp loader + * + *

This is a loader for data from Flickr. 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.

+ * + *

Depends on:

+ * + * + * @example Usage in TimeMap.init(): + + datasets: [ + { + title: "Flickr Dataset", + type: "flickr", + options: { + // This is just the latest geotagged photo stream - try adding + // an "id" or "tag" or "photoset" parameter to get what you want + url: "http://www.flickr.com/services/feeds/geo/?format=json&jsoncallback=" + } + } + ] + * + * @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 + */ +TimeMap.loaders.flickr = function(options) { + var loader = new TimeMap.loaders.jsonp(options); + + // preload function for Flickr feeds + loader.preload = function(data) { + return data["items"]; + }; + + // transform function for Flickr feeds + loader.transform = function(data) { + var item = { + title: data["title"], + start: data["date_taken"], + point: { + lat: data["latitude"], + lon: data["longitude"] + }, + options: { + description: data["description"] + .replace(/>/g, ">") + .replace(/</g, "<") + .replace(/"/g, '"') + } + }; + 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 new file mode 100644 index 00000000..30e82de1 --- /dev/null +++ b/htdocs/js/timemap/loaders/georss.js @@ -0,0 +1,203 @@ +/* + * Timemap.js Copyright 2008 Nick Rabinowitz. + * Licensed under the MIT License (see LICENSE.txt) + */ + +/** + * @fileOverview + * GeoRSS Loader + * + * @author Nick Rabinowitz (www.nickrabinowitz.com) + */ + +/*globals GXml, TimeMap, TimeMapDataset */ + +/** + * @class + * GeoRSS loader factory - inherits from remote loader. + * + *

This is a loader class for GeoRSS feeds. Parsing is complicated by the + * diversity of GeoRSS formats; this parser handles:

+ * + *

and looks for geographic information in the following formats:

+ * + *

At the moment, this only supports points; polygons, polylines, and boxes + * will be added at some later point.

+ * + * @example Usage in TimeMap.init(): + + datasets: [ + { + title: "GeoRSS Dataset", + type: "georss", // Data to be loaded in GeoRSS + options: { + url: "mydata.rss" // GeoRSS file to load - must be a local URL + } + } + ] + * + * @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 + */ +TimeMap.loaders.georss = function(options) { + var loader = new TimeMap.loaders.remote(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 + */ +TimeMap.loaders.georss.parse = function(rss) { + var items = [], data, node, placemarks, pm; + node = GXml.parse(rss); + + // get TimeMap utilty functions + // assigning to variables should compress better + var util = TimeMap.util; + var getTagValue = util.getTagValue, + getNodeList = util.getNodeList, + makePoint = util.makePoint, + makePoly = util.makePoly, + formatDate = util.formatDate, + nsMap = util.nsMap; + + // define namespaces + nsMap.georss = 'http://www.georss.org/georss'; + nsMap.gml = 'http://www.opengis.net/gml'; + nsMap.geo = 'http://www.w3.org/2003/01/geo/wgs84_pos#'; + nsMap.kml = 'http://www.opengis.net/kml/2.2'; + + // determine whether this is an Atom feed or an RSS feed + var feedType = (node.firstChild.tagName == 'rss') ? 'rss' : 'atom'; + + // look for placemarks + var tName = (feedType == 'rss' ? "item" : "entry"); + placemarks = getNodeList(node, tName); + for (var i=0; i 0) { + data.start = getTagValue(nList[0], "when", "kml"); + } + // look for timespan + if (!data.start) { + nList = getNodeList(pm, "TimeSpan", "kml"); + if (nList.length > 0) { + data.start = getTagValue(nList[0], "begin", "kml"); + data.end = getTagValue(nList[0], "end", "kml") || + // unbounded spans end at the present time + formatDate(new Date()); + } + } + // otherwise, use pubDate/updated elements + if (!data.start) { + if (feedType == 'rss') { + // RSS needs date conversion + var d = new Date(Date.parse(getTagValue(pm, "pubDate"))); + // reformat + data.start = formatDate(d); + } else { + // atom uses ISO 8601 + data.start = getTagValue(pm, "updated"); + } + } + // find placemark - single geometry only for the moment + PLACEMARK: { + var coords, geom; + // look for point, GeoRSS-Simple + coords = getTagValue(pm, "point", 'georss'); + if (coords) { + data.point = makePoint(coords); + break PLACEMARK; + } + // look for point, GML + nList = getNodeList(pm, "Point", 'gml'); + if (nList.length > 0) { + // GML + coords = getTagValue(nList[0], "pos", 'gml'); + // GML + if (!coords) { + coords = getTagValue(nList[0], "coordinates", 'gml'); + } + if (coords) { + data.point = makePoint(coords); + break PLACEMARK; + } + } + // look for point, W3C Geo + if (getTagValue(pm, "lat", 'geo')) { + coords = [ + getTagValue(pm, "lat", 'geo'), + getTagValue(pm, "long", 'geo') + ]; + data.point = makePoint(coords); + break PLACEMARK; + } + // look for polyline, GeoRSS-Simple + coords = getTagValue(pm, "line", 'georss'); + if (coords) { + data.polyline = makePoly(coords); + break PLACEMARK; + } + // look for polygon, GeoRSS-Simple + coords = getTagValue(pm, "polygon", 'georss'); + if (coords) { + data.polygon = makePoly(coords); + break PLACEMARK; + } + // look for polyline, GML + nList = getNodeList(pm, "LineString", 'gml'); + if (nList.length > 0) { + geom = "polyline"; + } else { + nList = getNodeList(pm, "Polygon", 'gml'); + if (nList.length > 0) { + geom = "polygon"; + } + } + if (nList.length > 0) { + // GML + coords = getTagValue(nList[0], "posList", 'gml'); + // GML + if (!coords) { + coords = getTagValue(nList[0], "coordinates", 'gml'); + } + if (coords) { + data[geom] = makePoly(coords); + break PLACEMARK; + } + } + + // XXX: deal with boxes + } + items.push(data); + } + + // clean up + node = null; + placemarks = null; + pm = null; + nList = null; + return items; +}; diff --git a/htdocs/js/timemap/loaders/google_spreadsheet.js b/htdocs/js/timemap/loaders/google_spreadsheet.js new file mode 100644 index 00000000..e912df87 --- /dev/null +++ b/htdocs/js/timemap/loaders/google_spreadsheet.js @@ -0,0 +1,117 @@ +/* + * Timemap.js Copyright 2008 Nick Rabinowitz. + * Licensed under the MIT License (see LICENSE.txt) + */ + +/** + * @fileOverview + * Google Spreadsheet Loader + * + * @author Nick Rabinowitz (www.nickrabinowitz.com) + */ + +/** + * @class + * Google spreadsheet loader factory - inherits from jsonp loader. + * + *

This is a loader for data from Google Spreadsheets. 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

+ * + *

See http://code.google.com/apis/spreadsheets/docs/2.0/reference.html#gsx_reference + * for details on how spreadsheet column ids are derived. Note that date fields + * must be in yyyy-mm-dd format - you may need to set the cell format as "plain text" + * 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.

+ * + *

Depends on:

+ *
    + *
  • loaders/jsonp.js
  • + *
+ * + * @example Usage in TimeMap.init(): + + datasets: [ + { + title: "Google Spreadsheet by key", + type: "gss", + options: { + key: "pjUcDAp-oNIOjmx3LCxT4XA" // Spreadsheet key + } + }, + { + title: "Google Spreadsheet by url", + type: "gss", + options: { + url: "http://spreadsheets.google.com/feeds/list/pjUcDAp-oNIOjmx3LCxT4XA/1/public/values?alt=json-in-script&callback=" + } + } + ] + * + * @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 + */ +TimeMap.loaders.gss = function(options) { + var loader = new TimeMap.loaders.jsonp(options); + + // 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 + loader.preload = function(data) { + return data["feed"]["entry"]; + }; + + // transform function for spreadsheet data + 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") + } + }; + // hook for further transformation + if (options.transformFunction) + item = options.transformFunction(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' +}; diff --git a/htdocs/js/timemap/loaders/json.js b/htdocs/js/timemap/loaders/json.js new file mode 100644 index 00000000..558f13dd --- /dev/null +++ b/htdocs/js/timemap/loaders/json.js @@ -0,0 +1,139 @@ +/* + * Timemap.js Copyright 2008 Nick Rabinowitz. + * Licensed under the MIT License (see LICENSE.txt) + */ + +/** + * @fileOverview + * JSON Loaders (JSONP, JSON String) + * + * @author Nick Rabinowitz (www.nickrabinowitz.com) + */ + +/** + * @class + * JSONP loader class - 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.

+ * + * @example Usage in TimeMap.init(): + + datasets: [ + { + title: "JSONP Dataset", + type: "jsonp", + options: { + url: "http://www.test.com/getsomejson.php?callback=" + } + } + ] + * + * @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
+ * 
+ */ +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); +}; + +/** + * @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.

+ * + *

Depends on:

+ *
    + *
  • lib/json2.pack.js
  • + *
+ * + * @example Usage in TimeMap.init(): + + datasets: [ + { + title: "JSON String Dataset", + type: "json_string", + options: { + url: "mydata.json" // Must be a local URL + } + } + ] + * + * @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 + */ +TimeMap.loaders.json_string = function(options) { + var loader = new TimeMap.loaders.remote(options); + 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 +// the specific one you want. +TimeMap.loaders.json = TimeMap.loaders.jsonp; diff --git a/htdocs/js/timemap/loaders/kml.js b/htdocs/js/timemap/loaders/kml.js new file mode 100644 index 00000000..55206ec3 --- /dev/null +++ b/htdocs/js/timemap/loaders/kml.js @@ -0,0 +1,168 @@ +/* + * Timemap.js Copyright 2008 Nick Rabinowitz. + * Licensed under the MIT License (see LICENSE.txt) + */ + +/** + * @fileOverview + * KML Loader + * + * @author Nick Rabinowitz (www.nickrabinowitz.com) + */ + +/*globals GXml, TimeMap */ + +/** + * @class + * KML loader factory - inherits from remote loader + * + *

This is a loader class for KML files. Currently supports all geometry + * types (point, polyline, polygon, and overlay) and multiple geometries.

+ * + * @example Usage in TimeMap.init(): + + datasets: [ + { + title: "KML Dataset", + type: "kml", + options: { + url: "mydata.kml" // Must be local + } + } + ] + * + * @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 + */ +TimeMap.loaders.kml = function(options) { + var loader = new TimeMap.loaders.remote(options); + loader.parse = TimeMap.loaders.kml.parse; + return loader; +} + +/** + * Static function to parse KML with time data. + * + * @param {XML string} kml KML to be parsed + * @return {TimeMapItem Array} Array of TimeMapItems + */ +TimeMap.loaders.kml.parse = function(kml) { + var items = [], data, kmlnode, placemarks, pm, i, j; + kmlnode = GXml.parse(kml); + + // get TimeMap utilty functions + // assigning to variables should compress better + var util = TimeMap.util; + var getTagValue = util.getTagValue, + getNodeList = util.getNodeList, + makePoint = util.makePoint, + makePoly = util.makePoly, + formatDate = util.formatDate; + + // recursive time data search + var findNodeTime = function(n, data) { + var check = false; + // look for instant timestamp + var nList = getNodeList(n, "TimeStamp"); + if (nList.length > 0) { + data.start = getTagValue(nList[0], "when"); + check = true; + } + // otherwise look for span + else { + nList = getNodeList(n, "TimeSpan"); + if (nList.length > 0) { + data.start = getTagValue(nList[0], "begin"); + data.end = getTagValue(nList[0], "end") || + // unbounded spans end at the present time + formatDate(new Date()); + check = true; + } + } + // try looking recursively at parent nodes + if (!check) { + var pn = n.parentNode; + if (pn.nodeName == "Folder" || pn.nodeName=="Document") { + findNodeTime(pn, data); + } + pn = null; + } + }; + + // look for placemarks + placemarks = getNodeList(kmlnode, "Placemark"); + 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 + * 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
  • + *
+ * + * @example Usage in TimeMap.init(): + + datasets: [ + { + title: "Freebase Dataset", + type: "metaweb", + options: { + query: [ + { + // query here - see Metaweb API + } + ], + transformFunction: function(data) { + // map returned data to the expected format - see + // http://code.google.com/p/timemap/wiki/JsonFormat + return data; + } + } + } + ] + * + * @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 + */ +TimeMap.loaders.metaweb = function(options) { + var loader = new TimeMap.loaders.jsonp(options); + + // Host and service - default to freebase.com + loader.HOST = options.host || "http://www.freebase.com"; + loader.QUERY_SERVICE = options.service || "/api/service/mqlread"; + + // Metaweb preload functon + 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) { + // uncomment for debugging + /* + // If error, get error message and throw + var error = innerEnvelope.messages[0]; + throw error.code + ": " + error.message; + */ + return []; + } + // Get result from inner envelope + var result = innerEnvelope.result; + return 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/timemap.js b/htdocs/js/timemap/timemap.js new file mode 100644 index 00000000..d16d7cb0 --- /dev/null +++ b/htdocs/js/timemap/timemap.js @@ -0,0 +1,1935 @@ +/*! + * Timemap.js Copyright 2008 Nick Rabinowitz. + * Licensed under the MIT License (see LICENSE.txt) + */ + +/** + * @fileOverview + * Core functions for the timemap.js library. + * Timemap.js is intended to sync a SIMILE Timeline with a Google Map. + * Dependencies: Google Maps API v2, SIMILE Timeline v1.2 - 2.3.1 + * Thanks to Jorn Clausen (http://www.oe-files.de) for initial concept and code. + * + * @author Nick Rabinowitz (www.nickrabinowitz.com) + */ + +// globals - for JSLint +/*global GBrowserIsCompatible, GLargeMapControl, GLatLngBounds, GMap2 */ +/*global GMapTypeControl, GDownloadUrl, GEvent, GGroundOverlay, GIcon */ +/*global GMarker, GPolygon, GPolyline, GSize, GLatLng, G_DEFAULT_ICON */ +/*global G_DEFAULT_MAP_TYPES, G_NORMAL_MAP, G_PHYSICAL_MAP, G_HYBRID_MAP */ +/*global G_MOON_VISIBLE_MAP, G_SKY_VISIBLE_MAP, G_SATELLITE_MAP, Timeline */ + +// A couple of aliases to save a few bytes +var DT = Timeline.DateTime, +// Google icon path +GIP = "http://www.google.com/intl/en_us/mapfiles/ms/icons/"; + +/*---------------------------------------------------------------------------- + * TimeMap Class + *---------------------------------------------------------------------------*/ + +/** + * @class + * The TimeMap object holds references to timeline, map, and datasets. + * This will create the visible map, but not the timeline, which must be initialized separately. + * + * @constructor + * @param {element} tElement The timeline element. + * @param {element} mElement The map element. + * @param {Object} options A container for optional arguments:
+ *   {Boolean} syncBands            Whether to synchronize all bands in timeline
+ *   {GLatLng} mapCenter            Point for map center
+ *   {Number} mapZoom               Intial map zoom level
+ *   {GMapType/String} mapType      The maptype for the map
+ *   {Array} mapTypes               The set of maptypes available for the map
+ *   {Function/String} mapFilter    How to hide/show map items depending on timeline state;
+                                    options: "hidePastFuture", "showMomentOnly", or function
+ *   {Boolean} showMapTypeCtrl      Whether to display the map type control
+ *   {Boolean} showMapCtrl          Whether to show map navigation control
+ *   {Boolean} centerMapOnItems     Whether to center and zoom the map based on loaded item positions
+ *   {Function} openInfoWindow      Function redefining how info window opens
+ *   {Function} closeInfoWindow     Function redefining how info window closes
+ * 
+ */ +function TimeMap(tElement, mElement, options) { + // save elements + + /** + * Map element + * @type DOM Element + */ + this.mElement = mElement; + /** + * Timeline element + * @type DOM Element + */ + this.tElement = tElement; + + /** + * Map of datasets + * @type Object + */ + this.datasets = {}; + /** + * Filter chains for this timemap + * @type Object + */ + this.filters = {}; + /** + * Bounds of the map + * @type GLatLngBounds + */ + this.mapBounds = new GLatLngBounds(); + + // set defaults for options + + /** + * Container for optional settings passed in the "options" parameter + * @type Object + */ + this.opts = options || {}; // make sure the options object isn't null + // allow map types to be specified by key + if (typeof(options.mapType) == 'string') { + options.mapType = TimeMap.mapTypes[options.mapType]; + } + // allow map filters to be specified by key + if (typeof(options.mapFilter) == 'string') { + options.mapFilter = TimeMap.filters[options.mapFilter]; + } + // these options only needed for map initialization + var mapCenter = options.mapCenter || new GLatLng(0,0), + mapZoom = options.mapZoom || 0, + mapType = options.mapType || G_PHYSICAL_MAP, + mapTypes = options.mapTypes || [G_NORMAL_MAP, G_SATELLITE_MAP, G_PHYSICAL_MAP], + showMapTypeCtrl = ('showMapTypeCtrl' in options) ? options.showMapTypeCtrl : true, + showMapCtrl = ('showMapCtrl' in options) ? options.showMapCtrl : true; + + // these options need to be saved for later + this.opts.syncBands = ('syncBands' in options) ? options.syncBands : true; + this.opts.mapFilter = options.mapFilter || TimeMap.filters.hidePastFuture; + this.opts.centerOnItems = ('centerMapOnItems' in options) ? options.centerMapOnItems : true; + this.opts.theme = TimeMapTheme.create(options.theme, options); + + // initialize map + if (GBrowserIsCompatible()) { + /** + * The associated map object + * @type GMap2 + */ + this.map = new GMap2(this.mElement); + var map = this.map; + if (showMapCtrl) { + map.addControl(new GLargeMapControl()); + } + if (showMapTypeCtrl) { + map.addControl(new GMapTypeControl()); + } + // drop all existing types + var i; + for (i=G_DEFAULT_MAP_TYPES.length-1; i>0; i--) { + map.removeMapType(G_DEFAULT_MAP_TYPES[i]); + } + // you can't remove the last maptype, so add a new one first + map.addMapType(mapTypes[0]); + map.removeMapType(G_DEFAULT_MAP_TYPES[0]); + // add the rest of the new types + for (i=1; iThis is an attempt to create a general initialization script that will + * work in most cases. If you need a more complex initialization, write your + * own script instead of using this one.

+ * + *

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

+ * + *

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

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