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