<commit_before>/**
 *  ACCORDION
 *
 * A lightwight vanilla JS accordion with an exstensible API
 */

// import uuid from 'uuid/v4';
// const uuidV4 = uuid;
/* eslint-disable no-unused-vars */
import arrayFromPolyfill from 'array-from-polyfill';
import onCSSTransitionEnd from 'transition-end';

/**
 * CONSTRUCTOR
 * initialises the object
 */
class BadgerAccordion {
    constructor(el, options) {
        const container = document.querySelector(el);

        // If el is not defined
        if (container == null) {
            return;
        }

        const defaults = {
            headerClass:        '.js-badger-accordion-header',
            panelClass:         '.js-badger-accordion-panel',
            panelInnerClass:    '.js-badger-accordion-panel-inner',
            hidenClass:         'is-hidden',
            initalisedClass:    'badger-accordion--initalised',
            headerDataAttr:     'data-badger-accordion-header-id',
            openMultiplePanels: false,
            openHeadersOnLoad:  [],
            headerOpenLabel:    'Open accordion panel',
            headerCloseLabel:   'Close accordion panel'
            // toggleEl:            // If you want to use a different element to trigger the accordion
        };

        // Options
        this.settings = Object.assign({}, defaults, options);

        // Setting getting elements
        this.container = container;
        this.headers = Array.from( this.container.querySelectorAll(this.settings.headerClass) );
        this.panels = Array.from( this.container.querySelectorAll(this.settings.panelClass) );
        this.toggleEl = this.settings.toggleEl !== undefined ? Array.from(this.container.querySelectorAll(this.settings.toggleEl)) : this.headers;

        // This is for managing state of the accordion. It by default sets
        // all accordion panels to be closed
        this.states = [].map.call(this.headers, () => {
            return { state: 'closed' };
        });

        this.ids = [].map.call(this.headers, () => {
            return { id: Math.floor((Math.random() * 1000000) + 1) };
        });

        // This is to ensure that once an open/close event has been fired
        // another cannot start until the first event has finished.
        // @TODO - get this working...
        this.toggling = false;

        // Initiating the accordion
        if( this.container ) {
            this.init();
        } else {
            /* eslint-disable no-console */
            console.log('Something is wrong with you markup...');
        }
    }

    /**
     *  INIT
     *
     *  Initalises the accordion
     */
    init() {
        // Sets up ID, aria attrs & data-attrs
        this._setupAttributes();

        // Setting up the inital view of the accordion
        this._initalState();

        // Setting the height of each panel
        this._setPanelHeight();

        // Inserting data-attribute onto each `header`
        this._insertDataAttrs();

        // Adding listeners to headers
        this._addListeners();

        // Adds class to accordion for initalisation
        this._finishInitalisation();
    }

    /**
     *  INSERT DATA ATTRS
     *
     *  Updates state object for inital loading of the accordion
     */
    _initalState() {
        // Sets state object as per `this.settings.openHeadersOnLoad`
        const headersToOpen = this.settings.openHeadersOnLoad;

        if (headersToOpen.length) {
            this._openHeadersOnLoad(headersToOpen);
        }

        // Render DOM as per the updates `this.states` object
        this._renderDom();
    }

    /**
     *  INSERT DATA ATTRS
     *
     *  Adds `headerDataAttr` to all headers
     */
    _insertDataAttrs() {
        this.headers.forEach( (header, index) => {
            header.setAttribute(this.settings.headerDataAttr, index);
        });
    }

    /**
     *  FINISH INITALISATION
     *
     *  Adds in `initalisedClass` to accordion
     */
    _finishInitalisation() {
        this.container.classList.add(this.settings.initalisedClass);
    }

    /**
     *  ADD LISTENERS
     *
     *  Adds click event to each header
     */
    _addListeners() {
        // So we can reference the badger-accordion object inside out eventListener
        const _this = this;

        // Adding click event to accordion
        this.headers.forEach((header, index) => {
            header.addEventListener('click', function() {
                // Getting the target of the click
                // const clickedEl = event.target;

                _this.handleClick(header, index);
            });
        });
    }

    /**
     *  HANDLE CLICK
     *
     *  Handles click and checks if click was on an header element
     *  @param {object} targetHeader - The header node you want to open
     */
    handleClick(targetHeader, headerIndex) {
        // Removing current `.` from `this.settings.headerClass` class so it can
        // be checked against the `targetHeader` classList
        const targetHeaderClass = this.settings.headerClass.substr(1);

        // Checking that the thing that was clicked on was the accordions header
        if (targetHeader.classList.contains(targetHeaderClass) && this.toggling === false) {
            this.toggling = true;

            // Updating states
            this.setState(headerIndex);

            // Render DOM as per the updates `this.states` object
            this._renderDom();
        }
    }

    /**
     *  SET STATES
     *
     *  Sets the state for all headers. The 'target header' will have its state toggeled
     *  @param {object} targetHeaderId - The header node you want to open
     */
    setState(targetHeaderId) {
        const states = this.getState();

        // If `this.settings.openMultiplePanels` is false we need to ensure only one panel
        // be can open at once. If it is false then all panels state APART from the one that
        // has just been clicked needs to be set to 'closed'.
        if (!this.settings.openMultiplePanels) {
            states.filter((state, index) => {
                if (index != targetHeaderId) {
                    state.state = 'closed';
                }
            });
        }

        // Toggles the state value of the target header. This was `array.find` but `find`
        // isnt supported in IE11
        states.filter((state, index) => {
            if (index == targetHeaderId) {
                const newState = this.toggleState(state.state);
                return (state.state = newState);
            }
        });
    }

    /**
     *  RENDER DOM
     *
     *  Renders the accordion in the DOM using the `this.states` object
     */
    _renderDom() {
        // const states = this.getState();

        // Filter through all open headers and open them
        this.states.filter( (state, index) => {
            if(state.state === 'open') {
                this.open(index);
            }
        });

        // Filter through all closed headers and closes them
        this.states.filter( (state, index) => {
            if(state.state === 'closed') {
                this.close(index);
            }
        });
    }

    /**
     *  OPEN
     *
     *  Closes a specific panel
     *  @param {object} header - The header node you want to open
     */
    open(headerIndex) {
        this.togglePanel('open', headerIndex);
    }

    /**
     *  CLOSE
     *
     *  Closes a specific panel
     *  @param {object} header - The header node you want to close
     */
    close(headerIndex) {
        this.togglePanel('closed', headerIndex);
    }

    /**
     *  OPEN ALL
     *
     *  Opens all panels
     */
    openAll() {
        this.headers.forEach( header => {
            this.togglePanel('open', header);
        });
    }

    /**
     *  CLOSE ALL
     *
     *  Closes all panels
     */
    closeAll() {
        this.headers.forEach( header => {
            this.togglePanel('closed', header);
        });
    }

    /**
     *  GET STATE
     *
     *  Getting state of headers. By default gets state of all headers
     *  @param {string} animationAction - The animation you want to invoke
     *  @param {object} header          - The header node you want to animate
     */
    togglePanel(animationAction, headerIndex) {
        if(animationAction !== undefined && headerIndex !== undefined) {
            if(animationAction === 'closed') {
                // 1. Getting ID of panel that we want to close
                const header        = this.headers[headerIndex];
                const panelToClose  = this.panels[headerIndex];

                // 2. Closeing panel
                panelToClose.classList.add(this.settings.hidenClass);

                // 3. Set aria attrs
                header.setAttribute('aria-expanded', false);
                header.setAttribute('aria-label', this.settings.headerOpenLabel);

                // 4. Resetting toggling so a new event can be fired
                panelToClose.onCSSTransitionEnd(() => this.toggling = false );
            } else if(animationAction === 'open') {
                // 1. Getting ID of panel that we want to open
                const header      = this.headers[headerIndex];
                const panelToOpen = this.panels[headerIndex];

                // 2. Closeing panel
                panelToOpen.classList.remove(this.settings.hidenClass);

                // 3. Set aria attrs
                header.setAttribute('aria-expanded', true);
                header.setAttribute('aria-label', this.settings.headerCloseLabel);

                // 4. Resetting toggling so a new event can be fired
                panelToOpen.onCSSTransitionEnd(() => this.toggling = false );
            }
        }
    }

    // @TODO - is this needed anymore?
    // checkState(headerId) {
    //     let state = this.states[headerId].state;
    //
    //     if(state === 'closed') {
    //         return state;
    //     } else if(state === 'open') {
    //         return state;
    //     }
    // }

    /**
     *  GET STATE
     *
     *  Getting state of headers. By default gets state of all headers
     *  @param {array} headerIds - Id/'s of the headers you want to check
     */
    getState(headerIds = []) {
        if(headerIds.length && Array.isArray(headerIds)) {
            let states = headerIds.map( header => this.states[header] );

            return states;
        } else {
            return this.states;
        }
    }

    /**
     *  TOGGLE STATE
     *
     *  Toggling the state value
     *  @param {string} currentState - Current state value for a header
     */
    toggleState(currentState) {
        if(currentState !== undefined) {
            return (currentState === 'closed') ? 'open' : 'closed';
        }
    }

    /**
     *  HEADERS TO OPEN
     *
     *  Setting which headers should be open when accordion is initalised
     *  @param {array} headersToOpen - Array of ID's for the headers to be open
     */
    _openHeadersOnLoad(headersToOpen = []) {
        if (headersToOpen.length && Array.isArray(headersToOpen)) {
            let headers = headersToOpen.filter(header => header != undefined);

            headers.forEach(header => {
                return (this.states[header].state = 'open');
            });
        }
    }

    /**
     *  SET PANEL HEIGHT
     *
     *  Setting height for panels using pannels inner element
     */
    _setPanelHeight() {
        // [].forEach.(this.panels, (panel) => {
        this.panels.forEach(panel => {
            const panelInner = panel.querySelector(this.settings.panelInnerClass);

            let activeHeight = panelInner.offsetHeight;

            return panel.style.maxHeight = `${activeHeight}px`;
        });
    }

    _setupHeaders() {
        this.headers.forEach( (header, index) => {
            header.setAttribute('id', `badger-accordion-header-${this.ids[index].id}`);
            header.setAttribute('aria-controls', `badger-accordion-panel-${this.ids[index].id}`);
            header.setAttribute('aria-label', this.settings.headerOpenLabel);
        });
    }

    _setupPanels() {
        this.panels.forEach( (panel, index) => {
            panel.setAttribute('id', `badger-accordion-panel-${this.ids[index].id}`);
            panel.setAttribute('aria-labeledby', `badger-accordion-header-${this.ids[index].id}`);
        });
    }

    _setupAttributes() {
        // Adding ID & aria-controls
        this._setupHeaders();

        // Adding ID & aria-labeledby
        this._setupPanels();

        // Inserting data-attribute onto each `header`
        this._insertDataAttrs();
    }
}

