<commit_before>'use strict';

const enums = require('./constants');

const uuid = require('uuid');
const debug = require('debug')('busy:node');
const Message = require('./message');
const noop = require('./noop');
const EventEmitter = require('events');
const net = require('net');
const extend = require('gextend');
const Keypath = require('gkeypath');

const utils = require('./utils');

class Node extends EventEmitter {
    constructor(config, callback) {
        super();

        if (!utils._isObject(config)) {
            config = {};
            var args = [].slice.call(arguments);
            args.map((arg)=> {
                if (utils._isString(arg)) {
                    return config.id = arg;
                }
                if (utils._isNumber(arg)) {
                    return config.port = arg;
                }
                if (utils._isFunction(arg)) {
                    config.onConnect = arg;
                }
            });
        }

        if(callback) {
            config.onConnect = callback;
        }

        config = extend({}, {
            id: uuid.v4(),
            onConnect: noop,
            maxConnectionAttempts: 5,
            timeout: 0
        }, config);

        if(!config.port && !config.path) {
            config.port = enums.DEFAULT_PORT;
        }

        this._config = config;

        // Set up an array to store waiting callbacks
        this._callbacks = {};
        this._isConnected = false;
        this._connectionAttempts = 0;
        this._connectBridge();
    }

    /**
     * Node#tell()
     * @param {string} whoWhat - the node name and event
     *                         in the following format:
     *                         "node_name:event_type"
     * @param {csv} args - any amount of arguments to be
     *                    sent along with the remote event
     * @param {function} callback - a callback to be run
     *                            when the remote event calls
     *                            its callback
     *
     * @return {Node}
     */
    tell(/*whoWhat, ...args, callback*/) {

        var self = this;
        var args = [].slice.call(arguments);
      // parse the arguments special array into a standard array

        // grab the who-what off the front
        var whoWhat = args.shift();

        var callback;

        // is there a function on the end here?
        if (utils._isFunction(utils._last(args))) {
            // cool, pop it off so we can store it in the waiters
            callback = args.pop();
        }

        var parts = whoWhat.split(':');

        if (parts.length !== 2) {
            throw new Error('Invalid tell statement, should be (node_name:event_name)');
        }

        var nodeName = parts[0];
        var eventName = parts[1];

        var destination = [nodeName, eventName, args];

        var msg = new Message(enums.TELL, destination);

        if (callback) {
            self._callbacks[msg.getId()] = callback;
        }

        self._writeBridge(msg);
        return self;
    }

    shout() {
        this._writeBridge(new Message(enums.SHOUT, [].slice.call(arguments)));
        return this;
    }

    leave(force) {
        if (force) {
            debug('node force leaving');
            return this._forceLeave();
        }

        debug('node safely leaving');

        var msg = new Message(enums.LEAVE);

        this._callbacks[msg.getId()] = function () {
            this._forceLeave();
        };

        this._writeBridge(msg);
        return this;
    }

    _forceLeave () {
        this._bridge.end();
        return this;
    }

    _connectBridge() {
        if (this._bridge) {
            this._bridge.removeAllListeners();
        }
        this._bridge = null;

        // Lets connect to the Bridge
        this._bridge = net.connect(this._config);

        this._bridge.setTimeout(this._config.timeout);
        this._bridge.on('data', this._onData.bind(this));
        this._bridge.on('connect', this._onConnect.bind(this));
        this._bridge.on('error', this._onSocketError.bind(this));
    }

    _onConnect() {
      // Set hasConnected to true, so if the socket errors out we won't attempt
      // to reconnect.
        this._isConnected = true;

        var ident, messageId;

        // Identify the node to the bridge
        debug('sending IDENT');

        // Create a new Message
        ident = new Message(enums.IDENT);

        // Get the ID of the message so we can store the callback as a waiter
        messageId = ident.getId();

        // store the callback in the waiters list
        this._callbacks[messageId] = this._config.onConnect;

        // tell the bridge
        this._writeBridge(ident);
    }

    _onSocketError(e) {
        var self = this;
        debug('%s', e);

        if (!this._isConnected) {
            this._connectionAttempts++;
            if (this._connectionAttempts < this._config.maxConnectionAttempts) {
                debug('attempting to reconnect... (try number: %d)', this._connectionAttempts);
                setTimeout(function () {
                    self._connectBridge();
                }, this._connectionAttempts * enums.CONNECT_BACKOFF_MULT);
            } else {
                this._config.onConnect('error: node could not connect to bridge!');
            }
        } else {
            this.emit('error', e);
        }
    }

    _onData(rawData) {
        var self = this;

        try {
            var events = rawData.toString().split(enums.EOL);

            events.forEach(function (event) {
                if (!event || event.charCodeAt(0) === 10) {
                    return;
                }

                debug('node <-- bridge: %s', event);
                var message = Message.fromString(event);
                // var message = JSON.parse(event);

                switch (message.method) {
                    case enums.ACK:
                        self._onACKMessage(message);
                        break;
                    case enums.TELL:
                        self._onTellMessage(message);
                        break;
                    case enums.SHOUT:
                        self._onShoutMessage(message);
                        break;
                    case enums.CLOSE:
                        self._onCloseMessage(message);
                        break;
                }
            });
        } catch (e) {
            return debug('node received invalid data, ignoring...');
        }
    }

    /**********************************************************
    * Command Handlers
    ***********************************************************/

    _onACKMessage(message) {
        debug('received ACK from Bridge for message: %s', message.data[0]);
        // Save the message ack id - we'll be plucking it from the array later.
        var mid = message.data[0];
        var handler = this._callbacks[mid] || noop;
        var data = message.data[1] || [];
        handler.apply(this, data);

        delete this._callbacks[mid];
    }

    _onTellMessage(message) {
        var self = this;
        var from = message.data[0];
        var cbid = message.data[1];
        var eventName = message.data[2];
        var params = message.data[3];

        debug('received TELL(%s) from %s', eventName, from);

      // Define a callback function that can be called by a receiving function
        var cb = function () {
            // grab all the args
            var args = [].slice.call(arguments);

            // construct the message
            var msg = new Message(enums.ACK, [from, cbid, args]);

            // send the message
            self._writeBridge(msg);
        };

        // push the callback to the end of the params array
        params.push(cb);

        // push the event name to the beginning
        params.unshift(eventName);

        // emit that!
        this.emit.apply(this, params);
    }

    _onShoutMessage (message) {
        debug('received SHOUT(%s)', message.data[0]);
        this.emit.apply(this, message.data);
    }

    _onCloseMessage (message) {
        debug('received CLOSE');
        message.data.unshift(enums.CLOSE.toLowerCase());
        this.emit.apply(this, message.data);
    }

    _writeBridge (msg) {
        msg.setNodeName(this._config.id);
        try {
            this._bridge.write(msg.toString());
        } catch (e) {
            this.emit('error', e);
        }
    }
}

module.exports = Node;
<commit_msg>add cleanup to node<commit_after>'use strict';

const enums = require('./constants');
const exit = require('./cleanup');
const uuid = require('uuid');
const debug = require('debug')('busy:node');
const Message = require('./message');
const noop = require('./noop');
const EventEmitter = require('events');
const net = require('net');
const extend = require('gextend');
const Keypath = require('gkeypath');

const utils = require('./utils');

class Node extends EventEmitter {
    constructor(config, callback) {
        super();

        //TODO: Remove this!
        if (!utils._isObject(config)) {
            config = {};
            var args = [].slice.call(arguments);
            args.map(arg => {
                if (utils._isString(arg)) {
                    return config.id = arg;
                }
                if (utils._isNumber(arg)) {
                    return config.port = arg;
                }
                if (utils._isFunction(arg)) {
                    config.onConnect = arg;
                }
            });
        }

        if (callback) {
            config.onConnect = callback;
        }

        config = extend({}, {
            id: uuid.v4(),
            onConnect: noop,
            maxConnectionAttempts: 5,
            timeout: 0
        }, config);

        if (!config.port && !config.path) {
            config.port = enums.DEFAULT_PORT;
        }

        this._config = config;

        // Set up an object to store waiting callbacks
        this._callbacks = {};
        this._isConnected = false;
        this._connectionAttempts = 0;
        this._connectBridge();
    }

    /**
     * TODO: Rename to publish
     * Node#tell()
     *
     * @param {string} whoWhat - the node name and event
     *                         in the following format:
     *                         "node_name:event_type"
     * @param {csv} args - any amount of arguments to be
     *                    sent along with the remote event
     * @param {function} callback - a callback to be run
     *                            when the remote event calls
     *                            its callback
     *
     * @return {Node}
     */
    tell( /*whoWhat, ...args, callback*/ ) {

        const self = this;
        const args = [].slice.call(arguments);
        // parse the arguments special array into a standard array

        // grab the who-what off the front
        const whoWhat = args.shift();

        let callback;

        // is there a function on the end here?
        if (utils._isFunction(utils._last(args))) {
            // cool, pop it off so we can store it in the waiters
            callback = args.pop();
        }

        //nodeName:eventName e.g. node1:ping
        const parts = whoWhat.split(':');

        if (parts.length !== 2) {
            throw new Error('Invalid tell statement, should be (node_name:event_name)');
        }

        const [nodeName, eventName] = parts;
        const destination = [nodeName, eventName, args];

        const msg = new Message(enums.TELL, destination);

        if (callback) {
            self._callbacks[msg.getId()] = callback;
        }

        self._writeBridge(msg);
        return self;
    }

    /**
     * Rename to broadcast
     */
    shout() {
        this._writeBridge(new Message(enums.SHOUT, [].slice.call(arguments)));
        return this;
    }

    leave(force) {
        if (force) {
            debug('node force leaving');
            return this._forceLeave();
        }

        debug('node safely leaving');

        const msg = new Message(enums.LEAVE);

        this._callbacks[msg.getId()] = function() {
            this._forceLeave();
        };

        this._writeBridge(msg);

        return this;
    }

    _forceLeave() {
        this._bridge.end();
        this.emit('leave.force');
        return this;
    }

    _connectBridge() {
        if (this._bridge) {
            this._bridge.removeAllListeners();
        }

        this._bridge = null;

        // Lets connect to the Bridge
        this._bridge = net.connect(this._config);
        // this._bridge = require('./transport').createServer(this._onConnection.bind(this));

        this._registerSignalHandlers();

        this._bridge.setTimeout(this._config.timeout);
        this._bridge.on('data', this._onData.bind(this));
        this._bridge.on('connect', this._onConnect.bind(this));
        this._bridge.on('error', this._onSocketError.bind(this));
    }

