1 /* 
  2  * Timemap.js Copyright 2010 Nick Rabinowitz.
  3  * Licensed under the MIT License (see LICENSE.txt)
  4  */
  5 
  6 /**
  7  * @fileOverview
  8  * Progressive loader
  9  *
 10  * @author Nick Rabinowitz (www.nickrabinowitz.com)
 11  */
 12  
 13 // for JSLint
 14 /*global TimeMap */
 15 
 16 /**
 17  * @class
 18  * Progressive loader class - basically a wrapper for another remote loader that can
 19  * load data progressively by date range, depending on timeline position.
 20  *
 21  * <p>The progressive loader can take either another loader or parameters for 
 22  * another loader. It expects a loader with a "url" attribute including placeholder
 23  * strings [start] and [end] for the start and end dates to retrieve. The assumption 
 24  * is that the data service can take start and end parameters and return the data for 
 25  * that date range.</p>
 26  *
 27  * @example
 28 TimeMap.init({
 29     datasets: [
 30         {
 31             title: "Progressive JSONP Dataset",
 32             type: "progressive",
 33             options: {
 34                 type: "jsonp",
 35                 url: "http://www.test.com/getsomejson.php?start=[start]&end=[end]callback="
 36             }
 37         }
 38     ],
 39     // etc...
 40 });
 41  *
 42  * @example
 43 TimeMap.init({
 44     datasets: [
 45         {
 46             title: "Progressive KML Dataset",
 47             type: "progressive",
 48             options: {
 49                 loader: new TimeMap.loaders.kml({
 50                     url: "/mydata.kml?start=[start]&end=[end]"
 51                 })
 52             }
 53         }
 54     ],
 55     // etc...
 56 }); 
 57  * @see <a href="../../examples/progressive.html">Progressive Loader Example</a>
 58  *
 59  * @constructor
 60  * @param {Object} options          All options for the loader
 61  * @param {TimeMap.loaders.remote} [options.loader] Instantiated loader class (overrides "type")
 62  * @param {String} [options.type]                   Name of loader class to use
 63  * @param {String|Date} options.start               Start of initial date range, as date or string
 64  * @param {Number} options.interval                 Size in milliseconds of date ranges to load at a time
 65  * @param {String|Date} [options.dataMinDate]       Minimum date available in data (optional, will avoid
 66  *                                                  unnecessary service requests if supplied)
 67  * @param {String|Date} [options.dataMaxDate]       Maximum date available in data (optional, will avoid
 68  *                                                  unnecessary service requests if supplied)
 69  * @param {Function} [options.formatUrl]            Function taking (urlTemplate, start, end) and returning
 70  *                                                  a URL formatted as needed by the service
 71  * @param {Function} [options.formatDate={@link TimeMap.util.formatDate}]           
 72  *                                                  Function to turn a date into a string formatted
 73  *                                                  as needed by the service
 74  * @param {mixed} [options[...]]                    Other options needed by the "type" loader
 75  */
 76 TimeMap.loaders.progressive = function(options) {
 77     // get loader
 78     var loader = options.loader, 
 79         type = options.type;
 80     if (!loader) {
 81         // get loader class
 82         var loaderClass = (typeof(type) == 'string') ? TimeMap.loaders[type] : type;
 83         loader = new loaderClass(options);
 84     }
 85     
 86     // save loader attributes
 87     var baseUrl = loader.opts.url, 
 88         baseLoadFunction = loader.load,
 89         interval = options.interval,
 90         formatDate = options.formatDate || TimeMap.util.formatDate,
 91         formatUrl = options.formatUrl || function(url, start, end) {
 92             return url
 93                 .replace('[start]', formatDate(start))
 94                 .replace('[end]', formatDate(end));
 95         },
 96         parseDate = TimeMap.dateParsers.hybrid,
 97         zeroDate = parseDate(options.start), 
 98         dataMinDate = parseDate(options.dataMinDate), 
 99         dataMaxDate = parseDate(options.dataMaxDate),
100         loaded = {};
101     
102     // We don't start with a TimeMap reference, so we need
103     // to stick the listener in on the first load() call
104     var addListener = function(dataset) {
105         var band = dataset.timemap.timeline.getBand(0);
106         // add listener
107         band.addOnScrollListener(function() {
108             // determine relevant blocks
109             var now = band.getCenterVisibleDate(),
110                 currBlock = Math.floor((now.getTime() - zeroDate.getTime()) / interval),
111                 currBlockTime = zeroDate.getTime() + (interval * currBlock),
112                 nextBlockTime = currBlockTime + interval,
113                 prevBlockTime = currBlockTime - interval,
114                 // no callback necessary?
115                 callback = function() {
116                     dataset.timemap.timeline.layout();
117                 };
118             
119             // is the current block loaded?
120             if ((!dataMaxDate || currBlockTime < dataMaxDate.getTime()) &&
121                 (!dataMinDate || currBlockTime > dataMinDate.getTime()) &&
122                 !loaded[currBlock]) {
123                 // load it
124                 // console.log("loading current block (" + currBlock + ")");
125                 loader.load(dataset, callback, new Date(currBlockTime), currBlock);
126             }
127             // are we close enough to load the next block, and is it loaded?
128             if (nextBlockTime < band.getMaxDate().getTime() &&
129                 (!dataMaxDate || nextBlockTime < dataMaxDate.getTime()) &&
130                 !loaded[currBlock + 1]) {
131                 // load next block
132                 // console.log("loading next block (" + (currBlock + 1) + ")");
133                 loader.load(dataset, callback, new Date(nextBlockTime), currBlock + 1);
134             }
135             // are we close enough to load the previous block, and is it loaded?
136             if (prevBlockTime > band.getMinDate().getTime() &&
137                 (!dataMinDate || prevBlockTime > dataMinDate.getTime()) &&
138                 !loaded[currBlock - 1]) {
139                 // load previous block
140                 // console.log("loading prev block (" + (currBlock - 1)  + ")");
141                 loader.load(dataset, callback, new Date(prevBlockTime), currBlock - 1);
142             }
143         });
144         // kill this function so that listener is only added once
145         addListener = false;
146     };
147     
148     /**
149      * Load data based on current time
150      * @name TimeMap.loaders.progressive#load
151      * @function
152      * @param {TimeMapDataset} dataset      Dataset to load data into
153      * @param {Function} callback           Callback to execute when data is loaded
154      * @param {Date} start                  Start date to load data from
155      * @param {Number} currBlock            Index of the current time block
156      */
157     loader.load = function(dataset, callback, start, currBlock) {
158         // set start date, defaulting to zero date
159         start = parseDate(start) || zeroDate;
160         // set current block, defaulting to 0
161         currBlock = currBlock || 0;
162         // set end by interval
163         var end = new Date(start.getTime() + interval);
164         
165         // set current block as loaded
166         // XXX: Failed loads will give a false positive here...
167         // but I'm not sure how else to avoid multiple loads :(
168         loaded[currBlock] = true;
169         
170         // put dates into URL
171         loader.opts.url = formatUrl(baseUrl, start, end);
172         
173         // load data
174         baseLoadFunction.call(loader, dataset, function() {
175             // add onscroll listener if not yet done
176             if (addListener) {
177                 addListener(dataset);
178             }
179             // run callback
180             callback();
181         });
182     };
183     
184     return loader;
185 };
186