// Export
export default BadgerAccordion;
<commit_msg>Added rol=“presentation” to accordion<commit_after>/**
 *  ACCORDION
 *
 * A lightwight vanilla JS accordion with an exstensible API
 */

// import uuid from 'uuid/v4';
// const uuidV4 = uuid;
/* eslint-disable no-unused-vars */
import arrayFromPolyfill from 'array-from-polyfill';
import onCSSTransitionEnd from 'transition-end';

/**
 * CONSTRUCTOR
 * initialises the object
 */
class BadgerAccordion {
    constructor(el, options) {
        const container = document.querySelector(el);

        // If el is not defined
        if (container == null) {
            return;
        }

        const defaults = {
            headerClass:        '.js-badger-accordion-header',
            panelClass:         '.js-badger-accordion-panel',
            panelInnerClass:    '.js-badger-accordion-panel-inner',
            hidenClass:         'is-hidden',
            initalisedClass:    'badger-accordion--initalised',
            headerDataAttr:     'data-badger-accordion-header-id',
            openMultiplePanels: false,
            openHeadersOnLoad:  [],
            headerOpenLabel:    'Open accordion panel',
            headerCloseLabel:   'Close accordion panel'
            // toggleEl:            // If you want to use a different element to trigger the accordion
        };

        // Options
        this.settings = Object.assign({}, defaults, options);

        // Setting getting elements
        this.container = container;
        this.headers = Array.from( this.container.querySelectorAll(this.settings.headerClass) );
        this.panels = Array.from( this.container.querySelectorAll(this.settings.panelClass) );
        this.toggleEl = this.settings.toggleEl !== undefined ? Array.from(this.container.querySelectorAll(this.settings.toggleEl)) : this.headers;

        // This is for managing state of the accordion. It by default sets
        // all accordion panels to be closed
        this.states = [].map.call(this.headers, () => {
            return { state: 'closed' };
        });

        this.ids = [].map.call(this.headers, () => {
            return { id: Math.floor((Math.random() * 1000000) + 1) };
        });

        // This is to ensure that once an open/close event has been fired
        // another cannot start until the first event has finished.
        // @TODO - get this working...
        this.toggling = false;

        // Initiating the accordion
        if( this.container ) {
            this.init();
        } else {
            /* eslint-disable no-console */
            console.log('Something is wrong with you markup...');
        }
    }

    /**
     *  INIT
     *
     *  Initalises the accordion
     */
    init() {
        // Sets up ID, aria attrs & data-attrs
        this._setupAttributes();

        // Setting up the inital view of the accordion
        this._initalState();

        // Setting the height of each panel
        this._setPanelHeight();

        // Inserting data-attribute onto each `header`
        this._insertDataAttrs();

        // Adding listeners to headers
        this._addListeners();

        // Adds class to accordion for initalisation
        this._finishInitalisation();
    }

    /**
     *  INSERT DATA ATTRS
     *
     *  Updates state object for inital loading of the accordion
     */
    _initalState() {
        // Sets state object as per `this.settings.openHeadersOnLoad`
        const headersToOpen = this.settings.openHeadersOnLoad;

        if (headersToOpen.length) {
            this._openHeadersOnLoad(headersToOpen);
        }

        // Render DOM as per the updates `this.states` object
        this._renderDom();
    }

    /**
     *  INSERT DATA ATTRS
     *
     *  Adds `headerDataAttr` to all headers
     */
    _insertDataAttrs() {
        this.headers.forEach( (header, index) => {
            header.setAttribute(this.settings.headerDataAttr, index);
        });
    }

    /**
     *  FINISH INITALISATION
     *
     *  Adds in `initalisedClass` to accordion
     */
    _finishInitalisation() {
        this.container.classList.add(this.settings.initalisedClass);
        this.container.setAttribute('role', 'presentation');
    }

    /**
     *  ADD LISTENERS
     *
     *  Adds click event to each header
     */
    _addListeners() {
        // So we can reference the badger-accordion object inside out eventListener
        const _this = this;

        // Adding click event to accordion
        this.headers.forEach((header, index) => {
            header.addEventListener('click', function() {
                // Getting the target of the click
                // const clickedEl = event.target;

                _this.handleClick(header, index);
            });
        });
    }

    /**
     *  HANDLE CLICK
     *
     *  Handles click and checks if click was on an header element
     *  @param {object} targetHeader - The header node you want to open
     */
    handleClick(targetHeader, headerIndex) {
        // Removing current `.` from `this.settings.headerClass` class so it can
        // be checked against the `targetHeader` classList
        const targetHeaderClass = this.settings.headerClass.substr(1);

        // Checking that the thing that was clicked on was the accordions header
        if (targetHeader.classList.contains(targetHeaderClass) && this.toggling === false) {
            this.toggling = true;

            // Updating states
            this.setState(headerIndex);

            // Render DOM as per the updates `this.states` object
            this._renderDom();
        }
    }

    /**
     *  SET STATES
     *
     *  Sets the state for all headers. The 'target header' will have its state toggeled
     *  @param {object} targetHeaderId - The header node you want to open
     */
    setState(targetHeaderId) {
        const states = this.getState();

        // If `this.settings.openMultiplePanels` is false we need to ensure only one panel
        // be can open at once. If it is false then all panels state APART from the one that
        // has just been clicked needs to be set to 'closed'.
        if (!this.settings.openMultiplePanels) {
            states.filter((state, index) => {
                if (index != targetHeaderId) {
                    state.state = 'closed';
                }
            });
        }

        // Toggles the state value of the target header. This was `array.find` but `find`
        // isnt supported in IE11
        states.filter((state, index) => {
            if (index == targetHeaderId) {
                const newState = this.toggleState(state.state);
                return (state.state = newState);
            }
        });
    }

    /**
     *  RENDER DOM
     *
     *  Renders the accordion in the DOM using the `this.states` object
     */
    _renderDom() {
        // const states = this.getState();

        // Filter through all open headers and open them
        this.states.filter( (state, index) => {
            if(state.state === 'open') {
                this.open(index);
            }
        });

        // Filter through all closed headers and closes them
        this.states.filter( (state, index) => {
            if(state.state === 'closed') {
                this.close(index);
            }
        });
    }

    /**
     *  OPEN
     *
     *  Closes a specific panel
     *  @param {object} header - The header node you want to open
     */
    open(headerIndex) {
        this.togglePanel('open', headerIndex);
    }

    /**
     *  CLOSE
     *
     *  Closes a specific panel
     *  @param {object} header - The header node you want to close
     */
    close(headerIndex) {
        this.togglePanel('closed', headerIndex);
    }

    /**
     *  OPEN ALL
     *
     *  Opens all panels
     */
    openAll() {
        this.headers.forEach( header => {
            this.togglePanel('open', header);
        });
    }

    /**
     *  CLOSE ALL
     *
     *  Closes all panels
     */
    closeAll() {
        this.headers.forEach( header => {
            this.togglePanel('closed', header);
        });
    }

    /**
     *  GET STATE
     *
     *  Getting state of headers. By default gets state of all headers
     *  @param {string} animationAction - The animation you want to invoke
     *  @param {object} header          - The header node you want to animate
     */
    togglePanel(animationAction, headerIndex) {
        if(animationAction !== undefined && headerIndex !== undefined) {
            if(animationAction === 'closed') {
                // 1. Getting ID of panel that we want to close
                const header        = this.headers[headerIndex];
                const panelToClose  = this.panels[headerIndex];

                // 2. Closeing panel
                panelToClose.classList.add(this.settings.hidenClass);

                // 3. Set aria attrs
                header.setAttribute('aria-expanded', false);
                header.setAttribute('aria-label', this.settings.headerOpenLabel);

                // 4. Resetting toggling so a new event can be fired
                panelToClose.onCSSTransitionEnd(() => this.toggling = false );
            } else if(animationAction === 'open') {
                // 1. Getting ID of panel that we want to open
                const header      = this.headers[headerIndex];
                const panelToOpen = this.panels[headerIndex];

                // 2. Closeing panel
                panelToOpen.classList.remove(this.settings.hidenClass);

                // 3. Set aria attrs
                header.setAttribute('aria-expanded', true);
                header.setAttribute('aria-label', this.settings.headerCloseLabel);

                // 4. Resetting toggling so a new event can be fired
                panelToOpen.onCSSTransitionEnd(() => this.toggling = false );
            }
        }
    }

    // @TODO - is this needed anymore?
    // checkState(headerId) {
    //     let state = this.states[headerId].state;
    //
    //     if(state === 'closed') {
    //         return state;
    //     } else if(state === 'open') {
    //         return state;
    //     }
    // }

    /**
     *  GET STATE
     *
     *  Getting state of headers. By default gets state of all headers
     *  @param {array} headerIds - Id/'s of the headers you want to check
     */
    getState(headerIds = []) {
        if(headerIds.length && Array.isArray(headerIds)) {
            let states = headerIds.map( header => this.states[header] );

            return states;
        } else {
            return this.states;
        }
    }

    /**
     *  TOGGLE STATE
     *
     *  Toggling the state value
     *  @param {string} currentState - Current state value for a header
     */
    toggleState(currentState) {
        if(currentState !== undefined) {
            return (currentState === 'closed') ? 'open' : 'closed';
        }
    }

    /**
     *  HEADERS TO OPEN
     *
     *  Setting which headers should be open when accordion is initalised
     *  @param {array} headersToOpen - Array of ID's for the headers to be open
     */
    _openHeadersOnLoad(headersToOpen = []) {
        if (headersToOpen.length && Array.isArray(headersToOpen)) {
            let headers = headersToOpen.filter(header => header != undefined);

            headers.forEach(header => {
                return (this.states[header].state = 'open');
            });
        }
    }

    /**
     *  SET PANEL HEIGHT
     *
     *  Setting height for panels using pannels inner element
     */
    _setPanelHeight() {
        // [].forEach.(this.panels, (panel) => {
        this.panels.forEach(panel => {
            const panelInner = panel.querySelector(this.settings.panelInnerClass);

            let activeHeight = panelInner.offsetHeight;

            return panel.style.maxHeight = `${activeHeight}px`;
        });
    }

    _setupHeaders() {
        this.headers.forEach( (header, index) => {
            header.setAttribute('id', `badger-accordion-header-${this.ids[index].id}`);
            header.setAttribute('aria-controls', `badger-accordion-panel-${this.ids[index].id}`);
            header.setAttribute('aria-label', this.settings.headerOpenLabel);
        });
    }

    _setupPanels() {
        this.panels.forEach( (panel, index) => {
            panel.setAttribute('id', `badger-accordion-panel-${this.ids[index].id}`);
            panel.setAttribute('aria-labeledby', `badger-accordion-header-${this.ids[index].id}`);
        });
    }

