1 // Copyright 2014 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.
6 * @param {Element} container Content container.
7 * @param {ErrorBanner} errorBanner Error banner.
8 * @param {cr.ui.ArrayDataModel} dataModel Data model.
9 * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
10 * @param {VolumeManagerWrapper} volumeManager Volume manager.
11 * @param {function()} toggleMode Function to switch to the Slide mode.
15 container, errorBanner, dataModel, selectionModel, volumeManager,
17 this.mosaic_ = new Mosaic(container.ownerDocument, errorBanner,
18 dataModel, selectionModel, volumeManager);
19 container.appendChild(this.mosaic_);
21 this.toggleMode_ = toggleMode;
22 this.mosaic_.addEventListener('dblclick', this.toggleMode_);
23 this.showingTimeoutID_ = null;
27 * @return {Mosaic} The mosaic control.
29 MosaicMode.prototype.getMosaic = function() { return this.mosaic_; };
32 * @return {string} Mode name.
34 MosaicMode.prototype.getName = function() { return 'mosaic'; };
37 * @return {string} Mode title.
39 MosaicMode.prototype.getTitle = function() { return 'GALLERY_MOSAIC'; };
42 * Execute an action (this mode has no busy state).
43 * @param {function()} action Action to execute.
45 MosaicMode.prototype.executeWhenReady = function(action) { action(); };
48 * @return {boolean} Always true (no toolbar fading in this mode).
50 MosaicMode.prototype.hasActiveTool = function() { return true; };
55 * @param {Event} event Event.
57 MosaicMode.prototype.onKeyDown = function(event) {
58 switch (util.getKeyModifiers(event) + event.keyIdentifier) {
60 if (!document.activeElement ||
61 document.activeElement.localName !== 'button') {
63 event.preventDefault();
67 this.mosaic_.onKeyDown(event);
70 ////////////////////////////////////////////////////////////////////////////////
75 * @param {Document} document Document.
76 * @param {ErrorBanner} errorBanner Error banner.
77 * @param {cr.ui.ArrayDataModel} dataModel Data model.
78 * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
79 * @param {VolumeManagerWrapper} volumeManager Volume manager.
80 * @return {Element} Mosaic element.
83 function Mosaic(document, errorBanner, dataModel, selectionModel,
85 var self = document.createElement('div');
86 Mosaic.decorate(self, errorBanner, dataModel, selectionModel, volumeManager);
91 * Inherits from HTMLDivElement.
93 Mosaic.prototype.__proto__ = HTMLDivElement.prototype;
96 * Default layout delay in ms.
100 Mosaic.LAYOUT_DELAY = 200;
103 * Smooth scroll animation duration when scrolling using keyboard or
104 * clicking on a partly visible tile. In ms.
108 Mosaic.ANIMATED_SCROLL_DURATION = 500;
111 * Decorates a Mosaic instance.
113 * @param {Mosaic} self Self pointer.
114 * @param {ErrorBanner} errorBanner Error banner.
115 * @param {cr.ui.ArrayDataModel} dataModel Data model.
116 * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
117 * @param {VolumeManagerWrapper} volumeManager Volume manager.
119 Mosaic.decorate = function(
120 self, errorBanner, dataModel, selectionModel, volumeManager) {
121 self.__proto__ = Mosaic.prototype;
122 self.className = 'mosaic';
124 self.dataModel_ = dataModel;
125 self.selectionModel_ = selectionModel;
126 self.volumeManager_ = volumeManager;
127 self.errorBanner_ = errorBanner;
129 // Initialization is completed lazily on the first call to |init|.
133 * Initializes the mosaic element.
135 Mosaic.prototype.init = function() {
137 return; // Already initialized, nothing to do.
139 this.layoutModel_ = new Mosaic.Layout();
142 this.selectionController_ =
143 new Mosaic.SelectionController(this.selectionModel_, this.layoutModel_);
146 for (var i = 0; i !== this.dataModel_.length; i++) {
148 this.volumeManager_.getLocationInfo(this.dataModel_.item(i).getEntry());
150 new Mosaic.Tile(this, this.dataModel_.item(i), locationInfo));
153 this.selectionModel_.selectedIndexes.forEach(function(index) {
154 this.tiles_[index].select(true);
157 this.initTiles_(this.tiles_);
159 // The listeners might be called while some tiles are still loading.
160 this.initListeners_();
164 * @return {boolean} Whether mosaic is initialized.
166 Mosaic.prototype.isInitialized = function() {
167 return !!this.tiles_;
171 * Starts listening to events.
173 * We keep listening to events even when the mosaic is hidden in order to
174 * keep the layout up to date.
178 Mosaic.prototype.initListeners_ = function() {
179 this.ownerDocument.defaultView.addEventListener(
180 'resize', this.onResize_.bind(this));
182 var mouseEventBound = this.onMouseEvent_.bind(this);
183 this.addEventListener('mousemove', mouseEventBound);
184 this.addEventListener('mousedown', mouseEventBound);
185 this.addEventListener('mouseup', mouseEventBound);
186 this.addEventListener('scroll', this.onScroll_.bind(this));
188 this.selectionModel_.addEventListener('change', this.onSelection_.bind(this));
189 this.selectionModel_.addEventListener('leadIndexChange',
190 this.onLeadChange_.bind(this));
192 this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
193 this.dataModel_.addEventListener('content', this.onContentChange_.bind(this));
197 * Smoothly scrolls the container to the specified position using
198 * f(x) = sqrt(x) speed function normalized to animation duration.
199 * @param {number} targetPosition Horizontal scroll position in pixels.
201 Mosaic.prototype.animatedScrollTo = function(targetPosition) {
202 if (this.scrollAnimation_) {
203 webkitCancelAnimationFrame(this.scrollAnimation_);
204 this.scrollAnimation_ = null;
207 // Mouse move events are fired without touching the mouse because of scrolling
208 // the container. Therefore, these events have to be suppressed.
209 this.suppressHovering_ = true;
211 // Calculates integral area from t1 to t2 of f(x) = sqrt(x) dx.
212 var integral = function(t1, t2) {
213 return 2.0 / 3.0 * Math.pow(t2, 3.0 / 2.0) -
214 2.0 / 3.0 * Math.pow(t1, 3.0 / 2.0);
217 var delta = targetPosition - this.scrollLeft;
218 var factor = delta / integral(0, Mosaic.ANIMATED_SCROLL_DURATION);
219 var startTime = Date.now();
220 var lastPosition = 0;
221 var scrollOffset = this.scrollLeft;
223 var animationFrame = function() {
224 var position = Date.now() - startTime;
226 integral(Math.max(0, Mosaic.ANIMATED_SCROLL_DURATION - position),
227 Math.max(0, Mosaic.ANIMATED_SCROLL_DURATION - lastPosition));
228 scrollOffset += step;
230 var oldScrollLeft = this.scrollLeft;
231 var newScrollLeft = Math.round(scrollOffset);
233 if (oldScrollLeft !== newScrollLeft)
234 this.scrollLeft = newScrollLeft;
236 if (step === 0 || this.scrollLeft !== newScrollLeft) {
237 this.scrollAnimation_ = null;
238 // Release the hovering lock after a safe delay to avoid hovering
239 // a tile because of altering |this.scrollLeft|.
240 setTimeout(function() {
241 if (!this.scrollAnimation_)
242 this.suppressHovering_ = false;
245 // Continue the animation.
246 this.scrollAnimation_ = requestAnimationFrame(animationFrame);
249 lastPosition = position;
252 // Start the animation.
253 this.scrollAnimation_ = requestAnimationFrame(animationFrame);
257 * @return {Mosaic.Tile} Selected tile or undefined if no selection.
259 Mosaic.prototype.getSelectedTile = function() {
260 return this.tiles_ && this.tiles_[this.selectionModel_.selectedIndex];
264 * @param {number} index Tile index.
265 * @return {ImageRect} Tile's image rectangle.
267 Mosaic.prototype.getTileRect = function(index) {
268 var tile = this.tiles_[index];
269 return tile && tile.getImageRect();
273 * @param {number} index Tile index.
274 * Scroll the given tile into the viewport.
276 Mosaic.prototype.scrollIntoView = function(index) {
277 var tile = this.tiles_[index];
278 if (tile) tile.scrollIntoView();
282 * Initializes multiple tiles.
284 * @param {Array.<Mosaic.Tile>} tiles Array of tiles.
287 Mosaic.prototype.initTiles_ = function(tiles) {
288 for (var i = 0; i < tiles.length; i++) {
296 Mosaic.prototype.reload = function() {
297 this.layoutModel_.reset_();
298 this.tiles_.forEach(function(t) { t.markUnloaded(); });
299 this.initTiles_(this.tiles_);
303 * Layouts the tiles in the order of their indices.
305 * Starts where it last stopped (at #0 the first time).
306 * Stops when all tiles are processed or when the next tile is still loading.
308 Mosaic.prototype.layout = function() {
309 if (this.layoutTimer_) {
310 clearTimeout(this.layoutTimer_);
311 this.layoutTimer_ = null;
314 var index = this.layoutModel_.getTileCount();
315 if (index === this.tiles_.length)
316 break; // All tiles done.
317 var tile = this.tiles_[index];
318 if (!tile.isInitialized())
319 break; // Next layout will try to restart from here.
320 this.layoutModel_.add(tile, index + 1 === this.tiles_.length);
322 this.loadVisibleTiles_();
326 * Schedules the layout.
328 * @param {number=} opt_delay Delay in ms.
330 Mosaic.prototype.scheduleLayout = function(opt_delay) {
331 if (!this.layoutTimer_) {
332 this.layoutTimer_ = setTimeout(function() {
333 this.layoutTimer_ = null;
335 }.bind(this), opt_delay || 0);
344 Mosaic.prototype.onResize_ = function() {
345 this.layoutModel_.setViewportSize(this.clientWidth, this.clientHeight -
346 (Mosaic.Layout.PADDING_TOP + Mosaic.Layout.PADDING_BOTTOM));
347 this.scheduleLayout();
351 * Mouse event handler.
353 * @param {Event} event Event.
356 Mosaic.prototype.onMouseEvent_ = function(event) {
357 // Navigating with mouse, enable hover state.
358 if (!this.suppressHovering_)
359 this.classList.add('hover-visible');
361 if (event.type === 'mousemove')
365 for (var target = event.target;
366 target && (target !== this);
367 target = target.parentNode) {
368 if (target.classList.contains('mosaic-tile')) {
369 index = this.dataModel_.indexOf(target.getItem());
373 this.selectionController_.handlePointerDownUp(event, index);
380 Mosaic.prototype.onScroll_ = function() {
381 requestAnimationFrame(function() {
382 this.loadVisibleTiles_();
387 * Selection change handler.
389 * @param {Event} event Event.
392 Mosaic.prototype.onSelection_ = function(event) {
393 for (var i = 0; i !== event.changes.length; i++) {
394 var change = event.changes[i];
395 var tile = this.tiles_[change.index];
396 if (tile) tile.select(change.selected);
401 * Leads item change handler.
403 * @param {Event} event Event.
406 Mosaic.prototype.onLeadChange_ = function(event) {
407 var index = event.newValue;
409 var tile = this.tiles_[index];
410 if (tile) tile.scrollIntoView();
415 * Splice event handler.
417 * @param {Event} event Event.
420 Mosaic.prototype.onSplice_ = function(event) {
421 var index = event.index;
422 this.layoutModel_.invalidateFromTile_(index);
424 if (event.removed.length) {
425 for (var t = 0; t !== event.removed.length; t++) {
426 // If the layout for the tile has not done yet, the parent is null.
427 // And the layout will not be done after onSplice_ because it is removed
429 if (this.tiles_[index + t].parentNode)
430 this.removeChild(this.tiles_[index + t]);
433 this.tiles_.splice(index, event.removed.length);
435 // No items left, show the banner.
436 if (this.getItemCount_() === 0)
437 this.errorBanner_.show('GALLERY_NO_IMAGES');
439 this.scheduleLayout(Mosaic.LAYOUT_DELAY);
442 if (event.added.length) {
444 for (var t = 0; t !== event.added.length; t++)
445 newTiles.push(new Mosaic.Tile(this, this.dataModel_.item(index + t)));
447 this.tiles_.splice.apply(this.tiles_, [index, 0].concat(newTiles));
448 this.initTiles_(newTiles);
449 this.scheduleLayout(Mosaic.LAYOUT_DELAY);
452 if (this.tiles_.length !== this.dataModel_.length)
453 console.error('Mosaic is out of sync');
457 * Content change handler.
459 * @param {Event} event Event.
462 Mosaic.prototype.onContentChange_ = function(event) {
467 return; // Thumbnail unchanged, nothing to do.
469 var index = this.dataModel_.indexOf(event.item);
470 if (index !== this.selectionModel_.selectedIndex)
471 console.error('Content changed for unselected item');
473 this.layoutModel_.invalidateFromTile_(index);
474 this.tiles_[index].init();
475 this.tiles_[index].unload();
476 this.tiles_[index].load(
477 Mosaic.Tile.LoadMode.HIGH_DPI,
478 this.scheduleLayout.bind(this, Mosaic.LAYOUT_DELAY));
482 * Keydown event handler.
484 * @param {Event} event Event.
485 * @return {boolean} True if the event has been consumed.
487 Mosaic.prototype.onKeyDown = function(event) {
488 this.selectionController_.handleKeyDown(event);
489 if (event.defaultPrevented) // Navigating with keyboard, hide hover state.
490 this.classList.remove('hover-visible');
491 return event.defaultPrevented;
495 * @return {boolean} True if the mosaic zoom effect can be applied. It is
496 * too slow if there are to many images.
497 * TODO(kaznacheev): Consider unloading the images that are out of the viewport.
499 Mosaic.prototype.canZoom = function() {
500 return this.tiles_.length < 100;
506 Mosaic.prototype.show = function() {
507 // If the items are empty, just show the error message.
508 if (this.getItemCount_() === 0)
509 this.errorBanner_.show('GALLERY_NO_IMAGES');
511 var duration = ImageView.MODE_TRANSITION_DURATION;
512 if (this.canZoom()) {
513 // Fade in in parallel with the zoom effect.
514 this.setAttribute('visible', 'zooming');
516 // Mosaic is not animating but the large image is. Fade in the mosaic
517 // shortly before the large image animation is done.
520 this.showingTimeoutID_ = setTimeout(function() {
521 this.showingTimeoutID_ = null;
522 // Make the selection visible.
523 // If the mosaic is not animated it will start fading in now.
524 this.setAttribute('visible', 'normal');
525 this.loadVisibleTiles_();
526 }.bind(this), duration);
532 Mosaic.prototype.hide = function() {
533 this.errorBanner_.clear();
535 if (this.showingTimeoutID_ !== null) {
536 clearTimeout(this.showingTimeoutID_);
537 this.showingTimeoutID_ = null;
539 this.removeAttribute('visible');
543 * Checks if the mosaic view is visible.
544 * @return {boolean} True if visible, false otherwise.
547 Mosaic.prototype.isVisible_ = function() {
548 return this.hasAttribute('visible');
552 * Loads visible tiles. Ignores consecutive calls. Does not reload already
556 Mosaic.prototype.loadVisibleTiles_ = function() {
557 if (this.loadVisibleTilesSuppressed_) {
558 this.loadVisibleTilesScheduled_ = true;
562 this.loadVisibleTilesSuppressed_ = true;
563 this.loadVisibleTilesScheduled_ = false;
564 setTimeout(function() {
565 this.loadVisibleTilesSuppressed_ = false;
566 if (this.loadVisibleTilesScheduled_)
567 this.loadVisibleTiles_();
570 // Tiles only in the viewport (visible).
571 var visibleRect = new ImageRect(
572 0, 0, this.clientWidth, this.clientHeight);
574 // Tiles in the viewport and also some distance on the left and right.
575 var renderableRect = new ImageRect(
578 3 * this.clientWidth,
581 // Unload tiles out of scope.
582 for (var index = 0; index < this.tiles_.length; index++) {
583 var tile = this.tiles_[index];
584 var imageRect = tile.getImageRect();
585 // Unload a thumbnail.
586 if (imageRect && !imageRect.intersects(renderableRect))
590 // Load the visible tiles first.
591 var allVisibleLoaded = true;
592 // Show high-dpi only when the mosaic view is visible.
593 var loadMode = this.isVisible_() ? Mosaic.Tile.LoadMode.HIGH_DPI :
594 Mosaic.Tile.LoadMode.LOW_DPI;
595 for (var index = 0; index < this.tiles_.length; index++) {
596 var tile = this.tiles_[index];
597 var imageRect = tile.getImageRect();
599 if (!tile.isLoading(loadMode) && !tile.isLoaded(loadMode) && imageRect &&
600 imageRect.intersects(visibleRect)) {
601 tile.load(loadMode, function() {});
602 allVisibleLoaded = false;
606 // Load also another, nearby, if the visible has been already loaded.
607 if (allVisibleLoaded) {
608 for (var index = 0; index < this.tiles_.length; index++) {
609 var tile = this.tiles_[index];
610 var imageRect = tile.getImageRect();
612 if (!tile.isLoading() && !tile.isLoaded() && imageRect &&
613 imageRect.intersects(renderableRect)) {
614 tile.load(Mosaic.Tile.LoadMode.LOW_DPI, function() {});
621 * Applies reset the zoom transform.
623 * @param {ImageRect} tileRect Tile rectangle. Reset the transform if null.
624 * @param {ImageRect} imageRect Large image rectangle. Reset the transform if
626 * @param {boolean=} opt_instant True of the transition should be instant.
628 Mosaic.prototype.transform = function(tileRect, imageRect, opt_instant) {
630 this.style.webkitTransitionDuration = '0';
632 this.style.webkitTransitionDuration =
633 ImageView.MODE_TRANSITION_DURATION + 'ms';
636 if (this.canZoom() && tileRect && imageRect) {
637 var scaleX = imageRect.width / tileRect.width;
638 var scaleY = imageRect.height / tileRect.height;
639 var shiftX = (imageRect.left + imageRect.width / 2) -
640 (tileRect.left + tileRect.width / 2);
641 var shiftY = (imageRect.top + imageRect.height / 2) -
642 (tileRect.top + tileRect.height / 2);
643 this.style.webkitTransform =
644 'translate(' + shiftX * scaleX + 'px, ' + shiftY * scaleY + 'px)' +
645 'scaleX(' + scaleX + ') scaleY(' + scaleY + ')';
647 this.style.webkitTransform = '';
652 * @return {number} Item count
655 Mosaic.prototype.getItemCount_ = function() {
656 return this.dataModel_.length;
659 ////////////////////////////////////////////////////////////////////////////////
662 * Creates a selection controller that is to be used with grid.
663 * @param {cr.ui.ListSelectionModel} selectionModel The selection model to
665 * @param {Mosaic.Layout} layoutModel The layout model to use.
667 * @extends {!cr.ui.ListSelectionController}
669 Mosaic.SelectionController = function(selectionModel, layoutModel) {
670 cr.ui.ListSelectionController.call(this, selectionModel);
671 this.layoutModel_ = layoutModel;
675 * Extends cr.ui.ListSelectionController.
677 Mosaic.SelectionController.prototype.__proto__ =
678 cr.ui.ListSelectionController.prototype;
681 Mosaic.SelectionController.prototype.getLastIndex = function() {
682 return this.layoutModel_.getLaidOutTileCount() - 1;
686 Mosaic.SelectionController.prototype.getIndexBefore = function(index) {
687 return this.layoutModel_.getHorizontalAdjacentIndex(index, -1);
691 Mosaic.SelectionController.prototype.getIndexAfter = function(index) {
692 return this.layoutModel_.getHorizontalAdjacentIndex(index, 1);
696 Mosaic.SelectionController.prototype.getIndexAbove = function(index) {
697 return this.layoutModel_.getVerticalAdjacentIndex(index, -1);
701 Mosaic.SelectionController.prototype.getIndexBelow = function(index) {
702 return this.layoutModel_.getVerticalAdjacentIndex(index, 1);
705 ////////////////////////////////////////////////////////////////////////////////
710 * @param {string=} opt_mode Layout mode.
711 * @param {Mosaic.Density=} opt_maxDensity Layout density.
714 Mosaic.Layout = function(opt_mode, opt_maxDensity) {
715 this.mode_ = opt_mode || Mosaic.Layout.MODE_TENTATIVE;
716 this.maxDensity_ = opt_maxDensity || Mosaic.Density.createHighest();
721 * Blank space at the top of the mosaic element. We do not do that in CSS
722 * to make transition effects easier.
724 Mosaic.Layout.PADDING_TOP = 50;
727 * Blank space at the bottom of the mosaic element.
729 Mosaic.Layout.PADDING_BOTTOM = 50;
732 * Horizontal and vertical spacing between images. Should be kept in sync
733 * with the style of .mosaic-item in gallery.css (= 2 * ( 4 + 1))
735 Mosaic.Layout.SPACING = 10;
738 * Margin for scrolling using keyboard. Distance between a selected tile
741 Mosaic.Layout.SCROLL_MARGIN = 30;
744 * Layout mode: commit to DOM immediately.
746 Mosaic.Layout.MODE_FINAL = 'final';
749 * Layout mode: do not commit layout to DOM until it is complete or the viewport
752 Mosaic.Layout.MODE_TENTATIVE = 'tentative';
755 * Layout mode: never commit layout to DOM.
757 Mosaic.Layout.MODE_DRY_RUN = 'dry_run';
764 Mosaic.Layout.prototype.reset_ = function() {
766 this.newColumn_ = null;
767 this.density_ = Mosaic.Density.createLowest();
768 if (this.mode_ !== Mosaic.Layout.MODE_DRY_RUN) // DRY_RUN is sticky.
769 this.mode_ = Mosaic.Layout.MODE_TENTATIVE;
773 * @param {number} width Viewport width.
774 * @param {number} height Viewport height.
776 Mosaic.Layout.prototype.setViewportSize = function(width, height) {
777 this.viewportWidth_ = width;
778 this.viewportHeight_ = height;
783 * @return {number} Total width of the layout.
785 Mosaic.Layout.prototype.getWidth = function() {
786 var lastColumn = this.getLastColumn_();
787 return lastColumn ? lastColumn.getRight() : 0;
791 * @return {number} Total height of the layout.
793 Mosaic.Layout.prototype.getHeight = function() {
794 var firstColumn = this.columns_[0];
795 return firstColumn ? firstColumn.getHeight() : 0;
799 * @return {Array.<Mosaic.Tile>} All tiles in the layout.
801 Mosaic.Layout.prototype.getTiles = function() {
802 return Array.prototype.concat.apply([],
803 this.columns_.map(function(c) { return c.getTiles(); }));
807 * @return {number} Total number of tiles added to the layout.
809 Mosaic.Layout.prototype.getTileCount = function() {
810 return this.getLaidOutTileCount() +
811 (this.newColumn_ ? this.newColumn_.getTileCount() : 0);
815 * @return {Mosaic.Column} The last column or null for empty layout.
818 Mosaic.Layout.prototype.getLastColumn_ = function() {
819 return this.columns_.length ? this.columns_[this.columns_.length - 1] : null;
823 * @return {number} Total number of tiles in completed columns.
825 Mosaic.Layout.prototype.getLaidOutTileCount = function() {
826 var lastColumn = this.getLastColumn_();
827 return lastColumn ? lastColumn.getNextTileIndex() : 0;
831 * Adds a tile to the layout.
833 * @param {Mosaic.Tile} tile The tile to be added.
834 * @param {boolean} isLast True if this tile is the last.
836 Mosaic.Layout.prototype.add = function(tile, isLast) {
837 var layoutQueue = [tile];
839 // There are two levels of backtracking in the layout algorithm.
840 // |Mosaic.Layout.density_| tracks the state of the 'global' backtracking
841 // which aims to use as much of the viewport space as possible.
842 // It starts with the lowest density and increases it until the layout
843 // fits into the viewport. If it does not fit even at the highest density,
844 // the layout continues with the highest density.
846 // |Mosaic.Column.density_| tracks the state of the 'local' backtracking
847 // which aims to avoid producing unnaturally looking columns.
848 // It starts with the current global density and decreases it until the column
851 while (layoutQueue.length) {
852 if (!this.newColumn_) {
853 var lastColumn = this.getLastColumn_();
854 this.newColumn_ = new Mosaic.Column(
855 this.columns_.length,
856 lastColumn ? lastColumn.getNextRowIndex() : 0,
857 lastColumn ? lastColumn.getNextTileIndex() : 0,
858 lastColumn ? lastColumn.getRight() : 0,
859 this.viewportHeight_,
860 this.density_.clone());
863 this.newColumn_.add(layoutQueue.shift());
865 var isFinalColumn = isLast && !layoutQueue.length;
867 if (!this.newColumn_.prepareLayout(isFinalColumn))
868 continue; // Column is incomplete.
870 if (this.newColumn_.isSuboptimal()) {
871 layoutQueue = this.newColumn_.getTiles().concat(layoutQueue);
872 this.newColumn_.retryWithLowerDensity();
876 this.columns_.push(this.newColumn_);
877 this.newColumn_ = null;
879 if (this.mode_ === Mosaic.Layout.MODE_FINAL && isFinalColumn) {
884 if (this.getWidth() > this.viewportWidth_) {
885 // Viewport completely filled.
886 if (this.density_.equals(this.maxDensity_)) {
887 // Max density reached, commit if tentative, just continue if dry run.
888 if (this.mode_ === Mosaic.Layout.MODE_TENTATIVE)
893 // Rollback the entire layout, retry with higher density.
894 layoutQueue = this.getTiles().concat(layoutQueue);
896 this.density_.increase();
900 if (isFinalColumn && this.mode_ === Mosaic.Layout.MODE_TENTATIVE) {
901 // The complete tentative layout fits into the viewport.
902 var stretched = this.findHorizontalLayout_();
904 this.columns_ = stretched.columns_;
905 // Center the layout in the viewport and commit.
906 this.commit_((this.viewportWidth_ - this.getWidth()) / 2,
907 (this.viewportHeight_ - this.getHeight()) / 2);
913 * Commits the tentative layout.
915 * @param {number=} opt_offsetX Horizontal offset.
916 * @param {number=} opt_offsetY Vertical offset.
919 Mosaic.Layout.prototype.commit_ = function(opt_offsetX, opt_offsetY) {
920 for (var i = 0; i !== this.columns_.length; i++) {
921 this.columns_[i].layout(opt_offsetX, opt_offsetY);
923 this.mode_ = Mosaic.Layout.MODE_FINAL;
927 * Finds the most horizontally stretched layout built from the same tiles.
929 * The main layout algorithm fills the entire available viewport height.
930 * If there is too few tiles this results in a layout that is unnaturally
931 * stretched in the vertical direction.
933 * This method tries a number of smaller heights and returns the most
934 * horizontally stretched layout that still fits into the viewport.
936 * @return {Mosaic.Layout} A horizontally stretched layout.
939 Mosaic.Layout.prototype.findHorizontalLayout_ = function() {
940 // If the layout aspect ratio is not dramatically different from
941 // the viewport aspect ratio then there is no need to optimize.
942 if (this.getWidth() / this.getHeight() >
943 this.viewportWidth_ / this.viewportHeight_ * 0.9)
946 var tiles = this.getTiles();
947 if (tiles.length === 1)
948 return null; // Single tile layout is always the same.
950 var tileHeights = tiles.map(function(t) { return t.getMaxContentHeight(); });
951 var minTileHeight = Math.min.apply(null, tileHeights);
953 for (var h = minTileHeight; h < this.viewportHeight_; h += minTileHeight) {
954 var layout = new Mosaic.Layout(
955 Mosaic.Layout.MODE_DRY_RUN, this.density_.clone());
956 layout.setViewportSize(this.viewportWidth_, h);
957 for (var t = 0; t !== tiles.length; t++)
958 layout.add(tiles[t], t + 1 === tiles.length);
960 if (layout.getWidth() <= this.viewportWidth_)
968 * Invalidates the layout after the given tile was modified (added, deleted or
969 * changed dimensions).
971 * @param {number} index Tile index.
974 Mosaic.Layout.prototype.invalidateFromTile_ = function(index) {
975 var columnIndex = this.getColumnIndexByTile_(index);
977 return; // Index not in the layout, probably already invalidated.
979 if (this.columns_[columnIndex].getLeft() >= this.viewportWidth_) {
980 // The columns to the right cover the entire viewport width, so there is no
981 // chance that the modified layout would fit into the viewport.
982 // No point in restarting the entire layout, keep the columns to the right.
983 console.assert(this.mode_ === Mosaic.Layout.MODE_FINAL,
984 'Expected FINAL layout mode');
985 this.columns_ = this.columns_.slice(0, columnIndex);
986 this.newColumn_ = null;
988 // There is a chance that the modified layout would fit into the viewport.
990 this.mode_ = Mosaic.Layout.MODE_TENTATIVE;
995 * Gets the index of the tile to the left or to the right from the given tile.
997 * @param {number} index Tile index.
998 * @param {number} direction -1 for left, 1 for right.
999 * @return {number} Adjacent tile index.
1001 Mosaic.Layout.prototype.getHorizontalAdjacentIndex = function(
1003 var column = this.getColumnIndexByTile_(index);
1005 console.error('Cannot find column for tile #' + index);
1009 var row = this.columns_[column].getRowByTileIndex(index);
1011 console.error('Cannot find row for tile #' + index);
1015 var sameRowNeighbourIndex = index + direction;
1016 if (row.hasTile(sameRowNeighbourIndex))
1017 return sameRowNeighbourIndex;
1019 var adjacentColumn = column + direction;
1020 if (adjacentColumn < 0 || adjacentColumn === this.columns_.length)
1023 return this.columns_[adjacentColumn].
1024 getEdgeTileIndex_(row.getCenterY(), -direction);
1028 * Gets the index of the tile to the top or to the bottom from the given tile.
1030 * @param {number} index Tile index.
1031 * @param {number} direction -1 for above, 1 for below.
1032 * @return {number} Adjacent tile index.
1034 Mosaic.Layout.prototype.getVerticalAdjacentIndex = function(
1036 var column = this.getColumnIndexByTile_(index);
1038 console.error('Cannot find column for tile #' + index);
1042 var row = this.columns_[column].getRowByTileIndex(index);
1044 console.error('Cannot find row for tile #' + index);
1048 // Find the first item in the next row, or the last item in the previous row.
1049 var adjacentRowNeighbourIndex =
1050 row.getEdgeTileIndex_(direction) + direction;
1052 if (adjacentRowNeighbourIndex < 0 ||
1053 adjacentRowNeighbourIndex > this.getTileCount() - 1)
1056 if (!this.columns_[column].hasTile(adjacentRowNeighbourIndex)) {
1057 // It is not in the current column, so return it.
1058 return adjacentRowNeighbourIndex;
1060 // It is in the current column, so we have to find optically the closest
1061 // tile in the adjacent row.
1062 var adjacentRow = this.columns_[column].getRowByTileIndex(
1063 adjacentRowNeighbourIndex);
1064 var previousTileCenterX = row.getTileByIndex(index).getCenterX();
1066 // Find the closest one.
1067 var closestIndex = -1;
1068 var closestDistance;
1069 var adjacentRowTiles = adjacentRow.getTiles();
1070 for (var t = 0; t !== adjacentRowTiles.length; t++) {
1072 Math.abs(adjacentRowTiles[t].getCenterX() - previousTileCenterX);
1073 if (closestIndex === -1 || distance < closestDistance) {
1074 closestIndex = adjacentRow.getEdgeTileIndex_(-1) + t;
1075 closestDistance = distance;
1078 return closestIndex;
1083 * @param {number} index Tile index.
1084 * @return {number} Index of the column containing the given tile.
1087 Mosaic.Layout.prototype.getColumnIndexByTile_ = function(index) {
1088 for (var c = 0; c !== this.columns_.length; c++) {
1089 if (this.columns_[c].hasTile(index))
1096 * Scales the given array of size values to satisfy 3 conditions:
1097 * 1. The new sizes must be integer.
1098 * 2. The new sizes must sum up to the given |total| value.
1099 * 3. The relative proportions of the sizes should be as close to the original
1102 * @param {Array.<number>} sizes Array of sizes.
1103 * @param {number} newTotal New total size.
1105 Mosaic.Layout.rescaleSizesToNewTotal = function(sizes, newTotal) {
1108 var partialTotals = [0];
1109 for (var i = 0; i !== sizes.length; i++) {
1111 partialTotals.push(total);
1114 var scale = newTotal / total;
1116 for (i = 0; i !== sizes.length; i++) {
1117 sizes[i] = Math.round(partialTotals[i + 1] * scale) -
1118 Math.round(partialTotals[i] * scale);
1122 ////////////////////////////////////////////////////////////////////////////////
1125 * Representation of the layout density.
1127 * @param {number} horizontal Horizontal density, number tiles per row.
1128 * @param {number} vertical Vertical density, frequency of rows forced to
1129 * contain a single tile.
1132 Mosaic.Density = function(horizontal, vertical) {
1133 this.horizontal = horizontal;
1134 this.vertical = vertical;
1138 * Minimal horizontal density (tiles per row).
1140 Mosaic.Density.MIN_HORIZONTAL = 1;
1143 * Minimal horizontal density (tiles per row).
1145 Mosaic.Density.MAX_HORIZONTAL = 3;
1148 * Minimal vertical density: force 1 out of 2 rows to containt a single tile.
1150 Mosaic.Density.MIN_VERTICAL = 2;
1153 * Maximal vertical density: force 1 out of 3 rows to containt a single tile.
1155 Mosaic.Density.MAX_VERTICAL = 3;
1158 * @return {Mosaic.Density} Lowest density.
1160 Mosaic.Density.createLowest = function() {
1161 return new Mosaic.Density(
1162 Mosaic.Density.MIN_HORIZONTAL,
1163 Mosaic.Density.MIN_VERTICAL /* ignored when horizontal is at min */);
1167 * @return {Mosaic.Density} Highest density.
1169 Mosaic.Density.createHighest = function() {
1170 return new Mosaic.Density(
1171 Mosaic.Density.MAX_HORIZONTAL,
1172 Mosaic.Density.MAX_VERTICAL);
1176 * @return {Mosaic.Density} A clone of this density object.
1178 Mosaic.Density.prototype.clone = function() {
1179 return new Mosaic.Density(this.horizontal, this.vertical);
1183 * @param {Mosaic.Density} that The other object.
1184 * @return {boolean} True if equal.
1186 Mosaic.Density.prototype.equals = function(that) {
1187 return this.horizontal === that.horizontal &&
1188 this.vertical === that.vertical;
1192 * Increases the density to the next level.
1194 Mosaic.Density.prototype.increase = function() {
1195 if (this.horizontal === Mosaic.Density.MIN_HORIZONTAL ||
1196 this.vertical === Mosaic.Density.MAX_VERTICAL) {
1197 console.assert(this.horizontal < Mosaic.Density.MAX_HORIZONTAL);
1199 this.vertical = Mosaic.Density.MIN_VERTICAL;
1206 * Decreases horizontal density.
1208 Mosaic.Density.prototype.decreaseHorizontal = function() {
1209 console.assert(this.horizontal > Mosaic.Density.MIN_HORIZONTAL);
1214 * @param {number} tileCount Number of tiles in the row.
1215 * @param {number} rowIndex Global row index.
1216 * @return {boolean} True if the row is complete.
1218 Mosaic.Density.prototype.isRowComplete = function(tileCount, rowIndex) {
1219 return (tileCount === this.horizontal) || (rowIndex % this.vertical) === 0;
1222 ////////////////////////////////////////////////////////////////////////////////
1225 * A column in a mosaic layout. Contains rows.
1227 * @param {number} index Column index.
1228 * @param {number} firstRowIndex Global row index.
1229 * @param {number} firstTileIndex Index of the first tile in the column.
1230 * @param {number} left Left edge coordinate.
1231 * @param {number} maxHeight Maximum height.
1232 * @param {Mosaic.Density} density Layout density.
1235 Mosaic.Column = function(index, firstRowIndex, firstTileIndex, left, maxHeight,
1237 this.index_ = index;
1238 this.firstRowIndex_ = firstRowIndex;
1239 this.firstTileIndex_ = firstTileIndex;
1241 this.maxHeight_ = maxHeight;
1242 this.density_ = density;
1248 * Resets the layout.
1251 Mosaic.Column.prototype.reset_ = function() {
1254 this.newRow_ = null;
1258 * @return {number} Number of tiles in the column.
1260 Mosaic.Column.prototype.getTileCount = function() { return this.tiles_.length };
1263 * @return {number} Index of the last tile + 1.
1265 Mosaic.Column.prototype.getNextTileIndex = function() {
1266 return this.firstTileIndex_ + this.getTileCount();
1270 * @return {number} Global index of the last row + 1.
1272 Mosaic.Column.prototype.getNextRowIndex = function() {
1273 return this.firstRowIndex_ + this.rows_.length;
1277 * @return {Array.<Mosaic.Tile>} Array of tiles in the column.
1279 Mosaic.Column.prototype.getTiles = function() { return this.tiles_ };
1282 * @param {number} index Tile index.
1283 * @return {boolean} True if this column contains the tile with the given index.
1285 Mosaic.Column.prototype.hasTile = function(index) {
1286 return this.firstTileIndex_ <= index &&
1287 index < (this.firstTileIndex_ + this.getTileCount());
1291 * @param {number} y Y coordinate.
1292 * @param {number} direction -1 for left, 1 for right.
1293 * @return {number} Index of the tile lying on the edge of the column at the
1294 * given y coordinate.
1297 Mosaic.Column.prototype.getEdgeTileIndex_ = function(y, direction) {
1298 for (var r = 0; r < this.rows_.length; r++) {
1299 if (this.rows_[r].coversY(y))
1300 return this.rows_[r].getEdgeTileIndex_(direction);
1306 * @param {number} index Tile index.
1307 * @return {Mosaic.Row} The row containing the tile with a given index.
1309 Mosaic.Column.prototype.getRowByTileIndex = function(index) {
1310 for (var r = 0; r !== this.rows_.length; r++) {
1311 if (this.rows_[r].hasTile(index))
1312 return this.rows_[r];
1318 * Adds a tile to the column.
1320 * @param {Mosaic.Tile} tile The tile to add.
1322 Mosaic.Column.prototype.add = function(tile) {
1323 var rowIndex = this.getNextRowIndex();
1326 this.newRow_ = new Mosaic.Row(this.getNextTileIndex());
1328 this.tiles_.push(tile);
1329 this.newRow_.add(tile);
1331 if (this.density_.isRowComplete(this.newRow_.getTileCount(), rowIndex)) {
1332 this.rows_.push(this.newRow_);
1333 this.newRow_ = null;
1338 * Prepares the column layout.
1340 * @param {boolean=} opt_force True if the layout must be performed even for an
1341 * incomplete column.
1342 * @return {boolean} True if the layout was performed.
1344 Mosaic.Column.prototype.prepareLayout = function(opt_force) {
1345 if (opt_force && this.newRow_) {
1346 this.rows_.push(this.newRow_);
1347 this.newRow_ = null;
1350 if (this.rows_.length === 0)
1353 this.width_ = Math.min.apply(
1354 null, this.rows_.map(function(row) { return row.getMaxWidth() }));
1358 this.rowHeights_ = [];
1359 for (var r = 0; r !== this.rows_.length; r++) {
1360 var rowHeight = this.rows_[r].getHeightForWidth(this.width_);
1361 this.height_ += rowHeight;
1362 this.rowHeights_.push(rowHeight);
1365 var overflow = this.height_ / this.maxHeight_;
1366 if (!opt_force && (overflow < 1))
1370 // Scale down the column width and height.
1371 this.width_ = Math.round(this.width_ / overflow);
1372 this.height_ = this.maxHeight_;
1373 Mosaic.Layout.rescaleSizesToNewTotal(this.rowHeights_, this.maxHeight_);
1380 * Retries the column layout with less tiles per row.
1382 Mosaic.Column.prototype.retryWithLowerDensity = function() {
1383 this.density_.decreaseHorizontal();
1388 * @return {number} Column left edge coordinate.
1390 Mosaic.Column.prototype.getLeft = function() { return this.left_ };
1393 * @return {number} Column right edge coordinate after the layout.
1395 Mosaic.Column.prototype.getRight = function() {
1396 return this.left_ + this.width_;
1400 * @return {number} Column height after the layout.
1402 Mosaic.Column.prototype.getHeight = function() { return this.height_ };
1405 * Performs the column layout.
1406 * @param {number=} opt_offsetX Horizontal offset.
1407 * @param {number=} opt_offsetY Vertical offset.
1409 Mosaic.Column.prototype.layout = function(opt_offsetX, opt_offsetY) {
1410 opt_offsetX = opt_offsetX || 0;
1411 opt_offsetY = opt_offsetY || 0;
1412 var rowTop = Mosaic.Layout.PADDING_TOP;
1413 for (var r = 0; r !== this.rows_.length; r++) {
1414 this.rows_[r].layout(
1415 opt_offsetX + this.left_,
1416 opt_offsetY + rowTop,
1418 this.rowHeights_[r]);
1419 rowTop += this.rowHeights_[r];
1424 * Checks if the column layout is too ugly to be displayed.
1426 * @return {boolean} True if the layout is suboptimal.
1428 Mosaic.Column.prototype.isSuboptimal = function() {
1430 this.rows_.map(function(row) { return row.getTileCount() });
1432 var maxTileCount = Math.max.apply(null, tileCounts);
1433 if (maxTileCount === 1)
1434 return false; // Every row has exactly 1 tile, as optimal as it gets.
1437 this.tiles_.map(function(tile) { return tile.getMaxContentHeight() });
1439 // Ugly layout #1: all images are small and some are one the same row.
1440 var allSmall = Math.max.apply(null, sizes) <= Mosaic.Tile.SMALL_IMAGE_SIZE;
1444 // Ugly layout #2: all images are large and none occupies an entire row.
1445 var allLarge = Math.min.apply(null, sizes) > Mosaic.Tile.SMALL_IMAGE_SIZE;
1446 var allCombined = Math.min.apply(null, tileCounts) !== 1;
1447 if (allLarge && allCombined)
1450 // Ugly layout #3: some rows have too many tiles for the resulting width.
1451 if (this.width_ / maxTileCount < 100)
1457 ////////////////////////////////////////////////////////////////////////////////
1460 * A row in a mosaic layout. Contains tiles.
1462 * @param {number} firstTileIndex Index of the first tile in the row.
1465 Mosaic.Row = function(firstTileIndex) {
1466 this.firstTileIndex_ = firstTileIndex;
1471 * @param {Mosaic.Tile} tile The tile to add.
1473 Mosaic.Row.prototype.add = function(tile) {
1474 console.assert(this.getTileCount() < Mosaic.Density.MAX_HORIZONTAL);
1475 this.tiles_.push(tile);
1479 * @return {Array.<Mosaic.Tile>} Array of tiles in the row.
1481 Mosaic.Row.prototype.getTiles = function() { return this.tiles_ };
1484 * Gets a tile by index.
1485 * @param {number} index Tile index.
1486 * @return {Mosaic.Tile} Requested tile or null if not found.
1488 Mosaic.Row.prototype.getTileByIndex = function(index) {
1489 if (!this.hasTile(index))
1491 return this.tiles_[index - this.firstTileIndex_];
1496 * @return {number} Number of tiles in the row.
1498 Mosaic.Row.prototype.getTileCount = function() { return this.tiles_.length };
1501 * @param {number} index Tile index.
1502 * @return {boolean} True if this row contains the tile with the given index.
1504 Mosaic.Row.prototype.hasTile = function(index) {
1505 return this.firstTileIndex_ <= index &&
1506 index < (this.firstTileIndex_ + this.tiles_.length);
1510 * @param {number} y Y coordinate.
1511 * @return {boolean} True if this row covers the given Y coordinate.
1513 Mosaic.Row.prototype.coversY = function(y) {
1514 return this.top_ <= y && y < (this.top_ + this.height_);
1518 * @return {number} Y coordinate of the tile center.
1520 Mosaic.Row.prototype.getCenterY = function() {
1521 return this.top_ + Math.round(this.height_ / 2);
1525 * Gets the first or the last tile.
1527 * @param {number} direction -1 for the first tile, 1 for the last tile.
1528 * @return {number} Tile index.
1531 Mosaic.Row.prototype.getEdgeTileIndex_ = function(direction) {
1533 return this.firstTileIndex_;
1535 return this.firstTileIndex_ + this.getTileCount() - 1;
1539 * @return {number} Aspect ration of the combined content box of this row.
1542 Mosaic.Row.prototype.getTotalContentAspectRatio_ = function() {
1544 for (var t = 0; t !== this.tiles_.length; t++)
1545 sum += this.tiles_[t].getAspectRatio();
1550 * @return {number} Total horizontal spacing in this row. This includes
1551 * the spacing between the tiles and both left and right margins.
1555 Mosaic.Row.prototype.getTotalHorizontalSpacing_ = function() {
1556 return Mosaic.Layout.SPACING * this.getTileCount();
1560 * @return {number} Maximum width that this row may have without overscaling
1563 Mosaic.Row.prototype.getMaxWidth = function() {
1564 var contentHeight = Math.min.apply(null,
1565 this.tiles_.map(function(tile) { return tile.getMaxContentHeight() }));
1568 Math.round(contentHeight * this.getTotalContentAspectRatio_());
1569 return contentWidth + this.getTotalHorizontalSpacing_();
1573 * Computes the height that best fits the supplied row width given
1574 * aspect ratios of the tiles in this row.
1576 * @param {number} width Row width.
1577 * @return {number} Height.
1579 Mosaic.Row.prototype.getHeightForWidth = function(width) {
1580 var contentWidth = width - this.getTotalHorizontalSpacing_();
1582 Math.round(contentWidth / this.getTotalContentAspectRatio_());
1583 return contentHeight + Mosaic.Layout.SPACING;
1587 * Positions the row in the mosaic.
1589 * @param {number} left Left position.
1590 * @param {number} top Top position.
1591 * @param {number} width Width.
1592 * @param {number} height Height.
1594 Mosaic.Row.prototype.layout = function(left, top, width, height) {
1596 this.height_ = height;
1598 var contentWidth = width - this.getTotalHorizontalSpacing_();
1599 var contentHeight = height - Mosaic.Layout.SPACING;
1601 var tileContentWidth = this.tiles_.map(
1602 function(tile) { return tile.getAspectRatio() });
1604 Mosaic.Layout.rescaleSizesToNewTotal(tileContentWidth, contentWidth);
1606 var tileLeft = left;
1607 for (var t = 0; t !== this.tiles_.length; t++) {
1608 var tileWidth = tileContentWidth[t] + Mosaic.Layout.SPACING;
1609 this.tiles_[t].layout(tileLeft, top, tileWidth, height);
1610 tileLeft += tileWidth;
1614 ////////////////////////////////////////////////////////////////////////////////
1617 * A single tile of the image mosaic.
1619 * @param {Element} container Container element.
1620 * @param {Gallery.Item} item Gallery item associated with this tile.
1621 * @param {EntryLocation} locationInfo Location information for the tile.
1622 * @return {Element} The new tile element.
1625 Mosaic.Tile = function(container, item, locationInfo) {
1626 var self = container.ownerDocument.createElement('div');
1627 Mosaic.Tile.decorate(self, container, item, locationInfo);
1632 * @param {Element} self Self pointer.
1633 * @param {Element} container Container element.
1634 * @param {Gallery.Item} item Gallery item associated with this tile.
1635 * @param {EntryLocation} locationInfo Location info for the tile image.
1637 Mosaic.Tile.decorate = function(self, container, item, locationInfo) {
1638 self.__proto__ = Mosaic.Tile.prototype;
1639 self.className = 'mosaic-tile';
1641 self.container_ = container;
1643 self.left_ = null; // Mark as not laid out.
1644 self.hidpiEmbedded_ = locationInfo && locationInfo.isDriveBased;
1648 * Load mode for the tile's image.
1651 Mosaic.Tile.LoadMode = {
1657 * Inherit from HTMLDivElement.
1659 Mosaic.Tile.prototype.__proto__ = HTMLDivElement.prototype;
1662 * Minimum tile content size.
1664 Mosaic.Tile.MIN_CONTENT_SIZE = 64;
1667 * Maximum tile content size.
1669 Mosaic.Tile.MAX_CONTENT_SIZE = 512;
1672 * Default size for a tile with no thumbnail image.
1674 Mosaic.Tile.GENERIC_ICON_SIZE = 128;
1677 * Max size of an image considered to be 'small'.
1678 * Small images are laid out slightly differently.
1680 Mosaic.Tile.SMALL_IMAGE_SIZE = 160;
1683 * @return {Gallery.Item} The Gallery item.
1685 Mosaic.Tile.prototype.getItem = function() { return this.item_; };
1688 * @return {number} Maximum content height that this tile can have.
1690 Mosaic.Tile.prototype.getMaxContentHeight = function() {
1691 return this.maxContentHeight_;
1695 * @return {number} The aspect ratio of the tile image.
1697 Mosaic.Tile.prototype.getAspectRatio = function() { return this.aspectRatio_; };
1700 * @return {boolean} True if the tile is initialized.
1702 Mosaic.Tile.prototype.isInitialized = function() {
1703 return !!this.maxContentHeight_;
1707 * Checks whether the image of specified (or better resolution) has been loaded.
1709 * @param {Mosaic.Tile.LoadMode=} opt_loadMode Loading mode, default: LOW_DPI.
1710 * @return {boolean} True if the tile is loaded with the specified dpi or
1713 Mosaic.Tile.prototype.isLoaded = function(opt_loadMode) {
1714 var loadMode = opt_loadMode || Mosaic.Tile.LoadMode.LOW_DPI;
1716 case Mosaic.Tile.LoadMode.LOW_DPI:
1717 if (this.imagePreloaded_ || this.imageLoaded_)
1720 case Mosaic.Tile.LoadMode.HIGH_DPI:
1721 if (this.imageLoaded_)
1729 * Checks whether the image of specified (or better resolution) is being loaded.
1731 * @param {Mosaic.Tile.LoadMode=} opt_loadMode Loading mode, default: LOW_DPI.
1732 * @return {boolean} True if the tile is being loaded with the specified dpi or
1735 Mosaic.Tile.prototype.isLoading = function(opt_loadMode) {
1736 var loadMode = opt_loadMode || Mosaic.Tile.LoadMode.LOW_DPI;
1738 case Mosaic.Tile.LoadMode.LOW_DPI:
1739 if (this.imagePreloading_ || this.imageLoading_)
1742 case Mosaic.Tile.LoadMode.HIGH_DPI:
1743 if (this.imageLoading_)
1751 * Marks the tile as not loaded to prevent it from participating in the layout.
1753 Mosaic.Tile.prototype.markUnloaded = function() {
1754 this.maxContentHeight_ = 0;
1755 if (this.thumbnailLoader_) {
1756 this.thumbnailLoader_.cancel();
1757 this.imagePreloaded_ = false;
1758 this.imagePreloading_ = false;
1759 this.imageLoaded_ = false;
1760 this.imageLoading_ = false;
1765 * Initializes the thumbnail in the tile. Does not load an image, but sets
1766 * target dimensions using metadata.
1768 Mosaic.Tile.prototype.init = function() {
1769 var metadata = this.getItem().getMetadata();
1770 this.markUnloaded();
1771 this.left_ = null; // Mark as not laid out.
1773 // Set higher priority for the selected elements to load them first.
1774 var priority = this.getAttribute('selected') ? 2 : 3;
1776 // Use embedded thumbnails on Drive, since they have higher resolution.
1777 this.thumbnailLoader_ = new ThumbnailLoader(
1778 this.getItem().getEntry(),
1779 ThumbnailLoader.LoaderType.CANVAS,
1781 undefined, // Media type.
1782 this.hidpiEmbedded_ ?
1783 ThumbnailLoader.UseEmbedded.USE_EMBEDDED :
1784 ThumbnailLoader.UseEmbedded.NO_EMBEDDED,
1787 // If no hidpi embedded thumbnail available, then use the low resolution
1789 if (!this.hidpiEmbedded_) {
1790 this.thumbnailPreloader_ = new ThumbnailLoader(
1791 this.getItem().getEntry(),
1792 ThumbnailLoader.LoaderType.CANVAS,
1794 undefined, // Media type.
1795 ThumbnailLoader.UseEmbedded.USE_EMBEDDED,
1796 // Preloaders have always higher priotity, so the preload images
1797 // are loaded as soon as possible.
1801 // Dimensions are always acquired from the metadata. For local files, it is
1802 // extracted from headers. For Drive files, it is received via the Drive API.
1803 // If the dimensions are not available, then the fallback dimensions will be
1804 // used (same as for the generic icon).
1807 if (metadata.media && metadata.media.width) {
1808 width = metadata.media.width;
1809 height = metadata.media.height;
1810 } else if (metadata.external && metadata.external.imageWidth &&
1811 metadata.external.imageHeight) {
1812 width = metadata.external.imageWidth;
1813 height = metadata.external.imageHeight;
1815 // No dimensions in metadata, then use the generic dimensions.
1816 width = Mosaic.Tile.GENERIC_ICON_SIZE;
1817 height = Mosaic.Tile.GENERIC_ICON_SIZE;
1820 if (width > height) {
1821 if (width > Mosaic.Tile.MAX_CONTENT_SIZE) {
1822 height = Math.round(height * Mosaic.Tile.MAX_CONTENT_SIZE / width);
1823 width = Mosaic.Tile.MAX_CONTENT_SIZE;
1826 if (height > Mosaic.Tile.MAX_CONTENT_SIZE) {
1827 width = Math.round(width * Mosaic.Tile.MAX_CONTENT_SIZE / height);
1828 height = Mosaic.Tile.MAX_CONTENT_SIZE;
1831 this.maxContentHeight_ = Math.max(Mosaic.Tile.MIN_CONTENT_SIZE, height);
1832 this.aspectRatio_ = width / height;
1836 * Loads an image into the tile.
1838 * The mode argument is a hint. Use low-dpi for faster response, and high-dpi
1839 * for better output, but possibly affecting performance.
1841 * If the mode is high-dpi, then a the high-dpi image is loaded, but also
1842 * low-dpi image is loaded for preloading (if available).
1843 * For the low-dpi mode, only low-dpi image is loaded. If not available, then
1844 * the high-dpi image is loaded as a fallback.
1846 * @param {Mosaic.Tile.LoadMode} loadMode Loading mode.
1847 * @param {function(boolean)} onImageLoaded Callback when image is loaded.
1848 * The argument is true for success, false for failure.
1850 Mosaic.Tile.prototype.load = function(loadMode, onImageLoaded) {
1851 // Attaches the image to the tile and finalizes loading process for the
1852 // specified loader.
1853 var finalizeLoader = function(mode, success, loader) {
1854 if (success && this.wrapper_) {
1855 // Show the fade-in animation only when previously there was no image
1856 // attached in this tile.
1857 if (!this.imageLoaded_ && !this.imagePreloaded_)
1858 this.wrapper_.classList.add('animated');
1860 this.wrapper_.classList.remove('animated');
1862 loader.attachImage(this.wrapper_, ThumbnailLoader.FillMode.OVER_FILL);
1863 onImageLoaded(success);
1865 case Mosaic.Tile.LoadMode.LOW_DPI:
1866 this.imagePreloading_ = false;
1867 this.imagePreloaded_ = true;
1869 case Mosaic.Tile.LoadMode.HIGH_DPI:
1870 this.imageLoading_ = false;
1871 this.imageLoaded_ = true;
1876 // Always load the low-dpi image first if it is available for the fastest
1878 if (!this.imagePreloading_ && this.thumbnailPreloader_) {
1879 this.imagePreloading_ = true;
1880 this.thumbnailPreloader_.loadDetachedImage(function(success) {
1881 // Hi-dpi loaded first, ignore this call then.
1882 if (this.imageLoaded_)
1884 finalizeLoader(Mosaic.Tile.LoadMode.LOW_DPI,
1886 this.thumbnailPreloader_);
1890 // Load the high-dpi image only when it is requested, or the low-dpi is not
1892 if (!this.imageLoading_ &&
1893 (loadMode === Mosaic.Tile.LoadMode.HIGH_DPI || !this.imagePreloading_)) {
1894 this.imageLoading_ = true;
1895 this.thumbnailLoader_.loadDetachedImage(function(success) {
1896 // Cancel preloading, since the hi-dpi image is ready.
1897 if (this.thumbnailPreloader_)
1898 this.thumbnailPreloader_.cancel();
1899 finalizeLoader(Mosaic.Tile.LoadMode.HIGH_DPI,
1901 this.thumbnailLoader_);
1907 * Unloads an image from the tile.
1909 Mosaic.Tile.prototype.unload = function() {
1910 this.thumbnailLoader_.cancel();
1911 if (this.thumbnailPreloader_)
1912 this.thumbnailPreloader_.cancel();
1913 this.imagePreloaded_ = false;
1914 this.imageLoaded_ = false;
1915 this.imagePreloading_ = false;
1916 this.imageLoading_ = false;
1917 this.wrapper_.innerText = '';
1921 * Selects/unselects the tile.
1923 * @param {boolean} on True if selected.
1925 Mosaic.Tile.prototype.select = function(on) {
1927 this.setAttribute('selected', true);
1929 this.removeAttribute('selected');
1933 * Positions the tile in the mosaic.
1935 * @param {number} left Left position.
1936 * @param {number} top Top position.
1937 * @param {number} width Width.
1938 * @param {number} height Height.
1940 Mosaic.Tile.prototype.layout = function(left, top, width, height) {
1943 this.width_ = width;
1944 this.height_ = height;
1946 this.style.left = left + 'px';
1947 this.style.top = top + 'px';
1948 this.style.width = width + 'px';
1949 this.style.height = height + 'px';
1951 if (!this.wrapper_) { // First time, create DOM.
1952 this.container_.appendChild(this);
1953 var border = util.createChild(this, 'img-border');
1954 this.wrapper_ = util.createChild(border, 'img-wrapper');
1956 if (this.hasAttribute('selected'))
1957 this.scrollIntoView(false);
1959 if (this.imageLoaded_) {
1960 this.thumbnailLoader_.attachImage(this.wrapper_,
1961 ThumbnailLoader.FillMode.OVER_FILL);
1966 * If the tile is not fully visible scroll the parent to make it fully visible.
1967 * @param {boolean=} opt_animated True, if scroll should be animated,
1970 Mosaic.Tile.prototype.scrollIntoView = function(opt_animated) {
1971 if (this.left_ === null) // Not laid out.
1975 var tileLeft = this.left_ - Mosaic.Layout.SCROLL_MARGIN;
1976 if (tileLeft < this.container_.scrollLeft) {
1977 targetPosition = tileLeft;
1979 var tileRight = this.left_ + this.width_ + Mosaic.Layout.SCROLL_MARGIN;
1980 var scrollRight = this.container_.scrollLeft + this.container_.clientWidth;
1981 if (tileRight > scrollRight)
1982 targetPosition = tileRight - this.container_.clientWidth;
1985 if (targetPosition) {
1986 if (opt_animated === false)
1987 this.container_.scrollLeft = targetPosition;
1989 this.container_.animatedScrollTo(targetPosition);
1994 * @return {ImageRect} Rectangle occupied by the tile's image,
1995 * relative to the viewport.
1997 Mosaic.Tile.prototype.getImageRect = function() {
1998 if (this.left_ === null) // Not laid out.
2001 var margin = Mosaic.Layout.SPACING / 2;
2002 return new ImageRect(this.left_ - this.container_.scrollLeft, this.top_,
2003 this.width_, this.height_).inflate(-margin, -margin);
2007 * @return {number} X coordinate of the tile center.
2009 Mosaic.Tile.prototype.getCenterX = function() {
2010 return this.left_ + Math.round(this.width_ / 2);