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 {VolumeManagerWrapper} volumeManager Volume manager.
12 * @param {function} toggleMode Function to switch to the Slide mode.
16 container
, dataModel
, selectionModel
, volumeManager
, toggleMode
) {
17 this.mosaic_
= new Mosaic(
18 container
.ownerDocument
, 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 {cr.ui.ArrayDataModel} dataModel Data model.
77 * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
78 * @param {VolumeManagerWrapper} volumeManager Volume manager.
79 * @return {Element} Mosaic element.
82 function Mosaic(document
, dataModel
, selectionModel
, volumeManager
) {
83 var self
= document
.createElement('div');
84 Mosaic
.decorate(self
, dataModel
, selectionModel
, volumeManager
);
89 * Inherits from HTMLDivElement.
91 Mosaic
.prototype.__proto__
= HTMLDivElement
.prototype;
94 * Default layout delay in ms.
98 Mosaic
.LAYOUT_DELAY
= 200;
101 * Smooth scroll animation duration when scrolling using keyboard or
102 * clicking on a partly visible tile. In ms.
106 Mosaic
.ANIMATED_SCROLL_DURATION
= 500;
109 * Decorates a Mosaic instance.
111 * @param {Mosaic} self Self pointer.
112 * @param {cr.ui.ArrayDataModel} dataModel Data model.
113 * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
114 * @param {VolumeManagerWrapper} volumeManager Volume manager.
116 Mosaic
.decorate = function(
117 self
, dataModel
, selectionModel
, volumeManager
) {
118 self
.__proto__
= Mosaic
.prototype;
119 self
.className
= 'mosaic';
121 self
.dataModel_
= dataModel
;
122 self
.selectionModel_
= selectionModel
;
123 self
.volumeManager_
= volumeManager
;
125 // Initialization is completed lazily on the first call to |init|.
129 * Initializes the mosaic element.
131 Mosaic
.prototype.init = function() {
133 return; // Already initialized, nothing to do.
135 this.layoutModel_
= new Mosaic
.Layout();
138 this.selectionController_
=
139 new Mosaic
.SelectionController(this.selectionModel_
, this.layoutModel_
);
142 for (var i
= 0; i
!== this.dataModel_
.length
; i
++) {
144 this.volumeManager_
.getLocationInfo(this.dataModel_
.item(i
).getEntry());
146 new Mosaic
.Tile(this, this.dataModel_
.item(i
), locationInfo
));
149 this.selectionModel_
.selectedIndexes
.forEach(function(index
) {
150 this.tiles_
[index
].select(true);
153 this.initTiles_(this.tiles_
);
155 // The listeners might be called while some tiles are still loading.
156 this.initListeners_();
160 * @return {boolean} Whether mosaic is initialized.
162 Mosaic
.prototype.isInitialized = function() {
163 return !!this.tiles_
;
167 * Starts listening to events.
169 * We keep listening to events even when the mosaic is hidden in order to
170 * keep the layout up to date.
174 Mosaic
.prototype.initListeners_ = function() {
175 this.ownerDocument
.defaultView
.addEventListener(
176 'resize', this.onResize_
.bind(this));
178 var mouseEventBound
= this.onMouseEvent_
.bind(this);
179 this.addEventListener('mousemove', mouseEventBound
);
180 this.addEventListener('mousedown', mouseEventBound
);
181 this.addEventListener('mouseup', mouseEventBound
);
182 this.addEventListener('scroll', this.onScroll_
.bind(this));
184 this.selectionModel_
.addEventListener('change', this.onSelection_
.bind(this));
185 this.selectionModel_
.addEventListener('leadIndexChange',
186 this.onLeadChange_
.bind(this));
188 this.dataModel_
.addEventListener('splice', this.onSplice_
.bind(this));
189 this.dataModel_
.addEventListener('content', this.onContentChange_
.bind(this));
193 * Smoothly scrolls the container to the specified position using
194 * f(x) = sqrt(x) speed function normalized to animation duration.
195 * @param {number} targetPosition Horizontal scroll position in pixels.
197 Mosaic
.prototype.animatedScrollTo = function(targetPosition
) {
198 if (this.scrollAnimation_
) {
199 webkitCancelAnimationFrame(this.scrollAnimation_
);
200 this.scrollAnimation_
= null;
203 // Mouse move events are fired without touching the mouse because of scrolling
204 // the container. Therefore, these events have to be suppressed.
205 this.suppressHovering_
= true;
207 // Calculates integral area from t1 to t2 of f(x) = sqrt(x) dx.
208 var integral = function(t1
, t2
) {
209 return 2.0 / 3.0 * Math
.pow(t2
, 3.0 / 2.0) -
210 2.0 / 3.0 * Math
.pow(t1
, 3.0 / 2.0);
213 var delta
= targetPosition
- this.scrollLeft
;
214 var factor
= delta
/ integral(0, Mosaic
.ANIMATED_SCROLL_DURATION
);
215 var startTime
= Date
.now();
216 var lastPosition
= 0;
217 var scrollOffset
= this.scrollLeft
;
219 var animationFrame = function() {
220 var position
= Date
.now() - startTime
;
222 integral(Math
.max(0, Mosaic
.ANIMATED_SCROLL_DURATION
- position
),
223 Math
.max(0, Mosaic
.ANIMATED_SCROLL_DURATION
- lastPosition
));
224 scrollOffset
+= step
;
226 var oldScrollLeft
= this.scrollLeft
;
227 var newScrollLeft
= Math
.round(scrollOffset
);
229 if (oldScrollLeft
!== newScrollLeft
)
230 this.scrollLeft
= newScrollLeft
;
232 if (step
=== 0 || this.scrollLeft
!== newScrollLeft
) {
233 this.scrollAnimation_
= null;
234 // Release the hovering lock after a safe delay to avoid hovering
235 // a tile because of altering |this.scrollLeft|.
236 setTimeout(function() {
237 if (!this.scrollAnimation_
)
238 this.suppressHovering_
= false;
241 // Continue the animation.
242 this.scrollAnimation_
= requestAnimationFrame(animationFrame
);
245 lastPosition
= position
;
248 // Start the animation.
249 this.scrollAnimation_
= requestAnimationFrame(animationFrame
);
253 * @return {Mosaic.Tile} Selected tile or undefined if no selection.
255 Mosaic
.prototype.getSelectedTile = function() {
256 return this.tiles_
&& this.tiles_
[this.selectionModel_
.selectedIndex
];
260 * @param {number} index Tile index.
261 * @return {Rect} Tile's image rectangle.
263 Mosaic
.prototype.getTileRect = function(index
) {
264 var tile
= this.tiles_
[index
];
265 return tile
&& tile
.getImageRect();
269 * @param {number} index Tile index.
270 * Scroll the given tile into the viewport.
272 Mosaic
.prototype.scrollIntoView = function(index
) {
273 var tile
= this.tiles_
[index
];
274 if (tile
) tile
.scrollIntoView();
278 * Initializes multiple tiles.
280 * @param {Array.<Mosaic.Tile>} tiles Array of tiles.
283 Mosaic
.prototype.initTiles_ = function(tiles
) {
284 for (var i
= 0; i
< tiles
.length
; i
++) {
292 Mosaic
.prototype.reload = function() {
293 this.layoutModel_
.reset_();
294 this.tiles_
.forEach(function(t
) { t
.markUnloaded(); });
295 this.initTiles_(this.tiles_
);
299 * Layouts the tiles in the order of their indices.
301 * Starts where it last stopped (at #0 the first time).
302 * Stops when all tiles are processed or when the next tile is still loading.
304 Mosaic
.prototype.layout = function() {
305 if (this.layoutTimer_
) {
306 clearTimeout(this.layoutTimer_
);
307 this.layoutTimer_
= null;
310 var index
= this.layoutModel_
.getTileCount();
311 if (index
=== this.tiles_
.length
)
312 break; // All tiles done.
313 var tile
= this.tiles_
[index
];
314 if (!tile
.isInitialized())
315 break; // Next layout will try to restart from here.
316 this.layoutModel_
.add(tile
, index
+ 1 === this.tiles_
.length
);
318 this.loadVisibleTiles_();
322 * Schedules the layout.
324 * @param {number=} opt_delay Delay in ms.
326 Mosaic
.prototype.scheduleLayout = function(opt_delay
) {
327 if (!this.layoutTimer_
) {
328 this.layoutTimer_
= setTimeout(function() {
329 this.layoutTimer_
= null;
331 }.bind(this), opt_delay
|| 0);
340 Mosaic
.prototype.onResize_ = function() {
341 this.layoutModel_
.setViewportSize(this.clientWidth
, this.clientHeight
-
342 (Mosaic
.Layout
.PADDING_TOP
+ Mosaic
.Layout
.PADDING_BOTTOM
));
343 this.scheduleLayout();
347 * Mouse event handler.
349 * @param {Event} event Event.
352 Mosaic
.prototype.onMouseEvent_ = function(event
) {
353 // Navigating with mouse, enable hover state.
354 if (!this.suppressHovering_
)
355 this.classList
.add('hover-visible');
357 if (event
.type
=== 'mousemove')
361 for (var target
= event
.target
;
362 target
&& (target
!== this);
363 target
= target
.parentNode
) {
364 if (target
.classList
.contains('mosaic-tile')) {
365 index
= this.dataModel_
.indexOf(target
.getItem());
369 this.selectionController_
.handlePointerDownUp(event
, index
);
376 Mosaic
.prototype.onScroll_ = function() {
377 requestAnimationFrame(function() {
378 this.loadVisibleTiles_();
383 * Selection change handler.
385 * @param {Event} event Event.
388 Mosaic
.prototype.onSelection_ = function(event
) {
389 for (var i
= 0; i
!== event
.changes
.length
; i
++) {
390 var change
= event
.changes
[i
];
391 var tile
= this.tiles_
[change
.index
];
392 if (tile
) tile
.select(change
.selected
);
397 * Leads item change handler.
399 * @param {Event} event Event.
402 Mosaic
.prototype.onLeadChange_ = function(event
) {
403 var index
= event
.newValue
;
405 var tile
= this.tiles_
[index
];
406 if (tile
) tile
.scrollIntoView();
411 * Splice event handler.
413 * @param {Event} event Event.
416 Mosaic
.prototype.onSplice_ = function(event
) {
417 var index
= event
.index
;
418 this.layoutModel_
.invalidateFromTile_(index
);
420 if (event
.removed
.length
) {
421 for (var t
= 0; t
!== event
.removed
.length
; t
++) {
422 // If the layout for the tile has not done yet, the parent is null.
423 // And the layout will not be done after onSplice_ because it is removed
425 if (this.tiles_
[index
+ t
].parentNode
)
426 this.removeChild(this.tiles_
[index
+ t
]);
429 this.tiles_
.splice(index
, event
.removed
.length
);
430 this.scheduleLayout(Mosaic
.LAYOUT_DELAY
);
433 if (event
.added
.length
) {
435 for (var t
= 0; t
!== event
.added
.length
; t
++)
436 newTiles
.push(new Mosaic
.Tile(this, this.dataModel_
.item(index
+ t
)));
438 this.tiles_
.splice
.apply(this.tiles_
, [index
, 0].concat(newTiles
));
439 this.initTiles_(newTiles
);
440 this.scheduleLayout(Mosaic
.LAYOUT_DELAY
);
443 if (this.tiles_
.length
!== this.dataModel_
.length
)
444 console
.error('Mosaic is out of sync');
448 * Content change handler.
450 * @param {Event} event Event.
453 Mosaic
.prototype.onContentChange_ = function(event
) {
458 return; // Thumbnail unchanged, nothing to do.
460 var index
= this.dataModel_
.indexOf(event
.item
);
461 if (index
!== this.selectionModel_
.selectedIndex
)
462 console
.error('Content changed for unselected item');
464 this.layoutModel_
.invalidateFromTile_(index
);
465 this.tiles_
[index
].init();
466 this.tiles_
[index
].unload();
467 this.tiles_
[index
].load(
468 Mosaic
.Tile
.LoadMode
.HIGH_DPI
,
469 this.scheduleLayout
.bind(this, Mosaic
.LAYOUT_DELAY
));
473 * Keydown event handler.
475 * @param {Event} event Event.
476 * @return {boolean} True if the event has been consumed.
478 Mosaic
.prototype.onKeyDown = function(event
) {
479 this.selectionController_
.handleKeyDown(event
);
480 if (event
.defaultPrevented
) // Navigating with keyboard, hide hover state.
481 this.classList
.remove('hover-visible');
482 return event
.defaultPrevented
;
486 * @return {boolean} True if the mosaic zoom effect can be applied. It is
487 * too slow if there are to many images.
488 * TODO(kaznacheev): Consider unloading the images that are out of the viewport.
490 Mosaic
.prototype.canZoom = function() {
491 return this.tiles_
.length
< 100;
497 Mosaic
.prototype.show = function() {
498 var duration
= ImageView
.MODE_TRANSITION_DURATION
;
499 if (this.canZoom()) {
500 // Fade in in parallel with the zoom effect.
501 this.setAttribute('visible', 'zooming');
503 // Mosaic is not animating but the large image is. Fade in the mosaic
504 // shortly before the large image animation is done.
507 this.showingTimeoutID_
= setTimeout(function() {
508 this.showingTimeoutID_
= null;
509 // Make the selection visible.
510 // If the mosaic is not animated it will start fading in now.
511 this.setAttribute('visible', 'normal');
512 this.loadVisibleTiles_();
513 }.bind(this), duration
);
519 Mosaic
.prototype.hide = function() {
520 if (this.showingTimeoutID_
!== null) {
521 clearTimeout(this.showingTimeoutID_
);
522 this.showingTimeoutID_
= null;
524 this.removeAttribute('visible');
528 * Checks if the mosaic view is visible.
529 * @return {boolean} True if visible, false otherwise.
532 Mosaic
.prototype.isVisible_ = function() {
533 return this.hasAttribute('visible');
537 * Loads visible tiles. Ignores consecutive calls. Does not reload already
541 Mosaic
.prototype.loadVisibleTiles_ = function() {
542 if (this.loadVisibleTilesSuppressed_
) {
543 this.loadVisibleTilesScheduled_
= true;
547 this.loadVisibleTilesSuppressed_
= true;
548 this.loadVisibleTilesScheduled_
= false;
549 setTimeout(function() {
550 this.loadVisibleTilesSuppressed_
= false;
551 if (this.loadVisibleTilesScheduled_
)
552 this.loadVisibleTiles_();
555 // Tiles only in the viewport (visible).
556 var visibleRect
= new Rect(0,
561 // Tiles in the viewport and also some distance on the left and right.
562 var renderableRect
= new Rect(-this.clientWidth
,
564 3 * this.clientWidth
,
567 // Unload tiles out of scope.
568 for (var index
= 0; index
< this.tiles_
.length
; index
++) {
569 var tile
= this.tiles_
[index
];
570 var imageRect
= tile
.getImageRect();
571 // Unload a thumbnail.
572 if (imageRect
&& !imageRect
.intersects(renderableRect
))
576 // Load the visible tiles first.
577 var allVisibleLoaded
= true;
578 // Show high-dpi only when the mosaic view is visible.
579 var loadMode
= this.isVisible_() ? Mosaic
.Tile
.LoadMode
.HIGH_DPI
:
580 Mosaic
.Tile
.LoadMode
.LOW_DPI
;
581 for (var index
= 0; index
< this.tiles_
.length
; index
++) {
582 var tile
= this.tiles_
[index
];
583 var imageRect
= tile
.getImageRect();
585 if (!tile
.isLoading(loadMode
) && !tile
.isLoaded(loadMode
) && imageRect
&&
586 imageRect
.intersects(visibleRect
)) {
587 tile
.load(loadMode
, function() {});
588 allVisibleLoaded
= false;
592 // Load also another, nearby, if the visible has been already loaded.
593 if (allVisibleLoaded
) {
594 for (var index
= 0; index
< this.tiles_
.length
; index
++) {
595 var tile
= this.tiles_
[index
];
596 var imageRect
= tile
.getImageRect();
598 if (!tile
.isLoading() && !tile
.isLoaded() && imageRect
&&
599 imageRect
.intersects(renderableRect
)) {
600 tile
.load(Mosaic
.Tile
.LoadMode
.LOW_DPI
, function() {});
607 * Applies reset the zoom transform.
609 * @param {Rect} tileRect Tile rectangle. Reset the transform if null.
610 * @param {Rect} imageRect Large image rectangle. Reset the transform if null.
611 * @param {boolean=} opt_instant True of the transition should be instant.
613 Mosaic
.prototype.transform = function(tileRect
, imageRect
, opt_instant
) {
615 this.style
.webkitTransitionDuration
= '0';
617 this.style
.webkitTransitionDuration
=
618 ImageView
.MODE_TRANSITION_DURATION
+ 'ms';
621 if (this.canZoom() && tileRect
&& imageRect
) {
622 var scaleX
= imageRect
.width
/ tileRect
.width
;
623 var scaleY
= imageRect
.height
/ tileRect
.height
;
624 var shiftX
= (imageRect
.left
+ imageRect
.width
/ 2) -
625 (tileRect
.left
+ tileRect
.width
/ 2);
626 var shiftY
= (imageRect
.top
+ imageRect
.height
/ 2) -
627 (tileRect
.top
+ tileRect
.height
/ 2);
628 this.style
.webkitTransform
=
629 'translate(' + shiftX
* scaleX
+ 'px, ' + shiftY
* scaleY
+ 'px)' +
630 'scaleX(' + scaleX
+ ') scaleY(' + scaleY
+ ')';
632 this.style
.webkitTransform
= '';
636 ////////////////////////////////////////////////////////////////////////////////
639 * Creates a selection controller that is to be used with grid.
640 * @param {cr.ui.ListSelectionModel} selectionModel The selection model to
642 * @param {Mosaic.Layout} layoutModel The layout model to use.
644 * @extends {!cr.ui.ListSelectionController}
646 Mosaic
.SelectionController = function(selectionModel
, layoutModel
) {
647 cr
.ui
.ListSelectionController
.call(this, selectionModel
);
648 this.layoutModel_
= layoutModel
;
652 * Extends cr.ui.ListSelectionController.
654 Mosaic
.SelectionController
.prototype.__proto__
=
655 cr
.ui
.ListSelectionController
.prototype;
658 Mosaic
.SelectionController
.prototype.getLastIndex = function() {
659 return this.layoutModel_
.getLaidOutTileCount() - 1;
663 Mosaic
.SelectionController
.prototype.getIndexBefore = function(index
) {
664 return this.layoutModel_
.getHorizontalAdjacentIndex(index
, -1);
668 Mosaic
.SelectionController
.prototype.getIndexAfter = function(index
) {
669 return this.layoutModel_
.getHorizontalAdjacentIndex(index
, 1);
673 Mosaic
.SelectionController
.prototype.getIndexAbove = function(index
) {
674 return this.layoutModel_
.getVerticalAdjacentIndex(index
, -1);
678 Mosaic
.SelectionController
.prototype.getIndexBelow = function(index
) {
679 return this.layoutModel_
.getVerticalAdjacentIndex(index
, 1);
682 ////////////////////////////////////////////////////////////////////////////////
687 * @param {string=} opt_mode Layout mode.
688 * @param {Mosaic.Density=} opt_maxDensity Layout density.
691 Mosaic
.Layout = function(opt_mode
, opt_maxDensity
) {
692 this.mode_
= opt_mode
|| Mosaic
.Layout
.MODE_TENTATIVE
;
693 this.maxDensity_
= opt_maxDensity
|| Mosaic
.Density
.createHighest();
698 * Blank space at the top of the mosaic element. We do not do that in CSS
699 * to make transition effects easier.
701 Mosaic
.Layout
.PADDING_TOP
= 50;
704 * Blank space at the bottom of the mosaic element.
706 Mosaic
.Layout
.PADDING_BOTTOM
= 50;
709 * Horizontal and vertical spacing between images. Should be kept in sync
710 * with the style of .mosaic-item in gallery.css (= 2 * ( 4 + 1))
712 Mosaic
.Layout
.SPACING
= 10;
715 * Margin for scrolling using keyboard. Distance between a selected tile
718 Mosaic
.Layout
.SCROLL_MARGIN
= 30;
721 * Layout mode: commit to DOM immediately.
723 Mosaic
.Layout
.MODE_FINAL
= 'final';
726 * Layout mode: do not commit layout to DOM until it is complete or the viewport
729 Mosaic
.Layout
.MODE_TENTATIVE
= 'tentative';
732 * Layout mode: never commit layout to DOM.
734 Mosaic
.Layout
.MODE_DRY_RUN
= 'dry_run';
741 Mosaic
.Layout
.prototype.reset_ = function() {
743 this.newColumn_
= null;
744 this.density_
= Mosaic
.Density
.createLowest();
745 if (this.mode_
!== Mosaic
.Layout
.MODE_DRY_RUN
) // DRY_RUN is sticky.
746 this.mode_
= Mosaic
.Layout
.MODE_TENTATIVE
;
750 * @param {number} width Viewport width.
751 * @param {number} height Viewport height.
753 Mosaic
.Layout
.prototype.setViewportSize = function(width
, height
) {
754 this.viewportWidth_
= width
;
755 this.viewportHeight_
= height
;
760 * @return {number} Total width of the layout.
762 Mosaic
.Layout
.prototype.getWidth = function() {
763 var lastColumn
= this.getLastColumn_();
764 return lastColumn
? lastColumn
.getRight() : 0;
768 * @return {number} Total height of the layout.
770 Mosaic
.Layout
.prototype.getHeight = function() {
771 var firstColumn
= this.columns_
[0];
772 return firstColumn
? firstColumn
.getHeight() : 0;
776 * @return {Array.<Mosaic.Tile>} All tiles in the layout.
778 Mosaic
.Layout
.prototype.getTiles = function() {
779 return Array
.prototype.concat
.apply([],
780 this.columns_
.map(function(c
) { return c
.getTiles(); }));
784 * @return {number} Total number of tiles added to the layout.
786 Mosaic
.Layout
.prototype.getTileCount = function() {
787 return this.getLaidOutTileCount() +
788 (this.newColumn_
? this.newColumn_
.getTileCount() : 0);
792 * @return {Mosaic.Column} The last column or null for empty layout.
795 Mosaic
.Layout
.prototype.getLastColumn_ = function() {
796 return this.columns_
.length
? this.columns_
[this.columns_
.length
- 1] : null;
800 * @return {number} Total number of tiles in completed columns.
802 Mosaic
.Layout
.prototype.getLaidOutTileCount = function() {
803 var lastColumn
= this.getLastColumn_();
804 return lastColumn
? lastColumn
.getNextTileIndex() : 0;
808 * Adds a tile to the layout.
810 * @param {Mosaic.Tile} tile The tile to be added.
811 * @param {boolean} isLast True if this tile is the last.
813 Mosaic
.Layout
.prototype.add = function(tile
, isLast
) {
814 var layoutQueue
= [tile
];
816 // There are two levels of backtracking in the layout algorithm.
817 // |Mosaic.Layout.density_| tracks the state of the 'global' backtracking
818 // which aims to use as much of the viewport space as possible.
819 // It starts with the lowest density and increases it until the layout
820 // fits into the viewport. If it does not fit even at the highest density,
821 // the layout continues with the highest density.
823 // |Mosaic.Column.density_| tracks the state of the 'local' backtracking
824 // which aims to avoid producing unnaturally looking columns.
825 // It starts with the current global density and decreases it until the column
828 while (layoutQueue
.length
) {
829 if (!this.newColumn_
) {
830 var lastColumn
= this.getLastColumn_();
831 this.newColumn_
= new Mosaic
.Column(
832 this.columns_
.length
,
833 lastColumn
? lastColumn
.getNextRowIndex() : 0,
834 lastColumn
? lastColumn
.getNextTileIndex() : 0,
835 lastColumn
? lastColumn
.getRight() : 0,
836 this.viewportHeight_
,
837 this.density_
.clone());
840 this.newColumn_
.add(layoutQueue
.shift());
842 var isFinalColumn
= isLast
&& !layoutQueue
.length
;
844 if (!this.newColumn_
.prepareLayout(isFinalColumn
))
845 continue; // Column is incomplete.
847 if (this.newColumn_
.isSuboptimal()) {
848 layoutQueue
= this.newColumn_
.getTiles().concat(layoutQueue
);
849 this.newColumn_
.retryWithLowerDensity();
853 this.columns_
.push(this.newColumn_
);
854 this.newColumn_
= null;
856 if (this.mode_
=== Mosaic
.Layout
.MODE_FINAL
&& isFinalColumn
) {
861 if (this.getWidth() > this.viewportWidth_
) {
862 // Viewport completely filled.
863 if (this.density_
.equals(this.maxDensity_
)) {
864 // Max density reached, commit if tentative, just continue if dry run.
865 if (this.mode_
=== Mosaic
.Layout
.MODE_TENTATIVE
)
870 // Rollback the entire layout, retry with higher density.
871 layoutQueue
= this.getTiles().concat(layoutQueue
);
873 this.density_
.increase();
877 if (isFinalColumn
&& this.mode_
=== Mosaic
.Layout
.MODE_TENTATIVE
) {
878 // The complete tentative layout fits into the viewport.
879 var stretched
= this.findHorizontalLayout_();
881 this.columns_
= stretched
.columns_
;
882 // Center the layout in the viewport and commit.
883 this.commit_((this.viewportWidth_
- this.getWidth()) / 2,
884 (this.viewportHeight_
- this.getHeight()) / 2);
890 * Commits the tentative layout.
892 * @param {number=} opt_offsetX Horizontal offset.
893 * @param {number=} opt_offsetY Vertical offset.
896 Mosaic
.Layout
.prototype.commit_ = function(opt_offsetX
, opt_offsetY
) {
897 for (var i
= 0; i
!== this.columns_
.length
; i
++) {
898 this.columns_
[i
].layout(opt_offsetX
, opt_offsetY
);
900 this.mode_
= Mosaic
.Layout
.MODE_FINAL
;
904 * Finds the most horizontally stretched layout built from the same tiles.
906 * The main layout algorithm fills the entire available viewport height.
907 * If there is too few tiles this results in a layout that is unnaturally
908 * stretched in the vertical direction.
910 * This method tries a number of smaller heights and returns the most
911 * horizontally stretched layout that still fits into the viewport.
913 * @return {Mosaic.Layout} A horizontally stretched layout.
916 Mosaic
.Layout
.prototype.findHorizontalLayout_ = function() {
917 // If the layout aspect ratio is not dramatically different from
918 // the viewport aspect ratio then there is no need to optimize.
919 if (this.getWidth() / this.getHeight() >
920 this.viewportWidth_
/ this.viewportHeight_
* 0.9)
923 var tiles
= this.getTiles();
924 if (tiles
.length
=== 1)
925 return null; // Single tile layout is always the same.
927 var tileHeights
= tiles
.map(function(t
) { return t
.getMaxContentHeight(); });
928 var minTileHeight
= Math
.min
.apply(null, tileHeights
);
930 for (var h
= minTileHeight
; h
< this.viewportHeight_
; h
+= minTileHeight
) {
931 var layout
= new Mosaic
.Layout(
932 Mosaic
.Layout
.MODE_DRY_RUN
, this.density_
.clone());
933 layout
.setViewportSize(this.viewportWidth_
, h
);
934 for (var t
= 0; t
!== tiles
.length
; t
++)
935 layout
.add(tiles
[t
], t
+ 1 === tiles
.length
);
937 if (layout
.getWidth() <= this.viewportWidth_
)
945 * Invalidates the layout after the given tile was modified (added, deleted or
946 * changed dimensions).
948 * @param {number} index Tile index.
951 Mosaic
.Layout
.prototype.invalidateFromTile_ = function(index
) {
952 var columnIndex
= this.getColumnIndexByTile_(index
);
954 return; // Index not in the layout, probably already invalidated.
956 if (this.columns_
[columnIndex
].getLeft() >= this.viewportWidth_
) {
957 // The columns to the right cover the entire viewport width, so there is no
958 // chance that the modified layout would fit into the viewport.
959 // No point in restarting the entire layout, keep the columns to the right.
960 console
.assert(this.mode_
=== Mosaic
.Layout
.MODE_FINAL
,
961 'Expected FINAL layout mode');
962 this.columns_
= this.columns_
.slice(0, columnIndex
);
963 this.newColumn_
= null;
965 // There is a chance that the modified layout would fit into the viewport.
967 this.mode_
= Mosaic
.Layout
.MODE_TENTATIVE
;
972 * Gets the index of the tile to the left or to the right from the given tile.
974 * @param {number} index Tile index.
975 * @param {number} direction -1 for left, 1 for right.
976 * @return {number} Adjacent tile index.
978 Mosaic
.Layout
.prototype.getHorizontalAdjacentIndex = function(
980 var column
= this.getColumnIndexByTile_(index
);
982 console
.error('Cannot find column for tile #' + index
);
986 var row
= this.columns_
[column
].getRowByTileIndex(index
);
988 console
.error('Cannot find row for tile #' + index
);
992 var sameRowNeighbourIndex
= index
+ direction
;
993 if (row
.hasTile(sameRowNeighbourIndex
))
994 return sameRowNeighbourIndex
;
996 var adjacentColumn
= column
+ direction
;
997 if (adjacentColumn
< 0 || adjacentColumn
=== this.columns_
.length
)
1000 return this.columns_
[adjacentColumn
].
1001 getEdgeTileIndex_(row
.getCenterY(), -direction
);
1005 * Gets the index of the tile to the top or to the bottom from the given tile.
1007 * @param {number} index Tile index.
1008 * @param {number} direction -1 for above, 1 for below.
1009 * @return {number} Adjacent tile index.
1011 Mosaic
.Layout
.prototype.getVerticalAdjacentIndex = function(
1013 var column
= this.getColumnIndexByTile_(index
);
1015 console
.error('Cannot find column for tile #' + index
);
1019 var row
= this.columns_
[column
].getRowByTileIndex(index
);
1021 console
.error('Cannot find row for tile #' + index
);
1025 // Find the first item in the next row, or the last item in the previous row.
1026 var adjacentRowNeighbourIndex
=
1027 row
.getEdgeTileIndex_(direction
) + direction
;
1029 if (adjacentRowNeighbourIndex
< 0 ||
1030 adjacentRowNeighbourIndex
> this.getTileCount() - 1)
1033 if (!this.columns_
[column
].hasTile(adjacentRowNeighbourIndex
)) {
1034 // It is not in the current column, so return it.
1035 return adjacentRowNeighbourIndex
;
1037 // It is in the current column, so we have to find optically the closest
1038 // tile in the adjacent row.
1039 var adjacentRow
= this.columns_
[column
].getRowByTileIndex(
1040 adjacentRowNeighbourIndex
);
1041 var previousTileCenterX
= row
.getTileByIndex(index
).getCenterX();
1043 // Find the closest one.
1044 var closestIndex
= -1;
1045 var closestDistance
;
1046 var adjacentRowTiles
= adjacentRow
.getTiles();
1047 for (var t
= 0; t
!== adjacentRowTiles
.length
; t
++) {
1049 Math
.abs(adjacentRowTiles
[t
].getCenterX() - previousTileCenterX
);
1050 if (closestIndex
=== -1 || distance
< closestDistance
) {
1051 closestIndex
= adjacentRow
.getEdgeTileIndex_(-1) + t
;
1052 closestDistance
= distance
;
1055 return closestIndex
;
1060 * @param {number} index Tile index.
1061 * @return {number} Index of the column containing the given tile.
1064 Mosaic
.Layout
.prototype.getColumnIndexByTile_ = function(index
) {
1065 for (var c
= 0; c
!== this.columns_
.length
; c
++) {
1066 if (this.columns_
[c
].hasTile(index
))
1073 * Scales the given array of size values to satisfy 3 conditions:
1074 * 1. The new sizes must be integer.
1075 * 2. The new sizes must sum up to the given |total| value.
1076 * 3. The relative proportions of the sizes should be as close to the original
1079 * @param {Array.<number>} sizes Array of sizes.
1080 * @param {number} newTotal New total size.
1082 Mosaic
.Layout
.rescaleSizesToNewTotal = function(sizes
, newTotal
) {
1085 var partialTotals
= [0];
1086 for (var i
= 0; i
!== sizes
.length
; i
++) {
1088 partialTotals
.push(total
);
1091 var scale
= newTotal
/ total
;
1093 for (i
= 0; i
!== sizes
.length
; i
++) {
1094 sizes
[i
] = Math
.round(partialTotals
[i
+ 1] * scale
) -
1095 Math
.round(partialTotals
[i
] * scale
);
1099 ////////////////////////////////////////////////////////////////////////////////
1102 * Representation of the layout density.
1104 * @param {number} horizontal Horizontal density, number tiles per row.
1105 * @param {number} vertical Vertical density, frequency of rows forced to
1106 * contain a single tile.
1109 Mosaic
.Density = function(horizontal
, vertical
) {
1110 this.horizontal
= horizontal
;
1111 this.vertical
= vertical
;
1115 * Minimal horizontal density (tiles per row).
1117 Mosaic
.Density
.MIN_HORIZONTAL
= 1;
1120 * Minimal horizontal density (tiles per row).
1122 Mosaic
.Density
.MAX_HORIZONTAL
= 3;
1125 * Minimal vertical density: force 1 out of 2 rows to containt a single tile.
1127 Mosaic
.Density
.MIN_VERTICAL
= 2;
1130 * Maximal vertical density: force 1 out of 3 rows to containt a single tile.
1132 Mosaic
.Density
.MAX_VERTICAL
= 3;
1135 * @return {Mosaic.Density} Lowest density.
1137 Mosaic
.Density
.createLowest = function() {
1138 return new Mosaic
.Density(
1139 Mosaic
.Density
.MIN_HORIZONTAL
,
1140 Mosaic
.Density
.MIN_VERTICAL
/* ignored when horizontal is at min */);
1144 * @return {Mosaic.Density} Highest density.
1146 Mosaic
.Density
.createHighest = function() {
1147 return new Mosaic
.Density(
1148 Mosaic
.Density
.MAX_HORIZONTAL
,
1149 Mosaic
.Density
.MAX_VERTICAL
);
1153 * @return {Mosaic.Density} A clone of this density object.
1155 Mosaic
.Density
.prototype.clone = function() {
1156 return new Mosaic
.Density(this.horizontal
, this.vertical
);
1160 * @param {Mosaic.Density} that The other object.
1161 * @return {boolean} True if equal.
1163 Mosaic
.Density
.prototype.equals = function(that
) {
1164 return this.horizontal
=== that
.horizontal
&&
1165 this.vertical
=== that
.vertical
;
1169 * Increases the density to the next level.
1171 Mosaic
.Density
.prototype.increase = function() {
1172 if (this.horizontal
=== Mosaic
.Density
.MIN_HORIZONTAL
||
1173 this.vertical
=== Mosaic
.Density
.MAX_VERTICAL
) {
1174 console
.assert(this.horizontal
< Mosaic
.Density
.MAX_HORIZONTAL
);
1176 this.vertical
= Mosaic
.Density
.MIN_VERTICAL
;
1183 * Decreases horizontal density.
1185 Mosaic
.Density
.prototype.decreaseHorizontal = function() {
1186 console
.assert(this.horizontal
> Mosaic
.Density
.MIN_HORIZONTAL
);
1191 * @param {number} tileCount Number of tiles in the row.
1192 * @param {number} rowIndex Global row index.
1193 * @return {boolean} True if the row is complete.
1195 Mosaic
.Density
.prototype.isRowComplete = function(tileCount
, rowIndex
) {
1196 return (tileCount
=== this.horizontal
) || (rowIndex
% this.vertical
) === 0;
1199 ////////////////////////////////////////////////////////////////////////////////
1202 * A column in a mosaic layout. Contains rows.
1204 * @param {number} index Column index.
1205 * @param {number} firstRowIndex Global row index.
1206 * @param {number} firstTileIndex Index of the first tile in the column.
1207 * @param {number} left Left edge coordinate.
1208 * @param {number} maxHeight Maximum height.
1209 * @param {Mosaic.Density} density Layout density.
1212 Mosaic
.Column = function(index
, firstRowIndex
, firstTileIndex
, left
, maxHeight
,
1214 this.index_
= index
;
1215 this.firstRowIndex_
= firstRowIndex
;
1216 this.firstTileIndex_
= firstTileIndex
;
1218 this.maxHeight_
= maxHeight
;
1219 this.density_
= density
;
1225 * Resets the layout.
1228 Mosaic
.Column
.prototype.reset_ = function() {
1231 this.newRow_
= null;
1235 * @return {number} Number of tiles in the column.
1237 Mosaic
.Column
.prototype.getTileCount = function() { return this.tiles_
.length
};
1240 * @return {number} Index of the last tile + 1.
1242 Mosaic
.Column
.prototype.getNextTileIndex = function() {
1243 return this.firstTileIndex_
+ this.getTileCount();
1247 * @return {number} Global index of the last row + 1.
1249 Mosaic
.Column
.prototype.getNextRowIndex = function() {
1250 return this.firstRowIndex_
+ this.rows_
.length
;
1254 * @return {Array.<Mosaic.Tile>} Array of tiles in the column.
1256 Mosaic
.Column
.prototype.getTiles = function() { return this.tiles_
};
1259 * @param {number} index Tile index.
1260 * @return {boolean} True if this column contains the tile with the given index.
1262 Mosaic
.Column
.prototype.hasTile = function(index
) {
1263 return this.firstTileIndex_
<= index
&&
1264 index
< (this.firstTileIndex_
+ this.getTileCount());
1268 * @param {number} y Y coordinate.
1269 * @param {number} direction -1 for left, 1 for right.
1270 * @return {number} Index of the tile lying on the edge of the column at the
1271 * given y coordinate.
1274 Mosaic
.Column
.prototype.getEdgeTileIndex_ = function(y
, direction
) {
1275 for (var r
= 0; r
< this.rows_
.length
; r
++) {
1276 if (this.rows_
[r
].coversY(y
))
1277 return this.rows_
[r
].getEdgeTileIndex_(direction
);
1283 * @param {number} index Tile index.
1284 * @return {Mosaic.Row} The row containing the tile with a given index.
1286 Mosaic
.Column
.prototype.getRowByTileIndex = function(index
) {
1287 for (var r
= 0; r
!== this.rows_
.length
; r
++)
1288 if (this.rows_
[r
].hasTile(index
))
1289 return this.rows_
[r
];
1295 * Adds a tile to the column.
1297 * @param {Mosaic.Tile} tile The tile to add.
1299 Mosaic
.Column
.prototype.add = function(tile
) {
1300 var rowIndex
= this.getNextRowIndex();
1303 this.newRow_
= new Mosaic
.Row(this.getNextTileIndex());
1305 this.tiles_
.push(tile
);
1306 this.newRow_
.add(tile
);
1308 if (this.density_
.isRowComplete(this.newRow_
.getTileCount(), rowIndex
)) {
1309 this.rows_
.push(this.newRow_
);
1310 this.newRow_
= null;
1315 * Prepares the column layout.
1317 * @param {boolean=} opt_force True if the layout must be performed even for an
1318 * incomplete column.
1319 * @return {boolean} True if the layout was performed.
1321 Mosaic
.Column
.prototype.prepareLayout = function(opt_force
) {
1322 if (opt_force
&& this.newRow_
) {
1323 this.rows_
.push(this.newRow_
);
1324 this.newRow_
= null;
1327 if (this.rows_
.length
=== 0)
1330 this.width_
= Math
.min
.apply(
1331 null, this.rows_
.map(function(row
) { return row
.getMaxWidth() }));
1335 this.rowHeights_
= [];
1336 for (var r
= 0; r
!== this.rows_
.length
; r
++) {
1337 var rowHeight
= this.rows_
[r
].getHeightForWidth(this.width_
);
1338 this.height_
+= rowHeight
;
1339 this.rowHeights_
.push(rowHeight
);
1342 var overflow
= this.height_
/ this.maxHeight_
;
1343 if (!opt_force
&& (overflow
< 1))
1347 // Scale down the column width and height.
1348 this.width_
= Math
.round(this.width_
/ overflow
);
1349 this.height_
= this.maxHeight_
;
1350 Mosaic
.Layout
.rescaleSizesToNewTotal(this.rowHeights_
, this.maxHeight_
);
1357 * Retries the column layout with less tiles per row.
1359 Mosaic
.Column
.prototype.retryWithLowerDensity = function() {
1360 this.density_
.decreaseHorizontal();
1365 * @return {number} Column left edge coordinate.
1367 Mosaic
.Column
.prototype.getLeft = function() { return this.left_
};
1370 * @return {number} Column right edge coordinate after the layout.
1372 Mosaic
.Column
.prototype.getRight = function() {
1373 return this.left_
+ this.width_
;
1377 * @return {number} Column height after the layout.
1379 Mosaic
.Column
.prototype.getHeight = function() { return this.height_
};
1382 * Performs the column layout.
1383 * @param {number=} opt_offsetX Horizontal offset.
1384 * @param {number=} opt_offsetY Vertical offset.
1386 Mosaic
.Column
.prototype.layout = function(opt_offsetX
, opt_offsetY
) {
1387 opt_offsetX
= opt_offsetX
|| 0;
1388 opt_offsetY
= opt_offsetY
|| 0;
1389 var rowTop
= Mosaic
.Layout
.PADDING_TOP
;
1390 for (var r
= 0; r
!== this.rows_
.length
; r
++) {
1391 this.rows_
[r
].layout(
1392 opt_offsetX
+ this.left_
,
1393 opt_offsetY
+ rowTop
,
1395 this.rowHeights_
[r
]);
1396 rowTop
+= this.rowHeights_
[r
];
1401 * Checks if the column layout is too ugly to be displayed.
1403 * @return {boolean} True if the layout is suboptimal.
1405 Mosaic
.Column
.prototype.isSuboptimal = function() {
1407 this.rows_
.map(function(row
) { return row
.getTileCount() });
1409 var maxTileCount
= Math
.max
.apply(null, tileCounts
);
1410 if (maxTileCount
=== 1)
1411 return false; // Every row has exactly 1 tile, as optimal as it gets.
1414 this.tiles_
.map(function(tile
) { return tile
.getMaxContentHeight() });
1416 // Ugly layout #1: all images are small and some are one the same row.
1417 var allSmall
= Math
.max
.apply(null, sizes
) <= Mosaic
.Tile
.SMALL_IMAGE_SIZE
;
1421 // Ugly layout #2: all images are large and none occupies an entire row.
1422 var allLarge
= Math
.min
.apply(null, sizes
) > Mosaic
.Tile
.SMALL_IMAGE_SIZE
;
1423 var allCombined
= Math
.min
.apply(null, tileCounts
) !== 1;
1424 if (allLarge
&& allCombined
)
1427 // Ugly layout #3: some rows have too many tiles for the resulting width.
1428 if (this.width_
/ maxTileCount
< 100)
1434 ////////////////////////////////////////////////////////////////////////////////
1437 * A row in a mosaic layout. Contains tiles.
1439 * @param {number} firstTileIndex Index of the first tile in the row.
1442 Mosaic
.Row = function(firstTileIndex
) {
1443 this.firstTileIndex_
= firstTileIndex
;
1448 * @param {Mosaic.Tile} tile The tile to add.
1450 Mosaic
.Row
.prototype.add = function(tile
) {
1451 console
.assert(this.getTileCount() < Mosaic
.Density
.MAX_HORIZONTAL
);
1452 this.tiles_
.push(tile
);
1456 * @return {Array.<Mosaic.Tile>} Array of tiles in the row.
1458 Mosaic
.Row
.prototype.getTiles = function() { return this.tiles_
};
1461 * Gets a tile by index.
1462 * @param {number} index Tile index.
1463 * @return {Mosaic.Tile} Requested tile or null if not found.
1465 Mosaic
.Row
.prototype.getTileByIndex = function(index
) {
1466 if (!this.hasTile(index
))
1468 return this.tiles_
[index
- this.firstTileIndex_
];
1473 * @return {number} Number of tiles in the row.
1475 Mosaic
.Row
.prototype.getTileCount = function() { return this.tiles_
.length
};
1478 * @param {number} index Tile index.
1479 * @return {boolean} True if this row contains the tile with the given index.
1481 Mosaic
.Row
.prototype.hasTile = function(index
) {
1482 return this.firstTileIndex_
<= index
&&
1483 index
< (this.firstTileIndex_
+ this.tiles_
.length
);
1487 * @param {number} y Y coordinate.
1488 * @return {boolean} True if this row covers the given Y coordinate.
1490 Mosaic
.Row
.prototype.coversY = function(y
) {
1491 return this.top_
<= y
&& y
< (this.top_
+ this.height_
);
1495 * @return {number} Y coordinate of the tile center.
1497 Mosaic
.Row
.prototype.getCenterY = function() {
1498 return this.top_
+ Math
.round(this.height_
/ 2);
1502 * Gets the first or the last tile.
1504 * @param {number} direction -1 for the first tile, 1 for the last tile.
1505 * @return {number} Tile index.
1508 Mosaic
.Row
.prototype.getEdgeTileIndex_ = function(direction
) {
1510 return this.firstTileIndex_
;
1512 return this.firstTileIndex_
+ this.getTileCount() - 1;
1516 * @return {number} Aspect ration of the combined content box of this row.
1519 Mosaic
.Row
.prototype.getTotalContentAspectRatio_ = function() {
1521 for (var t
= 0; t
!== this.tiles_
.length
; t
++)
1522 sum
+= this.tiles_
[t
].getAspectRatio();
1527 * @return {number} Total horizontal spacing in this row. This includes
1528 * the spacing between the tiles and both left and right margins.
1532 Mosaic
.Row
.prototype.getTotalHorizontalSpacing_ = function() {
1533 return Mosaic
.Layout
.SPACING
* this.getTileCount();
1537 * @return {number} Maximum width that this row may have without overscaling
1540 Mosaic
.Row
.prototype.getMaxWidth = function() {
1541 var contentHeight
= Math
.min
.apply(null,
1542 this.tiles_
.map(function(tile
) { return tile
.getMaxContentHeight() }));
1545 Math
.round(contentHeight
* this.getTotalContentAspectRatio_());
1546 return contentWidth
+ this.getTotalHorizontalSpacing_();
1550 * Computes the height that best fits the supplied row width given
1551 * aspect ratios of the tiles in this row.
1553 * @param {number} width Row width.
1554 * @return {number} Height.
1556 Mosaic
.Row
.prototype.getHeightForWidth = function(width
) {
1557 var contentWidth
= width
- this.getTotalHorizontalSpacing_();
1559 Math
.round(contentWidth
/ this.getTotalContentAspectRatio_());
1560 return contentHeight
+ Mosaic
.Layout
.SPACING
;
1564 * Positions the row in the mosaic.
1566 * @param {number} left Left position.
1567 * @param {number} top Top position.
1568 * @param {number} width Width.
1569 * @param {number} height Height.
1571 Mosaic
.Row
.prototype.layout = function(left
, top
, width
, height
) {
1573 this.height_
= height
;
1575 var contentWidth
= width
- this.getTotalHorizontalSpacing_();
1576 var contentHeight
= height
- Mosaic
.Layout
.SPACING
;
1578 var tileContentWidth
= this.tiles_
.map(
1579 function(tile
) { return tile
.getAspectRatio() });
1581 Mosaic
.Layout
.rescaleSizesToNewTotal(tileContentWidth
, contentWidth
);
1583 var tileLeft
= left
;
1584 for (var t
= 0; t
!== this.tiles_
.length
; t
++) {
1585 var tileWidth
= tileContentWidth
[t
] + Mosaic
.Layout
.SPACING
;
1586 this.tiles_
[t
].layout(tileLeft
, top
, tileWidth
, height
);
1587 tileLeft
+= tileWidth
;
1591 ////////////////////////////////////////////////////////////////////////////////
1594 * A single tile of the image mosaic.
1596 * @param {Element} container Container element.
1597 * @param {Gallery.Item} item Gallery item associated with this tile.
1598 * @param {EntryLocation} locationInfo Location information for the tile.
1599 * @return {Element} The new tile element.
1602 Mosaic
.Tile = function(container
, item
, locationInfo
) {
1603 var self
= container
.ownerDocument
.createElement('div');
1604 Mosaic
.Tile
.decorate(self
, container
, item
, locationInfo
);
1609 * @param {Element} self Self pointer.
1610 * @param {Element} container Container element.
1611 * @param {Gallery.Item} item Gallery item associated with this tile.
1612 * @param {EntryLocation} locationInfo Location info for the tile image.
1614 Mosaic
.Tile
.decorate = function(self
, container
, item
, locationInfo
) {
1615 self
.__proto__
= Mosaic
.Tile
.prototype;
1616 self
.className
= 'mosaic-tile';
1618 self
.container_
= container
;
1620 self
.left_
= null; // Mark as not laid out.
1621 self
.hidpiEmbedded_
= locationInfo
&& locationInfo
.isDriveBased
;
1625 * Load mode for the tile's image.
1628 Mosaic
.Tile
.LoadMode
= {
1634 * Inherit from HTMLDivElement.
1636 Mosaic
.Tile
.prototype.__proto__
= HTMLDivElement
.prototype;
1639 * Minimum tile content size.
1641 Mosaic
.Tile
.MIN_CONTENT_SIZE
= 64;
1644 * Maximum tile content size.
1646 Mosaic
.Tile
.MAX_CONTENT_SIZE
= 512;
1649 * Default size for a tile with no thumbnail image.
1651 Mosaic
.Tile
.GENERIC_ICON_SIZE
= 128;
1654 * Max size of an image considered to be 'small'.
1655 * Small images are laid out slightly differently.
1657 Mosaic
.Tile
.SMALL_IMAGE_SIZE
= 160;
1660 * @return {Gallery.Item} The Gallery item.
1662 Mosaic
.Tile
.prototype.getItem = function() { return this.item_
; };
1665 * @return {number} Maximum content height that this tile can have.
1667 Mosaic
.Tile
.prototype.getMaxContentHeight = function() {
1668 return this.maxContentHeight_
;
1672 * @return {number} The aspect ratio of the tile image.
1674 Mosaic
.Tile
.prototype.getAspectRatio = function() { return this.aspectRatio_
; };
1677 * @return {boolean} True if the tile is initialized.
1679 Mosaic
.Tile
.prototype.isInitialized = function() {
1680 return !!this.maxContentHeight_
;
1684 * Checks whether the image of specified (or better resolution) has been loaded.
1686 * @param {Mosaic.Tile.LoadMode=} opt_loadMode Loading mode, default: LOW_DPI.
1687 * @return {boolean} True if the tile is loaded with the specified dpi or
1690 Mosaic
.Tile
.prototype.isLoaded = function(opt_loadMode
) {
1691 var loadMode
= opt_loadMode
|| Mosaic
.Tile
.LoadMode
.LOW_DPI
;
1693 case Mosaic
.Tile
.LoadMode
.LOW_DPI
:
1694 if (this.imagePreloaded_
|| this.imageLoaded_
)
1697 case Mosaic
.Tile
.LoadMode
.HIGH_DPI
:
1698 if (this.imageLoaded_
)
1706 * Checks whether the image of specified (or better resolution) is being loaded.
1708 * @param {Mosaic.Tile.LoadMode=} opt_loadMode Loading mode, default: LOW_DPI.
1709 * @return {boolean} True if the tile is being loaded with the specified dpi or
1712 Mosaic
.Tile
.prototype.isLoading = function(opt_loadMode
) {
1713 var loadMode
= opt_loadMode
|| Mosaic
.Tile
.LoadMode
.LOW_DPI
;
1715 case Mosaic
.Tile
.LoadMode
.LOW_DPI
:
1716 if (this.imagePreloading_
|| this.imageLoading_
)
1719 case Mosaic
.Tile
.LoadMode
.HIGH_DPI
:
1720 if (this.imageLoading_
)
1728 * Marks the tile as not loaded to prevent it from participating in the layout.
1730 Mosaic
.Tile
.prototype.markUnloaded = function() {
1731 this.maxContentHeight_
= 0;
1732 if (this.thumbnailLoader_
) {
1733 this.thumbnailLoader_
.cancel();
1734 this.imagePreloaded_
= false;
1735 this.imagePreloading_
= false;
1736 this.imageLoaded_
= false;
1737 this.imageLoading_
= false;
1742 * Initializes the thumbnail in the tile. Does not load an image, but sets
1743 * target dimensions using metadata.
1745 Mosaic
.Tile
.prototype.init = function() {
1746 var metadata
= this.getItem().getMetadata();
1747 this.markUnloaded();
1748 this.left_
= null; // Mark as not laid out.
1750 // Set higher priority for the selected elements to load them first.
1751 var priority
= this.getAttribute('selected') ? 2 : 3;
1753 // Use embedded thumbnails on Drive, since they have higher resolution.
1754 this.thumbnailLoader_
= new ThumbnailLoader(
1755 this.getItem().getEntry(),
1756 ThumbnailLoader
.LoaderType
.CANVAS
,
1758 undefined, // Media type.
1759 this.hidpiEmbedded_
?
1760 ThumbnailLoader
.UseEmbedded
.USE_EMBEDDED
:
1761 ThumbnailLoader
.UseEmbedded
.NO_EMBEDDED
,
1764 // If no hidpi embedded thumbnail available, then use the low resolution
1766 if (!this.hidpiEmbedded_
) {
1767 this.thumbnailPreloader_
= new ThumbnailLoader(
1768 this.getItem().getEntry(),
1769 ThumbnailLoader
.LoaderType
.CANVAS
,
1771 undefined, // Media type.
1772 ThumbnailLoader
.UseEmbedded
.USE_EMBEDDED
,
1773 2); // Preloaders have always higher priotity, so the preload images
1774 // are loaded as soon as possible.
1777 // Dimensions are always acquired from the metadata. For local files, it is
1778 // extracted from headers. For Drive files, it is received via the Drive API.
1779 // If the dimensions are not available, then the fallback dimensions will be
1780 // used (same as for the generic icon).
1783 if (metadata
.media
&& metadata
.media
.width
) {
1784 width
= metadata
.media
.width
;
1785 height
= metadata
.media
.height
;
1786 } else if (metadata
.drive
&& metadata
.drive
.imageWidth
&&
1787 metadata
.drive
.imageHeight
) {
1788 width
= metadata
.drive
.imageWidth
;
1789 height
= metadata
.drive
.imageHeight
;
1791 // No dimensions in metadata, then use the generic dimensions.
1792 width
= Mosaic
.Tile
.GENERIC_ICON_SIZE
;
1793 height
= Mosaic
.Tile
.GENERIC_ICON_SIZE
;
1796 if (width
> height
) {
1797 if (width
> Mosaic
.Tile
.MAX_CONTENT_SIZE
) {
1798 height
= Math
.round(height
* Mosaic
.Tile
.MAX_CONTENT_SIZE
/ width
);
1799 width
= Mosaic
.Tile
.MAX_CONTENT_SIZE
;
1802 if (height
> Mosaic
.Tile
.MAX_CONTENT_SIZE
) {
1803 width
= Math
.round(width
* Mosaic
.Tile
.MAX_CONTENT_SIZE
/ height
);
1804 height
= Mosaic
.Tile
.MAX_CONTENT_SIZE
;
1807 this.maxContentHeight_
= Math
.max(Mosaic
.Tile
.MIN_CONTENT_SIZE
, height
);
1808 this.aspectRatio_
= width
/ height
;
1812 * Loads an image into the tile.
1814 * The mode argument is a hint. Use low-dpi for faster response, and high-dpi
1815 * for better output, but possibly affecting performance.
1817 * If the mode is high-dpi, then a the high-dpi image is loaded, but also
1818 * low-dpi image is loaded for preloading (if available).
1819 * For the low-dpi mode, only low-dpi image is loaded. If not available, then
1820 * the high-dpi image is loaded as a fallback.
1822 * @param {Mosaic.Tile.LoadMode} loadMode Loading mode.
1823 * @param {function(boolean)} onImageLoaded Callback when image is loaded.
1824 * The argument is true for success, false for failure.
1826 Mosaic
.Tile
.prototype.load = function(loadMode
, onImageLoaded
) {
1827 // Attaches the image to the tile and finalizes loading process for the
1828 // specified loader.
1829 var finalizeLoader = function(mode
, success
, loader
) {
1830 if (success
&& this.wrapper_
) {
1831 // Show the fade-in animation only when previously there was no image
1832 // attached in this tile.
1833 if (!this.imageLoaded_
&& !this.imagePreloaded_
)
1834 this.wrapper_
.classList
.add('animated');
1836 this.wrapper_
.classList
.remove('animated');
1838 loader
.attachImage(this.wrapper_
, ThumbnailLoader
.FillMode
.OVER_FILL
);
1839 onImageLoaded(success
);
1841 case Mosaic
.Tile
.LoadMode
.LOW_DPI
:
1842 this.imagePreloading_
= false;
1843 this.imagePreloaded_
= true;
1845 case Mosaic
.Tile
.LoadMode
.HIGH_DPI
:
1846 this.imageLoading_
= false;
1847 this.imageLoaded_
= true;
1852 // Always load the low-dpi image first if it is available for the fastest
1854 if (!this.imagePreloading_
&& this.thumbnailPreloader_
) {
1855 this.imagePreloading_
= true;
1856 this.thumbnailPreloader_
.loadDetachedImage(function(success
) {
1857 // Hi-dpi loaded first, ignore this call then.
1858 if (this.imageLoaded_
)
1860 finalizeLoader(Mosaic
.Tile
.LoadMode
.LOW_DPI
,
1862 this.thumbnailPreloader_
);
1866 // Load the high-dpi image only when it is requested, or the low-dpi is not
1868 if (!this.imageLoading_
&&
1869 (loadMode
=== Mosaic
.Tile
.LoadMode
.HIGH_DPI
|| !this.imagePreloading_
)) {
1870 this.imageLoading_
= true;
1871 this.thumbnailLoader_
.loadDetachedImage(function(success
) {
1872 // Cancel preloading, since the hi-dpi image is ready.
1873 if (this.thumbnailPreloader_
)
1874 this.thumbnailPreloader_
.cancel();
1875 finalizeLoader(Mosaic
.Tile
.LoadMode
.HIGH_DPI
,
1877 this.thumbnailLoader_
);
1883 * Unloads an image from the tile.
1885 Mosaic
.Tile
.prototype.unload = function() {
1886 this.thumbnailLoader_
.cancel();
1887 if (this.thumbnailPreloader_
)
1888 this.thumbnailPreloader_
.cancel();
1889 this.imagePreloaded_
= false;
1890 this.imageLoaded_
= false;
1891 this.imagePreloading_
= false;
1892 this.imageLoading_
= false;
1893 this.wrapper_
.innerText
= '';
1897 * Selects/unselects the tile.
1899 * @param {boolean} on True if selected.
1901 Mosaic
.Tile
.prototype.select = function(on
) {
1903 this.setAttribute('selected', true);
1905 this.removeAttribute('selected');
1909 * Positions the tile in the mosaic.
1911 * @param {number} left Left position.
1912 * @param {number} top Top position.
1913 * @param {number} width Width.
1914 * @param {number} height Height.
1916 Mosaic
.Tile
.prototype.layout = function(left
, top
, width
, height
) {
1919 this.width_
= width
;
1920 this.height_
= height
;
1922 this.style
.left
= left
+ 'px';
1923 this.style
.top
= top
+ 'px';
1924 this.style
.width
= width
+ 'px';
1925 this.style
.height
= height
+ 'px';
1927 if (!this.wrapper_
) { // First time, create DOM.
1928 this.container_
.appendChild(this);
1929 var border
= util
.createChild(this, 'img-border');
1930 this.wrapper_
= util
.createChild(border
, 'img-wrapper');
1932 if (this.hasAttribute('selected'))
1933 this.scrollIntoView(false);
1935 if (this.imageLoaded_
) {
1936 this.thumbnailLoader_
.attachImage(this.wrapper_
,
1937 ThumbnailLoader
.FillMode
.OVER_FILL
);
1942 * If the tile is not fully visible scroll the parent to make it fully visible.
1943 * @param {boolean=} opt_animated True, if scroll should be animated,
1946 Mosaic
.Tile
.prototype.scrollIntoView = function(opt_animated
) {
1947 if (this.left_
=== null) // Not laid out.
1951 var tileLeft
= this.left_
- Mosaic
.Layout
.SCROLL_MARGIN
;
1952 if (tileLeft
< this.container_
.scrollLeft
) {
1953 targetPosition
= tileLeft
;
1955 var tileRight
= this.left_
+ this.width_
+ Mosaic
.Layout
.SCROLL_MARGIN
;
1956 var scrollRight
= this.container_
.scrollLeft
+ this.container_
.clientWidth
;
1957 if (tileRight
> scrollRight
)
1958 targetPosition
= tileRight
- this.container_
.clientWidth
;
1961 if (targetPosition
) {
1962 if (opt_animated
=== false)
1963 this.container_
.scrollLeft
= targetPosition
;
1965 this.container_
.animatedScrollTo(targetPosition
);
1970 * @return {Rect} Rectangle occupied by the tile's image,
1971 * relative to the viewport.
1973 Mosaic
.Tile
.prototype.getImageRect = function() {
1974 if (this.left_
=== null) // Not laid out.
1977 var margin
= Mosaic
.Layout
.SPACING
/ 2;
1978 return new Rect(this.left_
- this.container_
.scrollLeft
, this.top_
,
1979 this.width_
, this.height_
).inflate(-margin
, -margin
);
1983 * @return {number} X coordinate of the tile center.
1985 Mosaic
.Tile
.prototype.getCenterX = function() {
1986 return this.left_
+ Math
.round(this.width_
/ 2);