<commit_before>import { BrowserRouter as Router, Route, Redirect, Switch } from 'react-router-dom';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as Actions from '../actions';
import LoginDialog from '../components/LoginDialog';
import RecordsList from '../components/RecordsList';
import RecordForm from '../components/RecordForm';
import Budgets from '../components/Budgets';
import Spinner from '../components/Spinner';
import AddRecordButton from '../components/AddRecordButton';
import RecordModel from '../models/Record';
import './../App.css';

import NavigationHeader from '../components/NavigationHeader';

class App extends Component {
    static propTypes = {
        actions: PropTypes.object.isRequired,
        auth: PropTypes.object.isRequired,
        records: PropTypes.object.isRequired,
    };

    componentDidMount() {
        if (this.isAuthenticated()) {
            this.props.actions.loadDataForRecordsPage();
            this.props.actions.loadDataForBudgetsPage();
        } else {
            this.props.actions.openAuthDialog();
        }
    }

    isAuthenticated() {
        return null !== this.props.auth.token;
    }

    render() {
        const props = this.props;
        const actions = props.actions;
        return (
            <Router>
                <React.Fragment>
                    <NavigationHeader />
                    {/* redirect to "/records" page by default */}
                    <Route exact path="/" render={() => <Redirect to="/records" />} />

                        <Route
                            exact
                            path="/records"
                            render={({ match, history }) => (
                                <RecordsList
                                    currentPage={props.records.currentPage}
                                    list={props.records.list}
                                    visitNextPage={actions.visitNextRecordsPage}
                                    visitPrevPage={actions.visitPrevRecordsPage}
                                    match={match}
                                    history={history}
                                />
                            )}
                        />
                        <Switch>
                            <Route path="/records/new" component={RecordForm} />
                            {props.records.list.length && (
                                <Route
                                    path="/records/:recordId"
                                    render={({ match }) => {
                                        const id = parseInt(match.params.recordId, 10);
                                        const data = props.records.list.find(r => r.id === id);
                                        const record = new RecordModel(data);
                                        return <RecordForm record={record} />;
                                    }}
                                />
                            )}
                        </Switch>
                        <Route
                            exact
                            path="/budgets"
                            render={() => <Budgets list={props.budgets.list} />}
                        />

                    <LoginDialog
                        authenticate={actions.authenticate}
                        auth={props.auth}
                        errors={props.auth.errors}
                        isOpen={props.auth.isDialogOpen}
                    />
                    <Spinner isVisible={props.spinner.isVisible} />
                    <Route path="/records" exact component={AddRecordButton} />
                </React.Fragment>
            </Router>
        );
    }
}

const mapStateToProps = state => {
    return {
        auth: state.auth,
        records: state.records,
        budgets: state.budgets,
        spinner: state.spinner,
    };
};

const mapDispatchToProps = dispatch => ({
    actions: bindActionCreators(Actions, dispatch),
});

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(App);
<commit_msg>use hash router<commit_after>import { HashRouter as Router, Route, Redirect, Switch } from 'react-router-dom';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as Actions from '../actions';
import LoginDialog from '../components/LoginDialog';
import RecordsList from '../components/RecordsList';
import RecordForm from '../components/RecordForm';
import Budgets from '../components/Budgets';
import Spinner from '../components/Spinner';
import AddRecordButton from '../components/AddRecordButton';
import RecordModel from '../models/Record';
import './../App.css';

import NavigationHeader from '../components/NavigationHeader';

class App extends Component {
    static propTypes = {
        actions: PropTypes.object.isRequired,
        auth: PropTypes.object.isRequired,
        records: PropTypes.object.isRequired,
    };

    componentDidMount() {
        if (this.isAuthenticated()) {
            this.props.actions.loadDataForRecordsPage();
            this.props.actions.loadDataForBudgetsPage();
        } else {
            this.props.actions.openAuthDialog();
        }
    }

    isAuthenticated() {
        return null !== this.props.auth.token;
    }

    render() {
        const props = this.props;
        const actions = props.actions;
        return (
            <Router>
                <React.Fragment>
                    <NavigationHeader />
                    {/* redirect to "/records" page by default */}
                    <Route exact path="/" render={() => <Redirect to="/records" />} />

                        <Route
                            exact
                            path="/records"
                            render={({ match, history }) => (
                                <RecordsList
                                    currentPage={props.records.currentPage}
                                    list={props.records.list}
                                    visitNextPage={actions.visitNextRecordsPage}
                                    visitPrevPage={actions.visitPrevRecordsPage}
                                    match={match}
                                    history={history}
                                />
                            )}
                        />
                        <Switch>
                            <Route path="/records/new" component={RecordForm} />
                            {props.records.list.length && (
                                <Route
                                    path="/records/:recordId"
                                    render={({ match }) => {
                                        const id = parseInt(match.params.recordId, 10);
                                        const data = props.records.list.find(r => r.id === id);
                                        const record = new RecordModel(data);
                                        return <RecordForm record={record} />;
                                    }}
                                />
                            )}
                        </Switch>
                        <Route
                            exact
                            path="/budgets"
                            render={() => <Budgets list={props.budgets.list} />}
                        />

                    <LoginDialog
                        authenticate={actions.authenticate}
                        auth={props.auth}
                        errors={props.auth.errors}
                        isOpen={props.auth.isDialogOpen}
                    />
                    <Spinner isVisible={props.spinner.isVisible} />
                    <Route path="/records" exact component={AddRecordButton} />
                </React.Fragment>
            </Router>
        );
    }
}

const mapStateToProps = state => {
    return {
        auth: state.auth,
        records: state.records,
        budgets: state.budgets,
        spinner: state.spinner,
    };
};

const mapDispatchToProps = dispatch => ({
    actions: bindActionCreators(Actions, dispatch),
});

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(App);
<|endoftext|>


<commit_before>/**
 * Retrieve a column from a matrix
 * @param {Matrix | Array} matrix
 * @param {number} index    Zero based column index
 * @return {Matrix | Array} Returns the column as a vector
 *
 * Taken from: https://github.com/josdejong/mathjs/issues/230
 */
function col(matrix, index) {
    var rows = math.size(matrix).valueOf()[0];
    return math.flatten(math.subset(matrix, math.index([0, rows], index)));
}

/**
 * Retrieve a row from a matrix
 * @param {Matrix | Array} matrix
 * @param {number} index    Zero based row index
 * @return {Matrix | Array} Returns the row as a vector
 *
 * Taken from: https://github.com/josdejong/mathjs/issues/230
 */
function row(matrix, index) {
    var rows = math.size(matrix).valueOf()[1];
    return math.flatten(math.subset(matrix, math.index(index, [0, rows])));
}

/**
 * A helper function for returning the largest index of a 1xN vector.
 *
 * @param {Matrix} 1xN vector, of which index with the greatest element will be returned
 */
function vectorArgmax(vector) {
    var max = 0;
    var argmax = -1;

    var arr = math.squeeze(vector);

    for (k = 0; k < arr.size()[0]; k++) {
        var curr = arr.get([k])
        if (curr > max) {
            max = curr;
            argmax = k;
        }
    }

    return argmax;
}

// Needs cleanup
function elementwiseMul(vector1, vector2) {
    var arr1 = math.squeeze(vector1);
    var arr2 = math.squeeze(vector2);

    var result;
    if (arr1.size()[0] != arr2.size()[0]) {
        throw "Vectors are not of the same shape!";
    } else {
        result = math.zeros(1, arr1.size()[0]);

        for (i = 0; i < arr1.size()[0]; i++) {
            result.set([0, i], vector1.get([i]) * vector2.get([i]));
        }

        return result
    }
}

/**
 * Computes forward probabilities for given parameters.
 * @param {Matrix} initialProbabilities          initial state probabilities for the HMM (1xN)
 * @param {Matrix} transitionProbabilities       state transition probabilities (NxN)
 * @param {HashTable} observationProbabilities   a hash table that contains a vector (1xN) that contains the observation probabilities
 *                                               per state for each observation symbol
 * @param {Array} observations                   an array of observations of which probability will be calculated according to the model
 * @returns {*[]}                                an array containing alpha values and their computations
 */
function forward(initialProbabilities, transitionProbabilities, observationProbabilities, observations) {
    var numStates = initialProbabilities.size()[1];
    var alpha = math.zeros(observations.length, numStates);

    // Contains computation strings
    var alphaComputations = math.matrix([observations.length, numStates]);
    var arrows = math.matrix([observations.length, numStates]);

    // Fill the initial conditions
    for (i = 0; i < numStates; i++) {
        alpha.set([0, i], initialProbabilities.get([0, i]) * observationProbabilities[observations[0]].get([0, i]));

        var tempStr = 'initial probability for state ' + i + ' * observation probability for \'' + observations[i] + '\' at state ' + i + '\n';
        tempStr += initialProbabilities.get([0, i]) + '*' + observationProbabilities[observations[0]].get([0, i]);
        alphaComputations.set([0, i], tempStr);
    }

    // Recursion step
    // TODO: Cleanup, we iterate multiple times for the same thing
    for (t = 1; t < observations.length; t++) {
        for (j = 0; j < numStates; j++) {
            alpha.set([t, j], math.multiply(row(alpha, t - 1), col(transitionProbabilities, j)) * observationProbabilities[observations[t]].get([0, j]));

            var tempStr = '( alpha_' + t + ' . probabilities of going to state ' + j + ' ) * observation probability for \'' + observations[t] + '\' at alpha_' + (t + 1) + '\n';
            tempStr += '( ' + math.string(row(alpha, t - 1)) + ' . ' + math.string(col(transitionProbabilities, j)) + ' ) * ' + observationProbabilities[observations[t]].get([0, j])
            alphaComputations.set([t, j], tempStr);

            tempStr = '';
            for (k = 0; k < numStates; k++) {
                tempStr += alpha.get([t - 1, k]) + ' * ' + transitionProbabilities.get([k, j]) + ',';
            }

            tempStr = tempStr.substr(0, tempStr.length - 1);

            arrows.set([t, j], tempStr);
        }
    }

    return [alpha, alphaComputations, arrows]
}

/**
 * Runs Viterbi algorithm and decodes most likely state sequence for a given set of observations.
 * @param {Matrix} initialProbabilities          initial state probabilities for the HMM (1xN)
 * @param {Matrix} transitionProbabilities       state transition probabilities (NxN)
 * @param {HashTable} observationProbabilities   a hash table that contains a vector (1xN) that contains the observation probabilities
 *                                               per state for each observation symbol
 * @param {Array} observations                   an array of observations of which probability will be calculated according to the model
 * @returns {*[]}                                an array containing best state sequence, its probability, delta and phi values.
 */
