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.
8 * @param {Element} container Content container.
9 * @param {cr.ui.ArrayDataModel} dataModel Data model.
10 * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
11 * @param {MetadataCache} metadataCache Metadata cache.
12 * @param {VolumeManagerWrapper} volumeManager Volume manager.
13 * @param {function} toggleMode Function to switch to the Slide mode.
17 container, dataModel, selectionModel, metadataCache, volumeManager,
19 this.mosaic_ = new Mosaic(
20 container.ownerDocument, dataModel, selectionModel, metadataCache,
22 container.appendChild(this.mosaic_);
24 this.toggleMode_ = toggleMode;
25 this.mosaic_.addEventListener('dblclick', this.toggleMode_);
26 this.showingTimeoutID_ = null;
30 * @return {Mosaic} The mosaic control.
32 MosaicMode.prototype.getMosaic = function() { return this.mosaic_ };
35 * @return {string} Mode name.
37 MosaicMode.prototype.getName = function() { return 'mosaic' };
40 * @return {string} Mode title.
42 MosaicMode.prototype.getTitle = function() { return 'GALLERY_MOSAIC' };
45 * Execute an action (this mode has no busy state).
46 * @param {function} action Action to execute.
48 MosaicMode.prototype.executeWhenReady = function(action) { action() };
51 * @return {boolean} Always true (no toolbar fading in this mode).
53 MosaicMode.prototype.hasActiveTool = function() { return true };
58 * @param {Event} event Event.
60 MosaicMode.prototype.onKeyDown = function(event) {
61 switch (util.getKeyModifiers(event) + event.keyIdentifier) {
63 if (!document.activeElement ||
64 document.activeElement.localName !== 'button') {
66 event.preventDefault();
70 this.mosaic_.onKeyDown(event);
73 ////////////////////////////////////////////////////////////////////////////////
78 * @param {Document} document Document.
79 * @param {cr.ui.ArrayDataModel} dataModel Data model.
80 * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
81 * @param {MetadataCache} metadataCache Metadata cache.
82 * @param {VolumeManagerWrapper} volumeManager Volume manager.
83 * @return {Element} Mosaic element.
86 function Mosaic(document, dataModel, selectionModel, metadataCache,
88 var self = document.createElement('div');
90 self, dataModel, selectionModel, metadataCache, volumeManager);
95 * Inherits from HTMLDivElement.
97 Mosaic.prototype.__proto__ = HTMLDivElement.prototype;
100 * Default layout delay in ms.
104 Mosaic.LAYOUT_DELAY = 200;
107 * Smooth scroll animation duration when scrolling using keyboard or
108 * clicking on a partly visible tile. In ms.
112 Mosaic.ANIMATED_SCROLL_DURATION = 500;
115 * Decorates a Mosaic instance.
117 * @param {Mosaic} self Self pointer.
118 * @param {cr.ui.ArrayDataModel} dataModel Data model.
119 * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
120 * @param {MetadataCache} metadataCache Metadata cache.
121 * @param {VolumeManagerWrapper} volumeManager Volume manager.
123 Mosaic.decorate = function(
124 self, dataModel, selectionModel, metadataCache, volumeManager) {
125 self.__proto__ = Mosaic.prototype;
126 self.className = 'mosaic';
128 self.dataModel_ = dataModel;
129 self.selectionModel_ = selectionModel;
130 self.metadataCache_ = metadataCache;
131 self.volumeManager_ = volumeManager;
133 // Initialization is completed lazily on the first call to |init|.
137 * Initializes the mosaic element.
139 Mosaic.prototype.init = function() {
141 return; // Already initialized, nothing to do.
143 this.layoutModel_ = new Mosaic.Layout();
146 this.selectionController_ =
147 new Mosaic.SelectionController(this.selectionModel_, this.layoutModel_);
150 for (var i = 0; i !== this.dataModel_.length; i++) {
152 this.volumeManager_.getLocationInfo(this.dataModel_.item(i).getEntry());
154 new Mosaic.Tile(this, this.dataModel_.item(i), locationInfo));
157 this.selectionModel_.selectedIndexes.forEach(function(index) {
158 this.tiles_[index].select(true);
161 this.initTiles_(this.tiles_);
163 // The listeners might be called while some tiles are still loading.
164 this.initListeners_();
168 * @return {boolean} Whether mosaic is initialized.
170 Mosaic.prototype.isInitialized = function() {
171 return !!this.tiles_;
175 * Starts listening to events.
177 * We keep listening to events even when the mosaic is hidden in order to
178 * keep the layout up to date.
182 Mosaic.prototype.initListeners_ = function() {
183 this.ownerDocument.defaultView.addEventListener(
184 'resize', this.onResize_.bind(this));
186 var mouseEventBound = this.onMouseEvent_.bind(this);
187 this.addEventListener('mousemove', mouseEventBound);
188 this.addEventListener('mousedown', mouseEventBound);
189 this.addEventListener('mouseup', mouseEventBound);
190 this.addEventListener('scroll', this.onScroll_.bind(this));
192 this.selectionModel_.addEventListener('change', this.onSelection_.bind(this));
193 this.selectionModel_.addEventListener('leadIndexChange',
194 this.onLeadChange_.bind(this));
196 this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
197 this.dataModel_.addEventListener('content', this.onContentChange_.bind(this));
201 * Smoothly scrolls the container to the specified position using
202 * f(x) = sqrt(x) speed function normalized to animation duration.
203 * @param {number} targetPosition Horizontal scroll position in pixels.
205 Mosaic.prototype.animatedScrollTo = function(targetPosition) {
206 if (this.scrollAnimation_) {
207 webkitCancelAnimationFrame(this.scrollAnimation_);
208 this.scrollAnimation_ = null;
211 // Mouse move events are fired without touching the mouse because of scrolling
212 // the container. Therefore, these events have to be suppressed.
213 this.suppressHovering_ = true;
215 // Calculates integral area from t1 to t2 of f(x) = sqrt(x) dx.
216 var integral = function(t1, t2) {
217 return 2.0 / 3.0 * Math.pow(t2, 3.0 / 2.0) -
218 2.0 / 3.0 * Math.pow(t1, 3.0 / 2.0);
221 var delta = targetPosition - this.scrollLeft;
222 var factor = delta / integral(0, Mosaic.ANIMATED_SCROLL_DURATION);
223 var startTime = Date.now();
224 var lastPosition = 0;
225 var scrollOffset = this.scrollLeft;
227 var animationFrame = function() {
228 var position = Date.now() - startTime;
230 integral(Math.max(0, Mosaic.ANIMATED_SCROLL_DURATION - position),
231 Math.max(0, Mosaic.ANIMATED_SCROLL_DURATION - lastPosition));
232 scrollOffset += step;
234 var oldScrollLeft = this.scrollLeft;
235 var newScrollLeft = Math.round(scrollOffset);
237 if (oldScrollLeft !== newScrollLeft)
238 this.scrollLeft = newScrollLeft;
240 if (step === 0 || this.scrollLeft !== newScrollLeft) {
241 this.scrollAnimation_ = null;
242 // Release the hovering lock after a safe delay to avoid hovering
243 // a tile because of altering |this.scrollLeft|.
244 setTimeout(function() {
245 if (!this.scrollAnimation_)
246 this.suppressHovering_ = false;
249 // Continue the animation.
250 this.scrollAnimation_ = requestAnimationFrame(animationFrame);
253 lastPosition = position;
256 // Start the animation.
257 this.scrollAnimation_ = requestAnimationFrame(animationFrame);
261 * @return {Mosaic.Tile} Selected tile or undefined if no selection.
263 Mosaic.prototype.getSelectedTile = function() {
264 return this.tiles_ && this.tiles_[this.selectionModel_.selectedIndex];
268 * @param {number} index Tile index.
269 * @return {Rect} Tile's image rectangle.
271 Mosaic.prototype.getTileRect = function(index) {
272 var tile = this.tiles_[index];
273 return tile && tile.getImageRect();
277 * @param {number} index Tile index.
278 * Scroll the given tile into the viewport.
280 Mosaic.prototype.scrollIntoView = function(index) {
281 var tile = this.tiles_[index];
282 if (tile) tile.scrollIntoView();
286 * Initializes multiple tiles.
288 * @param {Array.<Mosaic.Tile>} tiles Array of tiles.
289 * @param {function()=} opt_callback Completion callback.
292 Mosaic.prototype.initTiles_ = function(tiles, opt_callback) {
293 // We do not want to use tile indices in asynchronous operations because they
294 // do not survive data model splices. Copy tile references instead.
295 tiles = tiles.slice();
297 // Throttle the metadata access so that we do not overwhelm the file system.
298 var MAX_CHUNK_SIZE = 10;
300 var loadChunk = function() {
302 if (opt_callback) opt_callback();
305 var chunkSize = Math.min(tiles.length, MAX_CHUNK_SIZE);
307 for (var i = 0; i !== chunkSize; i++) {
308 this.initTile_(tiles.shift(), function() {
309 if (++loaded === chunkSize) {
321 * Initializes a single tile.
323 * @param {Mosaic.Tile} tile Tile.
324 * @param {function()} callback Completion callback.
327 Mosaic.prototype.initTile_ = function(tile, callback) {
328 var onImageMeasured = callback;
329 this.metadataCache_.getOne(tile.getItem().getEntry(), Gallery.METADATA_TYPE,
331 tile.init(metadata, onImageMeasured);
338 Mosaic.prototype.reload = function() {
339 this.layoutModel_.reset_();
340 this.tiles_.forEach(function(t) { t.markUnloaded() });
341 this.initTiles_(this.tiles_);
345 * Layouts the tiles in the order of their indices.
347 * Starts where it last stopped (at #0 the first time).
348 * Stops when all tiles are processed or when the next tile is still loading.
350 Mosaic.prototype.layout = function() {
351 if (this.layoutTimer_) {
352 clearTimeout(this.layoutTimer_);
353 this.layoutTimer_ = null;
356 var index = this.layoutModel_.getTileCount();
357 if (index === this.tiles_.length)
358 break; // All tiles done.
359 var tile = this.tiles_[index];
360 if (!tile.isInitialized())
361 break; // Next layout will try to restart from here.
362 this.layoutModel_.add(tile, index + 1 === this.tiles_.length);
364 this.loadVisibleTiles_();
368 * Schedules the layout.
370 * @param {number=} opt_delay Delay in ms.
372 Mosaic.prototype.scheduleLayout = function(opt_delay) {
373 if (!this.layoutTimer_) {
374 this.layoutTimer_ = setTimeout(function() {
375 this.layoutTimer_ = null;
377 }.bind(this), opt_delay || 0);
386 Mosaic.prototype.onResize_ = function() {
387 this.layoutModel_.setViewportSize(this.clientWidth, this.clientHeight -
388 (Mosaic.Layout.PADDING_TOP + Mosaic.Layout.PADDING_BOTTOM));
389 this.scheduleLayout();
393 * Mouse event handler.
395 * @param {Event} event Event.
398 Mosaic.prototype.onMouseEvent_ = function(event) {
399 // Navigating with mouse, enable hover state.
400 if (!this.suppressHovering_)
401 this.classList.add('hover-visible');
403 if (event.type === 'mousemove')
407 for (var target = event.target;
408 target && (target !== this);
409 target = target.parentNode) {
410 if (target.classList.contains('mosaic-tile')) {
411 index = this.dataModel_.indexOf(target.getItem());
415 this.selectionController_.handlePointerDownUp(event, index);
422 Mosaic.prototype.onScroll_ = function() {
423 requestAnimationFrame(function() {
424 this.loadVisibleTiles_();
429 * Selection change handler.
431 * @param {Event} event Event.
434 Mosaic.prototype.onSelection_ = function(event) {
435 for (var i = 0; i !== event.changes.length; i++) {
436 var change = event.changes[i];
437 var tile = this.tiles_[change.index];
438 if (tile) tile.select(change.selected);
443 * Leads item change handler.
445 * @param {Event} event Event.
448 Mosaic.prototype.onLeadChange_ = function(event) {
449 var index = event.newValue;
451 var tile = this.tiles_[index];
452 if (tile) tile.scrollIntoView();
457 * Splice event handler.
459 * @param {Event} event Event.
462 Mosaic.prototype.onSplice_ = function(event) {
463 var index = event.index;
464 this.layoutModel_.invalidateFromTile_(index);
466 if (event.removed.length) {
467 for (var t = 0; t !== event.removed.length; t++) {
468 // If the layout for the tile has not done yet, the parent is null.
469 // And the layout will not be done after onSplice_ because it is removed
471 if (this.tiles_[index + t].parentNode)
472 this.removeChild(this.tiles_[index + t]);
475 this.tiles_.splice(index, event.removed.length);
476 this.scheduleLayout(Mosaic.LAYOUT_DELAY);
479 if (event.added.length) {
481 for (var t = 0; t !== event.added.length; t++)
482 newTiles.push(new Mosaic.Tile(this, this.dataModel_.item(index + t)));
484 this.tiles_.splice.apply(this.tiles_, [index, 0].concat(newTiles));
485 this.initTiles_(newTiles);
488 if (this.tiles_.length !== this.dataModel_.length)
489 console.error('Mosaic is out of sync');
493 * Content change handler.
495 * @param {Event} event Event.
498 Mosaic.prototype.onContentChange_ = function(event) {
503 return; // Thumbnail unchanged, nothing to do.
505 var index = this.dataModel_.indexOf(event.item);
506 if (index !== this.selectionModel_.selectedIndex)
507 console.error('Content changed for unselected item');
509 this.layoutModel_.invalidateFromTile_(index);
510 this.tiles_[index].init(event.metadata, function() {
511 this.tiles_[index].unload();
512 this.tiles_[index].load(
513 Mosaic.Tile.LoadMode.HIGH_DPI,
514 this.scheduleLayout.bind(this, Mosaic.LAYOUT_DELAY));
519 * Keydown event handler.
521 * @param {Event} event Event.
522 * @return {boolean} True if the event has been consumed.
524 Mosaic.prototype.onKeyDown = function(event) {
525 this.selectionController_.handleKeyDown(event);
526 if (event.defaultPrevented) // Navigating with keyboard, hide hover state.
527 this.classList.remove('hover-visible');
528 return event.defaultPrevented;
532 * @return {boolean} True if the mosaic zoom effect can be applied. It is
533 * too slow if there are to many images.
534 * TODO(kaznacheev): Consider unloading the images that are out of the viewport.
536 Mosaic.prototype.canZoom = function() {
537 return this.tiles_.length < 100;
543 Mosaic.prototype.show = function() {
544 var duration = ImageView.MODE_TRANSITION_DURATION;
545 if (this.canZoom()) {
546 // Fade in in parallel with the zoom effect.
547 this.setAttribute('visible', 'zooming');
549 // Mosaic is not animating but the large image is. Fade in the mosaic
550 // shortly before the large image animation is done.
553 this.showingTimeoutID_ = setTimeout(function() {
554 this.showingTimeoutID_ = null;
555 // Make the selection visible.
556 // If the mosaic is not animated it will start fading in now.
557 this.setAttribute('visible', 'normal');
558 this.loadVisibleTiles_();
559 }.bind(this), duration);
565 Mosaic.prototype.hide = function() {
566 if (this.showingTimeoutID_ !== null) {
567 clearTimeout(this.showingTimeoutID_);
568 this.showingTimeoutID_ = null;
570 this.removeAttribute('visible');
574 * Checks if the mosaic view is visible.
575 * @return {boolean} True if visible, false otherwise.
578 Mosaic.prototype.isVisible_ = function() {
579 return this.hasAttribute('visible');
583 * Loads visible tiles. Ignores consecutive calls. Does not reload already
587 Mosaic.prototype.loadVisibleTiles_ = function() {
588 if (this.loadVisibleTilesSuppressed_) {
589 this.loadVisibleTilesScheduled_ = true;
593 this.loadVisibleTilesSuppressed_ = true;
594 this.loadVisibleTilesScheduled_ = false;
595 setTimeout(function() {
596 this.loadVisibleTilesSuppressed_ = false;
597 if (this.loadVisibleTilesScheduled_)
598 this.loadVisibleTiles_();
601 // Tiles only in the viewport (visible).
602 var visibleRect = new Rect(0,
607 // Tiles in the viewport and also some distance on the left and right.
608 var renderableRect = new Rect(-this.clientWidth,
610 3 * this.clientWidth,
613 // Unload tiles out of scope.
614 for (var index = 0; index < this.tiles_.length; index++) {
615 var tile = this.tiles_[index];
616 var imageRect = tile.getImageRect();
617 // Unload a thumbnail.
618 if (imageRect && !imageRect.intersects(renderableRect))
622 // Load the visible tiles first.
623 var allVisibleLoaded = true;
624 // Show high-dpi only when the mosaic view is visible.
625 var loadMode = this.isVisible_() ? Mosaic.Tile.LoadMode.HIGH_DPI :
626 Mosaic.Tile.LoadMode.LOW_DPI;
627 for (var index = 0; index < this.tiles_.length; index++) {
628 var tile = this.tiles_[index];
629 var imageRect = tile.getImageRect();
631 if (!tile.isLoading(loadMode) && !tile.isLoaded(loadMode) && imageRect &&
632 imageRect.intersects(visibleRect)) {
633 tile.load(loadMode, function() {});
634 allVisibleLoaded = false;
638 // Load also another, nearby, if the visible has been already loaded.
639 if (allVisibleLoaded) {
640 for (var index = 0; index < this.tiles_.length; index++) {
641 var tile = this.tiles_[index];
642 var imageRect = tile.getImageRect();
644 if (!tile.isLoading() && !tile.isLoaded() && imageRect &&
645 imageRect.intersects(renderableRect)) {
646 tile.load(Mosaic.Tile.LoadMode.LOW_DPI, function() {});
653 * Applies reset the zoom transform.
655 * @param {Rect} tileRect Tile rectangle. Reset the transform if null.
656 * @param {Rect} imageRect Large image rectangle. Reset the transform if null.
657 * @param {boolean=} opt_instant True of the transition should be instant.
659 Mosaic.prototype.transform = function(tileRect, imageRect, opt_instant) {
661 this.style.webkitTransitionDuration = '0';
663 this.style.webkitTransitionDuration =
664 ImageView.MODE_TRANSITION_DURATION + 'ms';
667 if (this.canZoom() && tileRect && imageRect) {
668 var scaleX = imageRect.width / tileRect.width;
669 var scaleY = imageRect.height / tileRect.height;
670 var shiftX = (imageRect.left + imageRect.width / 2) -
671 (tileRect.left + tileRect.width / 2);
672 var shiftY = (imageRect.top + imageRect.height / 2) -
673 (tileRect.top + tileRect.height / 2);
674 this.style.webkitTransform =
675 'translate(' + shiftX * scaleX + 'px, ' + shiftY * scaleY + 'px)' +
676 'scaleX(' + scaleX + ') scaleY(' + scaleY + ')';
678 this.style.webkitTransform = '';
682 ////////////////////////////////////////////////////////////////////////////////
685 * Creates a selection controller that is to be used with grid.
686 * @param {cr.ui.ListSelectionModel} selectionModel The selection model to
688 * @param {Mosaic.Layout} layoutModel The layout model to use.
690 * @extends {!cr.ui.ListSelectionController}
692 Mosaic.SelectionController = function(selectionModel, layoutModel) {
693 cr.ui.ListSelectionController.call(this, selectionModel);
694 this.layoutModel_ = layoutModel;
698 * Extends cr.ui.ListSelectionController.
700 Mosaic.SelectionController.prototype.__proto__ =
701 cr.ui.ListSelectionController.prototype;
704 Mosaic.SelectionController.prototype.getLastIndex = function() {
705 return this.layoutModel_.getLaidOutTileCount() - 1;
709 Mosaic.SelectionController.prototype.getIndexBefore = function(index) {
710 return this.layoutModel_.getHorizontalAdjacentIndex(index, -1);
714 Mosaic.SelectionController.prototype.getIndexAfter = function(index) {
715 return this.layoutModel_.getHorizontalAdjacentIndex(index, 1);
719 Mosaic.SelectionController.prototype.getIndexAbove = function(index) {
720 return this.layoutModel_.getVerticalAdjacentIndex(index, -1);
724 Mosaic.SelectionController.prototype.getIndexBelow = function(index) {
725 return this.layoutModel_.getVerticalAdjacentIndex(index, 1);
728 ////////////////////////////////////////////////////////////////////////////////
733 * @param {string=} opt_mode Layout mode.
734 * @param {Mosaic.Density=} opt_maxDensity Layout density.
737 Mosaic.Layout = function(opt_mode, opt_maxDensity) {
738 this.mode_ = opt_mode || Mosaic.Layout.MODE_TENTATIVE;
739 this.maxDensity_ = opt_maxDensity || Mosaic.Density.createHighest();
744 * Blank space at the top of the mosaic element. We do not do that in CSS
745 * to make transition effects easier.
747 Mosaic.Layout.PADDING_TOP = 50;
750 * Blank space at the bottom of the mosaic element.
752 Mosaic.Layout.PADDING_BOTTOM = 50;
755 * Horizontal and vertical spacing between images. Should be kept in sync
756 * with the style of .mosaic-item in gallery.css (= 2 * ( 4 + 1))
758 Mosaic.Layout.SPACING = 10;
761 * Margin for scrolling using keyboard. Distance between a selected tile
764 Mosaic.Layout.SCROLL_MARGIN = 30;
767 * Layout mode: commit to DOM immediately.
769 Mosaic.Layout.MODE_FINAL = 'final';
772 * Layout mode: do not commit layout to DOM until it is complete or the viewport
775 Mosaic.Layout.MODE_TENTATIVE = 'tentative';
778 * Layout mode: never commit layout to DOM.
780 Mosaic.Layout.MODE_DRY_RUN = 'dry_run';
787 Mosaic.Layout.prototype.reset_ = function() {
789 this.newColumn_ = null;
790 this.density_ = Mosaic.Density.createLowest();
791 if (this.mode_ !== Mosaic.Layout.MODE_DRY_RUN) // DRY_RUN is sticky.
792 this.mode_ = Mosaic.Layout.MODE_TENTATIVE;
796 * @param {number} width Viewport width.
797 * @param {number} height Viewport height.
799 Mosaic.Layout.prototype.setViewportSize = function(width, height) {
800 this.viewportWidth_ = width;
801 this.viewportHeight_ = height;
806 * @return {number} Total width of the layout.
808 Mosaic.Layout.prototype.getWidth = function() {
809 var lastColumn = this.getLastColumn_();
810 return lastColumn ? lastColumn.getRight() : 0;
814 * @return {number} Total height of the layout.
816 Mosaic.Layout.prototype.getHeight = function() {
817 var firstColumn = this.columns_[0];
818 return firstColumn ? firstColumn.getHeight() : 0;
822 * @return {Array.<Mosaic.Tile>} All tiles in the layout.
824 Mosaic.Layout.prototype.getTiles = function() {
825 return Array.prototype.concat.apply([],
826 this.columns_.map(function(c) { return c.getTiles() }));
830 * @return {number} Total number of tiles added to the layout.
832 Mosaic.Layout.prototype.getTileCount = function() {
833 return this.getLaidOutTileCount() +
834 (this.newColumn_ ? this.newColumn_.getTileCount() : 0);
838 * @return {Mosaic.Column} The last column or null for empty layout.
841 Mosaic.Layout.prototype.getLastColumn_ = function() {
842 return this.columns_.length ? this.columns_[this.columns_.length - 1] : null;
846 * @return {number} Total number of tiles in completed columns.
848 Mosaic.Layout.prototype.getLaidOutTileCount = function() {
849 var lastColumn = this.getLastColumn_();
850 return lastColumn ? lastColumn.getNextTileIndex() : 0;
854 * Adds a tile to the layout.
856 * @param {Mosaic.Tile} tile The tile to be added.
857 * @param {boolean} isLast True if this tile is the last.
859 Mosaic.Layout.prototype.add = function(tile, isLast) {
860 var layoutQueue = [tile];
862 // There are two levels of backtracking in the layout algorithm.
863 // |Mosaic.Layout.density_| tracks the state of the 'global' backtracking
864 // which aims to use as much of the viewport space as possible.
865 // It starts with the lowest density and increases it until the layout
866 // fits into the viewport. If it does not fit even at the highest density,
867 // the layout continues with the highest density.
869 // |Mosaic.Column.density_| tracks the state of the 'local' backtracking
870 // which aims to avoid producing unnaturally looking columns.
871 // It starts with the current global density and decreases it until the column
874 while (layoutQueue.length) {
875 if (!this.newColumn_) {
876 var lastColumn = this.getLastColumn_();
877 this.newColumn_ = new Mosaic.Column(
878 this.columns_.length,
879 lastColumn ? lastColumn.getNextRowIndex() : 0,
880 lastColumn ? lastColumn.getNextTileIndex() : 0,
881 lastColumn ? lastColumn.getRight() : 0,
882 this.viewportHeight_,
883 this.density_.clone());
886 this.newColumn_.add(layoutQueue.shift());
888 var isFinalColumn = isLast && !layoutQueue.length;
890 if (!this.newColumn_.prepareLayout(isFinalColumn))
891 continue; // Column is incomplete.
893 if (this.newColumn_.isSuboptimal()) {
894 layoutQueue = this.newColumn_.getTiles().concat(layoutQueue);
895 this.newColumn_.retryWithLowerDensity();
899 this.columns_.push(this.newColumn_);
900 this.newColumn_ = null;
902 if (this.mode_ === Mosaic.Layout.MODE_FINAL) {
903 this.getLastColumn_().layout();
907 if (this.getWidth() > this.viewportWidth_) {
908 // Viewport completely filled.
909 if (this.density_.equals(this.maxDensity_)) {
910 // Max density reached, commit if tentative, just continue if dry run.
911 if (this.mode_ === Mosaic.Layout.MODE_TENTATIVE)
916 // Rollback the entire layout, retry with higher density.
917 layoutQueue = this.getTiles().concat(layoutQueue);
919 this.density_.increase();
923 if (isFinalColumn && this.mode_ === Mosaic.Layout.MODE_TENTATIVE) {
924 // The complete tentative layout fits into the viewport.
925 var stretched = this.findHorizontalLayout_();
927 this.columns_ = stretched.columns_;
928 // Center the layout in the viewport and commit.
929 this.commit_((this.viewportWidth_ - this.getWidth()) / 2,
930 (this.viewportHeight_ - this.getHeight()) / 2);
936 * Commits the tentative layout.
938 * @param {number=} opt_offsetX Horizontal offset.
939 * @param {number=} opt_offsetY Vertical offset.
942 Mosaic.Layout.prototype.commit_ = function(opt_offsetX, opt_offsetY) {
943 console.assert(this.mode_ !== Mosaic.Layout.MODE_FINAL,
944 'Did not expect final layout');
945 for (var i = 0; i !== this.columns_.length; i++) {
946 this.columns_[i].layout(opt_offsetX, opt_offsetY);
948 this.mode_ = Mosaic.Layout.MODE_FINAL;
952 * Finds the most horizontally stretched layout built from the same tiles.
954 * The main layout algorithm fills the entire available viewport height.
955 * If there is too few tiles this results in a layout that is unnaturally
956 * stretched in the vertical direction.
958 * This method tries a number of smaller heights and returns the most
959 * horizontally stretched layout that still fits into the viewport.
961 * @return {Mosaic.Layout} A horizontally stretched layout.
964 Mosaic.Layout.prototype.findHorizontalLayout_ = function() {
965 // If the layout aspect ratio is not dramatically different from
966 // the viewport aspect ratio then there is no need to optimize.
967 if (this.getWidth() / this.getHeight() >
968 this.viewportWidth_ / this.viewportHeight_ * 0.9)
971 var tiles = this.getTiles();
972 if (tiles.length === 1)
973 return null; // Single tile layout is always the same.
975 var tileHeights = tiles.map(function(t) { return t.getMaxContentHeight() });
976 var minTileHeight = Math.min.apply(null, tileHeights);
978 for (var h = minTileHeight; h < this.viewportHeight_; h += minTileHeight) {
979 var layout = new Mosaic.Layout(
980 Mosaic.Layout.MODE_DRY_RUN, this.density_.clone());
981 layout.setViewportSize(this.viewportWidth_, h);
982 for (var t = 0; t !== tiles.length; t++)
983 layout.add(tiles[t], t + 1 === tiles.length);
985 if (layout.getWidth() <= this.viewportWidth_)
993 * Invalidates the layout after the given tile was modified (added, deleted or
994 * changed dimensions).
996 * @param {number} index Tile index.
999 Mosaic.Layout.prototype.invalidateFromTile_ = function(index) {
1000 var columnIndex = this.getColumnIndexByTile_(index);
1001 if (columnIndex < 0)
1002 return; // Index not in the layout, probably already invalidated.
1004 if (this.columns_[columnIndex].getLeft() >= this.viewportWidth_) {
1005 // The columns to the right cover the entire viewport width, so there is no
1006 // chance that the modified layout would fit into the viewport.
1007 // No point in restarting the entire layout, keep the columns to the right.
1008 console.assert(this.mode_ === Mosaic.Layout.MODE_FINAL,
1009 'Expected FINAL layout mode');
1010 this.columns_ = this.columns_.slice(0, columnIndex);
1011 this.newColumn_ = null;
1013 // There is a chance that the modified layout would fit into the viewport.
1015 this.mode_ = Mosaic.Layout.MODE_TENTATIVE;
1020 * Gets the index of the tile to the left or to the right from the given tile.
1022 * @param {number} index Tile index.
1023 * @param {number} direction -1 for left, 1 for right.
1024 * @return {number} Adjacent tile index.
1026 Mosaic.Layout.prototype.getHorizontalAdjacentIndex = function(
1028 var column = this.getColumnIndexByTile_(index);
1030 console.error('Cannot find column for tile #' + index);
1034 var row = this.columns_[column].getRowByTileIndex(index);
1036 console.error('Cannot find row for tile #' + index);
1040 var sameRowNeighbourIndex = index + direction;
1041 if (row.hasTile(sameRowNeighbourIndex))
1042 return sameRowNeighbourIndex;
1044 var adjacentColumn = column + direction;
1045 if (adjacentColumn < 0 || adjacentColumn === this.columns_.length)
1048 return this.columns_[adjacentColumn].
1049 getEdgeTileIndex_(row.getCenterY(), -direction);
1053 * Gets the index of the tile to the top or to the bottom from the given tile.
1055 * @param {number} index Tile index.
1056 * @param {number} direction -1 for above, 1 for below.
1057 * @return {number} Adjacent tile index.
1059 Mosaic.Layout.prototype.getVerticalAdjacentIndex = function(
1061 var column = this.getColumnIndexByTile_(index);
1063 console.error('Cannot find column for tile #' + index);
1067 var row = this.columns_[column].getRowByTileIndex(index);
1069 console.error('Cannot find row for tile #' + index);
1073 // Find the first item in the next row, or the last item in the previous row.
1074 var adjacentRowNeighbourIndex =
1075 row.getEdgeTileIndex_(direction) + direction;
1077 if (adjacentRowNeighbourIndex < 0 ||
1078 adjacentRowNeighbourIndex > this.getTileCount() - 1)
1081 if (!this.columns_[column].hasTile(adjacentRowNeighbourIndex)) {
1082 // It is not in the current column, so return it.
1083 return adjacentRowNeighbourIndex;
1085 // It is in the current column, so we have to find optically the closest
1086 // tile in the adjacent row.
1087 var adjacentRow = this.columns_[column].getRowByTileIndex(
1088 adjacentRowNeighbourIndex);
1089 var previousTileCenterX = row.getTileByIndex(index).getCenterX();
1091 // Find the closest one.
1092 var closestIndex = -1;
1093 var closestDistance;
1094 var adjacentRowTiles = adjacentRow.getTiles();
1095 for (var t = 0; t !== adjacentRowTiles.length; t++) {
1097 Math.abs(adjacentRowTiles[t].getCenterX() - previousTileCenterX);
1098 if (closestIndex === -1 || distance < closestDistance) {
1099 closestIndex = adjacentRow.getEdgeTileIndex_(-1) + t;
1100 closestDistance = distance;
1103 return closestIndex;
1108 * @param {number} index Tile index.
1109 * @return {number} Index of the column containing the given tile.
1112 Mosaic.Layout.prototype.getColumnIndexByTile_ = function(index) {
1113 for (var c = 0; c !== this.columns_.length; c++) {
1114 if (this.columns_[c].hasTile(index))
1121 * Scales the given array of size values to satisfy 3 conditions:
1122 * 1. The new sizes must be integer.
1123 * 2. The new sizes must sum up to the given |total| value.
1124 * 3. The relative proportions of the sizes should be as close to the original
1127 * @param {Array.<number>} sizes Array of sizes.
1128 * @param {number} newTotal New total size.
1130 Mosaic.Layout.rescaleSizesToNewTotal = function(sizes, newTotal) {
1133 var partialTotals = [0];
1134 for (var i = 0; i !== sizes.length; i++) {
1136 partialTotals.push(total);
1139 var scale = newTotal / total;
1141 for (i = 0; i !== sizes.length; i++) {
1142 sizes[i] = Math.round(partialTotals[i + 1] * scale) -
1143 Math.round(partialTotals[i] * scale);
1147 ////////////////////////////////////////////////////////////////////////////////
1150 * Representation of the layout density.
1152 * @param {number} horizontal Horizontal density, number tiles per row.
1153 * @param {number} vertical Vertical density, frequency of rows forced to
1154 * contain a single tile.
1157 Mosaic.Density = function(horizontal, vertical) {
1158 this.horizontal = horizontal;
1159 this.vertical = vertical;
1163 * Minimal horizontal density (tiles per row).
1165 Mosaic.Density.MIN_HORIZONTAL = 1;
1168 * Minimal horizontal density (tiles per row).
1170 Mosaic.Density.MAX_HORIZONTAL = 3;
1173 * Minimal vertical density: force 1 out of 2 rows to containt a single tile.
1175 Mosaic.Density.MIN_VERTICAL = 2;
1178 * Maximal vertical density: force 1 out of 3 rows to containt a single tile.
1180 Mosaic.Density.MAX_VERTICAL = 3;
1183 * @return {Mosaic.Density} Lowest density.
1185 Mosaic.Density.createLowest = function() {
1186 return new Mosaic.Density(
1187 Mosaic.Density.MIN_HORIZONTAL,
1188 Mosaic.Density.MIN_VERTICAL /* ignored when horizontal is at min */);
1192 * @return {Mosaic.Density} Highest density.
1194 Mosaic.Density.createHighest = function() {
1195 return new Mosaic.Density(
1196 Mosaic.Density.MAX_HORIZONTAL,
1197 Mosaic.Density.MAX_VERTICAL);
1201 * @return {Mosaic.Density} A clone of this density object.
1203 Mosaic.Density.prototype.clone = function() {
1204 return new Mosaic.Density(this.horizontal, this.vertical);
1208 * @param {Mosaic.Density} that The other object.
1209 * @return {boolean} True if equal.
1211 Mosaic.Density.prototype.equals = function(that) {
1212 return this.horizontal === that.horizontal &&
1213 this.vertical === that.vertical;
1217 * Increases the density to the next level.
1219 Mosaic.Density.prototype.increase = function() {
1220 if (this.horizontal === Mosaic.Density.MIN_HORIZONTAL ||
1221 this.vertical === Mosaic.Density.MAX_VERTICAL) {
1222 console.assert(this.horizontal < Mosaic.Density.MAX_HORIZONTAL);
1224 this.vertical = Mosaic.Density.MIN_VERTICAL;
1231 * Decreases horizontal density.
1233 Mosaic.Density.prototype.decreaseHorizontal = function() {
1234 console.assert(this.horizontal > Mosaic.Density.MIN_HORIZONTAL);
1239 * @param {number} tileCount Number of tiles in the row.
1240 * @param {number} rowIndex Global row index.
1241 * @return {boolean} True if the row is complete.
1243 Mosaic.Density.prototype.isRowComplete = function(tileCount, rowIndex) {
1244 return (tileCount === this.horizontal) || (rowIndex % this.vertical) === 0;
1247 ////////////////////////////////////////////////////////////////////////////////
1250 * A column in a mosaic layout. Contains rows.
1252 * @param {number} index Column index.
1253 * @param {number} firstRowIndex Global row index.
1254 * @param {number} firstTileIndex Index of the first tile in the column.
1255 * @param {number} left Left edge coordinate.
1256 * @param {number} maxHeight Maximum height.
1257 * @param {Mosaic.Density} density Layout density.
1260 Mosaic.Column = function(index, firstRowIndex, firstTileIndex, left, maxHeight,
1262 this.index_ = index;
1263 this.firstRowIndex_ = firstRowIndex;
1264 this.firstTileIndex_ = firstTileIndex;
1266 this.maxHeight_ = maxHeight;
1267 this.density_ = density;
1273 * Resets the layout.
1276 Mosaic.Column.prototype.reset_ = function() {
1279 this.newRow_ = null;
1283 * @return {number} Number of tiles in the column.
1285 Mosaic.Column.prototype.getTileCount = function() { return this.tiles_.length };
1288 * @return {number} Index of the last tile + 1.
1290 Mosaic.Column.prototype.getNextTileIndex = function() {
1291 return this.firstTileIndex_ + this.getTileCount();
1295 * @return {number} Global index of the last row + 1.
1297 Mosaic.Column.prototype.getNextRowIndex = function() {
1298 return this.firstRowIndex_ + this.rows_.length;
1302 * @return {Array.<Mosaic.Tile>} Array of tiles in the column.
1304 Mosaic.Column.prototype.getTiles = function() { return this.tiles_ };
1307 * @param {number} index Tile index.
1308 * @return {boolean} True if this column contains the tile with the given index.
1310 Mosaic.Column.prototype.hasTile = function(index) {
1311 return this.firstTileIndex_ <= index &&
1312 index < (this.firstTileIndex_ + this.getTileCount());
1316 * @param {number} y Y coordinate.
1317 * @param {number} direction -1 for left, 1 for right.
1318 * @return {number} Index of the tile lying on the edge of the column at the
1319 * given y coordinate.
1322 Mosaic.Column.prototype.getEdgeTileIndex_ = function(y, direction) {
1323 for (var r = 0; r < this.rows_.length; r++) {
1324 if (this.rows_[r].coversY(y))
1325 return this.rows_[r].getEdgeTileIndex_(direction);
1331 * @param {number} index Tile index.
1332 * @return {Mosaic.Row} The row containing the tile with a given index.
1334 Mosaic.Column.prototype.getRowByTileIndex = function(index) {
1335 for (var r = 0; r !== this.rows_.length; r++)
1336 if (this.rows_[r].hasTile(index))
1337 return this.rows_[r];
1343 * Adds a tile to the column.
1345 * @param {Mosaic.Tile} tile The tile to add.
1347 Mosaic.Column.prototype.add = function(tile) {
1348 var rowIndex = this.getNextRowIndex();
1351 this.newRow_ = new Mosaic.Row(this.getNextTileIndex());
1353 this.tiles_.push(tile);
1354 this.newRow_.add(tile);
1356 if (this.density_.isRowComplete(this.newRow_.getTileCount(), rowIndex)) {
1357 this.rows_.push(this.newRow_);
1358 this.newRow_ = null;
1363 * Prepares the column layout.
1365 * @param {boolean=} opt_force True if the layout must be performed even for an
1366 * incomplete column.
1367 * @return {boolean} True if the layout was performed.
1369 Mosaic.Column.prototype.prepareLayout = function(opt_force) {
1370 if (opt_force && this.newRow_) {
1371 this.rows_.push(this.newRow_);
1372 this.newRow_ = null;
1375 if (this.rows_.length === 0)
1378 this.width_ = Math.min.apply(
1379 null, this.rows_.map(function(row) { return row.getMaxWidth() }));
1383 this.rowHeights_ = [];
1384 for (var r = 0; r !== this.rows_.length; r++) {
1385 var rowHeight = this.rows_[r].getHeightForWidth(this.width_);
1386 this.height_ += rowHeight;
1387 this.rowHeights_.push(rowHeight);
1390 var overflow = this.height_ / this.maxHeight_;
1391 if (!opt_force && (overflow < 1))
1395 // Scale down the column width and height.
1396 this.width_ = Math.round(this.width_ / overflow);
1397 this.height_ = this.maxHeight_;
1398 Mosaic.Layout.rescaleSizesToNewTotal(this.rowHeights_, this.maxHeight_);
1405 * Retries the column layout with less tiles per row.
1407 Mosaic.Column.prototype.retryWithLowerDensity = function() {
1408 this.density_.decreaseHorizontal();
1413 * @return {number} Column left edge coordinate.
1415 Mosaic.Column.prototype.getLeft = function() { return this.left_ };
1418 * @return {number} Column right edge coordinate after the layout.
1420 Mosaic.Column.prototype.getRight = function() {
1421 return this.left_ + this.width_;
1425 * @return {number} Column height after the layout.
1427 Mosaic.Column.prototype.getHeight = function() { return this.height_ };
1430 * Performs the column layout.
1431 * @param {number=} opt_offsetX Horizontal offset.
1432 * @param {number=} opt_offsetY Vertical offset.
1434 Mosaic.Column.prototype.layout = function(opt_offsetX, opt_offsetY) {
1435 opt_offsetX = opt_offsetX || 0;
1436 opt_offsetY = opt_offsetY || 0;
1437 var rowTop = Mosaic.Layout.PADDING_TOP;
1438 for (var r = 0; r !== this.rows_.length; r++) {
1439 this.rows_[r].layout(
1440 opt_offsetX + this.left_,
1441 opt_offsetY + rowTop,
1443 this.rowHeights_[r]);
1444 rowTop += this.rowHeights_[r];
1449 * Checks if the column layout is too ugly to be displayed.
1451 * @return {boolean} True if the layout is suboptimal.
1453 Mosaic.Column.prototype.isSuboptimal = function() {
1455 this.rows_.map(function(row) { return row.getTileCount() });
1457 var maxTileCount = Math.max.apply(null, tileCounts);
1458 if (maxTileCount === 1)
1459 return false; // Every row has exactly 1 tile, as optimal as it gets.
1462 this.tiles_.map(function(tile) { return tile.getMaxContentHeight() });
1464 // Ugly layout #1: all images are small and some are one the same row.
1465 var allSmall = Math.max.apply(null, sizes) <= Mosaic.Tile.SMALL_IMAGE_SIZE;
1469 // Ugly layout #2: all images are large and none occupies an entire row.
1470 var allLarge = Math.min.apply(null, sizes) > Mosaic.Tile.SMALL_IMAGE_SIZE;
1471 var allCombined = Math.min.apply(null, tileCounts) !== 1;
1472 if (allLarge && allCombined)
1475 // Ugly layout #3: some rows have too many tiles for the resulting width.
1476 if (this.width_ / maxTileCount < 100)
1482 ////////////////////////////////////////////////////////////////////////////////
1485 * A row in a mosaic layout. Contains tiles.
1487 * @param {number} firstTileIndex Index of the first tile in the row.
1490 Mosaic.Row = function(firstTileIndex) {
1491 this.firstTileIndex_ = firstTileIndex;
1496 * @param {Mosaic.Tile} tile The tile to add.
1498 Mosaic.Row.prototype.add = function(tile) {
1499 console.assert(this.getTileCount() < Mosaic.Density.MAX_HORIZONTAL);
1500 this.tiles_.push(tile);
1504 * @return {Array.<Mosaic.Tile>} Array of tiles in the row.
1506 Mosaic.Row.prototype.getTiles = function() { return this.tiles_ };
1509 * Gets a tile by index.
1510 * @param {number} index Tile index.
1511 * @return {Mosaic.Tile} Requested tile or null if not found.
1513 Mosaic.Row.prototype.getTileByIndex = function(index) {
1514 if (!this.hasTile(index))
1516 return this.tiles_[index - this.firstTileIndex_];
1521 * @return {number} Number of tiles in the row.
1523 Mosaic.Row.prototype.getTileCount = function() { return this.tiles_.length };
1526 * @param {number} index Tile index.
1527 * @return {boolean} True if this row contains the tile with the given index.
1529 Mosaic.Row.prototype.hasTile = function(index) {
1530 return this.firstTileIndex_ <= index &&
1531 index < (this.firstTileIndex_ + this.tiles_.length);
1535 * @param {number} y Y coordinate.
1536 * @return {boolean} True if this row covers the given Y coordinate.
1538 Mosaic.Row.prototype.coversY = function(y) {
1539 return this.top_ <= y && y < (this.top_ + this.height_);
1543 * @return {number} Y coordinate of the tile center.
1545 Mosaic.Row.prototype.getCenterY = function() {
1546 return this.top_ + Math.round(this.height_ / 2);
1550 * Gets the first or the last tile.
1552 * @param {number} direction -1 for the first tile, 1 for the last tile.
1553 * @return {number} Tile index.
1556 Mosaic.Row.prototype.getEdgeTileIndex_ = function(direction) {
1558 return this.firstTileIndex_;
1560 return this.firstTileIndex_ + this.getTileCount() - 1;
1564 * @return {number} Aspect ration of the combined content box of this row.
1567 Mosaic.Row.prototype.getTotalContentAspectRatio_ = function() {
1569 for (var t = 0; t !== this.tiles_.length; t++)
1570 sum += this.tiles_[t].getAspectRatio();
1575 * @return {number} Total horizontal spacing in this row. This includes
1576 * the spacing between the tiles and both left and right margins.
1580 Mosaic.Row.prototype.getTotalHorizontalSpacing_ = function() {
1581 return Mosaic.Layout.SPACING * this.getTileCount();
1585 * @return {number} Maximum width that this row may have without overscaling
1588 Mosaic.Row.prototype.getMaxWidth = function() {
1589 var contentHeight = Math.min.apply(null,
1590 this.tiles_.map(function(tile) { return tile.getMaxContentHeight() }));
1593 Math.round(contentHeight * this.getTotalContentAspectRatio_());
1594 return contentWidth + this.getTotalHorizontalSpacing_();
1598 * Computes the height that best fits the supplied row width given
1599 * aspect ratios of the tiles in this row.
1601 * @param {number} width Row width.
1602 * @return {number} Height.
1604 Mosaic.Row.prototype.getHeightForWidth = function(width) {
1605 var contentWidth = width - this.getTotalHorizontalSpacing_();
1607 Math.round(contentWidth / this.getTotalContentAspectRatio_());
1608 return contentHeight + Mosaic.Layout.SPACING;
1612 * Positions the row in the mosaic.
1614 * @param {number} left Left position.
1615 * @param {number} top Top position.
1616 * @param {number} width Width.
1617 * @param {number} height Height.
1619 Mosaic.Row.prototype.layout = function(left, top, width, height) {
1621 this.height_ = height;
1623 var contentWidth = width - this.getTotalHorizontalSpacing_();
1624 var contentHeight = height - Mosaic.Layout.SPACING;
1626 var tileContentWidth = this.tiles_.map(
1627 function(tile) { return tile.getAspectRatio() });
1629 Mosaic.Layout.rescaleSizesToNewTotal(tileContentWidth, contentWidth);
1631 var tileLeft = left;
1632 for (var t = 0; t !== this.tiles_.length; t++) {
1633 var tileWidth = tileContentWidth[t] + Mosaic.Layout.SPACING;
1634 this.tiles_[t].layout(tileLeft, top, tileWidth, height);
1635 tileLeft += tileWidth;
1639 ////////////////////////////////////////////////////////////////////////////////
1642 * A single tile of the image mosaic.
1644 * @param {Element} container Container element.
1645 * @param {Gallery.Item} item Gallery item associated with this tile.
1646 * @param {EntryLocation} locationInfo Location information for the tile.
1647 * @return {Element} The new tile element.
1650 Mosaic.Tile = function(container, item, locationInfo) {
1651 var self = container.ownerDocument.createElement('div');
1652 Mosaic.Tile.decorate(self, container, item, locationInfo);
1657 * @param {Element} self Self pointer.
1658 * @param {Element} container Container element.
1659 * @param {Gallery.Item} item Gallery item associated with this tile.
1660 * @param {EntryLocation} locationInfo Location info for the tile image.
1662 Mosaic.Tile.decorate = function(self, container, item, locationInfo) {
1663 self.__proto__ = Mosaic.Tile.prototype;
1664 self.className = 'mosaic-tile';
1666 self.container_ = container;
1668 self.left_ = null; // Mark as not laid out.
1669 self.hidpiEmbedded_ = locationInfo && locationInfo.isDriveBased;
1673 * Load mode for the tile's image.
1676 Mosaic.Tile.LoadMode = {
1682 * Inherit from HTMLDivElement.
1684 Mosaic.Tile.prototype.__proto__ = HTMLDivElement.prototype;
1687 * Minimum tile content size.
1689 Mosaic.Tile.MIN_CONTENT_SIZE = 64;
1692 * Maximum tile content size.
1694 Mosaic.Tile.MAX_CONTENT_SIZE = 512;
1697 * Default size for a tile with no thumbnail image.
1699 Mosaic.Tile.GENERIC_ICON_SIZE = 128;
1702 * Max size of an image considered to be 'small'.
1703 * Small images are laid out slightly differently.
1705 Mosaic.Tile.SMALL_IMAGE_SIZE = 160;
1708 * @return {Gallery.Item} The Gallery item.
1710 Mosaic.Tile.prototype.getItem = function() { return this.item_; };
1713 * @return {number} Maximum content height that this tile can have.
1715 Mosaic.Tile.prototype.getMaxContentHeight = function() {
1716 return this.maxContentHeight_;
1720 * @return {number} The aspect ratio of the tile image.
1722 Mosaic.Tile.prototype.getAspectRatio = function() { return this.aspectRatio_; };
1725 * @return {boolean} True if the tile is initialized.
1727 Mosaic.Tile.prototype.isInitialized = function() {
1728 return !!this.maxContentHeight_;
1732 * Checks whether the image of specified (or better resolution) has been loaded.
1734 * @param {Mosaic.Tile.LoadMode=} opt_loadMode Loading mode, default: LOW_DPI.
1735 * @return {boolean} True if the tile is loaded with the specified dpi or
1738 Mosaic.Tile.prototype.isLoaded = function(opt_loadMode) {
1739 var loadMode = opt_loadMode || Mosaic.Tile.LoadMode.LOW_DPI;
1741 case Mosaic.Tile.LoadMode.LOW_DPI:
1742 if (this.imagePreloaded_ || this.imageLoaded_)
1745 case Mosaic.Tile.LoadMode.HIGH_DPI:
1746 if (this.imageLoaded_)
1754 * Checks whether the image of specified (or better resolution) is being loaded.
1756 * @param {Mosaic.Tile.LoadMode=} opt_loadMode Loading mode, default: LOW_DPI.
1757 * @return {boolean} True if the tile is being loaded with the specified dpi or
1760 Mosaic.Tile.prototype.isLoading = function(opt_loadMode) {
1761 var loadMode = opt_loadMode || Mosaic.Tile.LoadMode.LOW_DPI;
1763 case Mosaic.Tile.LoadMode.LOW_DPI:
1764 if (this.imagePreloading_ || this.imageLoading_)
1767 case Mosaic.Tile.LoadMode.HIGH_DPI:
1768 if (this.imageLoading_)
1776 * Marks the tile as not loaded to prevent it from participating in the layout.
1778 Mosaic.Tile.prototype.markUnloaded = function() {
1779 this.maxContentHeight_ = 0;
1780 if (this.thumbnailLoader_) {
1781 this.thumbnailLoader_.cancel();
1782 this.imagePreloaded_ = false;
1783 this.imagePreloading_ = false;
1784 this.imageLoaded_ = false;
1785 this.imageLoading_ = false;
1790 * Initializes the thumbnail in the tile. Does not load an image, but sets
1791 * target dimensions using metadata.
1793 * @param {Object} metadata Metadata object.
1794 * @param {function()} onImageMeasured Image measured callback.
1796 Mosaic.Tile.prototype.init = function(metadata, onImageMeasured) {
1797 this.markUnloaded();
1798 this.left_ = null; // Mark as not laid out.
1800 // Set higher priority for the selected elements to load them first.
1801 var priority = this.getAttribute('selected') ? 2 : 3;
1803 // Use embedded thumbnails on Drive, since they have higher resolution.
1804 this.thumbnailLoader_ = new ThumbnailLoader(
1805 this.getItem().getEntry(),
1806 ThumbnailLoader.LoaderType.CANVAS,
1808 undefined, // Media type.
1809 this.hidpiEmbedded_ ? ThumbnailLoader.UseEmbedded.USE_EMBEDDED :
1810 ThumbnailLoader.UseEmbedded.NO_EMBEDDED,
1813 // If no hidpi embedded thumbnail available, then use the low resolution
1815 if (!this.hidpiEmbedded_) {
1816 this.thumbnailPreloader_ = new ThumbnailLoader(
1817 this.getItem().getEntry(),
1818 ThumbnailLoader.LoaderType.CANVAS,
1820 undefined, // Media type.
1821 ThumbnailLoader.UseEmbedded.USE_EMBEDDED,
1822 2); // Preloaders have always higher priotity, so the preload images
1823 // are loaded as soon as possible.
1826 var setDimensions = function(width, height) {
1827 if (width > height) {
1828 if (width > Mosaic.Tile.MAX_CONTENT_SIZE) {
1829 height = Math.round(height * Mosaic.Tile.MAX_CONTENT_SIZE / width);
1830 width = Mosaic.Tile.MAX_CONTENT_SIZE;
1833 if (height > Mosaic.Tile.MAX_CONTENT_SIZE) {
1834 width = Math.round(width * Mosaic.Tile.MAX_CONTENT_SIZE / height);
1835 height = Mosaic.Tile.MAX_CONTENT_SIZE;
1838 this.maxContentHeight_ = Math.max(Mosaic.Tile.MIN_CONTENT_SIZE, height);
1839 this.aspectRatio_ = width / height;
1843 // Dimensions are always acquired from the metadata. For local files, it is
1844 // extracted from headers. For Drive files, it is received via the Drive API.
1845 // If the dimensions are not available, then the fallback dimensions will be
1846 // used (same as for the generic icon).
1847 if (metadata.media && metadata.media.width) {
1848 setDimensions(metadata.media.width, metadata.media.height);
1849 } else if (metadata.drive && metadata.drive.imageWidth &&
1850 metadata.drive.imageHeight) {
1851 setDimensions(metadata.drive.imageWidth, metadata.drive.imageHeight);
1853 // No dimensions in metadata, then use the generic dimensions.
1854 setDimensions(Mosaic.Tile.GENERIC_ICON_SIZE,
1855 Mosaic.Tile.GENERIC_ICON_SIZE);
1860 * Loads an image into the tile.
1862 * The mode argument is a hint. Use low-dpi for faster response, and high-dpi
1863 * for better output, but possibly affecting performance.
1865 * If the mode is high-dpi, then a the high-dpi image is loaded, but also
1866 * low-dpi image is loaded for preloading (if available).
1867 * For the low-dpi mode, only low-dpi image is loaded. If not available, then
1868 * the high-dpi image is loaded as a fallback.
1870 * @param {Mosaic.Tile.LoadMode} loadMode Loading mode.
1871 * @param {function(boolean)} onImageLoaded Callback when image is loaded.
1872 * The argument is true for success, false for failure.
1874 Mosaic.Tile.prototype.load = function(loadMode, onImageLoaded) {
1875 // Attaches the image to the tile and finalizes loading process for the
1876 // specified loader.
1877 var finalizeLoader = function(mode, success, loader) {
1878 if (success && this.wrapper_) {
1879 // Show the fade-in animation only when previously there was no image
1880 // attached in this tile.
1881 if (!this.imageLoaded_ && !this.imagePreloaded_)
1882 this.wrapper_.classList.add('animated');
1884 this.wrapper_.classList.remove('animated');
1886 loader.attachImage(this.wrapper_, ThumbnailLoader.FillMode.OVER_FILL);
1887 onImageLoaded(success);
1889 case Mosaic.Tile.LoadMode.LOW_DPI:
1890 this.imagePreloading_ = false;
1891 this.imagePreloaded_ = true;
1893 case Mosaic.Tile.LoadMode.HIGH_DPI:
1894 this.imageLoading_ = false;
1895 this.imageLoaded_ = true;
1900 // Always load the low-dpi image first if it is available for the fastest
1902 if (!this.imagePreloading_ && this.thumbnailPreloader_) {
1903 this.imagePreloading_ = true;
1904 this.thumbnailPreloader_.loadDetachedImage(function(success) {
1905 // Hi-dpi loaded first, ignore this call then.
1906 if (this.imageLoaded_)
1908 finalizeLoader(Mosaic.Tile.LoadMode.LOW_DPI,
1910 this.thumbnailPreloader_);
1914 // Load the high-dpi image only when it is requested, or the low-dpi is not
1916 if (!this.imageLoading_ &&
1917 (loadMode === Mosaic.Tile.LoadMode.HIGH_DPI || !this.imagePreloading_)) {
1918 this.imageLoading_ = true;
1919 this.thumbnailLoader_.loadDetachedImage(function(success) {
1920 // Cancel preloading, since the hi-dpi image is ready.
1921 if (this.thumbnailPreloader_)
1922 this.thumbnailPreloader_.cancel();
1923 finalizeLoader(Mosaic.Tile.LoadMode.HIGH_DPI,
1925 this.thumbnailLoader_);
1931 * Unloads an image from the tile.
1933 Mosaic.Tile.prototype.unload = function() {
1934 this.thumbnailLoader_.cancel();
1935 if (this.thumbnailPreloader_)
1936 this.thumbnailPreloader_.cancel();
1937 this.imagePreloaded_ = false;
1938 this.imageLoaded_ = false;
1939 this.imagePreloading_ = false;
1940 this.imageLoading_ = false;
1941 this.wrapper_.innerText = '';
1945 * Selects/unselects the tile.
1947 * @param {boolean} on True if selected.
1949 Mosaic.Tile.prototype.select = function(on) {
1951 this.setAttribute('selected', true);
1953 this.removeAttribute('selected');
1957 * Positions the tile in the mosaic.
1959 * @param {number} left Left position.
1960 * @param {number} top Top position.
1961 * @param {number} width Width.
1962 * @param {number} height Height.
1964 Mosaic.Tile.prototype.layout = function(left, top, width, height) {
1967 this.width_ = width;
1968 this.height_ = height;
1970 this.style.left = left + 'px';
1971 this.style.top = top + 'px';
1972 this.style.width = width + 'px';
1973 this.style.height = height + 'px';
1975 if (!this.wrapper_) { // First time, create DOM.
1976 this.container_.appendChild(this);
1977 var border = util.createChild(this, 'img-border');
1978 this.wrapper_ = util.createChild(border, 'img-wrapper');
1980 if (this.hasAttribute('selected'))
1981 this.scrollIntoView(false);
1983 if (this.imageLoaded_) {
1984 this.thumbnailLoader_.attachImage(this.wrapper_,
1985 ThumbnailLoader.FillMode.OVER_FILL);
1990 * If the tile is not fully visible scroll the parent to make it fully visible.
1991 * @param {boolean=} opt_animated True, if scroll should be animated,
1994 Mosaic.Tile.prototype.scrollIntoView = function(opt_animated) {
1995 if (this.left_ === null) // Not laid out.
1999 var tileLeft = this.left_ - Mosaic.Layout.SCROLL_MARGIN;
2000 if (tileLeft < this.container_.scrollLeft) {
2001 targetPosition = tileLeft;
2003 var tileRight = this.left_ + this.width_ + Mosaic.Layout.SCROLL_MARGIN;
2004 var scrollRight = this.container_.scrollLeft + this.container_.clientWidth;
2005 if (tileRight > scrollRight)
2006 targetPosition = tileRight - this.container_.clientWidth;
2009 if (targetPosition) {
2010 if (opt_animated === false)
2011 this.container_.scrollLeft = targetPosition;
2013 this.container_.animatedScrollTo(targetPosition);
2018 * @return {Rect} Rectangle occupied by the tile's image,
2019 * relative to the viewport.
2021 Mosaic.Tile.prototype.getImageRect = function() {
2022 if (this.left_ === null) // Not laid out.
2025 var margin = Mosaic.Layout.SPACING / 2;
2026 return new Rect(this.left_ - this.container_.scrollLeft, this.top_,
2027 this.width_, this.height_).inflate(-margin, -margin);
2031 * @return {number} X coordinate of the tile center.
2033 Mosaic.Tile.prototype.getCenterX = function() {
2034 return this.left_ + Math.round(this.width_ / 2);