    _onConnect() {
        // Set hasConnected to true, so if the socket errors out we won't attempt
        // to reconnect.
        this._isConnected = true;

        // Identify the node to the bridge
        debug('sending IDENT');

        // Create a new Message
        const ident = new Message(enums.IDENT);

        // Get the ID of the message so we can store the callback as a response
        const messageId = ident.getId();

        // store the callback in the waiters list
        this._callbacks[messageId] = this._config.onConnect;

        // tell the bridge
        this._writeBridge(ident);
    }

    _onSocketError(e) {
        debug('%s', e);

        if (!this._isConnected) {
            this._connectionAttempts++;
            if (this._connectionAttempts < this._config.maxConnectionAttempts) {
                debug('attempting to reconnect... (try number: %d)', this._connectionAttempts);
                setTimeout(_ => {
                    this._connectBridge();
                }, this._connectionAttempts * enums.CONNECT_BACKOFF_MULT);
            } else {
                this._config.onConnect('error: node could not connect to bridge!');
                this.emit('error.reconnect');
            }
        } else {
            this.emit('error', e);
        }
    }

    _onData(rawData) {
        try {
            const events = rawData.toString().split(enums.EOL);

            events.forEach(event => {
                if (!event || event.charCodeAt(0) === 10) {
                    return;
                }

                debug('node <-- bridge: %s', event);
                const message = Message.fromString(event);
                // var message = JSON.parse(event);

                switch (message.method) {
                    case enums.ACK:
                        this._onACKMessage(message);
                        break;
                    case enums.TELL:
                        this._onTellMessage(message);
                        break;
                    case enums.SHOUT:
                        this._onShoutMessage(message);
                        break;
                    case enums.CLOSE:
                        this._onCloseMessage(message);
                        break;
                }
            });
        } catch (e) {
            return debug('node received invalid data, ignoring...');
        }
    }

    /**********************************************************
     * Command Handlers
     ***********************************************************/

    _onACKMessage(message) {
        debug('received ACK from Bridge for message: %s', message.data[0]);
        // Save the message ack id - we'll be plucking it from the array later.
        const [mid, data = []] = message.data;
        const handler = this._callbacks[mid] || noop;

        //Handle message:
        handler.apply(this, data);

        //remove handler for this message
        delete this._callbacks[mid];
    }

    _onTellMessage(message) {
        var self = this;
        const [from, cbid, eventName, params] = message.data;

        debug('received TELL(%s) from %s', eventName, from);

        // Define a callback function that can be called by a receiving function
        // push the callback to the end of the params array
        params.push(() => {
            const args = [].slice.call(arguments);
            const msg = new Message(enums.ACK, [from, cbid, args]);
            this._writeBridge(msg);
        });

        // push the event name to the beginning
        params.unshift(eventName);

        this.emit.apply(this, params);
    }

    _onShoutMessage(message) {
        debug('received SHOUT(%s)', message.data[0]);
        this.emit.apply(this, message.data);
    }

    _onCloseMessage(message) {
        debug('received CLOSE');
        message.data.unshift(enums.CLOSE.toLowerCase());
        this.emit.apply(this, message.data);
    }

    /**
     * Send message over the wire.
     * We sign each message with the node's ID.
     *
     * @param {Message} msg
     */
    _writeBridge(msg) {
        msg.setNodeName(this._config.id);
        try {
            this._bridge.write(msg.toString());
        } catch (e) {
            this.emit('error', e);
        }
    }

    _registerSignalHandlers() {
        debug('register signal handlers...');
        exit(_ => {
            this._bridge.close();
        });
    }
}

module.exports = Node;<|endoftext|>


<commit_before>const DEFAULT_ENCODING = 'UTF8';
const DEFAULT_RESOURCE_TIMEOUT = 3500;

var fs = require('fs');
var path = require('path');
var util = require('util');
var EventEmitter = require('events').EventEmitter;

var _ = require('underscore');
var async = require('async');
var request = require('request');

var Page = function( page_path, options ){

	// properly inherit from EventEmitter part 1
	EventEmitter.call( this );

	var page = this;

	options = options || {};
	this.options = options;
	var server = this.options.server;
	var router = server.router;
	this.path = page_path;
	this.relative_path = path.relative( server.paths.views, page_path );

	// adds a route based on the page's path
	this.createRoute = function(){

		page.is_index = /index\.hbs$/i.test( this.relative_path );
		var route = this.relative_path.replace( /\.[a-z0-9]+$/i, '' ).replace( /\\/g, '/' );
		var route = '/'+ route;
		route = route.replace( '/index', '' ); // replace indexes with base routes
		route = route.replace( /{([a-z_-]*)}/ig, ':$1' ); // replace dynamic bits
		if( route === '' ) route = '/';
		page.route = route;

		// only overwrite existing routes if we're an index page
		var existing_route = _( router.routes.get ).find( function( route_data ){
			return route_data.path === route;
		});
		if( existing_route ){
			console.log('[SOLIDUS]'.red.bold +' Warning. You have a conflicting route at "'+ existing_route.path +'"' );
			if( !page.is_index ) return route; // return out if this isn't an index
			router.routes.get = _( router.routes.get ).without( existing_route ); // ensure the old route is removed if this is an index
		}

		router.get( route +'.json', function( req, res ){
			page.render( req, res, {
				json: true
			});
		});

		router.get( route, function( req, res ){
			page.render( req, res );
		});

		return route;

	};

	// reads the json configuration inside the view
	this.parsePage = function( callback ){

		fs.readFile( this.path, DEFAULT_ENCODING, function( err, data ){

			var params = {};
			var params_exec = /^{{!\s([\S\s]+?)\s}}/.exec( data );
			try {
				params = ( params_exec )? JSON.parse( params_exec[1] ): {};
			}
			catch( err ){
				if( server.options.log_level >= 1 ) console.log('[SOLIDUS]'.red.bold +' Error preprocessing "'+ page.path +'"', err );
			}
			finally {
				page.params = params;
				_( page ).extend({
					title: params.title,
					description: params.description,
					permanent_name: params.permanent_name,
					layout: params.layout
				});
				if( callback ) callback( params );
			}

		});

	};

	// fetches remote resources
	this.fetchResources = function( resources, params, callback ){

		var resources_data = {};

		if( resources ){
			var resources_array = _( resources ).pairs();
			async.each( resources_array, function( resource, cb ){
				var resource_name = resource[0];
				var resource_url = resource[1];
				for( var param in params ){
					var value = params[param];
					resource_url = resource_url.replace( '{'+ param +'}', value );
				}
				request.get({
					url: resource_url,
					json: true,
					timeout: DEFAULT_RESOURCE_TIMEOUT
				}, function( err, response, data ){
					if( err ){
						if( server.options.log_level >= 1 ) console.log('[SOLIDUS]'.red.bold +' Error retrieving resource "'+ resource_name +'":', err );
						return cb( err );
					} else if( 400 > response.statusCode && response.statusCode >= 200 ){
						resources_data[resource[0]] = data;
					} else {
						if( server.options.log_level >= 1 ) console.log('[SOLIDUS]'.red.bold +' Error retrieving resource "'+ resource_name +'":', response.statusCode );
					}
					cb();
				});
			}, function( err ){
				if( callback ) callback( err, resources_data );
			});
		}
		else {
			if( callback ) callback( resources_data );
		}

	};

	// preprocesses the page's context
	this.preprocess = function( context, callback ){

		var preprocessor = server.preprocessors[page.params.preprocessor];

		if( preprocessor ){
			preprocessor.process( context, function( processed_context ){
				if( callback ) callback( processed_context );
			});
		}
		else {
			if( callback ) callback( context );
		}

	};

	// generates the page's markup
	this.render = function( req, res, options ){

		options = options || {};

		var context = {
			page: {
				path: this.path,
				title: this.title,
				description: this.description,
				permanent_name: this.permanent_name
			},
			parameters: {},
			query: req.query,
			resources: {},
			assets: {
				scripts: '<script src="/compiled/scripts.js"></script>',
				styles: '<link rel="stylesheet" href="/compiled/styles.css" />'
			},
			layout: ( this.layout || this.layout === false )? this.layout: this.getLayout()
		};
		context = _( context ).defaults( router.locals );

		// req.params is actually an array with crap stuck to it
		// so we have to parse that stuff out into a real object
		var parameters = {};
		for( var key in req.params ) parameters[key] = req.params[key];
		context.parameters = parameters;

		this.fetchResources( page.params.resources, req.params, function( err, resources ){
			context.resources = resources;
			page.preprocess( context, function( context ){
				if( options.json ) return res.json( context );
				res.expose( context, 'solidus.context', 'context' );
				res.render( page.relative_path, context );
			});
		});

	};

	// get the view's layout
	this.getLayout = function(){

		var layouts = _( server.layouts ).sortBy( function( layout_path ){
			return -layout_path.length;
		});
		var local_layout = _( layouts ).find( function( layout_path ){
			var layout_regex = new RegExp( layout_path.replace( /layout\..+$/i, '' ), 'i' );
			return layout_regex.test( page.path );
		});
		if( !local_layout ) return null;
		local_layout = path.relative( server.paths.views, local_layout );

		return local_layout;

	};

	// removes the page's route
	this.destroy = function(){

		router.routes.get = _( router.routes.get ).reject( function( current_route ){
			return current_route.path === page.route;
		});

	};

	this.createRoute();
	this.parsePage( function(){
		page.emit( 'ready' );
	});

};

// properly inherit from EventEmitter part 2
util.inherits( Page, EventEmitter );

Page.layouts = {};

module.exports = Page;<commit_msg>change "permanent_name" to "name"<commit_after>const DEFAULT_ENCODING = 'UTF8';
const DEFAULT_RESOURCE_TIMEOUT = 3500;

var fs = require('fs');
var path = require('path');
var util = require('util');
var EventEmitter = require('events').EventEmitter;

var _ = require('underscore');
var async = require('async');
var request = require('request');

