Home | History | Annotate | Download | only in photo
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 'use strict';
      6 
      7 /**
      8  * Tile view displays images/videos tiles.
      9  *
     10  * @param {HTMLDocument} document Document.
     11  * @param {function(TailBox, callback)} prepareBox This function should provide
     12  *     the passed box with width and height properties of the image.
     13  * @param {function(TailBox, callback)} loadBox This function should display
     14  *     the image in the box respecting clientWidth and clientHeight.
     15  * @constructor
     16  */
     17 function TileView(document, prepareBox, loadBox) {
     18   var self = document.createElement('div');
     19   TileView.decorate(self, prepareBox, loadBox);
     20   return self;
     21 }
     22 
     23 TileView.prototype = { __proto__: HTMLDivElement.prototype };
     24 
     25 /**
     26  * The number of boxes updated at once after loading.
     27  */
     28 TileView.LOAD_CHUNK = 10;
     29 
     30 /**
     31  * The margin between the boxes (in pixels).
     32  */
     33 TileView.MARGIN = 10;
     34 
     35 /**
     36  * The delay between loading of two consecutive images.
     37  */
     38 TileView.LOAD_DELAY = 100;
     39 
     40 /**
     41  * @param {HTMLDivElement} self Element to decorate.
     42  * @param {function(TailBox, callback)} prepareBox See constructor.
     43  * @param {function(TailBox, callback)} loadBox See constructor.
     44  */
     45 TileView.decorate = function(self, prepareBox, loadBox) {
     46   self.__proto__ = TileView.prototype;
     47   self.classList.add('tile-view');
     48   self.prepareBox_ = prepareBox;
     49   self.loadBox_ = loadBox;
     50 };
     51 
     52 /**
     53  * Load and display media entries.
     54  * @param {Array.<FileEntry>} entries Entries list.
     55  */
     56 TileView.prototype.load = function(entries) {
     57   this.boxes_ = [];
     58 
     59   /**
     60    * The number of boxes for which the image size is already known.
     61    */
     62   this.preparedCount_ = 0;
     63 
     64   /**
     65    * The number of boxes already displaying the image.
     66    */
     67   this.loadedCount_ = 0;
     68 
     69   for (var index = 0; index < entries.length; index++) {
     70     var box = new TileBox(this, entries[index]);
     71     box.index = index;
     72     this.boxes_.push(box);
     73     this.prepareBox_(box, this.onBoxPrepared_.bind(this, box));
     74   }
     75 
     76   this.redraw();
     77 };
     78 
     79 /**
     80  * Redraws everything.
     81  */
     82 TileView.prototype.redraw = function() {
     83   // TODO(dgozman): if we decide to support resize or virtual scrolling,
     84   // we should save the chosen position for ready boxes, so they will not
     85   // move around.
     86 
     87   this.cellSize_ = Math.floor((this.clientHeight - 3 * TileView.MARGIN) / 2);
     88   this.textContent = '';
     89   for (var index = 0; index < this.boxes_.length; index++) {
     90     this.appendChild(this.boxes_[index]);
     91   }
     92   this.repositionBoxes_(0);
     93 };
     94 
     95 /**
     96  * This function sets positions for boxes.
     97  *
     98  * To do this we keep a 2x4 array of cells marked busy or empty.
     99  * When trying to put the next box, we choose a pattern (horizontal, vertical,
    100  * square, etc.) which fits into the empty cells and place an image there.
    101  * The preferred pattern has the same orientation as image itself. Images
    102  * with unknown size are always shown in 1x1 cell.
    103  *
    104  * @param {number} from First index.
    105  * @private
    106  */
    107 TileView.prototype.repositionBoxes_ = function(from) {
    108 
    109   var cellSize = this.cellSize_;
    110   var margin = TileView.MARGIN;
    111   var baseColAndEmpty = this.getBaseColAndEmpty_();
    112 
    113   // |empty| is a 2x4 array of busy/empty cells.
    114   var empty = baseColAndEmpty.empty;
    115 
    116   // |baseCol| is the (tileview-wide) number of first column in |empty| array.
    117   var baseCol = baseColAndEmpty.baseCol;
    118 
    119   for (var index = from; index < this.boxes_.length; index++) {
    120     while (!empty[0][0] && !empty[1][0]) {
    121       // Skip the full columns at the start.
    122       empty[0].shift();
    123       empty[0].push(true);
    124       empty[1].shift();
    125       empty[1].push(true);
    126       baseCol++;
    127     }
    128     // Here we always have an empty cell in the first column fo |empty| array,
    129     // and |baseCol| is the column number of it.
    130 
    131     var box = this.boxes_[index];
    132     var imageWidth = box.width || 0;
    133     var imageHeight = box.height || 0;
    134 
    135     // Possible positions of the box:
    136     //   p - the probability of this pattern to be used;
    137     //   w - the width of resulting image (in columns);
    138     //   h - the height of resulting image (in rows);
    139     //   allowed - whether this pattern is allowed for this particular box.
    140     var patterns = [
    141       {p: 0.2, w: 2, h: 2, allowed: index < this.preparedCount_},
    142       {p: 0.6, w: 1, h: 2, allowed: imageHeight > imageWidth &&
    143                                     index < this.preparedCount_},
    144       {p: 0.3, w: 2, h: 1, allowed: imageHeight < imageWidth &&
    145                                     index < this.preparedCount_},
    146       {p: 1.0, w: 1, h: 1, allowed: true} // Every image can be shown as 1x1.
    147     ];
    148 
    149     // The origin point is top-left empty cell, which must be in the
    150     // first column.
    151     var col = 0;
    152     var row = empty[0][0] ? 0 : 1;
    153 
    154     for (var pIndex = 0; pIndex < patterns.length; pIndex++) {
    155       var pattern = patterns[pIndex];
    156       if (Math.random() > pattern.p || !pattern.allowed) continue;
    157       if (!this.canUsePattern_(empty, row, col, pattern)) continue;
    158 
    159       // Found a pattern to use.
    160       box.rect.row = row;
    161       box.rect.col = col + baseCol;
    162       box.rect.width = pattern.w;
    163       box.rect.height = pattern.h;
    164 
    165       // Now mark the cells as busy and stop.
    166       this.usePattern_(empty, row, col, pattern);
    167       break;
    168     }
    169 
    170     box.setPositionFromRect(margin, cellSize);
    171   }
    172 };
    173 
    174 /**
    175  * @param {number} from Starting index.
    176  * @return {Object} An object containing the array of cells marked empty/busy
    177  *   and a base (left one) column number.
    178  * @private
    179  */
    180 TileView.prototype.getBaseColAndEmpty_ = function(from) {
    181   // 2x4 array indicating whether the place is empty or not.
    182   var empty = [[true, true, true, true], [true, true, true, true]];
    183   var baseCol = 0;
    184 
    185   if (from > 0) {
    186     baseCol = this.boxes_[from - 1].rect.col;
    187     if (from > 1) {
    188       baseCol = Math.min(baseCol, this.boxes_[from - 2].rect.col);
    189     }
    190 
    191     for (var b = from - 2; b < from; b++) {
    192       if (b < 0) continue;
    193       var rect = this.boxes_[b].rect;
    194       for (var i = 0; i < rect.height; i++) {
    195         for (var j = 0; j < rect.width; j++) {
    196           empty[i + rect.row][j + rect.col - baseCol] = false;
    197         }
    198       }
    199     }
    200   }
    201 
    202   return {empty: empty, baseCol: baseCol};
    203 };
    204 
    205 /**
    206  * @param {Array} empty The empty/busy cells array.
    207  * @param {number} row The origin row.
    208  * @param {number} col The origin column.
    209  * @param {Object} pattern The pattern (see |repositionBoxes_|).
    210  * @return {boolean} Whether the pattern may be used at this origin.
    211  * @private
    212  */
    213 TileView.prototype.canUsePattern_ = function(empty, row, col, pattern) {
    214   if (row + pattern.h > 2 || col + pattern.w > 4)
    215     return false;
    216 
    217   var can = true;
    218   for (var r = 0; r < pattern.h; r++) {
    219     for (var c = 0; c < pattern.w; c++) {
    220       can = can && empty[row + r][col + c];
    221     }
    222   }
    223   return can;
    224 };
    225 
    226 /**
    227  * Marks pattern's cells as busy.
    228  * @param {Array} empty The empty/busy cells array.
    229  * @param {number} row The origin row.
    230  * @param {number} col The origin column.
    231  * @param {Object} pattern The pattern (see |repositionBoxes_|).
    232  * @private
    233  */
    234 TileView.prototype.usePattern_ = function(empty, row, col, pattern) {
    235   for (var r = 0; r < pattern.h; r++) {
    236     for (var c = 0; c < pattern.w; c++) {
    237       empty[row + r][col + c] = false;
    238     }
    239   }
    240 };
    241 
    242 /**
    243  * Called when box is ready.
    244  * @param {TileBox} box The box.
    245  * @private
    246  */
    247 TileView.prototype.onBoxPrepared_ = function(box) {
    248   box.ready = true;
    249   var to = this.preparedCount_;
    250   while (to < this.boxes_.length && this.boxes_[to].ready) {
    251     to++;
    252   }
    253 
    254   if (to >= Math.min(this.preparedCount_ + TileView.LOAD_CHUNK,
    255                      this.boxes_.length)) {
    256     var last = this.preparedCount_;
    257     this.preparedCount_ = to;
    258     this.repositionBoxes_(last);
    259 
    260     if (this.loadedCount_ == last) {
    261       // All previously prepared boxes have been loaded - start the next one.
    262       var nextBox = this.boxes_[this.loadedCount_];
    263       setTimeout(this.loadBox_, TileView.LOAD_DELAY,
    264                  nextBox, this.onBoxLoaded.bind(this, nextBox));
    265     }
    266   }
    267 };
    268 
    269 /**
    270  * Called when box is loaded.
    271  * @param {TileBox} box The box.
    272  */
    273 TileView.prototype.onBoxLoaded = function(box) {
    274   if (this.loadedCount_ != box.index)
    275     console.error('inconsistent loadedCount');
    276   this.loadedCount_ = box.index + 1;
    277 
    278   var nextIndex = box.index + 1;
    279   if (nextIndex < this.preparedCount_) {
    280     var nextBox = this.boxes_[nextIndex];
    281     setTimeout(this.loadBox_, TileView.LOAD_DELAY,
    282                nextBox, this.onBoxLoaded.bind(this, nextBox));
    283   }
    284 };
    285 
    286 
    287 
    288 /**
    289  * Container for functions to work with local TileView.
    290  */
    291 TileView.local = {};
    292 
    293 /**
    294  * Decorates a TileView to show local files.
    295  * @param {HTMLDivElement} view The view.
    296  * @param {MetadataCache} metadataCache Metadata cache.
    297  */
    298 TileView.local.decorate = function(view, metadataCache) {
    299   TileView.decorate(view, TileView.local.prepareBox, TileView.local.loadBox);
    300   view.metadataCache = metadataCache;
    301 };
    302 
    303 /**
    304  * Prepares image for local tile view box.
    305  * @param {TileBox} box The box.
    306  * @param {function} callback The callback.
    307  */
    308 TileView.local.prepareBox = function(box, callback) {
    309   box.view_.metadataCache.get(box.entry, 'media', function(media) {
    310     if (!media) {
    311       box.width = 0;
    312       box.height = 0;
    313       box.imageTransform = null;
    314     } else {
    315       if (media.imageTransform && media.imageTransform.rotate90 % 2 == 1) {
    316         box.width = media.height;
    317         box.height = media.width;
    318       } else {
    319         box.width = media.width;
    320         box.height = media.height;
    321       }
    322       box.imageTransform = media.imageTransform;
    323     }
    324 
    325     callback();
    326   });
    327 };
    328 
    329 /**
    330  * Loads the image for local tile view box.
    331  * @param {TileBox} box The box.
    332  * @param {function} callback The callback.
    333  */
    334 TileView.local.loadBox = function(box, callback) {
    335   var onLoaded = function(fullCanvas) {
    336     try {
    337       var canvas = box.ownerDocument.createElement('canvas');
    338       canvas.width = box.clientWidth;
    339       canvas.height = box.clientHeight;
    340       var context = canvas.getContext('2d');
    341       context.drawImage(fullCanvas, 0, 0, canvas.width, canvas.height);
    342       box.appendChild(canvas);
    343     } catch (e) {
    344       // TODO(dgozman): classify possible exceptions here and reraise others.
    345     }
    346     callback();
    347   };
    348 
    349   var transformFetcher = function(url, onFetched) {
    350     onFetched(box.imageTransform);
    351   };
    352 
    353   var imageLoader = new ImageUtil.ImageLoader(box.ownerDocument);
    354   imageLoader.load(box.entry.toURL(), transformFetcher, onLoaded);
    355 };
    356 
    357 
    358 
    359 /**
    360  * Container for functions to work with drive TileView.
    361  */
    362 TileView.drive = {};
    363 
    364 /**
    365  * Decorates a TileView to show drive files.
    366  * @param {HTMLDivElement} view The view.
    367  * @param {MetadataCache} metadataCache Metadata cache.
    368  */
    369 TileView.drive.decorate = function(view, metadataCache) {
    370   TileView.decorate(view, TileView.drive.prepareBox, TileView.drive.loadBox);
    371   view.metadataCache = metadataCache;
    372 };
    373 
    374 /**
    375  * Prepares image for drive tile view box.
    376  * @param {TileBox} box The box.
    377  * @param {function} callback The callback.
    378  */
    379 TileView.drive.prepareBox = function(box, callback) {
    380   box.view_.metadataCache.get(box.entry, 'thumbnail', function(thumbnail) {
    381     if (!thumbnail) {
    382       box.width = 0;
    383       box.height = 0;
    384       callback();
    385       return;
    386     }
    387 
    388     // TODO(dgozman): remove this hack if we ask for larger thumbnails in
    389     // drive code.
    390     var thumbnailUrl = thumbnail.url.replace(/240$/, '512');
    391 
    392     box.image = new Image();
    393     box.image.onload = function(e) {
    394       box.width = box.image.width;
    395       box.height = box.image.height;
    396       callback();
    397     };
    398     box.image.onerror = function() {
    399       box.image = null;
    400       callback();
    401     };
    402     box.image.src = thumbnailUrl;
    403   });
    404 };
    405 
    406 /**
    407  * Loads the image for drive tile view box.
    408  * @param {TileBox} box The box.
    409  * @param {function} callback The callback.
    410  */
    411 TileView.drive.loadBox = function(box, callback) {
    412   box.appendChild(box.image);
    413   callback();
    414 };
    415 
    416 
    417 
    418 
    419 /**
    420  * Tile box is a part of tile view.
    421  * @param {TailView} view The parent view.
    422  * @param {Entry} entry Image file entry.
    423  * @constructor
    424  */
    425 function TileBox(view, entry) {
    426   var self = view.ownerDocument.createElement('div');
    427   TileBox.decorate(self, view, entry);
    428   return self;
    429 }
    430 
    431 TileBox.prototype = { __proto__: HTMLDivElement.prototype };
    432 
    433 /**
    434  * @param {HTMLDivElement} self Element to decorate.
    435  * @param {TailView} view The parent view.
    436  * @param {Entry} entry Image file entry.
    437  */
    438 TileBox.decorate = function(self, view, entry) {
    439   self.__proto__ = TileBox.prototype;
    440   self.classList.add('tile-box');
    441 
    442   self.view_ = view;
    443   self.entry = entry;
    444 
    445   self.ready = false;
    446   self.rect = {row: 0, col: 0, width: 0, height: 0};
    447 
    448   self.index = null;
    449   self.height = null;
    450   self.width = null;
    451 };
    452 
    453 /**
    454  * Sets box position according to the |rect| property and given sizes.
    455  * @param {number} margin Margin between cells.
    456  * @param {number} cellSize The size of one cell.
    457  * @constructor
    458  */
    459 TileBox.setPositionFromRect = function(margin, cellSize) {
    460   this.style.top = margin + (cellSize + margin) * this.rect.row + 'px';
    461   this.style.left = margin + (cellSize + margin) * this.rect.col + 'px';
    462   this.style.height = (cellSize + margin) * this.rect.height - margin + 'px';
    463   this.style.width = (cellSize + margin) * this.rect.width - margin + 'px';
    464 };
    465