screenManager.js

define(['utils', 'screenModel', 'IPlugin', 'errors'], function(Utils, Screen, IPlugin, Errors) {
    "use strict";

    /**
     * @class
     * Класс-менеджер моделей контента
     * @constructor ScreenManager
     * @extends IPlugin
     */
    function ScreenManager() {
        this._history = [];
        this._curScreen = null;
        this._relativeUpdateFn = this._updateRelativeScreen.bind(this);
        Screen.registerUpdateFn(this._relativeUpdateFn);
    }
    Utils.inherite(ScreenManager, IPlugin);
    /**
     * Применить конфигурацию к панели. Учитывает опции maxHistoryLength, isDirectPath, cyclicStep, getLeft, getTop, getRight, getBottom.
     * @param {Moving~config} config - конфигурация
     * @memberOf ScreenManager
     */
    ScreenManager.prototype.configure = function(config) {
        if (typeof config === 'object') {
            if (config.maxHistoryLength !== undefined) {
                if (typeof config.maxHistoryLength === 'number' && config.maxHistoryLength >= 0) {
                    this._maxHistoryLength = config.maxHistoryLength;
                    if (this._history.length > this._maxHistoryLength) {
                        this._history = this._history.slice(this._history.length - this._maxHistoryLength);
                    }
                } else {
                    throw new Errors.ArgumentError('maxHistoryLength', config.maxHistoryLength);
                }
            }
            if (config.isDirectPath !== undefined) {
                this._isDirectPath = config.isDirectPath;
            }
            if (config.cyclicStep !== undefined) {
                this._cyclicStep = config.cyclicStep;
            }
            if (config.getLeft !== undefined) {
                this._getLeft = config.getLeft;
            }
            if (config.getRight !== undefined) {
                this._getRight = config.getRight;
            }
            if (config.getTop !== undefined) {
                this._getTop = config.getTop;
            }
            if (config.getBottom !== undefined) {
                this._getBottom = config.getBottom;
            }
            if (config.savePrevious !== undefined) {
                this._savePrevious = config.savePrevious;
            }

        }
    };

    ScreenManager.prototype._updateScreens = function(side, screen, isSaveHistory) {
        var updated = false;
        if (screen) {
            if (this._curScreen !== screen) {
                updated = true;
            }
            this._curScreen = screen;
        } else if (this.getRelativeScreen(side)) {
            var prevScreen = this._curScreen;
            this._curScreen = this.getRelativeScreen(side);

            if (side !== 'center') {
                updated = true;
            }
        }

        if (updated && isSaveHistory !== false) {
            this._history.push({
                screen: prevScreen,
                side: Utils.oppositeSide(side),
                lastSide: this._lastSide,
                lastScreen: this._lastScreen
            });
            if (this._history.length > this._maxHistoryLength) {
                this._history.shift();
            }
        }

        return this._curScreen;
    };

    ScreenManager.prototype._getRelativeScreenByScreen = function(screen, side) {
        if (!(screen instanceof Screen)) {
            throw new Errors.ArgumentError('screen', screen);
        }
        if (Utils.sidesWithCenter.indexOf(side) === -1) {
            throw new Errors.ArgumentError('side', side);
        }

        if (side === 'center') {
            return screen;
        } else if (side === 'left') {
            return this._getLeft(screen);
        } else if (side === 'top') {
            return this._getTop(screen, this._cyclicStep);
        } else if (side === 'right') {
            return this._getRight(screen);
        } else if (side === 'bottom') {
            return this._getBottom(screen, this._cyclicStep);
        }
        return null;
    };
    ScreenManager.prototype._setRelativeScreen = function(side, baseScreen, targetScreen, isCheckSave, reverse) {
        if (!(targetScreen instanceof Screen)) {
            throw new Errors.ArgumentError('targetScreen', targetScreen);
        }
        if (!(baseScreen instanceof Screen)) {
            throw new Errors.ArgumentError('baseScreen', baseScreen);
        }

        var savePrevious = isCheckSave ? this._savePrevious : true;

        if (side === (reverse ? 'left' : 'right') && savePrevious) {
            var childIndex = baseScreen.getChildIndex(targetScreen);
            if (childIndex !== -1) {
                baseScreen.defaultChildIndex(childIndex);
            } else {
                throw new Errors.FatalError('Base screen is ' + baseScreen.toString() + '. Child screen not found: ', targetScreen.toString());
            }
        }
        if (side === (reverse ? 'right' : 'left') && savePrevious) {
            var parentIndex = baseScreen.getParentIndex(targetScreen);
            if (parentIndex !== -1) {
                baseScreen.defaultParentIndex(parentIndex);
            } else {
                throw new Errors.FatalError('Base screen is ' + baseScreen.toString() + '. Parent screen not found: ', targetScreen.toString());
            }
        }
    };
    ScreenManager.prototype._updateRelativeScreen = function(screen) {
        if (!(screen instanceof Screen)) {
            throw new Errors.ArgumentError('screen', screen);
        }

        if (!this._getRight(screen)) {
            screen.defaultChildIndex(0);
        }
        if (!this._getLeft(screen)) {
            screen.defaultParentIndex(0);
        }
    };

    /**
     * Возвращает модель текущей ячейки панели.
     * @returns {ScreenModel|null} модель текущей ячейки панели
     * @memberOf ScreenManager
     */
    ScreenManager.prototype.getCurScreen = function() {
        return this._curScreen;
    };
    /**
     * Возвращает модель, располагающуюся рядом с текущей ячейкой панели.
     * @param {string} side - сторона относительно текущей ячейки
     * @returns {ScreenModel|null} модель текущей ячейки панели
     * @memberOf ScreenManager
     */
    ScreenManager.prototype.getRelativeScreen = function(side) {
        return this._getRelativeScreenByScreen(this._curScreen, side);
    };
    /**
     * Очистить историю перемещений в панели.
     * @memberOf ScreenManager
     */
    ScreenManager.prototype.clearHistory = function() {
        this._history = [];
    };
    /**
     * Удаляет из истории перемещений последнее удачное перемещение в панели и возвращает модель, которая располагается в последней посещенной ячейке из истории.
     * @returns {ScreenModel} Модель из последнего удачного перемещения в истории
     * @memberOf ScreenManager
     */
    ScreenManager.prototype.popHistory = function() {
        var res = this._history.pop();
        if (res) {
            this._lastSide = res.lastSide;
            this._lastScreen = res.lastScreen;
        } else {
            this._lastSide = null;
            this._lastScreen = null;
        }
        return res;
    };
    ScreenManager.prototype._containsHistory = function(screen) {
        return this._history.some(function(val) {
            return val.screen === screen;
        });
    };

    /**
     * Поиск кратчайшего пути от одной модели до другой. Если для разных ячеек панели используются одинаковые модели, результат непредсказуем.
     * @param {ScreenModel} start - Модель, от которой начинает поиск пути
     * @param {ScreenModel} end - Конечная модель, в которую ищется путь
     * @returns {null|ScreenModel[]} Путь от начальной модели до конечной модели в панели
     * @memberOf ScreenManager
     */
    ScreenManager.prototype.findShortestPath = function (start, end) {
        function findPaths(start, end) {
            var costs = {},
                open = {'0': [start]},
                predecessors = {},
                keys;

            var addToOpen = function (cost, vertex) {
                if (!open[cost]) open[cost] = [];
                open[cost].push(vertex);
            };

            costs[start] = 0;

            while (open) {
                if(!(keys = Object.keys(open)).length) break;

                keys.sort(function (a, b) {
                    return a - b;
                });

                var key = keys[0],
                    bucket = open[key],
                    node = bucket.shift(),
                    currentCost = +key,
                    adjacentNodes;

                if (self._isDirectPath) {
                    adjacentNodes = node && node._children
                            .concat(node._parents)// todo если оставить только children будет настоящий поиск в графе
                            .concat(self._getRelativeScreenByScreen(node, 'top'))
                            .concat(self._getRelativeScreenByScreen(node, 'bottom')) || [];
                } else {
                    adjacentNodes = node && [self._getRelativeScreenByScreen(node, 'left'),
                            self._getRelativeScreenByScreen(node, 'top'),
                            self._getRelativeScreenByScreen(node, 'right'),
                            self._getRelativeScreenByScreen(node, 'bottom')
                        ] || [];
                }

                if (!bucket.length) delete open[key];

                for (var i = 0; i < adjacentNodes.length; i++) {
                    var vertex = adjacentNodes[i],
                        totalCost = currentCost + 1,
                        vertexCost = costs[vertex];

                    if ((vertexCost === undefined) || (vertexCost > totalCost)) {
                        costs[vertex] = totalCost;
                        addToOpen(totalCost, vertex);
                        predecessors[vertex] = node;
                    }
                }
            }

            if (costs[end] === undefined) {
                return null;
            } else {
                return predecessors;
            }
        }
        function extractShortest(predecessors, end) {
            var nodes = [],
                u = end;

            while (u) {
                nodes.push(u);
                u = predecessors[u];
            }

            nodes.reverse();
            return nodes;
        }

        var self = this;
        var predecessors = findPaths(start, end);
        return !predecessors ? null : extractShortest(predecessors, end);
    };

    /**
     * Уничтожить ScreenManager
     * @memberOf ScreenManager
     */
    ScreenManager.prototype.destroy = function() {
        Screen.unregisterUpdateFn(this._relativeUpdateFn);
        this._history = null;
        this._curScreen = null;
    };

    return ScreenManager;
});