| OLD | NEW |
| (Empty) |
| 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 }; | |
| OLD | NEW |