function viterbi(initialProbabilities, transitionProbabilities, observationProbabilities, observations) {
    var numStates = initialProbabilities.size()[1];

    var delta = math.zeros(observations.length, numStates);
    var phi = math.zeros(observations.length, numStates);

    // Fill the initial conditions
    for (i = 0; i < numStates; i++) {
        delta.set([0, i], initialProbabilities.get([0, i]) * observationProbabilities[observations[0]].get([0, i]));
        phi.set([0, i], 0)
    }

    /*
     Recursion step
     */
    for (t = 1; t < observations.length; t++) {
        for (j = 0; j < numStates; j++) {
            var multiplication = elementwiseMul(row(delta, t - 1), col(transitionProbabilities, j));

            // Find the argmax
            var argmax = vectorArgmax(multiplication);

            phi.set([t, j], argmax);
            delta.set([t, j], multiplication.get([0, argmax]) * observationProbabilities[observations[t]].get([0, j]))
        }
    }

    /*
     Decoding step.
     */
    var stateSequence = math.zeros(observations.length, 1);

    // Set the last element to the most likely value
    var mostLikelyState = vectorArgmax(row(delta, observations.length - 1));
    stateSequence.set([observations.length - 1, 0], mostLikelyState);

    // Start decoding backwards
    for (t = observations.length - 2; t >= 0; t--) {
        var prevState = stateSequence.get([t + 1, 0]);
        stateSequence.set([t, 0], phi.get([t + 1, prevState]));
    }

    var bestStateSequenceProbability = delta.get([observations.length - 1, mostLikelyState]);

    return [stateSequence, bestStateSequenceProbability, delta, phi]
}

/**
 * Creates flat-start probabilities for the UI.
 *
 * Sample usage: getFlatStartProbabilities(5, ['a', 'b', 'c', 'd', 'e']);
 * @param numStates             number of states in the HMM
 * @param observationAlphabet   an array containing the set of observations that can be encountered
 * @returns {*[]}               an array containing initial state probabilities, state transition probabilities and observation probabilities
 */
function getFlatStartProbabilities(numStates, observationAlphabet) {
    var initialProbabilities = math.zeros(1, numStates);
    var transitionProbabilities = math.zeros(numStates, numStates);
    var observationProbabilities = new Object();

    var stateFlatStart = 1.0 / numStates;

    for (i = 0; i < numStates; i++) {
        initialProbabilities.set([0, i], stateFlatStart);

        for (j = 0; j < numStates; j++) {
            transitionProbabilities.set([i, j], stateFlatStart);
        }
    }

    observationAlphabet.forEach(function (elem) {
        observationProbabilities[elem] = initialProbabilities.clone();
    });

    return [initialProbabilities, transitionProbabilities, observationProbabilities];
}

/**
 * Creates random probabilities for the UI.
 *
 * Sample usage: getFlatStartProbabilities(5, ['a', 'b', 'c', 'd', 'e']);
 * @param numStates             number of states in the HMM
 * @param observationAlphabet   an array containing the set of observations that can be encountered
 * @returns {*[]}               an array containing initial state probabilities, state transition probabilities and observation probabilities
 */
function getRandomStartProbabilities(numStates, observationAlphabet) {
    var initialProbabilities = math.zeros(1, numStates);
    var transitionProbabilities = math.zeros(numStates, numStates);
    var observationProbabilities = new Object();

    var initialProbabilitiesNormalization = 0.0;

    for (i = 0; i < numStates; i++) {
        initialProbabilities.set([0, i], math.random(0, 1));

        for (j = 0; j < numStates; j++) {
            transitionProbabilities.set([i, j], math.random(0, 1));
        }
    }

    observationAlphabet.forEach(function (elem) {
        var probabilities = math.zeros(1, numStates);

        for (i = 0; i < numStates; i++) {
            probabilities.set([0, i], math.random(0, 1));
        }

        observationProbabilities[elem] = probabilities;
    });

    /*
     Normalization
     */
    var initialProbNorm = math.sum(initialProbabilities);
    initialProbabilities = math.divide(initialProbabilities, initialProbNorm)

    // Maybe there is a better way of doing this.
    for (i = 0; i < numStates; i++) {
        var transProbNorm = math.sum(row(transitionProbabilities, i));
        for (j = 0; j < numStates; j++) {
            transitionProbabilities.set([i, j], transitionProbabilities.get([i, j]) / transProbNorm);
        }
    }

    observationAlphabet.forEach(function (elem) {
        var obsProbNorm = math.sum(observationProbabilities[elem]);
        observationProbabilities[elem] = math.divide(observationProbabilities[elem], obsProbNorm);
    });

    return [initialProbabilities, transitionProbabilities, observationProbabilities];
}<commit_msg>Added deltaComputations and arrows for Viterbi.<commit_after>/**
 * Retrieve a column from a matrix
 * @param {Matrix | Array} matrix
 * @param {number} index    Zero based column index
 * @return {Matrix | Array} Returns the column as a vector
 *
 * Taken from: https://github.com/josdejong/mathjs/issues/230
 */
function col(matrix, index) {
    var rows = math.size(matrix).valueOf()[0];
    return math.flatten(math.subset(matrix, math.index([0, rows], index)));
}

/**
 * Retrieve a row from a matrix
 * @param {Matrix | Array} matrix
 * @param {number} index    Zero based row index
 * @return {Matrix | Array} Returns the row as a vector
 *
 * Taken from: https://github.com/josdejong/mathjs/issues/230
 */
function row(matrix, index) {
    var rows = math.size(matrix).valueOf()[1];
    return math.flatten(math.subset(matrix, math.index(index, [0, rows])));
}

/**
 * A helper function for returning the largest index of a 1xN vector.
 *
 * @param {Matrix} 1xN vector, of which index with the greatest element will be returned
 */
function vectorArgmax(vector) {
    var max = 0;
    var argmax = -1;

    var arr = math.squeeze(vector);

    for (k = 0; k < arr.size()[0]; k++) {
        var curr = arr.get([k])
        if (curr > max) {
            max = curr;
            argmax = k;
        }
    }

    return argmax;
}

// Needs cleanup
function elementwiseMul(vector1, vector2) {
    var arr1 = math.squeeze(vector1);
    var arr2 = math.squeeze(vector2);

    var result;
    if (arr1.size()[0] != arr2.size()[0]) {
        throw "Vectors are not of the same shape!";
    } else {
        result = math.zeros(1, arr1.size()[0]);

        for (i = 0; i < arr1.size()[0]; i++) {
            result.set([0, i], vector1.get([i]) * vector2.get([i]));
        }

        return result
    }
}

/**
 * Computes forward probabilities for given parameters.
 * @param {Matrix} initialProbabilities          initial state probabilities for the HMM (1xN)
 * @param {Matrix} transitionProbabilities       state transition probabilities (NxN)
 * @param {HashTable} observationProbabilities   a hash table that contains a vector (1xN) that contains the observation probabilities
 *                                               per state for each observation symbol
 * @param {Array} observations                   an array of observations of which probability will be calculated according to the model
 * @returns {*[]}                                an array containing alpha values and their computations
 */
function forward(initialProbabilities, transitionProbabilities, observationProbabilities, observations) {
    var numStates = initialProbabilities.size()[1];
    var alpha = math.zeros(observations.length, numStates);

    // Contains computation strings
    var alphaComputations = math.matrix([observations.length, numStates]);
    var arrows = math.matrix([observations.length, numStates]);

    // Fill the initial conditions
    for (i = 0; i < numStates; i++) {
        alpha.set([0, i], initialProbabilities.get([0, i]) * observationProbabilities[observations[0]].get([0, i]));

        var tempStr = 'initial probability for state ' + i + ' * observation probability for \'' + observations[i] + '\' at state ' + i + '\n';
        tempStr += initialProbabilities.get([0, i]) + '*' + observationProbabilities[observations[0]].get([0, i]);
        alphaComputations.set([0, i], tempStr);
    }

    // Recursion step
    // TODO: Cleanup, we iterate multiple times for the same thing
    for (t = 1; t < observations.length; t++) {
        for (j = 0; j < numStates; j++) {
            alpha.set([t, j], math.multiply(row(alpha, t - 1), col(transitionProbabilities, j)) * observationProbabilities[observations[t]].get([0, j]));

            var tempStr = '( alpha_' + t + ' . probabilities of going to state ' + j + ' ) * observation probability for \'' + observations[t] + '\' at alpha_' + (t + 1) + '\n';
            tempStr += '( ' + math.string(row(alpha, t - 1)) + ' . ' + math.string(col(transitionProbabilities, j)) + ' ) * ' + observationProbabilities[observations[t]].get([0, j])
            alphaComputations.set([t, j], tempStr);

            tempStr = '';
            for (k = 0; k < numStates; k++) {
                tempStr += alpha.get([t - 1, k]) + ' * ' + transitionProbabilities.get([k, j]) + ', ';
            }

            tempStr = tempStr.substr(0, tempStr.length - 2);

            arrows.set([t, j], tempStr);
        }
    }

    return [alpha, alphaComputations, arrows]
}

/**
 * Runs Viterbi algorithm and decodes most likely state sequence for a given set of observations.
 * @param {Matrix} initialProbabilities          initial state probabilities for the HMM (1xN)
 * @param {Matrix} transitionProbabilities       state transition probabilities (NxN)
 * @param {HashTable} observationProbabilities   a hash table that contains a vector (1xN) that contains the observation probabilities
 *                                               per state for each observation symbol
 * @param {Array} observations                   an array of observations of which probability will be calculated according to the model
 * @returns {*[]}                                an array containing best state sequence, its probability, delta and phi values.
 */
function viterbi(initialProbabilities, transitionProbabilities, observationProbabilities, observations) {
    var numStates = initialProbabilities.size()[1];

    var delta = math.zeros(observations.length, numStates);
    var phi = math.zeros(observations.length, numStates);

    var deltaComputations = math.matrix([observations.length, numStates]);
    var arrows = math.matrix([observations.length, numStates]);

    // Fill the initial conditions
    for (i = 0; i < numStates; i++) {
        delta.set([0, i], initialProbabilities.get([0, i]) * observationProbabilities[observations[0]].get([0, i]));
        phi.set([0, i], 0);

        var tempStr = 'initial probability for state ' + i + ' * observation probability for \'' + observations[i] + '\' at state ' + i + '\n';
        tempStr += initialProbabilities.get([0, i]) + '*' + observationProbabilities[observations[0]].get([0, i]);
        deltaComputations.set([0, i], tempStr);
    }

    /*
     Recursion step
     */
    // TODO: Cleanup, we iterate multiple times for the same thing
    for (t = 1; t < observations.length; t++) {
        for (j = 0; j < numStates; j++) {
            var multiplication = elementwiseMul(row(delta, t - 1), col(transitionProbabilities, j));

            // Find the argmax
            var argmax = vectorArgmax(multiplication);

            phi.set([t, j], argmax);
            delta.set([t, j], multiplication.get([0, argmax]) * observationProbabilities[observations[t]].get([0, j]));

            var tempStr = 'max ( elementwise multiplication of delta_' + (t - 1) + ' and probabilities of going to state ' + j + ' )\n';
            var arrowStr = '';

            for (k = 0; k < numStates; k++) {
                arrowStr += delta.get([t - 1, k]) + ' * ' + transitionProbabilities.get([k, j]) + ', ';
            }
            arrowStr = arrowStr.substr(0, arrowStr.length - 2);

            tempStr += 'max( ' + arrowStr + ' )';

            deltaComputations.set([t, j], tempStr);
            arrows.set([t, j], arrowStr);
        }
    }

    /*
     Decoding step.
     */
    var stateSequence = math.zeros(observations.length, 1);

    // Set the last element to the most likely value
    var mostLikelyState = vectorArgmax(row(delta, observations.length - 1));
    stateSequence.set([observations.length - 1, 0], mostLikelyState);

    // Start decoding backwards
    for (t = observations.length - 2; t >= 0; t--) {
        var prevState = stateSequence.get([t + 1, 0]);
        stateSequence.set([t, 0], phi.get([t + 1, prevState]));
    }

    var bestStateSequenceProbability = delta.get([observations.length - 1, mostLikelyState]);

    return [stateSequence, bestStateSequenceProbability, delta, phi, deltaComputations, arrows]
}