var Page = function( page_path, options ){

	// properly inherit from EventEmitter part 1
	EventEmitter.call( this );

	var page = this;

	options = options || {};
	this.options = options;
	var server = this.options.server;
	var router = server.router;
	this.path = page_path;
	this.relative_path = path.relative( server.paths.views, page_path );

	// adds a route based on the page's path
	this.createRoute = function(){

		page.is_index = /index\.hbs$/i.test( this.relative_path );
		var route = this.relative_path.replace( /\.[a-z0-9]+$/i, '' ).replace( /\\/g, '/' );
		var route = '/'+ route;
		route = route.replace( '/index', '' ); // replace indexes with base routes
		route = route.replace( /{([a-z_-]*)}/ig, ':$1' ); // replace dynamic bits
		if( route === '' ) route = '/';
		page.route = route;

		// only overwrite existing routes if we're an index page
		var existing_route = _( router.routes.get ).find( function( route_data ){
			return route_data.path === route;
		});
		if( existing_route ){
			console.log('[SOLIDUS]'.red.bold +' Warning. You have a conflicting route at "'+ existing_route.path +'"' );
			if( !page.is_index ) return route; // return out if this isn't an index
			router.routes.get = _( router.routes.get ).without( existing_route ); // ensure the old route is removed if this is an index
		}

		router.get( route +'.json', function( req, res ){
			page.render( req, res, {
				json: true
			});
		});

		router.get( route, function( req, res ){
			page.render( req, res );
		});

		return route;

	};

	// reads the json configuration inside the view
	this.parsePage = function( callback ){

		fs.readFile( this.path, DEFAULT_ENCODING, function( err, data ){

			var params = {};
			var params_exec = /^{{!\s([\S\s]+?)\s}}/.exec( data );
			try {
				params = ( params_exec )? JSON.parse( params_exec[1] ): {};
			}
			catch( err ){
				if( server.options.log_level >= 1 ) console.log('[SOLIDUS]'.red.bold +' Error preprocessing "'+ page.path +'"', err );
			}
			finally {
				page.params = params;
				_( page ).extend({
					title: params.title,
					description: params.description,
					name: params.name,
					layout: params.layout
				});
				if( callback ) callback( params );
			}

		});

	};

	// fetches remote resources
	this.fetchResources = function( resources, params, callback ){

		var resources_data = {};

		if( resources ){
			var resources_array = _( resources ).pairs();
			async.each( resources_array, function( resource, cb ){
				var resource_name = resource[0];
				var resource_url = resource[1];
				for( var param in params ){
					var value = params[param];
					resource_url = resource_url.replace( '{'+ param +'}', value );
				}
				request.get({
					url: resource_url,
					json: true,
					timeout: DEFAULT_RESOURCE_TIMEOUT
				}, function( err, response, data ){
					if( err ){
						if( server.options.log_level >= 1 ) console.log('[SOLIDUS]'.red.bold +' Error retrieving resource "'+ resource_name +'":', err );
						return cb( err );
					} else if( 400 > response.statusCode && response.statusCode >= 200 ){
						resources_data[resource[0]] = data;
					} else {
						if( server.options.log_level >= 1 ) console.log('[SOLIDUS]'.red.bold +' Error retrieving resource "'+ resource_name +'":', response.statusCode );
					}
					cb();
				});
			}, function( err ){
				if( callback ) callback( err, resources_data );
			});
		}
		else {
			if( callback ) callback( resources_data );
		}

	};

	// preprocesses the page's context
	this.preprocess = function( context, callback ){

		var preprocessor = server.preprocessors[page.params.preprocessor];

		if( preprocessor ){
			preprocessor.process( context, function( processed_context ){
				if( callback ) callback( processed_context );
			});
		}
		else {
			if( callback ) callback( context );
		}

	};

	// generates the page's markup
	this.render = function( req, res, options ){

		options = options || {};

		var context = {
			page: {
				path: this.path,
				title: this.title,
				description: this.description,
				name: this.name
			},
			parameters: {},
			query: req.query,
			resources: {},
			assets: {
				scripts: '<script src="/compiled/scripts.js"></script>',
				styles: '<link rel="stylesheet" href="/compiled/styles.css" />'
			},
			layout: ( this.layout || this.layout === false )? this.layout: this.getLayout()
		};
		context = _( context ).defaults( router.locals );

		// req.params is actually an array with crap stuck to it
		// so we have to parse that stuff out into a real object
		var parameters = {};
		for( var key in req.params ) parameters[key] = req.params[key];
		context.parameters = parameters;

		this.fetchResources( page.params.resources, req.params, function( err, resources ){
			context.resources = resources;
			page.preprocess( context, function( context ){
				if( options.json ) return res.json( context );
				res.expose( context, 'solidus.context', 'context' );
				res.render( page.relative_path, context );
			});
		});

	};

	// get the view's layout
	this.getLayout = function(){

		var layouts = _( server.layouts ).sortBy( function( layout_path ){
			return -layout_path.length;
		});
		var local_layout = _( layouts ).find( function( layout_path ){
			var layout_regex = new RegExp( layout_path.replace( /layout\..+$/i, '' ), 'i' );
			return layout_regex.test( page.path );
		});
		if( !local_layout ) return null;
		local_layout = path.relative( server.paths.views, local_layout );

		return local_layout;

	};

	// removes the page's route
	this.destroy = function(){

		router.routes.get = _( router.routes.get ).reject( function( current_route ){
			return current_route.path === page.route;
		});

	};

	this.createRoute();
	this.parsePage( function(){
		page.emit( 'ready' );
	});

};

// properly inherit from EventEmitter part 2
util.inherits( Page, EventEmitter );

Page.layouts = {};

module.exports = Page;<|endoftext|>


<commit_before>(function () {
    'use strict';

    var assert      = require('assert');

    module.exports          = all;
    module.exports.getBody  = getBody;

    function all(streams, options, callback) {
        if (!callback) {
            callback    = options;

            options     = {
                end: true
            };
        }

        assert(streams, 'streams could not be empty!');
        assert(options, 'callback could not be empty!');

        pipe(streams, options, callback);
    }

    function pipe(streams, options, callback) {
        var error,
            read    = streams[0];

        read.pause();

        setListeners(streams, options, onError);

        read.on('end', onEnd);
        read.on('error', onReadError);

        read.resume();

        function onError(e) {
            error = e;
        }

        function onReadError(e) {
            error = e;
            onEnd();
        }

        function onEnd() {
            unsetListeners(streams, onError);
            read.removeListener('end', onEnd);
            read.removeListener('error', onReadError);

            callback(error);
        }
    }

    function setListeners(streams, options, fn) {
        var main;

        streams.forEach(function(stream) {
            stream.on('error', fn);

            if (!main)
                main = stream;
            else
                main = main.pipe(stream, {
                    end: options.end
                });
        });
    }

    function unsetListeners(streams, fn) {
        streams.forEach(function(stream) {
            stream.removeListener('error', fn);
        });
    }

    /**
     * get body of readStream
     *
     * @param readStream
     * @param callback
     */
    function getBody(readStream, callback) {
        var error,
            body    = '';

        assert(readStream, 'could not be empty!');
        assert(callback, 'could not be empty!');

        readStream.on('data', onData);
        readStream.on('error', onError);
        readStream.on('end', onEnd);

        function onData(chunk) {
            body += chunk;
        }

        function onError(e) {
            error = e ;
        }

        function onEnd() {
            readStream.removeListener('data', onData);
            readStream.removeListener('error', onError);
            readStream.removeListener('end', onEnd);

            callback(error, body);
        }
    }
})();
<commit_msg>fix(pipe) crash when can read but could not write<commit_after>(function () {
    'use strict';

    var assert      = require('assert');

    module.exports          = all;
    module.exports.getBody  = getBody;

    function all(streams, options, callback) {
        if (!callback) {
            callback    = options;

            options     = {
                end: true
            };
        }

        if (typeof options.end === 'undefined')
            options.end = true;

        assert(streams, 'streams could not be empty!');
        assert(options, 'callback could not be empty!');

        pipe(streams, options, callback);
    }

    function pipe(streams, options, callback) {
        var error,
            isEnd   = typeof options.end === 'undefined',

            last    = streams.length - 1,
            read    = streams[0],
            write   = streams[last];

        read.pause();

        setListeners(streams, options, onError);

        read.on('error', onResultError);
        write.on('error', onResultError);

        if (options.end || isEnd)
            write.on('finish', onEnd);
        else
            read.on('end', onEnd);

        read.resume();

        function onError(e) {
            error = e;
        }

        function onResultError(e) {
            error = e;
            onEnd();
        }

        function onEnd() {
            unsetListeners(streams, onError);

            read.removeListener('error', onResultError);
            read.removeListener('end', onEnd);

            write.removeListener('error', onResultError);
            write.removeListener('finish', onEnd);

            callback(error);
        }
    }

    function setListeners(streams, options, fn) {
        var main;

        streams.forEach(function(stream) {
            stream.on('error', fn);

            if (!main)
                main = stream;
            else
                main = main.pipe(stream, {
                    end: options.end
                });
        });
    }

    function unsetListeners(streams, fn) {
        streams.forEach(function(stream) {
            stream.removeListener('error', fn);
        });
    }

    /**
     * get body of readStream
     *
     * @param readStream
     * @param callback
     */
    function getBody(readStream, callback) {
        var error,
            body    = '';

        assert(readStream, 'could not be empty!');
        assert(callback, 'could not be empty!');

        readStream.on('data', onData);
        readStream.on('error', onError);
        readStream.on('end', onEnd);

        function onData(chunk) {
            body += chunk;
        }

        function onError(e) {
            error = e ;
        }

        function onEnd() {
            readStream.removeListener('data', onData);
            readStream.removeListener('error', onError);
            readStream.removeListener('end', onEnd);

            callback(error, body);
        }
    }
})();
<|endoftext|>


