FlickrRestStore.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. define("dojox/data/FlickrRestStore", ["dojo/_base/lang", "dojo/_base/declare", "dojo/_base/array", "dojo/io/script", "dojox/data/FlickrStore", "dojo/_base/connect"],
  2. function(lang, declare, array, scriptIO, FlickrStore, connect) {
  3. /*===== var FlickrStore = dojox.data.FlickrStore; =====*/
  4. var FlickrRestStore = declare("dojox.data.FlickrRestStore",
  5. FlickrStore, {
  6. constructor: function(/*Object*/args){
  7. // summary:
  8. // Initializer for the FlickrRestStore store.
  9. // description:
  10. // The FlickrRestStore is a Datastore interface to one of the basic services
  11. // of the Flickr service, the public photo feed. This does not provide
  12. // access to all the services of Flickr.
  13. // This store cannot do * and ? filtering as the flickr service
  14. // provides no interface for wildcards.
  15. if(args){
  16. if(args.label){
  17. this.label = args.label;
  18. }
  19. if(args.apikey){
  20. this._apikey = args.apikey;
  21. }
  22. }
  23. this._cache = [];
  24. this._prevRequests = {};
  25. this._handlers = {};
  26. this._prevRequestRanges = [];
  27. this._maxPhotosPerUser = {};
  28. this._id = FlickrRestStore.prototype._id++;
  29. },
  30. // _id: Integer
  31. // A unique identifier for this store.
  32. _id: 0,
  33. // _requestCount: Integer
  34. // A counter for the number of requests made. This is used to define
  35. // the callback function that Flickr will use.
  36. _requestCount: 0,
  37. // _flickrRestUrl: String
  38. // The URL to the Flickr REST services.
  39. _flickrRestUrl: "http://www.flickr.com/services/rest/",
  40. // _apikey: String
  41. // The users API key to be used when accessing Flickr REST services.
  42. _apikey: null,
  43. // _storeRef: String
  44. // A key used to mark an data store item as belonging to this store.
  45. _storeRef: "_S",
  46. // _cache: Array
  47. // An Array of all previously downloaded picture info.
  48. _cache: null,
  49. // _prevRequests: Object
  50. // A HashMap used to record the signature of a request to prevent duplicate
  51. // request being made.
  52. _prevRequests: null,
  53. // _handlers: Object
  54. // A HashMap used to record the handlers registered for a single remote request. Multiple
  55. // requests may be made for the same information before the first request has finished.
  56. // Each element of this Object is an array of handlers to call back when the request finishes.
  57. // This prevents multiple requests being made for the same information.
  58. _handlers: null,
  59. // _sortAttributes: Object
  60. // A quick lookup of valid attribute names in a sort query.
  61. _sortAttributes: {
  62. "date-posted": true,
  63. "date-taken": true,
  64. "interestingness": true
  65. },
  66. _fetchItems: function( /*Object*/ request,
  67. /*Function*/ fetchHandler,
  68. /*Function*/ errorHandler){
  69. // summary: Fetch flickr items that match to a query
  70. // request:
  71. // A request object
  72. // fetchHandler:
  73. // A function to call for fetched items
  74. // errorHandler:
  75. // A function to call on error
  76. var query = {};
  77. if(!request.query){
  78. request.query = query = {};
  79. } else {
  80. lang.mixin(query, request.query);
  81. }
  82. var primaryKey = [];
  83. var secondaryKey = [];
  84. //Build up the content to send the request for.
  85. var content = {
  86. format: "json",
  87. method: "flickr.photos.search",
  88. api_key: this._apikey,
  89. extras: "owner_name,date_upload,date_taken"
  90. };
  91. var isRest = false;
  92. if(query.userid){
  93. isRest = true;
  94. content.user_id = request.query.userid;
  95. primaryKey.push("userid"+request.query.userid);
  96. }
  97. if(query.groupid){
  98. isRest = true;
  99. content.group_id = query.groupid;
  100. primaryKey.push("groupid" + query.groupid);
  101. }
  102. if(query.apikey){
  103. isRest = true;
  104. content.api_key = request.query.apikey;
  105. secondaryKey.push("api"+request.query.apikey);
  106. }else if(content.api_key){
  107. isRest = true;
  108. request.query.apikey = content.api_key;
  109. secondaryKey.push("api"+content.api_key);
  110. }else{
  111. throw Error("dojox.data.FlickrRestStore: An API key must be specified.");
  112. }
  113. request._curCount = request.count;
  114. if(query.page){
  115. content.page = request.query.page;
  116. secondaryKey.push("page" + content.page);
  117. }else if(("start" in request) && request.start !== null){
  118. if(!request.count){
  119. request.count = 20;
  120. }
  121. var diff = request.start % request.count;
  122. var start = request.start, count = request.count;
  123. // If the count does not divide cleanly into the start number,
  124. // more work has to be done to figure out the best page to request
  125. if(diff !== 0) {
  126. if(start < count / 2){
  127. // If the first record requested is less than half the
  128. // amount requested, then request from 0 to the count record
  129. count = start + count;
  130. start = 0;
  131. }else{
  132. var divLimit = 20, div = 2;
  133. for(var i = divLimit; i > 0; i--){
  134. if(start % i === 0 && (start/i) >= count){
  135. div = i;
  136. break;
  137. }
  138. }
  139. count = start/div;
  140. }
  141. request._realStart = request.start;
  142. request._realCount = request.count;
  143. request._curStart = start;
  144. request._curCount = count;
  145. }else{
  146. request._realStart = request._realCount = null;
  147. request._curStart = request.start;
  148. request._curCount = request.count;
  149. }
  150. content.page = (start / count) + 1;
  151. secondaryKey.push("page" + content.page);
  152. }
  153. if(request._curCount){
  154. content.per_page = request._curCount;
  155. secondaryKey.push("count" + request._curCount);
  156. }
  157. if(query.lang){
  158. content.lang = request.query.lang;
  159. primaryKey.push("lang" + request.lang);
  160. }
  161. if(query.setid){
  162. content.method = "flickr.photosets.getPhotos";
  163. content.photoset_id = request.query.setid;
  164. primaryKey.push("set" + request.query.setid);
  165. }
  166. if(query.tags){
  167. if(query.tags instanceof Array){
  168. content.tags = query.tags.join(",");
  169. }else{
  170. content.tags = query.tags;
  171. }
  172. primaryKey.push("tags" + content.tags);
  173. if(query["tag_mode"] && (query.tag_mode.toLowerCase() === "any" ||
  174. query.tag_mode.toLowerCase() === "all")){
  175. content.tag_mode = query.tag_mode;
  176. }
  177. }
  178. if(query.text){
  179. content.text=query.text;
  180. primaryKey.push("text:"+query.text);
  181. }
  182. //The store only supports a single sort attribute, even though the
  183. //Read API technically allows multiple sort attributes
  184. if(query.sort && query.sort.length > 0){
  185. //The default sort attribute is 'date-posted'
  186. if(!query.sort[0].attribute){
  187. query.sort[0].attribute = "date-posted";
  188. }
  189. //If the sort attribute is valid, check if it is ascending or
  190. //descending.
  191. if(this._sortAttributes[query.sort[0].attribute]) {
  192. if(query.sort[0].descending){
  193. content.sort = query.sort[0].attribute + "-desc";
  194. }else{
  195. content.sort = query.sort[0].attribute + "-asc";
  196. }
  197. }
  198. }else{
  199. //The default sort in the Dojo Data API is ascending.
  200. content.sort = "date-posted-asc";
  201. }
  202. primaryKey.push("sort:"+content.sort);
  203. //Generate a unique key for this request, so the store can
  204. //detect duplicate requests.
  205. primaryKey = primaryKey.join(".");
  206. secondaryKey = secondaryKey.length > 0 ? "." + secondaryKey.join(".") : "";
  207. var requestKey = primaryKey + secondaryKey;
  208. //Make a copy of the request, in case the source object is modified
  209. //before the request completes
  210. request = {
  211. query: query,
  212. count: request._curCount,
  213. start: request._curStart,
  214. _realCount: request._realCount,
  215. _realStart: request._realStart,
  216. onBegin: request.onBegin,
  217. onComplete: request.onComplete,
  218. onItem: request.onItem
  219. };
  220. var thisHandler = {
  221. request: request,
  222. fetchHandler: fetchHandler,
  223. errorHandler: errorHandler
  224. };
  225. //If the request has already been made, but not yet completed,
  226. //then add the callback handler to the list of handlers
  227. //for this request, and finish.
  228. if(this._handlers[requestKey]){
  229. this._handlers[requestKey].push(thisHandler);
  230. return;
  231. }
  232. this._handlers[requestKey] = [thisHandler];
  233. //Linking this up to Flickr is a PAIN!
  234. var handle = null;
  235. var getArgs = {
  236. url: this._flickrRestUrl,
  237. preventCache: this.urlPreventCache,
  238. content: content,
  239. callbackParamName: "jsoncallback"
  240. };
  241. var doHandle = lang.hitch(this, function(processedData, data, handler){
  242. var onBegin = handler.request.onBegin;
  243. handler.request.onBegin = null;
  244. var maxPhotos;
  245. var req = handler.request;
  246. if(("_realStart" in req) && req._realStart != null){
  247. req.start = req._realStart;
  248. req.count = req._realCount;
  249. req._realStart = req._realCount = null;
  250. }
  251. //If the request contains an onBegin method, the total number
  252. //of photos must be calculated.
  253. if(onBegin){
  254. var photos = null;
  255. if(data){
  256. photos = (data.photoset ? data.photoset : data.photos);
  257. }
  258. if(photos && ("perpage" in photos) && ("pages" in photos)){
  259. if(photos.perpage * photos.pages <= handler.request.start + handler.request.count){
  260. //If the final page of results has been received, it is possible to
  261. //know exactly how many photos there are
  262. maxPhotos = handler.request.start + photos.photo.length;
  263. }else{
  264. //If the final page of results has not yet been received,
  265. //it is not possible to tell exactly how many photos exist, so
  266. //return the number of pages multiplied by the number of photos per page.
  267. maxPhotos = photos.perpage * photos.pages;
  268. }
  269. this._maxPhotosPerUser[primaryKey] = maxPhotos;
  270. onBegin(maxPhotos, handler.request);
  271. }else if(this._maxPhotosPerUser[primaryKey]){
  272. onBegin(this._maxPhotosPerUser[primaryKey], handler.request);
  273. }
  274. }
  275. //Call whatever functions the caller has defined on the request object, except for onBegin
  276. handler.fetchHandler(processedData, handler.request);
  277. if(onBegin){
  278. //Replace the onBegin function, if it existed.
  279. handler.request.onBegin = onBegin;
  280. }
  281. });
  282. //Define a callback for the script that iterates through a list of
  283. //handlers for this piece of data. Multiple requests can come into
  284. //the store for the same data.
  285. var myHandler = lang.hitch(this, function(data){
  286. //The handler should not be called more than once, so disconnect it.
  287. //if(handle !== null){ dojo.disconnect(handle); }
  288. if(data.stat != "ok"){
  289. errorHandler(null, request);
  290. }else{ //Process the items...
  291. var handlers = this._handlers[requestKey];
  292. if(!handlers){
  293. console.log("FlickrRestStore: no handlers for data", data);
  294. return;
  295. }
  296. this._handlers[requestKey] = null;
  297. this._prevRequests[requestKey] = data;
  298. //Process the data once.
  299. var processedData = this._processFlickrData(data, request, primaryKey);
  300. if(!this._prevRequestRanges[primaryKey]){
  301. this._prevRequestRanges[primaryKey] = [];
  302. }
  303. this._prevRequestRanges[primaryKey].push({
  304. start: request.start,
  305. end: request.start + (data.photoset ? data.photoset.photo.length : data.photos.photo.length)
  306. });
  307. //Iterate through the array of handlers, calling each one.
  308. array.forEach(handlers, function(i){
  309. doHandle(processedData, data, i);
  310. });
  311. }
  312. });
  313. var data = this._prevRequests[requestKey];
  314. //If the data was previously retrieved, there is no need to fetch it again.
  315. if(data){
  316. this._handlers[requestKey] = null;
  317. doHandle(this._cache[primaryKey], data, thisHandler);
  318. return;
  319. }else if(this._checkPrevRanges(primaryKey, request.start, request.count)){
  320. //If this range of data has already been retrieved, reuse it.
  321. this._handlers[requestKey] = null;
  322. doHandle(this._cache[primaryKey], null, thisHandler);
  323. return;
  324. }
  325. var deferred = scriptIO.get(getArgs);
  326. deferred.addCallback(myHandler);
  327. //We only set up the errback, because the callback isn't ever really used because we have
  328. //to link to the jsonFlickrFeed function....
  329. deferred.addErrback(function(error){
  330. connect.disconnect(handle);
  331. errorHandler(error, request);
  332. });
  333. },
  334. getAttributes: function(item){
  335. // summary:
  336. // See dojo.data.api.Read.getAttributes()
  337. return [
  338. "title", "author", "imageUrl", "imageUrlSmall", "imageUrlMedium",
  339. "imageUrlThumb", "imageUrlLarge", "imageUrlOriginal", "link", "dateTaken", "datePublished"
  340. ];
  341. },
  342. getValues: function(item, attribute){
  343. // summary:
  344. // See dojo.data.api.Read.getValue()
  345. this._assertIsItem(item);
  346. this._assertIsAttribute(attribute);
  347. switch(attribute){
  348. case "title":
  349. return [ this._unescapeHtml(item.title) ]; // String
  350. case "author":
  351. return [ item.ownername ]; // String
  352. case "imageUrlSmall":
  353. return [ item.media.s ]; // String
  354. case "imageUrl":
  355. return [ item.media.l ]; // String
  356. case "imageUrlOriginal":
  357. return [ item.media.o ]; // String
  358. case "imageUrlLarge":
  359. return [ item.media.l ]; // String
  360. case "imageUrlMedium":
  361. return [ item.media.m ]; // String
  362. case "imageUrlThumb":
  363. return [ item.media.t ]; // String
  364. case "link":
  365. return [ "http://www.flickr.com/photos/" + item.owner + "/" + item.id ]; // String
  366. case "dateTaken":
  367. return [ item.datetaken ];
  368. case "datePublished":
  369. return [ item.datepublished ];
  370. default:
  371. return undefined;
  372. }
  373. },
  374. _processFlickrData: function(/* Object */data, /* Object */request, /* String */ cacheKey){
  375. // summary: Processes the raw data from Flickr and updates the internal cache.
  376. // data:
  377. // Data returned from Flickr
  378. // request:
  379. // The original dojo.data.Request object passed in by the user.
  380. // If the data contains an 'item' object, it has not come from the REST
  381. // services, so process it using the FlickrStore.
  382. if(data.items){
  383. return FlickrStore.prototype._processFlickrData.apply(this,arguments);
  384. }
  385. var template = ["http://farm", null, ".static.flickr.com/", null, "/", null, "_", null];
  386. var items = [];
  387. var photos = (data.photoset ? data.photoset : data.photos);
  388. if(data.stat == "ok" && photos && photos.photo){
  389. items = photos.photo;
  390. //Add on the store ref so that isItem can work.
  391. for(var i = 0; i < items.length; i++){
  392. var item = items[i];
  393. item[this._storeRef] = this;
  394. template[1] = item.farm;
  395. template[3] = item.server;
  396. template[5] = item.id;
  397. template[7] = item.secret;
  398. var base = template.join("");
  399. item.media = {
  400. s: base + "_s.jpg",
  401. m: base + "_m.jpg",
  402. l: base + ".jpg",
  403. t: base + "_t.jpg",
  404. o: base + "_o.jpg"
  405. };
  406. if(!item.owner && data.photoset){
  407. item.owner = data.photoset.owner;
  408. }
  409. }
  410. }
  411. var start = request.start ? request.start : 0;
  412. var arr = this._cache[cacheKey];
  413. if(!arr){
  414. this._cache[cacheKey] = arr = [];
  415. }
  416. array.forEach(items, function(i, idx){
  417. arr[idx+ start] = i;
  418. });
  419. return arr; // Array
  420. },
  421. _checkPrevRanges: function(primaryKey, start, count){
  422. var end = start + count;
  423. var arr = this._prevRequestRanges[primaryKey];
  424. return (!!arr) && array.some(arr, function(item){
  425. return ((start >= item.start)&&(end <= item.end));
  426. });
  427. }
  428. });
  429. return FlickrRestStore;
  430. });