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     // quick string/date check
 87     function cleanDate(d) {
 88         if (typeof(d) == "string") {
 89             d = TimeMapDataset.hybridParser(d);
 90         }
 91         return d;
 92     }
 93     
 94     // save loader attributes
 95     var baseUrl = loader.url, 
 96         baseLoadFunction = loader.load,
 97         interval = options.interval,
 98         formatDate = options.formatDate || TimeMap.util.formatDate,
 99         formatUrl =  options.formatUrl,
100         zeroDate = cleanDate(options.start), 
101         dataMinDate = cleanDate(options.dataMinDate), 
102         dataMaxDate = cleanDate(options.dataMaxDate),
103         loaded = {};
104     
105     if (!formatUrl) {
106         formatUrl = function(url, start, end) {
107             return url
108                 .replace('[start]', formatDate(start))
109                 .replace('[end]', formatDate(end));
110         }
111     }
112     
113     // We don't start with a TimeMap reference, so we need
114     // to stick the listener in on the first load() call
115     var addListener = function(dataset) {
116         var band = dataset.timemap.timeline.getBand(0);
117         // add listener
118         band.addOnScrollListener(function() {
119             // determine relevant blocks
120             var now = band.getCenterVisibleDate(),
121                 currBlock = Math.floor((now.getTime() - zeroDate.getTime()) / interval),
122                 currBlockTime = zeroDate.getTime() + (interval * currBlock)
123                 nextBlockTime = currBlockTime + interval,
124                 prevBlockTime = currBlockTime - interval,
125                 // no callback necessary?
126                 callback = function() {
127                     dataset.timemap.timeline.layout();
128                 };
129             
130             // is the current block loaded?
131             if ((!dataMaxDate || currBlockTime < dataMaxDate.getTime()) &&
132                 (!dataMinDate || currBlockTime > dataMinDate.getTime()) &&
133                 !loaded[currBlock]) {
134                 // load it
135                 // console.log("loading current block (" + currBlock + ")");
136                 loader.load(dataset, callback, new Date(currBlockTime), currBlock);
137             }
138             // are we close enough to load the next block, and is it loaded?
139             if (nextBlockTime < band.getMaxDate().getTime() &&
140                 (!dataMaxDate || nextBlockTime < dataMaxDate.getTime()) &&
141                 !loaded[currBlock + 1]) {
142                 // load next block
143                 // console.log("loading next block (" + (currBlock + 1) + ")");
144                 loader.load(dataset, callback, new Date(nextBlockTime), currBlock + 1);
145             }
146             // are we close enough to load the previous block, and is it loaded?
147             if (prevBlockTime > band.getMinDate().getTime() &&
148                 (!dataMinDate || prevBlockTime > dataMinDate.getTime()) &&
149                 !loaded[currBlock - 1]) {
150                 // load previous block
151                 // console.log("loading prev block (" + (currBlock - 1)  + ")");
152                 loader.load(dataset, callback, new Date(prevBlockTime), currBlock - 1);
153             }
154         });
155         // kill this function so that listener is only added once
156         addListener = false;
157     };
158     
159     /**
160      * Load data based on current time
161      * @name TimeMap.loaders.progressive#load
162      * @function
163      * @param {TimeMapDataset} dataset      Dataset to load data into
164      * @param {Function} callback           Callback to execute when data is loaded
165      * @param {Date} start                  Start date to load data from
166      * @param {Number} currBlock            Index of the current time block
167      */
168     loader.load = function(dataset, callback, start, currBlock) {
169         // set start date, defaulting to zero date
170         start = cleanDate(start) || zeroDate;
171         // set current block, defaulting to 0
172         currBlock = currBlock || 0;
173         // set end by interval
174         var end = new Date(start.getTime() + interval);
175         
176         // set current block as loaded
177         // XXX: Failed loads will give a false positive here...
178         // but I'm not sure how else to avoid multiple loads :(
179         loaded[currBlock] = true;
180         
181         // put dates into URL
182         loader.url = formatUrl(baseUrl, start, end);
183         // console.log(loader.url);
184         
185         // load data
186         baseLoadFunction.call(loader, dataset, function() {
187             // add onscroll listener if not yet done
188             if (addListener) {
189                 addListener(dataset);
190             }
191             // run callback
192             callback();
193         });
194     };
195     
196     return loader;
197 };
198