<commit_before>(function () {
    'use strict';

    var fs          = require('fs'),
        assert      = require('assert');

    module.exports          = all;
    module.exports.getBody  = getBody;

    function all(streams, options, callback) {
        if (!callback) {
            callback    = options;

            options     = {
                end: true
            };
        } else if (typeof options.end === 'undefined')
            options.end = true;

        assert(streams, 'streams could not be empty!');
        assert(callback, 'callback could not be empty!');

        pipe(streams, options, callback);
    }

    function pipe(allStreams, options, callback) {
        var error, finish, end, open,
            readError, writeError,

            streams         = allStreams.slice(),
            read            = streams.shift(),
            write           = streams.pop(),

            isFsRead        = read instanceof fs.ReadStream,
            isFsWrite       = write instanceof fs.WriteStream,

            rm              = function(event, stream, fn) {
                stream.removeListener(event, fn);
            },

            rmAll           = function() {
                rm('error', write, onWriteError);
                rm('error', read, onReadError);
                rm('end', read, onReadEnd);
                rm('finish', write, onWriteFinish);
            },

            onWriteError    =  function(error) {
                writeError  = true;
                finish      = true;
                onError(error);
                onResult();
            },

            onReadError     = function(error) {
                readError   = true;
                end         = true;

                onError(error);
                onResult();
            },

            onReadEnd       = function() {
                end = true;
                onResult();
            },

            onWriteFinish   = function() {
                finish = true;
                onResult();
            };

        read.on('end', onReadEnd);
        read.on('error', onReadError);

        if (options.end)
            write.on('finish', onWriteFinish);

        callWhenOpen(write, function(e) {
            if (e)
                onWriteError(e);

            if (end && readError) {
                onEnd();
            } else {
                open = true;
                write.on('error', onWriteError);

                setListeners(streams, onError);
                fullPipe(allStreams, options);
            }
        });

        function onError(e) {
            error = e;
        }

        function onResult() {
            var justEnd     = end && !isFsWrite,
                justFinish  = write && !isFsRead,
                bothFinish  = end && finish;

            if (readError && finish) {
                onEnd();
            } else if (writeError && readError) {
                onEnd();
            } else if (writeError && end) {
                onEnd();
            } else if (open && readError) {
                onEnd();
            } else if (bothFinish || justEnd || justFinish) {
                onEnd();
            }
        }

        function onEnd() {
            rmAll();
            unsetListeners(streams, onError);

            callback(error);
        }
    }

    /*
     * when stream is fs.WriteStream
     * finish event could be emitted before
     * open and then everything crash
     */
    function callWhenOpen(stream, fn) {
        var isFsWrite   = stream instanceof fs.WriteStream,
            on = function(error) {
                var isError = error instanceof Error;

                stream.removeListener('open', on);
                stream.removeListener('error', on);

                if (isError)
                    fn(error);
                else
                    fn();
            };

        if (!isFsWrite) {
            fn();
        } else {
            stream.on('open', on);
            stream.on('error', on);
        }
    }

    function fullPipe(streams, options) {
        var main;

        streams.forEach(function(stream) {
            if (!main)
                main = stream;
            else
                main = main.pipe(stream, {
                    end: options.end
                });
        });
    }

    function setListeners(streams, fn) {
        streams.forEach(function(stream) {
            stream.on('error', fn);
        });
    }

    function unsetListeners(streams, fn) {
        streams.forEach(function(stream) {
            stream.removeListener('error', fn);
        });
    }

    /**
     * get body of readStream
     *
     * @param readStream
     * @param callback
     */
    function getBody(readStream, callback) {
        var error,
            body    = '';

        assert(readStream, 'could not be empty!');
        assert(callback, 'could not be empty!');

        readStream.on('data', onData);
        readStream.on('error', onError);
        readStream.on('end', onEnd);

        function onData(chunk) {
            body += chunk;
        }

        function onError(e) {
            error = e ;
        }

        function onEnd() {
            readStream.removeListener('data', onData);
            readStream.removeListener('error', onError);
            readStream.removeListener('end', onEnd);

            callback(error, body);
        }
    }
})();
<commit_msg>refactor(pipe) function expression: unnamed -> named<commit_after>(function () {
    'use strict';

    var fs          = require('fs'),
        assert      = require('assert');

    module.exports          = all;
    module.exports.getBody  = getBody;

    function all(streams, options, callback) {
        if (!callback) {
            callback    = options;

            options     = {
                end: true
            };
        } else if (typeof options.end === 'undefined')
            options.end = true;

        assert(streams, 'streams could not be empty!');
        assert(callback, 'callback could not be empty!');

        pipe(streams, options, callback);
    }

    function pipe(allStreams, options, callback) {
        var error, finish, end, open,
            readError, writeError,

            streams         = allStreams.slice(),
            read            = streams.shift(),
            write           = streams.pop(),

            isFsRead        = read instanceof fs.ReadStream,
            isFsWrite       = write instanceof fs.WriteStream,

            rm              = function(event, stream, fn) {
                stream.removeListener(event, fn);
            },

            rmAll           = function() {
                rm('error', write, onWriteError);
                rm('error', read, onReadError);
                rm('end', read, onReadEnd);
                rm('finish', write, onWriteFinish);
            };

        read.on('end', onReadEnd);
        read.on('error', onReadError);

        if (options.end)
            write.on('finish', onWriteFinish);

        callWhenOpen(write, function(e) {
            if (e)
                onWriteError(e);

            if (end && readError) {
                onEnd();
            } else {
                open = true;
                write.on('error', onWriteError);

                setListeners(streams, onError);
                fullPipe(allStreams, options);
            }
        });

        function onWriteError(error) {
            writeError  = true;
            finish      = true;
            onError(error);
            onResult();
        }

        function onReadError(error) {
            readError   = true;
            end         = true;

            onError(error);
            onResult();
        }

        function onReadEnd() {
            end = true;
            onResult();
        }

        function onWriteFinish() {
            finish = true;
            onResult();
        }

        function onError(e) {
            error = e;
        }

        function onResult() {
            var justEnd     = end && !isFsWrite,
                justFinish  = write && !isFsRead,
                bothFinish  = end && finish;

            if (readError && finish) {
                onEnd();
            } else if (writeError && readError) {
                onEnd();
            } else if (writeError && end) {
                onEnd();
            } else if (open && readError) {
                onEnd();
            } else if (bothFinish || justEnd || justFinish) {
                onEnd();
            }
        }

        function onEnd() {
            rmAll();
            unsetListeners(streams, onError);

            callback(error);
        }
    }

    /*
     * when stream is fs.WriteStream
     * finish event could be emitted before
     * open and then everything crash
     */
    function callWhenOpen(stream, fn) {
        var isFsWrite   = stream instanceof fs.WriteStream,
            on = function(error) {
                var isError = error instanceof Error;

                stream.removeListener('open', on);
                stream.removeListener('error', on);

                if (isError)
                    fn(error);
                else
                    fn();
            };

        if (!isFsWrite) {
            fn();
        } else {
            stream.on('open', on);
            stream.on('error', on);
        }
    }

    function fullPipe(streams, options) {
        var main;

        streams.forEach(function(stream) {
            if (!main)
                main = stream;
            else
                main = main.pipe(stream, {
                    end: options.end
                });
        });
    }

    function setListeners(streams, fn) {
        streams.forEach(function(stream) {
            stream.on('error', fn);
        });
    }

    function unsetListeners(streams, fn) {
        streams.forEach(function(stream) {
            stream.removeListener('error', fn);
        });
    }

    /**
     * get body of readStream
     *
     * @param readStream
     * @param callback
     */
    function getBody(readStream, callback) {
        var error,
            body    = '';

        assert(readStream, 'could not be empty!');
        assert(callback, 'could not be empty!');

        readStream.on('data', onData);
        readStream.on('error', onError);
        readStream.on('end', onEnd);

        function onData(chunk) {
            body += chunk;
        }

        function onError(e) {
            error = e ;
        }

        function onEnd() {
            readStream.removeListener('data', onData);
            readStream.removeListener('error', onError);
            readStream.removeListener('end', onEnd);

            callback(error, body);
        }
    }
})();
<|endoftext|>


<commit_before>/* Plug application system.
 * Copyright © 2011 Jan Keromnes, Thaddee Tyl. All rights reserved.
 * The following code is covered by the GPLv2 license. */

var fs = require('./fs'),
    nodepath = require ('path');

// The plug system.

exports.plug = function (query, path, cb) {

  // Get the file/folder corresponding to the query
  ///console.log('SERVER:ROOT: what is %s?', path[1]);
  fs.file (path[1], function (err, file) {

    function end(err, plug, data) {
      if (!plug || plug === 'none') cb(err, path[1], data || {});
      else cb(err, plug, data || {});
    }

    // If there was an error, abort!
    if (err) {
      // TODO change HTTP return code to 404 instead of 200
      end(err, '/util/404/404.html', {error: err.message});
      return;
    }

    // TODO If file requires a password and nothing matches, abort!

    // TODO If specifically asked for, send the untemplated raw file

    // PLUG MECHANISM

    var data = {}, delayed = false;

    // Environment variable lookup mechanism
    data.lookup = function(key) {
      // from most specific (url?key=value) to most generic (inherited metadata)
      if (query[key]) return query[key];
      if (file.meta[key]) return file.meta[key];
      return null;
    };

    // File info
    data.file = file;
    data.file.mime = fs.type.nameFromType[file.meta.type];
    data.files = [];
    data.title = nodepath.basename(file.path) || 'The File Tree';

    // Navigation crumbs
    data.nav = [];
    var crumbs = path[1].split('/').filter(function(e) {
      return e.length > 0;
    });
    var subpath = '/';
    data.nav.push({name: '', path: subpath});
    for ( var i = 0; i < crumbs.length; i++ ) {
      subpath += crumbs[i] + '/';
      data.nav.push({name: crumbs[i], path: subpath});
    }

    // Find a plug to use
    var plug = data.lookup('plug');

    if (plug === 'none') {
      end(); // no plug
      return;
    }

    if (file.isOfType('dir')) {

      // Delay callback, wait for subfiles
      delayed = true;

      // FIXME check for default FOLDER plug instead
      plug = plug || '/util/gateway/gateway.html';

      // Handle missing final '/'
      // FIXME consider redirecting user to path[1] + '/' instead
      if ( path[1][path[1].length-1] !== '/' ) {
        path[1] += '/'; data.path += '/';
      }

      // Get subfiles
      file.files (function (err, files) {
        if (err)  console.error(err);
        for (var i = 0; i < files.length; i++) {
          var filepath = files[i].path + (files[i].isOfType('dir') ? '/' : '');
          data.files.push({name: nodepath.basename(filepath), path: filepath});
        }
        ///console.log('server:root: data sent from dir is', data);
        end(err, plug, data);
      });

    } else if (file.isOfType('text/plain')) {

      // Delay callback, wait for file content
      delayed = true;

      // TODO check for default TEXT plug instead
      plug = plug || '/util/pencil/pencil.html';

      // TODO open file to load content
      file.open(function (err) {
        end(err, plug, data);
      });

    } else if (file.isOfType('binary')) {

      // TODO check for default BINARY plug instead
      plug = plug || 'none';

    }

    if (!delayed) {
      end(plug, data);
      return;
    }

  });

};

<commit_msg>Bugfixes in `lib/plug.js`<commit_after>/* Plug application system.
 * Copyright © 2011 Jan Keromnes, Thaddee Tyl. All rights reserved.
 * The following code is covered by the GPLv2 license. */

var fs = require('./fs'),
    nodepath = require ('path');

// The plug system.

