1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * @param {!Element} container Content container.
7 * @param {!ErrorBanner} errorBanner Error banner.
8 * @param {!cr.ui.ArrayDataModel} dataModel Data model.
9 * @param {!cr.ui.ListSelectionModel} selectionModel Selection model.
10 * @param {!VolumeManager} volumeManager Volume manager.
11 * @param {function(Event=)} toggleMode Function to switch to the Slide mode.
16 container
, errorBanner
, dataModel
, selectionModel
, volumeManager
,
18 this.mosaic_
= new Mosaic(assert(container
.ownerDocument
), errorBanner
,
19 dataModel
, selectionModel
, volumeManager
);
20 container
.appendChild(this.mosaic_
);
22 this.toggleMode_
= toggleMode
;
23 this.mosaic_
.addEventListener('dblclick', this.toggleMode_
);
24 this.showingTimeoutID_
= null;
28 * @return {!Mosaic} The mosaic control.
30 MosaicMode
.prototype.getMosaic = function() { return this.mosaic_
; };
33 * @return {string} Mode name.
35 MosaicMode
.prototype.getName = function() { return 'mosaic'; };
38 * @return {string} Mode title.
40 MosaicMode
.prototype.getTitle = function() { return 'GALLERY_MOSAIC'; };
43 * Execute an action (this mode has no busy state).
44 * @param {function()} action Action to execute.
46 MosaicMode
.prototype.executeWhenReady = function(action
) { action(); };
49 * @return {boolean} Always true (no toolbar fading in this mode).
51 MosaicMode
.prototype.hasActiveTool = function() { return true; };
56 * @param {!Event} event Event.
58 MosaicMode
.prototype.onKeyDown = function(event
) {
59 switch (util
.getKeyModifiers(event
) + event
.keyIdentifier
) {
61 if (!document
.activeElement
||
62 document
.activeElement
.localName
!== 'button') {
64 event
.preventDefault();
68 this.mosaic_
.onKeyDown(event
);
72 * Enters the debug mode.
74 MosaicMode
.prototype.debugMe = function() {
75 this.mosaic_
.debugMe();
78 ////////////////////////////////////////////////////////////////////////////////
83 * @param {!Document} document Document.
84 * @param {!ErrorBanner} errorBanner Error banner.
85 * @param {!cr.ui.ArrayDataModel} dataModel Data model.
86 * @param {!cr.ui.ListSelectionModel} selectionModel Selection model.
87 * @param {!VolumeManager} volumeManager Volume manager.
88 * @return {!Element} Mosaic element.
91 * @extends {HTMLDivElement}
92 * @suppress {checkStructDictInheritance}
94 function Mosaic(document
, errorBanner
, dataModel
, selectionModel
,
96 // This is a hack to make closure compiler recognize definitions of fields
97 // with this decorate pattern. When this constructor is called as "new
98 // Mosaic(...)", "this" should be Mosaic. In that case, this calls this
99 // constructor again with setting this as HTMLDivElement. When this condition
100 // is false, this method decorates the "this" object, and returns it.
101 if (this instanceof Mosaic
) {
102 return Mosaic
.call(/** @type {Mosaic} */ (document
.createElement('div')),
103 document
, errorBanner
, dataModel
, selectionModel
, volumeManager
);
106 this.__proto__
= Mosaic
.prototype;
107 this.className
= 'mosaic';
110 * @type {!cr.ui.ArrayDataModel}
113 this.dataModel_
= dataModel
;
116 * @type {!cr.ui.ListSelectionModel}
119 this.selectionModel_
= selectionModel
;
122 * @type {!VolumeManager}
125 this.volumeManager_
= volumeManager
;
128 * @type {!ErrorBanner}
131 this.errorBanner_
= errorBanner
;
134 * @type {Array.<!Mosaic.Tile>}
143 this.loadVisibleTilesSuppressed_
= false;
149 this.loadVisibleTilesScheduled_
= false;
155 this.showingTimeoutID_
= 0;
158 * @type {Mosaic.SelectionController}
161 this.selectionController_
= null;
164 * @type {Mosaic.Layout}
167 this.layoutModel_
= null;
173 this.suppressHovering_
= false;
179 this.layoutTimer_
= 0;
185 this.scrollAnimation_
= 0;
187 // Initialization is completed lazily on the first call to |init|.
193 * Inherits from HTMLDivElement.
195 Mosaic
.prototype.__proto__
= HTMLDivElement
.prototype;
198 * Default layout delay in ms.
202 Mosaic
.LAYOUT_DELAY
= 200;
205 * Smooth scroll animation duration when scrolling using keyboard or
206 * clicking on a partly visible tile. In ms.
210 Mosaic
.ANIMATED_SCROLL_DURATION
= 500;
213 * Initializes the mosaic element.
215 Mosaic
.prototype.init = function() {
217 return; // Already initialized, nothing to do.
219 this.layoutModel_
= new Mosaic
.Layout();
222 this.selectionController_
=
223 new Mosaic
.SelectionController(this.selectionModel_
, this.layoutModel_
);
226 for (var i
= 0; i
!== this.dataModel_
.length
; i
++) {
228 this.volumeManager_
.getLocationInfo(this.dataModel_
.item(i
).getEntry());
232 assertInstanceof(this.dataModel_
.item(i
), Gallery
.Item
),
236 this.selectionModel_
.selectedIndexes
.forEach(function(index
) {
237 this.tiles_
[index
].select(true);
240 this.initTiles_(this.tiles_
);
242 // The listeners might be called while some tiles are still loading.
243 this.initListeners_();
247 * @return {boolean} Whether mosaic is initialized.
249 Mosaic
.prototype.isInitialized = function() {
250 return !!this.tiles_
;
254 * Starts listening to events.
256 * We keep listening to events even when the mosaic is hidden in order to
257 * keep the layout up to date.
261 Mosaic
.prototype.initListeners_ = function() {
262 this.ownerDocument
.defaultView
.addEventListener(
263 'resize', this.onResize_
.bind(this));
265 var mouseEventBound
= this.onMouseEvent_
.bind(this);
266 this.addEventListener('mousemove', mouseEventBound
);
267 this.addEventListener('mousedown', mouseEventBound
);
268 this.addEventListener('mouseup', mouseEventBound
);
269 this.addEventListener('scroll', this.onScroll_
.bind(this));
271 this.selectionModel_
.addEventListener('change', this.onSelection_
.bind(this));
272 this.selectionModel_
.addEventListener('leadIndexChange',
273 this.onLeadChange_
.bind(this));
275 this.dataModel_
.addEventListener('splice', this.onSplice_
.bind(this));
276 this.dataModel_
.addEventListener('content', this.onContentChange_
.bind(this));
280 * Smoothly scrolls the container to the specified position using
281 * f(x) = sqrt(x) speed function normalized to animation duration.
282 * @param {number} targetPosition Horizontal scroll position in pixels.
284 Mosaic
.prototype.animatedScrollTo = function(targetPosition
) {
285 if (this.scrollAnimation_
) {
286 webkitCancelAnimationFrame(this.scrollAnimation_
);
287 this.scrollAnimation_
= 0;
290 // Mouse move events are fired without touching the mouse because of scrolling
291 // the container. Therefore, these events have to be suppressed.
292 this.suppressHovering_
= true;
294 // Calculates integral area from t1 to t2 of f(x) = sqrt(x) dx.
295 var integral = function(t1
, t2
) {
296 return 2.0 / 3.0 * Math
.pow(t2
, 3.0 / 2.0) -
297 2.0 / 3.0 * Math
.pow(t1
, 3.0 / 2.0);
300 var delta
= targetPosition
- this.scrollLeft
;
301 var factor
= delta
/ integral(0, Mosaic
.ANIMATED_SCROLL_DURATION
);
302 var startTime
= Date
.now();
303 var lastPosition
= 0;
304 var scrollOffset
= this.scrollLeft
;
306 var animationFrame = function() {
307 var position
= Date
.now() - startTime
;
309 integral(Math
.max(0, Mosaic
.ANIMATED_SCROLL_DURATION
- position
),
310 Math
.max(0, Mosaic
.ANIMATED_SCROLL_DURATION
- lastPosition
));
311 scrollOffset
+= step
;
313 var oldScrollLeft
= this.scrollLeft
;
314 var newScrollLeft
= Math
.round(scrollOffset
);
316 if (oldScrollLeft
!== newScrollLeft
)
317 this.scrollLeft
= newScrollLeft
;
319 if (step
=== 0 || this.scrollLeft
!== newScrollLeft
) {
320 this.scrollAnimation_
= null;
321 // Release the hovering lock after a safe delay to avoid hovering
322 // a tile because of altering |this.scrollLeft|.
323 setTimeout(function() {
324 if (!this.scrollAnimation_
)
325 this.suppressHovering_
= false;
328 // Continue the animation.
329 this.scrollAnimation_
= requestAnimationFrame(animationFrame
);
332 lastPosition
= position
;
335 // Start the animation.
336 this.scrollAnimation_
= requestAnimationFrame(animationFrame
);
340 * @return {Mosaic.Tile} Selected tile or undefined if no selection.
342 Mosaic
.prototype.getSelectedTile = function() {
343 return this.tiles_
&& this.tiles_
[this.selectionModel_
.selectedIndex
];
347 * @param {number} index Tile index.
348 * @return {ImageRect} Tile's image rectangle.
350 Mosaic
.prototype.getTileRect = function(index
) {
351 var tile
= this.tiles_
[index
];
352 return tile
&& tile
.getImageRect();
356 * Scroll the given tile into the viewport.
357 * @param {number} index Tile index.
359 Mosaic
.prototype.scrollIntoViewByIndex = function(index
) {
360 var tile
= this.tiles_
[index
];
361 if (tile
) tile
.scrollIntoView();
365 * Initializes multiple tiles.
367 * @param {!Array.<!Mosaic.Tile>} tiles Array of tiles.
370 Mosaic
.prototype.initTiles_ = function(tiles
) {
371 for (var i
= 0; i
< tiles
.length
; i
++) {
379 Mosaic
.prototype.reload = function() {
380 this.layoutModel_
.reset_();
381 this.tiles_
.forEach(function(t
) { t
.markUnloaded(); });
382 this.initTiles_(this.tiles_
);
386 * Layouts the tiles in the order of their indices.
388 * Starts where it last stopped (at #0 the first time).
389 * Stops when all tiles are processed or when the next tile is still loading.
391 Mosaic
.prototype.layout = function() {
392 if (this.layoutTimer_
) {
393 clearTimeout(this.layoutTimer_
);
394 this.layoutTimer_
= 0;
397 var index
= this.layoutModel_
.getTileCount();
398 if (index
=== this.tiles_
.length
)
399 break; // All tiles done.
400 var tile
= this.tiles_
[index
];
401 if (!tile
.isInitialized())
402 break; // Next layout will try to restart from here.
403 this.layoutModel_
.add(tile
, index
+ 1 === this.tiles_
.length
);
405 this.loadVisibleTiles_();
409 * Schedules the layout.
411 * @param {number=} opt_delay Delay in ms.
413 Mosaic
.prototype.scheduleLayout = function(opt_delay
) {
414 if (!this.layoutTimer_
) {
415 this.layoutTimer_
= setTimeout(function() {
416 this.layoutTimer_
= 0;
418 }.bind(this), opt_delay
|| 0);
427 Mosaic
.prototype.onResize_ = function() {
428 this.layoutModel_
.setViewportSize(this.clientWidth
, this.clientHeight
-
429 (Mosaic
.Layout
.PADDING_TOP
+ Mosaic
.Layout
.PADDING_BOTTOM
));
430 this.scheduleLayout();
434 * Mouse event handler.
436 * @param {!Event} event Event.
439 Mosaic
.prototype.onMouseEvent_ = function(event
) {
440 // Navigating with mouse, enable hover state.
441 if (!this.suppressHovering_
)
442 this.classList
.add('hover-visible');
444 if (event
.type
=== 'mousemove')
448 for (var target
= event
.target
;
449 target
&& (target
!== this);
450 target
= target
.parentNode
) {
451 if (target
.classList
.contains('mosaic-tile')) {
452 index
= this.dataModel_
.indexOf(target
.getItem());
456 this.selectionController_
.handlePointerDownUp(event
, index
);
463 Mosaic
.prototype.onScroll_ = function() {
464 requestAnimationFrame(function() {
465 this.loadVisibleTiles_();
470 * Selection change handler.
472 * @param {!Event} event Event.
475 Mosaic
.prototype.onSelection_ = function(event
) {
476 for (var i
= 0; i
!== event
.changes
.length
; i
++) {
477 var change
= event
.changes
[i
];
478 var tile
= this.tiles_
[change
.index
];
479 if (tile
) tile
.select(change
.selected
);
484 * Leads item change handler.
486 * @param {!Event} event Event.
489 Mosaic
.prototype.onLeadChange_ = function(event
) {
490 var index
= event
.newValue
;
492 var tile
= this.tiles_
[index
];
493 if (tile
) tile
.scrollIntoView();
498 * Splice event handler.
500 * @param {!Event} event Event.
503 Mosaic
.prototype.onSplice_ = function(event
) {
504 var index
= event
.index
;
505 this.layoutModel_
.invalidateFromTile_(index
);
507 if (event
.removed
.length
) {
508 for (var t
= 0; t
!== event
.removed
.length
; t
++) {
509 // If the layout for the tile has not done yet, the parent is null.
510 // And the layout will not be done after onSplice_ because it is removed
512 if (this.tiles_
[index
+ t
].parentNode
)
513 this.removeChild(this.tiles_
[index
+ t
]);
516 this.tiles_
.splice(index
, event
.removed
.length
);
518 // No items left, show the banner.
519 if (this.getItemCount_() === 0)
520 this.errorBanner_
.show('GALLERY_NO_IMAGES');
522 this.scheduleLayout(Mosaic
.LAYOUT_DELAY
);
525 if (event
.added
.length
) {
527 for (var t
= 0; t
!== event
.added
.length
; t
++)
528 newTiles
.push(new Mosaic
.Tile(this,
529 assertInstanceof(this.dataModel_
.item(index
+ t
), Gallery
.Item
)));
531 this.tiles_
.splice
.apply(this.tiles_
, [index
, 0].concat(newTiles
));
532 this.initTiles_(newTiles
);
533 this.scheduleLayout(Mosaic
.LAYOUT_DELAY
);
536 if (this.tiles_
.length
!== this.dataModel_
.length
)
537 console
.error('Mosaic is out of sync');
541 * Content change handler.
543 * @param {!Event} event Event.
546 Mosaic
.prototype.onContentChange_ = function(event
) {
550 if (!event
.thumbnailChanged
)
551 return; // Thumbnail unchanged, nothing to do.
553 var index
= this.dataModel_
.indexOf(event
.item
);
554 if (index
!== this.selectionModel_
.selectedIndex
)
555 console
.error('Content changed for unselected item');
557 this.layoutModel_
.invalidateFromTile_(index
);
558 this.tiles_
[index
].init();
559 this.tiles_
[index
].unload();
560 this.tiles_
[index
].load(
561 Mosaic
.Tile
.LoadMode
.HIGH_DPI
,
562 this.scheduleLayout
.bind(this, Mosaic
.LAYOUT_DELAY
));
566 * Keydown event handler.
568 * @param {!Event} event Event.
569 * @return {boolean} True if the event has been consumed.
571 Mosaic
.prototype.onKeyDown = function(event
) {
572 this.selectionController_
.handleKeyDown(event
);
573 if (event
.defaultPrevented
) // Navigating with keyboard, hide hover state.
574 this.classList
.remove('hover-visible');
575 return event
.defaultPrevented
;
579 * @return {boolean} True if the mosaic zoom effect can be applied. It is
580 * too slow if there are to many images.
581 * TODO(kaznacheev): Consider unloading the images that are out of the viewport.
583 Mosaic
.prototype.canZoom = function() {
584 return this.tiles_
.length
< 100;
590 Mosaic
.prototype.show = function() {
591 // If the items are empty, just show the error message.
592 if (this.getItemCount_() === 0)
593 this.errorBanner_
.show('GALLERY_NO_IMAGES');
595 var duration
= ImageView
.MODE_TRANSITION_DURATION
;
596 if (this.canZoom()) {
597 // Fade in in parallel with the zoom effect.
598 this.setAttribute('visible', 'zooming');
600 // Mosaic is not animating but the large image is. Fade in the mosaic
601 // shortly before the large image animation is done.
604 this.showingTimeoutID_
= setTimeout(function() {
605 this.showingTimeoutID_
= 0;
606 // Make the selection visible.
607 // If the mosaic is not animated it will start fading in now.
608 this.setAttribute('visible', 'normal');
609 this.loadVisibleTiles_();
610 }.bind(this), duration
);
616 Mosaic
.prototype.hide = function() {
617 this.errorBanner_
.clear();
619 if (this.showingTimeoutID_
!== 0) {
620 clearTimeout(this.showingTimeoutID_
);
621 this.showingTimeoutID_
= 0;
623 this.removeAttribute('visible');
627 * Checks if the mosaic view is visible.
628 * @return {boolean} True if visible, false otherwise.
631 Mosaic
.prototype.isVisible_ = function() {
632 return this.hasAttribute('visible');
636 * Loads visible tiles. Ignores consecutive calls. Does not reload already
640 Mosaic
.prototype.loadVisibleTiles_ = function() {
641 if (this.loadVisibleTilesSuppressed_
) {
642 this.loadVisibleTilesScheduled_
= true;
646 this.loadVisibleTilesSuppressed_
= true;
647 this.loadVisibleTilesScheduled_
= false;
648 setTimeout(function() {
649 this.loadVisibleTilesSuppressed_
= false;
650 if (this.loadVisibleTilesScheduled_
)
651 this.loadVisibleTiles_();
654 // Tiles only in the viewport (visible).
655 var visibleRect
= new ImageRect(
656 0, 0, this.clientWidth
, this.clientHeight
);
658 // Tiles in the viewport and also some distance on the left and right.
659 var renderableRect
= new ImageRect(
662 3 * this.clientWidth
,
665 // Unload tiles out of scope.
666 for (var index
= 0; index
< this.tiles_
.length
; index
++) {
667 var tile
= this.tiles_
[index
];
668 var imageRect
= tile
.getImageRect();
669 // Unload a thumbnail.
670 if (imageRect
&& !imageRect
.intersects(renderableRect
))
674 // Load the visible tiles first.
675 var allVisibleLoaded
= true;
676 // Show high-dpi only when the mosaic view is visible.
677 var loadMode
= this.isVisible_() ? Mosaic
.Tile
.LoadMode
.HIGH_DPI
:
678 Mosaic
.Tile
.LoadMode
.LOW_DPI
;
679 for (var index
= 0; index
< this.tiles_
.length
; index
++) {
680 var tile
= this.tiles_
[index
];
681 var imageRect
= tile
.getImageRect();
683 if (!tile
.isLoading(loadMode
) && !tile
.isLoaded(loadMode
) && imageRect
&&
684 imageRect
.intersects(visibleRect
)) {
685 tile
.load(loadMode
, function() {});
686 allVisibleLoaded
= false;
690 // Load also another, nearby, if the visible has been already loaded.
691 if (allVisibleLoaded
) {
692 for (var index
= 0; index
< this.tiles_
.length
; index
++) {
693 var tile
= this.tiles_
[index
];
694 var imageRect
= tile
.getImageRect();
696 if (!tile
.isLoading() && !tile
.isLoaded() && imageRect
&&
697 imageRect
.intersects(renderableRect
)) {
698 tile
.load(Mosaic
.Tile
.LoadMode
.LOW_DPI
, function() {});
705 * Applies reset the zoom transform.
707 * @param {ImageRect} tileRect Tile rectangle. Reset the transform if null.
708 * @param {ImageRect} imageRect Large image rectangle. Reset the transform if
710 * @param {boolean=} opt_instant True of the transition should be instant.
712 Mosaic
.prototype.transform = function(tileRect
, imageRect
, opt_instant
) {
714 this.style
.webkitTransitionDuration
= '0';
716 this.style
.webkitTransitionDuration
=
717 ImageView
.MODE_TRANSITION_DURATION
+ 'ms';
720 if (this.canZoom() && tileRect
&& imageRect
) {
721 var scaleX
= imageRect
.width
/ tileRect
.width
;
722 var scaleY
= imageRect
.height
/ tileRect
.height
;
723 var shiftX
= (imageRect
.left
+ imageRect
.width
/ 2) -
724 (tileRect
.left
+ tileRect
.width
/ 2);
725 var shiftY
= (imageRect
.top
+ imageRect
.height
/ 2) -
726 (tileRect
.top
+ tileRect
.height
/ 2);
727 this.style
.webkitTransform
=
728 'translate(' + shiftX
* scaleX
+ 'px, ' + shiftY
* scaleY
+ 'px)' +
729 'scaleX(' + scaleX
+ ') scaleY(' + scaleY
+ ')';
731 this.style
.webkitTransform
= '';
736 * @return {number} Item count
739 Mosaic
.prototype.getItemCount_ = function() {
740 return this.dataModel_
.length
;
744 * Enters the debug me.
746 Mosaic
.prototype.debugMe = function() {
747 this.classList
.add('debug-me');
750 ////////////////////////////////////////////////////////////////////////////////
753 * Creates a selection controller that is to be used with grid.
754 * @param {!cr.ui.ListSelectionModel} selectionModel The selection model to
756 * @param {!Mosaic.Layout} layoutModel The layout model to use.
759 * @extends {cr.ui.ListSelectionController}
760 * @suppress {checkStructDictInheritance}
762 Mosaic
.SelectionController = function(selectionModel
, layoutModel
) {
763 cr
.ui
.ListSelectionController
.call(this, selectionModel
);
764 this.layoutModel_
= layoutModel
;
768 * Extends cr.ui.ListSelectionController.
770 Mosaic
.SelectionController
.prototype.__proto__
=
771 cr
.ui
.ListSelectionController
.prototype;
774 Mosaic
.SelectionController
.prototype.getLastIndex = function() {
775 return this.layoutModel_
.getLaidOutTileCount() - 1;
779 Mosaic
.SelectionController
.prototype.getIndexBefore = function(index
) {
780 return this.layoutModel_
.getHorizontalAdjacentIndex(index
, -1);
784 Mosaic
.SelectionController
.prototype.getIndexAfter = function(index
) {
785 return this.layoutModel_
.getHorizontalAdjacentIndex(index
, 1);
789 Mosaic
.SelectionController
.prototype.getIndexAbove = function(index
) {
790 return this.layoutModel_
.getVerticalAdjacentIndex(index
, -1);
794 Mosaic
.SelectionController
.prototype.getIndexBelow = function(index
) {
795 return this.layoutModel_
.getVerticalAdjacentIndex(index
, 1);
798 ////////////////////////////////////////////////////////////////////////////////
803 * @param {string=} opt_mode Layout mode.
804 * @param {Mosaic.Density=} opt_maxDensity Layout density.
808 Mosaic
.Layout = function(opt_mode
, opt_maxDensity
) {
809 this.mode_
= opt_mode
|| Mosaic
.Layout
.Mode
.TENTATIVE
;
810 this.maxDensity_
= opt_maxDensity
|| Mosaic
.Density
.createHighest();
813 * @type {!Array.<!Mosaic.Column>}
819 * @type {Mosaic.Column}
822 this.newColumn_
= null;
828 this.viewportWidth_
= 0;
834 this.viewportHeight_
= 0;
837 * @type {Mosaic.Density}
840 this.density_
= null;
846 * Blank space at the top of the mosaic element. We do not do that in CSS
847 * to make transition effects easier.
851 Mosaic
.Layout
.PADDING_TOP
= 50;
854 * Blank space at the bottom of the mosaic element.
858 Mosaic
.Layout
.PADDING_BOTTOM
= 50;
861 * Horizontal and vertical spacing between images. Should be kept in sync
862 * with the style of .mosaic-item in gallery.css (= 2 * ( 4 + 1))
866 Mosaic
.Layout
.SPACING
= 10;
869 * Margin for scrolling using keyboard. Distance between a selected tile
874 Mosaic
.Layout
.SCROLL_MARGIN
= 30;
880 Mosaic
.Layout
.Mode
= {
881 // Commit to DOM immediately.
883 // Do not commit layout to DOM until it is complete or the viewport
885 TENTATIVE
: 'tentative',
886 // Never commit layout to DOM.
895 Mosaic
.Layout
.prototype.reset_ = function() {
897 this.newColumn_
= null;
898 this.density_
= Mosaic
.Density
.createLowest();
899 if (this.mode_
!== Mosaic
.Layout
.Mode
.DRY_RUN
) // DRY_RUN is sticky.
900 this.mode_
= Mosaic
.Layout
.Mode
.TENTATIVE
;
904 * @param {number} width Viewport width.
905 * @param {number} height Viewport height.
907 Mosaic
.Layout
.prototype.setViewportSize = function(width
, height
) {
908 this.viewportWidth_
= width
;
909 this.viewportHeight_
= height
;
914 * @return {number} Total width of the layout.
916 Mosaic
.Layout
.prototype.getWidth = function() {
917 var lastColumn
= this.getLastColumn_();
918 return lastColumn
? lastColumn
.getRight() : 0;
922 * @return {number} Total height of the layout.
924 Mosaic
.Layout
.prototype.getHeight = function() {
925 var firstColumn
= this.columns_
[0];
926 return firstColumn
? firstColumn
.getHeight() : 0;
930 * @return {!Array.<!Mosaic.Tile>} All tiles in the layout.
932 Mosaic
.Layout
.prototype.getTiles = function() {
933 return Array
.prototype.concat
.apply([],
934 this.columns_
.map(function(c
) { return c
.getTiles(); }));
938 * @return {number} Total number of tiles added to the layout.
940 Mosaic
.Layout
.prototype.getTileCount = function() {
941 return this.getLaidOutTileCount() +
942 (this.newColumn_
? this.newColumn_
.getTileCount() : 0);
946 * @return {Mosaic.Column} The last column or null for empty layout.
949 Mosaic
.Layout
.prototype.getLastColumn_ = function() {
950 return this.columns_
.length
? this.columns_
[this.columns_
.length
- 1] : null;
954 * @return {number} Total number of tiles in completed columns.
956 Mosaic
.Layout
.prototype.getLaidOutTileCount = function() {
957 var lastColumn
= this.getLastColumn_();
958 return lastColumn
? lastColumn
.getNextTileIndex() : 0;
962 * Adds a tile to the layout.
964 * @param {!Mosaic.Tile} tile The tile to be added.
965 * @param {boolean} isLast True if this tile is the last.
967 Mosaic
.Layout
.prototype.add = function(tile
, isLast
) {
968 var layoutQueue
= [tile
];
970 // There are two levels of backtracking in the layout algorithm.
971 // |Mosaic.Layout.density_| tracks the state of the 'global' backtracking
972 // which aims to use as much of the viewport space as possible.
973 // It starts with the lowest density and increases it until the layout
974 // fits into the viewport. If it does not fit even at the highest density,
975 // the layout continues with the highest density.
977 // |Mosaic.Column.density_| tracks the state of the 'local' backtracking
978 // which aims to avoid producing unnaturally looking columns.
979 // It starts with the current global density and decreases it until the column
982 while (layoutQueue
.length
) {
983 if (!this.newColumn_
) {
984 var lastColumn
= this.getLastColumn_();
985 this.newColumn_
= new Mosaic
.Column(
986 this.columns_
.length
,
987 lastColumn
? lastColumn
.getNextRowIndex() : 0,
988 lastColumn
? lastColumn
.getNextTileIndex() : 0,
989 lastColumn
? lastColumn
.getRight() : 0,
990 this.viewportHeight_
,
991 this.density_
.clone());
994 this.newColumn_
.add(layoutQueue
.shift());
996 var isFinalColumn
= isLast
&& !layoutQueue
.length
;
998 if (!this.newColumn_
.prepareLayout(isFinalColumn
))
999 continue; // Column is incomplete.
1001 if (this.newColumn_
.isSuboptimal()) {
1002 layoutQueue
= this.newColumn_
.getTiles().concat(layoutQueue
);
1003 this.newColumn_
.retryWithLowerDensity();
1007 this.columns_
.push(this.newColumn_
);
1008 this.newColumn_
= null;
1010 if (this.mode_
=== Mosaic
.Layout
.Mode
.FINAL
&& isFinalColumn
) {
1015 if (this.getWidth() > this.viewportWidth_
) {
1016 // Viewport completely filled.
1017 if (this.density_
.equals(this.maxDensity_
)) {
1018 // Max density reached, commit if tentative, just continue if dry run.
1019 if (this.mode_
=== Mosaic
.Layout
.Mode
.TENTATIVE
)
1024 // Rollback the entire layout, retry with higher density.
1025 layoutQueue
= this.getTiles().concat(layoutQueue
);
1027 this.density_
.increase();
1031 if (isFinalColumn
&& this.mode_
=== Mosaic
.Layout
.Mode
.TENTATIVE
) {
1032 // The complete tentative layout fits into the viewport.
1033 var stretched
= this.findHorizontalLayout_();
1035 this.columns_
= stretched
.columns_
;
1036 // Center the layout in the viewport and commit.
1037 this.commit_((this.viewportWidth_
- this.getWidth()) / 2,
1038 (this.viewportHeight_
- this.getHeight()) / 2);
1044 * Commits the tentative layout.
1046 * @param {number=} opt_offsetX Horizontal offset.
1047 * @param {number=} opt_offsetY Vertical offset.
1050 Mosaic
.Layout
.prototype.commit_ = function(opt_offsetX
, opt_offsetY
) {
1051 for (var i
= 0; i
!== this.columns_
.length
; i
++) {
1052 this.columns_
[i
].layout(opt_offsetX
, opt_offsetY
);
1054 this.mode_
= Mosaic
.Layout
.Mode
.FINAL
;
1058 * Finds the most horizontally stretched layout built from the same tiles.
1060 * The main layout algorithm fills the entire available viewport height.
1061 * If there is too few tiles this results in a layout that is unnaturally
1062 * stretched in the vertical direction.
1064 * This method tries a number of smaller heights and returns the most
1065 * horizontally stretched layout that still fits into the viewport.
1067 * @return {Mosaic.Layout} A horizontally stretched layout.
1070 Mosaic
.Layout
.prototype.findHorizontalLayout_ = function() {
1071 // If the layout aspect ratio is not dramatically different from
1072 // the viewport aspect ratio then there is no need to optimize.
1073 if (this.getWidth() / this.getHeight() >
1074 this.viewportWidth_
/ this.viewportHeight_
* 0.9)
1077 var tiles
= this.getTiles();
1078 if (tiles
.length
=== 1)
1079 return null; // Single tile layout is always the same.
1081 var tileHeights
= tiles
.map(function(t
) { return t
.getMaxContentHeight(); });
1082 var minTileHeight
= Math
.min
.apply(null, tileHeights
);
1084 for (var h
= minTileHeight
; h
< this.viewportHeight_
; h
+= minTileHeight
) {
1085 var layout
= new Mosaic
.Layout(
1086 Mosaic
.Layout
.Mode
.DRY_RUN
, this.density_
.clone());
1087 layout
.setViewportSize(this.viewportWidth_
, h
);
1088 for (var t
= 0; t
!== tiles
.length
; t
++)
1089 layout
.add(tiles
[t
], t
+ 1 === tiles
.length
);
1091 if (layout
.getWidth() <= this.viewportWidth_
)
1099 * Invalidates the layout after the given tile was modified (added, deleted or
1100 * changed dimensions).
1102 * @param {number} index Tile index.
1105 Mosaic
.Layout
.prototype.invalidateFromTile_ = function(index
) {
1106 var columnIndex
= this.getColumnIndexByTile_(index
);
1107 if (columnIndex
< 0)
1108 return; // Index not in the layout, probably already invalidated.
1110 if (this.columns_
[columnIndex
].getLeft() >= this.viewportWidth_
) {
1111 // The columns to the right cover the entire viewport width, so there is no
1112 // chance that the modified layout would fit into the viewport.
1113 // No point in restarting the entire layout, keep the columns to the right.
1114 console
.assert(this.mode_
=== Mosaic
.Layout
.Mode
.FINAL
,
1115 'Expected FINAL layout mode');
1116 this.columns_
= this.columns_
.slice(0, columnIndex
);
1117 this.newColumn_
= null;
1119 // There is a chance that the modified layout would fit into the viewport.
1121 this.mode_
= Mosaic
.Layout
.Mode
.TENTATIVE
;
1126 * Gets the index of the tile to the left or to the right from the given tile.
1128 * @param {number} index Tile index.
1129 * @param {number} direction -1 for left, 1 for right.
1130 * @return {number} Adjacent tile index.
1132 Mosaic
.Layout
.prototype.getHorizontalAdjacentIndex = function(
1134 var column
= this.getColumnIndexByTile_(index
);
1136 console
.error('Cannot find column for tile #' + index
);
1140 var row
= this.columns_
[column
].getRowByTileIndex(index
);
1142 console
.error('Cannot find row for tile #' + index
);
1146 var sameRowNeighbourIndex
= index
+ direction
;
1147 if (row
.hasTile(sameRowNeighbourIndex
))
1148 return sameRowNeighbourIndex
;
1150 var adjacentColumn
= column
+ direction
;
1151 if (adjacentColumn
< 0 || adjacentColumn
=== this.columns_
.length
)
1154 return this.columns_
[adjacentColumn
].
1155 getEdgeTileIndex_(row
.getCenterY(), -direction
);
1159 * Gets the index of the tile to the top or to the bottom from the given tile.
1161 * @param {number} index Tile index.
1162 * @param {number} direction -1 for above, 1 for below.
1163 * @return {number} Adjacent tile index.
1165 Mosaic
.Layout
.prototype.getVerticalAdjacentIndex = function(
1167 var column
= this.getColumnIndexByTile_(index
);
1169 console
.error('Cannot find column for tile #' + index
);
1173 var row
= this.columns_
[column
].getRowByTileIndex(index
);
1175 console
.error('Cannot find row for tile #' + index
);
1179 // Find the first item in the next row, or the last item in the previous row.
1180 var adjacentRowNeighbourIndex
=
1181 row
.getEdgeTileIndex_(direction
) + direction
;
1183 if (adjacentRowNeighbourIndex
< 0 ||
1184 adjacentRowNeighbourIndex
> this.getTileCount() - 1)
1187 if (!this.columns_
[column
].hasTile(adjacentRowNeighbourIndex
)) {
1188 // It is not in the current column, so return it.
1189 return adjacentRowNeighbourIndex
;
1191 // It is in the current column, so we have to find optically the closest
1192 // tile in the adjacent row.
1193 var adjacentRow
= this.columns_
[column
].getRowByTileIndex(
1194 adjacentRowNeighbourIndex
);
1195 var previousTileCenterX
= row
.getTileByIndex(index
).getCenterX();
1197 // Find the closest one.
1198 var closestIndex
= -1;
1199 var closestDistance
;
1200 var adjacentRowTiles
= adjacentRow
.getTiles();
1201 for (var t
= 0; t
!== adjacentRowTiles
.length
; t
++) {
1203 Math
.abs(adjacentRowTiles
[t
].getCenterX() - previousTileCenterX
);
1204 if (closestIndex
=== -1 || distance
< closestDistance
) {
1205 closestIndex
= adjacentRow
.getEdgeTileIndex_(-1) + t
;
1206 closestDistance
= distance
;
1209 return closestIndex
;
1214 * @param {number} index Tile index.
1215 * @return {number} Index of the column containing the given tile.
1218 Mosaic
.Layout
.prototype.getColumnIndexByTile_ = function(index
) {
1219 for (var c
= 0; c
!== this.columns_
.length
; c
++) {
1220 if (this.columns_
[c
].hasTile(index
))
1227 * Scales the given array of size values to satisfy 3 conditions:
1228 * 1. The new sizes must be integer.
1229 * 2. The new sizes must sum up to the given |total| value.
1230 * 3. The relative proportions of the sizes should be as close to the original
1233 * @param {!Array.<number>} sizes Array of sizes.
1234 * @param {number} newTotal New total size.
1236 Mosaic
.Layout
.rescaleSizesToNewTotal = function(sizes
, newTotal
) {
1239 var partialTotals
= [0];
1240 for (var i
= 0; i
!== sizes
.length
; i
++) {
1242 partialTotals
.push(total
);
1245 var scale
= newTotal
/ total
;
1247 for (i
= 0; i
!== sizes
.length
; i
++) {
1248 sizes
[i
] = Math
.round(partialTotals
[i
+ 1] * scale
) -
1249 Math
.round(partialTotals
[i
] * scale
);
1253 ////////////////////////////////////////////////////////////////////////////////
1256 * Representation of the layout density.
1258 * @param {number} horizontal Horizontal density, number tiles per row.
1259 * @param {number} vertical Vertical density, frequency of rows forced to
1260 * contain a single tile.
1264 Mosaic
.Density = function(horizontal
, vertical
) {
1265 this.horizontal
= horizontal
;
1266 this.vertical
= vertical
;
1270 * Minimal horizontal density (tiles per row).
1274 Mosaic
.Density
.MIN_HORIZONTAL
= 1;
1277 * Minimal horizontal density (tiles per row).
1281 Mosaic
.Density
.MAX_HORIZONTAL
= 3;
1284 * Minimal vertical density: force 1 out of 2 rows to containt a single tile.
1288 Mosaic
.Density
.MIN_VERTICAL
= 2;
1291 * Maximal vertical density: force 1 out of 3 rows to containt a single tile.
1295 Mosaic
.Density
.MAX_VERTICAL
= 3;
1298 * @return {!Mosaic.Density} Lowest density.
1300 Mosaic
.Density
.createLowest = function() {
1301 return new Mosaic
.Density(
1302 Mosaic
.Density
.MIN_HORIZONTAL
,
1303 Mosaic
.Density
.MIN_VERTICAL
/* ignored when horizontal is at min */);
1307 * @return {!Mosaic.Density} Highest density.
1309 Mosaic
.Density
.createHighest = function() {
1310 return new Mosaic
.Density(
1311 Mosaic
.Density
.MAX_HORIZONTAL
,
1312 Mosaic
.Density
.MAX_VERTICAL
);
1316 * @return {!Mosaic.Density} A clone of this density object.
1318 Mosaic
.Density
.prototype.clone = function() {
1319 return new Mosaic
.Density(this.horizontal
, this.vertical
);
1323 * @param {!Mosaic.Density} that The other object.
1324 * @return {boolean} True if equal.
1326 Mosaic
.Density
.prototype.equals = function(that
) {
1327 return this.horizontal
=== that
.horizontal
&&
1328 this.vertical
=== that
.vertical
;
1332 * Increases the density to the next level.
1334 Mosaic
.Density
.prototype.increase = function() {
1335 if (this.horizontal
=== Mosaic
.Density
.MIN_HORIZONTAL
||
1336 this.vertical
=== Mosaic
.Density
.MAX_VERTICAL
) {
1337 console
.assert(this.horizontal
< Mosaic
.Density
.MAX_HORIZONTAL
);
1339 this.vertical
= Mosaic
.Density
.MIN_VERTICAL
;
1346 * Decreases horizontal density.
1348 Mosaic
.Density
.prototype.decreaseHorizontal = function() {
1349 console
.assert(this.horizontal
> Mosaic
.Density
.MIN_HORIZONTAL
);
1354 * @param {number} tileCount Number of tiles in the row.
1355 * @param {number} rowIndex Global row index.
1356 * @return {boolean} True if the row is complete.
1358 Mosaic
.Density
.prototype.isRowComplete = function(tileCount
, rowIndex
) {
1359 return (tileCount
=== this.horizontal
) || (rowIndex
% this.vertical
) === 0;
1362 ////////////////////////////////////////////////////////////////////////////////
1365 * A column in a mosaic layout. Contains rows.
1367 * @param {number} index Column index.
1368 * @param {number} firstRowIndex Global row index.
1369 * @param {number} firstTileIndex Index of the first tile in the column.
1370 * @param {number} left Left edge coordinate.
1371 * @param {number} maxHeight Maximum height.
1372 * @param {!Mosaic.Density} density Layout density.
1376 Mosaic
.Column = function(index
, firstRowIndex
, firstTileIndex
, left
, maxHeight
,
1378 this.index_
= index
;
1379 this.firstRowIndex_
= firstRowIndex
;
1380 this.firstTileIndex_
= firstTileIndex
;
1382 this.maxHeight_
= maxHeight
;
1383 this.density_
= density
;
1392 * @type {!Array.<!Mosaic.Tile>}
1398 * @type {!Array.<!Mosaic.Row>}
1404 * @type {Mosaic.Row}
1407 this.newRow_
= null;
1410 * @type {!Array.<number>}
1413 this.rowHeights_
= [];
1425 * Resets the layout.
1428 Mosaic
.Column
.prototype.reset_ = function() {
1431 this.newRow_
= null;
1435 * @return {number} Number of tiles in the column.
1437 Mosaic
.Column
.prototype.getTileCount = function() { return this.tiles_
.length
};
1440 * @return {number} Index of the last tile + 1.
1442 Mosaic
.Column
.prototype.getNextTileIndex = function() {
1443 return this.firstTileIndex_
+ this.getTileCount();
1447 * @return {number} Global index of the last row + 1.
1449 Mosaic
.Column
.prototype.getNextRowIndex = function() {
1450 return this.firstRowIndex_
+ this.rows_
.length
;
1454 * @return {!Array.<!Mosaic.Tile>} Array of tiles in the column.
1456 Mosaic
.Column
.prototype.getTiles = function() { return this.tiles_
};
1459 * @param {number} index Tile index.
1460 * @return {boolean} True if this column contains the tile with the given index.
1462 Mosaic
.Column
.prototype.hasTile = function(index
) {
1463 return this.firstTileIndex_
<= index
&&
1464 index
< (this.firstTileIndex_
+ this.getTileCount());
1468 * @param {number} y Y coordinate.
1469 * @param {number} direction -1 for left, 1 for right.
1470 * @return {number} Index of the tile lying on the edge of the column at the
1471 * given y coordinate.
1474 Mosaic
.Column
.prototype.getEdgeTileIndex_ = function(y
, direction
) {
1475 for (var r
= 0; r
< this.rows_
.length
; r
++) {
1476 if (this.rows_
[r
].coversY(y
))
1477 return this.rows_
[r
].getEdgeTileIndex_(direction
);
1483 * @param {number} index Tile index.
1484 * @return {Mosaic.Row} The row containing the tile with a given index.
1486 Mosaic
.Column
.prototype.getRowByTileIndex = function(index
) {
1487 for (var r
= 0; r
!== this.rows_
.length
; r
++) {
1488 if (this.rows_
[r
].hasTile(index
))
1489 return this.rows_
[r
];
1495 * Adds a tile to the column.
1497 * @param {!Mosaic.Tile} tile The tile to add.
1499 Mosaic
.Column
.prototype.add = function(tile
) {
1500 var rowIndex
= this.getNextRowIndex();
1503 this.newRow_
= new Mosaic
.Row(this.getNextTileIndex());
1505 this.tiles_
.push(tile
);
1506 this.newRow_
.add(tile
);
1508 if (this.density_
.isRowComplete(this.newRow_
.getTileCount(), rowIndex
)) {
1509 this.rows_
.push(this.newRow_
);
1510 this.newRow_
= null;
1515 * Prepares the column layout.
1517 * @param {boolean=} opt_force True if the layout must be performed even for an
1518 * incomplete column.
1519 * @return {boolean} True if the layout was performed.
1521 Mosaic
.Column
.prototype.prepareLayout = function(opt_force
) {
1522 if (opt_force
&& this.newRow_
) {
1523 this.rows_
.push(this.newRow_
);
1524 this.newRow_
= null;
1527 if (this.rows_
.length
=== 0)
1530 this.width_
= Math
.min
.apply(
1531 null, this.rows_
.map(function(row
) { return row
.getMaxWidth() }));
1535 this.rowHeights_
= [];
1536 for (var r
= 0; r
!== this.rows_
.length
; r
++) {
1537 var rowHeight
= this.rows_
[r
].getHeightForWidth(this.width_
);
1538 this.height_
+= rowHeight
;
1539 this.rowHeights_
.push(rowHeight
);
1542 var overflow
= this.height_
/ this.maxHeight_
;
1543 if (!opt_force
&& (overflow
< 1))
1547 // Scale down the column width and height.
1548 this.width_
= Math
.round(this.width_
/ overflow
);
1549 this.height_
= this.maxHeight_
;
1550 Mosaic
.Layout
.rescaleSizesToNewTotal(this.rowHeights_
, this.maxHeight_
);
1557 * Retries the column layout with less tiles per row.
1559 Mosaic
.Column
.prototype.retryWithLowerDensity = function() {
1560 this.density_
.decreaseHorizontal();
1565 * @return {number} Column left edge coordinate.
1567 Mosaic
.Column
.prototype.getLeft = function() { return this.left_
};
1570 * @return {number} Column right edge coordinate after the layout.
1572 Mosaic
.Column
.prototype.getRight = function() {
1573 return this.left_
+ this.width_
;
1577 * @return {number} Column height after the layout.
1579 Mosaic
.Column
.prototype.getHeight = function() { return this.height_
};
1582 * Performs the column layout.
1583 * @param {number=} opt_offsetX Horizontal offset.
1584 * @param {number=} opt_offsetY Vertical offset.
1586 Mosaic
.Column
.prototype.layout = function(opt_offsetX
, opt_offsetY
) {
1587 opt_offsetX
= opt_offsetX
|| 0;
1588 opt_offsetY
= opt_offsetY
|| 0;
1589 var rowTop
= Mosaic
.Layout
.PADDING_TOP
;
1590 for (var r
= 0; r
!== this.rows_
.length
; r
++) {
1591 this.rows_
[r
].layout(
1592 opt_offsetX
+ this.left_
,
1593 opt_offsetY
+ rowTop
,
1595 this.rowHeights_
[r
]);
1596 rowTop
+= this.rowHeights_
[r
];
1601 * Checks if the column layout is too ugly to be displayed.
1603 * @return {boolean} True if the layout is suboptimal.
1605 Mosaic
.Column
.prototype.isSuboptimal = function() {
1607 this.rows_
.map(function(row
) { return row
.getTileCount() });
1609 var maxTileCount
= Math
.max
.apply(null, tileCounts
);
1610 if (maxTileCount
=== 1)
1611 return false; // Every row has exactly 1 tile, as optimal as it gets.
1614 this.tiles_
.map(function(tile
) { return tile
.getMaxContentHeight() });
1616 // Ugly layout #1: all images are small and some are one the same row.
1617 var allSmall
= Math
.max
.apply(null, sizes
) <= Mosaic
.Tile
.SMALL_IMAGE_SIZE
;
1621 // Ugly layout #2: all images are large and none occupies an entire row.
1622 var allLarge
= Math
.min
.apply(null, sizes
) > Mosaic
.Tile
.SMALL_IMAGE_SIZE
;
1623 var allCombined
= Math
.min
.apply(null, tileCounts
) !== 1;
1624 if (allLarge
&& allCombined
)
1627 // Ugly layout #3: some rows have too many tiles for the resulting width.
1628 if (this.width_
/ maxTileCount
< 100)
1634 ////////////////////////////////////////////////////////////////////////////////
1637 * A row in a mosaic layout. Contains tiles.
1639 * @param {number} firstTileIndex Index of the first tile in the row.
1643 Mosaic
.Row = function(firstTileIndex
) {
1644 this.firstTileIndex_
= firstTileIndex
;
1661 * @param {!Mosaic.Tile} tile The tile to add.
1663 Mosaic
.Row
.prototype.add = function(tile
) {
1664 console
.assert(this.getTileCount() < Mosaic
.Density
.MAX_HORIZONTAL
);
1665 this.tiles_
.push(tile
);
1669 * @return {!Array.<!Mosaic.Tile>} Array of tiles in the row.
1671 Mosaic
.Row
.prototype.getTiles = function() { return this.tiles_
};
1674 * Gets a tile by index.
1675 * @param {number} index Tile index.
1676 * @return {Mosaic.Tile} Requested tile or null if not found.
1678 Mosaic
.Row
.prototype.getTileByIndex = function(index
) {
1679 if (!this.hasTile(index
))
1681 return this.tiles_
[index
- this.firstTileIndex_
];
1686 * @return {number} Number of tiles in the row.
1688 Mosaic
.Row
.prototype.getTileCount = function() { return this.tiles_
.length
};
1691 * @param {number} index Tile index.
1692 * @return {boolean} True if this row contains the tile with the given index.
1694 Mosaic
.Row
.prototype.hasTile = function(index
) {
1695 return this.firstTileIndex_
<= index
&&
1696 index
< (this.firstTileIndex_
+ this.tiles_
.length
);
1700 * @param {number} y Y coordinate.
1701 * @return {boolean} True if this row covers the given Y coordinate.
1703 Mosaic
.Row
.prototype.coversY = function(y
) {
1704 return this.top_
<= y
&& y
< (this.top_
+ this.height_
);
1708 * @return {number} Y coordinate of the tile center.
1710 Mosaic
.Row
.prototype.getCenterY = function() {
1711 return this.top_
+ Math
.round(this.height_
/ 2);
1715 * Gets the first or the last tile.
1717 * @param {number} direction -1 for the first tile, 1 for the last tile.
1718 * @return {number} Tile index.
1721 Mosaic
.Row
.prototype.getEdgeTileIndex_ = function(direction
) {
1723 return this.firstTileIndex_
;
1725 return this.firstTileIndex_
+ this.getTileCount() - 1;
1729 * @return {number} Aspect ration of the combined content box of this row.
1732 Mosaic
.Row
.prototype.getTotalContentAspectRatio_ = function() {
1734 for (var t
= 0; t
!== this.tiles_
.length
; t
++)
1735 sum
+= this.tiles_
[t
].getAspectRatio();
1740 * @return {number} Total horizontal spacing in this row. This includes
1741 * the spacing between the tiles and both left and right margins.
1745 Mosaic
.Row
.prototype.getTotalHorizontalSpacing_ = function() {
1746 return Mosaic
.Layout
.SPACING
* this.getTileCount();
1750 * @return {number} Maximum width that this row may have without overscaling
1753 Mosaic
.Row
.prototype.getMaxWidth = function() {
1754 var contentHeight
= Math
.min
.apply(null,
1755 this.tiles_
.map(function(tile
) { return tile
.getMaxContentHeight() }));
1758 Math
.round(contentHeight
* this.getTotalContentAspectRatio_());
1759 return contentWidth
+ this.getTotalHorizontalSpacing_();
1763 * Computes the height that best fits the supplied row width given
1764 * aspect ratios of the tiles in this row.
1766 * @param {number} width Row width.
1767 * @return {number} Height.
1769 Mosaic
.Row
.prototype.getHeightForWidth = function(width
) {
1770 var contentWidth
= width
- this.getTotalHorizontalSpacing_();
1772 Math
.round(contentWidth
/ this.getTotalContentAspectRatio_());
1773 return contentHeight
+ Mosaic
.Layout
.SPACING
;
1777 * Positions the row in the mosaic.
1779 * @param {number} left Left position.
1780 * @param {number} top Top position.
1781 * @param {number} width Width.
1782 * @param {number} height Height.
1784 Mosaic
.Row
.prototype.layout = function(left
, top
, width
, height
) {
1786 this.height_
= height
;
1788 var contentWidth
= width
- this.getTotalHorizontalSpacing_();
1789 var contentHeight
= height
- Mosaic
.Layout
.SPACING
;
1791 var tileContentWidth
= this.tiles_
.map(
1792 function(tile
) { return tile
.getAspectRatio() });
1794 Mosaic
.Layout
.rescaleSizesToNewTotal(tileContentWidth
, contentWidth
);
1796 var tileLeft
= left
;
1797 for (var t
= 0; t
!== this.tiles_
.length
; t
++) {
1798 var tileWidth
= tileContentWidth
[t
] + Mosaic
.Layout
.SPACING
;
1799 this.tiles_
[t
].layout(tileLeft
, top
, tileWidth
, height
);
1800 tileLeft
+= tileWidth
;
1804 ////////////////////////////////////////////////////////////////////////////////
1807 * A single tile of the image mosaic.
1809 * @param {!Element} container Container element.
1810 * @param {!Gallery.Item} item Gallery item associated with this tile.
1811 * @param {EntryLocation=} opt_locationInfo Location information for the tile.
1812 * @return {!Element} The new tile element.
1814 * @extends {HTMLDivElement}
1816 * @suppress {checkStructDictInheritance}
1818 Mosaic
.Tile = function(container
, item
, opt_locationInfo
) {
1819 // This is a hack to make closure compiler recognize definitions of fields
1820 // with this decorate pattern. When this constructor is called as "new
1821 // Mosaic.Tile(...)", "this" should be Mosaic.Tile. In that case, this calls
1822 // this constructor again with setting this as HTMLDivElement. When this
1823 // condition is false, this method decorates the "this" object, and returns
1825 if (this instanceof Mosaic
.Tile
) {
1826 return Mosaic
.Tile
.call(
1827 /** @type {Mosaic.Tile} */ (document
.createElement('div')),
1828 container
, item
, opt_locationInfo
);
1831 this.__proto__
= Mosaic
.Tile
.prototype;
1832 this.className
= 'mosaic-tile';
1838 this.container_
= container
;
1841 * @type {!Gallery.Item}
1850 this.left_
= null; // Mark as not laid out.
1874 this.maxContentHeight_
= 0;
1880 this.aspectRatio_
= 0;
1883 * @type {ThumbnailLoader}
1886 this.thumbnailPreloader_
= null;
1889 * @type {ThumbnailLoader}
1892 this.thumbnailLoader_
= null;
1898 this.imagePreloaded_
= false;
1904 this.imageLoaded_
= false;
1910 this.imagePreloading_
= false;
1916 this.imageLoading_
= false;
1919 * @type {HTMLDivElement}
1922 this.wrapper_
= null;
1928 * Load mode for the tile's image.
1931 Mosaic
.Tile
.LoadMode
= {
1937 * Inherit from HTMLDivElement.
1939 Mosaic
.Tile
.prototype.__proto__
= HTMLDivElement
.prototype;
1942 * Minimum tile content size.
1946 Mosaic
.Tile
.MIN_CONTENT_SIZE
= 64;
1949 * Maximum tile content size.
1953 Mosaic
.Tile
.MAX_CONTENT_SIZE
= 512;
1956 * Default size for a tile with no thumbnail image.
1960 Mosaic
.Tile
.GENERIC_ICON_SIZE
= 128;
1963 * Max size of an image considered to be 'small'.
1964 * Small images are laid out slightly differently.
1968 Mosaic
.Tile
.SMALL_IMAGE_SIZE
= 160;
1971 * @return {!Gallery.Item} The Gallery item.
1973 Mosaic
.Tile
.prototype.getItem = function() { return this.item_
; };
1976 * @return {number} Maximum content height that this tile can have.
1978 Mosaic
.Tile
.prototype.getMaxContentHeight = function() {
1979 return this.maxContentHeight_
;
1983 * @return {number} The aspect ratio of the tile image.
1985 Mosaic
.Tile
.prototype.getAspectRatio = function() { return this.aspectRatio_
; };
1988 * @return {boolean} True if the tile is initialized.
1990 Mosaic
.Tile
.prototype.isInitialized = function() {
1991 return !!this.maxContentHeight_
;
1995 * Checks whether the image of specified (or better resolution) has been loaded.
1997 * @param {Mosaic.Tile.LoadMode=} opt_loadMode Loading mode, default: LOW_DPI.
1998 * @return {boolean} True if the tile is loaded with the specified dpi or
2001 Mosaic
.Tile
.prototype.isLoaded = function(opt_loadMode
) {
2002 var loadMode
= opt_loadMode
|| Mosaic
.Tile
.LoadMode
.LOW_DPI
;
2004 case Mosaic
.Tile
.LoadMode
.LOW_DPI
:
2005 if (this.imagePreloaded_
|| this.imageLoaded_
)
2008 case Mosaic
.Tile
.LoadMode
.HIGH_DPI
:
2009 if (this.imageLoaded_
)
2017 * Checks whether the image of specified (or better resolution) is being loaded.
2019 * @param {Mosaic.Tile.LoadMode=} opt_loadMode Loading mode, default: LOW_DPI.
2020 * @return {boolean} True if the tile is being loaded with the specified dpi or
2023 Mosaic
.Tile
.prototype.isLoading = function(opt_loadMode
) {
2024 var loadMode
= opt_loadMode
|| Mosaic
.Tile
.LoadMode
.LOW_DPI
;
2026 case Mosaic
.Tile
.LoadMode
.LOW_DPI
:
2027 if (this.imagePreloading_
|| this.imageLoading_
)
2030 case Mosaic
.Tile
.LoadMode
.HIGH_DPI
:
2031 if (this.imageLoading_
)
2039 * Marks the tile as not loaded to prevent it from participating in the layout.
2041 Mosaic
.Tile
.prototype.markUnloaded = function() {
2042 this.maxContentHeight_
= 0;
2043 if (this.thumbnailLoader_
) {
2044 this.thumbnailLoader_
.cancel();
2045 this.imagePreloaded_
= false;
2046 this.imagePreloading_
= false;
2047 this.imageLoaded_
= false;
2048 this.imageLoading_
= false;
2053 * Initializes the thumbnail in the tile. Does not load an image, but sets
2054 * target dimensions using metadata.
2056 Mosaic
.Tile
.prototype.init = function() {
2057 this.markUnloaded();
2058 this.left_
= null; // Mark as not laid out.
2060 // Set higher priority for the selected elements to load them first.
2061 var priority
= this.getAttribute('selected') ? 2 : 3;
2063 // Use embedded thumbnails on Drive, since they have higher resolution.
2064 this.thumbnailLoader_
= new ThumbnailLoader(
2065 this.getItem().getEntry(),
2066 ThumbnailLoader
.LoaderType
.CANVAS
,
2067 this.getItem().getThumbnailMetadataItem(),
2068 undefined, // Media type.
2070 ThumbnailLoader
.LoadTarget
.EXTERNAL_METADATA
,
2071 ThumbnailLoader
.LoadTarget
.FILE_ENTRY
2074 // If no hidpi embedded thumbnail available, then use the low resolution
2076 if (this.thumbnailLoader_
.getLoadTarget() ===
2077 ThumbnailLoader
.LoadTarget
.FILE_ENTRY
) {
2078 this.thumbnailPreloader_
= new ThumbnailLoader(
2079 this.getItem().getEntry(),
2080 ThumbnailLoader
.LoaderType
.CANVAS
,
2081 this.getItem().getThumbnailMetadataItem(),
2082 undefined, // Media type.
2084 ThumbnailLoader
.LoadTarget
.CONTENT_METADATA
2086 // Preloaders have always higher priotity, so the preload images
2087 // are loaded as soon as possible.
2089 if (!this.thumbnailPreloader_
.getLoadTarget())
2090 this.thumbnailPreloader_
= null;
2093 // Dimensions are always acquired from the metadata. For local files, it is
2094 // extracted from headers. For Drive files, it is received via the Drive API.
2095 // If the dimensions are not available, then the fallback dimensions will be
2096 // used (same as for the generic icon).
2097 var metadataItem
= this.getItem().getMetadataItem();
2100 if (metadataItem
.imageWidth
&& metadataItem
.imageHeight
) {
2101 width
= metadataItem
.imageWidth
;
2102 height
= metadataItem
.imageHeight
;
2104 // No dimensions in metadata, then use the generic dimensions.
2105 width
= Mosaic
.Tile
.GENERIC_ICON_SIZE
;
2106 height
= Mosaic
.Tile
.GENERIC_ICON_SIZE
;
2109 if (width
> height
) {
2110 if (width
> Mosaic
.Tile
.MAX_CONTENT_SIZE
) {
2111 height
= Math
.round(height
* Mosaic
.Tile
.MAX_CONTENT_SIZE
/ width
);
2112 width
= Mosaic
.Tile
.MAX_CONTENT_SIZE
;
2115 if (height
> Mosaic
.Tile
.MAX_CONTENT_SIZE
) {
2116 width
= Math
.round(width
* Mosaic
.Tile
.MAX_CONTENT_SIZE
/ height
);
2117 height
= Mosaic
.Tile
.MAX_CONTENT_SIZE
;
2120 this.maxContentHeight_
= Math
.max(Mosaic
.Tile
.MIN_CONTENT_SIZE
, height
);
2121 this.aspectRatio_
= width
/ height
;
2125 * Loads an image into the tile.
2127 * The mode argument is a hint. Use low-dpi for faster response, and high-dpi
2128 * for better output, but possibly affecting performance.
2130 * If the mode is high-dpi, then a the high-dpi image is loaded, but also
2131 * low-dpi image is loaded for preloading (if available).
2132 * For the low-dpi mode, only low-dpi image is loaded. If not available, then
2133 * the high-dpi image is loaded as a fallback.
2135 * @param {!Mosaic.Tile.LoadMode} loadMode Loading mode.
2136 * @param {function(boolean)} onImageLoaded Callback when image is loaded.
2137 * The argument is true for success, false for failure.
2139 Mosaic
.Tile
.prototype.load = function(loadMode
, onImageLoaded
) {
2140 // Attaches the image to the tile and finalizes loading process for the
2141 // specified loader.
2142 var finalizeLoader = function(mode
, success
, loader
) {
2143 if (success
&& this.wrapper_
) {
2144 // Show the fade-in animation only when previously there was no image
2145 // attached in this tile.
2146 if (!this.imageLoaded_
&& !this.imagePreloaded_
)
2147 this.wrapper_
.classList
.add('animated');
2149 this.wrapper_
.classList
.remove('animated');
2152 // Add debug mode classes.
2153 this.wrapper_
.classList
.remove('load-target-content-metadata');
2154 this.wrapper_
.classList
.remove('load-target-external-metadata');
2155 this.wrapper_
.classList
.remove('load-target-file-entry');
2156 switch (loader
.getLoadTarget()) {
2157 case ThumbnailLoader
.LoadTarget
.CONTENT_METADATA
:
2158 this.wrapper_
.classList
.add('load-target-content-metadata');
2160 case ThumbnailLoader
.LoadTarget
.EXTERNAL_METADATA
:
2161 this.wrapper_
.classList
.add('load-target-external-metadata');
2163 case ThumbnailLoader
.LoadTarget
.FILE_ENTRY
:
2164 this.wrapper_
.classList
.add('load-target-file-entry');
2167 loader
.attachImage(this.wrapper_
, ThumbnailLoader
.FillMode
.OVER_FILL
);
2168 onImageLoaded(success
);
2171 case Mosaic
.Tile
.LoadMode
.LOW_DPI
:
2172 this.imagePreloading_
= false;
2173 this.imagePreloaded_
= true;
2175 case Mosaic
.Tile
.LoadMode
.HIGH_DPI
:
2176 this.imageLoading_
= false;
2177 this.imageLoaded_
= true;
2182 // Always load the low-dpi image first if it is available for the fastest
2184 if (!this.imagePreloading_
&& this.thumbnailPreloader_
) {
2185 this.imagePreloading_
= true;
2186 this.thumbnailPreloader_
.loadDetachedImage(function(success
) {
2187 // Hi-dpi loaded first, ignore this call then.
2188 if (this.imageLoaded_
)
2190 finalizeLoader(Mosaic
.Tile
.LoadMode
.LOW_DPI
,
2192 this.thumbnailPreloader_
);
2196 // Load the high-dpi image only when it is requested, or the low-dpi is not
2198 if (!this.imageLoading_
&&
2199 (loadMode
=== Mosaic
.Tile
.LoadMode
.HIGH_DPI
|| !this.imagePreloading_
)) {
2200 this.imageLoading_
= true;
2201 this.thumbnailLoader_
.loadDetachedImage(function(success
) {
2202 // Cancel preloading, since the hi-dpi image is ready.
2203 if (this.thumbnailPreloader_
)
2204 this.thumbnailPreloader_
.cancel();
2205 finalizeLoader(Mosaic
.Tile
.LoadMode
.HIGH_DPI
,
2207 this.thumbnailLoader_
);
2213 * Unloads an image from the tile.
2215 Mosaic
.Tile
.prototype.unload = function() {
2216 this.thumbnailLoader_
.cancel();
2217 if (this.thumbnailPreloader_
)
2218 this.thumbnailPreloader_
.cancel();
2219 this.imagePreloaded_
= false;
2220 this.imageLoaded_
= false;
2221 this.imagePreloading_
= false;
2222 this.imageLoading_
= false;
2223 this.wrapper_
.innerText
= '';
2227 * Selects/unselects the tile.
2229 * @param {boolean} on True if selected.
2231 Mosaic
.Tile
.prototype.select = function(on
) {
2233 this.setAttribute('selected', true);
2235 this.removeAttribute('selected');
2239 * Positions the tile in the mosaic.
2241 * @param {number} left Left position.
2242 * @param {number} top Top position.
2243 * @param {number} width Width.
2244 * @param {number} height Height.
2246 Mosaic
.Tile
.prototype.layout = function(left
, top
, width
, height
) {
2249 this.width_
= width
;
2250 this.height_
= height
;
2252 this.style
.left
= left
+ 'px';
2253 this.style
.top
= top
+ 'px';
2254 this.style
.width
= width
+ 'px';
2255 this.style
.height
= height
+ 'px';
2257 if (!this.wrapper_
) { // First time, create DOM.
2258 this.container_
.appendChild(this);
2259 var border
= util
.createChild(this, 'img-border');
2260 this.wrapper_
= assertInstanceof(util
.createChild(border
, 'img-wrapper'),
2263 if (this.hasAttribute('selected'))
2264 this.scrollIntoView(false);
2266 if (this.imageLoaded_
) {
2267 this.thumbnailLoader_
.attachImage(this.wrapper_
,
2268 ThumbnailLoader
.FillMode
.OVER_FILL
);
2273 * If the tile is not fully visible scroll the parent to make it fully visible.
2274 * @param {boolean=} opt_animated True, if scroll should be animated,
2277 Mosaic
.Tile
.prototype.scrollIntoView = function(opt_animated
) {
2278 if (this.left_
=== null) // Not laid out.
2282 var tileLeft
= this.left_
- Mosaic
.Layout
.SCROLL_MARGIN
;
2283 if (tileLeft
< this.container_
.scrollLeft
) {
2284 targetPosition
= tileLeft
;
2286 var tileRight
= this.left_
+ this.width_
+ Mosaic
.Layout
.SCROLL_MARGIN
;
2287 var scrollRight
= this.container_
.scrollLeft
+ this.container_
.clientWidth
;
2288 if (tileRight
> scrollRight
)
2289 targetPosition
= tileRight
- this.container_
.clientWidth
;
2292 if (targetPosition
) {
2293 if (opt_animated
=== false)
2294 this.container_
.scrollLeft
= targetPosition
;
2296 this.container_
.animatedScrollTo(targetPosition
);
2301 * @return {ImageRect} Rectangle occupied by the tile's image,
2302 * relative to the viewport.
2304 Mosaic
.Tile
.prototype.getImageRect = function() {
2305 if (this.left_
=== null) // Not laid out.
2308 var margin
= Mosaic
.Layout
.SPACING
/ 2;
2309 return new ImageRect(this.left_
- this.container_
.scrollLeft
, this.top_
,
2310 this.width_
, this.height_
).inflate(-margin
, -margin
);
2314 * @return {number} X coordinate of the tile center.
2316 Mosaic
.Tile
.prototype.getCenterX = function() {
2317 return this.left_
+ Math
.round(this.width_
/ 2);