aboutsummaryrefslogtreecommitdiff
path: root/includes/js/dojox/data/FlickrRestStore.js
blob: 466d6df2f0f527b12a679f425caeb2047209c1b9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
if(!dojo._hasResource["dojox.data.FlickrRestStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojox.data.FlickrRestStore"] = true;
dojo.provide("dojox.data.FlickrRestStore");

dojo.require("dojox.data.FlickrStore");

dojo.declare("dojox.data.FlickrRestStore",
	dojox.data.FlickrStore, {
	constructor: function(/*Object*/args){ 
		// summary:
		//	Initializer for the FlickrRestStore store.  
		// description:
		//	The FlickrRestStore is a Datastore interface to one of the basic services
		//	of the Flickr service, the public photo feed.  This does not provide
		//	access to all the services of Flickr.
		//	This store cannot do * and ? filtering as the flickr service 
		//	provides no interface for wildcards.
		if(args && args.label){
			if(args.label) {
				this.label = args.label;
			}
			if(args.apikey) {
				this._apikey = args.apikey;
			}
		}
		this._cache = [];
		this._prevRequests = {};
		this._handlers = {};
		this._prevRequestRanges = [];
		this._maxPhotosPerUser = {};
		this._id = dojox.data.FlickrRestStore.prototype._id++;
	},
	
	// _id: Integer
	// A unique identifier for this store.
	_id: 0,
	
	// _requestCount: Integer
	// A counter for the number of requests made. This is used to define
	// the callback function that Flickr will use.
	_requestCount: 0,
	
	// _flickrRestUrl: String
	//	The URL to the Flickr REST services.
	_flickrRestUrl: "http://www.flickr.com/services/rest/",
	
	// _apikey: String
	//	The users API key to be used when accessing Flickr REST services.
	_apikey: null,
	
	// _storeRef: String
	//	A key used to mark an data store item as belonging to this store.
	_storeRef: "_S",
	
	// _cache: Array
	//	An Array of all previously downloaded picture info.
	_cache: null,
	
	// _prevRequests: Object
	//	A HashMap used to record the signature of a request to prevent duplicate 
	//	request being made.
	_prevRequests: null,
	
	// _handlers: Object
	//	A HashMap used to record the handlers registered for a single remote request.  Multiple 
	//	requests may be made for the same information before the first request has finished. 
	//	Each element of this Object is an array of handlers to call back when the request finishes.
	//	This prevents multiple requests being made for the same information.  
	_handlers: null,
	
	// _sortAttributes: Object
	// A quick lookup of valid attribute names in a sort query.
	_sortAttributes: {
		"date-posted": true,
		"date-taken": true,
		"interestingness": true
	},
			
	_fetchItems: function(request, fetchHandler, errorHandler){
		// summary: Fetch flickr items that match to a query
		// request:
		//	A request object
		// fetchHandler:
		//	A function to call for fetched items
		// errorHandler:
		//	A function to call on error
		var query = {};
		if(!request.query){
			request.query = query = {};
		} else {
			dojo.mixin(query, request.query);	
		}
		
		var primaryKey = [];
		var secondaryKey = [];
		
		//Generate a unique function to be called back
		var callbackFn = "FlickrRestStoreCallback_" + this._id + "_" + (++this._requestCount);
		//Build up the content to send the request for.
		var content = {
			format: "json",
			method: "flickr.photos.search",
			api_key: this._apikey,
			extras: "owner_name,date_upload,date_taken",
			jsoncallback: callbackFn
		};
		var isRest = false;
		if(query.userid){
			isRest = true;
			content.user_id = request.query.userid;
			primaryKey.push("userid"+request.query.userid);
		}
		if(query.apikey){
			isRest = true;
			content.api_key = request.query.apikey;
			secondaryKey.push("api"+request.query.apikey);
		} else{
			throw Error("dojox.data.FlickrRestStore: An API key must be specified.");
		}
		request._curCount = request.count;
		if(query.page){
			content.page = request.query.page;
			secondaryKey.push("page" + content.page);
		}else if(typeof(request.start) != "undefined" && request.start != null) {
			if(!request.count){
				request.count = 20;
			}
			var diff = request.start % request.count;
			var start = request.start, count = request.count;
			//If the count does not divide cleanly into the start number,
			//more work has to be done to figure out the best page to request
			if(diff != 0) {
				if(start < count / 2) {
					//If the first record requested is less than half the amount requested,
					//then request from 0 to the count record
					count = start + count;
					start = 0; 
				} else {
					var divLimit = 20, div = 2;
					for(var i = divLimit; i > 0; i--) {
						if(start % i == 0 && (start/i) >= count){
							div = i;
							break;
						}
					}
					count = start/div;
				}
				request._realStart = request.start;
				request._realCount = request.count;
				request._curStart = start;
				request._curCount = count;
			} else {
				request._realStart = request._realCount = null;
				request._curStart = request.start;
				request._curCount = request.count;
			}
			
			content.page = (start / count) + 1;
			secondaryKey.push("page" + content.page);
		}
		if(request._curCount){
			content.per_page = request._curCount;
			secondaryKey.push("count" + request._curCount);
		}
		
		if(query.lang){
			content.lang = request.query.lang;
			primaryKey.push("lang" + request.lang);
		}
		var url = this._flickrRestUrl;
		
		if(query.setid){
			content.method = "flickr.photosets.getPhotos";
			content.photoset_id = request.query.set; 
			primaryKey.push("set" + request.query.set);
		}
		
		if(query.tags){
			if(query.tags instanceof Array){
				content.tags = query.tags.join(",");
			} else {
				content.tags=query.tags;				
			}
			primaryKey.push("tags" + content.tags);
			
			if(query["tag_mode"] && (query.tag_mode.toLowerCase() == "any"
				|| query.tag_mode.toLowerCase() == "all")){
				content.tag_mode = query.tag_mode;
			}
		}
		if(query.text){
			content.text=query.text;
			primaryKey.push("text:"+query.text);
		}
		
		//The store only supports a single sort attribute, even though the
		//Read API technically allows multiple sort attributes
		if(query.sort && query.sort.length > 0){			
			//The default sort attribute is 'date-posted'
			if(!query.sort[0].attribute){
				query.sort[0].attribute = "date-posted";
			}
			
			//If the sort attribute is valid, check if it is ascending or
			//descending.
			if(this._sortAttributes[query.sort[0].attribute]) {
				if(query.sort[0].descending){
					content.sort = query.sort[0].attribute + "-desc";
				} else {
					content.sort = query.sort[0].attribute + "-asc";
				}
			}
		} else {
			//The default sort in the Dojo Data API is ascending.
			content.sort = "date-posted-asc";
		}
		primaryKey.push("sort:"+content.sort);
	
		//Generate a unique key for this request, so the store can 
		//detect duplicate requests.
		primaryKey = primaryKey.join(".");
		secondaryKey = secondaryKey.length > 0 ? "." + secondaryKey.join(".") : "";
		var requestKey = primaryKey + secondaryKey;
		
		//Make a copy of the request, in case the source object is modified
		//before the request completes
		request = {
			query: query,
			count: request._curCount,
			start: request._curStart,
			_realCount: request._realCount,
			_realStart: request._realStart,
			onBegin: request.onBegin,
			onComplete: request.onComplete,
			onItem: request.onItem
		};

		var thisHandler = {
			request: request,
	    	fetchHandler: fetchHandler,
	    	errorHandler: errorHandler
	   	};

	   	//If the request has already been made, but not yet completed,
	   	//then add the callback handler to the list of handlers
	   	//for this request, and finish.
	   	if(this._handlers[requestKey]){
	    	this._handlers[requestKey].push(thisHandler);
	    	return;
	   	}

  		this._handlers[requestKey] = [thisHandler];

  		//Linking this up to Flickr is a PAIN!
  		var self = this;
  		var handle = null;
  		var getArgs = {
			url: this._flickrRestUrl,
			preventCache: true,
			content: content
		};
		
  		var doHandle = function(processedData, data, handler){
			var onBegin = handler.request.onBegin;
			handler.request.onBegin = null;
			var maxPhotos;
			var req = handler.request;
			
			if(typeof(req._realStart) != undefined && req._realStart != null) {
				req.start = req._realStart;
				req.count = req._realCount;
				req._realStart = req._realCount = null;
			}

			//If the request contains an onBegin method, the total number
			//of photos must be calculated.
			if(onBegin){
				if(data && typeof(data.photos.perpage) != "undefined" && typeof(data.photos.pages) != "undefined"){
						if(data.photos.perpage * data.photos.pages <= handler.request.start + handler.request.count){
							//If the final page of results has been received, it is possible to 
							//know exactly how many photos there are
							maxPhotos = handler.request.start + data.photos.photo.length;                
						}else{
							//If the final page of results has not yet been received,
							//it is not possible to tell exactly how many photos exist, so
							//return the number of pages multiplied by the number of photos per page.
							maxPhotos = data.photos.perpage * data.photos.pages;
						}
						self._maxPhotosPerUser[primaryKey] = maxPhotos;
						onBegin(maxPhotos, handler.request);
				} else if(self._maxPhotosPerUser[primaryKey]) {
					onBegin(self._maxPhotosPerUser[primaryKey], handler.request);
				}
			}
			//Call whatever functions the caller has defined on the request object, except for onBegin
			handler.fetchHandler(processedData, handler.request);
			if(onBegin){
				//Replace the onBegin function, if it existed.
				handler.request.onBegin = onBegin;
			}
		};
		
		//Define a callback for the script that iterates through a list of 
		//handlers for this piece of data.  Multiple requests can come into
		//the store for the same data.
		var myHandler = function(data){
			//The handler should not be called more than once, so disconnect it.
			//if(handle !== null){ dojo.disconnect(handle); }
			if(data.stat != "ok"){
				errorHandler(null, request);
			}else{ //Process the items...
				var handlers = self._handlers[requestKey];
				if(!handlers){
					console.log("FlickrRestStore: no handlers for data", data);
					return;
				}

				self._handlers[requestKey] = null;
				self._prevRequests[requestKey] = data;

				//Process the data once.
				var processedData = self._processFlickrData(data, request, primaryKey);
				if(!self._prevRequestRanges[primaryKey]) {
					self._prevRequestRanges[primaryKey] = [];
				}
				self._prevRequestRanges[primaryKey].push({
					start: request.start,
					end: request.start + data.photos.photo.length
				});

				//Iterate through the array of handlers, calling each one.
				for(var i = 0; i < handlers.length; i++ ){
					doHandle(processedData, data, handlers[i]);
				}
			}
		};

		var data = this._prevRequests[requestKey];
		
		//If the data was previously retrieved, there is no need to fetch it again.
		if(data){
			this._handlers[requestKey] = null;
			doHandle(this._cache[primaryKey], data, thisHandler);
			return;
		} else if(this._checkPrevRanges(primaryKey, request.start, request.count)) {
			//If this range of data has already been retrieved, reuse it.
			this._handlers[requestKey] = null;
			doHandle(this._cache[primaryKey], null, thisHandler);
			return;
		}
		
		dojo.global[callbackFn] = function(data){
			myHandler(data);
			//Clean up the function, it should never be called again
			dojo.global[callbackFn] = null;
		};
				
		var deferred = dojo.io.script.get(getArgs);
		
		//We only set up the errback, because the callback isn't ever really used because we have
		//to link to the jsonFlickrFeed function....
		deferred.addErrback(function(error){
			dojo.disconnect(handle);
			errorHandler(error, request);
		});
	},
	
	getAttributes: function(item){
		//	summary: 
		//      See dojo.data.api.Read.getAttributes()
		return ["title", "author", "imageUrl", "imageUrlSmall", 
					"imageUrlMedium", "imageUrlThumb", "link",
					"dateTaken", "datePublished"]; 
	},
	
	getValues: function(item, attribute){
		//	summary:
		//      See dojo.data.api.Read.getValue()
		this._assertIsItem(item);
		this._assertIsAttribute(attribute);
		if(attribute === "title"){
			return [this._unescapeHtml(item.title)]; // String
		}else if(attribute === "author"){
			return [item.ownername]; // String
		}else if(attribute === "imageUrlSmall"){
			return [item.media.s]; // String
		}else if(attribute === "imageUrl"){
			return [item.media.l]; // String
		}else if(attribute === "imageUrlMedium"){
			return [item.media.m]; // String
		}else if(attribute === "imageUrlThumb"){
			return [item.media.t]; // String
		}else if(attribute === "link"){
			return ["http://www.flickr.com/photos/" + item.owner + "/" + item.id]; // String
		}else if(attribute === "dateTaken"){
			return item.datetaken;
		}else if(attribute === "datePublished"){
			return item.datepublished;
		}
		
		return undefined;
	},

	_processFlickrData: function(/* Object */data, /* Object */request, /* String */ cacheKey){
		// summary: Processes the raw data from Flickr and updates the internal cache.
		// data: 
		//		Data returned from Flickr
		// request: 
		//		The original dojo.data.Request object passed in by the user.
		
		//If the data contains an 'item' object, it has not come from the REST services,
		//so process it using the FlickrStore.
		if(data.items){
			return dojox.data.FlickrStore.prototype._processFlickrData.apply(this,arguments);
		}

		var template = ["http://farm", null, ".static.flickr.com/", null, "/", null, "_", null];
		
		var items = [];
		if(data.stat == "ok" && data.photos && data.photos.photo){
			items = data.photos.photo;
			
			//Add on the store ref so that isItem can work.
			for(var i = 0; i < items.length; i++){
				var item = items[i];
				item[this._storeRef] = this;
				
				template[1] = item.farm;
				template[3] = item.server;
				template[5] = item.id;
				template[7] = item.secret;
				 
				var base = template.join("");
				item.media = {
					s: base + "_s.jpg",
				 	m: base + "_m.jpg",
				 	l: base + ".jpg",
				 	t: base + "_t.jpg"
				};
			}
		}
		var start = request.start ? request.start : 0;
		var arr = this._cache[cacheKey];
		if(!arr) {
			this._cache[cacheKey] = arr = [];
		}
		for(var count = 0; count < items.length; count++){
			arr[count + start] = items[count];
		}

		return arr; // Array
	},
	
	_checkPrevRanges: function(primaryKey, start, count) {
		var end = start + count;
		var arr = this._prevRequestRanges[primaryKey];
		if(!arr) {
			return false;
		}
		for(var i = 0; i< arr.length; i++) {
			if(start >= arr[i].start &&
			   end <= arr[i].end) {
				return true;
			}
		}
		return false;
	}
});


}