exports.plug = function (query, path, cb) {

  // Get the file/folder corresponding to the query
  ///console.log('SERVER:ROOT: what is %s?', path[1]);
  fs.file (path[1], function (err, file) {

    function end(err, plug, data) {
      if (!plug || plug === 'none') {
        //console.log('ENDING1',err,path[1],JSON.stringify(data||{}));
        cb(err, path[1], data || {});
      } else {
        //console.log('ENDING2',err,plug,JSON.stringify(data||{}));
        cb(err, plug, data || {});
      }
    }

    // If there was an error, abort!
    if (err) {
      // TODO change HTTP return code to 404 instead of 200
      end(err, '/util/404/404.html', {error: err.message});
      return;
    }

    // TODO If file requires a password and nothing matches, abort!

    // TODO If specifically asked for, send the untemplated raw file

    // PLUG MECHANISM

    var data = {}, delayed = false;

    // Environment variable lookup mechanism
    data.lookup = function(key) {
      // from most specific (url?key=value) to most generic (inherited metadata)
      if (query[key]) return query[key];
      if (file.meta[key]) return file.meta[key];
      return null;
    };

    // File info
    data.file = file;
    data.file.mime = fs.type.nameFromType[file.meta.type];
    data.files = [];
    data.title = nodepath.basename(file.path) || 'The File Tree';

    // Navigation crumbs
    data.nav = [];
    var crumbs = path[1].split('/').filter(function(e) {
      return e.length > 0;
    });
    var subpath = '/';
    data.nav.push({name: '', path: subpath});
    for ( var i = 0; i < crumbs.length; i++ ) {
      subpath += crumbs[i] + '/';
      data.nav.push({name: crumbs[i], path: subpath});
    }

    // Find a plug to use
    var plug = data.lookup('plug');

    if (plug === 'none') {
      end(); // no plug
      return;
    }

    if (file.isOfType('dir')) {

      // Delay callback, wait for subfiles
      delayed = true;

      // FIXME check for default FOLDER plug instead
      plug = plug || '/util/gateway/gateway.html';

      // Handle missing final '/'
      // FIXME consider redirecting user to path[1] + '/' instead
      if ( path[1][path[1].length-1] !== '/' ) {
        path[1] += '/';
      }

      // Get subfiles
      file.files (function (err, files) {
        if (err)  console.error(err);
        for (var i = 0; i < files.length; i++) {
          var filepath = files[i].path + (files[i].isOfType('dir') ? '/' : '');
          data.files.push({name: nodepath.basename(filepath), path: filepath});
        }
        ///console.log('server:root: data sent from dir is', data);
        end(err, plug, data);
      });

    } else if (file.isOfType('text/plain')) {

      // Delay callback, wait for file content
      delayed = true;

      // TODO check for default TEXT plug instead
      plug = plug || '/util/pencil/pencil.html';

      // TODO open file to load content
      file.open(function (err) {
        end(err, plug, data);
      });

    } else if (file.isOfType('binary')) {

      // TODO check for default BINARY plug instead
      plug = plug || 'none';

    }

    if (!delayed) {
      //console.error('WAS NOT DELAYED');
      end(err, plug, data);
      return;
    }

  });

};

<|endoftext|>


<commit_before>/*
---

script: Object.js

description: An observable object

license: Public domain (http://unlicense.org).

authors: Yaroslaff Fedin

requires:
  - LSD

provides:
  - LSD.Object

...
*/

LSD.Object = function(object) {
  if (object) for (var key in object) this.set(key, object[key]);
};

LSD.Object.prototype = {
  set: function(key, value, memo, index) {
    if (index == null || index === true || index === false) index = key.indexOf('.');
    if (index > -1) {
      for (var bit, end, obj = this, i = 0;;) {
        bit = key.substring(i, index)
        i = index + 1;
        if (!end) {
          if (!obj[bit]) {
            var o = new LSD.Object;
            obj.set(bit, o);
            obj = o;
          } else {
            obj = obj[bit];
          }
        } else obj.set(bit, value);
        index = key.indexOf('.', i);
        if (index == -1) {
          if (!end && (end = true)) {
            index = key.length;
          } else break
        }
      }
    } else {
      var old = this[key];
      if (old === value && typeof old != 'undefined') return false;
      if (old) this.fireEvent('beforechange', key, old, false);
      this[key] = value;
      this[key] = value = this.fireEvent('change', key, value, true, old, memo);
      var watched = this._watched;
      if (watched && (watched = watched[key]))
        for (var i = 0, fn; fn = watched[i++];)
          if (fn.call) fn(value, old);
          else LSD.Object.callback(this, fn, key, value, old, memo);
      return true;
    }
  },
  unset: function(key, value, memo, index) {
    if (index == null || index === true || index === false) index = key.indexOf('.');
    if (index > -1) {
      for (var bit, end, obj = this, i = 0;;) {
        bit = key.substring(i, index)
        i = index + 1;
        if (end) obj.unset(bit, value);
        else obj = obj[bit];
        index = key.indexOf('.', i);
        if (index == -1) {
          if (!end && (end = true)) {
            index = key.length;
          } else break
        }
      }
    } else {
      var old = this[key];
      if (old == null && value != null) return false;
      this.fireEvent('change', key, old, false, null, memo);
      var watched = this._watched;
      if (watched && (watched = watched[key]))
        for (var i = 0, fn; fn = watched[i++];)
          if (fn.call) fn(null, old);
          else LSD.Object.callback(this, fn, key, null, old, memo);
      delete this[key];
      return true;
    }
  },
  mix: function(name, value, state, reverse, merge) {
    if (!name.indexOf) {
      for (var prop in name)
        if (name.has ? name.has(prop) : name.hasOwnProperty(prop))
          this.mix(prop, name[prop], state, reverse, merge);
    } else {
      if (typeOf(value) == 'object') {
        var obj = this[name];
        if (!obj || !obj.merge) this.set(name, (obj = new LSD.Object.Stack));
        if (merge) obj[state !== false ? 'merge' : 'unmerge'](value, reverse);
        else obj.mix(value, null, state, reverse);
      } else {
        this[state !== false ? 'set' : 'unset'](name, value, null, reverse);
      }
    }
  },
  merge: function(object, reverse) {
    if (object.watch && object.addEvent) {
      var self = this;
      var watcher = function(name, value, state, old) {
        if (state) self.mix(name, value, true, reverse, true);
        if (!state || old != null) self.mix(name, state ? old : value, false, reverse, true);
      }
      watcher.callback = this;
      object.addEvent('change', watcher);
    }
    this.mix(object, null, true, reverse, true);
  },
  unmerge: function(object, reverse) {
    if (object.unwatch && object.removeEvent) {
      object.removeEvent('change', this);
    }
    this.mix(object, null, false, reverse, true);
  },
  include: function(key, memo) {
    return this.set(key, true, memo)
  },
  erase: function(key, memo) {
    return this.unset(key, true, memo)
  },
  write: function(key, value, memo) {
    if (value == null) this.unset(key, this[key], memo)
    else this.set(key, value, memo);
  },
  fireEvent: function(key, a, b, c, d, e) {
    var storage = this._events;
    if (storage) {
      var collection = storage[key];
      if (collection) for (var i = 0, fn; fn = collection[i++];) {
        var result = fn(a, b, c, d, e);
        if (result != null) b = result;
      }
    }
    return b;
  },
  addEvent: function(key, callback, unshift) {
    var storage = this._events;
    if (!storage) storage = this._events = {};
    (storage[key] || (storage[key] = []))[unshift ? 'unshift' : 'push'](callback);
    return this;
  },
  removeEvent: function(key, callback) {
    var group = this._events[key]
    for (var j = group.length; --j > -1;) {
      var listener = group[j];
      if (listener === callback || listener.callback === callback) {
        group.splice(j, 1);
        break;
      }
    }
    return this;
  },
  watch: function(key, callback, lazy) {
    var index = key.indexOf('.');
    if (index > -1) {
      var finder = function(value, old) {
        var object = value || old;
        if (object.watch) {
          object[value ? 'watch' : 'unwatch'](key.substring(index + 1), callback);
        } else {
          var result = Object.getFromPath(object, key.substring(index + 1));
          if (result != null) callback(result);
        }
      };
      finder.callback = callback;
      this.watch(key.substr(0, index), finder)
    } else {
      var watched = (this._watched || (this._watched = {}));
      (watched[key] || (watched[key] = [])).push(callback);
      var value = this[key];
      if (!lazy && value != null) {
        if (callback.call) callback(value);
        else LSD.Object.callback(this, callback, key, value);
      }
    }
  },
  unwatch: function(key, callback) {
    var index = key.indexOf('.');
    if (index > -1) {
      this.unwatch(key.substr(0, index), callback)
    } else if (this._watched) {
      var watched = this._watched[key];
      var value = this[key];
      for (var i = 0, fn; fn = watched[i]; i++) {
        var match = fn.callback || fn;
        if (match.push) {
          if (!callback.push || callback[0] != match[0] || callback[1] != match[1]) continue;
        } else if (match != callback && fn != callback) continue;
        watched.splice(i, 1);
        if (value != null) {
          if (callback.call) fn(null, value);
          else LSD.Object.callback(this, fn, key, null, value);
        }
        break;
      }
    }
  },
  has: function(key) {
    return this.hasOwnProperty(key) && (key.charAt(0) != '_')
  },
  join: function(separator) {
    var ary = [];
    for (var key in this)
      if (this.has ? this.has(key) : this.hasOwnProperty(key))
        ary.push(key);
    return ary.join(separator)
  }
};

LSD.toObject = LSD.Object.toObject = LSD.Object.prototype.toObject = function() {
  if (this === LSD.Object || this === LSD) var obj = arguments[0];
  else var obj = this;
  if (obj == null) return null;
  if (obj._toObject) {
    if (obj._toObject.call) {
      return obj._toObject.apply(obj, arguments);
    } else if (obj._toObject.push) {
      var object = {};
      for (var i = 0, prop; prop = obj._toObject[i++];)
        if (obj[prop]) object[prop] = LSD.toObject(obj[prop]);
    } else {
      var object = {};
      for (var prop in obj)
        if (prop in obj._toObject) object[prop] = LSD.toObject(obj[prop])
    }
  } else if (obj.push) {
    var object = [];
    for (var i = 0, j = obj.length; i < j; i++)
      object[i] = LSD.toObject(obj[i]);
  } else if (obj.setDate) {
    var object = obj.format('compact');
  } else if (!obj.indexOf && typeof obj == 'object') {
    var object = {};
    for (var key in obj)
      if (obj.has ? obj.has(key) : obj.hasOwnProperty(key)) {
        var val = obj[key];
        object[key] = !val || val.push || val.exec || val.call || val.indexOf ? val : LSD.toObject(val);
      }
  }
  return object || obj;
}

