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