/**
 * Creates flat-start probabilities for the UI.
 *
 * Sample usage: getFlatStartProbabilities(5, ['a', 'b', 'c', 'd', 'e']);
 * @param numStates             number of states in the HMM
 * @param observationAlphabet   an array containing the set of observations that can be encountered
 * @returns {*[]}               an array containing initial state probabilities, state transition probabilities and observation probabilities
 */
function getFlatStartProbabilities(numStates, observationAlphabet) {
    var initialProbabilities = math.zeros(1, numStates);
    var transitionProbabilities = math.zeros(numStates, numStates);
    var observationProbabilities = new Object();

    var stateFlatStart = 1.0 / numStates;

    for (i = 0; i < numStates; i++) {
        initialProbabilities.set([0, i], stateFlatStart);

        for (j = 0; j < numStates; j++) {
            transitionProbabilities.set([i, j], stateFlatStart);
        }
    }

    observationAlphabet.forEach(function (elem) {
        observationProbabilities[elem] = initialProbabilities.clone();
    });

    return [initialProbabilities, transitionProbabilities, observationProbabilities];
}

/**
 * Creates random probabilities for the UI.
 *
 * Sample usage: getFlatStartProbabilities(5, ['a', 'b', 'c', 'd', 'e']);
 * @param numStates             number of states in the HMM
 * @param observationAlphabet   an array containing the set of observations that can be encountered
 * @returns {*[]}               an array containing initial state probabilities, state transition probabilities and observation probabilities
 */
function getRandomStartProbabilities(numStates, observationAlphabet) {
    var initialProbabilities = math.zeros(1, numStates);
    var transitionProbabilities = math.zeros(numStates, numStates);
    var observationProbabilities = new Object();

    var initialProbabilitiesNormalization = 0.0;

    for (i = 0; i < numStates; i++) {
        initialProbabilities.set([0, i], math.random(0, 1));

        for (j = 0; j < numStates; j++) {
            transitionProbabilities.set([i, j], math.random(0, 1));
        }
    }

    observationAlphabet.forEach(function (elem) {
        var probabilities = math.zeros(1, numStates);

        for (i = 0; i < numStates; i++) {
            probabilities.set([0, i], math.random(0, 1));
        }

        observationProbabilities[elem] = probabilities;
    });

    /*
     Normalization
     */
    var initialProbNorm = math.sum(initialProbabilities);
    initialProbabilities = math.divide(initialProbabilities, initialProbNorm)

    // Maybe there is a better way of doing this.
    for (i = 0; i < numStates; i++) {
        var transProbNorm = math.sum(row(transitionProbabilities, i));
        for (j = 0; j < numStates; j++) {
            transitionProbabilities.set([i, j], transitionProbabilities.get([i, j]) / transProbNorm);
        }
    }

    observationAlphabet.forEach(function (elem) {
        var obsProbNorm = math.sum(observationProbabilities[elem]);
        observationProbabilities[elem] = math.divide(observationProbabilities[elem], obsProbNorm);
    });

    return [initialProbabilities, transitionProbabilities, observationProbabilities];
}<|endoftext|>


<commit_before>/**
 * BananaJs
 * Get an RGB color palette from a set of keywords
 *
 * (c) 2015-2020 Alejandro U. Alvarez
 * Bananajs may be freely distributed under the MIT license.
 * For all details and documentation:
 * http://github.com/aurbano/bananajs
 *
 * @author Alejandro U. Alvarez <urbanoalvarez.es>
 * @license MIT License
 */
"use strict";

var Bananajs = function(options) {

  var Banana = this;

  if (typeof(options.googleApi) === "undefined") {
    throw {
      message: "Error: Bananajs: The Google API was not passed, please pass {googleApi: google}"
    };
  }

  if (typeof(ColorThief) === "undefined") {
    throw {
      message: "Error: Bananajs: Please include ColorThief"
    };
  }

  Banana.thief = new ColorThief();
  // Banana settings
  Banana.options = {
    debug: options.debug || false, // debug on/off
    quality: options.quality || 10, // quality setting for Color Thief
    minImages: options.minImages || 1, // minimum number of images required for sampling
    images: options.images || 4, // initial number of images requested to google
    maxImages: options.maxImages || 4 // maximum number of images before giving up
  };

  var log = function() {
    if (options.debug) console.log.apply(console, arguments);
  };

  options.googleApi.load("search", "1");

  function getImageColor(url, number) {
    return new Promise(function(resolve) {
      var img = new Image();
      img.crossOrigin = "";
      img.onload = function() {
        try {
          resolve({
            color: Banana.thief.getColor(img, options.quality || 10),
            palette: Banana.thief.getPalette(img, number)
          });
        } catch (e) {
          log("Bananajs: Image download failed");
          resolve(null);
        }
      };
      img.onerror = function() {
        resolve(null);
      };
      img.src = url;
    });
  }

  function getRGBData(colorName, number) {
    return new Promise(function(resolve, reject) {
      // Create an Image Search instance.
      var imageSearch = new options.googleApi.search.ImageSearch();

      // Set searchComplete as the callback function when a search is
      // complete.  The imageSearch object will have results in it.
      imageSearch.setSearchCompleteCallback(this, function() {
        var images = imageSearch.results;
        if (images.length < 1) {
          log("Bananajs: No images found");
          reject({
            message: "No results"
          });
        }

        var promises = [];

        images.forEach(function(image) {
          log("Bananajs: Loading image: ", image.url);
          promises.push(getImageColor(image.url, number));
        });

        Promise.all(promises).then(function(data) {
          resolve(data);
        });
      }, null);

      // Find me a beautiful car.
      imageSearch.execute(colorName);
    });
  }

  function getColor(colorName, resolve) {
    getRGBData(colorName, 0).then(function(data) {
      var r = 0,
        g = 0,
        b = 0,
        count = 0;
      data.forEach(function(info) {
        if (info !== null) {
          count++;
          r += info.color[0];
          g += info.color[1];
          b += info.color[2];
        }
      });
      r /= count;
      g /= count;
      b /= count;
      r = Math.round(r);
      g = Math.round(g);
      b = Math.round(b);

      log("Bananajs: Finished processing, color: ", r, g, b);

      resolve([r, g, b]);
    });
  }

  function getPalette(colorName, number, resolve) {
    getRGBData(colorName, number).then(function(data) {
      var palette = [],
        count = 0,
        i;
      for (i = 0; i < number; i++) {
        palette[i] = [0, 0, 0];
      }
      data.forEach(function(info) {
        if (info !== null && info.palette.length > 0) {
          count++;
          for (i = 0; i < info.palette.length; i++) {
            palette[i][0] += info.palette[i][0];
            palette[i][1] += info.palette[i][1];
            palette[i][2] += info.palette[i][2];
          }
        }
      });
      for (i = 0; i < palette.length; i++) {
        palette[i][0] = Math.round(palette[i][0] / count);
        palette[i][1] = Math.round(palette[i][1] / count);
        palette[i][2] = Math.round(palette[i][2] / count);
      }

      log("Bananajs: Finished processing, palette: ", palette);

      resolve(palette);
    });
  }

  // Expose API
  Banana.getColor = function(colorName) {
    log("Bananajs: Processing color for '" + colorName + "'");
    return new Promise(function(resolve, reject) {
      getColor(colorName, resolve, reject);
    });
  };

  Banana.getPalette = function(colorName, number) {
    log("Bananajs: Processing palette for '" + colorName + "'");
    return new Promise(function(resolve, reject) {
      getPalette(colorName, number, resolve, reject);
    });
  };
};
<commit_msg>improve algorithm, add options<commit_after>/**
 * BananaJs
 * Get an RGB color palette from a set of keywords
 *
 * (c) 2015-2020 Alejandro U. Alvarez
 * Bananajs may be freely distributed under the MIT license.
 * For all details and documentation:
 * http://github.com/aurbano/bananajs
 *
 * @author Alejandro U. Alvarez <urbanoalvarez.es>
 * @license MIT License
 */
"use strict";