    _setupAttributes() {
        // Adding ID & aria-controls
        this._setupHeaders();

        // Adding ID & aria-labeledby
        this._setupPanels();

        // Inserting data-attribute onto each `header`
        this._insertDataAttrs();
    }
}

// Export
export default BadgerAccordion;
<|endoftext|>


<commit_before>import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import Radium from 'radium';
import Main from './main';
import { Link } from 'react-router';

//define your styling with Javascript objects
//use flexbox for styling "https://css-tricks.com/snippets/css/a-guide-to-flexbox/"
const styles = {
    mainComponent: {
        backgroundColor: 'blue',
    },
};

class Login extends Component {
    constructor(props) {
        super(props);
        this.state = {
            enabled: true,
        };
    }
    //set component initial state above

    //custom functions here- use this.setState() to change the state of the app
    toggleEnabled() {
        this.setState({ enabled: !this.state.enabled });
    }

    componentWillMount() {
    }

    componentDidMount() {
    }

    componentWillReceiveProps() {
    }

    componentDidUpdate() {
    }

    componentWillUnmount() {
    }

    render() {
        //define your content- JSX variables and dynamic JSX
      const
        toggleEnabled = this.toggleEnabled;
        //return your JSX with defined content (anything in the render method occurs anytime a change occurs, to limit rerendering add changes to component lifecycle)
        //bind your functions to allow them access to the this functionality
        return (

                    Your component build goes here

                <Link to="Main">Start</Link>

        );
    }
}
//required to allow the inline styling using radium
Login = Radium(Login);

//define your incoming props
Login.propTypes = {
}

export default Login;
<commit_msg>mod login<commit_after>import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import Radium from 'radium';
import Main from './main';
import { Link } from 'react-router';

//define your styling with Javascript objects
//use flexbox for styling "https://css-tricks.com/snippets/css/a-guide-to-flexbox/"
const styles = {
    mainComponent: {
        backgroundColor: '#4A90E2',
        display: 'flex',
        flexFlow: 'column wrap',
        height: '100vh',
        width: '100vw',
        justifyContent: 'center',
        alignItems: 'center'
    },
    logoDiv:{
        backgroundColor: 'pink',
        display: 'flex',
        height: '28%',
        width: '100vw',
        justifyContent: 'center',
        alignItems: 'center'
    },
    startButtonDiv:{
        backgroundColor: 'orange',
        color: 'blue',
        display: 'flex',
        height: '5%',
        width: '30%',
        justifyContent: 'center',
        alignItems: 'center'
    }
};

class Login extends Component {
    constructor(props) {
        super(props);
        this.state = {
            enabled: true,
        };
    }
    //set component initial state above

    //custom functions here- use this.setState() to change the state of the app
    toggleEnabled() {
        this.setState({ enabled: !this.state.enabled });
    }

    componentWillMount() {
    }

    componentDidMount() {
    }

    componentWillReceiveProps() {
    }

    componentDidUpdate() {
    }

    componentWillUnmount() {
    }

    render() {
        //define your content- JSX variables and dynamic JSX
      const
        toggleEnabled = this.toggleEnabled;
        //return your JSX with defined content (anything in the render method occurs anytime a change occurs, to limit rerendering add changes to component lifecycle)
        //bind your functions to allow them access to the this functionality
        return (

                        <Link to="Main">Start</Link>

        );
    }
}
//required to allow the inline styling using radium
Login = Radium(Login);

//define your incoming props
Login.propTypes = {
}

export default Login;
<|endoftext|>


<commit_before>import { Box2, Mesh, Object3D, Vector2, Vector3, Vector4 } from '../Three';
import { GlyphStringGeometry, GlyphStringGeometryCache } from './GlyphStringGeometry';

// temporary objects for raycasting

const _ssOrigin = new Vector4();
const _mouse = new Vector3();
const _labelEnd = new Vector3();
const _ssLabelOrigin = new Vector3();

class GlyphStringBase extends Mesh {

	constructor ( text, glyphMaterial, geometry ) {

		super( geometry, glyphMaterial );

		this.name = text;

		this.labelOrigin = new Vector3();
		this.labelOffset = new Vector2();
		this.labelBox = new Box2();
		this.lastFrame = 0;

		this.updateLabelOffset();

	}

	getWidth () {

		return this.geometry.width * this.material.scaleFactor;

	}

	getHeight () {

		return this.material.scaleFactor;

	}

	updateLabelOffset () {

		this.labelOffset.set( this.getWidth(), this.getHeight(), 0 );

		this.material.rotateVector( this.labelOffset );
		this.material.rotateVector( this.labelOffset );

	}

	updateLabelBox ( camera ) {

		const glyphMaterial = this.material;
		const scale = glyphMaterial.toScreenSpace;
		const labelOrigin = this.labelOrigin;

		// get box origin in screen space
		this.getWorldPosition( _ssLabelOrigin );

		labelOrigin.copy( _ssLabelOrigin );
		labelOrigin.project( camera );
		labelOrigin.multiply( scale );

		// rotate into alignment with text rotation
		glyphMaterial.rotateVector( labelOrigin );

		// find other corner = origin + offset (maintained in coords aligned with rotation)
		_labelEnd.copy( labelOrigin );
		_labelEnd.add( this.labelOffset );

		this.labelBox.setFromPoints( [ labelOrigin, _labelEnd ] );

	}

	getDepth( cameraManager ) {

		if ( this.lastFrame < cameraManager.getLastFrame() ) {

			this.updateLabelBox( cameraManager.activeCamera );
			this.lastFrame = cameraManager.getLastFrame();

		}

		// label origin in screen space
		return this.labelOrigin.z;

	}

	checkOcclusion ( labels, currentIndex ) {

		if ( ! this.visible ) return;

		const l = labels.length;

		for ( let i = currentIndex  + 1; i < l; i++ ) {

			const nextLabel = labels[ i ];

			if ( ! nextLabel.visible ) continue;

			if ( this.labelBox.intersectsBox( nextLabel.labelBox ) ) {

				this.visible = false;
				return;

			}

		}

	}

	raycast ( raycaster, intersects ) {

		if ( ! this.visible ) return intersects;

		const ray = raycaster.ray;
		const camera = raycaster.camera;
		const glyphMaterial = this.material;
		const scale = glyphMaterial.toScreenSpace;

		ray.at( 1, _ssOrigin );

		// ndc space [ - 1.0, 1.0 ]

		_ssOrigin.w = 1;

		_ssOrigin.applyMatrix4( camera.matrixWorldInverse );
		_ssOrigin.applyMatrix4( camera.projectionMatrix );
		_ssOrigin.multiplyScalar( 1 / _ssOrigin.w );

		// screen space
		_mouse.copy( _ssOrigin );
		_mouse.multiply( scale );

		// rotated screen space
		glyphMaterial.rotateVector( _mouse );

		// FIXME - we don't check for objects outside of view
		this.updateLabelBox( camera );

		if ( this.labelBox.containsPoint( _mouse ) ) {

			intersects.push( { object: this, distance: raycaster.ray.origin.distanceTo( _ssLabelOrigin ) } );

		}

		return intersects;

	}

}

class GlyphString extends GlyphStringBase {

	constructor ( text, glyphMaterial, ctx, yOffset ) {

		const glyphStringCache = ctx.glyphStringCache;

		let cache = glyphStringCache.get( glyphMaterial );

		if ( cache === undefined ) {

			// create material cache
			cache = new GlyphStringGeometryCache( glyphMaterial );
			glyphStringCache.set( glyphMaterial, cache );

		}

		const geometry = cache.getGeometry( text, yOffset );

		super( text, glyphMaterial, geometry );

		geometry.instanceBuffer.onUpload( Object3D.onUploadDropBuffer );

	}

}

class MutableGlyphString extends GlyphStringBase {

	constructor ( text, glyphMaterial ) {

		super( text, glyphMaterial, new GlyphStringGeometry( text, glyphMaterial.getAtlas() ) );

	}

	replaceString ( newstring ) {

		if ( newstring.length !== this.name.length ) {

			console.warn( 'new string has invalid length', newstring, this.name.length, newstring.length );
			return;

		}

		this.geometry.setString( newstring );
		this.updateLabelOffset();

	}

}

export { GlyphString, MutableGlyphString };<commit_msg>Add type for GlyphStrings<commit_after>import { Box2, Mesh, Object3D, Vector2, Vector3, Vector4 } from '../Three';
import { GlyphStringGeometry, GlyphStringGeometryCache } from './GlyphStringGeometry';

// temporary objects for raycasting

const _ssOrigin = new Vector4();
const _mouse = new Vector3();
const _labelEnd = new Vector3();
const _ssLabelOrigin = new Vector3();

class GlyphStringBase extends Mesh {

	constructor ( text, glyphMaterial, geometry ) {

		super( geometry, glyphMaterial );

		this.name = text;
		this.type = 'CV.GlyphString';

		this.labelOrigin = new Vector3();
		this.labelOffset = new Vector2();
		this.labelBox = new Box2();
		this.lastFrame = 0;

		this.updateLabelOffset();

	}

	getWidth () {

		return this.geometry.width * this.material.scaleFactor;

	}

	getHeight () {

		return this.material.scaleFactor;

	}

	updateLabelOffset () {

		this.labelOffset.set( this.getWidth(), this.getHeight(), 0 );

		this.material.rotateVector( this.labelOffset );
		this.material.rotateVector( this.labelOffset );

	}

	updateLabelBox ( camera ) {

		const glyphMaterial = this.material;
		const scale = glyphMaterial.toScreenSpace;
		const labelOrigin = this.labelOrigin;

		// get box origin in screen space
		this.getWorldPosition( _ssLabelOrigin );

		labelOrigin.copy( _ssLabelOrigin );
		labelOrigin.project( camera );
		labelOrigin.multiply( scale );

		// rotate into alignment with text rotation
		glyphMaterial.rotateVector( labelOrigin );

		// find other corner = origin + offset (maintained in coords aligned with rotation)
		_labelEnd.copy( labelOrigin );
		_labelEnd.add( this.labelOffset );

		this.labelBox.setFromPoints( [ labelOrigin, _labelEnd ] );

	}

	getDepth( cameraManager ) {

		if ( this.lastFrame < cameraManager.getLastFrame() ) {

			this.updateLabelBox( cameraManager.activeCamera );
			this.lastFrame = cameraManager.getLastFrame();

		}

		// label origin in screen space
		return this.labelOrigin.z;

	}

	checkOcclusion ( labels, currentIndex ) {

		if ( ! this.visible ) return;

		const l = labels.length;

		for ( let i = currentIndex  + 1; i < l; i++ ) {

			const nextLabel = labels[ i ];

			if ( ! nextLabel.visible ) continue;

			if ( this.labelBox.intersectsBox( nextLabel.labelBox ) ) {

				this.visible = false;
				return;

			}

		}

	}

