1 | /*! |
---|
2 | * TimeMap Copyright 2008 Nick Rabinowitz. |
---|
3 | * Licensed under the MIT License (see LICENSE.txt) |
---|
4 | */ |
---|
5 | |
---|
6 | /**--------------------------------------------------------------------------- |
---|
7 | * TimeMap |
---|
8 | * |
---|
9 | * @author Nick Rabinowitz (www.nickrabinowitz.com) |
---|
10 | * The TimeMap object is intended to sync a SIMILE Timeline with a Google Map. |
---|
11 | * Dependencies: Google Maps API v2, SIMILE Timeline v1.2 or v2.2.0 |
---|
12 | * Thanks to Jörn Clausen (http://www.oe-files.de) for initial concept and code. |
---|
13 | *---------------------------------------------------------------------------*/ |
---|
14 | |
---|
15 | // globals - for JSLint |
---|
16 | /*global GBrowserIsCompatible, GLargeMapControl, GLatLngBounds, GMap2 */ |
---|
17 | /*global GMapTypeControl, GDownloadUrl, GEvent, GGroundOverlay, GIcon */ |
---|
18 | /*global GMarker, GPolygon, GPolyline, GSize, GLatLng, G_DEFAULT_ICON */ |
---|
19 | /*global G_DEFAULT_MAP_TYPES, G_NORMAL_MAP, G_PHYSICAL_MAP, G_HYBRID_MAP */ |
---|
20 | /*global G_MOON_VISIBLE_MAP, G_SKY_VISIBLE_MAP, G_SATELLITE_MAP, Timeline */ |
---|
21 | |
---|
22 | // A couple of aliases to save a few bytes |
---|
23 | |
---|
24 | |
---|
25 | var DT = Timeline.DateTime, |
---|
26 | // Google icon path |
---|
27 | GIP = "http://www.google.com/intl/en_us/mapfiles/ms/icons/"; |
---|
28 | |
---|
29 | /*---------------------------------------------------------------------------- |
---|
30 | * TimeMap Class - holds references to timeline, map, and datasets |
---|
31 | *---------------------------------------------------------------------------*/ |
---|
32 | |
---|
33 | /** |
---|
34 | * Creates a new TimeMap with map placemarks synched to timeline events |
---|
35 | * This will create the visible map, but not the timeline, which must be initialized separately. |
---|
36 | * |
---|
37 | * @constructor |
---|
38 | * @param {element} tElement The timeline element. |
---|
39 | * @param {element} mElement The map element. |
---|
40 | * @param {Object} options A container for optional arguments: |
---|
41 | * {Boolean} syncBands Whether to synchronize all bands in timeline |
---|
42 | * {GLatLng} mapCenter Point for map center |
---|
43 | * {Number} mapZoom Intial map zoom level |
---|
44 | * {GMapType/String} mapType The maptype for the map |
---|
45 | * {Array} mapTypes The set of maptypes available for the map |
---|
46 | * {Function/String} mapFilter How to hide/show map items depending on timeline state; |
---|
47 | options: "hidePastFuture", "showMomentOnly" |
---|
48 | * {Boolean} showMapTypeCtrl Whether to display the map type control |
---|
49 | * {Boolean} showMapCtrl Whether to show map navigation control |
---|
50 | * {Boolean} centerMapOnItems Whether to center and zoom the map based on loaded item positions |
---|
51 | * {Function} openInfoWindow Function redefining how info window opens |
---|
52 | * {Function} closeInfoWindow Function redefining how info window closes |
---|
53 | */ |
---|
54 | function TimeMap(tElement, mElement, options) { |
---|
55 | // save elements |
---|
56 | this.mElement = mElement; |
---|
57 | this.tElement = tElement; |
---|
58 | // initialize array of datasets |
---|
59 | this.datasets = {}; |
---|
60 | // initialize filters |
---|
61 | this.filters = {}; |
---|
62 | // initialize map bounds |
---|
63 | this.mapBounds = new GLatLngBounds(); |
---|
64 | |
---|
65 | // set defaults for options |
---|
66 | // other options can be set directly on the map or timeline |
---|
67 | this.opts = options || {}; // make sure the options object isn't null |
---|
68 | // allow map types to be specified by key |
---|
69 | if (typeof(options.mapType) == 'string') { |
---|
70 | options.mapType = TimeMap.mapTypes[options.mapType]; |
---|
71 | } |
---|
72 | // allow map filters to be specified by key |
---|
73 | if (typeof(options.mapFilter) == 'string') { |
---|
74 | options.mapFilter = TimeMap.filters[options.mapFilter]; |
---|
75 | } |
---|
76 | // these options only needed for map initialization |
---|
77 | var mapCenter = options.mapCenter || new GLatLng(0,0), |
---|
78 | mapZoom = options.mapZoom || 0, |
---|
79 | mapType = options.mapType || G_PHYSICAL_MAP, |
---|
80 | mapTypes = options.mapTypes || [G_NORMAL_MAP, G_SATELLITE_MAP, G_PHYSICAL_MAP], |
---|
81 | showMapTypeCtrl = ('showMapTypeCtrl' in options) ? options.showMapTypeCtrl : true, |
---|
82 | showMapCtrl = ('showMapCtrl' in options) ? options.showMapCtrl : true; |
---|
83 | |
---|
84 | // these options need to be saved for later |
---|
85 | this.opts.syncBands = ('syncBands' in options) ? options.syncBands : true; |
---|
86 | this.opts.mapFilter = options.mapFilter || TimeMap.filters.hidePastFuture; |
---|
87 | this.opts.centerOnItems = ('centerMapOnItems' in options) ? options.centerMapOnItems : true; |
---|
88 | |
---|
89 | // initialize map |
---|
90 | if (GBrowserIsCompatible()) { |
---|
91 | var map = this.map = new GMap2(this.mElement); |
---|
92 | if (showMapCtrl) { |
---|
93 | map.addControl(new GLargeMapControl()); |
---|
94 | } |
---|
95 | if (showMapTypeCtrl) { |
---|
96 | map.addControl(new GMapTypeControl()); |
---|
97 | } |
---|
98 | // drop all existing types |
---|
99 | var i; |
---|
100 | for (i=G_DEFAULT_MAP_TYPES.length-1; i>0; i--) { |
---|
101 | map.removeMapType(G_DEFAULT_MAP_TYPES[i]); |
---|
102 | } |
---|
103 | // you can't remove the last maptype, so add a new one first |
---|
104 | map.addMapType(mapTypes[0]); |
---|
105 | map.removeMapType(G_DEFAULT_MAP_TYPES[0]); |
---|
106 | // add the rest of the new types |
---|
107 | for (i=1; i<mapTypes.length; i++) { |
---|
108 | map.addMapType(mapTypes[i]); |
---|
109 | } |
---|
110 | map.enableDoubleClickZoom(); |
---|
111 | map.enableScrollWheelZoom(); |
---|
112 | map.enableContinuousZoom(); |
---|
113 | // initialize map center and zoom |
---|
114 | map.setCenter(mapCenter, mapZoom); |
---|
115 | // must be called after setCenter, for reasons unclear |
---|
116 | map.setMapType(mapType); |
---|
117 | } |
---|
118 | } |
---|
119 | |
---|
120 | /** |
---|
121 | * Current library version. |
---|
122 | */ |
---|
123 | TimeMap.version = "1.5pre"; |
---|
124 | |
---|
125 | /** |
---|
126 | * Intializes a TimeMap. |
---|
127 | * |
---|
128 | * This is an attempt to create a general initialization script that will |
---|
129 | * work in most cases. If you need a more complex initialization, write your |
---|
130 | * own script instead of using this one. |
---|
131 | * |
---|
132 | * The idea here is to throw all of the standard intialization settings into |
---|
133 | * a large object and then pass it to the TimeMap.init() function. The full |
---|
134 | * data format is outlined below, but if you leave elements off the script |
---|
135 | * will use default settings instead. |
---|
136 | * |
---|
137 | * Call TimeMap.init() inside of an onLoad() function (or a jQuery |
---|
138 | * $.(document).ready() function, or whatever you prefer). See the examples |
---|
139 | * for usage. |
---|
140 | * |
---|
141 | * @param {Object} config Full set of configuration options. |
---|
142 | * See examples/timemapinit_usage.js for format. |
---|
143 | */ |
---|
144 | TimeMap.init = function(config) { |
---|
145 | |
---|
146 | // check required elements |
---|
147 | if (!('mapId' in config) || !config.mapId) { |
---|
148 | throw "TimeMap.init: No id for map"; |
---|
149 | } |
---|
150 | if (!('timelineId' in config) || !config.timelineId) { |
---|
151 | throw "TimeMap.init: No id for timeline"; |
---|
152 | } |
---|
153 | |
---|
154 | // set defaults |
---|
155 | config = config || {}; // make sure the config object isn't null |
---|
156 | config.options = config.options || {}; |
---|
157 | config.datasets = config.datasets || []; |
---|
158 | config.bandInfo = config.bandInfo || false; |
---|
159 | config.scrollTo = config.scrollTo || "earliest"; |
---|
160 | if (!config.bandInfo && !config.bands) { |
---|
161 | var intervals = config.bandIntervals || |
---|
162 | config.options.bandIntervals || |
---|
163 | [DT.WEEK, DT.MONTH]; |
---|
164 | // allow intervals to be specified by key |
---|
165 | if (typeof(intervals) == 'string') { |
---|
166 | intervals = TimeMap.intervals[intervals]; |
---|
167 | } |
---|
168 | // save for later reference |
---|
169 | config.options.bandIntervals = intervals; |
---|
170 | // make default band info |
---|
171 | config.bandInfo = [ |
---|
172 | { |
---|
173 | width: "80%", |
---|
174 | intervalUnit: intervals[0], |
---|
175 | intervalPixels: 70 |
---|
176 | }, |
---|
177 | { |
---|
178 | width: "20%", |
---|
179 | intervalUnit: intervals[1], |
---|
180 | intervalPixels: 100, |
---|
181 | showEventText: false, |
---|
182 | overview: true, |
---|
183 | trackHeight: 0.4, |
---|
184 | trackGap: 0.2 |
---|
185 | } |
---|
186 | ]; |
---|
187 | } |
---|
188 | |
---|
189 | // create the TimeMap object |
---|
190 | var tm = new TimeMap( |
---|
191 | document.getElementById(config.timelineId), |
---|
192 | document.getElementById(config.mapId), |
---|
193 | config.options); |
---|
194 | |
---|
195 | // create the dataset objects |
---|
196 | var datasets = [], x, ds, dsOptions, dsId; |
---|
197 | for (x=0; x < config.datasets.length; x++) { |
---|
198 | ds = config.datasets[x]; |
---|
199 | dsOptions = ds.options || {}; |
---|
200 | dsOptions.title = ds.title || ''; |
---|
201 | dsOptions.theme = ds.theme; |
---|
202 | dsOptions.dateParser = ds.dateParser; |
---|
203 | dsId = ds.id || "ds" + x; |
---|
204 | datasets[x] = tm.createDataset(dsId, dsOptions); |
---|
205 | if (x > 0) { |
---|
206 | // set all to the same eventSource |
---|
207 | datasets[x].eventSource = datasets[0].eventSource; |
---|
208 | } |
---|
209 | } |
---|
210 | // add a pointer to the eventSource in the TimeMap |
---|
211 | tm.eventSource = datasets[0].eventSource; |
---|
212 | |
---|
213 | // set up timeline bands |
---|
214 | var bands = []; |
---|
215 | // ensure there's at least an empty eventSource |
---|
216 | var eventSource = (datasets[0] && datasets[0].eventSource) || new Timeline.DefaultEventSource(); |
---|
217 | // check for pre-initialized bands (manually created with Timeline.createBandInfo()) |
---|
218 | if (config.bands) { |
---|
219 | bands = config.bands; |
---|
220 | // substitute dataset event source |
---|
221 | for (x=0; x < bands.length; x++) { |
---|
222 | // assume that these have been set up like "normal" Timeline bands: |
---|
223 | // with an empty event source if events are desired, and null otherwise |
---|
224 | if (bands[x].eventSource !== null) { |
---|
225 | bands[x].eventSource = eventSource; |
---|
226 | } |
---|
227 | } |
---|
228 | } |
---|
229 | // otherwise, make bands from band info |
---|
230 | else { |
---|
231 | for (x=0; x < config.bandInfo.length; x++) { |
---|
232 | var bandInfo = config.bandInfo[x]; |
---|
233 | // if eventSource is explicitly set to null or false, ignore |
---|
234 | if (!(('eventSource' in bandInfo) && !bandInfo.eventSource)) { |
---|
235 | bandInfo.eventSource = eventSource; |
---|
236 | } |
---|
237 | else { |
---|
238 | bandInfo.eventSource = null; |
---|
239 | } |
---|
240 | bands[x] = Timeline.createBandInfo(bandInfo); |
---|
241 | if (x > 0 && TimeMap.TimelineVersion() == "1.2") { |
---|
242 | // set all to the same layout |
---|
243 | bands[x].eventPainter.setLayout(bands[0].eventPainter.getLayout()); |
---|
244 | } |
---|
245 | } |
---|
246 | } |
---|
247 | // initialize timeline |
---|
248 | tm.initTimeline(bands); |
---|
249 | |
---|
250 | // initialize load manager |
---|
251 | var loadManager = TimeMap.loadManager; |
---|
252 | loadManager.init(tm, config.datasets.length, config); |
---|
253 | |
---|
254 | // load data! |
---|
255 | for (x=0; x < config.datasets.length; x++) { |
---|
256 | (function(x) { // deal with closure issues |
---|
257 | var data = config.datasets[x], options, type, callback, loaderClass, loader; |
---|
258 | // support some older syntax |
---|
259 | options = data.options || data.data || {}; |
---|
260 | type = data.type || options.type; |
---|
261 | callback = function() { loadManager.increment() }; |
---|
262 | // get loader class |
---|
263 | loaderClass = (typeof(type) == 'string') ? TimeMap.loaders[type] : type; |
---|
264 | // load with appropriate loader |
---|
265 | loader = new loaderClass(options); |
---|
266 | loader.load(datasets[x], callback); |
---|
267 | })(x); |
---|
268 | } |
---|
269 | // return timemap object for later manipulation |
---|
270 | return tm; |
---|
271 | }; |
---|
272 | |
---|
273 | // for backwards compatibility |
---|
274 | var timemapInit = TimeMap.init; |
---|
275 | |
---|
276 | /** |
---|
277 | * Load manager - static singleton for managing multiple asynchronous loads |
---|
278 | */ |
---|
279 | TimeMap.loadManager = new function() { |
---|
280 | |
---|
281 | /** |
---|
282 | * Initialize (or reset) the load manager |
---|
283 | * |
---|
284 | * @param {TimeMap} tm TimeMap instance |
---|
285 | * @param {int} target Number of datasets we're loading |
---|
286 | * @param {Object} options Container for optional functions |
---|
287 | */ |
---|
288 | this.init = function(tm, target, config) { |
---|
289 | this.count = 0; |
---|
290 | this.tm = tm; |
---|
291 | this.target = target; |
---|
292 | this.opts = config || {}; |
---|
293 | }; |
---|
294 | |
---|
295 | /** |
---|
296 | * Increment the count of loaded datasets |
---|
297 | */ |
---|
298 | this.increment = function() { |
---|
299 | this.count++; |
---|
300 | if (this.count >= this.target) { |
---|
301 | this.complete(); |
---|
302 | } |
---|
303 | }; |
---|
304 | |
---|
305 | /** |
---|
306 | * Function to fire when all loads are complete |
---|
307 | */ |
---|
308 | this.complete = function() { |
---|
309 | // custom function including timeline scrolling and layout |
---|
310 | var func = this.opts.dataLoadedFunction; |
---|
311 | if (func) { |
---|
312 | func(tm); |
---|
313 | } else { |
---|
314 | var d = new Date(); |
---|
315 | var eventSource = this.tm.eventSource; |
---|
316 | var scrollTo = this.opts.scrollTo; |
---|
317 | // make sure there are events to scroll to |
---|
318 | if (scrollTo && eventSource.getCount() > 0) { |
---|
319 | switch (scrollTo) { |
---|
320 | case "now": |
---|
321 | break; |
---|
322 | case "earliest": |
---|
323 | d = eventSource.getEarliestDate(); |
---|
324 | break; |
---|
325 | case "latest": |
---|
326 | d = eventSource.getLatestDate(); |
---|
327 | break; |
---|
328 | default: |
---|
329 | // assume it's a date, try to parse |
---|
330 | if (typeof(scrollTo) == 'string') { |
---|
331 | scrollTo = TimeMapDataset.hybridParser(scrollTo); |
---|
332 | } |
---|
333 | // either the parse worked, or it was a date to begin with |
---|
334 | if (scrollTo.constructor == Date) d = scrollTo; |
---|
335 | } |
---|
336 | this.tm.timeline.getBand(0).setCenterVisibleDate(d); |
---|
337 | } |
---|
338 | this.tm.timeline.layout(); |
---|
339 | // custom function to be called when data is loaded |
---|
340 | func = this.opts.dataDisplayedFunction; |
---|
341 | if (func) { |
---|
342 | func(tm); |
---|
343 | } |
---|
344 | } |
---|
345 | }; |
---|
346 | }; |
---|
347 | |
---|
348 | /** |
---|
349 | * Map of different data loader functions. |
---|
350 | * New loaders should add their loader function to this map; loader |
---|
351 | * functions are passed an object with parameters in TimeMap.init(). |
---|
352 | */ |
---|
353 | TimeMap.loaders = {}; |
---|
354 | |
---|
355 | /** |
---|
356 | * Basic loader class, for pre-loaded data. |
---|
357 | * Other types of loaders should take the same parameter. |
---|
358 | * |
---|
359 | * @param {Object} options All options for the loader: |
---|
360 | * {Array} data Array of items to load |
---|
361 | * {Function} preloadFunction Function to call on data before loading |
---|
362 | * {Function} transformFunction Function to call on individual items before loading |
---|
363 | */ |
---|
364 | TimeMap.loaders.basic = function(options) { |
---|
365 | // get standard functions |
---|
366 | TimeMap.loaders.mixin(this, options); |
---|
367 | // allow "value" for backwards compatibility |
---|
368 | this.data = options.items || options.value || []; |
---|
369 | } |
---|
370 | |
---|
371 | /** |
---|
372 | * New loaders should implement a load function with the same parameters. |
---|
373 | * |
---|
374 | * @param {TimeMapDataset} dataset Dataset to load data into |
---|
375 | * @param {Function} callback Function to call once data is loaded |
---|
376 | */ |
---|
377 | TimeMap.loaders.basic.prototype.load = function(dataset, callback) { |
---|
378 | // preload |
---|
379 | var items = this.preload(this.data); |
---|
380 | // load |
---|
381 | dataset.loadItems(items, this.transform); |
---|
382 | // run callback |
---|
383 | callback(); |
---|
384 | } |
---|
385 | |
---|
386 | /** |
---|
387 | * Generic class for loading remote data with a custom parser function |
---|
388 | * |
---|
389 | * @param {Object} options All options for the loader: |
---|
390 | * {Array} url URL of file to load (NB: must be local address) |
---|
391 | * {Function} parserFunction Parser function to turn data into JavaScript array |
---|
392 | * {Function} preloadFunction Function to call on data before loading |
---|
393 | * {Function} transformFunction Function to call on individual items before loading |
---|
394 | */ |
---|
395 | TimeMap.loaders.remote = function(options) { |
---|
396 | // get standard functions |
---|
397 | TimeMap.loaders.mixin(this, options); |
---|
398 | // get URL to load |
---|
399 | this.url = options.url; |
---|
400 | } |
---|
401 | |
---|
402 | /** |
---|
403 | * KML load function. |
---|
404 | * |
---|
405 | * @param {TimeMapDataset} dataset Dataset to load data into |
---|
406 | * @param {Function} callback Function to call once data is loaded |
---|
407 | */ |
---|
408 | TimeMap.loaders.remote.prototype.load = function(dataset, callback) { |
---|
409 | var loader = this; |
---|
410 | // get items |
---|
411 | GDownloadUrl(this.url, function(result) { |
---|
412 | // parse |
---|
413 | var items = loader.parse(result); |
---|
414 | // load |
---|
415 | items = loader.preload(items); |
---|
416 | dataset.loadItems(items, loader.transform); |
---|
417 | // callback |
---|
418 | callback(); |
---|
419 | }); |
---|
420 | } |
---|
421 | |
---|
422 | /** |
---|
423 | * Save a few lines of code by adding standard functions |
---|
424 | * |
---|
425 | * @param {Function} loader Loader to add functions to |
---|
426 | * @param {Object} options Options for the loader: |
---|
427 | * {Function} parserFunction Parser function to turn data into JavaScript array |
---|
428 | * {Function} preloadFunction Function to call on data before loading |
---|
429 | * {Function} transformFunction Function to call on individual items before loading |
---|
430 | */ |
---|
431 | TimeMap.loaders.mixin = function(loader, options) { |
---|
432 | // set preload and transform functions |
---|
433 | var dummy = function(data) { return data; }; |
---|
434 | loader.parse = options.parserFunction || dummy; |
---|
435 | loader.preload = options.preloadFunction || dummy; |
---|
436 | loader.transform = options.transformFunction || dummy; |
---|
437 | } |
---|
438 | |
---|
439 | /** |
---|
440 | * Map of common timeline intervals. Add custom intervals here if you |
---|
441 | * want to refer to them by key rather than as literals. |
---|
442 | */ |
---|
443 | TimeMap.intervals = { |
---|
444 | 'sec': [DT.SECOND, DT.MINUTE], |
---|
445 | 'min': [DT.MINUTE, DT.HOUR], |
---|
446 | 'hr': [DT.HOUR, DT.DAY], |
---|
447 | 'day': [DT.DAY, DT.WEEK], |
---|
448 | 'wk': [DT.WEEK, DT.MONTH], |
---|
449 | 'mon': [DT.MONTH, DT.YEAR], |
---|
450 | 'yr': [DT.YEAR, DT.DECADE], |
---|
451 | 'dec': [DT.DECADE, DT.CENTURY] |
---|
452 | }; |
---|
453 | |
---|
454 | /** |
---|
455 | * Map of Google map types. Using keys rather than literals allows |
---|
456 | * for serialization of the map type. |
---|
457 | */ |
---|
458 | TimeMap.mapTypes = { |
---|
459 | 'normal':G_NORMAL_MAP, |
---|
460 | 'satellite':G_SATELLITE_MAP, |
---|
461 | 'hybrid':G_HYBRID_MAP, |
---|
462 | 'physical':G_PHYSICAL_MAP, |
---|
463 | 'moon':G_MOON_VISIBLE_MAP, |
---|
464 | 'sky':G_SKY_VISIBLE_MAP |
---|
465 | }; |
---|
466 | |
---|
467 | /** |
---|
468 | * Create an empty dataset object and add it to the timemap |
---|
469 | * |
---|
470 | * @param {String} id The id of the dataset |
---|
471 | * @param {Object} options A container for optional arguments for dataset constructor |
---|
472 | * @return {TimeMapDataset} The new dataset object |
---|
473 | */ |
---|
474 | TimeMap.prototype.createDataset = function(id, options) { |
---|
475 | options = options || {}; // make sure the options object isn't null |
---|
476 | if (!("title" in options)) { |
---|
477 | options.title = id; |
---|
478 | } |
---|
479 | var dataset = new TimeMapDataset(this, options); |
---|
480 | this.datasets[id] = dataset; |
---|
481 | // add event listener |
---|
482 | if (this.opts.centerOnItems) { |
---|
483 | var tm = this; |
---|
484 | GEvent.addListener(dataset, 'itemsloaded', function() { |
---|
485 | var map = tm.map, bounds = tm.mapBounds; |
---|
486 | // determine the zoom level from the bounds |
---|
487 | map.setZoom(map.getBoundsZoomLevel(bounds)); |
---|
488 | // determine the center from the bounds |
---|
489 | map.setCenter(bounds.getCenter()); |
---|
490 | }); |
---|
491 | } |
---|
492 | return dataset; |
---|
493 | }; |
---|
494 | |
---|
495 | /** |
---|
496 | * Run a function on each dataset in the timemap. This is the preferred |
---|
497 | * iteration method, as it allows for future iterator options. |
---|
498 | * |
---|
499 | * @param {Function} f The function to run |
---|
500 | */ |
---|
501 | TimeMap.prototype.each = function(f) { |
---|
502 | for (var id in this.datasets) { |
---|
503 | if (this.datasets.hasOwnProperty(id)) { |
---|
504 | f(this.datasets[id]); |
---|
505 | } |
---|
506 | } |
---|
507 | }; |
---|
508 | |
---|
509 | /** |
---|
510 | * Initialize the timeline - this must happen separately to allow full control of |
---|
511 | * timeline properties. |
---|
512 | * |
---|
513 | * @param {BandInfo Array} bands Array of band information objects for timeline |
---|
514 | */ |
---|
515 | TimeMap.prototype.initTimeline = function(bands) { |
---|
516 | |
---|
517 | // synchronize & highlight timeline bands |
---|
518 | for (var x=1; x < bands.length; x++) { |
---|
519 | if (this.opts.syncBands) { |
---|
520 | bands[x].syncWith = (x-1); |
---|
521 | } |
---|
522 | bands[x].highlight = true; |
---|
523 | } |
---|
524 | |
---|
525 | // initialize timeline |
---|
526 | this.timeline = Timeline.create(this.tElement, bands); |
---|
527 | |
---|
528 | // set event listeners |
---|
529 | var tm = this; |
---|
530 | // update map on timeline scroll |
---|
531 | this.timeline.getBand(0).addOnScrollListener(function() { |
---|
532 | tm.filter("map"); |
---|
533 | }); |
---|
534 | |
---|
535 | // hijack timeline popup window to open info window |
---|
536 | var painter = this.timeline.getBand(0).getEventPainter().constructor; |
---|
537 | painter.prototype._showBubble = function(x, y, evt) { |
---|
538 | evt.item.openInfoWindow(); |
---|
539 | }; |
---|
540 | |
---|
541 | // filter chain for map placemarks |
---|
542 | this.addFilterChain("map", |
---|
543 | function(item) { |
---|
544 | item.showPlacemark(); |
---|
545 | }, |
---|
546 | function(item) { |
---|
547 | item.hidePlacemark(); |
---|
548 | } |
---|
549 | ); |
---|
550 | |
---|
551 | // filter: hide when item is hidden |
---|
552 | this.addFilter("map", function(item) { |
---|
553 | return item.visible; |
---|
554 | }); |
---|
555 | // filter: hide when dataset is hidden |
---|
556 | this.addFilter("map", function(item) { |
---|
557 | return item.dataset.visible; |
---|
558 | }); |
---|
559 | |
---|
560 | // filter: hide map items depending on timeline state |
---|
561 | this.addFilter("map", this.opts.mapFilter); |
---|
562 | |
---|
563 | // filter chain for timeline events |
---|
564 | this.addFilterChain("timeline", |
---|
565 | function(item) { |
---|
566 | item.showEvent(); |
---|
567 | }, |
---|
568 | function(item) { |
---|
569 | item.hideEvent(); |
---|
570 | } |
---|
571 | ); |
---|
572 | |
---|
573 | // filter: hide when item is hidden |
---|
574 | this.addFilter("timeline", function(item) { |
---|
575 | return item.visible; |
---|
576 | }); |
---|
577 | // filter: hide when dataset is hidden |
---|
578 | this.addFilter("timeline", function(item) { |
---|
579 | return item.dataset.visible; |
---|
580 | }); |
---|
581 | |
---|
582 | // add callback for window resize |
---|
583 | var resizeTimerID = null; |
---|
584 | var oTimeline = this.timeline; |
---|
585 | window.onresize = function() { |
---|
586 | if (resizeTimerID === null) { |
---|
587 | resizeTimerID = window.setTimeout(function() { |
---|
588 | resizeTimerID = null; |
---|
589 | oTimeline.layout(); |
---|
590 | }, 500); |
---|
591 | } |
---|
592 | }; |
---|
593 | }; |
---|
594 | |
---|
595 | /** |
---|
596 | * Update items, hiding or showing according to filters |
---|
597 | * |
---|
598 | * @param {String} fid Filter chain to update on |
---|
599 | */ |
---|
600 | TimeMap.prototype.filter = function(fid) { |
---|
601 | var filters = this.filters[fid]; |
---|
602 | // if no filters exist, forget it |
---|
603 | if (!filters || !filters.chain || filters.chain.length === 0) { |
---|
604 | return; |
---|
605 | } |
---|
606 | // run items through filter |
---|
607 | this.each(function(ds) { |
---|
608 | ds.each(function(item) { |
---|
609 | F_LOOP: { |
---|
610 | for (var i = filters.chain.length - 1; i >= 0; i--) { |
---|
611 | if (!filters.chain[i](item)) { |
---|
612 | // false condition |
---|
613 | filters.off(item); |
---|
614 | break F_LOOP; |
---|
615 | } |
---|
616 | } |
---|
617 | // true condition |
---|
618 | filters.on(item); |
---|
619 | } |
---|
620 | }); |
---|
621 | }); |
---|
622 | }; |
---|
623 | |
---|
624 | /** |
---|
625 | * Add a new filter chain |
---|
626 | * |
---|
627 | * @param {String} fid Id of the filter chain |
---|
628 | * @param {Function} fon Function to run on an item if filter is true |
---|
629 | * @param {Function} foff Function to run on an item if filter is false |
---|
630 | */ |
---|
631 | TimeMap.prototype.addFilterChain = function(fid, fon, foff) { |
---|
632 | this.filters[fid] = { |
---|
633 | chain:[], |
---|
634 | on: fon, |
---|
635 | off: foff |
---|
636 | }; |
---|
637 | }; |
---|
638 | |
---|
639 | /** |
---|
640 | * Remove a filter chain |
---|
641 | * |
---|
642 | * @param {String} fid Id of the filter chain |
---|
643 | */ |
---|
644 | TimeMap.prototype.removeFilterChain = function(fid, on, off) { |
---|
645 | this.filters[fid] = null; |
---|
646 | }; |
---|
647 | |
---|
648 | /** |
---|
649 | * Add a function to a filter chain |
---|
650 | * |
---|
651 | * @param {String} fid Id of the filter chain |
---|
652 | * @param {Function} f Function to add |
---|
653 | */ |
---|
654 | TimeMap.prototype.addFilter = function(fid, f) { |
---|
655 | if (this.filters[fid] && this.filters[fid].chain) { |
---|
656 | this.filters[fid].chain.push(f); |
---|
657 | } |
---|
658 | }; |
---|
659 | |
---|
660 | /** |
---|
661 | * Remove a function from a filter chain |
---|
662 | * |
---|
663 | * @param {String} fid Id of the filter chain |
---|
664 | * XXX: Support index here |
---|
665 | */ |
---|
666 | TimeMap.prototype.removeFilter = function(fid) { |
---|
667 | if (this.filters[fid] && this.filters[fid].chain) { |
---|
668 | this.filters[fid].chain.pop(); |
---|
669 | } |
---|
670 | }; |
---|
671 | |
---|
672 | /** |
---|
673 | * Map of different filter functions. Adding new filters to this |
---|
674 | * map allows them to be specified by string name. |
---|
675 | */ |
---|
676 | TimeMap.filters = {}; |
---|
677 | |
---|
678 | /** |
---|
679 | * Static filter function: Hide items not shown on the timeline |
---|
680 | * |
---|
681 | * @param {TimeMapItem} item Item to test for filter |
---|
682 | * @return {Boolean} Whether to show the item |
---|
683 | */ |
---|
684 | TimeMap.filters.hidePastFuture = function(item) { |
---|
685 | var topband = item.dataset.timemap.timeline.getBand(0); |
---|
686 | var maxVisibleDate = topband.getMaxVisibleDate().getTime(); |
---|
687 | var minVisibleDate = topband.getMinVisibleDate().getTime(); |
---|
688 | if (item.event !== null) { |
---|
689 | var itemStart = item.event.getStart().getTime(); |
---|
690 | var itemEnd = item.event.getEnd().getTime(); |
---|
691 | // hide items in the future |
---|
692 | if (itemStart > maxVisibleDate) { |
---|
693 | return false; |
---|
694 | } |
---|
695 | // hide items in the past |
---|
696 | else if (itemEnd < minVisibleDate || |
---|
697 | (item.event.isInstant() && itemStart < minVisibleDate)) { |
---|
698 | return false; |
---|
699 | } |
---|
700 | } |
---|
701 | return true; |
---|
702 | }; |
---|
703 | |
---|
704 | /** |
---|
705 | * Static filter function: Hide items not shown on the timeline |
---|
706 | * |
---|
707 | * @param {TimeMapItem} item Item to test for filter |
---|
708 | * @return {Boolean} Whether to show the item |
---|
709 | */ |
---|
710 | TimeMap.filters.showMomentOnly = function(item) { |
---|
711 | var topband = item.dataset.timemap.timeline.getBand(0); |
---|
712 | var momentDate = topband.getCenterVisibleDate().getTime(); |
---|
713 | if (item.event !== null) { |
---|
714 | var itemStart = item.event.getStart().getTime(); |
---|
715 | var itemEnd = item.event.getEnd().getTime(); |
---|
716 | // hide items in the future |
---|
717 | if (itemStart > momentDate) { |
---|
718 | return false; |
---|
719 | } |
---|
720 | // hide items in the past |
---|
721 | else if (itemEnd < momentDate || |
---|
722 | (item.event.isInstant() && itemStart < momentDate)) { |
---|
723 | return false; |
---|
724 | } |
---|
725 | } |
---|
726 | return true; |
---|
727 | }; |
---|
728 | |
---|
729 | /*---------------------------------------------------------------------------- |
---|
730 | * TimeMapDataset Class - holds references to items and visual themes |
---|
731 | *---------------------------------------------------------------------------*/ |
---|
732 | |
---|
733 | /** |
---|
734 | * Create a new TimeMap dataset to hold a set of items |
---|
735 | * |
---|
736 | * @constructor |
---|
737 | * @param {TimeMap} timemap Reference to the timemap object |
---|
738 | * @param {Object} options Object holding optional arguments: |
---|
739 | * {String} title Title of the dataset (for the legend) |
---|
740 | * {String or theme object} theme Theme settings. |
---|
741 | * {String or Function} dateParser Function to replace default date parser. |
---|
742 | * {Function} openInfoWindow Function redefining how info window opens |
---|
743 | * {Function} closeInfoWindow Function redefining how info window closes |
---|
744 | */ |
---|
745 | function TimeMapDataset(timemap, options) { |
---|
746 | // hold reference to timemap |
---|
747 | this.timemap = timemap; |
---|
748 | // initialize timeline event source |
---|
749 | this.eventSource = new Timeline.DefaultEventSource(); |
---|
750 | // initialize array of items |
---|
751 | this.items = []; |
---|
752 | // for show/hide functions |
---|
753 | this.visible = true; |
---|
754 | |
---|
755 | // set defaults for options |
---|
756 | this.opts = options || {}; // make sure the options object isn't null |
---|
757 | this.opts.title = options.title || ""; |
---|
758 | |
---|
759 | // get theme by key or object |
---|
760 | if (typeof(options.theme) == "string") { |
---|
761 | options.theme = TimeMapDataset.themes[options.theme]; |
---|
762 | } |
---|
763 | this.opts.theme = options.theme || this.timemap.opts.theme || new TimeMapDatasetTheme({}); |
---|
764 | // allow icon path override in options or timemap options |
---|
765 | this.opts.theme.eventIconPath = options.eventIconPath || |
---|
766 | this.timemap.opts.eventIconPath || this.opts.theme.eventIconPath; |
---|
767 | this.opts.theme.eventIcon = options.eventIconPath + this.opts.theme.eventIconImage; |
---|
768 | |
---|
769 | // allow for other data parsers (e.g. Gregorgian) by key or function |
---|
770 | if (typeof(options.dateParser) == "string") { |
---|
771 | options.dateParser = TimeMapDataset.dateParsers[options.dateParser]; |
---|
772 | } |
---|
773 | this.opts.dateParser = options.dateParser || TimeMapDataset.hybridParser; |
---|
774 | |
---|
775 | // get functions |
---|
776 | this.getItems = function() { return this.items; }; |
---|
777 | this.getTitle = function() { return this.opts.title; }; |
---|
778 | } |
---|
779 | |
---|
780 | /** |
---|
781 | * Wrapper to fix Timeline Gregorian parser for invalid strings |
---|
782 | * |
---|
783 | * @param {String} s String to parse into a Date object |
---|
784 | * @return {Date} Parsed date or null |
---|
785 | */ |
---|
786 | TimeMapDataset.gregorianParser = function(s) { |
---|
787 | d = DT.parseGregorianDateTime(s); |
---|
788 | // check for invalid dates |
---|
789 | if (!d.getFullYear()) d = null; |
---|
790 | return d; |
---|
791 | }; |
---|
792 | |
---|
793 | /** |
---|
794 | * Parse dates with the ISO 8601 parser, then fall back on the Gregorian |
---|
795 | * parser if the first parse fails |
---|
796 | * |
---|
797 | * @param {String} s String to parse into a Date object |
---|
798 | * @return {Date} Parsed date or null |
---|
799 | */ |
---|
800 | TimeMapDataset.hybridParser = function(s) { |
---|
801 | var d = DT.parseIso8601DateTime(s); |
---|
802 | if (!d) { |
---|
803 | d = TimeMapDataset.gregorianParser(s); |
---|
804 | } |
---|
805 | return d; |
---|
806 | }; |
---|
807 | |
---|
808 | /** |
---|
809 | * Map of supported date parsers. Add custom date parsers here if you |
---|
810 | * want to refer to them by key rather than as a function name. |
---|
811 | */ |
---|
812 | TimeMapDataset.dateParsers = { |
---|
813 | 'hybrid': TimeMapDataset.hybridParser, |
---|
814 | 'iso8601': DT.parseIso8601DateTime, |
---|
815 | 'gregorian': TimeMapDataset.gregorianParser |
---|
816 | }; |
---|
817 | |
---|
818 | /** |
---|
819 | * Run a function on each item in the dataset. This is the preferred |
---|
820 | * iteration method, as it allows for future iterator options. |
---|
821 | * |
---|
822 | * @param {Function} f The function to run |
---|
823 | */ |
---|
824 | TimeMapDataset.prototype.each = function(f) { |
---|
825 | for (var x=0; x < this.items.length; x++) { |
---|
826 | f(this.items[x]); |
---|
827 | } |
---|
828 | }; |
---|
829 | |
---|
830 | /** |
---|
831 | * Add items to map and timeline. |
---|
832 | * Each item has both a timeline event and a map placemark. |
---|
833 | * |
---|
834 | * @param {Object} data Data to be loaded. See loadItem() below for the format. |
---|
835 | * @param {Function} transform If data is not in the above format, transformation function to make it so |
---|
836 | */ |
---|
837 | TimeMapDataset.prototype.loadItems = function(data, transform) { |
---|
838 | for (var x=0; x < data.length; x++) { |
---|
839 | this.loadItem(data[x], transform); |
---|
840 | } |
---|
841 | GEvent.trigger(this, 'itemsloaded'); |
---|
842 | }; |
---|
843 | |
---|
844 | /* |
---|
845 | * Add one item to map and timeline. |
---|
846 | * Each item has both a timeline event and a map placemark. |
---|
847 | * |
---|
848 | * @param {Object} data Data to be loaded, in the following format: |
---|
849 | * {String} title Title of the item (visible on timeline) |
---|
850 | * {DateTime} start Start time of the event on the timeline |
---|
851 | * {DateTime} end End time of the event on the timeline (duration events only) |
---|
852 | * {Object} point Data for a single-point placemark: |
---|
853 | * {Float} lat Latitude of map marker |
---|
854 | * {Float} lon Longitude of map marker |
---|
855 | * {Array of points} polyline Data for a polyline placemark, in format above |
---|
856 | * {Array of points} polygon Data for a polygon placemark, in format above |
---|
857 | * {Object} overlay Data for a ground overlay: |
---|
858 | * {String} image URL of image to overlay |
---|
859 | * {Float} north Northern latitude of the overlay |
---|
860 | * {Float} south Southern latitude of the overlay |
---|
861 | * {Float} east Eastern longitude of the overlay |
---|
862 | * {Float} west Western longitude of the overlay |
---|
863 | * {Object} options Optional arguments to be passed to the TimeMapItem (@see TimeMapItem) |
---|
864 | * @param {Function} transform If data is not in the above format, transformation function to make it so |
---|
865 | */ |
---|
866 | TimeMapDataset.prototype.loadItem = function(data, transform) { |
---|
867 | // apply transformation, if any |
---|
868 | if (transform !== undefined) { |
---|
869 | data = transform(data); |
---|
870 | } |
---|
871 | // transform functions can return a null value to skip a datum in the set |
---|
872 | if (data === null) { |
---|
873 | return; |
---|
874 | } |
---|
875 | |
---|
876 | // use item theme if provided, defaulting to dataset theme |
---|
877 | var options = data.options || {}; |
---|
878 | if (typeof(options.theme) == "string") { |
---|
879 | options.theme = TimeMapDataset.themes[options.theme]; |
---|
880 | } |
---|
881 | var theme = options.theme || this.opts.theme; |
---|
882 | theme.eventIconPath = options.eventIconPath || this.opts.theme.eventIconPath; |
---|
883 | theme.eventIcon = theme.eventIconPath + theme.eventIconImage; |
---|
884 | |
---|
885 | var tm = this.timemap; |
---|
886 | |
---|
887 | // create timeline event |
---|
888 | var parser = this.opts.dateParser, start = data.start, end = data.end, instant; |
---|
889 | start = (start === undefined||start === "") ? null : parser(start); |
---|
890 | end = (end === undefined||end === "") ? null : parser(end); |
---|
891 | instant = (end === undefined); |
---|
892 | var eventIcon = theme.eventIcon, |
---|
893 | title = data.title, |
---|
894 | // allow event-less placemarks - these will be always present on map |
---|
895 | event = null; |
---|
896 | if (start !== null) { |
---|
897 | var eventClass = Timeline.DefaultEventSource.Event; |
---|
898 | if (TimeMap.TimelineVersion() == "1.2") { |
---|
899 | // attributes by parameter |
---|
900 | event = new eventClass(start, end, null, null, |
---|
901 | instant, title, null, null, null, eventIcon, theme.eventColor, |
---|
902 | theme.eventTextColor); |
---|
903 | } else { |
---|
904 | var textColor = theme.eventTextColor; |
---|
905 | if (!textColor) { |
---|
906 | // tweak to show old-style events |
---|
907 | textColor = (theme.classicTape && !instant) ? '#FFFFFF' : '#000000'; |
---|
908 | } |
---|
909 | // attributes in object |
---|
910 | event = new eventClass({ |
---|
911 | "start": start, |
---|
912 | "end": end, |
---|
913 | "instant": instant, |
---|
914 | "text": title, |
---|
915 | "icon": eventIcon, |
---|
916 | "color": theme.eventColor, |
---|
917 | "textColor": textColor |
---|
918 | }); |
---|
919 | } |
---|
920 | } |
---|
921 | |
---|
922 | // set the icon, if any, outside the closure |
---|
923 | var markerIcon = ("icon" in data) ? data.icon : theme.icon, |
---|
924 | bounds = tm.mapBounds; // save some bytes |
---|
925 | |
---|
926 | |
---|
927 | // internal function: create map placemark |
---|
928 | // takes a data object (could be full data, could be just placemark) |
---|
929 | // returns an object with {placemark, type, point} |
---|
930 | var createPlacemark = function(pdata) { |
---|
931 | var placemark = null, type = "", point = null; |
---|
932 | // point placemark |
---|
933 | if ("point" in pdata) { |
---|
934 | point = new GLatLng( |
---|
935 | parseFloat(pdata.point.lat), |
---|
936 | parseFloat(pdata.point.lon) |
---|
937 | ); |
---|
938 | // add point to visible map bounds |
---|
939 | if (tm.opts.centerOnItems) { |
---|
940 | bounds.extend(point); |
---|
941 | } |
---|
942 | placemark = new GMarker(point, { icon: markerIcon }); |
---|
943 | type = "marker"; |
---|
944 | point = placemark.getLatLng(); |
---|
945 | } |
---|
946 | // polyline and polygon placemarks |
---|
947 | else if ("polyline" in pdata || "polygon" in pdata) { |
---|
948 | var points = [], line; |
---|
949 | if ("polyline" in pdata) { |
---|
950 | line = pdata.polyline; |
---|
951 | } else { |
---|
952 | line = pdata.polygon; |
---|
953 | } |
---|
954 | for (var x=0; x<line.length; x++) { |
---|
955 | point = new GLatLng( |
---|
956 | parseFloat(line[x].lat), |
---|
957 | parseFloat(line[x].lon) |
---|
958 | ); |
---|
959 | points.push(point); |
---|
960 | // add point to visible map bounds |
---|
961 | if (tm.opts.centerOnItems) { |
---|
962 | bounds.extend(point); |
---|
963 | } |
---|
964 | } |
---|
965 | if ("polyline" in pdata) { |
---|
966 | placemark = new GPolyline(points, |
---|
967 | theme.lineColor, |
---|
968 | theme.lineWeight, |
---|
969 | theme.lineOpacity); |
---|
970 | type = "polyline"; |
---|
971 | point = placemark.getVertex(Math.floor(placemark.getVertexCount()/2)); |
---|
972 | } else { |
---|
973 | placemark = new GPolygon(points, |
---|
974 | theme.polygonLineColor, |
---|
975 | theme.polygonLineWeight, |
---|
976 | theme.polygonLineOpacity, |
---|
977 | theme.fillColor, |
---|
978 | theme.fillOpacity); |
---|
979 | type = "polygon"; |
---|
980 | point = placemark.getBounds().getCenter(); |
---|
981 | } |
---|
982 | } |
---|
983 | // ground overlay placemark |
---|
984 | else if ("overlay" in pdata) { |
---|
985 | var sw = new GLatLng( |
---|
986 | parseFloat(pdata.overlay.south), |
---|
987 | parseFloat(pdata.overlay.west) |
---|
988 | ); |
---|
989 | var ne = new GLatLng( |
---|
990 | parseFloat(pdata.overlay.north), |
---|
991 | parseFloat(pdata.overlay.east) |
---|
992 | ); |
---|
993 | // add to visible bounds |
---|
994 | if (tm.opts.centerOnItems) { |
---|
995 | bounds.extend(sw); |
---|
996 | bounds.extend(ne); |
---|
997 | } |
---|
998 | // create overlay |
---|
999 | var overlayBounds = new GLatLngBounds(sw, ne); |
---|
1000 | placemark = new GGroundOverlay(pdata.overlay.image, overlayBounds); |
---|
1001 | type = "overlay"; |
---|
1002 | point = overlayBounds.getCenter(); |
---|
1003 | } |
---|
1004 | return { |
---|
1005 | "placemark": placemark, |
---|
1006 | "type": type, |
---|
1007 | "point": point |
---|
1008 | }; |
---|
1009 | }; |
---|
1010 | |
---|
1011 | // create placemark or placemarks |
---|
1012 | var placemark = [], pdataArr = [], pdata = null, type = "", point = null, i; |
---|
1013 | // array of placemark objects |
---|
1014 | if ("placemarks" in data) { |
---|
1015 | pdataArr = data.placemarks; |
---|
1016 | } else { |
---|
1017 | // we have one or more single placemarks |
---|
1018 | var types = ["point", "polyline", "polygon", "overlay"]; |
---|
1019 | for (i=0; i<types.length; i++) { |
---|
1020 | if (types[i] in data) { |
---|
1021 | pdata = {}; |
---|
1022 | pdata[types[i]] = data[types[i]]; |
---|
1023 | pdataArr.push(pdata); |
---|
1024 | } |
---|
1025 | } |
---|
1026 | } |
---|
1027 | if (pdataArr) { |
---|
1028 | for (i=0; i<pdataArr.length; i++) { |
---|
1029 | // create the placemark |
---|
1030 | var p = createPlacemark(pdataArr[i]); |
---|
1031 | // take the first point and type as a default |
---|
1032 | point = point || p.point; |
---|
1033 | type = type || p.type; |
---|
1034 | placemark.push(p.placemark); |
---|
1035 | } |
---|
1036 | } |
---|
1037 | // override type for arrays |
---|
1038 | if (placemark.length > 1) { |
---|
1039 | type = "array"; |
---|
1040 | } |
---|
1041 | |
---|
1042 | options.title = title; |
---|
1043 | options.type = type || "none"; |
---|
1044 | options.theme = theme; |
---|
1045 | // check for custom infoPoint and convert to GLatLng |
---|
1046 | if (options.infoPoint) { |
---|
1047 | options.infoPoint = new GLatLng( |
---|
1048 | parseFloat(options.infoPoint.lat), |
---|
1049 | parseFloat(options.infoPoint.lon) |
---|
1050 | ); |
---|
1051 | } else { |
---|
1052 | options.infoPoint = point; |
---|
1053 | } |
---|
1054 | |
---|
1055 | // create item and cross-references |
---|
1056 | var item = new TimeMapItem(placemark, event, this, options); |
---|
1057 | // add event if it exists |
---|
1058 | if (event !== null) { |
---|
1059 | event.item = item; |
---|
1060 | this.eventSource.add(event); |
---|
1061 | } |
---|
1062 | // add placemark(s) if any exist |
---|
1063 | if (placemark.length > 0) { |
---|
1064 | for (i=0; i<placemark.length; i++) { |
---|
1065 | placemark[i].item = item; |
---|
1066 | // add listener to make placemark open when event is clicked |
---|
1067 | GEvent.addListener(placemark[i], "click", function() { |
---|
1068 | item.openInfoWindow(); |
---|
1069 | }); |
---|
1070 | // add placemark and event to map and timeline |
---|
1071 | tm.map.addOverlay(placemark[i]); |
---|
1072 | // hide placemarks until the next refresh |
---|
1073 | placemark[i].hide(); |
---|
1074 | } |
---|
1075 | } |
---|
1076 | // add the item to the dataset |
---|
1077 | this.items.push(item); |
---|
1078 | // return the item object |
---|
1079 | return item; |
---|
1080 | }; |
---|
1081 | |
---|
1082 | /*---------------------------------------------------------------------------- |
---|
1083 | * Predefined visual themes for datasets, based on Google markers |
---|
1084 | *---------------------------------------------------------------------------*/ |
---|
1085 | |
---|
1086 | /** |
---|
1087 | * Create a new theme for a TimeMap dataset, defining colors and images |
---|
1088 | * |
---|
1089 | * @constructor |
---|
1090 | * @param {Object} options A container for optional arguments: |
---|
1091 | * {GIcon} icon Icon for marker placemarks |
---|
1092 | * {String} color Default color in hex for events, polylines, polygons |
---|
1093 | * {String} lineColor Color for polylines, defaults to options.color |
---|
1094 | * {String} polygonLineColor Color for polygon outlines, defaults to lineColor |
---|
1095 | * {Number} lineOpacity Opacity for polylines |
---|
1096 | * {Number} polgonLineOpacity Opacity for polygon outlines, defaults to options.lineOpacity |
---|
1097 | * {Number} lineWeight Line weight in pixels for polylines |
---|
1098 | * {Number} polygonLineWeight Line weight for polygon outlines, defaults to options.lineWeight |
---|
1099 | * {String} fillColor Color for polygon fill, defaults to options.color |
---|
1100 | * {String} fillOpacity Opacity for polygon fill |
---|
1101 | * {String} eventColor Background color for duration events |
---|
1102 | * {URL} eventIcon Icon URL for instant events |
---|
1103 | */ |
---|
1104 | function TimeMapDatasetTheme(options) { |
---|
1105 | // work out various defaults - the default theme is Google's reddish color |
---|
1106 | options = options || {}; |
---|
1107 | |
---|
1108 | if (!options.icon) { |
---|
1109 | // make new red icon |
---|
1110 | var markerIcon = new GIcon(G_DEFAULT_ICON); |
---|
1111 | this.iconImage = options.iconImage || GIP + "red-dot.png"; |
---|
1112 | markerIcon.image = this.iconImage; |
---|
1113 | markerIcon.iconSize = new GSize(32, 32); |
---|
1114 | markerIcon.shadow = GIP + "msmarker.shadow.png"; |
---|
1115 | markerIcon.shadowSize = new GSize(59, 32); |
---|
1116 | markerIcon.iconAnchor = new GPoint(16, 33); |
---|
1117 | markerIcon.infoWindowAnchor = new GPoint(18, 3); |
---|
1118 | } |
---|
1119 | |
---|
1120 | this.icon = options.icon || markerIcon; |
---|
1121 | this.color = options.color || "#FE766A"; |
---|
1122 | this.lineColor = options.lineColor || this.color; |
---|
1123 | this.polygonLineColor = options.polygonLineColor || this.lineColor; |
---|
1124 | this.lineOpacity = options.lineOpacity || 1; |
---|
1125 | this.polgonLineOpacity = options.polgonLineOpacity || this.lineOpacity; |
---|
1126 | this.lineWeight = options.lineWeight || 2; |
---|
1127 | this.polygonLineWeight = options.polygonLineWeight || this.lineWeight; |
---|
1128 | this.fillColor = options.fillColor || this.color; |
---|
1129 | this.fillOpacity = options.fillOpacity || 0.25; |
---|
1130 | this.eventColor = options.eventColor || this.color; |
---|
1131 | this.eventTextColor = options.eventTextColor || null; |
---|
1132 | this.eventIconPath = options.eventIconPath || "timemap/images/"; |
---|
1133 | this.eventIconImage = options.eventIconImage || "red-circle.png"; |
---|
1134 | this.eventIcon = options.eventIcon || this.eventIconPath + this.eventIconImage; |
---|
1135 | |
---|
1136 | // whether to use the older "tape" event style for the newer Timeline versions |
---|
1137 | // NB: this needs additional css to work - see examples/artists.html |
---|
1138 | this.classicTape = ("classicTape" in options) ? options.classicTape : false; |
---|
1139 | } |
---|
1140 | |
---|
1141 | TimeMapDataset.redTheme = function(options) { |
---|
1142 | return new TimeMapDatasetTheme(options); |
---|
1143 | }; |
---|
1144 | |
---|
1145 | TimeMapDataset.blueTheme = function(options) { |
---|
1146 | options = options || {}; |
---|
1147 | options.iconImage = GIP + "blue-dot.png"; |
---|
1148 | options.color = "#5A7ACF"; |
---|
1149 | options.eventIconImage = "blue-circle.png"; |
---|
1150 | return new TimeMapDatasetTheme(options); |
---|
1151 | }; |
---|
1152 | |
---|
1153 | TimeMapDataset.greenTheme = function(options) { |
---|
1154 | options = options || {}; |
---|
1155 | options.iconImage = GIP + "green-dot.png"; |
---|
1156 | options.color = "#19CF54"; |
---|
1157 | options.eventIconImage = "green-circle.png"; |
---|
1158 | return new TimeMapDatasetTheme(options); |
---|
1159 | }; |
---|
1160 | |
---|
1161 | TimeMapDataset.ltblueTheme = function(options) { |
---|
1162 | options = options || {}; |
---|
1163 | options.iconImage = GIP + "ltblue-dot.png"; |
---|
1164 | options.color = "#5ACFCF"; |
---|
1165 | options.eventIconImage = "ltblue-circle.png"; |
---|
1166 | return new TimeMapDatasetTheme(options); |
---|
1167 | }; |
---|
1168 | |
---|
1169 | TimeMapDataset.purpleTheme = function(options) { |
---|
1170 | options = options || {}; |
---|
1171 | options.iconImage = GIP + "purple-dot.png"; |
---|
1172 | options.color = "#8E67FD"; |
---|
1173 | options.eventIconImage = "purple-circle.png"; |
---|
1174 | return new TimeMapDatasetTheme(options); |
---|
1175 | }; |
---|
1176 | |
---|
1177 | TimeMapDataset.orangeTheme = function(options) { |
---|
1178 | options = options || {}; |
---|
1179 | options.iconImage = GIP + "orange-dot.png"; |
---|
1180 | options.color = "#FF9900"; |
---|
1181 | options.eventIconImage = "orange-circle.png"; |
---|
1182 | return new TimeMapDatasetTheme(options); |
---|
1183 | }; |
---|
1184 | |
---|
1185 | TimeMapDataset.yellowTheme = function(options) { |
---|
1186 | options = options || {}; |
---|
1187 | options.iconImage = GIP + "yellow-dot.png"; |
---|
1188 | options.color = "#ECE64A"; |
---|
1189 | options.eventIconImage = "yellow-circle.png"; |
---|
1190 | return new TimeMapDatasetTheme(options); |
---|
1191 | }; |
---|
1192 | |
---|
1193 | /** |
---|
1194 | * Map of themes. Add custom themes to this map if you want |
---|
1195 | * to load them by key rather than as an object. |
---|
1196 | */ |
---|
1197 | TimeMapDataset.themes = { |
---|
1198 | 'red': TimeMapDataset.redTheme(), |
---|
1199 | 'blue': TimeMapDataset.blueTheme(), |
---|
1200 | 'green': TimeMapDataset.greenTheme(), |
---|
1201 | 'ltblue': TimeMapDataset.ltblueTheme(), |
---|
1202 | 'orange': TimeMapDataset.orangeTheme(), |
---|
1203 | 'yellow': TimeMapDataset.yellowTheme(), |
---|
1204 | 'purple': TimeMapDataset.purpleTheme() |
---|
1205 | }; |
---|
1206 | |
---|
1207 | |
---|
1208 | /*---------------------------------------------------------------------------- |
---|
1209 | * TimeMapItem Class - holds references to map placemark and timeline event |
---|
1210 | *---------------------------------------------------------------------------*/ |
---|
1211 | |
---|
1212 | /** |
---|
1213 | * Create a new TimeMap item with a map placemark and a timeline event |
---|
1214 | * |
---|
1215 | * @constructor |
---|
1216 | * @param {placemark} placemark Placemark or array of placemarks (GMarker, GPolyline, etc) |
---|
1217 | * @param {Event} event The timeline event |
---|
1218 | * @param {TimeMapDataset} dataset Reference to the parent dataset object |
---|
1219 | * @param {Object} options A container for optional arguments: |
---|
1220 | * {String} title Title of the item |
---|
1221 | * {String} description Plain-text description of the item |
---|
1222 | * {String} type Type of map placemark used (marker. polyline, polygon) |
---|
1223 | * {GLatLng} infoPoint Point indicating the center of this item |
---|
1224 | * {String} infoHtml Full HTML for the info window |
---|
1225 | * {String} infoUrl URL from which to retrieve full HTML for the info window |
---|
1226 | * {Function} openInfoWindow Function redefining how info window opens |
---|
1227 | * {Function} closeInfoWindow Function redefining how info window closes |
---|
1228 | */ |
---|
1229 | function TimeMapItem(placemark, event, dataset, options) { |
---|
1230 | // initialize vars |
---|
1231 | this.event = event; |
---|
1232 | this.dataset = dataset; |
---|
1233 | this.map = dataset.timemap.map; |
---|
1234 | |
---|
1235 | // initialize placemark(s) with some type juggling |
---|
1236 | if (placemark && TimeMap.isArray(placemark) && placemark.length === 0) { |
---|
1237 | placemark = null; |
---|
1238 | } |
---|
1239 | if (placemark && placemark.length == 1) { |
---|
1240 | placemark = placemark[0]; |
---|
1241 | } |
---|
1242 | this.placemark = placemark; |
---|
1243 | |
---|
1244 | // set defaults for options |
---|
1245 | this.opts = options || {}; |
---|
1246 | this.opts.type = options.type || ''; |
---|
1247 | this.opts.title = options.title || ''; |
---|
1248 | this.opts.description = options.description || ''; |
---|
1249 | this.opts.infoPoint = options.infoPoint || null; |
---|
1250 | this.opts.infoHtml = options.infoHtml || ''; |
---|
1251 | this.opts.infoUrl = options.infoUrl || ''; |
---|
1252 | |
---|
1253 | // get functions |
---|
1254 | this.getType = function() { return this.opts.type; }; |
---|
1255 | this.getTitle = function() { return this.opts.title; }; |
---|
1256 | this.getInfoPoint = function() { |
---|
1257 | // default to map center if placemark not set |
---|
1258 | return this.opts.infoPoint || this.map.getCenter(); |
---|
1259 | }; |
---|
1260 | |
---|
1261 | // items initialize visible |
---|
1262 | this.visible = true; |
---|
1263 | // placemarks initialize hidden |
---|
1264 | this.placemarkVisible = false; |
---|
1265 | // events initialize visible |
---|
1266 | this.eventVisible = true; |
---|
1267 | |
---|
1268 | // allow for custom open/close functions, set at item, dataset, or timemap level |
---|
1269 | this.openInfoWindow = options.openInfoWindow || |
---|
1270 | dataset.opts.openInfoWindow || |
---|
1271 | dataset.timemap.opts.openInfoWindow || |
---|
1272 | false; |
---|
1273 | if (!this.openInfoWindow) { |
---|
1274 | if (this.opts.infoUrl !== "") { |
---|
1275 | // load via AJAX if URL is provided |
---|
1276 | this.openInfoWindow = TimeMapItem.openInfoWindowAjax; |
---|
1277 | } else { |
---|
1278 | // otherwise default to basic window |
---|
1279 | this.openInfoWindow = TimeMapItem.openInfoWindowBasic; |
---|
1280 | } |
---|
1281 | } |
---|
1282 | this.closeInfoWindow = options.closeInfoWindow || |
---|
1283 | dataset.opts.closeInfoWindow || |
---|
1284 | dataset.timemap.opts.closeInfoWindow || |
---|
1285 | TimeMapItem.closeInfoWindowBasic; |
---|
1286 | } |
---|
1287 | |
---|
1288 | /** |
---|
1289 | * Show the map placemark |
---|
1290 | */ |
---|
1291 | TimeMapItem.prototype.showPlacemark = function() { |
---|
1292 | if (this.placemark) { |
---|
1293 | if (this.getType() == "array") { |
---|
1294 | for (var i=0; i<this.placemark.length; i++) { |
---|
1295 | this.placemark[i].show(); |
---|
1296 | } |
---|
1297 | } else { |
---|
1298 | this.placemark.show(); |
---|
1299 | } |
---|
1300 | this.placemarkVisible = true; |
---|
1301 | } |
---|
1302 | }; |
---|
1303 | |
---|
1304 | /** |
---|
1305 | * Hide the map placemark |
---|
1306 | */ |
---|
1307 | TimeMapItem.prototype.hidePlacemark = function() { |
---|
1308 | if (this.placemark) { |
---|
1309 | if (this.getType() == "array") { |
---|
1310 | for (var i=0; i<this.placemark.length; i++) { |
---|
1311 | this.placemark[i].hide(); |
---|
1312 | } |
---|
1313 | } else { |
---|
1314 | this.placemark.hide(); |
---|
1315 | } |
---|
1316 | this.placemarkVisible = false; |
---|
1317 | } |
---|
1318 | this.closeInfoWindow(); |
---|
1319 | }; |
---|
1320 | |
---|
1321 | /** |
---|
1322 | * Show the timeline event |
---|
1323 | * NB: Will likely require calling timeline.layout() |
---|
1324 | */ |
---|
1325 | TimeMapItem.prototype.showEvent = function() { |
---|
1326 | if (this.event) { |
---|
1327 | if (this.eventVisible === false){ |
---|
1328 | this.dataset.timemap.timeline.getBand(0) |
---|
1329 | .getEventSource()._events._events.add(this.event); |
---|
1330 | } |
---|
1331 | this.eventVisible = true; |
---|
1332 | } |
---|
1333 | }; |
---|
1334 | |
---|
1335 | /** |
---|
1336 | * Show the timeline event |
---|
1337 | * NB: Will likely require calling timeline.layout() |
---|
1338 | */ |
---|
1339 | TimeMapItem.prototype.hideEvent = function() { |
---|
1340 | if (this.event) { |
---|
1341 | if (this.eventVisible == true){ |
---|
1342 | this.dataset.timemap.timeline.getBand(0) |
---|
1343 | .getEventSource()._events._events.remove(this.event); |
---|
1344 | } |
---|
1345 | this.eventVisible = false; |
---|
1346 | } |
---|
1347 | }; |
---|
1348 | |
---|
1349 | /** |
---|
1350 | * Standard open info window function, using static text in map window |
---|
1351 | */ |
---|
1352 | TimeMapItem.openInfoWindowBasic = function() { |
---|
1353 | var html = this.opts.infoHtml; |
---|
1354 | // create content for info window if none is provided |
---|
1355 | if (html === "") { |
---|
1356 | html = '<div class="infotitle">' + this.opts.title + '</div>'; |
---|
1357 | if (this.opts.description !== "") { |
---|
1358 | html += '<div class="infodescription">' + this.opts.description + '</div>'; |
---|
1359 | } |
---|
1360 | } |
---|
1361 | // scroll timeline if necessary |
---|
1362 | if (this.placemark && !this.visible && this.event) { |
---|
1363 | var topband = this.dataset.timemap.timeline.getBand(0); |
---|
1364 | topband.setCenterVisibleDate(this.event.getStart()); |
---|
1365 | } |
---|
1366 | // open window |
---|
1367 | if (this.getType() == "marker") { |
---|
1368 | this.placemark.openInfoWindowHtml(html); |
---|
1369 | } else { |
---|
1370 | this.map.openInfoWindowHtml(this.getInfoPoint(), html); |
---|
1371 | } |
---|
1372 | // custom functions will need to set this as well |
---|
1373 | this.selected = true; |
---|
1374 | }; |
---|
1375 | |
---|
1376 | /** |
---|
1377 | * Open info window function using ajax-loaded text in map window |
---|
1378 | */ |
---|
1379 | TimeMapItem.openInfoWindowAjax = function() { |
---|
1380 | if (this.opts.infoHtml !== "") { // already loaded - change to static |
---|
1381 | this.openInfoWindow = TimeMapItem.openInfoWindowBasic; |
---|
1382 | this.openInfoWindow(); |
---|
1383 | } else { // load content via AJAX |
---|
1384 | if (this.opts.infoUrl !== "") { |
---|
1385 | var item = this; |
---|
1386 | GDownloadUrl(this.opts.infoUrl, function(result) { |
---|
1387 | item.opts.infoHtml = result; |
---|
1388 | item.openInfoWindow(); |
---|
1389 | }); |
---|
1390 | } else { // fall back on basic function |
---|
1391 | this.openInfoWindow = TimeMapItem.openInfoWindowBasic; |
---|
1392 | this.openInfoWindow(); |
---|
1393 | } |
---|
1394 | } |
---|
1395 | }; |
---|
1396 | |
---|
1397 | /** |
---|
1398 | * Standard close window function, using the map window |
---|
1399 | */ |
---|
1400 | TimeMapItem.closeInfoWindowBasic = function() { |
---|
1401 | if (this.getType() == "marker") { |
---|
1402 | this.placemark.closeInfoWindow(); |
---|
1403 | } else { |
---|
1404 | var infoWindow = this.map.getInfoWindow(); |
---|
1405 | // close info window if its point is the same as this item's point |
---|
1406 | if (infoWindow.getPoint() == this.getInfoPoint() && !infoWindow.isHidden()) { |
---|
1407 | this.map.closeInfoWindow(); |
---|
1408 | } |
---|
1409 | } |
---|
1410 | // custom functions will need to set this as well |
---|
1411 | this.selected = false; |
---|
1412 | }; |
---|
1413 | |
---|
1414 | /*---------------------------------------------------------------------------- |
---|
1415 | * Utility functions, attached to TimeMap to avoid namespace issues |
---|
1416 | *---------------------------------------------------------------------------*/ |
---|
1417 | |
---|
1418 | /** |
---|
1419 | * Convenience trim function |
---|
1420 | * |
---|
1421 | * @param {String} str String to trim |
---|
1422 | * @return {String} Trimmed string |
---|
1423 | */ |
---|
1424 | TimeMap.trim = function(str) { |
---|
1425 | str = str && String(str) || ''; |
---|
1426 | return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); |
---|
1427 | }; |
---|
1428 | |
---|
1429 | /** |
---|
1430 | * Convenience array tester |
---|
1431 | * |
---|
1432 | * @param {Object} o Object to test |
---|
1433 | * @return {Boolean} Whether the object is an array |
---|
1434 | */ |
---|
1435 | TimeMap.isArray = function(o) { |
---|
1436 | return o && !(o.propertyIsEnumerable('length')) && |
---|
1437 | typeof o === 'object' && typeof o.length === 'number'; |
---|
1438 | }; |
---|
1439 | |
---|
1440 | /** |
---|
1441 | * Get XML tag value as a string |
---|
1442 | * |
---|
1443 | * @param {XML Node} n Node in which to look for tag |
---|
1444 | * @param {String} tag Name of tag to look for |
---|
1445 | * @param {String} ns Optional namespace |
---|
1446 | * @return {String} Tag value as string |
---|
1447 | */ |
---|
1448 | TimeMap.getTagValue = function(n, tag, ns) { |
---|
1449 | var str = ""; |
---|
1450 | var nList = TimeMap.getNodeList(n, tag, ns); |
---|
1451 | if (nList.length > 0) { |
---|
1452 | n = nList[0].firstChild; |
---|
1453 | // fix for extra-long nodes |
---|
1454 | // see http://code.google.com/p/timemap/issues/detail?id=36 |
---|
1455 | while(n !== null) { |
---|
1456 | str += n.nodeValue; |
---|
1457 | n = n.nextSibling; |
---|
1458 | } |
---|
1459 | } |
---|
1460 | return str; |
---|
1461 | }; |
---|
1462 | |
---|
1463 | /** |
---|
1464 | * Empty container for mapping XML namespaces to URLs |
---|
1465 | */ |
---|
1466 | TimeMap.nsMap = {}; |
---|
1467 | |
---|
1468 | /** |
---|
1469 | * Cross-browser implementation of getElementsByTagNameNS |
---|
1470 | * Note: Expects any applicable namespaces to be mapped in |
---|
1471 | * TimeMap.nsMap. XXX: There may be better ways to do this. |
---|
1472 | * |
---|
1473 | * @param {XML Node} n Node in which to look for tag |
---|
1474 | * @param {String} tag Name of tag to look for |
---|
1475 | * @param {String} ns Optional namespace |
---|
1476 | * @return {XML Node List} List of nodes with the specified tag name |
---|
1477 | */ |
---|
1478 | TimeMap.getNodeList = function(n, tag, ns) { |
---|
1479 | if (ns === undefined) { |
---|
1480 | // no namespace |
---|
1481 | return n.getElementsByTagName(tag); |
---|
1482 | } |
---|
1483 | if (n.getElementsByTagNameNS && TimeMap.nsMap[ns]) { |
---|
1484 | // function and namespace both exist |
---|
1485 | return n.getElementsByTagNameNS(TimeMap.nsMap[ns], tag); |
---|
1486 | } |
---|
1487 | // no function, try the colon tag name |
---|
1488 | return n.getElementsByTagName(ns + ':' + tag); |
---|
1489 | }; |
---|
1490 | |
---|
1491 | /** |
---|
1492 | * Make TimeMap.init()-style points from a GLatLng, array, or string |
---|
1493 | * |
---|
1494 | * @param {Object} coords GLatLng, array, or string to convert |
---|
1495 | * @param {Boolean} reversed Whether the points are KML-style lon/lat, rather than lat/lon |
---|
1496 | * @return {Object} TimeMap.init()-style point |
---|
1497 | */ |
---|
1498 | TimeMap.makePoint = function(coords, reversed) { |
---|
1499 | var latlon = null; |
---|
1500 | // GLatLng |
---|
1501 | if (coords.lat && coords.lng) { |
---|
1502 | latlon = [coords.lat(), coords.lng()]; |
---|
1503 | } |
---|
1504 | // array of coordinates |
---|
1505 | if (TimeMap.isArray(coords)) { |
---|
1506 | latlon = coords; |
---|
1507 | } |
---|
1508 | // string |
---|
1509 | if (latlon === null) { |
---|
1510 | // trim extra whitespace |
---|
1511 | coords = TimeMap.trim(coords); |
---|
1512 | if (coords.indexOf(',') > -1) { |
---|
1513 | // split on commas |
---|
1514 | latlon = coords.split(","); |
---|
1515 | } else { |
---|
1516 | // split on whitespace |
---|
1517 | latlon = coords.split(/[\r\n\f ]+/); |
---|
1518 | } |
---|
1519 | } |
---|
1520 | if (reversed) latlon.reverse(); |
---|
1521 | return { |
---|
1522 | "lat": TimeMap.trim(latlon[0]), |
---|
1523 | "lon": TimeMap.trim(latlon[1]) |
---|
1524 | }; |
---|
1525 | }; |
---|
1526 | |
---|
1527 | /** |
---|
1528 | * Make TimeMap.init()-style polyline/polygons from a whitespace-delimited |
---|
1529 | * string of coordinates (such as those in GeoRSS and KML) |
---|
1530 | * XXX: Any reason for this to take arrays of GLatLngs as well? |
---|
1531 | * |
---|
1532 | * @param {Object} coords String to convert |
---|
1533 | * @param {Boolean} reversed Whether the points are KML-style lon/lat, rather than lat/lon |
---|
1534 | * @return {Object} Formated coordinate array |
---|
1535 | */ |
---|
1536 | TimeMap.makePoly = function(coords, reversed) { |
---|
1537 | var poly = [], latlon; |
---|
1538 | var coordArr = TimeMap.trim(coords).split(/[\r\n\f ]+/); |
---|
1539 | if (coordArr.length == 0) return []; |
---|
1540 | // loop through coordinates |
---|
1541 | for (var x=0; x<coordArr.length; x++) { |
---|
1542 | latlon = (coordArr[x].indexOf(',')) ? |
---|
1543 | // comma-separated coordinates (KML-style lon/lat) |
---|
1544 | latlon = coordArr[x].split(",") : |
---|
1545 | // space-separated coordinates - increment to step by 2s |
---|
1546 | latlon = [coordArr[x], coordArr[++x]]; |
---|
1547 | if (reversed) latlon.reverse(); |
---|
1548 | poly.push({ |
---|
1549 | "lat": latlon[0], |
---|
1550 | "lon": latlon[1] |
---|
1551 | }); |
---|
1552 | } |
---|
1553 | return poly; |
---|
1554 | } |
---|
1555 | |
---|
1556 | /** |
---|
1557 | * Format a date as an ISO 8601 string |
---|
1558 | * |
---|
1559 | * @param {Date} d Date to format |
---|
1560 | * @param {int} precision Optional precision indicator: |
---|
1561 | * 3 (default): Show full date and time |
---|
1562 | * 2: Show full date and time, omitting seconds |
---|
1563 | * 1: Show date only |
---|
1564 | * @return {String} Formatted string |
---|
1565 | */ |
---|
1566 | TimeMap.formatDate = function(d, precision) { |
---|
1567 | // default to high precision |
---|
1568 | precision = precision || 3; |
---|
1569 | var str = ""; |
---|
1570 | if (d) { |
---|
1571 | // check for date.js support |
---|
1572 | if (d.toISOString) { |
---|
1573 | return d.toISOString(); |
---|
1574 | } |
---|
1575 | // otherwise, build ISO 8601 string |
---|
1576 | var pad = function(num) { |
---|
1577 | return ((num < 10) ? "0" : "") + num; |
---|
1578 | }; |
---|
1579 | var yyyy = d.getUTCFullYear(), |
---|
1580 | mo = d.getUTCMonth(), |
---|
1581 | dd = d.getUTCDate(); |
---|
1582 | str += yyyy + '-' + pad(mo + 1 ) + '-' + pad(dd); |
---|
1583 | // show time if top interval less than a week |
---|
1584 | if (precision > 1) { |
---|
1585 | var hh = d.getUTCHours(), |
---|
1586 | mm = d.getUTCMinutes(), |
---|
1587 | ss = d.getUTCSeconds(); |
---|
1588 | str += 'T' + pad(hh) + ':' + pad(mm); |
---|
1589 | // show seconds if the interval is less than a day |
---|
1590 | if (precision > 2) { |
---|
1591 | str += pad(ss); |
---|
1592 | } |
---|
1593 | str += 'Z'; |
---|
1594 | } |
---|
1595 | } |
---|
1596 | return str; |
---|
1597 | }; |
---|
1598 | |
---|
1599 | /** |
---|
1600 | * Determine the SIMILE Timeline version |
---|
1601 | * XXX: quite rough at the moment |
---|
1602 | * |
---|
1603 | * @return {String} At the moment, only "1.2", "2.2.0", or what Timeline provides |
---|
1604 | */ |
---|
1605 | TimeMap.TimelineVersion = function() { |
---|
1606 | // check for Timeline.version support - added in 2.3.0 |
---|
1607 | if (Timeline.version) { |
---|
1608 | return Timeline.version; |
---|
1609 | } |
---|
1610 | if (Timeline.DurationEventPainter) { |
---|
1611 | return "1.2"; |
---|
1612 | } else { |
---|
1613 | return "2.2.0"; |
---|
1614 | } |
---|
1615 | }; |
---|