var Bananajs = function(options) {

  var Banana = this;

  if (typeof(options.googleApi) === "undefined") {
    throw {
      message: "Error: Bananajs: The Google API was not passed, please pass {googleApi: google}"
    };
  }

  if (typeof(ColorThief) === "undefined") {
    throw {
      message: "Error: Bananajs: Please include ColorThief"
    };
  }

  Banana.thief = new ColorThief();
  // Banana settings
  Banana.options = {
    debug: options.debug || false, // debug on/off
    quality: options.quality || 10, // quality setting for Color Thief
    minImages: options.minImages || 1, // minimum number of images required for sampling
    images: options.images || 10, // initial number of images requested to google
    maxImages: options.maxImages || 4 // maximum number of failed images before giving up
  };

  var log = function() {
    if (Banana.options.debug) console.log.apply(console, arguments);
  };

  options.googleApi.load("search", "1");

  function getImageColor(url, number) {
    return new Promise(function(resolve) {
      var img = new Image();
      img.crossOrigin = "";
      img.onload = function() {
        try {
          resolve({
            color: Banana.thief.getColor(img, Banana.options.quality || 10),
            palette: Banana.thief.getPalette(img, number)
          });
        } catch (e) {
          log("Bananajs: Image download failed");
          resolve(null);
        }
      };
      img.onerror = function(e) {
        log("Bananajs: Image download failed");
        resolve(null);
      };
      img.src = url;
    });
  }

  function getRGBData(colorName, number) {
    return new Promise(function(resolve, reject) {
      // Create an Image Search instance.
      var imageSearch = new options.googleApi.search.ImageSearch();

      imageSearch.setResultSetSize(Banana.options.images);

      // Set searchComplete as the callback function when a search is
      // complete.  The imageSearch object will have results in it.
      imageSearch.setSearchCompleteCallback(this, function() {
        var images = imageSearch.results;
        if (images.length < 1) {
          log("Bananajs: No images found");
          reject({
            message: "No results"
          });
        }

        var promises = [];

        images.forEach(function(image) {
          log("Bananajs: Loading image: ", image.url);
          promises.push(getImageColor(image.url, number));
        });

        Promise.all(promises).then(function(data) {
          resolve(data);
        });
      }, null);

      // Find me a beautiful car.
      imageSearch.execute(colorName);
    });
  }

  function getColor(colorName, resolve) {
    getRGBData(colorName, 0).then(function(data) {
      var r = 0,
        g = 0,
        b = 0,
        count = 0;
      data.forEach(function(info) {
        if (info !== null) {
          count++;
          r += info.color[0];
          g += info.color[1];
          b += info.color[2];
        }
      });
      r /= count;
      g /= count;
      b /= count;
      r = Math.round(r);
      g = Math.round(g);
      b = Math.round(b);

      log("Bananajs: Finished processing (sampled "+count+" images), color: ", r, g, b);

      resolve([r, g, b]);
    });
  }

  function getPalette(colorName, number, resolve) {
    getRGBData(colorName, number).then(function(data) {
      var palette = [],
        count = 0,
        i;
      for (i = 0; i < number; i++) {
        palette[i] = [0, 0, 0];
      }
      data.forEach(function(info) {
        if (info !== null && info.palette.length > 0) {
          count++;
          for (i = 0; i < info.palette.length; i++) {
            palette[i][0] += info.palette[i][0];
            palette[i][1] += info.palette[i][1];
            palette[i][2] += info.palette[i][2];
          }
        }
      });
      for (i = 0; i < palette.length; i++) {
        palette[i][0] = Math.round(palette[i][0] / count);
        palette[i][1] = Math.round(palette[i][1] / count);
        palette[i][2] = Math.round(palette[i][2] / count);
      }

      log("Bananajs: Finished processing (sampled "+count+" images), palette: ", palette);

      resolve(palette);
    });
  }

  // Expose API
  Banana.getColor = function(colorName) {
    log("Bananajs: Processing color for '" + colorName + "'");
    return new Promise(function(resolve, reject) {
      getColor(colorName, resolve, reject);
    });
  };

  Banana.getPalette = function(colorName, number) {
    log("Bananajs: Processing palette for '" + colorName + "'");
    return new Promise(function(resolve, reject) {
      getPalette(colorName, number, resolve, reject);
    });
  };
};
<|endoftext|>


<commit_before>/**
 * Baobab Data Structure
 * ======================
 *
 * A handy data tree with cursors.
 */
import Emitter from 'emmett';
import Cursor from './cursor';
import {MonkeyDefinition, Monkey} from './monkey';
import Watcher from './watcher';
import type from './type';
import update from './update';
import * as helpers from './helpers';

const {
  arrayFrom,
  coercePath,
  deepFreeze,
  getIn,
  makeError,
  deepMerge,
  shallowClone,
  shallowMerge,
  uniqid
} = helpers;

/**
 * Baobab defaults
 */
const DEFAULTS = {

  // Should the tree handle its transactions on its own?
  autoCommit: true,

  // Should the transactions be handled asynchronously?
  asynchronous: true,

  // Should the tree's data be immutable?
  immutable: true,

  // Should the monkeys be lazy?
  lazyMonkeys: true,

  // Should the tree be persistent?
  persistent: true,

  // Should the tree's update be pure?
  pure: true,

  // Validation specifications
  validate: null,

  // Validation behavior 'rollback' or 'notify'
  validationBehavior: 'rollback'
};

/**
 * Function returning a string hash from a non-dynamic path expressed as an
 * array.
 *
 * @param  {array}  path - The path to hash.
 * @return {string} string - The resultant hash.
 */
function hashPath(path) {
  return 'λ' + path.map(step => {
    if (type.function(step) || type.object(step))
      return `#${uniqid()}#`;

    return step;
  }).join('λ');
}

/**
 * Baobab class
 *
 * @constructor
 * @param {object|array} [initialData={}]    - Initial data passed to the tree.
 * @param {object}       [opts]              - Optional options.
 * @param {boolean}      [opts.autoCommit]   - Should the tree auto-commit?
 * @param {boolean}      [opts.asynchronous] - Should the tree's transactions
 *                                             handled asynchronously?
 * @param {boolean}      [opts.immutable]    - Should the tree be immutable?
 * @param {boolean}      [opts.persistent]   - Should the tree be persistent?
 * @param {boolean}      [opts.pure]         - Should the tree be pure?
 * @param {function}     [opts.validate]     - Validation function.
 * @param {string}       [opts.validationBehaviour] - "rollback" or "notify".
 */
export default class Baobab extends Emitter {
  constructor(initialData, opts) {
    super();

    // Setting initialData to an empty object if no data is provided by use
    if (arguments.length < 1)
      initialData = {};

    // Checking whether given initial data is valid
    if (!type.object(initialData) && !type.array(initialData))
      throw makeError('Baobab: invalid data.', {data: initialData});

    // Merging given options with defaults
    this.options = shallowMerge({}, DEFAULTS, opts);

    // Disabling immutability & persistence if persistence if disabled
    if (!this.options.persistent) {
      this.options.immutable = false;
      this.options.pure = false;
    }

    // Privates
    this._identity = '[object Baobab]';
    this._cursors = {};
    this._future = null;
    this._transaction = [];
    this._affectedPathsIndex = {};
    this._monkeys = {};
    this._previousData = null;
    this._data = initialData;

    // Properties
    this.root = new Cursor(this, [], 'λ');
    delete this.root.release;

    // Does the user want an immutable tree?
    if (this.options.immutable)
      deepFreeze(this._data);

    // Bootstrapping root cursor's getters and setters
    const bootstrap = (name) => {
      this[name] = function() {
        const r = this.root[name].apply(this.root, arguments);
        return r instanceof Cursor ? this : r;
      };
    };

    [
      'apply',
      'concat',
      'deepMerge',
      'exists',
      'get',
      'push',
      'merge',
      'project',
      'serialize',
      'set',
      'splice',
      'unset',
      'unshift'
    ].forEach(bootstrap);

    // Registering the initial monkeys
    this._refreshMonkeys();

    // Initial validation
    const validationError = this.validate();

    if (validationError)
      throw Error('Baobab: invalid data.', {error: validationError});
  }

  /**
   * Internal method used to refresh the tree's monkey register on every
   * update.
   * Note 1) For the time being, placing monkeys beneath array nodes is not
   * allowed for performance reasons.
   *
   * @param  {mixed}   node      - The starting node.
   * @param  {array}   path      - The starting node's path.
   * @param  {string}  operation - The operation that lead to a refreshment.
   * @return {Baobab}            - The tree instance for chaining purposes.
   */
  _refreshMonkeys(node, path, operation) {

    const clean = (data, p = []) => {
      if (data instanceof Monkey) {
        data.release();
        update(
          this._monkeys,
          p,
          {type: 'unset'},
          {
            immutable: false,
            persistent: false,
            pure: false
          }
        );

        return;
      }

      if (type.object(data)) {
        for (const k in data)
          clean(data[k], p.concat(k));
      }
    };

    const register = [];

    const walk = (data, p = []) => {

      // Should we sit a monkey in the tree?
      if (data instanceof MonkeyDefinition ||
          data instanceof Monkey) {
        const monkey = new Monkey(
          this,
          p,
          data instanceof Monkey ? data.definition : data
        );

        register.push(monkey);

        update(
          this._monkeys,
          p,
          {type: 'set', value: monkey},
          {
            immutable: false,
            persistent: false,
            pure: false
          }
        );

        return;
      }

      // Object iteration
      if (type.object(data)) {
        for (const k in data)
          walk(data[k], p.concat(k));
      }
    };

    // Walking the whole tree
    if (!arguments.length) {
      walk(this._data);
      register.forEach(m => m.checkRecursivity());
    }
    else {
      const monkeysNode = getIn(this._monkeys, path).data;

      // Is this required that we clean some already existing monkeys?
      if (monkeysNode)
        clean(monkeysNode, path);

      // Let's walk the tree only from the updated point
      if (operation !== 'unset') {
        walk(node, path);
        register.forEach(m => m.checkRecursivity());
      }
    }

    return this;
  }

  /**
   * Method used to validate the tree's data.
   *
   * @return {boolean} - Is the tree valid?
   */
  validate(affectedPaths) {
    const {validate, validationBehavior: behavior} = this.options;

    if (typeof validate !== 'function')
      return null;

    const error = validate.call(
      this,
      this._previousData,
      this._data,
      affectedPaths || [[]]
    );

    if (error instanceof Error) {

      if (behavior === 'rollback') {
        this._data = this._previousData;
        this._affectedPathsIndex = {};
        this._transaction = [];
        this._previousData = this._data;
      }

      this.emit('invalid', {error});

      return error;
    }

    return null;
  }

  /**
   * Method used to select data within the tree by creating a cursor. Cursors
   * are kept as singletons by the tree for performance and hygiene reasons.
   *
   * Arity (1):
   * @param {path}    path - Path to select in the tree.
   *
   * Arity (*):
   * @param {...step} path - Path to select in the tree.
   *
   * @return {Cursor}      - The resultant cursor.
   */
  select(path) {

    // If no path is given, we simply return the root
    path = path || [];

    // Variadic
    if (arguments.length > 1)
      path = arrayFrom(arguments);

    // Checking that given path is valid
    if (!type.path(path))
      throw makeError('Baobab.select: invalid path.', {path});

    // Casting to array
    path = [].concat(path);

    // Computing hash (done here because it would be too late to do it in the
    // cursor's constructor since we need to hit the cursors' index first).
    const hash = hashPath(path);

    // Creating a new cursor or returning the already existing one for the
    // requested path.
    let cursor = this._cursors[hash];

    if (!cursor) {
      cursor = new Cursor(this, path, hash);
      this._cursors[hash] = cursor;
    }

    // Emitting an event to notify that a part of the tree was selected
    this.emit('select', {path, cursor});
    return cursor;
  }

