var cameraGrid = {}; cameraGrid.CameraGrid = function(container, sourceUrls, resolutions, getUrl) { this.container_ = container; this.sourceUrls_ = sourceUrls; this.resolutions_ = resolutions || this.defaultResolutions_; this.getUrl_ = getUrl || this.defaultGetUrl_; this.tileScaleWidth_ = this.resolutions_[0][0]; this.tileScaleHeight_ = this.resolutions_[0][1]; this.gridWidthCells_ = 0; this.gridHeightCells_ = 0; this.imgWidthPx_ = 0; this.imgHeightPx_ = 0; this.constraint_ = null; this.containerImgWidthPx_ = 0; this.ctonainerImgHeightPx_ = 0; this.containerConstraint_ = null; this.selected_ = null; this.scanning_ = false; this.buildCells_(); this.buildStylesheet_(); this.container_.tabIndex = 0; this.container_.focus(); this.container_.addEventListener('keypress', this.onKeyPress_.bind(this), false); this.container_.addEventListener('keydown', this.onKeyDown_.bind(this), false); window.addEventListener('resize', this.rebuildIfNeeded_.bind(this), false); this.rebuildIfNeeded_(); window.setInterval(this.onScanTimer_.bind(this), 3000); }; // Resolution list must be sorted ascending. // All resolutions must be the same aspect ratio. cameraGrid.CameraGrid.prototype.defaultResolutions_ = [ [ 160, 120 ], [ 240, 180 ], [ 320, 240 ], [ 480, 360 ], [ 640, 480 ], [ 800, 600 ], [ 1024, 768 ], [ 1280, 960 ], ]; cameraGrid.CameraGrid.prototype.defaultGetUrl_ = function(sourceUrl, width, height) { return sourceUrl + 'mjpg/video.mjpg?resolution=' + width + 'x' + height; }; cameraGrid.CameraGrid.prototype.disableScanning_ = function() { if (this.scanning_) { this.scanning_ = false; // Images might all be higher res than needed, so we refresh. this.buildImages_(); } }; cameraGrid.CameraGrid.prototype.setSelectedNoScan_ = function(index) { this.setSelected_(index); this.disableScanning_(); }; cameraGrid.CameraGrid.prototype.setSelected_ = function(index) { var old_index; if (this.selected_ == index) { this.removeCSSClass_(this.cells_[this.selected_], 'cameraGridFullScreen'); old_index = this.selected_; this.selected_ = null; } else { if (this.selected_ != null) { this.removeCSSClass_(this.cells_[this.selected_], 'cameraGridFullScreen'); old_index = this.selected_; } this.addCSSClass_(this.cells_[index], 'cameraGridFullScreen'); this.selected_ = index; } if (this.containerImgWidthPx_ != this.imgWidthPx_ || this.containerImgHeightPx_ != this.imgHeightPx_) { // Image stream should change when toggling full screen. if (old_index != null) { this.buildImage_(old_index); } if (this.selected_ != null) { this.buildImage_(this.selected_); } } }; cameraGrid.CameraGrid.prototype.buildCells_ = function() { this.cells_ = []; for (var i = 0; i < this.sourceUrls_.length; i++) { var cell = document.createElement('cameraGridCell'); this.cells_.push(cell); } }; cameraGrid.CameraGrid.prototype.addCSSClass_ = function(node, className) { var classes = node.className.split(' ').filter(function(className) { return className; }); if (classes.indexOf(className) != -1) { // Already has class. return; } classes.push(className); node.className = classes.join(' '); } cameraGrid.CameraGrid.prototype.removeCSSClass_ = function(node, className) { var classes = node.className.split(' ').filter(function(className) { return className; }); var i = classes.indexOf(className); if (i == -1) { // Already doesn't have class. return; } delete classes[i]; node.className = classes.join(' '); } cameraGrid.CameraGrid.prototype.buildStylesheet_ = function() { var style = document.createElement('style'); document.head.appendChild(style); style.sheet.insertRule('cameraGridRow {}', 0); this.rowHeightRule_ = style.sheet.cssRules[0]; style.sheet.insertRule('cameraGridCell {}', 0); this.cellWidthRule_ = style.sheet.cssRules[0]; style.sheet.insertRule('cameraGridImgContainer img {}', 0); this.imageScaleRule_ = style.sheet.cssRules[0]; style.sheet.insertRule('cameraGridCell.cameraGridFullScreen cameraGridImgContainer img {}', 0); this.containerImageScaleRule_ = style.sheet.cssRules[0]; style.sheet.insertRule('cameraGridRow { display: block; width: 100% }', 0); style.sheet.insertRule('cameraGridCell { display: inline-block; height: 100%; position: relative }', 0); style.sheet.insertRule('cameraGridImgContainer { position: absolute; top: 0; left: 0; bottom: 0; right: 0; text-align: center }', 0); style.sheet.insertRule('cameraGridImgContainer img { max-height: 100%; max-width: 100% }', 0); style.sheet.insertRule('.cameraGridContainer { font-size: 0; text-align: center; -webkit-user-select: none; -moz-user-select: none; }', 0); style.sheet.insertRule('cameraGridCell.cameraGridFullScreen { position: static }', 0); style.sheet.insertRule('cameraGridCell.cameraGridFullScreen cameraGridImgContainer { z-index: 1 }', 0); this.addCSSClass_(this.container_, 'cameraGridContainer'); }; cameraGrid.CameraGrid.prototype.calculateGrid_ = function() { var containerWidth = this.container_.offsetWidth; var containerHeight = this.container_.offsetHeight; var numTiles = this.sourceUrls_.length; var scaleFactor = ((containerHeight / this.tileScaleHeight_) / (containerWidth / this.tileScaleWidth_)); var gridHeight = Math.sqrt(scaleFactor * numTiles); var gridWidth = Math.sqrt(numTiles / scaleFactor); var gridOptions = [ [ Math.ceil(gridWidth), Math.floor(gridHeight) ], [ Math.floor(gridWidth), Math.ceil(gridHeight) ], [ Math.ceil(gridWidth), Math.ceil(gridHeight) ], ]; // Check all possible options. // We are optimizing for several dimensions (decreasing priority): // 1) Be able to fit all the tiles. // 2) Maximum scale for an image in each cell. // 3) Minimize number of cells. var minCells = Number.MAX_VALUE; var maxScale = 0.0; var chosenHeight, chosenWidth, chosenConstraint; for (var i = 0; i < gridOptions.length; i++) { var gridOption = gridOptions[i]; var numCells = gridOption[0] * gridOption[1]; if (numCells < numTiles) { // Can't fit all the tiles in (we've rounded down too far). continue; } var widthScale = (containerWidth / gridOption[0]) / this.tileScaleWidth_; var heightScale = (containerHeight / gridOption[1]) / this.tileScaleHeight_; var scale, constraint; if (widthScale < heightScale) { scale = widthScale; constraint = 'width'; } else { scale = heightScale; constraint = 'height'; } if (scale < maxScale) { // This would make cells smaller than another viable solution. continue; } if (scale == maxScale && numCells > minCells) { // Same cell size as another viable solution, but ours has more cells. continue; } chosenWidth = gridOption[0]; chosenHeight = gridOption[1]; chosenConstraint = constraint; minCells = numCells; maxScale = scale; } return { gridWidthCells: chosenWidth, gridHeightCells: chosenHeight, constraint: chosenConstraint, containerConstraint: scaleFactor > 1 ? 'width' : 'height', cellWidthPx: this.tileScaleWidth_ * maxScale, cellHeightPx: this.tileScaleHeight_ * maxScale, }; }; cameraGrid.CameraGrid.prototype.findMinimumResolution_ = function(tileWidth, tileHeight) { for (var i = 0; i < this.resolutions_.length; i++) { var resolution = this.resolutions_[i]; if (i + 1 < this.resolutions_.length && (resolution[0] < tileWidth && resolution[1] < tileHeight)) { continue; } return { imgWidthPx: resolution[0], imgHeightPx: resolution[1], }; } }; cameraGrid.CameraGrid.prototype.deletePreviousSiblings_ = function(element) { while (element.previousSibling) { element.parentNode.removeChild(element.previousSibling); } }; cameraGrid.CameraGrid.prototype.buildImage_ = function(index) { var sourceUrl = this.sourceUrls_[index]; var imgUrl = ( (this.scanning_ || index == this.selected_) ? this.getUrl_(sourceUrl, this.containerImgWidthPx_, this.containerImgHeightPx_) : this.getUrl_(sourceUrl, this.imgWidthPx_, this.imgHeightPx_)); var cell = this.cells_[index]; // cell > imgContainer(s) > img // Last imgContainer will eventually win. if (cell.lastChild && cell.lastChild.firstChild.src == imgUrl) { // We'd be re-adding the same image; skip. return; } var img = document.createElement('img'); img.src = imgUrl; var imgContainer = document.createElement('cameraGridImgContainer'); imgContainer.addEventListener('click', this.setSelected_.bind(this, index), false); img.addEventListener('load', this.deletePreviousSiblings_.bind(this, imgContainer), false); imgContainer.appendChild(img); cell.appendChild(imgContainer); }; cameraGrid.CameraGrid.prototype.buildImages_ = function() { for (var i = 0; i < this.sourceUrls_.length; i++) { this.buildImage_(i); } }; cameraGrid.CameraGrid.prototype.buildGrid_ = function() { this.container_.innerHTML = ''; this.rowHeightRule_.style.height = 100 / this.gridHeightCells_ + '%'; this.cellWidthRule_.style.width = 100 / this.gridWidthCells_ + '%'; var i = 0; for (var y = 0; y < this.gridHeightCells_; y++) { var row = document.createElement('cameraGridRow'); for (var x = 0; x < this.gridWidthCells_; x++) { if (i < this.cells_.length) { var cell = this.cells_[i]; row.appendChild(cell); i++; } } this.container_.appendChild(row); } }; cameraGrid.CameraGrid.prototype.setUpscaleRule_ = function(constraint, rule) { if (constraint == 'height') { rule.style.minWidth = 0; rule.style.minHeight = '100%'; } else { rule.style.minWidth = '100%'; rule.style.minHeight = 0; } }; cameraGrid.CameraGrid.prototype.rebuildIfNeeded_ = function() { var grid = this.calculateGrid_(); var resolution = this.findMinimumResolution_(grid.cellWidthPx, grid.cellHeightPx); var containerResolution = this.findMinimumResolution_(this.container_.offsetWidth, this.container_.offsetHeight); if (grid.constraint != this.constraint_) { this.constraint_ = grid.constraint; this.setUpscaleRule_(this.constraint_, this.imageScaleRule_); } if (grid.containerConstraint != this.containerConstraint_) { this.containerConstraint_ = grid.containerConstraint; this.setUpscaleRule_(this.containerConstraint_, this.containerImageScaleRule_); } if (resolution.imgWidthPx != this.imgWidthPx_ || resolution.imgHeightPx != this.imgHeightPx_) { // Need to recache images. this.imgWidthPx_ = resolution.imgWidthPx; this.imgHeightPx_ = resolution.imgHeightPx; this.buildImages_(); } if (containerResolution.imgWidthPx != this.containerImgWidthPx_ || containerResolution.imgHeightPx != this.containerImgHeightPx_) { this.containerImgWidthPx_ = containerResolution.imgWidthPx; this.containerImgHeightPx_ = containerResolution.imgHeightPx; if (this.selected_ != null) { this.buildImage_(this.selected_); } } if (grid.gridWidthCells != this.gridWidthCells_ || grid.gridHeightCells != this.gridHeightCells_) { this.gridWidthCells_ = grid.gridWidthCells; this.gridHeightCells_ = grid.gridHeightCells; this.buildGrid_(); } }; cameraGrid.CameraGrid.prototype.onKeyPress_ = function(e) { var character = String.fromCharCode(e.charCode); switch (character) { case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '0': var index = e.charCode - '1'.charCodeAt(0); if (index == -1) { index = 10; } if (index < this.cells_.length) { this.setSelectedNoScan_(index); } break; case 's': if (this.scanning_ && this.selected_ != null) { // Toggle off this.setSelectedNoScan_(this.selected_); return; } this.scanning_ = true; if (this.selected_ == null) { this.setSelected_(0); } break; case ' ': this.scanning_ = !this.scanning_; break; } }; cameraGrid.CameraGrid.prototype.scanLeft_ = function() { this.setSelected_(this.selected_ > 0 ? this.selected_ - 1 : this.cells_.length - 1); }; cameraGrid.CameraGrid.prototype.scanRight_ = function() { this.setSelected_((this.selected_ + 1) % this.cells_.length); }; cameraGrid.CameraGrid.prototype.onKeyDown_ = function(e) { switch (e.keyCode) { case 27: // Esc if (this.selected_ != null) { // Toggle selected feed off. this.setSelectedNoScan_(this.selected_); } break; case 37: // Left arrow if (this.selected_ != null) { this.scanLeft_(); this.disableScanning_(); } break; case 39: // Right arrow if (this.selected_ != null) { this.scanRight_(); this.disableScanning_(); } break; } }; cameraGrid.CameraGrid.prototype.onScanTimer_ = function() { if (!this.scanning_ || this.selected_ == null) { return; } this.scanRight_(); };