	raycast ( raycaster, intersects ) {

		if ( ! this.visible ) return intersects;

		const ray = raycaster.ray;
		const camera = raycaster.camera;
		const glyphMaterial = this.material;
		const scale = glyphMaterial.toScreenSpace;

		ray.at( 1, _ssOrigin );

		// ndc space [ - 1.0, 1.0 ]

		_ssOrigin.w = 1;

		_ssOrigin.applyMatrix4( camera.matrixWorldInverse );
		_ssOrigin.applyMatrix4( camera.projectionMatrix );
		_ssOrigin.multiplyScalar( 1 / _ssOrigin.w );

		// screen space
		_mouse.copy( _ssOrigin );
		_mouse.multiply( scale );

		// rotated screen space
		glyphMaterial.rotateVector( _mouse );

		// FIXME - we don't check for objects outside of view
		this.updateLabelBox( camera );

		if ( this.labelBox.containsPoint( _mouse ) ) {

			intersects.push( { object: this, distance: raycaster.ray.origin.distanceTo( _ssLabelOrigin ) } );

		}

		return intersects;

	}

}

class GlyphString extends GlyphStringBase {

	constructor ( text, glyphMaterial, ctx, yOffset ) {

		const glyphStringCache = ctx.glyphStringCache;

		let cache = glyphStringCache.get( glyphMaterial );

		if ( cache === undefined ) {

			// create material cache
			cache = new GlyphStringGeometryCache( glyphMaterial );
			glyphStringCache.set( glyphMaterial, cache );

		}

		const geometry = cache.getGeometry( text, yOffset );

		super( text, glyphMaterial, geometry );

		geometry.instanceBuffer.onUpload( Object3D.onUploadDropBuffer );

	}

}

class MutableGlyphString extends GlyphStringBase {

	constructor ( text, glyphMaterial ) {

		super( text, glyphMaterial, new GlyphStringGeometry( text, glyphMaterial.getAtlas() ) );

	}

	replaceString ( newstring ) {

		if ( newstring.length !== this.name.length ) {

			console.warn( 'new string has invalid length', newstring, this.name.length, newstring.length );
			return;

		}

		this.geometry.setString( newstring );
		this.updateLabelOffset();

	}

}

export { GlyphString, MutableGlyphString };<|endoftext|>


<commit_before>/* Stage
 *
 */

var Stage = (function(document, hyperaudio) {

	function Stage(options) {

		var self = this;

		this.options = hyperaudio.extend({}, this.options, {

			entity: 'STAGE', // Not really an option... More like a manifest

			target: '#stage', // The selector of element for the staging area.

			id: '', // The ID of the saved mix.
			mix: {
				//title, desc, type, editable
				// url: [!content] The url of the mix
				// content: [!url] The actual mix HTML
			},

			title: 'Title not set',
			desc: 'Description not set',
			type: 'beta',

			idAttr: 'data-id', // Attribute name that holds the transcript ID.
			transAttr: 'data-trans', // Attribute name that holds the transcript URL. [optional if ID not present]
			mp4Attr: 'data-mp4', // Attribute name that holds the transcript mp4 URL.
			webmAttr: 'data-webm', // Attribute name that holds the transcript webm URL.
			ytAttr: 'data-yt', // Attribute name that holds the transcript youtube URL.
			unitAttr: 'data-unit', // Attribute name that holds the transcript Unit.

			word: 'a',
			section: 'section',
			// timeAttr: 'data-m', // Attribute name that holds the timing information.

			dragdropClass: 'dragdrop',
			async: true, // When true, some operations are delayed by a timeout.
			projector: null
		}, options);

		// State Flags.
		this.ready = false;
		this.enabled = true;

		// Properties
		this.target = typeof this.options.target === 'string' ? document.querySelector(this.options.target) : this.options.target;
		this.article = document.createElement('article');
		this.mix = {};

		// The following lines assume that we found a target.

		this.target.appendChild(this.article);

		// Detect when an effect value is changed
		this.target.addEventListener('change', function(e) {
			self.changed();
		}, false);

		// this.target._tap = new Tap({el: this.target});
		// this.target.addEventListener('tap', function(event) {
		this.target.addEventListener('click', function(event) {
			var section, word, search;
			// event.preventDefault(); // Removed since it breaks checkbox clicks in effects.
			if(event.target.nodeName.toLowerCase() === self.options.word) {
				word = event.target;
				search = word;

				// Search up the parent tree for the section.
				while(search) {
					if(search.nodeName.toLowerCase() === self.options.section) {
						section = search;
						break; // exit while loop
					}
					search = search.parentNode;
				}

				if(self.options.projector) {
					self.options.projector.playWord(section,word);
				}
			}
		}, false);

		if(this.options.DEBUG) {
			this._debug();
		}

		if(this.options.projector) {
			this.options.projector.setStage(this);
		}

		if(this.options.id || this.options.mix.url || this.options.mix.content) {
			this.load();
		}
	}

	Stage.prototype = {
		mixDetails: function(details) {
			// [SHOULD] only really used to set the label, desc and type of the mix being saved.
			hyperaudio.extend(this.options, details);
		},
		updateStage: function(content) {
			// Need to maintain the existing article in the stage - Important for dragdrop.
			var tmp = document.createElement('div'); // Temporary DOM element
			tmp.innerHTML = content; // Add the content to the DOM element
			var articleElem = tmp.querySelector('article'); // Find the article in the content.
			// Can now insert the contents of the returned mix article into the maintained article.
			this.article.innerHTML = articleElem.innerHTML;

			// TODO: Should also clear any existing attributes on the article.

			// Now copy over any attributes
			var attr = articleElem.attributes;
			for(var i=0, l=attr.length; i < l; i++ ) {
				this.article.setAttribute(attr[i].name, attr[i].value);
			}
		},
		load: function(id) {
			var self = this;

			if(typeof id !== 'undefined') {
				if(typeof id === 'string') {
					this.options.id = id;
					this.options.mix = {};
				} else if(typeof id === 'object') {
					this.options.id = '';
					this.options.mix = id;
				} else {
					this.options.id = '';
					this.options.mix = {};
				}
			}

			if(this.target) {

				if(this.options.id) {

					hyperaudio.api.getMix(id, function(success) {
						if(success) {
							self.mix = hyperaudio.extend({}, this.mix);
							self.mixDetails({
								title: self.mix.label,
								desc: self.mix.desc,
								type: self.mix.type
							});
							self.updateStage(self.mix.content);

							// Setup the dragdrop on the loaded mix sections.
							self.initDragDrop();
							self._trigger(hyperaudio.event.load, {msg: 'Loaded mix'});
						} else {
							self._error(this.status + ' ' + this.statusText + ' : "' + id + '"');
						}
					});
				} else {
					this.mixDetails({
						title: this.options.mix.title,
						desc: this.options.mix.desc,
						type: this.options.mix.type
					});
					if(this.options.mix.url) {
						hyperaudio.xhr({
							url: this.options.mix.url,
							complete: function(event) {
								self.updateStage(this.responseText);
								if(self.options.mix.editable) {
									self.initDragDrop();
								} else {
									self.changed();
								}
								self._trigger(hyperaudio.event.load, {msg: 'Loaded "' + self.options.mix.url + '"'});
							},
							error: function(event) {
								self.target.innerHTML = 'Problem with mix URL.'; // TMP - This sort of things should not be in the lib code, but acting off an error event hander.
								self._error(this.status + ' ' + this.statusText + ' : "' + self.options.mix.url + '"');
							}
						});
					} else if(this.options.mix.content) {
						this.updateStage(this.options.mix.content);
						if(this.options.mix.editable) {
							this.initDragDrop();
						} else {
							this.changed();
						}
						this._trigger(hyperaudio.event.load, {msg: 'Loaded given content'});
					} else {
						this.target.innerHTML = 'Problem with mix.'; // TMP - This sort of things should not be in the lib code, but acting off an error event hander.
						this._error('Stage : No ID, URL or Content');
					}
				}
			}
		},

		save: function(callback) {
			// Save the staged production

			var self = this;

			hyperaudio.extend(this.mix, {
				label: this.options.title,
				desc: this.options.desc,
				type: this.options.type,
				content: this.target.innerHTML
			});

			if(this.target) {

				// Fudge the user system since getUsername nay works.
				// hyperaudio.api.guest = false;
				// hyperaudio.api.username = 'tester';

				hyperaudio.api.putMix(this.mix, function(success) {
					if(success) {
						if(success.saved) {
							self.mix = hyperaudio.extend({}, this.mix);
							self._trigger(hyperaudio.event.save, {msg: 'Saved mix'});
						} else if(success.needLogin) {
							// We need to login
							self._trigger(hyperaudio.event.unauthenticated, {msg: 'Sign In required to save'});
						} else {
							self._error('Stage: Save: Error with API putMix() response');
						}
					} else {
						self._error('Stage: Save: Error with API putMix() request');
					}
					self.callback(callback, success);
				});
			}
		},

		callback: function(callback, success) {
			if(typeof callback === 'function') {
				callback.call(this, success);
			}
		},

		clear: function() {
			// TODO: Should also clear any existing attributes on the article.
			this.article.innerHTML = '';
			this.mix = {};
			this.options.id = '';
			this.changed(true);
		},

		parse: function() {
			var self = this,
				opts = this.options;

			// Will need the popcorn.transcript highlighting as per the source transcripts.
		},

		initDragDrop: function() {
			var self = this,
				i, l, sections, effectType, bgmTitle, dragHtml;

			var capitaliseFirstLetter = function(string) {
				return string.charAt(0).toUpperCase() + string.slice(1);
			};

			if(this.target) {
				sections = this.target.getElementsByTagName('section');
				l = sections.length;
				for(i=0; i < l; i++) {

					dragHtml = '';

					// This code is to setup the drag-and-drop with a nice label. Otherwise the effects look bad after loading back in and dragged
					effectType = sections[i].getAttribute('data-effect');
					if(typeof effectType === 'string') {
						switch(effectType) {
							case 'fade':
							case 'trim':
							case 'title':
								dragHtml = capitaliseFirstLetter(effectType);
								break;
							case 'bgm':
								bgmTitleElem = sections[i].querySelector('.icon-music');
								if(bgmTitleElem) {
									dragHtml = bgmTitleElem.parentNode.innerHTML;
								} else {
									dragHtml = 'BGM';
								}
								break;
						}
					}

					// And we finally setup the DragDrop
					self.dropped(sections[i], dragHtml);
				}
			}
		},

		dropped: function(el, html) {
			var self = this;
			var actions;
			var draggableClass = '';

			var editBlock = function (e) {
				e.stopPropagation();
				this.parentNode._editBlock = new EditBlock({
					el: this.parentNode,
					stage: self
				});
			};

			if(this.target) {
				// hyperaudio.removeClass(this.target, this.options.dragdropClass);

				// add edit action if needed
				if ( !(/(^|\s)effect($|\s)/.test(el.className)) ) {
					actions = el.querySelector('.actions');
					if(actions) {
						actions._tap = new Tap({el: actions});
						actions.addEventListener('tap', editBlock, false);
					}
				} else {
					draggableClass = 'draggableEffect';
				}

				// Setup item for future dragdrop
				el._dragInstance = new DragDrop({
					handle: el,
					dropArea: this.target,
					html: html ? html : el.innerHTML,
					draggableClass: draggableClass,
					onDragStart: function () {
						hyperaudio.addClass(self.target, self.options.dragdropClass);
					},
					onDrop: function () {
						hyperaudio.removeClass(self.target, self.options.dragdropClass);
						self.changed();
					}
				});

				this.changed();
			}
		},

		changed: function(reset) {
			// Tell the projector the content changed
			if(this.options.projector) {
				this.options.projector.requestUpdate(reset);
			}
			this._trigger(hyperaudio.event.change, {msg: 'The mix has changed'});
		},

		enable: function() {
			this.enabled = true;
		},
		disable: function() {
			this.enabled = false;
		}
	};

	return Stage;
}(document, hyperaudio));
<commit_msg>Saving a Mix now updates the url with the mix id.<commit_after>/* Stage
 *
 */