  /**
   * Method used to update the tree. Updates are simply expressed by a path,
   * dynamic or not, and an operation.
   *
   * This is where path solving should happen and not in the cursor.
   *
   * @param  {path}   path      - The path where we'll apply the operation.
   * @param  {object} operation - The operation to apply.
   * @return {mixed} - Return the result of the update.
   */
  update(path, operation) {

    // Coercing path
    path = coercePath(path);

    if (!type.operationType(operation.type))
      throw makeError(
        `Baobab.update: unknown operation type "${operation.type}".`,
        {operation: operation}
      );

    // Solving the given path
    const {solvedPath, exists} = getIn(
      this._data,
      path
    );

    // If we couldn't solve the path, we throw
    if (!solvedPath)
      throw makeError('Baobab.update: could not solve the given path.', {
        path: solvedPath
      });

    // Read-only path?
    const monkeyPath = type.monkeyPath(this._monkeys, solvedPath);
    if (monkeyPath && solvedPath.length > monkeyPath.length)
      throw makeError('Baobab.update: attempting to update a read-only path.', {
        path: solvedPath
      });

    // We don't unset irrelevant paths
    if (operation.type === 'unset' && !exists)
      return;

    // If we merge data, we need to acknowledge monkeys
    let realOperation = operation;
    if (/merge/.test(operation.type)) {
      const monkeysNode = getIn(this._monkeys, solvedPath).data;

      if (type.object(monkeysNode)) {
        realOperation = shallowClone(realOperation);

        if (/deep/.test(realOperation.type))
          realOperation.value = deepMerge({}, monkeysNode, realOperation.value);
        else
          realOperation.value = shallowMerge({}, monkeysNode, realOperation.value);
      }
    }

    // Stashing previous data if this is the frame's first update
    if (!this._transaction.length)
      this._previousData = this._data;

    // Applying the operation
    const result = update(
      this._data,
      solvedPath,
      realOperation,
      this.options
    );

    const {data, node} = result;

    // If because of purity, the update was moot, we stop here
    if (!('data' in result))
      return node;

    // If the operation is push, the affected path is slightly different
    const affectedPath = solvedPath.concat(
      operation.type === 'push' ? node.length - 1 : []
    );

    const hash = hashPath(affectedPath);

    // Updating data and transaction
    this._data = data;
    this._affectedPathsIndex[hash] = true;
    this._transaction.push({...operation, path: affectedPath});

    // Updating the monkeys
    this._refreshMonkeys(node, solvedPath, operation.type);

    // Emitting a `write` event
    this.emit('write', {path: affectedPath});

    // Should we let the user commit?
    if (!this.options.autoCommit)
      return node;

    // Should we update asynchronously?
    if (!this.options.asynchronous) {
      this.commit();
      return node;
    }

    // Updating asynchronously
    if (!this._future)
      this._future = setTimeout(() => this.commit(), 0);

    // Finally returning the affected node
    return node;
  }

  /**
   * Method committing the updates of the tree and firing the tree's events.
   *
   * @return {Baobab} - The tree instance for chaining purposes.
   */
  commit() {

    // Clearing timeout if one was defined
    if (this._future)
      this._future = clearTimeout(this._future);

    const affectedPaths = Object.keys(this._affectedPathsIndex).map(h => {
      return h !== 'λ' ?
        h.split('λ').slice(1) :
        [];
    });

    // Is the tree still valid?
    const validationError = this.validate(affectedPaths);

    if (validationError)
      return this;

    // Caching to keep original references before we change them
    const transaction = this._transaction,
          previousData = this._previousData;

    this._affectedPathsIndex = {};
    this._transaction = [];
    this._previousData = this._data;

    // Emitting update event
    this.emit('update', {
      paths: affectedPaths,
      currentData: this._data,
      transaction,
      previousData
    });

    return this;
  }

  /**
   * Method used to watch a collection of paths within the tree. Very useful
   * to bind UI components and such to the tree.
   *
   * @param  {object} mapping - Mapping of paths to listen.
   * @return {Cursor}         - The created watcher.
   */
  watch(mapping) {
    return new Watcher(this, mapping);
  }

  /**
   * Method releasing the tree and its attached data from memory.
   */
  release() {
    let k;

    this.emit('release');

    delete this.root;

    delete this._data;
    delete this._previousData;
    delete this._transaction;
    delete this._affectedPathsIndex;
    delete this._monkeys;

    // Releasing cursors
    for (k in this._cursors)
      this._cursors[k].release();
    delete this._cursors;

    // Killing event emitter
    this.kill();
  }

  /**
   * Overriding the `toJSON` method for convenient use with JSON.stringify.
   *
   * @return {mixed} - Data at cursor.
   */
  toJSON() {
    return this.serialize();
  }

  /**
   * Overriding the `toString` method for debugging purposes.
   *
   * @return {string} - The baobab's identity.
   */
  toString() {
    return this._identity;
  }
}

/**
 * Creating statics
 */
Baobab.monkey = function(...args) {

  if (!args.length)
    throw new Error('Baobab.monkey: missing definition.');

  if (args.length === 1)
    return new MonkeyDefinition(args[0]);

  return new MonkeyDefinition(args);
};
Baobab.dynamicNode = Baobab.monkey;

/**
 * Exposing some internals for convenience
 */
Baobab.Cursor = Cursor;
Baobab.MonkeyDefinition = MonkeyDefinition;
Baobab.Monkey = Monkey;
Baobab.type = type;
Baobab.helpers = helpers;

/**
 * Version
 */
Object.defineProperty(Baobab, 'version', {
  value: '2.1.1'
});

/**
 * Exporting
 */
export default Baobab;
<commit_msg>Linter compliance<commit_after>/**
 * Baobab Data Structure
 * ======================
 *
 * A handy data tree with cursors.
 */
import Emitter from 'emmett';
import Cursor from './cursor';
import {MonkeyDefinition, Monkey} from './monkey';
import Watcher from './watcher';
import type from './type';
import update from './update';
import * as helpers from './helpers';

const {
  arrayFrom,
  coercePath,
  deepFreeze,
  getIn,
  makeError,
  deepMerge,
  shallowClone,
  shallowMerge,
  uniqid
} = helpers;

/**
 * Baobab defaults
 */
const DEFAULTS = {

  // Should the tree handle its transactions on its own?
  autoCommit: true,

  // Should the transactions be handled asynchronously?
  asynchronous: true,

  // Should the tree's data be immutable?
  immutable: true,

  // Should the monkeys be lazy?
  lazyMonkeys: true,

  // Should the tree be persistent?
  persistent: true,

  // Should the tree's update be pure?
  pure: true,

  // Validation specifications
  validate: null,

  // Validation behavior 'rollback' or 'notify'
  validationBehavior: 'rollback'
};

/**
 * Function returning a string hash from a non-dynamic path expressed as an
 * array.
 *
 * @param  {array}  path - The path to hash.
 * @return {string} string - The resultant hash.
 */
function hashPath(path) {
  return 'λ' + path.map(step => {
    if (type.function(step) || type.object(step))
      return `#${uniqid()}#`;

    return step;
  }).join('λ');
}

/**
 * Baobab class
 *
 * @constructor
 * @param {object|array} [initialData={}]    - Initial data passed to the tree.
 * @param {object}       [opts]              - Optional options.
 * @param {boolean}      [opts.autoCommit]   - Should the tree auto-commit?
 * @param {boolean}      [opts.asynchronous] - Should the tree's transactions
 *                                             handled asynchronously?
 * @param {boolean}      [opts.immutable]    - Should the tree be immutable?
 * @param {boolean}      [opts.persistent]   - Should the tree be persistent?
 * @param {boolean}      [opts.pure]         - Should the tree be pure?
 * @param {function}     [opts.validate]     - Validation function.
 * @param {string}       [opts.validationBehaviour] - "rollback" or "notify".
 */
export default class Baobab extends Emitter {
  constructor(initialData, opts) {
    super();

    // Setting initialData to an empty object if no data is provided by use
    if (arguments.length < 1)
      initialData = {};

    // Checking whether given initial data is valid
    if (!type.object(initialData) && !type.array(initialData))
      throw makeError('Baobab: invalid data.', {data: initialData});

    // Merging given options with defaults
    this.options = shallowMerge({}, DEFAULTS, opts);

    // Disabling immutability & persistence if persistence if disabled
    if (!this.options.persistent) {
      this.options.immutable = false;
      this.options.pure = false;
    }

    // Privates
    this._identity = '[object Baobab]';
    this._cursors = {};
    this._future = null;
    this._transaction = [];
    this._affectedPathsIndex = {};
    this._monkeys = {};
    this._previousData = null;
    this._data = initialData;

    // Properties
    this.root = new Cursor(this, [], 'λ');
    delete this.root.release;

    // Does the user want an immutable tree?
    if (this.options.immutable)
      deepFreeze(this._data);

    // Bootstrapping root cursor's getters and setters
    const bootstrap = (name) => {
      this[name] = function() {
        const r = this.root[name].apply(this.root, arguments);
        return r instanceof Cursor ? this : r;
      };
    };

    [
      'apply',
      'concat',
      'deepMerge',
      'exists',
      'get',
      'push',
      'merge',
      'project',
      'serialize',
      'set',
      'splice',
      'unset',
      'unshift'
    ].forEach(bootstrap);

    // Registering the initial monkeys
    this._refreshMonkeys();

    // Initial validation
    const validationError = this.validate();

    if (validationError)
      throw Error('Baobab: invalid data.', {error: validationError});
  }

  /**
   * Internal method used to refresh the tree's monkey register on every
   * update.
   * Note 1) For the time being, placing monkeys beneath array nodes is not
   * allowed for performance reasons.
   *
   * @param  {mixed}   node      - The starting node.
   * @param  {array}   path      - The starting node's path.
   * @param  {string}  operation - The operation that lead to a refreshment.
   * @return {Baobab}            - The tree instance for chaining purposes.
   */
  _refreshMonkeys(node, path, operation) {

    const clean = (data, p = []) => {
      if (data instanceof Monkey) {
        data.release();
        update(this._monkeys, p, {type: 'unset'}, {
          immutable: false,
          persistent: false,
          pure: false
        });

        return;
      }

      if (type.object(data)) {
        for (const k in data)
          clean(data[k], p.concat(k));
      }
    };

    const register = [];

    const walk = (data, p = []) => {

      // Should we sit a monkey in the tree?
      if (data instanceof MonkeyDefinition ||
          data instanceof Monkey) {
        const monkey = new Monkey(
          this,
          p,
          data instanceof Monkey ? data.definition : data
        );

        register.push(monkey);

        update(this._monkeys, p, {type: 'set', value: monkey}, {
          immutable: false,
          persistent: false,
          pure: false
        });

        return;
      }

      // Object iteration
      if (type.object(data)) {
        for (const k in data)
          walk(data[k], p.concat(k));
      }
    };

    // Walking the whole tree
    if (!arguments.length) {
      walk(this._data);
      register.forEach(m => m.checkRecursivity());
    }
    else {
      const monkeysNode = getIn(this._monkeys, path).data;

      // Is this required that we clean some already existing monkeys?
      if (monkeysNode)
        clean(monkeysNode, path);

      // Let's walk the tree only from the updated point
      if (operation !== 'unset') {
        walk(node, path);
        register.forEach(m => m.checkRecursivity());
      }
    }

    return this;
  }

  /**
   * Method used to validate the tree's data.
   *
   * @return {boolean} - Is the tree valid?
   */
  validate(affectedPaths) {
    const {validate, validationBehavior: behavior} = this.options;

    if (typeof validate !== 'function')
      return null;

    const error = validate.call(
      this,
      this._previousData,
      this._data,
      affectedPaths || [[]]
    );

    if (error instanceof Error) {

      if (behavior === 'rollback') {
        this._data = this._previousData;
        this._affectedPathsIndex = {};
        this._transaction = [];
        this._previousData = this._data;
      }

      this.emit('invalid', {error});

      return error;
    }

    return null;
  }