/*
  Stack object is an object that may have its values set from multiple sources.
  All of `set` and `unset` calls are logged, so when the value gets unset,
  it returns to previous value (that was set before by a different external object).

  It was designed to be symetric, so every .set is paired with .unset. Originally,
  unset raised exception when it could not find its value set before.

  That perhaps is too idealistic and doenst work in real world, so value that was
  set by some `set`/`unset` pair, can be unset by an outside `unset` call.
  A paired `unset` having nothing to unset will silently do nothing.
*/

LSD.Object.Stack = function(object) {
  if (object) for (var key in object) this.set(key, object[key]);
};

LSD.Object.Stack.prototype = Object.append(new LSD.Object, {
  set: function(key, value, memo, prepend) {
    var index = key.indexOf('.');
    if (index == -1) {
      var stack = this._stack;
      if (!stack) stack = this._stack = {};
      var group = stack[key];
      if (!group) group = stack[key] = []
      var length = (prepend || value == null) ? group.unshift(value) : group.push(value);
      value = group[length - 1];
    }
    return LSD.Object.prototype.set.call(this, key, value, memo, index);
  },
  unset: function(key, value, memo, prepend) {
    var index = key.indexOf('.');
    if (index == -1) {
      var group = this._stack[key], length = group.length;
      if (prepend) {
        for (var i = 0, j = length; i < j; i++)
          if (group[i] === value) {
            group.splice(i, 1);
            break;
          }
        if (j == i) return
      } else {
        for (var j = length; --j > -1; )
          if (group[j] === value) {
            group.splice(j, 1);
            break;
          }
        if (j == -1) return
      }
      value = group[length - 2];
      var method = length == 1 ? 'unset' : 'set';
    }
    return LSD.Object.prototype[method || 'unset'].call(this, key, value, memo, index);
  },
  write: function(key, value, memo) {
    if (value != null) {
      if (this[key] != null) this.unset(key, this[key], memo);
      this.set(key, value, memo);
    } else if (this[key] != null) this.unset(key, this[key], memo);
  }
})

LSD.Object.callback = function(object, callback, key, value, old, memo) {
  if (callback.substr) var subject = object, property = callback;
  else if (callback.watch && callback.set) var subject = callback, property = key;
  else if (callback.push) var subject = callback[0], property = callback[1];
  else throw "Callbacks should be either functions, strings, objects, or [object, string] arrays"
  if (property === true || property == false) property = key;
  // check for circular calls
  if (memo != null && memo.push) {
    for (var i = 0, a; a = memo[i++];)
      if (a[0] == object && a[1] == property) return;
  } else memo = [];
  memo.push([object, key]);
  if (value != null || typeof old == 'undefined') subject.set(property, value, memo);
  if (value == null || typeof old != 'undefined') subject.unset(property, old, memo);
};

/*

*/

LSD.Struct = function(properties) {
  if (this === LSD) {
    return function() {
      var object = new LSD.Struct(properties)
      if (arguments.length) LSD.Object.apply(object, arguments);
      return object;
    }
  }
  if (properties) {
    this._properties = properties;
    this.addEvent('change', function(name, value, state, old) {
      var prop = properties[name];
      if (prop) return prop.call(this, value, old);
    }.bind(this));
    this._toObject = properties
  };
};
LSD.Struct.prototype = LSD.Object.prototype;<commit_msg>Fix a bug where an undefined value was unset<commit_after>/*
---

script: Object.js

description: An observable object

license: Public domain (http://unlicense.org).

authors: Yaroslaff Fedin

requires:
  - LSD

provides:
  - LSD.Object

...
*/

LSD.Object = function(object) {
  if (object) for (var key in object) this.set(key, object[key]);
};

LSD.Object.prototype = {
  set: function(key, value, memo, index) {
    if (index == null || index === true || index === false) index = key.indexOf('.');
    if (index > -1) {
      for (var bit, end, obj = this, i = 0;;) {
        bit = key.substring(i, index)
        i = index + 1;
        if (!end) {
          if (!obj[bit]) {
            var o = new LSD.Object;
            obj.set(bit, o);
            obj = o;
          } else {
            obj = obj[bit];
          }
        } else obj.set(bit, value);
        index = key.indexOf('.', i);
        if (index == -1) {
          if (!end && (end = true)) {
            index = key.length;
          } else break
        }
      }
    } else {
      var old = this[key];
      if (old === value && typeof old != 'undefined') return false;
      if (old) this.fireEvent('beforechange', key, old, false);
      this[key] = value;
      this[key] = value = this.fireEvent('change', key, value, true, old, memo);
      var watched = this._watched;
      if (watched && (watched = watched[key]))
        for (var i = 0, fn; fn = watched[i++];)
          if (fn.call) fn(value, old);
          else LSD.Object.callback(this, fn, key, value, old, memo);
      return true;
    }
  },
  unset: function(key, value, memo, index) {
    if (index == null || index === true || index === false) index = key.indexOf('.');
    if (index > -1) {
      for (var bit, end, obj = this, i = 0;;) {
        bit = key.substring(i, index)
        i = index + 1;
        if (end) obj.unset(bit, value);
        else obj = obj[bit];
        index = key.indexOf('.', i);
        if (index == -1) {
          if (!end && (end = true)) {
            index = key.length;
          } else break
        }
      }
    } else {
      var old = this[key];
      if (old == null && value != null) return false;
      this.fireEvent('change', key, old, false, null, memo);
      var watched = this._watched;
      if (watched && (watched = watched[key]))
        for (var i = 0, fn; fn = watched[i++];)
          if (fn.call) fn(null, old);
          else LSD.Object.callback(this, fn, key, null, old, memo);
      delete this[key];
      return true;
    }
  },
  mix: function(name, value, state, reverse, merge) {
    if (!name.indexOf) {
      for (var prop in name)
        if (name.has ? name.has(prop) : name.hasOwnProperty(prop))
          this.mix(prop, name[prop], state, reverse, merge);
    } else {
      if (typeOf(value) == 'object') {
        var obj = this[name];
        if (!obj || !obj.merge) this.set(name, (obj = new LSD.Object.Stack));
        if (merge) obj[state !== false ? 'merge' : 'unmerge'](value, reverse);
        else obj.mix(value, null, state, reverse);
      } else {
        this[state !== false ? 'set' : 'unset'](name, value, null, reverse);
      }
    }
  },
  merge: function(object, reverse) {
    if (object.watch && object.addEvent) {
      var self = this;
      var watcher = function(name, value, state, old) {
        if (state) self.mix(name, value, true, reverse, true);
        if (!state || old != null) self.mix(name, state ? old : value, false, reverse, true);
      }
      watcher.callback = this;
      object.addEvent('change', watcher);
    }
    this.mix(object, null, true, reverse, true);
  },
  unmerge: function(object, reverse) {
    if (object.unwatch && object.removeEvent) {
      object.removeEvent('change', this);
    }
    this.mix(object, null, false, reverse, true);
  },
  include: function(key, memo) {
    return this.set(key, true, memo)
  },
  erase: function(key, memo) {
    return this.unset(key, true, memo)
  },
  write: function(key, value, memo) {
    if (value == null) this.unset(key, this[key], memo)
    else this.set(key, value, memo);
  },
  fireEvent: function(key, a, b, c, d, e) {
    var storage = this._events;
    if (storage) {
      var collection = storage[key];
      if (collection) for (var i = 0, fn; fn = collection[i++];) {
        var result = fn(a, b, c, d, e);
        if (result != null) b = result;
      }
    }
    return b;
  },
  addEvent: function(key, callback, unshift) {
    var storage = this._events;
    if (!storage) storage = this._events = {};
    (storage[key] || (storage[key] = []))[unshift ? 'unshift' : 'push'](callback);
    return this;
  },
  removeEvent: function(key, callback) {
    var group = this._events[key]
    for (var j = group.length; --j > -1;) {
      var listener = group[j];
      if (listener === callback || listener.callback === callback) {
        group.splice(j, 1);
        break;
      }
    }
    return this;
  },
  watch: function(key, callback, lazy) {
    var index = key.indexOf('.');
    if (index > -1) {
      var finder = function(value, old) {
        var object = value || old;
        if (object.watch) {
          object[value ? 'watch' : 'unwatch'](key.substring(index + 1), callback);
        } else {
          var result = Object.getFromPath(object, key.substring(index + 1));
          if (result != null) callback(result);
        }
      };
      finder.callback = callback;
      this.watch(key.substr(0, index), finder)
    } else {
      var watched = (this._watched || (this._watched = {}));
      (watched[key] || (watched[key] = [])).push(callback);
      var value = this[key];
      if (!lazy && value != null) {
        if (callback.call) callback(value);
        else LSD.Object.callback(this, callback, key, value);
      }
    }
  },
  unwatch: function(key, callback) {
    var index = key.indexOf('.');
    if (index > -1) {
      this.unwatch(key.substr(0, index), callback)
    } else if (this._watched) {
      var watched = this._watched[key];
      var value = this[key];
      for (var i = 0, fn; fn = watched[i]; i++) {
        var match = fn.callback || fn;
        if (match.push) {
          if (!callback.push || callback[0] != match[0] || callback[1] != match[1]) continue;
        } else if (match != callback && fn != callback) continue;
        watched.splice(i, 1);
        if (value != null) {
          if (callback.call) fn(null, value);
          else LSD.Object.callback(this, fn, key, null, value);
        }
        break;
      }
    }
  },
  has: function(key) {
    return this.hasOwnProperty(key) && (key.charAt(0) != '_')
  },
  join: function(separator) {
    var ary = [];
    for (var key in this)
      if (this.has ? this.has(key) : this.hasOwnProperty(key))
        ary.push(key);
    return ary.join(separator)
  }
};

LSD.toObject = LSD.Object.toObject = LSD.Object.prototype.toObject = function() {
  if (this === LSD.Object || this === LSD) var obj = arguments[0];
  else var obj = this;
  if (obj == null) return null;
  if (obj._toObject) {
    if (obj._toObject.call) {
      return obj._toObject.apply(obj, arguments);
    } else if (obj._toObject.push) {
      var object = {};
      for (var i = 0, prop; prop = obj._toObject[i++];)
        if (obj[prop]) object[prop] = LSD.toObject(obj[prop]);
    } else {
      var object = {};
      for (var prop in obj)
        if (prop in obj._toObject) object[prop] = LSD.toObject(obj[prop])
    }
  } else if (obj.push) {
    var object = [];
    for (var i = 0, j = obj.length; i < j; i++)
      object[i] = LSD.toObject(obj[i]);
  } else if (obj.setDate) {
    var object = obj.format('compact');
  } else if (!obj.indexOf && typeof obj == 'object') {
    var object = {};
    for (var key in obj)
      if (obj.has ? obj.has(key) : obj.hasOwnProperty(key)) {
        var val = obj[key];
        object[key] = !val || val.push || val.exec || val.call || val.indexOf ? val : LSD.toObject(val);
      }
  }
  return object || obj;
}

