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