  /**
   * Method used to select data within the tree by creating a cursor. Cursors
   * are kept as singletons by the tree for performance and hygiene reasons.
   *
   * Arity (1):
   * @param {path}    path - Path to select in the tree.
   *
   * Arity (*):
   * @param {...step} path - Path to select in the tree.
   *
   * @return {Cursor}      - The resultant cursor.
   */
  select(path) {

    // If no path is given, we simply return the root
    path = path || [];

    // Variadic
    if (arguments.length > 1)
      path = arrayFrom(arguments);

    // Checking that given path is valid
    if (!type.path(path))
      throw makeError('Baobab.select: invalid path.', {path});

    // Casting to array
    path = [].concat(path);

    // Computing hash (done here because it would be too late to do it in the
    // cursor's constructor since we need to hit the cursors' index first).
    const hash = hashPath(path);

    // Creating a new cursor or returning the already existing one for the
    // requested path.
    let cursor = this._cursors[hash];

    if (!cursor) {
      cursor = new Cursor(this, path, hash);
      this._cursors[hash] = cursor;
    }

    // Emitting an event to notify that a part of the tree was selected
    this.emit('select', {path, cursor});
    return cursor;
  }

  /**
   * Method used to update the tree. Updates are simply expressed by a path,
   * dynamic or not, and an operation.
   *
   * This is where path solving should happen and not in the cursor.
   *
   * @param  {path}   path      - The path where we'll apply the operation.
   * @param  {object} operation - The operation to apply.
   * @return {mixed} - Return the result of the update.
   */
  update(path, operation) {

    // Coercing path
    path = coercePath(path);

    if (!type.operationType(operation.type))
      throw makeError(
        `Baobab.update: unknown operation type "${operation.type}".`,
        {operation: operation}
      );

    // Solving the given path
    const {solvedPath, exists} = getIn(
      this._data,
      path
    );

    // If we couldn't solve the path, we throw
    if (!solvedPath)
      throw makeError('Baobab.update: could not solve the given path.', {
        path: solvedPath
      });

    // Read-only path?
    const monkeyPath = type.monkeyPath(this._monkeys, solvedPath);
    if (monkeyPath && solvedPath.length > monkeyPath.length)
      throw makeError('Baobab.update: attempting to update a read-only path.', {
        path: solvedPath
      });

    // We don't unset irrelevant paths
    if (operation.type === 'unset' && !exists)
      return;

    // If we merge data, we need to acknowledge monkeys
    let realOperation = operation;
    if (/merge/.test(operation.type)) {
      const monkeysNode = getIn(this._monkeys, solvedPath).data;

      if (type.object(monkeysNode)) {
        realOperation = shallowClone(realOperation);

        if (/deep/.test(realOperation.type))
          realOperation.value = deepMerge({}, monkeysNode, realOperation.value);
        else
          realOperation.value = shallowMerge({}, monkeysNode, realOperation.value);
      }
    }

    // Stashing previous data if this is the frame's first update
    if (!this._transaction.length)
      this._previousData = this._data;

    // Applying the operation
    const result = update(
      this._data,
      solvedPath,
      realOperation,
      this.options
    );

    const {data, node} = result;

    // If because of purity, the update was moot, we stop here
    if (!('data' in result))
      return node;

    // If the operation is push, the affected path is slightly different
    const affectedPath = solvedPath.concat(
      operation.type === 'push' ? node.length - 1 : []
    );

    const hash = hashPath(affectedPath);

    // Updating data and transaction
    this._data = data;
    this._affectedPathsIndex[hash] = true;
    this._transaction.push({...operation, path: affectedPath});

    // Updating the monkeys
    this._refreshMonkeys(node, solvedPath, operation.type);

    // Emitting a `write` event
    this.emit('write', {path: affectedPath});

    // Should we let the user commit?
    if (!this.options.autoCommit)
      return node;

    // Should we update asynchronously?
    if (!this.options.asynchronous) {
      this.commit();
      return node;
    }

    // Updating asynchronously
    if (!this._future)
      this._future = setTimeout(() => this.commit(), 0);

    // Finally returning the affected node
    return node;
  }

  /**
   * Method committing the updates of the tree and firing the tree's events.
   *
   * @return {Baobab} - The tree instance for chaining purposes.
   */
  commit() {

    // Clearing timeout if one was defined
    if (this._future)
      this._future = clearTimeout(this._future);

    const affectedPaths = Object.keys(this._affectedPathsIndex).map(h => {
      return h !== 'λ' ?
        h.split('λ').slice(1) :
        [];
    });

    // Is the tree still valid?
    const validationError = this.validate(affectedPaths);

    if (validationError)
      return this;

    // Caching to keep original references before we change them
    const transaction = this._transaction,
          previousData = this._previousData;

    this._affectedPathsIndex = {};
    this._transaction = [];
    this._previousData = this._data;

    // Emitting update event
    this.emit('update', {
      paths: affectedPaths,
      currentData: this._data,
      transaction,
      previousData
    });

    return this;
  }

  /**
   * Method used to watch a collection of paths within the tree. Very useful
   * to bind UI components and such to the tree.
   *
   * @param  {object} mapping - Mapping of paths to listen.
   * @return {Cursor}         - The created watcher.
   */
  watch(mapping) {
    return new Watcher(this, mapping);
  }

  /**
   * Method releasing the tree and its attached data from memory.
   */
  release() {
    let k;

    this.emit('release');

    delete this.root;

    delete this._data;
    delete this._previousData;
    delete this._transaction;
    delete this._affectedPathsIndex;
    delete this._monkeys;

    // Releasing cursors
    for (k in this._cursors)
      this._cursors[k].release();
    delete this._cursors;

    // Killing event emitter
    this.kill();
  }

  /**
   * Overriding the `toJSON` method for convenient use with JSON.stringify.
   *
   * @return {mixed} - Data at cursor.
   */
  toJSON() {
    return this.serialize();
  }

  /**
   * Overriding the `toString` method for debugging purposes.
   *
   * @return {string} - The baobab's identity.
   */
  toString() {
    return this._identity;
  }
}

/**
 * Creating statics
 */
Baobab.monkey = function(...args) {

  if (!args.length)
    throw new Error('Baobab.monkey: missing definition.');

  if (args.length === 1)
    return new MonkeyDefinition(args[0]);

  return new MonkeyDefinition(args);
};
Baobab.dynamicNode = Baobab.monkey;

/**
 * Exposing some internals for convenience
 */
Baobab.Cursor = Cursor;
Baobab.MonkeyDefinition = MonkeyDefinition;
Baobab.Monkey = Monkey;
Baobab.type = type;
Baobab.helpers = helpers;

/**
 * Version
 */
Object.defineProperty(Baobab, 'version', {
  value: '2.1.1'
});

/**
 * Exporting
 */
export default Baobab;
<|endoftext|>


<commit_before>import { expect } from 'chai';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
import _ from 'underscore';

require('../../../lib/helpers');

const meetingSeriesId = "AaBbCcDd01";
class MeetingSeries {
    constructor(id) {
        this._id = id;
    }
    upsertTopic() {}
    static findOne(id) {
        if (id === meetingSeriesId) {
            return dummySeries;
        }
        return undefined;
    }
}

const minuteId = "AaBbCcDd02";
class Minutes {
    constructor(id) {
        this._id = id;
    }
    upsertTopic() {}
    static findOne(id) {
        if (id === minuteId) {
            return dummyMinute;
        }
        return undefined;
    }
}

let dummyMinute = new Minutes(minuteId);
let dummySeries = new MeetingSeries(meetingSeriesId);

global.Random = {
    i: 1,
    id: function() {
        return this.i++;
    }
};

const {
        Topic
    } = proxyquire('../../../imports/topic', {
    'meteor/underscore': { _, '@noCallThru': true},
    './minutes': { Minutes, '@noCallThru': true},
    './meetingseries': { MeetingSeries, '@noCallThru': true}
});

const {
    InfoItem
    } = proxyquire('../../../imports/infoitem', {
    'meteor/underscore': { _, '@noCallThru': true},
    './topic': { Topic, '@noCallThru': true}
});