var Stage = (function(document, hyperaudio) {

	function Stage(options) {

		var self = this;

		this.options = hyperaudio.extend({}, this.options, {

			entity: 'STAGE', // Not really an option... More like a manifest

			target: '#stage', // The selector of element for the staging area.

			id: '', // The ID of the saved mix.
			mix: {
				//title, desc, type, editable
				// url: [!content] The url of the mix
				// content: [!url] The actual mix HTML
			},

			title: 'Title not set',
			desc: 'Description not set',
			type: 'beta',

			idAttr: 'data-id', // Attribute name that holds the transcript ID.
			transAttr: 'data-trans', // Attribute name that holds the transcript URL. [optional if ID not present]
			mp4Attr: 'data-mp4', // Attribute name that holds the transcript mp4 URL.
			webmAttr: 'data-webm', // Attribute name that holds the transcript webm URL.
			ytAttr: 'data-yt', // Attribute name that holds the transcript youtube URL.
			unitAttr: 'data-unit', // Attribute name that holds the transcript Unit.

			word: 'a',
			section: 'section',
			// timeAttr: 'data-m', // Attribute name that holds the timing information.

			dragdropClass: 'dragdrop',
			async: true, // When true, some operations are delayed by a timeout.
			projector: null
		}, options);

		// State Flags.
		this.ready = false;
		this.enabled = true;

		// Properties
		this.target = typeof this.options.target === 'string' ? document.querySelector(this.options.target) : this.options.target;
		this.article = document.createElement('article');
		this.mix = {};

		// The following lines assume that we found a target.

		this.target.appendChild(this.article);

		// Detect when an effect value is changed
		this.target.addEventListener('change', function(e) {
			self.changed();
		}, false);

		// this.target._tap = new Tap({el: this.target});
		// this.target.addEventListener('tap', function(event) {
		this.target.addEventListener('click', function(event) {
			var section, word, search;
			// event.preventDefault(); // Removed since it breaks checkbox clicks in effects.
			if(event.target.nodeName.toLowerCase() === self.options.word) {
				word = event.target;
				search = word;

				// Search up the parent tree for the section.
				while(search) {
					if(search.nodeName.toLowerCase() === self.options.section) {
						section = search;
						break; // exit while loop
					}
					search = search.parentNode;
				}

				if(self.options.projector) {
					self.options.projector.playWord(section,word);
				}
			}
		}, false);

		if(this.options.DEBUG) {
			this._debug();
		}

		if(this.options.projector) {
			this.options.projector.setStage(this);
		}

		if(this.options.id || this.options.mix.url || this.options.mix.content) {
			this.load();
		}
	}

	Stage.prototype = {
		mixDetails: function(details) {
			// [SHOULD] only really used to set the label, desc and type of the mix being saved.
			hyperaudio.extend(this.options, details);
		},
		updateStage: function(content) {
			// Need to maintain the existing article in the stage - Important for dragdrop.
			var tmp = document.createElement('div'); // Temporary DOM element
			tmp.innerHTML = content; // Add the content to the DOM element
			var articleElem = tmp.querySelector('article'); // Find the article in the content.
			// Can now insert the contents of the returned mix article into the maintained article.
			this.article.innerHTML = articleElem.innerHTML;

			// TODO: Should also clear any existing attributes on the article.

			// Now copy over any attributes
			var attr = articleElem.attributes;
			for(var i=0, l=attr.length; i < l; i++ ) {
				this.article.setAttribute(attr[i].name, attr[i].value);
			}
		},
		load: function(id) {
			var self = this;

			if(typeof id !== 'undefined') {
				if(typeof id === 'string') {
					this.options.id = id;
					this.options.mix = {};
				} else if(typeof id === 'object') {
					this.options.id = '';
					this.options.mix = id;
				} else {
					this.options.id = '';
					this.options.mix = {};
				}
			}

			if(this.target) {

				if(this.options.id) {

					hyperaudio.api.getMix(id, function(success) {
						if(success) {
							self.mix = hyperaudio.extend({}, this.mix);
							self.mixDetails({
								title: self.mix.label,
								desc: self.mix.desc,
								type: self.mix.type
							});
							self.updateStage(self.mix.content);

							// Setup the dragdrop on the loaded mix sections.
							self.initDragDrop();
							self._trigger(hyperaudio.event.load, {msg: 'Loaded mix'});
						} else {
							self._error(this.status + ' ' + this.statusText + ' : "' + id + '"');
						}
					});
				} else {
					this.mixDetails({
						title: this.options.mix.title,
						desc: this.options.mix.desc,
						type: this.options.mix.type
					});
					if(this.options.mix.url) {
						hyperaudio.xhr({
							url: this.options.mix.url,
							complete: function(event) {
								self.updateStage(this.responseText);
								if(self.options.mix.editable) {
									self.initDragDrop();
								} else {
									self.changed();
								}
								self._trigger(hyperaudio.event.load, {msg: 'Loaded "' + self.options.mix.url + '"'});
							},
							error: function(event) {
								self.target.innerHTML = 'Problem with mix URL.'; // TMP - This sort of things should not be in the lib code, but acting off an error event hander.
								self._error(this.status + ' ' + this.statusText + ' : "' + self.options.mix.url + '"');
							}
						});
					} else if(this.options.mix.content) {
						this.updateStage(this.options.mix.content);
						if(this.options.mix.editable) {
							this.initDragDrop();
						} else {
							this.changed();
						}
						this._trigger(hyperaudio.event.load, {msg: 'Loaded given content'});
					} else {
						this.target.innerHTML = 'Problem with mix.'; // TMP - This sort of things should not be in the lib code, but acting off an error event hander.
						this._error('Stage : No ID, URL or Content');
					}
				}
			}
		},

		save: function(callback) {
			// Save the staged production

			var self = this;

			hyperaudio.extend(this.mix, {
				label: this.options.title,
				desc: this.options.desc,
				type: this.options.type,
				content: this.target.innerHTML
			});

			if(this.target) {

				// Fudge the user system since getUsername nay works.
				// hyperaudio.api.guest = false;
				// hyperaudio.api.username = 'tester';

				hyperaudio.api.putMix(this.mix, function(success) {
					if(success) {
						if(success.saved) {
							self.mix = hyperaudio.extend({}, this.mix);
							if(!window.top.document.location.href.match(/[?&]m=/)) {
								window.top.history.pushState({
									mix: self.mix._id
								}, 'HAP: ' + self.mix.label, '?m=' + self.mix._id);
								// console.log('[stage|save] pushed to history')
							}
							self._trigger(hyperaudio.event.save, {msg: 'Saved mix'});
						} else if(success.needLogin) {
							// We need to login
							self._trigger(hyperaudio.event.unauthenticated, {msg: 'Sign In required to save'});
						} else {
							self._error('Stage: Save: Error with API putMix() response');
						}
					} else {
						self._error('Stage: Save: Error with API putMix() request');
					}
					self.callback(callback, success);
				});
			}
		},

		callback: function(callback, success) {
			if(typeof callback === 'function') {
				callback.call(this, success);
			}
		},

		clear: function() {
			// TODO: Should also clear any existing attributes on the article.
			this.article.innerHTML = '';
			this.mix = {};
			this.options.id = '';
			this.changed(true);
			window.top.history.pushState({}, 'HAP', '?');
		},

		parse: function() {
			var self = this,
				opts = this.options;

			// Will need the popcorn.transcript highlighting as per the source transcripts.
		},

		initDragDrop: function() {
			var self = this,
				i, l, sections, effectType, bgmTitle, dragHtml;

			var capitaliseFirstLetter = function(string) {
				return string.charAt(0).toUpperCase() + string.slice(1);
			};

			if(this.target) {
				sections = this.target.getElementsByTagName('section');
				l = sections.length;
				for(i=0; i < l; i++) {

					dragHtml = '';

					// This code is to setup the drag-and-drop with a nice label. Otherwise the effects look bad after loading back in and dragged
					effectType = sections[i].getAttribute('data-effect');
					if(typeof effectType === 'string') {
						switch(effectType) {
							case 'fade':
							case 'trim':
							case 'title':
								dragHtml = capitaliseFirstLetter(effectType);
								break;
							case 'bgm':
								bgmTitleElem = sections[i].querySelector('.icon-music');
								if(bgmTitleElem) {
									dragHtml = bgmTitleElem.parentNode.innerHTML;
								} else {
									dragHtml = 'BGM';
								}
								break;
						}
					}

					// And we finally setup the DragDrop
					self.dropped(sections[i], dragHtml);
				}
			}
		},

		dropped: function(el, html) {
			var self = this;
			var actions;
			var draggableClass = '';

			var editBlock = function (e) {
				e.stopPropagation();
				this.parentNode._editBlock = new EditBlock({
					el: this.parentNode,
					stage: self
				});
			};

			if(this.target) {
				// hyperaudio.removeClass(this.target, this.options.dragdropClass);

				// add edit action if needed
				if ( !(/(^|\s)effect($|\s)/.test(el.className)) ) {
					actions = el.querySelector('.actions');
					if(actions) {
						actions._tap = new Tap({el: actions});
						actions.addEventListener('tap', editBlock, false);
					}
				} else {
					draggableClass = 'draggableEffect';
				}

				// Setup item for future dragdrop
				el._dragInstance = new DragDrop({
					handle: el,
					dropArea: this.target,
					html: html ? html : el.innerHTML,
					draggableClass: draggableClass,
					onDragStart: function () {
						hyperaudio.addClass(self.target, self.options.dragdropClass);
					},
					onDrop: function () {
						hyperaudio.removeClass(self.target, self.options.dragdropClass);
						self.changed();
					}
				});

				this.changed();
			}
		},

		changed: function(reset) {
			// Tell the projector the content changed
			if(this.options.projector) {
				this.options.projector.requestUpdate(reset);
			}
			this._trigger(hyperaudio.event.change, {msg: 'The mix has changed'});
		},

		enable: function() {
			this.enabled = true;
		},
		disable: function() {
			this.enabled = false;
		}
	};

	return Stage;
}(document, hyperaudio));
<|endoftext|>