/*
  Stack object is an object that may have its values set from multiple sources.
  All of `set` and `unset` calls are logged, so when the value gets unset,
  it returns to previous value (that was set before by a different external object).

  It was designed to be symetric, so every .set is paired with .unset. Originally,
  unset raised exception when it could not find its value set before.

  That perhaps is too idealistic and doenst work in real world, so value that was
  set by some `set`/`unset` pair, can be unset by an outside `unset` call.
  A paired `unset` having nothing to unset will silently do nothing.
*/

LSD.Object.Stack = function(object) {
  if (object) for (var key in object) this.set(key, object[key]);
};

LSD.Object.Stack.prototype = Object.append(new LSD.Object, {
  set: function(key, value, memo, prepend) {
    var index = key.indexOf('.');
    if (index == -1) {
      var stack = this._stack;
      if (!stack) stack = this._stack = {};
      var group = stack[key];
      if (!group) group = stack[key] = []
      var length = (prepend || value == null) ? group.unshift(value) : group.push(value);
      value = group[length - 1];
    }
    return LSD.Object.prototype.set.call(this, key, value, memo, index);
  },
  unset: function(key, value, memo, prepend) {
    var index = key.indexOf('.');
    if (index == -1) {
      var group = this._stack[key], length = group.length;
      if (prepend) {
        for (var i = 0, j = length; i < j; i++)
          if (group[i] === value) {
            group.splice(i, 1);
            break;
          }
        if (j == i) return
      } else {
        for (var j = length; --j > -1; )
          if (group[j] === value) {
            group.splice(j, 1);
            break;
          }
        if (j == -1) return
      }
      if (length > 1) {
        var method = 'set';
        value = group[length - 2];
      } else var method = 'unset';
    }
    return LSD.Object.prototype[method || 'unset'].call(this, key, value, memo, index);
  },
  write: function(key, value, memo) {
    if (value != null) {
      if (this[key] != null) this.unset(key, this[key], memo);
      this.set(key, value, memo);
    } else if (this[key] != null) this.unset(key, this[key], memo);
  }
})

LSD.Object.callback = function(object, callback, key, value, old, memo) {
  if (callback.substr) var subject = object, property = callback;
  else if (callback.watch && callback.set) var subject = callback, property = key;
  else if (callback.push) var subject = callback[0], property = callback[1];
  else throw "Callbacks should be either functions, strings, objects, or [object, string] arrays"
  if (property === true || property == false) property = key;
  // check for circular calls
  if (memo != null && memo.push) {
    for (var i = 0, a; a = memo[i++];)
      if (a[0] == object && a[1] == property) return;
  } else memo = [];
  memo.push([object, key]);
  if (value != null || typeof old == 'undefined') subject.set(property, value, memo);
  if (value == null || typeof old != 'undefined') subject.unset(property, old, memo);
};

/*

*/

LSD.Struct = function(properties) {
  if (this === LSD) {
    return function() {
      var object = new LSD.Struct(properties)
      if (arguments.length) LSD.Object.apply(object, arguments);
      return object;
    }
  }
  if (properties) {
    this._properties = properties;
    this.addEvent('change', function(name, value, state, old) {
      var prop = properties[name];
      if (prop) return prop.call(this, value, old);
    }.bind(this));
    this._toObject = properties
  };
};
LSD.Struct.prototype = LSD.Object.prototype;<|endoftext|>


<commit_before>// Javascript for parsing and displaying departure information

var options =
{
  url: "http://bustracker.pvta.com/InfoPoint/rest/",
  title: "",                      // title to display at top of page
  logo_url: ""                    // logo to display at top of page
}

$(function() {
  loadAllStops();
  updateOptions();
  initTitle();

  setTimeout(removeFade, 500);
  // Load the BusInfoBoard with the selected stops
  $( '.go-button').on('click', function() {
    // Buttons are the row after the stop selector
    var stops = $(this).parents('.row').prev().find('.stops').val();
    document.location.href = busBoardURL(stops);
  });

  // List of all PVTA routes
  var routes = $('.routes');
  var routeStops = $('.route-stops');
  var everyStop;

  // Use the Chosen jQuery plugin for our multiple select boxes
  routes.chosen();
  routeStops.chosen();
  $('.nearby-stops').chosen();

/**This function will download and display
 * **all** of PVTA's stops, so that users can
 * type in and view a specific stop or
 * number of stops without having to first pick
 * a route.
 **/
  function loadAllStops(){
    var stops = [];
    /** If we've already downloaded
     * and sorted every stop, no
     * need to do it again
     **/
    var span = document.getElementById('stop-selector').innerHTML = "
Or: Search Stops
";
    if(everyStop){
      //Display the list of stops
      stopList(routeStops, everyStop);
      return;
    }
      $.ajax({
        url: options.url + 'stops/getallstops',
        success: function(allStops){
          // Set the everyStop variable to the list so
          // we don't have to download it all again.
          everyStop = allStops;
          everyStop = uniqueifyAndSort(everyStop);
          stopList(routeStops, everyStop);
        } //end success callback
    }); //end ajax call
  }

  // When a route is added or removed from the list, reload the list of stops
  // accessible by those routes
  routes.on("change", function() {
    var routes = $('.routes').val() || [];
    var remainingRoutes = routes.length;
    var stops = [];
    // For each route selected, we get a list of stops
    if(routes.length === 0) loadAllStops();
    else{
      var span = document.getElementById('stop-selector').innerHTML = "
Now Search Stops
";
      for (var i = 0; i < routes.length; i++) {
        $.ajax({
          url: options.url + "routedetails/get/" + routes[i],
          success: function(route_details) {
            stops.push(route_details.Stops);
            remainingRoutes--;
            if (remainingRoutes == 0) {
              // Put all of the stops into a single array and sort them
              stops = uniqueifyAndSort(stops);
              stopList(routeStops, stops);
            }
          } //end success callback
        }); //end ajax call
      } //end for
    } //end else
  });

  // Ask the user for their location
  getLocation();

  // Load all of the routes from the InfoPoint API
  $.ajax({
    url: options.url + "routes/getvisibleroutes",
    success: function(route_data) {
      // Sort routes by Route name
      route_data.sort(function(a,b) {
        if (a.RouteAbbreviation > b.RouteAbbreviation) {
          return 1;
        }
        if (a.RouteAbbreviation < b.RouteAbbreviation) {
          return -1;
        }
        return 0
      });

      for (var i = 0; i < route_data.length; i++) {
        routes.append('<option value="' + route_data[i].RouteId + '">' +
            route_data[i].RouteAbbreviation + " " + route_data[i].LongName +
            '</option>');
      }
      // Refresh our list
      routes.trigger('chosen:updated');
    }
  });
});

function initTitle() {
  var body = $('.body');

  if (typeof options.logo_url !== "undefined" && options.logo_url != "") {
    body.prepend('');
  }

  if (typeof options.title !== "undefined" && options.title != "") {
    body.prepend('
' + options.title + '
');
  }

}

function getLocation() {
  $('.nearby-holder').hide();
  if (Modernizr.geolocation) {
    return navigator.geolocation.getCurrentPosition(populateListGeo, function(){
      removeFade();
    });
  } else {
    // NoGeo option
  }
}

function stopList(select, stops) {
  fadeBlack(function() {
    select.empty();
    for(var i = 0; i < stops.length; i++) {
      select.append('<option value="' + stops[i].StopId + '">' + stops[i].Name + '</option>');
    }
    select.trigger('chosen:updated');
    removeFade();
  });
}

/**
* This function takes in a collection,
* (array, object, etc)
* It iterates over the entire thing, removes
* duplicate values, and sorts it by the StopID.
*
* This isn't really usable for anything other than
* stops rn. ISSUE: trying to pass an attribute as
* a string param to the function causes it to
* throw out everything except one stop.
*
* @param: collection: Object
* @return: collection: Object <-- the sorted and
* uniquified version of your input.
**/

function uniqueifyAndSort(collection){
  collection = _.uniq(_.union(_.flatten(collection)), _.iteratee('StopId'));
  collection.sort(function(a,b) {
  if (a.Name > b.Name) {
    return 1;
  }
  if (a.Name < b.Name) {
    return -1;
  }
  return 0
  });
  return collection;
}

function populateListGeo(pos) {
  fadeBlack(function() {
    var select = $('.nearby-stops');
    var lat = pos.coords.latitude;
    var lon = pos.coords.longitude;

    $.ajax({
      url: options.url + "stops/getallstops",
      success: function(stop_data) {
        // Sort the stops by distance
        stop_data.sort(function(a,b) {
          return distance(lat, lon, a.Latitude, a.Longitude) - distance(lat, lon, b.Latitude, b.Longitude);
        });
        // Make sure we don't try to find more stops than exist.
        var stop_count = Math.min(10, stop_data.length);
        // Get the nearest few stops, removing duplicates that appear for some reason
        stop_data = _.uniq(stop_data.slice(0, stop_count), _.iteratee('StopId'));
        stopList(select, stop_data);
        $('.nearby-holder').show();
        removeFade();
      }
    });
  });
}

// Distance in miles
function distance(lat1, lon1, lat2, lon2) {
	var radlat1 = Math.PI * lat1/180
	var radlat2 = Math.PI * lat2/180
	var radlon1 = Math.PI * lon1/180
	var radlon2 = Math.PI * lon2/180
	var theta = lon1-lon2
	var radtheta = Math.PI * theta/180
	var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
	dist = Math.acos(dist)
	dist = dist * 180/Math.PI
	dist = dist * 60 * 1.1515
	return dist
}

// Show our loader
function fadeBlack(callback) {
  if (typeof callback === "undefined") {
    callback = function(){};
  }

  var holder = $('.load-holder');
  holder.css({'z-index': 10});
  // If it's not black yet
  if (holder.hasClass('fade')) {
    holder.removeClass('fade');
    holder.one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', callback);
  } else {
    callback();
  }
}

// Hide our loader
function removeFade(callback) {
  if (typeof callback === "undefined") {
    callback = function(){};
  }

  var holder = $('.load-holder');
  // If it's already gone
  if (holder.hasClass('fade')) {
    callback();
  } else {
    holder.addClass('fade');
    holder.one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', function(e) {
      $(this).css({'z-index': -1});
      callback();
    });
  }
}