describe('Topic', function() {

    let topicDoc;

    beforeEach(function () {
        topicDoc = {
            subject: "topic-subject",
            infoItems: []
        }
    });

    describe('#constructor', function () {

        it('sets the reference to the parent minute correctly', function() {
            let myTopic = new Topic(dummyMinute._id, topicDoc);
            expect(myTopic._parentMinutes).to.equal(dummyMinute);
        });

        it('can instantiate a topic with the parent minutes object instead of its id', function () {
            let myTopic = new Topic(dummyMinute, topicDoc);
            expect(myTopic._parentMinutes).to.equal(dummyMinute);
        });

        it('sets the subject correctly', function() {
            let myTopic = new Topic(dummyMinute._id, topicDoc);
            expect(myTopic._topicDoc.subject).to.equal(topicDoc.subject);
        });

        it('sets the initial value of the isOpen-flag correctly', function() {
            let myTopic = new Topic(dummyMinute._id, topicDoc);
            expect(myTopic._topicDoc.isOpen).to.be.true;
        });

        it('sets the initial value of the isNew-flag correctly', function() {
            let myTopic = new Topic(dummyMinute._id, topicDoc);
            expect(myTopic._topicDoc.isNew).to.be.true;
        });

        it('enforces infoItems to be of type Array', function() {
            topicDoc.infoItems = 'something';
            let myTopic = new Topic(dummyMinute._id, topicDoc);

            expect(myTopic._topicDoc.infoItems).to.be.an('array');
        });
    });

    it('#findTopicIndexInArray', function() {
        let topicArray = [ topicDoc ];
        let index = Topic.findTopicIndexInArray(topicDoc._id, topicArray);
        expect(index).to.equal(0);
    });

    describe('#hasOpenActionItem', function() {

        it('returns false if the topic does not have any sub items', function() {
            expect(Topic.hasOpenActionItem(topicDoc)).to.be.false;
        });

        it('returns false if the topic has only closed action items', function() {
            topicDoc.infoItems.push({
                isOpen: false
            });

            expect(Topic.hasOpenActionItem(topicDoc)).to.be.false;
        });

        it('returns true if the topic has a closed action item (static method call)', function() {
            topicDoc.infoItems.push({
                isOpen: false
            });
            topicDoc.infoItems[0].isOpen = true;
            expect(Topic.hasOpenActionItem(topicDoc)).to.be.true;
        });

        it('returns true if the topic has a closed action item (object method call)', function() {
            topicDoc.infoItems.push({
                isOpen: false
            });
            topicDoc.infoItems[0].isOpen = true;
            let myTopic = new Topic(dummyMinute._id, topicDoc);
            expect(myTopic.hasOpenActionItem()).to.be.true;
        });

    });

    describe('#invalidateIsNewFlag', function () {

        let myTopic;

        beforeEach(function () {
            topicDoc.isNew = true;
            topicDoc.infoItems.push({
                isOpen: true,
                isNew: true,
                itemType: "actionItem",
                createdInMinute: dummyMinute._id
            });
            myTopic = new Topic(dummyMinute._id, topicDoc);
        });

        it('clears the isNew-Flag of the topic itself', function () {
            myTopic.invalidateIsNewFlag();
            expect(topicDoc.isNew).to.be.false;
        });

        it('clears the isNew-Flag of the action item', function () {
            myTopic.invalidateIsNewFlag();
            expect(topicDoc.infoItems[0].isNew).to.be.false;
        });

    });

    it('#toggleState', function () {
        let myTopic = new Topic(dummyMinute._id, topicDoc);

        let oldState = myTopic._topicDoc.isOpen;

        myTopic.toggleState();

        // state should have changed
        expect(myTopic._topicDoc.isOpen).to.not.equal(oldState);

    });

    describe('#upsertInfoItem', function() {

        let myTopic, topicItemDoc;

        beforeEach(function() {
            myTopic = new Topic(dummyMinute._id, topicDoc);

            topicItemDoc = {
                subject: "info-item-subject",
                createdAt: new Date()
            };
        });

        it('adds a new info item to our topic', function() {
            myTopic.upsertInfoItem(topicItemDoc);

            expect(myTopic.getInfoItems().length, "the topic should have exactly one item").to.equal(1);
            expect(myTopic.getInfoItems()[0]._id, "the item should have an id").to.not.be.false;
            expect(myTopic.getInfoItems()[0].subject, "the subject should be set correctly").to.equal(topicItemDoc.subject);
        });

        it('updates an existing info item', function() {
            myTopic.upsertInfoItem(topicItemDoc);

            // Change the subject and call the upsertTopicItem method again
            let topicItem = myTopic.getInfoItems()[0];
            topicItem.subject = "new_subject";

            myTopic.upsertInfoItem(topicItem);

            expect(myTopic.getInfoItems().length, "the topic should have exactly one item").to.equal(1);
            expect(myTopic.getInfoItems()[0]._id, "the item should have an id").to.equal(topicItem._id);
            expect(myTopic.getInfoItems()[0].subject, "the subject should be set correctly").to.equal(topicItem.subject);
        });

    });

    it('#findInfoItem', function() {
        let myTopic = new Topic(dummyMinute._id, topicDoc);
        let infoItemDoc = {
            _id: 'AaBbCcDd01',
            subject: "info-item-subject",
            createdAt: new Date(),
            createdInMinute: dummyMinute._id
        };

        // new info item is not added yet, so our topic should not find it
        let foundItem = myTopic.findInfoItem(infoItemDoc._id);
        expect(foundItem).to.equal(undefined);

        // now we add the info item to our topic
        myTopic.upsertInfoItem(infoItemDoc);

        foundItem = myTopic.findInfoItem(infoItemDoc._id);
        // foundItem should not be undefined
        expect(foundItem, "the result should not be undefined").to.not.equal(undefined);
        // the subject of the found item should be equal to its initial value
        expect(foundItem._infoItemDoc.subject, "the correct info item should be found").to.equal(infoItemDoc.subject);
    });

    it('#removeInfoItem', function() {
        let myTopic = new Topic(dummyMinute._id, topicDoc);

        let infoItemDoc = {
            _id: 'AaBbCcDd01',
            subject: "info-item-subject",
            createdAt: new Date()
        };
        let infoItemDoc2 = {
            _id: 'AaBbCcDd02',
            subject: "info-item-subject2",
            createdAt: new Date()
        };

        // now we add the info items to our topic
        myTopic.upsertInfoItem(infoItemDoc);
        myTopic.upsertInfoItem(infoItemDoc2);

        // check that the two info items was added
        let initialLength = myTopic.getInfoItems().length;

        // remove the second one
        myTopic.removeInfoItem(infoItemDoc2._id);

        let diff = initialLength - myTopic.getInfoItems().length;

        // check that there are now only one items
        expect(diff, "The length of the info items should be decreased by one").to.equal(1);

        // check that the first item is still part of our topic
        expect(myTopic.getInfoItems()[0]._id, "The other info item should not be removed.").to.equal(infoItemDoc._id);

    });

    describe('#tailorTopic', function() {

        let myTopic;

        beforeEach(function() {
            topicDoc.infoItems.push({
                subject: "myInfoItem",
                createdInMinute: dummyMinute._id
            });
            topicDoc.infoItems.push({
                subject: "myClosedActionItem",
                isOpen: false,
                itemType: "actionItem",
                createdInMinute: dummyMinute._id
            });
            topicDoc.infoItems.push({
                subject: "myOpenActionItem",
                isOpen: true,
                itemType: "actionItem",
                createdInMinute: dummyMinute._id
            });
            myTopic = new Topic(dummyMinute._id, topicDoc);
        });

        it('removes all info items and closed action items', function() {
            myTopic.tailorTopic();

            expect(myTopic.getInfoItems()).to.have.length(1);
        });

        it('keeps the open action items', function() {
            myTopic.tailorTopic();

            expect(myTopic._topicDoc.infoItems[0].isOpen).to.be.true;
        });

    });

    describe('#getOpenActionItems', function() {

        let myTopic;

        beforeEach(function () {
            topicDoc.infoItems.push({
                subject: "myInfoItem"
            });
            topicDoc.infoItems.push({
                subject: "myClosedActionItem",
                isOpen: false,
                itemType: "actionItem"
            });
            topicDoc.infoItems.push({
                subject: "myOpenActionItem",
                isOpen: true,
                itemType: "actionItem"
            });
            topicDoc.infoItems.push({
                subject: "my2ndOpenActionItem",
                isOpen: true,
                itemType: "actionItem"
            });
            myTopic = new Topic(dummyMinute._id, topicDoc);
        });

        it('returns the correct amount of items', function () {
            expect(myTopic.getOpenActionItems()).to.have.length(2);
        });

        it('returns only open action items', function () {
            myTopic.getOpenActionItems().forEach(item => {
                expect(item, "the item should be a action item").to.have.ownProperty('isOpen');
                expect(item.isOpen, "the item should marked as open").to.be.true;
            })
        });

    });

    it('#save', function() {
        let myTopic = new Topic(dummyMinute._id, topicDoc);

        // the save-method should call the upsertTopic-Method of the parent Minute
        // so we spy on it
        var spy = sinon.spy(dummyMinute, "upsertTopic");

        myTopic.save();

        expect(spy.calledOnce, "the upsertTopic method should be called once").to.be.true;
        expect(spy.calledWith(myTopic._topicDoc), "the document should be sent to the upsertTopic method").to.be.true;

        spy.restore();
    });

    it('#getDocument', function () {
        let myTopic = new Topic(dummyMinute._id, topicDoc);

        expect(myTopic.getDocument()).to.equal(topicDoc);
    });

});<commit_msg>Fix: unittests of topic class (mock meteor/meteor)<commit_after>import { expect } from 'chai';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
import _ from 'underscore';

require('../../../lib/helpers');

class MeteorError {}
let Meteor = {
    call: sinon.stub(),
    Error: MeteorError
};

const meetingSeriesId = "AaBbCcDd01";
class MeetingSeries {
    constructor(id) {
        this._id = id;
    }
    upsertTopic() {}
    static findOne(id) {
        if (id === meetingSeriesId) {
            return dummySeries;
        }
        return undefined;
    }
}

const minuteId = "AaBbCcDd02";
class Minutes {
    constructor(id) {
        this._id = id;
    }
    upsertTopic() {}
    static findOne(id) {
        if (id === minuteId) {
            return dummyMinute;
        }
        return undefined;
    }
}

let dummyMinute = new Minutes(minuteId);
let dummySeries = new MeetingSeries(meetingSeriesId);

global.Random = {
    i: 1,
    id: function() {
        return this.i++;
    }
};

const {
        Topic
    } = proxyquire('../../../imports/topic', {
    'meteor/meteor': { Meteor, '@noCallThru': true},
    'meteor/underscore': { _, '@noCallThru': true},
    './minutes': { Minutes, '@noCallThru': true},
    './meetingseries': { MeetingSeries, '@noCallThru': true}
});

const {
    InfoItem
    } = proxyquire('../../../imports/infoitem', {
    'meteor/underscore': { _, '@noCallThru': true},
    './topic': { Topic, '@noCallThru': true}
});