<commit_before>const t9e = function() {};

/**
 * Default size for new Tic-Tac-Toe boards.
 */

t9e.prototype.DEFAULT_SIZE = 3;

/**
 * Generate a square multidimensional array of arbitrary size.
 *
 * @param {number} [size=3] - Size of grid (x and y).
 * @param {function=} callback - Optional callback to prepopulate fields.
 * @return {array} array - Square multidimensional array.
 */

t9e.prototype.grid = function(size, callback) {
    if (size === undefined) {
        size = this.DEFAULT_SIZE;
    }

    let array = [...Array(size).keys()];

    if (this.isFunction(callback)) {
        array = array.map(callback);
    }

    return array.map(() => array);
};

/**
 * Test whether a variable is a function.
 *
 * @param {object} callback - Object to test.
 * @return {bool} - Object is a function, true/false.
 */

t9e.prototype.isFunction = function(callback) {
    return callback && Object.prototype.toString.call(callback) === '[object Function]';
};

/**
 * Transpose an array.
 *
 * This is used to assist in evaluation of win conditions: Rather than change the
 * test to fit board state, it's easier to fit the board to a single win
 * condition (by turning the board sideways).
 *
 *  1. If array is empty or falsy, return an array.
 *  2. If array[0] is not an array itself, return the parent reversed.
 *
 * @param {array} array - Multidimensional array of board cells.
 * @return {array} array - Original array transposed.
 */

t9e.prototype.transpose = function(array) {
    if (!array || !array.length) {
        return [];
    }

    if (!Array.isArray(array[0])) {
        return array.slice().reverse();
    }

    return array[0].map((_, index) => array.map(row => row[index]));
};

/**
 * Return the diagonal elements of a square multidimensional array.
 *
 * @param {array} array - Multidimensional array of board cells.
 * @param {array} array - Multidimensional array of diagonal elements.
 */

t9e.prototype.diagonals = function(array) {
    const reversed = array.slice().reverse();

    return [
        array.map((element, index) => array[index][index]),
        array.map((element, index) => reversed[index][index])
    ];
};

/**
 * Test whether a board has _any_ empty cells.
 *
 * '', null and undefined are falsy in JavaScript, and thus coerce to false.
 * The inner loops sees whether any cell contains a falsy value.
 *
 * @param {array} board - Multidimensional array of board cells.
 * @return {bool} - At least one cell of the board is empty, true/false.
 */

t9e.prototype.anyEmpty = function(board) {
    return board.some(row => row.some(cell => !cell));
};

/**
 * Test whether an array is a sqare multidimensional array.
 *
 * @param {array} array
 * @return {bool} - Array is multidimensonal and square, true/false.
 */

t9e.prototype.isSquare = function(array) {
    const size = array.length;

    return array.every(element => {
        return Array.isArray(element) && element.length === size;
    });
};

/**
 * Test whether a row of the passed game board contains the supplied character,
 * while the board is in the following configurations:
 *
 *  1. Board rows.
 *  2. Board columns (transposed to rows).
 *  3. Board diagonals (converted to rows).
 *
 * To explain: Rather than changing the test pattern to fit every board
 * configuration, it is easier to change the configuration to fit a single test
 * pattern:
 *
 * Does any row of the board contain _only_ the supplied character?
 *
 * @param {string} character - Character to test.
 * @param {array} board - Multidimensional array to test.
 * @param {bool} - A row of the board all contain the given character, true/false.
 */

t9e.prototype.check = function(character, board) {
    if (!board || !Array.isArray(board) || !this.isSquare(board)) {
        return false;
    }

    return [].concat(board, this.transpose(board), this.diagonals(board)).some(row => {
        return row.every(cell => cell === character);
    });
};

module.exports = Object.create(t9e.prototype);
<commit_msg>Changed callback to passed value for simplicity.<commit_after>const t9e = function() {};

/**
 * Default size for new Tic-Tac-Toe boards.
 */

t9e.prototype.DEFAULT_SIZE = 3;

/**
 * Generate a square multidimensional array of arbitrary size.
 *
 * @param {number} [size=3] - Size of grid (x and y).
 * @param {value=} value- Optional value to prepopulate into fields.
 * @return {array} array - Square multidimensional array.
 */

t9e.prototype.grid = function(size, value) {
    if (size === undefined) {
        size = this.DEFAULT_SIZE;
    }

    let array = [...Array(size).keys()];

    if (value !== undefined) {
        array = array.map(() => value);
    }

    return array.map(() => array);
};

/**
 * Transpose an array.
 *
 * This is used to assist in evaluation of win conditions: Rather than change the
 * test to fit board state, it's easier to fit the board to a single win
 * condition (by turning the board sideways).
 *
 *  1. If array is empty or falsy, return an array.
 *  2. If array[0] is not an array itself, return the parent reversed.
 *
 * @param {array} array - Multidimensional array of board cells.
 * @return {array} array - Original array transposed.
 */

t9e.prototype.transpose = function(array) {
    if (!array || !array.length) {
        return [];
    }

    if (!Array.isArray(array[0])) {
        return array.slice().reverse();
    }

    return array[0].map((_, index) => array.map(row => row[index]));
};

/**
 * Return the diagonal elements of a square multidimensional array.
 *
 * @param {array} array - Multidimensional array of board cells.
 * @param {array} array - Multidimensional array of diagonal elements.
 */

t9e.prototype.diagonals = function(array) {
    const reversed = array.slice().reverse();

    return [
        array.map((element, index) => array[index][index]),
        array.map((element, index) => reversed[index][index])
    ];
};

/**
 * Test whether a board has _any_ empty cells.
 *
 * '', null and undefined are falsy in JavaScript, and thus coerce to false.
 * The inner loops sees whether any cell contains a falsy value.
 *
 * @param {array} board - Multidimensional array of board cells.
 * @return {bool} - At least one cell of the board is empty, true/false.
 */

t9e.prototype.anyEmpty = function(board) {
    return board.some(row => row.some(cell => !cell));
};

/**
 * Test whether an array is a sqare multidimensional array.
 *
 * @param {array} array
 * @return {bool} - Array is multidimensonal and square, true/false.
 */

t9e.prototype.isSquare = function(array) {
    const size = array.length;

    return array.every(element => {
        return Array.isArray(element) && element.length === size;
    });
};

/**
 * Test whether a row of the passed game board contains the supplied character,
 * while the board is in the following configurations:
 *
 *  1. Board rows.
 *  2. Board columns (transposed to rows).
 *  3. Board diagonals (converted to rows).
 *
 * To explain: Rather than changing the test pattern to fit every board
 * configuration, it is easier to change the configuration to fit a single test
 * pattern:
 *
 * Does any row of the board contain _only_ the supplied character?
 *
 * @param {string} character - Character to test.
 * @param {array} board - Multidimensional array to test.
 * @param {bool} - A row of the board all contain the given character, true/false.
 */

t9e.prototype.check = function(character, board) {
    if (!board || !Array.isArray(board) || !this.isSquare(board)) {
        return false;
    }

    return [].concat(board, this.transpose(board), this.diagonals(board)).some(row => {
        return row.every(cell => cell === character);
    });
};

module.exports = Object.create(t9e.prototype);
<|endoftext|>


<commit_before>'use strict';

var sugar = require('sugar');
var irc = require('irc');
var webSocket = require('ws');

var channel = process.env.CHANNEL;
if (typeof channel === 'undefined') {
    console.log('ERROR: Must speficy environment variable "CHANNEL"!');
    process.exit();
}

var webAddress = 'http://www.caseyliss.com/showbot';
var TITLE_LIMIT = 75;

var titles = [];
var connections = [];
var links = [];

function sendToAll(packet) {
    connections.forEach(function (connection) {
        try {
            connection.send(JSON.stringify(packet));
        } catch (e) {
            console.log('sendToAll error: ' + e);
        }
    });
}

setInterval(saveBackup, 300000);

function saveBackup() {
    // TODO: Figure out what to do here.
}

function handleNewSuggestion(from, message) {
    var title = '';
    if (message.match(/^!s(?:uggest)?\s+(.+)/)) {
        title = RegExp.$1.compact();
    }

    if (title.length > TITLE_LIMIT) {
        client.say(from, 'That title is too long (over ' + TITLE_LIMIT +
            ' characters); please try again.');
        title = '';
    }
    if (title.length > 0) {

		var normalizedTitle = normalize(title);

        // Make sure this isn't a duplicate.
        if (titles.findAll({normalized: normalizedTitle}).length === 0) {
            title = {
                id: titles.length,
                author: from,
                title: title,
                normalized: normalizedTitle,
                votes: 0,
                votesBy: [],
                time: new Date()
            };
            titles.push(title);

            sendToAll({operation: 'NEW', title: title});
        } else {
            //client.say(channel, 'Sorry, ' + from + ', your title is a duplicate. Please try another!');
            client.say(from, 'Sorry, your title is a duplicate. Please try another!');
        }
    }
}

function normalize(title) {
	// Strip trailing periods from title
	title = title.toLowerCase();
	title = title.replace(/^[.\s]+|[.\s]+$/g, '');

	return title;
}

function handleSendVotes(from, message) {
    var titlesByVote = titles.sortBy(function (t) {
        return t.votes;
    }, true).to(3);

    client.say(from, 'Three most popular titles:');
    for (var i = 0; i < titlesByVote.length; ++i) {
        var votes = titlesByVote[i]['votes'];
        client.say(from, titlesByVote[i]['votes'] + ' vote' + (votes != 1 ? 's' : '') +  ': " ' + titlesByVote[i].title + '"');
    }
}

function handleNewLink(from, message) {
    if (message.startsWith('!link')) {
        message = message.substring(6);
    } else if (message.startsWith('!l')) {
        message = message.substring(3);
    }

    if (message.startsWith('http')) {
        var link = {
            id: links.length,
            author: from,
            link: message,
            time: new Date()
        };
        links.push(link);

        sendToAll({operation: 'NEWLINK', link: link});
    } else {
        client.say(from, "That doesn't look like a link to me.");
    }
}

