| 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 * @param {Element} container Content container. | |
| 9 * @param {cr.ui.ArrayDataModel} dataModel Data model. | |
| 10 * @param {cr.ui.ListSelectionModel} selectionModel Selection model. | |
| 11 * @param {MetadataCache} metadataCache Metadata cache. | |
| 12 * @param {VolumeManagerWrapper} volumeManager Volume manager. | |
| 13 * @param {function} toggleMode Function to switch to the Slide mode. | |
| 14 * @constructor | |
| 15 */ | |
| 16 function MosaicMode( | |
| 17 container, dataModel, selectionModel, metadataCache, volumeManager, | |
| 18 toggleMode) { | |
| 19 this.mosaic_ = new Mosaic( | |
| 20 container.ownerDocument, dataModel, selectionModel, metadataCache, | |
| 21 volumeManager); | |
| 22 container.appendChild(this.mosaic_); | |
| 23 | |
| 24 this.toggleMode_ = toggleMode; | |
| 25 this.mosaic_.addEventListener('dblclick', this.toggleMode_); | |
| 26 this.showingTimeoutID_ = null; | |
| 27 } | |
| 28 | |
| 29 /** | |
| 30 * @return {Mosaic} The mosaic control. | |
| 31 */ | |
| 32 MosaicMode.prototype.getMosaic = function() { return this.mosaic_ }; | |
| 33 | |
| 34 /** | |
| 35 * @return {string} Mode name. | |
| 36 */ | |
| 37 MosaicMode.prototype.getName = function() { return 'mosaic' }; | |
| 38 | |
| 39 /** | |
| 40 * @return {string} Mode title. | |
| 41 */ | |
| 42 MosaicMode.prototype.getTitle = function() { return 'GALLERY_MOSAIC' }; | |
| 43 | |
| 44 /** | |
| 45 * Execute an action (this mode has no busy state). | |
| 46 * @param {function} action Action to execute. | |
| 47 */ | |
| 48 MosaicMode.prototype.executeWhenReady = function(action) { action() }; | |
| 49 | |
| 50 /** | |
| 51 * @return {boolean} Always true (no toolbar fading in this mode). | |
| 52 */ | |
| 53 MosaicMode.prototype.hasActiveTool = function() { return true }; | |
| 54 | |
| 55 /** | |
| 56 * Keydown handler. | |
| 57 * | |
| 58 * @param {Event} event Event. | |
| 59 */ | |
| 60 MosaicMode.prototype.onKeyDown = function(event) { | |
| 61 switch (util.getKeyModifiers(event) + event.keyIdentifier) { | |
| 62 case 'Enter': | |
| 63 if (!document.activeElement || | |
| 64 document.activeElement.localName !== 'button') { | |
| 65 this.toggleMode_(); | |
| 66 event.preventDefault(); | |
| 67 } | |
| 68 return; | |
| 69 } | |
| 70 this.mosaic_.onKeyDown(event); | |
| 71 }; | |
| 72 | |
| 73 //////////////////////////////////////////////////////////////////////////////// | |
| 74 | |
| 75 /** | |
| 76 * Mosaic control. | |
| 77 * | |
| 78 * @param {Document} document Document. | |
| 79 * @param {cr.ui.ArrayDataModel} dataModel Data model. | |
| 80 * @param {cr.ui.ListSelectionModel} selectionModel Selection model. | |
| 81 * @param {MetadataCache} metadataCache Metadata cache. | |
| 82 * @param {VolumeManagerWrapper} volumeManager Volume manager. | |
| 83 * @return {Element} Mosaic element. | |
| 84 * @constructor | |
| 85 */ | |
| 86 function Mosaic(document, dataModel, selectionModel, metadataCache, | |
| 87 volumeManager) { | |
| 88 var self = document.createElement('div'); | |
| 89 Mosaic.decorate( | |
| 90 self, dataModel, selectionModel, metadataCache, volumeManager); | |
| 91 return self; | |
| 92 } | |
| 93 | |
| 94 /** | |
| 95 * Inherits from HTMLDivElement. | |
| 96 */ | |
| 97 Mosaic.prototype.__proto__ = HTMLDivElement.prototype; | |
| 98 | |
| 99 /** | |
| 100 * Default layout delay in ms. | |
| 101 * @const | |
| 102 * @type {number} | |
| 103 */ | |
| 104 Mosaic.LAYOUT_DELAY = 200; | |
| 105 | |
| 106 /** | |
| 107 * Smooth scroll animation duration when scrolling using keyboard or | |
| 108 * clicking on a partly visible tile. In ms. | |
| 109 * @const | |
| 110 * @type {number} | |
| 111 */ | |
| 112 Mosaic.ANIMATED_SCROLL_DURATION = 500; | |
| 113 | |
| 114 /** | |
| 115 * Decorates a Mosaic instance. | |
| 116 * | |
| 117 * @param {Mosaic} self Self pointer. | |
| 118 * @param {cr.ui.ArrayDataModel} dataModel Data model. | |
| 119 * @param {cr.ui.ListSelectionModel} selectionModel Selection model. | |
| 120 * @param {MetadataCache} metadataCache Metadata cache. | |
| 121 * @param {VolumeManagerWrapper} volumeManager Volume manager. | |
| 122 */ | |
| 123 Mosaic.decorate = function( | |
| 124 self, dataModel, selectionModel, metadataCache, volumeManager) { | |
| 125 self.__proto__ = Mosaic.prototype; | |
| 126 self.className = 'mosaic'; | |
| 127 | |
| 128 self.dataModel_ = dataModel; | |
| 129 self.selectionModel_ = selectionModel; | |
| 130 self.metadataCache_ = metadataCache; | |
| 131 self.volumeManager_ = volumeManager; | |
| 132 | |
| 133 // Initialization is completed lazily on the first call to |init|. | |
| 134 }; | |
| 135 | |
| 136 /** | |
| 137 * Initializes the mosaic element. | |
| 138 */ | |
| 139 Mosaic.prototype.init = function() { | |
| 140 if (this.tiles_) | |
| 141 return; // Already initialized, nothing to do. | |
| 142 | |
| 143 this.layoutModel_ = new Mosaic.Layout(); | |
| 144 this.onResize_(); | |
| 145 | |
| 146 this.selectionController_ = | |
| 147 new Mosaic.SelectionController(this.selectionModel_, this.layoutModel_); | |
| 148 | |
| 149 this.tiles_ = []; | |
| 150 for (var i = 0; i !== this.dataModel_.length; i++) { | |
| 151 var locationInfo = | |
| 152 this.volumeManager_.getLocationInfo(this.dataModel_.item(i).getEntry()); | |
| 153 this.tiles_.push( | |
| 154 new Mosaic.Tile(this, this.dataModel_.item(i), locationInfo)); | |
| 155 } | |
| 156 | |
| 157 this.selectionModel_.selectedIndexes.forEach(function(index) { | |
| 158 this.tiles_[index].select(true); | |
| 159 }.bind(this)); | |
| 160 | |
| 161 this.initTiles_(this.tiles_); | |
| 162 | |
| 163 // The listeners might be called while some tiles are still loading. | |
| 164 this.initListeners_(); | |
| 165 }; | |
| 166 | |
| 167 /** | |
| 168 * @return {boolean} Whether mosaic is initialized. | |
| 169 */ | |
| 170 Mosaic.prototype.isInitialized = function() { | |
| 171 return !!this.tiles_; | |
| 172 }; | |
| 173 | |
| 174 /** | |
| 175 * Starts listening to events. | |
| 176 * | |
| 177 * We keep listening to events even when the mosaic is hidden in order to | |
| 178 * keep the layout up to date. | |
| 179 * | |
| 180 * @private | |
| 181 */ | |
| 182 Mosaic.prototype.initListeners_ = function() { | |
| 183 this.ownerDocument.defaultView.addEventListener( | |
| 184 'resize', this.onResize_.bind(this)); | |
| 185 | |
| 186 var mouseEventBound = this.onMouseEvent_.bind(this); | |
| 187 this.addEventListener('mousemove', mouseEventBound); | |
| 188 this.addEventListener('mousedown', mouseEventBound); | |
| 189 this.addEventListener('mouseup', mouseEventBound); | |
| 190 this.addEventListener('scroll', this.onScroll_.bind(this)); | |
| 191 | |
| 192 this.selectionModel_.addEventListener('change', this.onSelection_.bind(this)); | |
| 193 this.selectionModel_.addEventListener('leadIndexChange', | |
| 194 this.onLeadChange_.bind(this)); | |
| 195 | |
| 196 this.dataModel_.addEventListener('splice', this.onSplice_.bind(this)); | |
| 197 this.dataModel_.addEventListener('content', this.onContentChange_.bind(this)); | |
| 198 }; | |
| 199 | |
| 200 /** | |
| 201 * Smoothly scrolls the container to the specified position using | |
| 202 * f(x) = sqrt(x) speed function normalized to animation duration. | |
| 203 * @param {number} targetPosition Horizontal scroll position in pixels. | |
| 204 */ | |
| 205 Mosaic.prototype.animatedScrollTo = function(targetPosition) { | |
| 206 if (this.scrollAnimation_) { | |
| 207 webkitCancelAnimationFrame(this.scrollAnimation_); | |
| 208 this.scrollAnimation_ = null; | |
| 209 } | |
| 210 | |
| 211 // Mouse move events are fired without touching the mouse because of scrolling | |
| 212 // the container. Therefore, these events have to be suppressed. | |
| 213 this.suppressHovering_ = true; | |
| 214 | |
| 215 // Calculates integral area from t1 to t2 of f(x) = sqrt(x) dx. | |
| 216 var integral = function(t1, t2) { | |
| 217 return 2.0 / 3.0 * Math.pow(t2, 3.0 / 2.0) - | |
| 218 2.0 / 3.0 * Math.pow(t1, 3.0 / 2.0); | |
| 219 }; | |
| 220 | |
| 221 var delta = targetPosition - this.scrollLeft; | |
| 222 var factor = delta / integral(0, Mosaic.ANIMATED_SCROLL_DURATION); | |
| 223 var startTime = Date.now(); | |
| 224 var lastPosition = 0; | |
| 225 var scrollOffset = this.scrollLeft; | |
| 226 | |
| 227 var animationFrame = function() { | |
| 228 var position = Date.now() - startTime; | |
| 229 var step = factor * | |
| 230 integral(Math.max(0, Mosaic.ANIMATED_SCROLL_DURATION - position), | |
| 231 Math.max(0, Mosaic.ANIMATED_SCROLL_DURATION - lastPosition)); | |
| 232 scrollOffset += step; | |
| 233 | |
| 234 var oldScrollLeft = this.scrollLeft; | |
| 235 var newScrollLeft = Math.round(scrollOffset); | |
| 236 | |
| 237 if (oldScrollLeft !== newScrollLeft) | |
| 238 this.scrollLeft = newScrollLeft; | |
| 239 | |
| 240 if (step === 0 || this.scrollLeft !== newScrollLeft) { | |
| 241 this.scrollAnimation_ = null; | |
| 242 // Release the hovering lock after a safe delay to avoid hovering | |
| 243 // a tile because of altering |this.scrollLeft|. | |
| 244 setTimeout(function() { | |
| 245 if (!this.scrollAnimation_) | |
| 246 this.suppressHovering_ = false; | |
| 247 }.bind(this), 100); | |
| 248 } else { | |
| 249 // Continue the animation. | |
| 250 this.scrollAnimation_ = requestAnimationFrame(animationFrame); | |
| 251 } | |
| 252 | |
| 253 lastPosition = position; | |
| 254 }.bind(this); | |
| 255 | |
| 256 // Start the animation. | |
| 257 this.scrollAnimation_ = requestAnimationFrame(animationFrame); | |
| 258 }; | |
| 259 | |
| 260 /** | |
| 261 * @return {Mosaic.Tile} Selected tile or undefined if no selection. | |
| 262 */ | |
| 263 Mosaic.prototype.getSelectedTile = function() { | |
| 264 return this.tiles_ && this.tiles_[this.selectionModel_.selectedIndex]; | |
| 265 }; | |
| 266 | |
| 267 /** | |
| 268 * @param {number} index Tile index. | |
| 269 * @return {Rect} Tile's image rectangle. | |
| 270 */ | |
| 271 Mosaic.prototype.getTileRect = function(index) { | |
| 272 var tile = this.tiles_[index]; | |
| 273 return tile && tile.getImageRect(); | |
| 274 }; | |
| 275 | |
| 276 /** | |
| 277 * @param {number} index Tile index. | |
| 278 * Scroll the given tile into the viewport. | |
| 279 */ | |
| 280 Mosaic.prototype.scrollIntoView = function(index) { | |
| 281 var tile = this.tiles_[index]; | |
| 282 if (tile) tile.scrollIntoView(); | |
| 283 }; | |
| 284 | |
| 285 /** | |
| 286 * Initializes multiple tiles. | |
| 287 * | |
| 288 * @param {Array.<Mosaic.Tile>} tiles Array of tiles. | |
| 289 * @param {function()=} opt_callback Completion callback. | |
| 290 * @private | |
| 291 */ | |
| 292 Mosaic.prototype.initTiles_ = function(tiles, opt_callback) { | |
| 293 // We do not want to use tile indices in asynchronous operations because they | |
| 294 // do not survive data model splices. Copy tile references instead. | |
| 295 tiles = tiles.slice(); | |
| 296 | |
| 297 // Throttle the metadata access so that we do not overwhelm the file system. | |
| 298 var MAX_CHUNK_SIZE = 10; | |
| 299 | |
| 300 var loadChunk = function() { | |
| 301 if (!tiles.length) { | |
| 302 if (opt_callback) opt_callback(); | |
| 303 return; | |
| 304 } | |
| 305 var chunkSize = Math.min(tiles.length, MAX_CHUNK_SIZE); | |
| 306 var loaded = 0; | |
| 307 for (var i = 0; i !== chunkSize; i++) { | |
| 308 this.initTile_(tiles.shift(), function() { | |
| 309 if (++loaded === chunkSize) { | |
| 310 this.layout(); | |
| 311 loadChunk(); | |
| 312 } | |
| 313 }.bind(this)); | |
| 314 } | |
| 315 }.bind(this); | |
| 316 | |
| 317 loadChunk(); | |
| 318 }; | |
| 319 | |
| 320 /** | |
| 321 * Initializes a single tile. | |
| 322 * | |
| 323 * @param {Mosaic.Tile} tile Tile. | |
| 324 * @param {function()} callback Completion callback. | |
| 325 * @private | |
| 326 */ | |
| 327 Mosaic.prototype.initTile_ = function(tile, callback) { | |
| 328 var onImageMeasured = callback; | |
| 329 this.metadataCache_.get(tile.getItem().getEntry(), Gallery.METADATA_TYPE, | |
| 330 function(metadata) { | |
| 331 tile.init(metadata, onImageMeasured); | |
| 332 }); | |
| 333 }; | |
| 334 | |
| 335 /** | |
| 336 * Reloads all tiles. | |
| 337 */ | |
| 338 Mosaic.prototype.reload = function() { | |
| 339 this.layoutModel_.reset_(); | |
| 340 this.tiles_.forEach(function(t) { t.markUnloaded() }); | |
| 341 this.initTiles_(this.tiles_); | |
| 342 }; | |
| 343 | |
| 344 /** | |
| 345 * Layouts the tiles in the order of their indices. | |
| 346 * | |
| 347 * Starts where it last stopped (at #0 the first time). | |
| 348 * Stops when all tiles are processed or when the next tile is still loading. | |
| 349 */ | |
| 350 Mosaic.prototype.layout = function() { | |
| 351 if (this.layoutTimer_) { | |
| 352 clearTimeout(this.layoutTimer_); | |
| 353 this.layoutTimer_ = null; | |
| 354 } | |
| 355 while (true) { | |
| 356 var index = this.layoutModel_.getTileCount(); | |
| 357 if (index === this.tiles_.length) | |
| 358 break; // All tiles done. | |
| 359 var tile = this.tiles_[index]; | |
| 360 if (!tile.isInitialized()) | |
| 361 break; // Next layout will try to restart from here. | |
| 362 this.layoutModel_.add(tile, index + 1 === this.tiles_.length); | |
| 363 } | |
| 364 this.loadVisibleTiles_(); | |
| 365 }; | |
| 366 | |
| 367 /** | |
| 368 * Schedules the layout. | |
| 369 * | |
| 370 * @param {number=} opt_delay Delay in ms. | |
| 371 */ | |
| 372 Mosaic.prototype.scheduleLayout = function(opt_delay) { | |
| 373 if (!this.layoutTimer_) { | |
| 374 this.layoutTimer_ = setTimeout(function() { | |
| 375 this.layoutTimer_ = null; | |
| 376 this.layout(); | |
| 377 }.bind(this), opt_delay || 0); | |
| 378 } | |
| 379 }; | |
| 380 | |
| 381 /** | |
| 382 * Resize handler. | |
| 383 * | |
| 384 * @private | |
| 385 */ | |
| 386 Mosaic.prototype.onResize_ = function() { | |
| 387 this.layoutModel_.setViewportSize(this.clientWidth, this.clientHeight - | |
| 388 (Mosaic.Layout.PADDING_TOP + Mosaic.Layout.PADDING_BOTTOM)); | |
| 389 this.scheduleLayout(); | |
| 390 }; | |
| 391 | |
| 392 /** | |
| 393 * Mouse event handler. | |
| 394 * | |
| 395 * @param {Event} event Event. | |
| 396 * @private | |
| 397 */ | |
| 398 Mosaic.prototype.onMouseEvent_ = function(event) { | |
| 399 // Navigating with mouse, enable hover state. | |
| 400 if (!this.suppressHovering_) | |
| 401 this.classList.add('hover-visible'); | |
| 402 | |
| 403 if (event.type === 'mousemove') | |
| 404 return; | |
| 405 | |
| 406 var index = -1; | |
| 407 for (var target = event.target; | |
| 408 target && (target !== this); | |
| 409 target = target.parentNode) { | |
| 410 if (target.classList.contains('mosaic-tile')) { | |
| 411 index = this.dataModel_.indexOf(target.getItem()); | |
| 412 break; | |
| 413 } | |
| 414 } | |
| 415 this.selectionController_.handlePointerDownUp(event, index); | |
| 416 }; | |
| 417 | |
| 418 /** | |
| 419 * Scroll handler. | |
| 420 * @private | |
| 421 */ | |
| 422 Mosaic.prototype.onScroll_ = function() { | |
| 423 requestAnimationFrame(function() { | |
| 424 this.loadVisibleTiles_(); | |
| 425 }.bind(this)); | |
| 426 }; | |
| 427 | |
| 428 /** | |
| 429 * Selection change handler. | |
| 430 * | |
| 431 * @param {Event} event Event. | |
| 432 * @private | |
| 433 */ | |
| 434 Mosaic.prototype.onSelection_ = function(event) { | |
| 435 for (var i = 0; i !== event.changes.length; i++) { | |
| 436 var change = event.changes[i]; | |
| 437 var tile = this.tiles_[change.index]; | |
| 438 if (tile) tile.select(change.selected); | |
| 439 } | |
| 440 }; | |
| 441 | |
| 442 /** | |
| 443 * Leads item change handler. | |
| 444 * | |
| 445 * @param {Event} event Event. | |
| 446 * @private | |
| 447 */ | |
| 448 Mosaic.prototype.onLeadChange_ = function(event) { | |
| 449 var index = event.newValue; | |
| 450 if (index >= 0) { | |
| 451 var tile = this.tiles_[index]; | |
| 452 if (tile) tile.scrollIntoView(); | |
| 453 } | |
| 454 }; | |
| 455 | |
| 456 /** | |
| 457 * Splice event handler. | |
| 458 * | |
| 459 * @param {Event} event Event. | |
| 460 * @private | |
| 461 */ | |
| 462 Mosaic.prototype.onSplice_ = function(event) { | |
| 463 var index = event.index; | |
| 464 this.layoutModel_.invalidateFromTile_(index); | |
| 465 | |
| 466 if (event.removed.length) { | |
| 467 for (var t = 0; t !== event.removed.length; t++) { | |
| 468 // If the layout for the tile has not done yet, the parent is null. | |
| 469 // And the layout will not be done after onSplice_ becuase it is removed | |
| 470 // from this.tiles_. | |
| 471 if (this.tiles_[index + t].parentNode) | |
| 472 this.removeChild(this.tiles_[index + t]); | |
| 473 } | |
| 474 | |
| 475 this.tiles_.splice(index, event.removed.length); | |
| 476 this.scheduleLayout(Mosaic.LAYOUT_DELAY); | |
| 477 } | |
| 478 | |
| 479 if (event.added.length) { | |
| 480 var newTiles = []; | |
| 481 for (var t = 0; t !== event.added.length; t++) | |
| 482 newTiles.push(new Mosaic.Tile(this, this.dataModel_.item(index + t))); | |
| 483 | |
| 484 this.tiles_.splice.apply(this.tiles_, [index, 0].concat(newTiles)); | |
| 485 this.initTiles_(newTiles); | |
| 486 } | |
| 487 | |
| 488 if (this.tiles_.length !== this.dataModel_.length) | |
| 489 console.error('Mosaic is out of sync'); | |
| 490 }; | |
| 491 | |
| 492 /** | |
| 493 * Content change handler. | |
| 494 * | |
| 495 * @param {Event} event Event. | |
| 496 * @private | |
| 497 */ | |
| 498 Mosaic.prototype.onContentChange_ = function(event) { | |
| 499 if (!this.tiles_) | |
| 500 return; | |
| 501 | |
| 502 if (!event.metadata) | |
| 503 return; // Thumbnail unchanged, nothing to do. | |
| 504 | |
| 505 var index = this.dataModel_.indexOf(event.item); | |
| 506 if (index !== this.selectionModel_.selectedIndex) | |
| 507 console.error('Content changed for unselected item'); | |
| 508 | |
| 509 this.layoutModel_.invalidateFromTile_(index); | |
| 510 this.tiles_[index].init(event.metadata, function() { | |
| 511 this.tiles_[index].unload(); | |
| 512 this.tiles_[index].load( | |
| 513 Mosaic.Tile.LoadMode.HIGH_DPI, | |
| 514 this.scheduleLayout.bind(this, Mosaic.LAYOUT_DELAY)); | |
| 515 }.bind(this)); | |
| 516 }; | |
| 517 | |
| 518 /** | |
| 519 * Keydown event handler. | |
| 520 * | |
| 521 * @param {Event} event Event. | |
| 522 * @return {boolean} True if the event has been consumed. | |
| 523 */ | |
| 524 Mosaic.prototype.onKeyDown = function(event) { | |
| 525 this.selectionController_.handleKeyDown(event); | |
| 526 if (event.defaultPrevented) // Navigating with keyboard, hide hover state. | |
| 527 this.classList.remove('hover-visible'); | |
| 528 return event.defaultPrevented; | |
| 529 }; | |
| 530 | |
| 531 /** | |
| 532 * @return {boolean} True if the mosaic zoom effect can be applied. It is | |
| 533 * too slow if there are to many images. | |
| 534 * TODO(kaznacheev): Consider unloading the images that are out of the viewport. | |
| 535 */ | |
| 536 Mosaic.prototype.canZoom = function() { | |
| 537 return this.tiles_.length < 100; | |
| 538 }; | |
| 539 | |
| 540 /** | |
| 541 * Shows the mosaic. | |
| 542 */ | |
| 543 Mosaic.prototype.show = function() { | |
| 544 var duration = ImageView.MODE_TRANSITION_DURATION; | |
| 545 if (this.canZoom()) { | |
| 546 // Fade in in parallel with the zoom effect. | |
| 547 this.setAttribute('visible', 'zooming'); | |
| 548 } else { | |
| 549 // Mosaic is not animating but the large image is. Fade in the mosaic | |
| 550 // shortly before the large image animation is done. | |
| 551 duration -= 100; | |
| 552 } | |
| 553 this.showingTimeoutID_ = setTimeout(function() { | |
| 554 this.showingTimeoutID_ = null; | |
| 555 // Make the selection visible. | |
| 556 // If the mosaic is not animated it will start fading in now. | |
| 557 this.setAttribute('visible', 'normal'); | |
| 558 this.loadVisibleTiles_(); | |
| 559 }.bind(this), duration); | |
| 560 }; | |
| 561 | |
| 562 /** | |
| 563 * Hides the mosaic. | |
| 564 */ | |
| 565 Mosaic.prototype.hide = function() { | |
| 566 if (this.showingTimeoutID_ !== null) { | |
| 567 clearTimeout(this.showingTimeoutID_); | |
| 568 this.showingTimeoutID_ = null; | |
| 569 } | |
| 570 this.removeAttribute('visible'); | |
| 571 }; | |
| 572 | |
| 573 /** | |
| 574 * Checks if the mosaic view is visible. | |
| 575 * @return {boolean} True if visible, false otherwise. | |
| 576 * @private | |
| 577 */ | |
| 578 Mosaic.prototype.isVisible_ = function() { | |
| 579 return this.hasAttribute('visible'); | |
| 580 }; | |
| 581 | |
| 582 /** | |
| 583 * Loads visible tiles. Ignores consecutive calls. Does not reload already | |
| 584 * loaded images. | |
| 585 * @private | |
| 586 */ | |
| 587 Mosaic.prototype.loadVisibleTiles_ = function() { | |
| 588 if (this.loadVisibleTilesSuppressed_) { | |
| 589 this.loadVisibleTilesScheduled_ = true; | |
| 590 return; | |
| 591 } | |
| 592 | |
| 593 this.loadVisibleTilesSuppressed_ = true; | |
| 594 this.loadVisibleTilesScheduled_ = false; | |
| 595 setTimeout(function() { | |
| 596 this.loadVisibleTilesSuppressed_ = false; | |
| 597 if (this.loadVisibleTilesScheduled_) | |
| 598 this.loadVisibleTiles_(); | |
| 599 }.bind(this), 100); | |
| 600 | |
| 601 // Tiles only in the viewport (visible). | |
| 602 var visibleRect = new Rect(0, | |
| 603 0, | |
| 604 this.clientWidth, | |
| 605 this.clientHeight); | |
| 606 | |
| 607 // Tiles in the viewport and also some distance on the left and right. | |
| 608 var renderableRect = new Rect(-this.clientWidth, | |
| 609 0, | |
| 610 3 * this.clientWidth, | |
| 611 this.clientHeight); | |
| 612 | |
| 613 // Unload tiles out of scope. | |
| 614 for (var index = 0; index < this.tiles_.length; index++) { | |
| 615 var tile = this.tiles_[index]; | |
| 616 var imageRect = tile.getImageRect(); | |
| 617 // Unload a thumbnail. | |
| 618 if (imageRect && !imageRect.intersects(renderableRect)) | |
| 619 tile.unload(); | |
| 620 } | |
| 621 | |
| 622 // Load the visible tiles first. | |
| 623 var allVisibleLoaded = true; | |
| 624 // Show high-dpi only when the mosaic view is visible. | |
| 625 var loadMode = this.isVisible_() ? Mosaic.Tile.LoadMode.HIGH_DPI : | |
| 626 Mosaic.Tile.LoadMode.LOW_DPI; | |
| 627 for (var index = 0; index < this.tiles_.length; index++) { | |
| 628 var tile = this.tiles_[index]; | |
| 629 var imageRect = tile.getImageRect(); | |
| 630 // Load a thumbnail. | |
| 631 if (!tile.isLoading(loadMode) && !tile.isLoaded(loadMode) && imageRect && | |
| 632 imageRect.intersects(visibleRect)) { | |
| 633 tile.load(loadMode, function() {}); | |
| 634 allVisibleLoaded = false; | |
| 635 } | |
| 636 } | |
| 637 | |
| 638 // Load also another, nearby, if the visible has been already loaded. | |
| 639 if (allVisibleLoaded) { | |
| 640 for (var index = 0; index < this.tiles_.length; index++) { | |
| 641 var tile = this.tiles_[index]; | |
| 642 var imageRect = tile.getImageRect(); | |
| 643 // Load a thumbnail. | |
| 644 if (!tile.isLoading() && !tile.isLoaded() && imageRect && | |
| 645 imageRect.intersects(renderableRect)) { | |
| 646 tile.load(Mosaic.Tile.LoadMode.LOW_DPI, function() {}); | |
| 647 } | |
| 648 } | |
| 649 } | |
| 650 }; | |
| 651 | |
| 652 /** | |
| 653 * Applies reset the zoom transform. | |
| 654 * | |
| 655 * @param {Rect} tileRect Tile rectangle. Reset the transform if null. | |
| 656 * @param {Rect} imageRect Large image rectangle. Reset the transform if null. | |
| 657 * @param {boolean=} opt_instant True of the transition should be instant. | |
| 658 */ | |
| 659 Mosaic.prototype.transform = function(tileRect, imageRect, opt_instant) { | |
| 660 if (opt_instant) { | |
| 661 this.style.webkitTransitionDuration = '0'; | |
| 662 } else { | |
| 663 this.style.webkitTransitionDuration = | |
| 664 ImageView.MODE_TRANSITION_DURATION + 'ms'; | |
| 665 } | |
| 666 | |
| 667 if (this.canZoom() && tileRect && imageRect) { | |
| 668 var scaleX = imageRect.width / tileRect.width; | |
| 669 var scaleY = imageRect.height / tileRect.height; | |
| 670 var shiftX = (imageRect.left + imageRect.width / 2) - | |
| 671 (tileRect.left + tileRect.width / 2); | |
| 672 var shiftY = (imageRect.top + imageRect.height / 2) - | |
| 673 (tileRect.top + tileRect.height / 2); | |
| 674 this.style.webkitTransform = | |
| 675 'translate(' + shiftX * scaleX + 'px, ' + shiftY * scaleY + 'px)' + | |
| 676 'scaleX(' + scaleX + ') scaleY(' + scaleY + ')'; | |
| 677 } else { | |
| 678 this.style.webkitTransform = ''; | |
| 679 } | |
| 680 }; | |
| 681 | |
| 682 //////////////////////////////////////////////////////////////////////////////// | |
| 683 | |
| 684 /** | |
| 685 * Creates a selection controller that is to be used with grid. | |
| 686 * @param {cr.ui.ListSelectionModel} selectionModel The selection model to | |
| 687 * interact with. | |
| 688 * @param {Mosaic.Layout} layoutModel The layout model to use. | |
| 689 * @constructor | |
| 690 * @extends {!cr.ui.ListSelectionController} | |
| 691 */ | |
| 692 Mosaic.SelectionController = function(selectionModel, layoutModel) { | |
| 693 cr.ui.ListSelectionController.call(this, selectionModel); | |
| 694 this.layoutModel_ = layoutModel; | |
| 695 }; | |
| 696 | |
| 697 /** | |
| 698 * Extends cr.ui.ListSelectionController. | |
| 699 */ | |
| 700 Mosaic.SelectionController.prototype.__proto__ = | |
| 701 cr.ui.ListSelectionController.prototype; | |
| 702 | |
| 703 /** @override */ | |
| 704 Mosaic.SelectionController.prototype.getLastIndex = function() { | |
| 705 return this.layoutModel_.getLaidOutTileCount() - 1; | |
| 706 }; | |
| 707 | |
| 708 /** @override */ | |
| 709 Mosaic.SelectionController.prototype.getIndexBefore = function(index) { | |
| 710 return this.layoutModel_.getHorizontalAdjacentIndex(index, -1); | |
| 711 }; | |
| 712 | |
| 713 /** @override */ | |
| 714 Mosaic.SelectionController.prototype.getIndexAfter = function(index) { | |
| 715 return this.layoutModel_.getHorizontalAdjacentIndex(index, 1); | |
| 716 }; | |
| 717 | |
| 718 /** @override */ | |
| 719 Mosaic.SelectionController.prototype.getIndexAbove = function(index) { | |
| 720 return this.layoutModel_.getVerticalAdjacentIndex(index, -1); | |
| 721 }; | |
| 722 | |
| 723 /** @override */ | |
| 724 Mosaic.SelectionController.prototype.getIndexBelow = function(index) { | |
| 725 return this.layoutModel_.getVerticalAdjacentIndex(index, 1); | |
| 726 }; | |
| 727 | |
| 728 //////////////////////////////////////////////////////////////////////////////// | |
| 729 | |
| 730 /** | |
| 731 * Mosaic layout. | |
| 732 * | |
| 733 * @param {string=} opt_mode Layout mode. | |
| 734 * @param {Mosaic.Density=} opt_maxDensity Layout density. | |
| 735 * @constructor | |
| 736 */ | |
| 737 Mosaic.Layout = function(opt_mode, opt_maxDensity) { | |
| 738 this.mode_ = opt_mode || Mosaic.Layout.MODE_TENTATIVE; | |
| 739 this.maxDensity_ = opt_maxDensity || Mosaic.Density.createHighest(); | |
| 740 this.reset_(); | |
| 741 }; | |
| 742 | |
| 743 /** | |
| 744 * Blank space at the top of the mosaic element. We do not do that in CSS | |
| 745 * to make transition effects easier. | |
| 746 */ | |
| 747 Mosaic.Layout.PADDING_TOP = 50; | |
| 748 | |
| 749 /** | |
| 750 * Blank space at the bottom of the mosaic element. | |
| 751 */ | |
| 752 Mosaic.Layout.PADDING_BOTTOM = 50; | |
| 753 | |
| 754 /** | |
| 755 * Horizontal and vertical spacing between images. Should be kept in sync | |
| 756 * with the style of .mosaic-item in gallery.css (= 2 * ( 4 + 1)) | |
| 757 */ | |
| 758 Mosaic.Layout.SPACING = 10; | |
| 759 | |
| 760 /** | |
| 761 * Margin for scrolling using keyboard. Distance between a selected tile | |
| 762 * and window border. | |
| 763 */ | |
| 764 Mosaic.Layout.SCROLL_MARGIN = 30; | |
| 765 | |
| 766 /** | |
| 767 * Layout mode: commit to DOM immediately. | |
| 768 */ | |
| 769 Mosaic.Layout.MODE_FINAL = 'final'; | |
| 770 | |
| 771 /** | |
| 772 * Layout mode: do not commit layout to DOM until it is complete or the viewport | |
| 773 * overflows. | |
| 774 */ | |
| 775 Mosaic.Layout.MODE_TENTATIVE = 'tentative'; | |
| 776 | |
| 777 /** | |
| 778 * Layout mode: never commit layout to DOM. | |
| 779 */ | |
| 780 Mosaic.Layout.MODE_DRY_RUN = 'dry_run'; | |
| 781 | |
| 782 /** | |
| 783 * Resets the layout. | |
| 784 * | |
| 785 * @private | |
| 786 */ | |
| 787 Mosaic.Layout.prototype.reset_ = function() { | |
| 788 this.columns_ = []; | |
| 789 this.newColumn_ = null; | |
| 790 this.density_ = Mosaic.Density.createLowest(); | |
| 791 if (this.mode_ !== Mosaic.Layout.MODE_DRY_RUN) // DRY_RUN is sticky. | |
| 792 this.mode_ = Mosaic.Layout.MODE_TENTATIVE; | |
| 793 }; | |
| 794 | |
| 795 /** | |
| 796 * @param {number} width Viewport width. | |
| 797 * @param {number} height Viewport height. | |
| 798 */ | |
| 799 Mosaic.Layout.prototype.setViewportSize = function(width, height) { | |
| 800 this.viewportWidth_ = width; | |
| 801 this.viewportHeight_ = height; | |
| 802 this.reset_(); | |
| 803 }; | |
| 804 | |
| 805 /** | |
| 806 * @return {number} Total width of the layout. | |
| 807 */ | |
| 808 Mosaic.Layout.prototype.getWidth = function() { | |
| 809 var lastColumn = this.getLastColumn_(); | |
| 810 return lastColumn ? lastColumn.getRight() : 0; | |
| 811 }; | |
| 812 | |
| 813 /** | |
| 814 * @return {number} Total height of the layout. | |
| 815 */ | |
| 816 Mosaic.Layout.prototype.getHeight = function() { | |
| 817 var firstColumn = this.columns_[0]; | |
| 818 return firstColumn ? firstColumn.getHeight() : 0; | |
| 819 }; | |
| 820 | |
| 821 /** | |
| 822 * @return {Array.<Mosaic.Tile>} All tiles in the layout. | |
| 823 */ | |
| 824 Mosaic.Layout.prototype.getTiles = function() { | |
| 825 return Array.prototype.concat.apply([], | |
| 826 this.columns_.map(function(c) { return c.getTiles() })); | |
| 827 }; | |
| 828 | |
| 829 /** | |
| 830 * @return {number} Total number of tiles added to the layout. | |
| 831 */ | |
| 832 Mosaic.Layout.prototype.getTileCount = function() { | |
| 833 return this.getLaidOutTileCount() + | |
| 834 (this.newColumn_ ? this.newColumn_.getTileCount() : 0); | |
| 835 }; | |
| 836 | |
| 837 /** | |
| 838 * @return {Mosaic.Column} The last column or null for empty layout. | |
| 839 * @private | |
| 840 */ | |
| 841 Mosaic.Layout.prototype.getLastColumn_ = function() { | |
| 842 return this.columns_.length ? this.columns_[this.columns_.length - 1] : null; | |
| 843 }; | |
| 844 | |
| 845 /** | |
| 846 * @return {number} Total number of tiles in completed columns. | |
| 847 */ | |
| 848 Mosaic.Layout.prototype.getLaidOutTileCount = function() { | |
| 849 var lastColumn = this.getLastColumn_(); | |
| 850 return lastColumn ? lastColumn.getNextTileIndex() : 0; | |
| 851 }; | |
| 852 | |
| 853 /** | |
| 854 * Adds a tile to the layout. | |
| 855 * | |
| 856 * @param {Mosaic.Tile} tile The tile to be added. | |
| 857 * @param {boolean} isLast True if this tile is the last. | |
| 858 */ | |
| 859 Mosaic.Layout.prototype.add = function(tile, isLast) { | |
| 860 var layoutQueue = [tile]; | |
| 861 | |
| 862 // There are two levels of backtracking in the layout algorithm. | |
| 863 // |Mosaic.Layout.density_| tracks the state of the 'global' backtracking | |
| 864 // which aims to use as much of the viewport space as possible. | |
| 865 // It starts with the lowest density and increases it until the layout | |
| 866 // fits into the viewport. If it does not fit even at the highest density, | |
| 867 // the layout continues with the highest density. | |
| 868 // | |
| 869 // |Mosaic.Column.density_| tracks the state of the 'local' backtracking | |
| 870 // which aims to avoid producing unnaturally looking columns. | |
| 871 // It starts with the current global density and decreases it until the column | |
| 872 // looks nice. | |
| 873 | |
| 874 while (layoutQueue.length) { | |
| 875 if (!this.newColumn_) { | |
| 876 var lastColumn = this.getLastColumn_(); | |
| 877 this.newColumn_ = new Mosaic.Column( | |
| 878 this.columns_.length, | |
| 879 lastColumn ? lastColumn.getNextRowIndex() : 0, | |
| 880 lastColumn ? lastColumn.getNextTileIndex() : 0, | |
| 881 lastColumn ? lastColumn.getRight() : 0, | |
| 882 this.viewportHeight_, | |
| 883 this.density_.clone()); | |
| 884 } | |
| 885 | |
| 886 this.newColumn_.add(layoutQueue.shift()); | |
| 887 | |
| 888 var isFinalColumn = isLast && !layoutQueue.length; | |
| 889 | |
| 890 if (!this.newColumn_.prepareLayout(isFinalColumn)) | |
| 891 continue; // Column is incomplete. | |
| 892 | |
| 893 if (this.newColumn_.isSuboptimal()) { | |
| 894 layoutQueue = this.newColumn_.getTiles().concat(layoutQueue); | |
| 895 this.newColumn_.retryWithLowerDensity(); | |
| 896 continue; | |
| 897 } | |
| 898 | |
| 899 this.columns_.push(this.newColumn_); | |
| 900 this.newColumn_ = null; | |
| 901 | |
| 902 if (this.mode_ === Mosaic.Layout.MODE_FINAL) { | |
| 903 this.getLastColumn_().layout(); | |
| 904 continue; | |
| 905 } | |
| 906 | |
| 907 if (this.getWidth() > this.viewportWidth_) { | |
| 908 // Viewport completely filled. | |
| 909 if (this.density_.equals(this.maxDensity_)) { | |
| 910 // Max density reached, commit if tentative, just continue if dry run. | |
| 911 if (this.mode_ === Mosaic.Layout.MODE_TENTATIVE) | |
| 912 this.commit_(); | |
| 913 continue; | |
| 914 } | |
| 915 | |
| 916 // Rollback the entire layout, retry with higher density. | |
| 917 layoutQueue = this.getTiles().concat(layoutQueue); | |
| 918 this.columns_ = []; | |
| 919 this.density_.increase(); | |
| 920 continue; | |
| 921 } | |
| 922 | |
| 923 if (isFinalColumn && this.mode_ === Mosaic.Layout.MODE_TENTATIVE) { | |
| 924 // The complete tentative layout fits into the viewport. | |
| 925 var stretched = this.findHorizontalLayout_(); | |
| 926 if (stretched) | |
| 927 this.columns_ = stretched.columns_; | |
| 928 // Center the layout in the viewport and commit. | |
| 929 this.commit_((this.viewportWidth_ - this.getWidth()) / 2, | |
| 930 (this.viewportHeight_ - this.getHeight()) / 2); | |
| 931 } | |
| 932 } | |
| 933 }; | |
| 934 | |
| 935 /** | |
| 936 * Commits the tentative layout. | |
| 937 * | |
| 938 * @param {number=} opt_offsetX Horizontal offset. | |
| 939 * @param {number=} opt_offsetY Vertical offset. | |
| 940 * @private | |
| 941 */ | |
| 942 Mosaic.Layout.prototype.commit_ = function(opt_offsetX, opt_offsetY) { | |
| 943 console.assert(this.mode_ !== Mosaic.Layout.MODE_FINAL, | |
| 944 'Did not expect final layout'); | |
| 945 for (var i = 0; i !== this.columns_.length; i++) { | |
| 946 this.columns_[i].layout(opt_offsetX, opt_offsetY); | |
| 947 } | |
| 948 this.mode_ = Mosaic.Layout.MODE_FINAL; | |
| 949 }; | |
| 950 | |
| 951 /** | |
| 952 * Finds the most horizontally stretched layout built from the same tiles. | |
| 953 * | |
| 954 * The main layout algorithm fills the entire available viewport height. | |
| 955 * If there is too few tiles this results in a layout that is unnaturally | |
| 956 * stretched in the vertical direction. | |
| 957 * | |
| 958 * This method tries a number of smaller heights and returns the most | |
| 959 * horizontally stretched layout that still fits into the viewport. | |
| 960 * | |
| 961 * @return {Mosaic.Layout} A horizontally stretched layout. | |
| 962 * @private | |
| 963 */ | |
| 964 Mosaic.Layout.prototype.findHorizontalLayout_ = function() { | |
| 965 // If the layout aspect ratio is not dramatically different from | |
| 966 // the viewport aspect ratio then there is no need to optimize. | |
| 967 if (this.getWidth() / this.getHeight() > | |
| 968 this.viewportWidth_ / this.viewportHeight_ * 0.9) | |
| 969 return null; | |
| 970 | |
| 971 var tiles = this.getTiles(); | |
| 972 if (tiles.length === 1) | |
| 973 return null; // Single tile layout is always the same. | |
| 974 | |
| 975 var tileHeights = tiles.map(function(t) { return t.getMaxContentHeight() }); | |
| 976 var minTileHeight = Math.min.apply(null, tileHeights); | |
| 977 | |
| 978 for (var h = minTileHeight; h < this.viewportHeight_; h += minTileHeight) { | |
| 979 var layout = new Mosaic.Layout( | |
| 980 Mosaic.Layout.MODE_DRY_RUN, this.density_.clone()); | |
| 981 layout.setViewportSize(this.viewportWidth_, h); | |
| 982 for (var t = 0; t !== tiles.length; t++) | |
| 983 layout.add(tiles[t], t + 1 === tiles.length); | |
| 984 | |
| 985 if (layout.getWidth() <= this.viewportWidth_) | |
| 986 return layout; | |
| 987 } | |
| 988 | |
| 989 return null; | |
| 990 }; | |
| 991 | |
| 992 /** | |
| 993 * Invalidates the layout after the given tile was modified (added, deleted or | |
| 994 * changed dimensions). | |
| 995 * | |
| 996 * @param {number} index Tile index. | |
| 997 * @private | |
| 998 */ | |
| 999 Mosaic.Layout.prototype.invalidateFromTile_ = function(index) { | |
| 1000 var columnIndex = this.getColumnIndexByTile_(index); | |
| 1001 if (columnIndex < 0) | |
| 1002 return; // Index not in the layout, probably already invalidated. | |
| 1003 | |
| 1004 if (this.columns_[columnIndex].getLeft() >= this.viewportWidth_) { | |
| 1005 // The columns to the right cover the entire viewport width, so there is no | |
| 1006 // chance that the modified layout would fit into the viewport. | |
| 1007 // No point in restarting the entire layout, keep the columns to the right. | |
| 1008 console.assert(this.mode_ === Mosaic.Layout.MODE_FINAL, | |
| 1009 'Expected FINAL layout mode'); | |
| 1010 this.columns_ = this.columns_.slice(0, columnIndex); | |
| 1011 this.newColumn_ = null; | |
| 1012 } else { | |
| 1013 // There is a chance that the modified layout would fit into the viewport. | |
| 1014 this.reset_(); | |
| 1015 this.mode_ = Mosaic.Layout.MODE_TENTATIVE; | |
| 1016 } | |
| 1017 }; | |
| 1018 | |
| 1019 /** | |
| 1020 * Gets the index of the tile to the left or to the right from the given tile. | |
| 1021 * | |
| 1022 * @param {number} index Tile index. | |
| 1023 * @param {number} direction -1 for left, 1 for right. | |
| 1024 * @return {number} Adjacent tile index. | |
| 1025 */ | |
| 1026 Mosaic.Layout.prototype.getHorizontalAdjacentIndex = function( | |
| 1027 index, direction) { | |
| 1028 var column = this.getColumnIndexByTile_(index); | |
| 1029 if (column < 0) { | |
| 1030 console.error('Cannot find column for tile #' + index); | |
| 1031 return -1; | |
| 1032 } | |
| 1033 | |
| 1034 var row = this.columns_[column].getRowByTileIndex(index); | |
| 1035 if (!row) { | |
| 1036 console.error('Cannot find row for tile #' + index); | |
| 1037 return -1; | |
| 1038 } | |
| 1039 | |
| 1040 var sameRowNeighbourIndex = index + direction; | |
| 1041 if (row.hasTile(sameRowNeighbourIndex)) | |
| 1042 return sameRowNeighbourIndex; | |
| 1043 | |
| 1044 var adjacentColumn = column + direction; | |
| 1045 if (adjacentColumn < 0 || adjacentColumn === this.columns_.length) | |
| 1046 return -1; | |
| 1047 | |
| 1048 return this.columns_[adjacentColumn]. | |
| 1049 getEdgeTileIndex_(row.getCenterY(), -direction); | |
| 1050 }; | |
| 1051 | |
| 1052 /** | |
| 1053 * Gets the index of the tile to the top or to the bottom from the given tile. | |
| 1054 * | |
| 1055 * @param {number} index Tile index. | |
| 1056 * @param {number} direction -1 for above, 1 for below. | |
| 1057 * @return {number} Adjacent tile index. | |
| 1058 */ | |
| 1059 Mosaic.Layout.prototype.getVerticalAdjacentIndex = function( | |
| 1060 index, direction) { | |
| 1061 var column = this.getColumnIndexByTile_(index); | |
| 1062 if (column < 0) { | |
| 1063 console.error('Cannot find column for tile #' + index); | |
| 1064 return -1; | |
| 1065 } | |
| 1066 | |
| 1067 var row = this.columns_[column].getRowByTileIndex(index); | |
| 1068 if (!row) { | |
| 1069 console.error('Cannot find row for tile #' + index); | |
| 1070 return -1; | |
| 1071 } | |
| 1072 | |
| 1073 // Find the first item in the next row, or the last item in the previous row. | |
| 1074 var adjacentRowNeighbourIndex = | |
| 1075 row.getEdgeTileIndex_(direction) + direction; | |
| 1076 | |
| 1077 if (adjacentRowNeighbourIndex < 0 || | |
| 1078 adjacentRowNeighbourIndex > this.getTileCount() - 1) | |
| 1079 return -1; | |
| 1080 | |
| 1081 if (!this.columns_[column].hasTile(adjacentRowNeighbourIndex)) { | |
| 1082 // It is not in the current column, so return it. | |
| 1083 return adjacentRowNeighbourIndex; | |
| 1084 } else { | |
| 1085 // It is in the current column, so we have to find optically the closest | |
| 1086 // tile in the adjacent row. | |
| 1087 var adjacentRow = this.columns_[column].getRowByTileIndex( | |
| 1088 adjacentRowNeighbourIndex); | |
| 1089 var previousTileCenterX = row.getTileByIndex(index).getCenterX(); | |
| 1090 | |
| 1091 // Find the closest one. | |
| 1092 var closestIndex = -1; | |
| 1093 var closestDistance; | |
| 1094 var adjacentRowTiles = adjacentRow.getTiles(); | |
| 1095 for (var t = 0; t !== adjacentRowTiles.length; t++) { | |
| 1096 var distance = | |
| 1097 Math.abs(adjacentRowTiles[t].getCenterX() - previousTileCenterX); | |
| 1098 if (closestIndex === -1 || distance < closestDistance) { | |
| 1099 closestIndex = adjacentRow.getEdgeTileIndex_(-1) + t; | |
| 1100 closestDistance = distance; | |
| 1101 } | |
| 1102 } | |
| 1103 return closestIndex; | |
| 1104 } | |
| 1105 }; | |
| 1106 | |
| 1107 /** | |
| 1108 * @param {number} index Tile index. | |
| 1109 * @return {number} Index of the column containing the given tile. | |
| 1110 * @private | |
| 1111 */ | |
| 1112 Mosaic.Layout.prototype.getColumnIndexByTile_ = function(index) { | |
| 1113 for (var c = 0; c !== this.columns_.length; c++) { | |
| 1114 if (this.columns_[c].hasTile(index)) | |
| 1115 return c; | |
| 1116 } | |
| 1117 return -1; | |
| 1118 }; | |
| 1119 | |
| 1120 /** | |
| 1121 * Scales the given array of size values to satisfy 3 conditions: | |
| 1122 * 1. The new sizes must be integer. | |
| 1123 * 2. The new sizes must sum up to the given |total| value. | |
| 1124 * 3. The relative proportions of the sizes should be as close to the original | |
| 1125 * as possible. | |
| 1126 * | |
| 1127 * @param {Array.<number>} sizes Array of sizes. | |
| 1128 * @param {number} newTotal New total size. | |
| 1129 */ | |
| 1130 Mosaic.Layout.rescaleSizesToNewTotal = function(sizes, newTotal) { | |
| 1131 var total = 0; | |
| 1132 | |
| 1133 var partialTotals = [0]; | |
| 1134 for (var i = 0; i !== sizes.length; i++) { | |
| 1135 total += sizes[i]; | |
| 1136 partialTotals.push(total); | |
| 1137 } | |
| 1138 | |
| 1139 var scale = newTotal / total; | |
| 1140 | |
| 1141 for (i = 0; i !== sizes.length; i++) { | |
| 1142 sizes[i] = Math.round(partialTotals[i + 1] * scale) - | |
| 1143 Math.round(partialTotals[i] * scale); | |
| 1144 } | |
| 1145 }; | |
| 1146 | |
| 1147 //////////////////////////////////////////////////////////////////////////////// | |
| 1148 | |
| 1149 /** | |
| 1150 * Representation of the layout density. | |
| 1151 * | |
| 1152 * @param {number} horizontal Horizontal density, number tiles per row. | |
| 1153 * @param {number} vertical Vertical density, frequency of rows forced to | |
| 1154 * contain a single tile. | |
| 1155 * @constructor | |
| 1156 */ | |
| 1157 Mosaic.Density = function(horizontal, vertical) { | |
| 1158 this.horizontal = horizontal; | |
| 1159 this.vertical = vertical; | |
| 1160 }; | |
| 1161 | |
| 1162 /** | |
| 1163 * Minimal horizontal density (tiles per row). | |
| 1164 */ | |
| 1165 Mosaic.Density.MIN_HORIZONTAL = 1; | |
| 1166 | |
| 1167 /** | |
| 1168 * Minimal horizontal density (tiles per row). | |
| 1169 */ | |
| 1170 Mosaic.Density.MAX_HORIZONTAL = 3; | |
| 1171 | |
| 1172 /** | |
| 1173 * Minimal vertical density: force 1 out of 2 rows to containt a single tile. | |
| 1174 */ | |
| 1175 Mosaic.Density.MIN_VERTICAL = 2; | |
| 1176 | |
| 1177 /** | |
| 1178 * Maximal vertical density: force 1 out of 3 rows to containt a single tile. | |
| 1179 */ | |
| 1180 Mosaic.Density.MAX_VERTICAL = 3; | |
| 1181 | |
| 1182 /** | |
| 1183 * @return {Mosaic.Density} Lowest density. | |
| 1184 */ | |
| 1185 Mosaic.Density.createLowest = function() { | |
| 1186 return new Mosaic.Density( | |
| 1187 Mosaic.Density.MIN_HORIZONTAL, | |
| 1188 Mosaic.Density.MIN_VERTICAL /* ignored when horizontal is at min */); | |
| 1189 }; | |
| 1190 | |
| 1191 /** | |
| 1192 * @return {Mosaic.Density} Highest density. | |
| 1193 */ | |
| 1194 Mosaic.Density.createHighest = function() { | |
| 1195 return new Mosaic.Density( | |
| 1196 Mosaic.Density.MAX_HORIZONTAL, | |
| 1197 Mosaic.Density.MAX_VERTICAL); | |
| 1198 }; | |
| 1199 | |
| 1200 /** | |
| 1201 * @return {Mosaic.Density} A clone of this density object. | |
| 1202 */ | |
| 1203 Mosaic.Density.prototype.clone = function() { | |
| 1204 return new Mosaic.Density(this.horizontal, this.vertical); | |
| 1205 }; | |
| 1206 | |
| 1207 /** | |
| 1208 * @param {Mosaic.Density} that The other object. | |
| 1209 * @return {boolean} True if equal. | |
| 1210 */ | |
| 1211 Mosaic.Density.prototype.equals = function(that) { | |
| 1212 return this.horizontal === that.horizontal && | |
| 1213 this.vertical === that.vertical; | |
| 1214 }; | |
| 1215 | |
| 1216 /** | |
| 1217 * Increases the density to the next level. | |
| 1218 */ | |
| 1219 Mosaic.Density.prototype.increase = function() { | |
| 1220 if (this.horizontal === Mosaic.Density.MIN_HORIZONTAL || | |
| 1221 this.vertical === Mosaic.Density.MAX_VERTICAL) { | |
| 1222 console.assert(this.horizontal < Mosaic.Density.MAX_HORIZONTAL); | |
| 1223 this.horizontal++; | |
| 1224 this.vertical = Mosaic.Density.MIN_VERTICAL; | |
| 1225 } else { | |
| 1226 this.vertical++; | |
| 1227 } | |
| 1228 }; | |
| 1229 | |
| 1230 /** | |
| 1231 * Decreases horizontal density. | |
| 1232 */ | |
| 1233 Mosaic.Density.prototype.decreaseHorizontal = function() { | |
| 1234 console.assert(this.horizontal > Mosaic.Density.MIN_HORIZONTAL); | |
| 1235 this.horizontal--; | |
| 1236 }; | |
| 1237 | |
| 1238 /** | |
| 1239 * @param {number} tileCount Number of tiles in the row. | |
| 1240 * @param {number} rowIndex Global row index. | |
| 1241 * @return {boolean} True if the row is complete. | |
| 1242 */ | |
| 1243 Mosaic.Density.prototype.isRowComplete = function(tileCount, rowIndex) { | |
| 1244 return (tileCount === this.horizontal) || (rowIndex % this.vertical) === 0; | |
| 1245 }; | |
| 1246 | |
| 1247 //////////////////////////////////////////////////////////////////////////////// | |
| 1248 | |
| 1249 /** | |
| 1250 * A column in a mosaic layout. Contains rows. | |
| 1251 * | |
| 1252 * @param {number} index Column index. | |
| 1253 * @param {number} firstRowIndex Global row index. | |
| 1254 * @param {number} firstTileIndex Index of the first tile in the column. | |
| 1255 * @param {number} left Left edge coordinate. | |
| 1256 * @param {number} maxHeight Maximum height. | |
| 1257 * @param {Mosaic.Density} density Layout density. | |
| 1258 * @constructor | |
| 1259 */ | |
| 1260 Mosaic.Column = function(index, firstRowIndex, firstTileIndex, left, maxHeight, | |
| 1261 density) { | |
| 1262 this.index_ = index; | |
| 1263 this.firstRowIndex_ = firstRowIndex; | |
| 1264 this.firstTileIndex_ = firstTileIndex; | |
| 1265 this.left_ = left; | |
| 1266 this.maxHeight_ = maxHeight; | |
| 1267 this.density_ = density; | |
| 1268 | |
| 1269 this.reset_(); | |
| 1270 }; | |
| 1271 | |
| 1272 /** | |
| 1273 * Resets the layout. | |
| 1274 * @private | |
| 1275 */ | |
| 1276 Mosaic.Column.prototype.reset_ = function() { | |
| 1277 this.tiles_ = []; | |
| 1278 this.rows_ = []; | |
| 1279 this.newRow_ = null; | |
| 1280 }; | |
| 1281 | |
| 1282 /** | |
| 1283 * @return {number} Number of tiles in the column. | |
| 1284 */ | |
| 1285 Mosaic.Column.prototype.getTileCount = function() { return this.tiles_.length }; | |
| 1286 | |
| 1287 /** | |
| 1288 * @return {number} Index of the last tile + 1. | |
| 1289 */ | |
| 1290 Mosaic.Column.prototype.getNextTileIndex = function() { | |
| 1291 return this.firstTileIndex_ + this.getTileCount(); | |
| 1292 }; | |
| 1293 | |
| 1294 /** | |
| 1295 * @return {number} Global index of the last row + 1. | |
| 1296 */ | |
| 1297 Mosaic.Column.prototype.getNextRowIndex = function() { | |
| 1298 return this.firstRowIndex_ + this.rows_.length; | |
| 1299 }; | |
| 1300 | |
| 1301 /** | |
| 1302 * @return {Array.<Mosaic.Tile>} Array of tiles in the column. | |
| 1303 */ | |
| 1304 Mosaic.Column.prototype.getTiles = function() { return this.tiles_ }; | |
| 1305 | |
| 1306 /** | |
| 1307 * @param {number} index Tile index. | |
| 1308 * @return {boolean} True if this column contains the tile with the given index. | |
| 1309 */ | |
| 1310 Mosaic.Column.prototype.hasTile = function(index) { | |
| 1311 return this.firstTileIndex_ <= index && | |
| 1312 index < (this.firstTileIndex_ + this.getTileCount()); | |
| 1313 }; | |
| 1314 | |
| 1315 /** | |
| 1316 * @param {number} y Y coordinate. | |
| 1317 * @param {number} direction -1 for left, 1 for right. | |
| 1318 * @return {number} Index of the tile lying on the edge of the column at the | |
| 1319 * given y coordinate. | |
| 1320 * @private | |
| 1321 */ | |
| 1322 Mosaic.Column.prototype.getEdgeTileIndex_ = function(y, direction) { | |
| 1323 for (var r = 0; r < this.rows_.length; r++) { | |
| 1324 if (this.rows_[r].coversY(y)) | |
| 1325 return this.rows_[r].getEdgeTileIndex_(direction); | |
| 1326 } | |
| 1327 return -1; | |
| 1328 }; | |
| 1329 | |
| 1330 /** | |
| 1331 * @param {number} index Tile index. | |
| 1332 * @return {Mosaic.Row} The row containing the tile with a given index. | |
| 1333 */ | |
| 1334 Mosaic.Column.prototype.getRowByTileIndex = function(index) { | |
| 1335 for (var r = 0; r !== this.rows_.length; r++) | |
| 1336 if (this.rows_[r].hasTile(index)) | |
| 1337 return this.rows_[r]; | |
| 1338 | |
| 1339 return null; | |
| 1340 }; | |
| 1341 | |
| 1342 /** | |
| 1343 * Adds a tile to the column. | |
| 1344 * | |
| 1345 * @param {Mosaic.Tile} tile The tile to add. | |
| 1346 */ | |
| 1347 Mosaic.Column.prototype.add = function(tile) { | |
| 1348 var rowIndex = this.getNextRowIndex(); | |
| 1349 | |
| 1350 if (!this.newRow_) | |
| 1351 this.newRow_ = new Mosaic.Row(this.getNextTileIndex()); | |
| 1352 | |
| 1353 this.tiles_.push(tile); | |
| 1354 this.newRow_.add(tile); | |
| 1355 | |
| 1356 if (this.density_.isRowComplete(this.newRow_.getTileCount(), rowIndex)) { | |
| 1357 this.rows_.push(this.newRow_); | |
| 1358 this.newRow_ = null; | |
| 1359 } | |
| 1360 }; | |
| 1361 | |
| 1362 /** | |
| 1363 * Prepares the column layout. | |
| 1364 * | |
| 1365 * @param {boolean=} opt_force True if the layout must be performed even for an | |
| 1366 * incomplete column. | |
| 1367 * @return {boolean} True if the layout was performed. | |
| 1368 */ | |
| 1369 Mosaic.Column.prototype.prepareLayout = function(opt_force) { | |
| 1370 if (opt_force && this.newRow_) { | |
| 1371 this.rows_.push(this.newRow_); | |
| 1372 this.newRow_ = null; | |
| 1373 } | |
| 1374 | |
| 1375 if (this.rows_.length === 0) | |
| 1376 return false; | |
| 1377 | |
| 1378 this.width_ = Math.min.apply( | |
| 1379 null, this.rows_.map(function(row) { return row.getMaxWidth() })); | |
| 1380 | |
| 1381 this.height_ = 0; | |
| 1382 | |
| 1383 this.rowHeights_ = []; | |
| 1384 for (var r = 0; r !== this.rows_.length; r++) { | |
| 1385 var rowHeight = this.rows_[r].getHeightForWidth(this.width_); | |
| 1386 this.height_ += rowHeight; | |
| 1387 this.rowHeights_.push(rowHeight); | |
| 1388 } | |
| 1389 | |
| 1390 var overflow = this.height_ / this.maxHeight_; | |
| 1391 if (!opt_force && (overflow < 1)) | |
| 1392 return false; | |
| 1393 | |
| 1394 if (overflow > 1) { | |
| 1395 // Scale down the column width and height. | |
| 1396 this.width_ = Math.round(this.width_ / overflow); | |
| 1397 this.height_ = this.maxHeight_; | |
| 1398 Mosaic.Layout.rescaleSizesToNewTotal(this.rowHeights_, this.maxHeight_); | |
| 1399 } | |
| 1400 | |
| 1401 return true; | |
| 1402 }; | |
| 1403 | |
| 1404 /** | |
| 1405 * Retries the column layout with less tiles per row. | |
| 1406 */ | |
| 1407 Mosaic.Column.prototype.retryWithLowerDensity = function() { | |
| 1408 this.density_.decreaseHorizontal(); | |
| 1409 this.reset_(); | |
| 1410 }; | |
| 1411 | |
| 1412 /** | |
| 1413 * @return {number} Column left edge coordinate. | |
| 1414 */ | |
| 1415 Mosaic.Column.prototype.getLeft = function() { return this.left_ }; | |
| 1416 | |
| 1417 /** | |
| 1418 * @return {number} Column right edge coordinate after the layout. | |
| 1419 */ | |
| 1420 Mosaic.Column.prototype.getRight = function() { | |
| 1421 return this.left_ + this.width_; | |
| 1422 }; | |
| 1423 | |
| 1424 /** | |
| 1425 * @return {number} Column height after the layout. | |
| 1426 */ | |
| 1427 Mosaic.Column.prototype.getHeight = function() { return this.height_ }; | |
| 1428 | |
| 1429 /** | |
| 1430 * Performs the column layout. | |
| 1431 * @param {number=} opt_offsetX Horizontal offset. | |
| 1432 * @param {number=} opt_offsetY Vertical offset. | |
| 1433 */ | |
| 1434 Mosaic.Column.prototype.layout = function(opt_offsetX, opt_offsetY) { | |
| 1435 opt_offsetX = opt_offsetX || 0; | |
| 1436 opt_offsetY = opt_offsetY || 0; | |
| 1437 var rowTop = Mosaic.Layout.PADDING_TOP; | |
| 1438 for (var r = 0; r !== this.rows_.length; r++) { | |
| 1439 this.rows_[r].layout( | |
| 1440 opt_offsetX + this.left_, | |
| 1441 opt_offsetY + rowTop, | |
| 1442 this.width_, | |
| 1443 this.rowHeights_[r]); | |
| 1444 rowTop += this.rowHeights_[r]; | |
| 1445 } | |
| 1446 }; | |
| 1447 | |
| 1448 /** | |
| 1449 * Checks if the column layout is too ugly to be displayed. | |
| 1450 * | |
| 1451 * @return {boolean} True if the layout is suboptimal. | |
| 1452 */ | |
| 1453 Mosaic.Column.prototype.isSuboptimal = function() { | |
| 1454 var tileCounts = | |
| 1455 this.rows_.map(function(row) { return row.getTileCount() }); | |
| 1456 | |
| 1457 var maxTileCount = Math.max.apply(null, tileCounts); | |
| 1458 if (maxTileCount === 1) | |
| 1459 return false; // Every row has exactly 1 tile, as optimal as it gets. | |
| 1460 | |
| 1461 var sizes = | |
| 1462 this.tiles_.map(function(tile) { return tile.getMaxContentHeight() }); | |
| 1463 | |
| 1464 // Ugly layout #1: all images are small and some are one the same row. | |
| 1465 var allSmall = Math.max.apply(null, sizes) <= Mosaic.Tile.SMALL_IMAGE_SIZE; | |
| 1466 if (allSmall) | |
| 1467 return true; | |
| 1468 | |
| 1469 // Ugly layout #2: all images are large and none occupies an entire row. | |
| 1470 var allLarge = Math.min.apply(null, sizes) > Mosaic.Tile.SMALL_IMAGE_SIZE; | |
| 1471 var allCombined = Math.min.apply(null, tileCounts) !== 1; | |
| 1472 if (allLarge && allCombined) | |
| 1473 return true; | |
| 1474 | |
| 1475 // Ugly layout #3: some rows have too many tiles for the resulting width. | |
| 1476 if (this.width_ / maxTileCount < 100) | |
| 1477 return true; | |
| 1478 | |
| 1479 return false; | |
| 1480 }; | |
| 1481 | |
| 1482 //////////////////////////////////////////////////////////////////////////////// | |
| 1483 | |
| 1484 /** | |
| 1485 * A row in a mosaic layout. Contains tiles. | |
| 1486 * | |
| 1487 * @param {number} firstTileIndex Index of the first tile in the row. | |
| 1488 * @constructor | |
| 1489 */ | |
| 1490 Mosaic.Row = function(firstTileIndex) { | |
| 1491 this.firstTileIndex_ = firstTileIndex; | |
| 1492 this.tiles_ = []; | |
| 1493 }; | |
| 1494 | |
| 1495 /** | |
| 1496 * @param {Mosaic.Tile} tile The tile to add. | |
| 1497 */ | |
| 1498 Mosaic.Row.prototype.add = function(tile) { | |
| 1499 console.assert(this.getTileCount() < Mosaic.Density.MAX_HORIZONTAL); | |
| 1500 this.tiles_.push(tile); | |
| 1501 }; | |
| 1502 | |
| 1503 /** | |
| 1504 * @return {Array.<Mosaic.Tile>} Array of tiles in the row. | |
| 1505 */ | |
| 1506 Mosaic.Row.prototype.getTiles = function() { return this.tiles_ }; | |
| 1507 | |
| 1508 /** | |
| 1509 * Gets a tile by index. | |
| 1510 * @param {number} index Tile index. | |
| 1511 * @return {Mosaic.Tile} Requested tile or null if not found. | |
| 1512 */ | |
| 1513 Mosaic.Row.prototype.getTileByIndex = function(index) { | |
| 1514 if (!this.hasTile(index)) | |
| 1515 return null; | |
| 1516 return this.tiles_[index - this.firstTileIndex_]; | |
| 1517 }; | |
| 1518 | |
| 1519 /** | |
| 1520 * | |
| 1521 * @return {number} Number of tiles in the row. | |
| 1522 */ | |
| 1523 Mosaic.Row.prototype.getTileCount = function() { return this.tiles_.length }; | |
| 1524 | |
| 1525 /** | |
| 1526 * @param {number} index Tile index. | |
| 1527 * @return {boolean} True if this row contains the tile with the given index. | |
| 1528 */ | |
| 1529 Mosaic.Row.prototype.hasTile = function(index) { | |
| 1530 return this.firstTileIndex_ <= index && | |
| 1531 index < (this.firstTileIndex_ + this.tiles_.length); | |
| 1532 }; | |
| 1533 | |
| 1534 /** | |
| 1535 * @param {number} y Y coordinate. | |
| 1536 * @return {boolean} True if this row covers the given Y coordinate. | |
| 1537 */ | |
| 1538 Mosaic.Row.prototype.coversY = function(y) { | |
| 1539 return this.top_ <= y && y < (this.top_ + this.height_); | |
| 1540 }; | |
| 1541 | |
| 1542 /** | |
| 1543 * @return {number} Y coordinate of the tile center. | |
| 1544 */ | |
| 1545 Mosaic.Row.prototype.getCenterY = function() { | |
| 1546 return this.top_ + Math.round(this.height_ / 2); | |
| 1547 }; | |
| 1548 | |
| 1549 /** | |
| 1550 * Gets the first or the last tile. | |
| 1551 * | |
| 1552 * @param {number} direction -1 for the first tile, 1 for the last tile. | |
| 1553 * @return {number} Tile index. | |
| 1554 * @private | |
| 1555 */ | |
| 1556 Mosaic.Row.prototype.getEdgeTileIndex_ = function(direction) { | |
| 1557 if (direction < 0) | |
| 1558 return this.firstTileIndex_; | |
| 1559 else | |
| 1560 return this.firstTileIndex_ + this.getTileCount() - 1; | |
| 1561 }; | |
| 1562 | |
| 1563 /** | |
| 1564 * @return {number} Aspect ration of the combined content box of this row. | |
| 1565 * @private | |
| 1566 */ | |
| 1567 Mosaic.Row.prototype.getTotalContentAspectRatio_ = function() { | |
| 1568 var sum = 0; | |
| 1569 for (var t = 0; t !== this.tiles_.length; t++) | |
| 1570 sum += this.tiles_[t].getAspectRatio(); | |
| 1571 return sum; | |
| 1572 }; | |
| 1573 | |
| 1574 /** | |
| 1575 * @return {number} Total horizontal spacing in this row. This includes | |
| 1576 * the spacing between the tiles and both left and right margins. | |
| 1577 * | |
| 1578 * @private | |
| 1579 */ | |
| 1580 Mosaic.Row.prototype.getTotalHorizontalSpacing_ = function() { | |
| 1581 return Mosaic.Layout.SPACING * this.getTileCount(); | |
| 1582 }; | |
| 1583 | |
| 1584 /** | |
| 1585 * @return {number} Maximum width that this row may have without overscaling | |
| 1586 * any of the tiles. | |
| 1587 */ | |
| 1588 Mosaic.Row.prototype.getMaxWidth = function() { | |
| 1589 var contentHeight = Math.min.apply(null, | |
| 1590 this.tiles_.map(function(tile) { return tile.getMaxContentHeight() })); | |
| 1591 | |
| 1592 var contentWidth = | |
| 1593 Math.round(contentHeight * this.getTotalContentAspectRatio_()); | |
| 1594 return contentWidth + this.getTotalHorizontalSpacing_(); | |
| 1595 }; | |
| 1596 | |
| 1597 /** | |
| 1598 * Computes the height that best fits the supplied row width given | |
| 1599 * aspect ratios of the tiles in this row. | |
| 1600 * | |
| 1601 * @param {number} width Row width. | |
| 1602 * @return {number} Height. | |
| 1603 */ | |
| 1604 Mosaic.Row.prototype.getHeightForWidth = function(width) { | |
| 1605 var contentWidth = width - this.getTotalHorizontalSpacing_(); | |
| 1606 var contentHeight = | |
| 1607 Math.round(contentWidth / this.getTotalContentAspectRatio_()); | |
| 1608 return contentHeight + Mosaic.Layout.SPACING; | |
| 1609 }; | |
| 1610 | |
| 1611 /** | |
| 1612 * Positions the row in the mosaic. | |
| 1613 * | |
| 1614 * @param {number} left Left position. | |
| 1615 * @param {number} top Top position. | |
| 1616 * @param {number} width Width. | |
| 1617 * @param {number} height Height. | |
| 1618 */ | |
| 1619 Mosaic.Row.prototype.layout = function(left, top, width, height) { | |
| 1620 this.top_ = top; | |
| 1621 this.height_ = height; | |
| 1622 | |
| 1623 var contentWidth = width - this.getTotalHorizontalSpacing_(); | |
| 1624 var contentHeight = height - Mosaic.Layout.SPACING; | |
| 1625 | |
| 1626 var tileContentWidth = this.tiles_.map( | |
| 1627 function(tile) { return tile.getAspectRatio() }); | |
| 1628 | |
| 1629 Mosaic.Layout.rescaleSizesToNewTotal(tileContentWidth, contentWidth); | |
| 1630 | |
| 1631 var tileLeft = left; | |
| 1632 for (var t = 0; t !== this.tiles_.length; t++) { | |
| 1633 var tileWidth = tileContentWidth[t] + Mosaic.Layout.SPACING; | |
| 1634 this.tiles_[t].layout(tileLeft, top, tileWidth, height); | |
| 1635 tileLeft += tileWidth; | |
| 1636 } | |
| 1637 }; | |
| 1638 | |
| 1639 //////////////////////////////////////////////////////////////////////////////// | |
| 1640 | |
| 1641 /** | |
| 1642 * A single tile of the image mosaic. | |
| 1643 * | |
| 1644 * @param {Element} container Container element. | |
| 1645 * @param {Gallery.Item} item Gallery item associated with this tile. | |
| 1646 * @param {EntryLocation} locationInfo Location information for the tile. | |
| 1647 * @return {Element} The new tile element. | |
| 1648 * @constructor | |
| 1649 */ | |
| 1650 Mosaic.Tile = function(container, item, locationInfo) { | |
| 1651 var self = container.ownerDocument.createElement('div'); | |
| 1652 Mosaic.Tile.decorate(self, container, item, locationInfo); | |
| 1653 return self; | |
| 1654 }; | |
| 1655 | |
| 1656 /** | |
| 1657 * @param {Element} self Self pointer. | |
| 1658 * @param {Element} container Container element. | |
| 1659 * @param {Gallery.Item} item Gallery item associated with this tile. | |
| 1660 * @param {EntryLocation} locationInfo Location info for the tile image. | |
| 1661 */ | |
| 1662 Mosaic.Tile.decorate = function(self, container, item, locationInfo) { | |
| 1663 self.__proto__ = Mosaic.Tile.prototype; | |
| 1664 self.className = 'mosaic-tile'; | |
| 1665 | |
| 1666 self.container_ = container; | |
| 1667 self.item_ = item; | |
| 1668 self.left_ = null; // Mark as not laid out. | |
| 1669 self.hidpiEmbedded_ = locationInfo && locationInfo.isDriveBased; | |
| 1670 }; | |
| 1671 | |
| 1672 /** | |
| 1673 * Load mode for the tile's image. | |
| 1674 * @enum {number} | |
| 1675 */ | |
| 1676 Mosaic.Tile.LoadMode = { | |
| 1677 LOW_DPI: 0, | |
| 1678 HIGH_DPI: 1 | |
| 1679 }; | |
| 1680 | |
| 1681 /** | |
| 1682 * Inherit from HTMLDivElement. | |
| 1683 */ | |
| 1684 Mosaic.Tile.prototype.__proto__ = HTMLDivElement.prototype; | |
| 1685 | |
| 1686 /** | |
| 1687 * Minimum tile content size. | |
| 1688 */ | |
| 1689 Mosaic.Tile.MIN_CONTENT_SIZE = 64; | |
| 1690 | |
| 1691 /** | |
| 1692 * Maximum tile content size. | |
| 1693 */ | |
| 1694 Mosaic.Tile.MAX_CONTENT_SIZE = 512; | |
| 1695 | |
| 1696 /** | |
| 1697 * Default size for a tile with no thumbnail image. | |
| 1698 */ | |
| 1699 Mosaic.Tile.GENERIC_ICON_SIZE = 128; | |
| 1700 | |
| 1701 /** | |
| 1702 * Max size of an image considered to be 'small'. | |
| 1703 * Small images are laid out slightly differently. | |
| 1704 */ | |
| 1705 Mosaic.Tile.SMALL_IMAGE_SIZE = 160; | |
| 1706 | |
| 1707 /** | |
| 1708 * @return {Gallery.Item} The Gallery item. | |
| 1709 */ | |
| 1710 Mosaic.Tile.prototype.getItem = function() { return this.item_ }; | |
| 1711 | |
| 1712 /** | |
| 1713 * @return {number} Maximum content height that this tile can have. | |
| 1714 */ | |
| 1715 Mosaic.Tile.prototype.getMaxContentHeight = function() { | |
| 1716 return this.maxContentHeight_; | |
| 1717 }; | |
| 1718 | |
| 1719 /** | |
| 1720 * @return {number} The aspect ratio of the tile image. | |
| 1721 */ | |
| 1722 Mosaic.Tile.prototype.getAspectRatio = function() { return this.aspectRatio_ }; | |
| 1723 | |
| 1724 /** | |
| 1725 * @return {boolean} True if the tile is initialized. | |
| 1726 */ | |
| 1727 Mosaic.Tile.prototype.isInitialized = function() { | |
| 1728 return !!this.maxContentHeight_; | |
| 1729 }; | |
| 1730 | |
| 1731 /** | |
| 1732 * Checks whether the image of specified (or better resolution) has been loaded. | |
| 1733 * | |
| 1734 * @param {Mosaic.Tile.LoadMode=} opt_loadMode Loading mode, default: LOW_DPI. | |
| 1735 * @return {boolean} True if the tile is loaded with the specified dpi or | |
| 1736 * better. | |
| 1737 */ | |
| 1738 Mosaic.Tile.prototype.isLoaded = function(opt_loadMode) { | |
| 1739 var loadMode = opt_loadMode || Mosaic.Tile.LoadMode.LOW_DPI; | |
| 1740 switch (loadMode) { | |
| 1741 case Mosaic.Tile.LoadMode.LOW_DPI: | |
| 1742 if (this.imagePreloaded_ || this.imageLoaded_) | |
| 1743 return true; | |
| 1744 break; | |
| 1745 case Mosaic.Tile.LoadMode.HIGH_DPI: | |
| 1746 if (this.imageLoaded_) | |
| 1747 return true; | |
| 1748 break; | |
| 1749 } | |
| 1750 return false; | |
| 1751 }; | |
| 1752 | |
| 1753 /** | |
| 1754 * Checks whether the image of specified (or better resolution) is being loaded. | |
| 1755 * | |
| 1756 * @param {Mosaic.Tile.LoadMode=} opt_loadMode Loading mode, default: LOW_DPI. | |
| 1757 * @return {boolean} True if the tile is being loaded with the specified dpi or | |
| 1758 * better. | |
| 1759 */ | |
| 1760 Mosaic.Tile.prototype.isLoading = function(opt_loadMode) { | |
| 1761 var loadMode = opt_loadMode || Mosaic.Tile.LoadMode.LOW_DPI; | |
| 1762 switch (loadMode) { | |
| 1763 case Mosaic.Tile.LoadMode.LOW_DPI: | |
| 1764 if (this.imagePreloading_ || this.imageLoading_) | |
| 1765 return true; | |
| 1766 break; | |
| 1767 case Mosaic.Tile.LoadMode.HIGH_DPI: | |
| 1768 if (this.imageLoading_) | |
| 1769 return true; | |
| 1770 break; | |
| 1771 } | |
| 1772 return false; | |
| 1773 }; | |
| 1774 | |
| 1775 /** | |
| 1776 * Marks the tile as not loaded to prevent it from participating in the layout. | |
| 1777 */ | |
| 1778 Mosaic.Tile.prototype.markUnloaded = function() { | |
| 1779 this.maxContentHeight_ = 0; | |
| 1780 if (this.thumbnailLoader_) { | |
| 1781 this.thumbnailLoader_.cancel(); | |
| 1782 this.imagePreloaded_ = false; | |
| 1783 this.imagePreloading_ = false; | |
| 1784 this.imageLoaded_ = false; | |
| 1785 this.imageLoading_ = false; | |
| 1786 } | |
| 1787 }; | |
| 1788 | |
| 1789 /** | |
| 1790 * Initializes the thumbnail in the tile. Does not load an image, but sets | |
| 1791 * target dimensions using metadata. | |
| 1792 * | |
| 1793 * @param {Object} metadata Metadata object. | |
| 1794 * @param {function()} onImageMeasured Image measured callback. | |
| 1795 */ | |
| 1796 Mosaic.Tile.prototype.init = function(metadata, onImageMeasured) { | |
| 1797 this.markUnloaded(); | |
| 1798 this.left_ = null; // Mark as not laid out. | |
| 1799 | |
| 1800 // Set higher priority for the selected elements to load them first. | |
| 1801 var priority = this.getAttribute('selected') ? 2 : 3; | |
| 1802 | |
| 1803 // Use embedded thumbnails on Drive, since they have higher resolution. | |
| 1804 this.thumbnailLoader_ = new ThumbnailLoader( | |
| 1805 this.getItem().getEntry(), | |
| 1806 ThumbnailLoader.LoaderType.CANVAS, | |
| 1807 metadata, | |
| 1808 undefined, // Media type. | |
| 1809 this.hidpiEmbedded_ ? ThumbnailLoader.UseEmbedded.USE_EMBEDDED : | |
| 1810 ThumbnailLoader.UseEmbedded.NO_EMBEDDED, | |
| 1811 priority); | |
| 1812 | |
| 1813 // If no hidpi embedded thumbnail available, then use the low resolution | |
| 1814 // for preloading. | |
| 1815 if (!this.hidpiEmbedded_) { | |
| 1816 this.thumbnailPreloader_ = new ThumbnailLoader( | |
| 1817 this.getItem().getEntry(), | |
| 1818 ThumbnailLoader.LoaderType.CANVAS, | |
| 1819 metadata, | |
| 1820 undefined, // Media type. | |
| 1821 ThumbnailLoader.UseEmbedded.USE_EMBEDDED, | |
| 1822 2); // Preloaders have always higher priotity, so the preload images | |
| 1823 // are loaded as soon as possible. | |
| 1824 } | |
| 1825 | |
| 1826 var setDimensions = function(width, height) { | |
| 1827 if (width > height) { | |
| 1828 if (width > Mosaic.Tile.MAX_CONTENT_SIZE) { | |
| 1829 height = Math.round(height * Mosaic.Tile.MAX_CONTENT_SIZE / width); | |
| 1830 width = Mosaic.Tile.MAX_CONTENT_SIZE; | |
| 1831 } | |
| 1832 } else { | |
| 1833 if (height > Mosaic.Tile.MAX_CONTENT_SIZE) { | |
| 1834 width = Math.round(width * Mosaic.Tile.MAX_CONTENT_SIZE / height); | |
| 1835 height = Mosaic.Tile.MAX_CONTENT_SIZE; | |
| 1836 } | |
| 1837 } | |
| 1838 this.maxContentHeight_ = Math.max(Mosaic.Tile.MIN_CONTENT_SIZE, height); | |
| 1839 this.aspectRatio_ = width / height; | |
| 1840 onImageMeasured(); | |
| 1841 }.bind(this); | |
| 1842 | |
| 1843 // Dimensions are always acquired from the metadata. For local files, it is | |
| 1844 // extracted from headers. For Drive files, it is received via the Drive API. | |
| 1845 // If the dimensions are not available, then the fallback dimensions will be | |
| 1846 // used (same as for the generic icon). | |
| 1847 if (metadata.media && metadata.media.width) { | |
| 1848 setDimensions(metadata.media.width, metadata.media.height); | |
| 1849 } else if (metadata.drive && metadata.drive.imageWidth && | |
| 1850 metadata.drive.imageHeight) { | |
| 1851 setDimensions(metadata.drive.imageWidth, metadata.drive.imageHeight); | |
| 1852 } else { | |
| 1853 // No dimensions in metadata, then use the generic dimensions. | |
| 1854 setDimensions(Mosaic.Tile.GENERIC_ICON_SIZE, | |
| 1855 Mosaic.Tile.GENERIC_ICON_SIZE); | |
| 1856 } | |
| 1857 }; | |
| 1858 | |
| 1859 /** | |
| 1860 * Loads an image into the tile. | |
| 1861 * | |
| 1862 * The mode argument is a hint. Use low-dpi for faster response, and high-dpi | |
| 1863 * for better output, but possibly affecting performance. | |
| 1864 * | |
| 1865 * If the mode is high-dpi, then a the high-dpi image is loaded, but also | |
| 1866 * low-dpi image is loaded for preloading (if available). | |
| 1867 * For the low-dpi mode, only low-dpi image is loaded. If not available, then | |
| 1868 * the high-dpi image is loaded as a fallback. | |
| 1869 * | |
| 1870 * @param {Mosaic.Tile.LoadMode} loadMode Loading mode. | |
| 1871 * @param {function(boolean)} onImageLoaded Callback when image is loaded. | |
| 1872 * The argument is true for success, false for failure. | |
| 1873 */ | |
| 1874 Mosaic.Tile.prototype.load = function(loadMode, onImageLoaded) { | |
| 1875 // Attaches the image to the tile and finalizes loading process for the | |
| 1876 // specified loader. | |
| 1877 var finalizeLoader = function(mode, success, loader) { | |
| 1878 if (success && this.wrapper_) { | |
| 1879 // Show the fade-in animation only when previously there was no image | |
| 1880 // attached in this tile. | |
| 1881 if (!this.imageLoaded_ && !this.imagePreloaded_) | |
| 1882 this.wrapper_.classList.add('animated'); | |
| 1883 else | |
| 1884 this.wrapper_.classList.remove('animated'); | |
| 1885 } | |
| 1886 loader.attachImage(this.wrapper_, ThumbnailLoader.FillMode.OVER_FILL); | |
| 1887 onImageLoaded(success); | |
| 1888 switch (mode) { | |
| 1889 case Mosaic.Tile.LoadMode.LOW_DPI: | |
| 1890 this.imagePreloading_ = false; | |
| 1891 this.imagePreloaded_ = true; | |
| 1892 break; | |
| 1893 case Mosaic.Tile.LoadMode.HIGH_DPI: | |
| 1894 this.imageLoading_ = false; | |
| 1895 this.imageLoaded_ = true; | |
| 1896 break; | |
| 1897 } | |
| 1898 }.bind(this); | |
| 1899 | |
| 1900 // Always load the low-dpi image first if it is available for the fastest | |
| 1901 // feedback. | |
| 1902 if (!this.imagePreloading_ && this.thumbnailPreloader_) { | |
| 1903 this.imagePreloading_ = true; | |
| 1904 this.thumbnailPreloader_.loadDetachedImage(function(success) { | |
| 1905 // Hi-dpi loaded first, ignore this call then. | |
| 1906 if (this.imageLoaded_) | |
| 1907 return; | |
| 1908 finalizeLoader(Mosaic.Tile.LoadMode.LOW_DPI, | |
| 1909 success, | |
| 1910 this.thumbnailPreloader_); | |
| 1911 }.bind(this)); | |
| 1912 } | |
| 1913 | |
| 1914 // Load the high-dpi image only when it is requested, or the low-dpi is not | |
| 1915 // available. | |
| 1916 if (!this.imageLoading_ && | |
| 1917 (loadMode === Mosaic.Tile.LoadMode.HIGH_DPI || !this.imagePreloading_)) { | |
| 1918 this.imageLoading_ = true; | |
| 1919 this.thumbnailLoader_.loadDetachedImage(function(success) { | |
| 1920 // Cancel preloading, since the hi-dpi image is ready. | |
| 1921 if (this.thumbnailPreloader_) | |
| 1922 this.thumbnailPreloader_.cancel(); | |
| 1923 finalizeLoader(Mosaic.Tile.LoadMode.HIGH_DPI, | |
| 1924 success, | |
| 1925 this.thumbnailLoader_); | |
| 1926 }.bind(this)); | |
| 1927 } | |
| 1928 }; | |
| 1929 | |
| 1930 /** | |
| 1931 * Unloads an image from the tile. | |
| 1932 */ | |
| 1933 Mosaic.Tile.prototype.unload = function() { | |
| 1934 this.thumbnailLoader_.cancel(); | |
| 1935 if (this.thumbnailPreloader_) | |
| 1936 this.thumbnailPreloader_.cancel(); | |
| 1937 this.imagePreloaded_ = false; | |
| 1938 this.imageLoaded_ = false; | |
| 1939 this.imagePreloading_ = false; | |
| 1940 this.imageLoading_ = false; | |
| 1941 this.wrapper_.innerText = ''; | |
| 1942 }; | |
| 1943 | |
| 1944 /** | |
| 1945 * Selects/unselects the tile. | |
| 1946 * | |
| 1947 * @param {boolean} on True if selected. | |
| 1948 */ | |
| 1949 Mosaic.Tile.prototype.select = function(on) { | |
| 1950 if (on) | |
| 1951 this.setAttribute('selected', true); | |
| 1952 else | |
| 1953 this.removeAttribute('selected'); | |
| 1954 }; | |
| 1955 | |
| 1956 /** | |
| 1957 * Positions the tile in the mosaic. | |
| 1958 * | |
| 1959 * @param {number} left Left position. | |
| 1960 * @param {number} top Top position. | |
| 1961 * @param {number} width Width. | |
| 1962 * @param {number} height Height. | |
| 1963 */ | |
| 1964 Mosaic.Tile.prototype.layout = function(left, top, width, height) { | |
| 1965 this.left_ = left; | |
| 1966 this.top_ = top; | |
| 1967 this.width_ = width; | |
| 1968 this.height_ = height; | |
| 1969 | |
| 1970 this.style.left = left + 'px'; | |
| 1971 this.style.top = top + 'px'; | |
| 1972 this.style.width = width + 'px'; | |
| 1973 this.style.height = height + 'px'; | |
| 1974 | |
| 1975 if (!this.wrapper_) { // First time, create DOM. | |
| 1976 this.container_.appendChild(this); | |
| 1977 var border = util.createChild(this, 'img-border'); | |
| 1978 this.wrapper_ = util.createChild(border, 'img-wrapper'); | |
| 1979 } | |
| 1980 if (this.hasAttribute('selected')) | |
| 1981 this.scrollIntoView(false); | |
| 1982 | |
| 1983 if (this.imageLoaded_) { | |
| 1984 this.thumbnailLoader_.attachImage(this.wrapper_, | |
| 1985 ThumbnailLoader.FillMode.FILL); | |
| 1986 } | |
| 1987 }; | |
| 1988 | |
| 1989 /** | |
| 1990 * If the tile is not fully visible scroll the parent to make it fully visible. | |
| 1991 * @param {boolean=} opt_animated True, if scroll should be animated, | |
| 1992 * default: true. | |
| 1993 */ | |
| 1994 Mosaic.Tile.prototype.scrollIntoView = function(opt_animated) { | |
| 1995 if (this.left_ === null) // Not laid out. | |
| 1996 return; | |
| 1997 | |
| 1998 var targetPosition; | |
| 1999 var tileLeft = this.left_ - Mosaic.Layout.SCROLL_MARGIN; | |
| 2000 if (tileLeft < this.container_.scrollLeft) { | |
| 2001 targetPosition = tileLeft; | |
| 2002 } else { | |
| 2003 var tileRight = this.left_ + this.width_ + Mosaic.Layout.SCROLL_MARGIN; | |
| 2004 var scrollRight = this.container_.scrollLeft + this.container_.clientWidth; | |
| 2005 if (tileRight > scrollRight) | |
| 2006 targetPosition = tileRight - this.container_.clientWidth; | |
| 2007 } | |
| 2008 | |
| 2009 if (targetPosition) { | |
| 2010 if (opt_animated === false) | |
| 2011 this.container_.scrollLeft = targetPosition; | |
| 2012 else | |
| 2013 this.container_.animatedScrollTo(targetPosition); | |
| 2014 } | |
| 2015 }; | |
| 2016 | |
| 2017 /** | |
| 2018 * @return {Rect} Rectangle occupied by the tile's image, | |
| 2019 * relative to the viewport. | |
| 2020 */ | |
| 2021 Mosaic.Tile.prototype.getImageRect = function() { | |
| 2022 if (this.left_ === null) // Not laid out. | |
| 2023 return null; | |
| 2024 | |
| 2025 var margin = Mosaic.Layout.SPACING / 2; | |
| 2026 return new Rect(this.left_ - this.container_.scrollLeft, this.top_, | |
| 2027 this.width_, this.height_).inflate(-margin, -margin); | |
| 2028 }; | |
| 2029 | |
| 2030 /** | |
| 2031 * @return {number} X coordinate of the tile center. | |
| 2032 */ | |
| 2033 Mosaic.Tile.prototype.getCenterX = function() { | |
| 2034 return this.left_ + Math.round(this.width_ / 2); | |
| 2035 }; | |
| OLD | NEW |