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