function handleHelp(from) {
    client.say(from, 'Options:');
    client.say(from, '!s {title} - suggest a title.');
    client.say(from, '!votes - get the three most highly voted titles.');
    client.say(from, '!link {URL} - suggest a link.');
    client.say(from, '!help - see this message.');
    client.say(from, 'To see titles/links, go to: ' + webAddress);
}

var client = new irc.Client('irc.freenode.net', 'accidentalbot', {
    channels: [channel]
});

client.addListener('join', function (channel, nick, message) {
    if (nick === client.nick) {
        console.log("Joined channel " + channel + ".");
    }
});

client.addListener('connect', function() {
    console.log("Connected to IRC.");
});

client.addListener('kick', function (channel, nick, by, reason) {
    if (nick === client.nick) {
        console.log("Kicked from channel " + channel + " by " + by + " because " + reason + ".");
    }
});

client.addListener('message', function (from, to, message) {
    if (message.startsWith("!votes")) {
        handleSendVotes(from, message);
    } else if (message.startsWith('!l')) {
        handleNewLink(from, message);
    } else if (message.startsWith('!help')) {
        handleHelp(from);
    }
});

client.addListener('message#', function (from, to, message) {
   if (message.startsWith("!s ")) {
       handleNewSuggestion(from, message);
   }
});

client.addListener('pm', function (from, message) {
   if (message.startsWith('!s')) {
        client.say(from, "I'm sorry, suggestions can only be made in " + channel + ".");
   }
});

client.addListener('error', function (message) {
    console.log('error: ', message);
});

/***************************************************
 * WEB SOCKETS                                     *
 ***************************************************/

var port = Number(process.env.PORT || 5001);
var proxied = process.env.PROXIED === 'true';
var socketServer = new webSocket.Server({port: port});

// DOS protection - we disconnect any address which sends more than windowLimit
// messages in a window of windowSize milliseconds.
var windowLimit = 50;
var windowSize = 5000;
var currentWindow = 0;
var recentMessages = {};
function floodedBy(socket) {
    // To be called each time we get a message or connection attempt.
    //
    // If that address has been flooding us, we disconnect all open connections
    // from that address and return `true` to indicate that it should be
    // ignored. (They will not be prevented from re-connecting after waiting
    // for the next window.)
    if (socket.readyState == socket.CLOSED) {
        return true;
    }

    var address = getRequestAddress(socket.upgradeReq);

    var updatedWindow = 0 | ((new Date) / windowSize);
    if (currentWindow !== updatedWindow) {
        currentWindow = updatedWindow;
        recentMessages = {};
    }

    if (address in recentMessages) {
        recentMessages[address]++;
    } else {
        recentMessages[address] = 1;
    }

    if (recentMessages[address] > windowLimit) {
        console.warn("Disconnecting flooding address: " + address);
        socket.terminate();

        for (var i = 0, l = connections.length; i < l; i++) {
            if (getRequestAddress(connections[i].upgradeReq) === address &&
                connections[i] != socket) {
                console.log("Disconnecting additional connection.");
                connections[i].terminate();
            }
        }

        return true;
    } else {
        return false;
    }
}

function getRequestAddress(request) {
    if (proxied && 'x-forwarded-for' in request.headers) {
        // This assumes that the X-Forwarded-For header is generated by a
        // trusted proxy such as Heroku. If not, a malicious user could take
        // advantage of this logic and use it to to spoof their IP.
        var forwardedForAddresses = request.headers['x-forwarded-for'].split(',');
        return forwardedForAddresses[forwardedForAddresses.length - 1].trim();
    } else {
        // This is valid for direct deployments, without routing/load balancing.
        return request.connection.remoteAddress;
    }
}

socketServer.on('connection', function(socket) {
    if (floodedBy(socket)) return;

    connections.push(socket);
    var address = getRequestAddress(socket.upgradeReq);
    console.log('Client connected: ' + address);

    // Instead of sending all of the information about current titles to the
    // newly-connecting user, which would include the IP addresses of other
    // users, we just send down the information they need.
    var titlesWithVotes = titles.map(function (title) {
        var isVoted = title.votesBy.some(function (testAddress) {
            return testAddress === address;
        });
        var newTitle = {
            id: title.id,
            author: title.author,
            title: title.title,
            votes: title.votes,
            voted: isVoted,
            time: title.time
        };
        return newTitle;
    });
    socket.send(JSON.stringify({operation: 'REFRESH', titles: titlesWithVotes, links: links}));

    socket.on('close', function () {
        console.log('Client disconnected: ' + address);
        connections.splice(connections.indexOf(socket), 1);
    });

    socket.on('error', function (reason, code) {
      console.log('socket error: reason ' + reason + ', code ' + code);
    });

    socket.on('message', function (data, flags) {
        if (floodedBy(socket)) return;

        if (flags.binary) {
            console.log("ignoring binary message from "  + address);
            return;
        }

        var packet;
        try {
            packet = JSON.parse(data);
        } catch (e) {
            console.log('error: malformed JSON message (' + e + '): '+ data);
            return;
        }

        if (packet.operation === 'VOTE') {
            var matches = titles.findAll({id: packet['id']});

            if (matches.length > 0) {
                var upvoted = matches[0];
                if (upvoted['votesBy'].any(address) === false) {
                    upvoted['votes'] = Number(upvoted['votes']) + 1;
                    upvoted['votesBy'].push(address);
                    sendToAll({operation: 'VOTE', votes: upvoted['votes'], id: upvoted['id']});
                    console.log('+1 for ' + upvoted['title'] + ' by ' + address);
                } else {
                    console.log('ignoring duplicate vote by ' + address + ' for ' + upvoted['title']);
                }
            } else {
                console.log('no matches for id: ' + packet['id']);
            }
        } else if (packet.operation === 'PING') {
            socket.send(JSON.stringify({operation: 'PONG'}));
        } else {
            console.log("Don't know what to do with " + packet['operation']);
        }
    });
});
<commit_msg>title deduplication<commit_after>'use strict';

var sugar = require('sugar');
var irc = require('irc');
var webSocket = require('ws');

var channel = process.env.CHANNEL;
if (typeof channel === 'undefined') {
    console.log('ERROR: Must speficy environment variable "CHANNEL"!');
    process.exit();
}

var webAddress = 'http://www.caseyliss.com/showbot';
var TITLE_LIMIT = 75;

var titles = [];
var connections = [];
var links = [];

function sendToAll(packet) {
    connections.forEach(function (connection) {
        try {
            connection.send(JSON.stringify(packet));
        } catch (e) {
            console.log('sendToAll error: ' + e);
        }
    });
}

setInterval(saveBackup, 300000);

function saveBackup() {
    // TODO: Figure out what to do here.
}

function handleNewSuggestion(from, message) {
    var title = '';
    if (message.match(/^!s(?:uggest)?\s+(.+)/)) {
        title = RegExp.$1.compact();
    }

    if (title.length > TITLE_LIMIT) {
        client.say(from, 'That title is too long (over ' + TITLE_LIMIT +
            ' characters); please try again.');
        title = '';
    }
    if (title.length > 0) {

		var normalizedTitle = normalize(title);

        // Make sure this isn't a duplicate.
        if (titles.findAll({normalized: normalizedTitle}).length === 0) {
            title = {
                id: titles.length,
                author: from,
                title: title,
                normalized: normalizedTitle,
                votes: 0,
                votesBy: [],
                time: new Date()
            };
            titles.push(title);

            sendToAll({operation: 'NEW', title: title});
        } else {
            //client.say(channel, 'Sorry, ' + from + ', your title is a duplicate. Please try another!');
            client.say(from, 'Sorry, your title is a duplicate. Please try another!');
        }
    }
}

function normalize(title) {
	// Strip trailing periods from title
	title = title.toLowerCase();
	title = title.replace(/[^a-zA-Z0-9]+/g, '');

	return title;
}

function handleSendVotes(from, message) {
    var titlesByVote = titles.sortBy(function (t) {
        return t.votes;
    }, true).to(3);

    client.say(from, 'Three most popular titles:');
    for (var i = 0; i < titlesByVote.length; ++i) {
        var votes = titlesByVote[i]['votes'];
        client.say(from, titlesByVote[i]['votes'] + ' vote' + (votes != 1 ? 's' : '') +  ': " ' + titlesByVote[i].title + '"');
    }
}

function handleNewLink(from, message) {
    if (message.startsWith('!link')) {
        message = message.substring(6);
    } else if (message.startsWith('!l')) {
        message = message.substring(3);
    }

    if (message.startsWith('http')) {
        var link = {
            id: links.length,
            author: from,
            link: message,
            time: new Date()
        };
        links.push(link);

        sendToAll({operation: 'NEWLINK', link: link});
    } else {
        client.say(from, "That doesn't look like a link to me.");
    }
}

function handleHelp(from) {
    client.say(from, 'Options:');
    client.say(from, '!s {title} - suggest a title.');
    client.say(from, '!votes - get the three most highly voted titles.');
    client.say(from, '!link {URL} - suggest a link.');
    client.say(from, '!help - see this message.');
    client.say(from, 'To see titles/links, go to: ' + webAddress);
}

var client = new irc.Client('irc.freenode.net', 'accidentalbot', {
    channels: [channel]
});

client.addListener('join', function (channel, nick, message) {
    if (nick === client.nick) {
        console.log("Joined channel " + channel + ".");
    }
});

client.addListener('connect', function() {
    console.log("Connected to IRC.");
});

client.addListener('kick', function (channel, nick, by, reason) {
    if (nick === client.nick) {
        console.log("Kicked from channel " + channel + " by " + by + " because " + reason + ".");
    }
});

client.addListener('message', function (from, to, message) {
    if (message.startsWith("!votes")) {
        handleSendVotes(from, message);
    } else if (message.startsWith('!l')) {
        handleNewLink(from, message);
    } else if (message.startsWith('!help')) {
        handleHelp(from);
    }
});

client.addListener('message#', function (from, to, message) {
   if (message.startsWith("!s ")) {
       handleNewSuggestion(from, message);
   }
});

client.addListener('pm', function (from, message) {
   if (message.startsWith('!s')) {
        client.say(from, "I'm sorry, suggestions can only be made in " + channel + ".");
   }
});

client.addListener('error', function (message) {
    console.log('error: ', message);
});

/***************************************************
 * WEB SOCKETS                                     *
 ***************************************************/

var port = Number(process.env.PORT || 5001);
var proxied = process.env.PROXIED === 'true';
var socketServer = new webSocket.Server({port: port});