// input is an array of strings.
// removes the 'StopSelector' portion of the URL;
// maintains the existing query string,
// and appends to that query string:
//   mobile: true,
//   stops: the stop IDs given
function busBoardURL(stops) {
  var url = window.location.href;
  var url_without_stop_selector = url.split("/StopSelector")[0]

  var query_obj = QueryStringAsObject()
  query_obj["mobile"] = true
  query_obj["stops"] = stops
  var new_query_string = ObjectAsQueryString(query_obj)

  return url_without_stop_selector + new_query_string
}

function updateOptions() {

  var query = QueryStringAsObject();

  var title_string = query.title;
  if (typeof title_string !== "undefined") {
    // To handle browsers adding a slash after the query string
    if (title_string.slice(-1) == "/") {
      title_string = title_string.slice(0,-1);
    }
    options.title = title_string;
  }

  var logo_url_string = query.logo_url;
  if (typeof logo_url_string !== 'undefined'){
    if (logo_url_string.slice(-1) == "/") {
      logo_url_string = logo_url_string.slice(0,-1);
    }
    options.logo_url = logo_url_string;
  }
}

function QueryStringAsObject() {
  //if there's no query string, return an empty object
  if(window.location.href.indexOf('?') == -1){
    return new Object()
  }
  var pairs = location.search.slice(1).split('&');

  var result = {};
  for(var i = 0; i < pairs.length; i++) {
      var pair = pairs[i].split('=');
      result[pair[0]] = decodeURIComponent(pair[1] || '');
  }

  return result;
}

function ObjectAsQueryString(obj){
  if (Object.keys(obj).length == 0){
    return ""
  }
  var query_string = '?'
  for (var key in obj){
    query_string += '&'
    query_string += key
    query_string += '='
    query_string += obj[key]
  }
  return query_string
}
<commit_msg>More correct Object-to-URL conversion<commit_after>// Javascript for parsing and displaying departure information

var options =
{
  url: "http://bustracker.pvta.com/InfoPoint/rest/",
  title: "",                      // title to display at top of page
  logo_url: ""                    // logo to display at top of page
}

$(function() {
  loadAllStops();
  updateOptions();
  initTitle();

  setTimeout(removeFade, 500);
  // Load the BusInfoBoard with the selected stops
  $( '.go-button').on('click', function() {
    // Buttons are the row after the stop selector
    var stops = $(this).parents('.row').prev().find('.stops').val();
    document.location.href = busBoardURL(stops);
  });

  // List of all PVTA routes
  var routes = $('.routes');
  var routeStops = $('.route-stops');
  var everyStop;

  // Use the Chosen jQuery plugin for our multiple select boxes
  routes.chosen();
  routeStops.chosen();
  $('.nearby-stops').chosen();

/**This function will download and display
 * **all** of PVTA's stops, so that users can
 * type in and view a specific stop or
 * number of stops without having to first pick
 * a route.
 **/
  function loadAllStops(){
    var stops = [];
    /** If we've already downloaded
     * and sorted every stop, no
     * need to do it again
     **/
    var span = document.getElementById('stop-selector').innerHTML = "
Or: Search Stops
";
    if(everyStop){
      //Display the list of stops
      stopList(routeStops, everyStop);
      return;
    }
      $.ajax({
        url: options.url + 'stops/getallstops',
        success: function(allStops){
          // Set the everyStop variable to the list so
          // we don't have to download it all again.
          everyStop = allStops;
          everyStop = uniqueifyAndSort(everyStop);
          stopList(routeStops, everyStop);
        } //end success callback
    }); //end ajax call
  }

  // When a route is added or removed from the list, reload the list of stops
  // accessible by those routes
  routes.on("change", function() {
    var routes = $('.routes').val() || [];
    var remainingRoutes = routes.length;
    var stops = [];
    // For each route selected, we get a list of stops
    if(routes.length === 0) loadAllStops();
    else{
      var span = document.getElementById('stop-selector').innerHTML = "
Now Search Stops
";
      for (var i = 0; i < routes.length; i++) {
        $.ajax({
          url: options.url + "routedetails/get/" + routes[i],
          success: function(route_details) {
            stops.push(route_details.Stops);
            remainingRoutes--;
            if (remainingRoutes == 0) {
              // Put all of the stops into a single array and sort them
              stops = uniqueifyAndSort(stops);
              stopList(routeStops, stops);
            }
          } //end success callback
        }); //end ajax call
      } //end for
    } //end else
  });

  // Ask the user for their location
  getLocation();

  // Load all of the routes from the InfoPoint API
  $.ajax({
    url: options.url + "routes/getvisibleroutes",
    success: function(route_data) {
      // Sort routes by Route name
      route_data.sort(function(a,b) {
        if (a.RouteAbbreviation > b.RouteAbbreviation) {
          return 1;
        }
        if (a.RouteAbbreviation < b.RouteAbbreviation) {
          return -1;
        }
        return 0
      });

      for (var i = 0; i < route_data.length; i++) {
        routes.append('<option value="' + route_data[i].RouteId + '">' +
            route_data[i].RouteAbbreviation + " " + route_data[i].LongName +
            '</option>');
      }
      // Refresh our list
      routes.trigger('chosen:updated');
    }
  });
});

function initTitle() {
  var body = $('.body');

  if (typeof options.logo_url !== "undefined" && options.logo_url != "") {
    body.prepend('');
  }

  if (typeof options.title !== "undefined" && options.title != "") {
    body.prepend('
' + options.title + '
');
  }

}

function getLocation() {
  $('.nearby-holder').hide();
  if (Modernizr.geolocation) {
    return navigator.geolocation.getCurrentPosition(populateListGeo, function(){
      removeFade();
    });
  } else {
    // NoGeo option
  }
}

function stopList(select, stops) {
  fadeBlack(function() {
    select.empty();
    for(var i = 0; i < stops.length; i++) {
      select.append('<option value="' + stops[i].StopId + '">' + stops[i].Name + '</option>');
    }
    select.trigger('chosen:updated');
    removeFade();
  });
}

/**
* This function takes in a collection,
* (array, object, etc)
* It iterates over the entire thing, removes
* duplicate values, and sorts it by the StopID.
*
* This isn't really usable for anything other than
* stops rn. ISSUE: trying to pass an attribute as
* a string param to the function causes it to
* throw out everything except one stop.
*
* @param: collection: Object
* @return: collection: Object <-- the sorted and
* uniquified version of your input.
**/

function uniqueifyAndSort(collection){
  collection = _.uniq(_.union(_.flatten(collection)), _.iteratee('StopId'));
  collection.sort(function(a,b) {
  if (a.Name > b.Name) {
    return 1;
  }
  if (a.Name < b.Name) {
    return -1;
  }
  return 0
  });
  return collection;
}

function populateListGeo(pos) {
  fadeBlack(function() {
    var select = $('.nearby-stops');
    var lat = pos.coords.latitude;
    var lon = pos.coords.longitude;

    $.ajax({
      url: options.url + "stops/getallstops",
      success: function(stop_data) {
        // Sort the stops by distance
        stop_data.sort(function(a,b) {
          return distance(lat, lon, a.Latitude, a.Longitude) - distance(lat, lon, b.Latitude, b.Longitude);
        });
        // Make sure we don't try to find more stops than exist.
        var stop_count = Math.min(10, stop_data.length);
        // Get the nearest few stops, removing duplicates that appear for some reason
        stop_data = _.uniq(stop_data.slice(0, stop_count), _.iteratee('StopId'));
        stopList(select, stop_data);
        $('.nearby-holder').show();
        removeFade();
      }
    });
  });
}

// Distance in miles
function distance(lat1, lon1, lat2, lon2) {
	var radlat1 = Math.PI * lat1/180
	var radlat2 = Math.PI * lat2/180
	var radlon1 = Math.PI * lon1/180
	var radlon2 = Math.PI * lon2/180
	var theta = lon1-lon2
	var radtheta = Math.PI * theta/180
	var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
	dist = Math.acos(dist)
	dist = dist * 180/Math.PI
	dist = dist * 60 * 1.1515
	return dist
}

// Show our loader
function fadeBlack(callback) {
  if (typeof callback === "undefined") {
    callback = function(){};
  }

  var holder = $('.load-holder');
  holder.css({'z-index': 10});
  // If it's not black yet
  if (holder.hasClass('fade')) {
    holder.removeClass('fade');
    holder.one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', callback);
  } else {
    callback();
  }
}

// Hide our loader
function removeFade(callback) {
  if (typeof callback === "undefined") {
    callback = function(){};
  }

  var holder = $('.load-holder');
  // If it's already gone
  if (holder.hasClass('fade')) {
    callback();
  } else {
    holder.addClass('fade');
    holder.one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', function(e) {
      $(this).css({'z-index': -1});
      callback();
    });
  }
}

// input is an array of strings.
// removes the 'StopSelector' portion of the URL;
// maintains the existing query string,
// and appends to that query string:
//   mobile: true,
//   stops: the stop IDs given
function busBoardURL(stops) {
  var url = window.location.href;
  var url_without_stop_selector = url.split("/StopSelector")[0]

  var query_obj = QueryStringAsObject()
  query_obj["mobile"] = true
  query_obj["stops"] = stops
  var new_query_string = ObjectAsQueryString(query_obj)

  return url_without_stop_selector + new_query_string
}

function updateOptions() {

  var query = QueryStringAsObject();

  var title_string = query.title;
  if (typeof title_string !== "undefined") {
    // To handle browsers adding a slash after the query string
    if (title_string.slice(-1) == "/") {
      title_string = title_string.slice(0,-1);
    }
    options.title = title_string;
  }

  var logo_url_string = query.logo_url;
  if (typeof logo_url_string !== 'undefined'){
    if (logo_url_string.slice(-1) == "/") {
      logo_url_string = logo_url_string.slice(0,-1);
    }
    options.logo_url = logo_url_string;
  }
}

function QueryStringAsObject() {
  //if there's no query string, return an empty object
  if(window.location.href.indexOf('?') == -1){
    return new Object()
  }
  var pairs = location.search.slice(1).split('&');

  var result = {};
  for(var i = 0; i < pairs.length; i++) {
      var pair = pairs[i].split('=');
      result[pair[0]] = decodeURIComponent(pair[1] || '');
  }

  return result;
}

function ObjectAsQueryString(obj){
  if (Object.keys(obj).length == 0){
    return ""
  }
  var pairs = []
  for (var key in obj){
    pairs.push(key + "=" + obj[key])
  }
  return '?' + pairs.join('&')
}
<|endoftext|>