describe('Topic', function() {

    let topicDoc;

    beforeEach(function () {
        topicDoc = {
            subject: "topic-subject",
            infoItems: []
        }
    });

    describe('#constructor', function () {

        it('sets the reference to the parent minute correctly', function() {
            let myTopic = new Topic(dummyMinute._id, topicDoc);
            expect(myTopic._parentMinutes).to.equal(dummyMinute);
        });

        it('can instantiate a topic with the parent minutes object instead of its id', function () {
            let myTopic = new Topic(dummyMinute, topicDoc);
            expect(myTopic._parentMinutes).to.equal(dummyMinute);
        });

        it('sets the subject correctly', function() {
            let myTopic = new Topic(dummyMinute._id, topicDoc);
            expect(myTopic._topicDoc.subject).to.equal(topicDoc.subject);
        });

        it('sets the initial value of the isOpen-flag correctly', function() {
            let myTopic = new Topic(dummyMinute._id, topicDoc);
            expect(myTopic._topicDoc.isOpen).to.be.true;
        });

        it('sets the initial value of the isNew-flag correctly', function() {
            let myTopic = new Topic(dummyMinute._id, topicDoc);
            expect(myTopic._topicDoc.isNew).to.be.true;
        });

        it('enforces infoItems to be of type Array', function() {
            topicDoc.infoItems = 'something';
            let myTopic = new Topic(dummyMinute._id, topicDoc);

            expect(myTopic._topicDoc.infoItems).to.be.an('array');
        });
    });

    it('#findTopicIndexInArray', function() {
        let topicArray = [ topicDoc ];
        let index = Topic.findTopicIndexInArray(topicDoc._id, topicArray);
        expect(index).to.equal(0);
    });

    describe('#hasOpenActionItem', function() {

        it('returns false if the topic does not have any sub items', function() {
            expect(Topic.hasOpenActionItem(topicDoc)).to.be.false;
        });

        it('returns false if the topic has only closed action items', function() {
            topicDoc.infoItems.push({
                isOpen: false
            });

            expect(Topic.hasOpenActionItem(topicDoc)).to.be.false;
        });

        it('returns true if the topic has a closed action item (static method call)', function() {
            topicDoc.infoItems.push({
                isOpen: false
            });
            topicDoc.infoItems[0].isOpen = true;
            expect(Topic.hasOpenActionItem(topicDoc)).to.be.true;
        });

        it('returns true if the topic has a closed action item (object method call)', function() {
            topicDoc.infoItems.push({
                isOpen: false
            });
            topicDoc.infoItems[0].isOpen = true;
            let myTopic = new Topic(dummyMinute._id, topicDoc);
            expect(myTopic.hasOpenActionItem()).to.be.true;
        });

    });

    describe('#invalidateIsNewFlag', function () {

        let myTopic;

        beforeEach(function () {
            topicDoc.isNew = true;
            topicDoc.infoItems.push({
                isOpen: true,
                isNew: true,
                itemType: "actionItem",
                createdInMinute: dummyMinute._id
            });
            myTopic = new Topic(dummyMinute._id, topicDoc);
        });

        it('clears the isNew-Flag of the topic itself', function () {
            myTopic.invalidateIsNewFlag();
            expect(topicDoc.isNew).to.be.false;
        });

        it('clears the isNew-Flag of the action item', function () {
            myTopic.invalidateIsNewFlag();
            expect(topicDoc.infoItems[0].isNew).to.be.false;
        });

    });

    it('#toggleState', function () {
        let myTopic = new Topic(dummyMinute._id, topicDoc);

        let oldState = myTopic._topicDoc.isOpen;

        myTopic.toggleState();

        // state should have changed
        expect(myTopic._topicDoc.isOpen).to.not.equal(oldState);

    });

    describe('#upsertInfoItem', function() {

        let myTopic, topicItemDoc;

        beforeEach(function() {
            myTopic = new Topic(dummyMinute._id, topicDoc);

            topicItemDoc = {
                subject: "info-item-subject",
                createdAt: new Date()
            };
        });

        it('adds a new info item to our topic', function() {
            myTopic.upsertInfoItem(topicItemDoc);

            expect(myTopic.getInfoItems().length, "the topic should have exactly one item").to.equal(1);
            expect(myTopic.getInfoItems()[0]._id, "the item should have an id").to.not.be.false;
            expect(myTopic.getInfoItems()[0].subject, "the subject should be set correctly").to.equal(topicItemDoc.subject);
        });

        it('updates an existing info item', function() {
            myTopic.upsertInfoItem(topicItemDoc);

            // Change the subject and call the upsertTopicItem method again
            let topicItem = myTopic.getInfoItems()[0];
            topicItem.subject = "new_subject";

            myTopic.upsertInfoItem(topicItem);

            expect(myTopic.getInfoItems().length, "the topic should have exactly one item").to.equal(1);
            expect(myTopic.getInfoItems()[0]._id, "the item should have an id").to.equal(topicItem._id);
            expect(myTopic.getInfoItems()[0].subject, "the subject should be set correctly").to.equal(topicItem.subject);
        });

    });

    it('#findInfoItem', function() {
        let myTopic = new Topic(dummyMinute._id, topicDoc);
        let infoItemDoc = {
            _id: 'AaBbCcDd01',
            subject: "info-item-subject",
            createdAt: new Date(),
            createdInMinute: dummyMinute._id
        };

        // new info item is not added yet, so our topic should not find it
        let foundItem = myTopic.findInfoItem(infoItemDoc._id);
        expect(foundItem).to.equal(undefined);

        // now we add the info item to our topic
        myTopic.upsertInfoItem(infoItemDoc);

        foundItem = myTopic.findInfoItem(infoItemDoc._id);
        // foundItem should not be undefined
        expect(foundItem, "the result should not be undefined").to.not.equal(undefined);
        // the subject of the found item should be equal to its initial value
        expect(foundItem._infoItemDoc.subject, "the correct info item should be found").to.equal(infoItemDoc.subject);
    });

    it('#removeInfoItem', function() {
        let myTopic = new Topic(dummyMinute._id, topicDoc);

        let infoItemDoc = {
            _id: 'AaBbCcDd01',
            subject: "info-item-subject",
            createdAt: new Date()
        };
        let infoItemDoc2 = {
            _id: 'AaBbCcDd02',
            subject: "info-item-subject2",
            createdAt: new Date()
        };

        // now we add the info items to our topic
        myTopic.upsertInfoItem(infoItemDoc);
        myTopic.upsertInfoItem(infoItemDoc2);

        // check that the two info items was added
        let initialLength = myTopic.getInfoItems().length;

        // remove the second one
        myTopic.removeInfoItem(infoItemDoc2._id);

        let diff = initialLength - myTopic.getInfoItems().length;

        // check that there are now only one items
        expect(diff, "The length of the info items should be decreased by one").to.equal(1);

        // check that the first item is still part of our topic
        expect(myTopic.getInfoItems()[0]._id, "The other info item should not be removed.").to.equal(infoItemDoc._id);

    });

    describe('#tailorTopic', function() {

        let myTopic;

        beforeEach(function() {
            topicDoc.infoItems.push({
                subject: "myInfoItem",
                createdInMinute: dummyMinute._id
            });
            topicDoc.infoItems.push({
                subject: "myClosedActionItem",
                isOpen: false,
                itemType: "actionItem",
                createdInMinute: dummyMinute._id
            });
            topicDoc.infoItems.push({
                subject: "myOpenActionItem",
                isOpen: true,
                itemType: "actionItem",
                createdInMinute: dummyMinute._id
            });
            myTopic = new Topic(dummyMinute._id, topicDoc);
        });

        it('removes all info items and closed action items', function() {
            myTopic.tailorTopic();

            expect(myTopic.getInfoItems()).to.have.length(1);
        });

        it('keeps the open action items', function() {
            myTopic.tailorTopic();

            expect(myTopic._topicDoc.infoItems[0].isOpen).to.be.true;
        });

    });

    describe('#getOpenActionItems', function() {

        let myTopic;

        beforeEach(function () {
            topicDoc.infoItems.push({
                subject: "myInfoItem"
            });
            topicDoc.infoItems.push({
                subject: "myClosedActionItem",
                isOpen: false,
                itemType: "actionItem"
            });
            topicDoc.infoItems.push({
                subject: "myOpenActionItem",
                isOpen: true,
                itemType: "actionItem"
            });
            topicDoc.infoItems.push({
                subject: "my2ndOpenActionItem",
                isOpen: true,
                itemType: "actionItem"
            });
            myTopic = new Topic(dummyMinute._id, topicDoc);
        });

        it('returns the correct amount of items', function () {
            expect(myTopic.getOpenActionItems()).to.have.length(2);
        });

        it('returns only open action items', function () {
            myTopic.getOpenActionItems().forEach(item => {
                expect(item, "the item should be a action item").to.have.ownProperty('isOpen');
                expect(item.isOpen, "the item should marked as open").to.be.true;
            })
        });

    });

    it('#save', function() {
        let myTopic = new Topic(dummyMinute._id, topicDoc);

        // the save-method should call the upsertTopic-Method of the parent Minute
        // so we spy on it
        var spy = sinon.spy(dummyMinute, "upsertTopic");

        myTopic.save();

        expect(spy.calledOnce, "the upsertTopic method should be called once").to.be.true;
        expect(spy.calledWith(myTopic._topicDoc), "the document should be sent to the upsertTopic method").to.be.true;

        spy.restore();
    });

    it('#getDocument', function () {
        let myTopic = new Topic(dummyMinute._id, topicDoc);

        expect(myTopic.getDocument()).to.equal(topicDoc);
    });

});<|endoftext|>


<commit_before>import React from 'react';
import { connect } from 'react-redux';

import NodeModal from './NodeModal';

export default class GraphPanel extends React.Component {
  constructor(props) {
    super(props);
    this.state = {showNodeModal: false, node: null};
  }
  closeNodeModal() {
    // TODO: Discard changes
    this.setState({ showNodeModal: false, node: null }); // TODO: ensure this won't leave leaks
  }
  nodeClickHandler(editor, node) {
    this.setState({ showNodeModal: true, node: node});
  }
  onDeleteNode() {
    this.closeNodeModal();
    this.props.onDeleteNode(this.state.node);
  }
  onSavePropsNode() {
    this.setState({showNodeModal: false, node: null}); // TODO: ensure this won't leave leaks
  }
  onChangeNodeProp(e) {
    this.state.node.properties[e.target.name] = e.target.value;
    this.setState({node: this.state.node});
  }
  render() {
    let nodemodal = null;
    if (this.state.showNodeModal) {
      nodemodal =
        <NodeModal
          node={this.state.node}
          nodedefs={this.props.nodedefs}
          onDeleteNode={this.onDeleteNode.bind(this)}
          closeNodeModal={this.closeNodeModal.bind(this)}
          onSavePropsNode={this.onSavePropsNode.bind(this)}
          onChangeNodeProp={this.onChangeNodeProp.bind(this)}
        />;
    }
    return (

        {nodemodal}

    );
  }
  // Can't use callback ref because a dynamic callback (needed to use 'this') gets fired every update
  componentDidMount() {
    this.props.onSetEditorPanel(this.refs.editor_panel, this.nodeClickHandler.bind(this));
  }
  componentWillUnmount() {
    this.props.onSetEditorPanel(null);
  }
}
<commit_msg>Added discard changes implementation in node properties<commit_after>import React from 'react';
import { connect } from 'react-redux';

import NodeModal from './NodeModal';

export default class GraphPanel extends React.Component {
  constructor(props) {
    super(props);
    this.state = {showNodeModal: false, node: null};
    this.initialNode = null;
  }
  closeNodeModal() {
    Object.keys(this.initialNode.props).map(
      (key) => {
        let p = this.initialNode.props[key];
        this.state.node.properties[key] = p;
      });
    this.setState({ showNodeModal: false, node: null }); // TODO: ensure this won't leave leaks
  }
  nodeClickHandler(editor, node) {
    this.setState({ showNodeModal: true, node: node});
    this.initialNode = React.cloneElement(node, node.properties);
  }
  onDeleteNode() {
    this.closeNodeModal();
    this.props.onDeleteNode(this.state.node);
  }
  onSavePropsNode() {
    this.setState({showNodeModal: false, node: null}); // TODO: ensure this won't leave leaks
  }
  onChangeNodeProp(e) {
    this.state.node.properties[e.target.name] = e.target.value;
    this.setState({node: this.state.node});
  }
  render() {
    let nodemodal = null;
    if (this.state.showNodeModal) {
      nodemodal =
        <NodeModal
          node={this.state.node}
          nodedefs={this.props.nodedefs}
          onDeleteNode={this.onDeleteNode.bind(this)}
          closeNodeModal={this.closeNodeModal.bind(this)}
          onSavePropsNode={this.onSavePropsNode.bind(this)}
          onChangeNodeProp={this.onChangeNodeProp.bind(this)}
        />;
    }
    return (

        {nodemodal}

    );
  }
  // Can't use callback ref because a dynamic callback (needed to use 'this') gets fired every update
  componentDidMount() {
    this.props.onSetEditorPanel(this.refs.editor_panel, this.nodeClickHandler.bind(this));
  }
  componentWillUnmount() {
    this.props.onSetEditorPanel(null);
  }
}
<|endoftext|>