// DOS protection - we disconnect any address which sends more than windowLimit
// messages in a window of windowSize milliseconds.
var windowLimit = 50;
var windowSize = 5000;
var currentWindow = 0;
var recentMessages = {};
function floodedBy(socket) {
    // To be called each time we get a message or connection attempt.
    //
    // If that address has been flooding us, we disconnect all open connections
    // from that address and return `true` to indicate that it should be
    // ignored. (They will not be prevented from re-connecting after waiting
    // for the next window.)
    if (socket.readyState == socket.CLOSED) {
        return true;
    }

    var address = getRequestAddress(socket.upgradeReq);

    var updatedWindow = 0 | ((new Date) / windowSize);
    if (currentWindow !== updatedWindow) {
        currentWindow = updatedWindow;
        recentMessages = {};
    }

    if (address in recentMessages) {
        recentMessages[address]++;
    } else {
        recentMessages[address] = 1;
    }

    if (recentMessages[address] > windowLimit) {
        console.warn("Disconnecting flooding address: " + address);
        socket.terminate();

        for (var i = 0, l = connections.length; i < l; i++) {
            if (getRequestAddress(connections[i].upgradeReq) === address &&
                connections[i] != socket) {
                console.log("Disconnecting additional connection.");
                connections[i].terminate();
            }
        }

        return true;
    } else {
        return false;
    }
}

function getRequestAddress(request) {
    if (proxied && 'x-forwarded-for' in request.headers) {
        // This assumes that the X-Forwarded-For header is generated by a
        // trusted proxy such as Heroku. If not, a malicious user could take
        // advantage of this logic and use it to to spoof their IP.
        var forwardedForAddresses = request.headers['x-forwarded-for'].split(',');
        return forwardedForAddresses[forwardedForAddresses.length - 1].trim();
    } else {
        // This is valid for direct deployments, without routing/load balancing.
        return request.connection.remoteAddress;
    }
}

socketServer.on('connection', function(socket) {
    if (floodedBy(socket)) return;

    connections.push(socket);
    var address = getRequestAddress(socket.upgradeReq);
    console.log('Client connected: ' + address);

    // Instead of sending all of the information about current titles to the
    // newly-connecting user, which would include the IP addresses of other
    // users, we just send down the information they need.
    var titlesWithVotes = titles.map(function (title) {
        var isVoted = title.votesBy.some(function (testAddress) {
            return testAddress === address;
        });
        var newTitle = {
            id: title.id,
            author: title.author,
            title: title.title,
            votes: title.votes,
            voted: isVoted,
            time: title.time
        };
        return newTitle;
    });
    socket.send(JSON.stringify({operation: 'REFRESH', titles: titlesWithVotes, links: links}));

    socket.on('close', function () {
        console.log('Client disconnected: ' + address);
        connections.splice(connections.indexOf(socket), 1);
    });

    socket.on('error', function (reason, code) {
      console.log('socket error: reason ' + reason + ', code ' + code);
    });

    socket.on('message', function (data, flags) {
        if (floodedBy(socket)) return;

        if (flags.binary) {
            console.log("ignoring binary message from "  + address);
            return;
        }

        var packet;
        try {
            packet = JSON.parse(data);
        } catch (e) {
            console.log('error: malformed JSON message (' + e + '): '+ data);
            return;
        }

        if (packet.operation === 'VOTE') {
            var matches = titles.findAll({id: packet['id']});

            if (matches.length > 0) {
                var upvoted = matches[0];
                if (upvoted['votesBy'].any(address) === false) {
                    upvoted['votes'] = Number(upvoted['votes']) + 1;
                    upvoted['votesBy'].push(address);
                    sendToAll({operation: 'VOTE', votes: upvoted['votes'], id: upvoted['id']});
                    console.log('+1 for ' + upvoted['title'] + ' by ' + address);
                } else {
                    console.log('ignoring duplicate vote by ' + address + ' for ' + upvoted['title']);
                }
            } else {
                console.log('no matches for id: ' + packet['id']);
            }
        } else if (packet.operation === 'PING') {
            socket.send(JSON.stringify({operation: 'PONG'}));
        } else {
            console.log("Don't know what to do with " + packet['operation']);
        }
    });
});
<|endoftext|>


<commit_before>import TileCanvasLayer from './EsriTileCanvasBase';
import declare from 'dojo/_base/declare';

function pad (num) {
  var str = '00' + num;
  return str.slice(str.length - 3);
}

export default declare('GladLayer', [TileCanvasLayer], {

  filter: function (data) {
    for (var i = 0; i < data.length; i += 4) {
      // Decode the rgba/pixel so I can filter on confidence and date ranges
      var slice = [data[i], data[i + 1], data[i + 2]];
      var values = this.decodeDate(slice);
      //- Check against confidence, min date, and max date
      if (
        values.date >= this.options.minDateValue &&
        values.date <= this.options.maxDateValue &&
        this.options.confidence.indexOf(values.confidence) > -1
      ) {
        // Set the alpha to the intensity
        data[i + 3] = values.intensity;
        // Make the pixel pink for glad alerts
        // Note, this may mess up the decode date function if it's called at a future date as the decoded information comes from the pixel
        data[i] = 220; // R
        data[i + 1] = 102; // G
        data[i + 2] = 153; // B
      } else {
        // Hide the pixel
        data[i + 3] = 0;
      }
    }
    return data;
  },

  decodeDate: function (pixel) {
    // Find the total days of the pixel by multiplying the red band by 255 and adding the green band
    const totalDays = (pixel[0] * 255) + pixel[1];
    // Dived the total days by 365 to get the year offset, add 15 to this to get current year
    // Example, parseInt(totalDays / 365) = 1, add 15, year is 2016
    const yearAsInt = totalDays / 365 + 15;
    // Multiple by 1000 to get in YYDDD format, i.e. 15000 or 16000
    const year = yearAsInt * 1000;
    // Add the remaining days to get the julian day for that year
    const julianDay = totalDays % 365;
    // Add julian to year to get the data value
    const date = ~~(year + julianDay);
    // Convert the blue band to a string and pad with 0's to three digits
    // It's rarely not three digits, except for cases where there is an intensity value and no date/confidence.
    // This is due to bilinear resampling
    const band3Str = pad(pixel[2]);
    // Parse confidence, confidence is stored as 1/2, subtract 1 so it's values are 0/1
    const confidence = ~~band3Str[0] - 1;
    // Parse raw intensity to make it visible, it is the second and third character in blue band, it's range is 1 - 55
    const rawIntensity = band3Str.slice(1, 3);
    // Scale it to make it visible
    let intensity = rawIntensity * 50;
    // Prevent intensity from being higher then the max value
    if (intensity > 255) { intensity = 255; }
    intensity = ~~intensity;
    // Return all components needed for filtering/labeling
    return {
      confidence: confidence,
      intensity: intensity,
      date: date
    };
  },

  setDateRange: function setDateRange (minDate, maxDate) {
    this.options.minDateValue = parseInt(minDate);
    this.options.maxDateValue = parseInt(maxDate);
    this.refresh();
  },

  setConfidenceLevel: function setConfidenceLevel (confidence) {
    //- Confidence can be 'all' or 'confirmed'
    this.options.confidence = confidence === 'all' ? [0, 1] : [1];
    this.refresh();
  }

});
<commit_msg>Updated GLAD layer date decoding<commit_after>import TileCanvasLayer from './EsriTileCanvasBase';
import declare from 'dojo/_base/declare';
import moment from 'moment';

// Modify the JS Date object
Date.prototype.getJulian = function() {
  // Convert date into moment Object
  const date = moment(this);
  // Get the year
  const year = date.year();
  // Create a moment object of the beginning of the year
  const startOfYear = moment(`${year}/01/01`, 'YYYY/MM/DD');
  // Find number of days passed
  const daysPassed = date.diff(startOfYear, 'days');
  // Append leading zeros if needed
  let daysPassedFormatted = daysPassed.toString();
  if (daysPassedFormatted.length < 3) {
    while (daysPassedFormatted.length < 3) {
      daysPassedFormatted = `0${daysPassedFormatted}`;
    }
  }
  // Return the correct Julian
  return `${date.format('YY')}${daysPassedFormatted}`;
};

export default declare('GladLayer', [TileCanvasLayer], {

  filter: function (data) {
    const todayDate = new Date().getJulian();
    const twoWeeksAgo = new Date();
    twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14);
    const weekAgoJulian = twoWeeksAgo.getJulian();
    for (var i = 0; i < data.length; i += 4) {
      var values = this.decodeDate(data[i], data[i + 1], data[i + 2]);

      // Check if pixel date is between Julian Date properties above
      if (values.date > this.options.minDateValue && values.date < this.options.maxDateValue) {
        // Check if we are only examining confirmed cases or not
        if (this.options.confidence.length === 1) {
          if (values.confidence > 0) {
            data[i + 3] = values.intensity;
            // Make the pixel pink for glad alerts
            if (values.date > weekAgoJulian && values.date < todayDate) {
              // Make the pixel yellow for recent alerts
              data[i] = 224; // R
              data[i + 1] = 190; // G
              data[i + 2] = 7; // B
            } else {
              data[i] = 220; // R
              data[i + 1] = 102; // G
              data[i + 2] = 153; // B
            }
          } else {
            // Hide the pixel
            data[i + 3] = 0;
            data[i + 2] = 0;
            data[i + 1] = 0;
            data[i] = 0;
          }
        // Glad is not confirmed
        } else {
          data[i + 3] = values.intensity;
          if (values.date > weekAgoJulian && values.date < todayDate) {
            // Make the pixel yellow for recent alerts
            data[i] = 224; // R
            data[i + 1] = 190; // G
            data[i + 2] = 7; // B
          } else {
              // Make the pixel pink for glad alerts
            data[i] = 220; // R
            data[i + 1] = 102; // G
            data[i + 2] = 153; // B
          }
        }
      // Hide pixel if outside of date range
      } else {
        // Hide the pixel
        data[i + 3] = 0;
        data[i + 2] = 0;
        data[i + 1] = 0;
        data[i] = 0;
      }

    }
    return data;
  },

  pad: function (num) {
    var str = '00' + num;
    return str.slice(str.length - 3);
  },

  decodeDate: function (pixelOne, pixelTwo, pixelThree) {

    const total_days = pixelOne * 255 + pixelTwo;
    const year_int = parseInt(total_days / 365) + 15;

    const band3_str = this.pad(pixelThree.toString());

    const intensity_raw = parseInt(band3_str.slice(1, 3));

    let intensity = intensity_raw * 50;
    if (intensity > 255) {
      intensity = 255;
    }
    return {
      confidence: parseInt(band3_str[0]) - 1,
      intensity: intensity,
      date: parseInt(year_int * 1000) + total_days % 365
    };
  },

  setDateRange: function setDateRange (minDate, maxDate) {
    this.options.minDateValue = parseInt(minDate);
    this.options.maxDateValue = parseInt(maxDate);
    this.refresh();
  },

  setConfidenceLevel: function setConfidenceLevel (confidence) {
    //- Confidence can be 'all' or 'confirmed'
    this.options.confidence = confidence === 'all' ? [0, 1] : [1];
    this.refresh();
  }

});
<|endoftext|>