/*jslint vars: true, plusplus: true, nomen: true, white: true */ /*global Pocketry, Matrix */ Pocketry.Layout = function () { 'use strict'; // `rowSpan` indicates the max. vertical size of an individual element function Layout(colCount, rowSpan) { this.colCount = colCount; this.rowSpan = rowSpan; this.init(); } Layout.prototype.init = function () { this.stack = []; // one-dimensional array of tiles // two-dimensional array; rows * columns // initialized to colCount * rowSpan width by default this.matrix = this.initMatrix(); delete this.scanStartPosition; }; Layout.prototype.initMatrix = function () { return new Matrix().rows(0, this.rowSpan).cols(0, this.colCount).map(); }; // adds a tile to the matrix // that object must have a `size` property of the form `[width, height]` Layout.prototype.add = function (tile) { if (tile.hidden) { this.stack.push(tile); // XXX: DRY! return []; } if (this._canFitTile(tile)) { var unplaced = this.walk( tryToFitTile(tile), tile.size, this.scanStartPosition ); if (unplaced) { // try again this._markRowAsScanned(); this.extend(); return this.add(tile); } this._markScanPositionAtTile(tile); this.stack.push(tile); } return []; }; Layout.prototype._canFitTile = function (tile) { return tile.size[0] <= this.matrix.size()[1]; }; Layout.prototype._markRowAsScanned = function () { this.scanStartPosition = { col: 0, row: this.matrix.size()[0] }; }; Layout.prototype._markScanPositionAtTile = function (tile) { // the position advances only // if the tile fits within the row height if (tile.size[1] >= this.rowSpan) { this.scanStartPosition = { col: tile.position.x + tile.size[0], row: (this.scanStartPosition ? this.scanStartPosition.row : 0) }; } }; /** * Predicate which determine if the current tile is placed * * @param tile - current tile to check * @returns {boolean} true if tile is placed */ function placedTile(tile) { return tile !== undefined && tile !== null; } function tryToFitTile(tile) { return function (tileSelection) { if (tileSelection.some(placedTile)) { return false; } else { tileSelection.map(function () { return tile; }); tile.position = { x: tileSelection.col.start, y: tileSelection.row.start }; return true; } }; } /** * Moves tiles in the stack to specified position. * If position is not specified put tile to the end. * * @param tile - tile to move * @param toPosition - position to move */ Layout.prototype.move = function (tile, toPosition) { // freezed tiles should not move if (tile.freezed) { return; } if (toPosition == null) { toPosition = -1; } var stack = this.stack; _.remove(stack, tile); if (toPosition !== -1) { stack.splice(toPosition, 0, tile); } else { stack.push(tile); } this.rebuild(); }; /** * Rebuild matrix according to the current tails stack state. * */ Layout.prototype.rebuild = function () { var self = this; var stack = this.stack; this.init(); stack.forEach(function (tile) { self.add(tile); }); }; /** * Returns a tile from the layout * denoted by its position * * @param position - tile position object * @returns {*} */ Layout.prototype.getTile = function (position) { return this.matrix.get(position.y, position.x); }; /** * Moves the given tile to the new position in the layout. * * @param tile - a tile to be repositioned * @param newPosition - new position coordinates within the layout */ Layout.prototype.moveTo = function (tile, newPosition) { var n = this.getStackNeighbor(newPosition); // do not move tiles if the neighbor is freezed if (n && n.freezed) { return; } var newStackIndex = this.getTileInsertIndex(n, newPosition); this.move(tile, newStackIndex); }; /** * Determines the nearest stack neighbor for the given position. * Depends on the walk algorithm implementation. * * @param position * @returns {*} */ Layout.prototype.getStackNeighbor = function (position) { position = normalizeBounds(position, this.matrix.size()); var neighbor = this.matrix.get(position.y, position.x); while (!neighbor) { position = stepUp(position, this.rowSpan); position = stepRight(position, this.matrix.size()[1] - 1, this.rowSpan); if (isLayoutEnded(position, this.stack)) { break; } neighbor = this.getStackNeighbor(position); } return neighbor; }; /** * This method returns the normalized movement coordinates. * Normalization ensures that target coordinates never get out of container's bounds. */ function normalizeBounds(position, container) { var normalized = {x: position.x, y: position.y}; normalized.x = Math.max(0, position.x); normalized.x = Math.min(container[1], position.x); normalized.y = Math.max(0, position.y); normalized.y = Math.min(container[0], position.y); return normalized; } function stepUp(position, rowHeight) { var offsetInsideRow = position.y % rowHeight; if (offsetInsideRow !== 0) { // if not topmost position in a row // position is advanced to the top position.y -= offsetInsideRow; } return position; } function stepRight(position, rowLength, rowHeight) { if (position.x >= rowLength) { // search in the next row position.x = 0; position.y += rowHeight; } else { position.x += 1; } return position; } function isLayoutEnded(position, stack) { var lastTile = stack[stack.length - 1]; return (position.x > lastTile.position.x && position.y >= lastTile.position.y); } /** * Determines whether the tile should be inserted before or after * its neighbor * @param neighbor - neighbor tile * @param position - tile target position * @returns {number} */ Layout.prototype.getTileInsertIndex = function (neighbor, position) { if (!neighbor) { return -1; } var stackIndex = this.stack.indexOf(neighbor), np = neighbor.position; if (position.x > np.x + neighbor.size[0] / 2) { stackIndex += 1; } return stackIndex; }; /** * Iterates through the layout. * Each iteration is based on the matrix selection. * Selection size is identical to the passed stepSize parameter. * * The resulting 'selection cursor' iterates through * all possible selection area positions. * * The callback is applied to each iteration selection. * * @param {Function} callback - function to be applied to each selection. * @param callback.selection - the current selection of the stepSize size. * @param callback.iteration - the current iteration number * @param stepSize - the [cols, rows] size of the iteration selection. * Defaults to [1,1] * @param scanOffset - denotes the number of rows and columns to skip * @returns {boolean} */ Layout.prototype.walk = function (callback, stepSize, scanOffset) { stepSize = stepSize || [1, 1]; scanOffset = scanOffset || {col: 0, row: 0}; var stop = false, row = scanOffset.row, totalRows = this.matrix.size()[0]; // walk row by row while ((row < totalRows) && !stop) { var currentRowToScan = this._getRowToScan(row, scanOffset.col); stop = this._scanRow(currentRowToScan, stepSize, callback); row += this.rowSpan; } return !stop; }; /** * Return a row selection from the underlying matrix. * This selection will be scanned by walk algorithm. * * @param rowOffset - the row index offset * @param colOffset - the col index offset * @returns {*|MatrixSelection} * @private */ Layout.prototype._getRowToScan = function (rowOffset, colOffset) { return this.matrix. rows( rowOffset, rowOffset + this.rowSpan ). cols( colOffset, this.matrix.size()[1] ); }; /** * Scans the specified row and apply the callback on each step * * @param row - the row to be scanned * @param stepSize - the size of the scan step * @param callback - the function to be applied on each step * @returns {boolean} * @private */ Layout.prototype._scanRow = function (row, stepSize, callback) { var stepNumber = 0, selection = {row: {start: 0, end: 0}, col: {start: 0, end: 0}}, stop = false; var steps = this._getNumberOfScanSteps(row, stepSize); while (stepNumber < steps && !stop) { selection = this._nextTopDownScanStep( stepNumber, stepSize, row); stop = callback( this.matrix. rows(selection.row.start, selection.row.end). cols(selection.col.start, selection.col.end), stepNumber ); stepNumber++; } return stop; }; /** * Returns the number of scan steps for the given matrix. * * @param matrix - matrix to be scanned * @param stepSize - the size of the scan step * @returns {number} * @private */ Layout.prototype._getNumberOfScanSteps = function (matrix, stepSize) { var sz = matrix.size(); // assume that adjacent steps are always of size 1 var inRow = sz[0] - stepSize[1] + 1; var inCol = sz[1] - stepSize[0] + 1; return inRow * inCol; }; /** * Compute next iteration step statically * based on the current iteration number. * The algorithm is 'top-down column scan', based on matrix dimensions: * * [ 1 3 ] * [ 2 4 ] * * [ 1 3 5 7 ] * [ 2 4 6 8 ] * * [ 1 4 7 ] * [ 2 5 8 ] * [ 3 6 9 ] * * The underlying matrix is divided in columns. * Each column is scanned top to bottom. * * @param stepNumber - current iteration number * @param stepSize - selection dimensions * @param scanArea - matrix selection to be scanned * @returns {*} - current walk selection */ Layout.prototype._nextTopDownScanStep = function (stepNumber, stepSize, scanArea) { var stepRowIdx, stepColIdx, stepsPerColumn; stepsPerColumn = scanArea.size()[0] - stepSize[1] + 1; stepColIdx = scanArea.col.start + Math.floor(stepNumber / stepsPerColumn); stepRowIdx = scanArea.row.start + stepNumber % stepsPerColumn; var step = { row: { start: stepRowIdx, end: stepRowIdx + stepSize[1] }, col: { start: stepColIdx, end: stepColIdx + stepSize[0] } }; return step; }; // add a set of rows (based on row span) Layout.prototype.extend = function () { var newRowIdx = this.matrix.size()[0]; this.matrix.rows(newRowIdx, newRowIdx + this.rowSpan).map(); }; return Layout; }();