<commit_before>/*jslint node: true */
'use strict';

var winston = require('winston'),
    express = require('express'),
    expressJoi = require('express-joi-validator'),
    expressWinston = require('express-winston'),
    bodyparser = require('body-parser'),
    mqtt = require('mqtt'),
    async = require('async'),
    path = require('path'),
    url = require('url'),
    joi = require('joi'),
    yaml = require('js-yaml'),
    jsonfile = require('jsonfile'),
    fs = require('fs'),
    semver = require('semver'),
    request = require('request');

var CONFIG_DIR = process.env.CONFIG_DIR || process.cwd(),
    CONFIG_FILE = path.join(CONFIG_DIR, 'config.yml'),
    SAMPLE_FILE = path.join(__dirname, '_config.yml'),
    STATE_FILE = path.join(CONFIG_DIR, 'state.json'),
    EVENTS_LOG = path.join(CONFIG_DIR, 'events.log'),
    ACCESS_LOG = path.join(CONFIG_DIR, 'access.log'),
    ERROR_LOG = path.join(CONFIG_DIR, 'error.log'),
    CURRENT_VERSION = require('./package').version,
    TOPIC_STATE = 'state',
    TOPIC_COMMAND = 'command',
    SUFFIX_STATE = 'state_suffix',
    SUFFIX_COMMAND = 'command_suffix';

var app = express(),
    client,
    subscriptions = [],
    callback = '',
    config = {},
    history = {};

// Write all events to disk as well
winston.add(winston.transports.File, {
    filename: EVENTS_LOG,
    json: false
});

/**
 * Load user configuration (or create it)
 * @method loadConfiguration
 * @return {Object} Configuration
 */
function loadConfiguration () {
    if (!fs.existsSync(CONFIG_FILE)) {
        winston.info('No previous configuration found, creating one');
        fs.writeFileSync(CONFIG_FILE, fs.readFileSync(SAMPLE_FILE));
    }

    return yaml.safeLoad(fs.readFileSync(CONFIG_FILE));
}

/**
 * Load the saved previous state from disk
 * @method loadSavedState
 * @return {Object} Configuration
 */
function loadSavedState () {
    var output;
    try {
        output = jsonfile.readFileSync(STATE_FILE);
    } catch (ex) {
        winston.info('No previous state found, continuing');
        output = {
            subscriptions: [],
            callback: '',
            history: {},
            version: '0.0.0'
        };
    }
    return output;
}

/**
 * Resubscribe on a periodic basis
 * @method saveState
 */
function saveState () {
    winston.info('Saving current state');
    jsonfile.writeFileSync(STATE_FILE, {
        subscriptions: subscriptions,
        callback: callback,
        history: history,
        version: CURRENT_VERSION
    }, {
        spaces: 4
    });
}

/**
 * Migrate the configuration from the current version to the latest version
 * @method migrateState
 * @param  {String}     version Version the state was written in before
 */
function migrateState (version) {
    // Make sure the object exists
    if (!config.mqtt) {
        config.mqtt = {};
    }

    // This is the previous default, but it's totally wrong
    if (!config.mqtt.preface) {
        config.mqtt.preface = '/smartthings';
    }

    // Default Suffixes
    if (!config.mqtt[SUFFIX_STATE]) {
        config.mqtt[SUFFIX_STATE] = '';
    }
    if (!config.mqtt[SUFFIX_COMMAND]) {
        config.mqtt[SUFFIX_COMMAND] = '';
    }

    // Default port
    if (!config.port) {
        config.port = 8080;
    }

    // Default protocol
    if (!url.parse(config.mqtt.host).protocol) {
        config.mqtt.host = 'mqtt://' + config.mqtt.host;
    }

    // Stuff was previously in subscription.json, load that and migrate it
    var SUBSCRIPTION_FILE = path.join(CONFIG_DIR, 'subscription.json');
    if (semver.lt(version, '1.1.0') && fs.existsSync(SUBSCRIPTION_FILE)) {
        var oldState = jsonfile.readFileSync(SUBSCRIPTION_FILE);
        callback = oldState.callback;
        subscriptions = oldState.topics;
    }

    saveState();
}

/**
 * Handle Device Change/Push event from SmartThings
 *
 * @method handlePushEvent
 * @param  {Request} req
 * @param  {Object}  req.body
 * @param  {String}  req.body.name  Device Name (e.g. "Bedroom Light")
 * @param  {String}  req.body.type  Device Property (e.g. "state")
 * @param  {String}  req.body.value Value of device (e.g. "on")
 * @param  {Result}  res            Result Object
 */
function handlePushEvent (req, res) {
    var topic = getTopicFor(req.body.name, req.body.type, TOPIC_STATE),
        value = req.body.value;

    winston.info('Incoming message from SmartThings: %s = %s', topic, value);
    history[topic] = value;

    client.publish(topic, value, {
        retain: true
    }, function () {
        res.send({
            status: 'OK'
        });
    });
}

/**
 * Handle Subscribe event from SmartThings
 *
 * @method handleSubscribeEvent
 * @param  {Request} req
 * @param  {Object}  req.body
 * @param  {Object}  req.body.devices  List of properties => device names
 * @param  {String}  req.body.callback Host and port for SmartThings Hub
 * @param  {Result}  res               Result Object
 */
function handleSubscribeEvent (req, res) {
    // Subscribe to all events
    subscriptions = [];
    Object.keys(req.body.devices).forEach(function (property) {
        req.body.devices[property].forEach(function (device) {
            subscriptions.push(getTopicFor(device, property, TOPIC_COMMAND));
        });
    });

    // Store callback
    callback = req.body.callback;

    // Store current state on disk
    saveState();

    // Subscribe to events
    winston.info('Subscribing to ' + subscriptions.join(', '));
    client.subscribe(subscriptions, function () {
        res.send({
            status: 'OK'
        });
    });
}

/**
 * Get the topic name for a given item
 * @method getTopicFor
 * @param  {String}    device   Device Name
 * @param  {String}    property Property
 * @param  {String}    type     Type of topic (command or state)
 * @return {String}             MQTT Topic name
 */
function getTopicFor (device, property, type) {
    var tree = [config.mqtt.preface, device, property],
        suffix;

    if (type === TOPIC_COMMAND) {
        suffix = config.mqtt[SUFFIX_COMMAND];
    } else if (type === TOPIC_STATE) {
        suffix = config.mqtt[SUFFIX_STATE];
    }

    if (suffix) {
        tree.push(suffix);
    }

    return tree.join('/');
}

/**
 * Parse incoming message from MQTT
 * @method parseMQTTMessage
 * @param  {String} topic   Topic channel the event came from
 * @param  {String} message Contents of the event
 */
function parseMQTTMessage (topic, message) {
    var contents = message.toString();
    winston.info('Incoming message from MQTT: %s = %s', topic, contents);

    // Remove the preface from the topic before splitting it
    var pieces = topic.substr(config.mqtt.preface.length + 1).split('/'),
        device = pieces[0],
        property = pieces[1],
        topicState = getTopicFor(device, 'switch', TOPIC_STATE),
        topicCommand = getTopicFor(device, 'switch', TOPIC_COMMAND);

    if (history[topicCommand] === contents) {
        winston.debug('Skipping duplicate message from: %s = %s', topic, contents);
        return;
    }
    history[topic] = contents;

    // If sending level data and the switch is off, don't send anything
    // SmartThings will turn the device on (which is confusing)
    if (property === 'level' && history[topicState] === 'off') {
        winston.info('Skipping level set due to device being off');
        return;
    }

    // If sending switch data and there is already a level value, send level instead
    // SmartThings will turn the device on
    if (property === 'switch' && contents === 'on' &&
        history[getTopicFor(device, 'level', TOPIC_COMMAND)] !== undefined) {
        winston.info('Passing level instead of switch on');
        property = 'level';
        contents = history[getTopicFor(device, 'level', TOPIC_COMMAND)];
    }

    request.post({
        url: 'http://' + callback,
        json: {
            name: device,
            type: property,
            value: contents
        }
    }, function (error, resp) {
        if (error) {
            // @TODO handle the response from SmartThings
            winston.error('Error from SmartThings Hub: %s', error.toString());
            winston.error(JSON.stringify(error, null, 4));
            winston.error(JSON.stringify(resp, null, 4));
        }
    });
}

// Main flow
async.series([
    function loadFromDisk (next) {
        var state;

        winston.info('Starting SmartThings MQTT Bridge - v%s', CURRENT_VERSION);
        winston.info('Loading configuration');
        config = loadConfiguration();

        winston.info('Loading previous state');
        state = loadSavedState();
        callback = state.callback;
        subscriptions = state.subscriptions;
        history = state.history;

        winston.info('Perfoming configuration migration');
        migrateState(state.version);

        process.nextTick(next);
    },
    function connectToMQTT (next) {
        winston.info('Connecting to MQTT at %s', config.mqtt.host);

        client = mqtt.connect(config.mqtt.host, config.mqtt);
        client.on('message', parseMQTTMessage);
        client.on('connect', function () {
            if (subscriptions.length > 0) {
                client.subscribe(subscriptions);
            }
            next();
            // @TODO Not call this twice if we get disconnected
            next = function () {};
        });
    },
    function configureCron (next) {
        winston.info('Configuring autosave');

        // Save current state every 15 minutes
        setInterval(saveState, 15 * 60 * 1000);

        process.nextTick(next);
    },
    function setupApp (next) {
        winston.info('Configuring API');

        // Accept JSON
        app.use(bodyparser.json());

        // Log all requests to disk
        app.use(expressWinston.logger({
            transports: [
                new winston.transports.File({
                    filename: ACCESS_LOG,
                    json: false
                })
            ]
        }));

        // Push event from SmartThings
        app.post('/push',
            expressJoi({
                body: {
                    //   "name": "Energy Meter",
                    name: joi.string().required(),
                    //   "value": "873",
                    value: joi.string().required(),
                    //   "type": "power",
                    type: joi.string().required()
                }
            }), handlePushEvent);

        // Subscribe event from SmartThings
        app.post('/subscribe',
            expressJoi({
                body: {
                    devices: joi.object().required(),
                    callback: joi.string().required()
                }
            }), handleSubscribeEvent);

        // Log all errors to disk
        app.use(expressWinston.errorLogger({
            transports: [
                new winston.transports.File({
                    filename: ERROR_LOG,
                    json: false
                })
            ]
        }));

        // Proper error messages with Joi
        app.use(function (err, req, res, next) {
            if (err.isBoom) {
                return res.status(err.output.statusCode).json(err.output.payload);
            }
        });

        app.listen(config.port, next);
    }
], function (error) {
    if (error) {
        return winston.error(error);
    }
    winston.info('Listening at http://localhost:%s', config.port);
});
<commit_msg>Fixing duplicate swallowed light requests.  This section is hairy when it comes to command/state topics.<commit_after>/*jslint node: true */
'use strict';

var winston = require('winston'),
    express = require('express'),
    expressJoi = require('express-joi-validator'),
    expressWinston = require('express-winston'),
    bodyparser = require('body-parser'),
    mqtt = require('mqtt'),
    async = require('async'),
    path = require('path'),
    url = require('url'),
    joi = require('joi'),
    yaml = require('js-yaml'),
    jsonfile = require('jsonfile'),
    fs = require('fs'),
    semver = require('semver'),
    request = require('request');

var CONFIG_DIR = process.env.CONFIG_DIR || process.cwd(),
    CONFIG_FILE = path.join(CONFIG_DIR, 'config.yml'),
    SAMPLE_FILE = path.join(__dirname, '_config.yml'),
    STATE_FILE = path.join(CONFIG_DIR, 'state.json'),
    EVENTS_LOG = path.join(CONFIG_DIR, 'events.log'),
    ACCESS_LOG = path.join(CONFIG_DIR, 'access.log'),
    ERROR_LOG = path.join(CONFIG_DIR, 'error.log'),
    CURRENT_VERSION = require('./package').version,
    TOPIC_STATE = 'state',
    TOPIC_COMMAND = 'command',
    SUFFIX_STATE = 'state_suffix',
    SUFFIX_COMMAND = 'command_suffix';

var app = express(),
    client,
    subscriptions = [],
    callback = '',
    config = {},
    history = {};

// Write all events to disk as well
winston.add(winston.transports.File, {
    filename: EVENTS_LOG,
    json: false
});

/**
 * Load user configuration (or create it)
 * @method loadConfiguration
 * @return {Object} Configuration
 */
function loadConfiguration () {
    if (!fs.existsSync(CONFIG_FILE)) {
        winston.info('No previous configuration found, creating one');
        fs.writeFileSync(CONFIG_FILE, fs.readFileSync(SAMPLE_FILE));
    }

    return yaml.safeLoad(fs.readFileSync(CONFIG_FILE));
}

/**
 * Load the saved previous state from disk
 * @method loadSavedState
 * @return {Object} Configuration
 */
function loadSavedState () {
    var output;
    try {
        output = jsonfile.readFileSync(STATE_FILE);
    } catch (ex) {
        winston.info('No previous state found, continuing');
        output = {
            subscriptions: [],
            callback: '',
            history: {},
            version: '0.0.0'
        };
    }
    return output;
}

/**
 * Resubscribe on a periodic basis
 * @method saveState
 */
function saveState () {
    winston.info('Saving current state');
    jsonfile.writeFileSync(STATE_FILE, {
        subscriptions: subscriptions,
        callback: callback,
        history: history,
        version: CURRENT_VERSION
    }, {
        spaces: 4
    });
}

/**
 * Migrate the configuration from the current version to the latest version
 * @method migrateState
 * @param  {String}     version Version the state was written in before
 */
function migrateState (version) {
    // Make sure the object exists
    if (!config.mqtt) {
        config.mqtt = {};
    }

    // This is the previous default, but it's totally wrong
    if (!config.mqtt.preface) {
        config.mqtt.preface = '/smartthings';
    }

    // Default Suffixes
    if (!config.mqtt[SUFFIX_STATE]) {
        config.mqtt[SUFFIX_STATE] = '';
    }
    if (!config.mqtt[SUFFIX_COMMAND]) {
        config.mqtt[SUFFIX_COMMAND] = '';
    }

    // Default port
    if (!config.port) {
        config.port = 8080;
    }

    // Default protocol
    if (!url.parse(config.mqtt.host).protocol) {
        config.mqtt.host = 'mqtt://' + config.mqtt.host;
    }

    // Stuff was previously in subscription.json, load that and migrate it
    var SUBSCRIPTION_FILE = path.join(CONFIG_DIR, 'subscription.json');
    if (semver.lt(version, '1.1.0') && fs.existsSync(SUBSCRIPTION_FILE)) {
        var oldState = jsonfile.readFileSync(SUBSCRIPTION_FILE);
        callback = oldState.callback;
        subscriptions = oldState.topics;
    }

    saveState();
}

/**
 * Handle Device Change/Push event from SmartThings
 *
 * @method handlePushEvent
 * @param  {Request} req
 * @param  {Object}  req.body
 * @param  {String}  req.body.name  Device Name (e.g. "Bedroom Light")
 * @param  {String}  req.body.type  Device Property (e.g. "state")
 * @param  {String}  req.body.value Value of device (e.g. "on")
 * @param  {Result}  res            Result Object
 */
function handlePushEvent (req, res) {
    var topic = getTopicFor(req.body.name, req.body.type, TOPIC_STATE),
        value = req.body.value;

    winston.info('Incoming message from SmartThings: %s = %s', topic, value);
    history[topic] = value;

    client.publish(topic, value, {
        retain: true
    }, function () {
        res.send({
            status: 'OK'
        });
    });
}

/**
 * Handle Subscribe event from SmartThings
 *
 * @method handleSubscribeEvent
 * @param  {Request} req
 * @param  {Object}  req.body
 * @param  {Object}  req.body.devices  List of properties => device names
 * @param  {String}  req.body.callback Host and port for SmartThings Hub
 * @param  {Result}  res               Result Object
 */
function handleSubscribeEvent (req, res) {
    // Subscribe to all events
    subscriptions = [];
    Object.keys(req.body.devices).forEach(function (property) {
        req.body.devices[property].forEach(function (device) {
            subscriptions.push(getTopicFor(device, property, TOPIC_COMMAND));
        });
    });

    // Store callback
    callback = req.body.callback;

    // Store current state on disk
    saveState();

    // Subscribe to events
    winston.info('Subscribing to ' + subscriptions.join(', '));
    client.subscribe(subscriptions, function () {
        res.send({
            status: 'OK'
        });
    });
}

/**
 * Get the topic name for a given item
 * @method getTopicFor
 * @param  {String}    device   Device Name
 * @param  {String}    property Property
 * @param  {String}    type     Type of topic (command or state)
 * @return {String}             MQTT Topic name
 */
function getTopicFor (device, property, type) {
    var tree = [config.mqtt.preface, device, property],
        suffix;

    if (type === TOPIC_COMMAND) {
        suffix = config.mqtt[SUFFIX_COMMAND];
    } else if (type === TOPIC_STATE) {
        suffix = config.mqtt[SUFFIX_STATE];
    }

    if (suffix) {
        tree.push(suffix);
    }

    return tree.join('/');
}

/**
 * Parse incoming message from MQTT
 * @method parseMQTTMessage
 * @param  {String} topic   Topic channel the event came from
 * @param  {String} message Contents of the event
 */
function parseMQTTMessage (topic, message) {
    var contents = message.toString();
    winston.info('Incoming message from MQTT: %s = %s', topic, contents);

    // Remove the preface from the topic before splitting it
    var pieces = topic.substr(config.mqtt.preface.length + 1).split('/'),
        device = pieces[0],
        property = pieces[1],
        topicState = getTopicFor(device, property, TOPIC_STATE),
        topicSwitchState = getTopicFor(device, 'switch', TOPIC_STATE),
        topicLevelCommand = getTopicFor(device, 'level', TOPIC_COMMAND);

    if (history[topicState] === contents) {
        winston.info('Skipping duplicate message from: %s = %s', topic, contents);
        return;
    }
    history[topic] = contents;

    // If sending level data and the switch is off, don't send anything
    // SmartThings will turn the device on (which is confusing)
    if (property === 'level' && history[topicSwitchState] === 'off') {
        winston.info('Skipping level set due to device being off');
        return;
    }

    // If sending switch data and there is already a level value, send level instead
    // SmartThings will turn the device on
    if (property === 'switch' && contents === 'on' &&
        history[topicLevelCommand] !== undefined) {
        winston.info('Passing level instead of switch on');
        property = 'level';
        contents = history[topicLevelCommand];
    }

    request.post({
        url: 'http://' + callback,
        json: {
            name: device,
            type: property,
            value: contents
        }
    }, function (error, resp) {
        if (error) {
            // @TODO handle the response from SmartThings
            winston.error('Error from SmartThings Hub: %s', error.toString());
            winston.error(JSON.stringify(error, null, 4));
            winston.error(JSON.stringify(resp, null, 4));
        }
    });
}

// Main flow
async.series([
    function loadFromDisk (next) {
        var state;

        winston.info('Starting SmartThings MQTT Bridge - v%s', CURRENT_VERSION);
        winston.info('Loading configuration');
        config = loadConfiguration();

        winston.info('Loading previous state');
        state = loadSavedState();
        callback = state.callback;
        subscriptions = state.subscriptions;
        history = state.history;

        winston.info('Perfoming configuration migration');
        migrateState(state.version);

        process.nextTick(next);
    },
    function connectToMQTT (next) {
        winston.info('Connecting to MQTT at %s', config.mqtt.host);

        client = mqtt.connect(config.mqtt.host, config.mqtt);
        client.on('message', parseMQTTMessage);
        client.on('connect', function () {
            if (subscriptions.length > 0) {
                client.subscribe(subscriptions);
            }
            next();
            // @TODO Not call this twice if we get disconnected
            next = function () {};
        });
    },
    function configureCron (next) {
        winston.info('Configuring autosave');

        // Save current state every 15 minutes
        setInterval(saveState, 15 * 60 * 1000);

        process.nextTick(next);
    },
    function setupApp (next) {
        winston.info('Configuring API');

        // Accept JSON
        app.use(bodyparser.json());

        // Log all requests to disk
        app.use(expressWinston.logger({
            transports: [
                new winston.transports.File({
                    filename: ACCESS_LOG,
                    json: false
                })
            ]
        }));

        // Push event from SmartThings
        app.post('/push',
            expressJoi({
                body: {
                    //   "name": "Energy Meter",
                    name: joi.string().required(),
                    //   "value": "873",
                    value: joi.string().required(),
                    //   "type": "power",
                    type: joi.string().required()
                }
            }), handlePushEvent);

        // Subscribe event from SmartThings
        app.post('/subscribe',
            expressJoi({
                body: {
                    devices: joi.object().required(),
                    callback: joi.string().required()
                }
            }), handleSubscribeEvent);

        // Log all errors to disk
        app.use(expressWinston.errorLogger({
            transports: [
                new winston.transports.File({
                    filename: ERROR_LOG,
                    json: false
                })
            ]
        }));

        // Proper error messages with Joi
        app.use(function (err, req, res, next) {
            if (err.isBoom) {
                return res.status(err.output.statusCode).json(err.output.payload);
            }
        });

        app.listen(config.port, next);
    }
], function (error) {
    if (error) {
        return winston.error(error);
    }
    winston.info('Listening at http://localhost:%s', config.port);
});
<|endoftext|>


<commit_before>// lobby.js ~ Copyright 2017 ~ Paul Beaudet MIT license
var socket = {
    io: io(),
    init: function(){
        socket.io.on('lobbyInfo', lobby.app.render);
        socket.io.on('confirm', lobby.app.confirm);
        socket.io.emit('getLobbyInfo', {
            lobbyname: window.location.href.split('/')[4]
        });
    }
};

var time = {
    compare: { // return true for available times
        nightToMorning: function(hour, start, end){return (hour <= start || hour > end);},
        morningToNight: function(hour, start, end){return (hour >= start && hour < end);},
    },
    getText: function(hour){ // convert military to civilian time
        if(hour){
            if(hour > 11){
                if(hour === 12){return '12PM';}
                else{return hour - 12 + 'PM';}
            } else {return hour + 'AM';}
        } else {return "12AM";}
    }
};

var lobby = {      // admin controls
    app: new Vue({ // I can only imagine this framework is full of dank memes
        el: '#app',
        data: {
            lobbyname: window.location.href.split('/')[4],
            openTimes: [],
            selectedTime: '',
            hasUserInfo: false,
            onloadTime: new Date(),
            who: '',
            confirmation: 'no hangout pending',
        },
        methods: {
            render: function(data){ // show availability information for this user
                var compare = 'morningToNight'; // default is available morning to night
                if(data.doNotDisturbStart > data.doNotDisturbEnd){compare = 'nightToMorning';}
                for(var hour = this.onloadTime.getHours() + 1; hour < 24; hour++){      // just for today
                    if(time.compare[compare](hour, data.doNotDisturbStart, data.doNotDisturbEnd)){
                        this.openTimes.push({text:time.getText(hour), value:hour}); // can has, render something
                    }
                }
                this.hasUserInfo = true; // signal that view is ready to be rendered
            },
            appointment: function(){
                if(this.who && this.selectedTime){
                    this.onloadTime.setHours(this.selectedTime);
                    socket.io.emit('appointment', { // first lets just assume for today an work from there
                        lobbyname: this.lobbyname,
                        time: this.onloadTime.getTime(),
                        who: this.who
                    });
                } else {
                    this.confirmation = 'Have to know who you are and when';
                }
            },
            confirm: function(data){  // confirms appointment was made or not
                if(data.ok){this.confirmation = 'hangout pending';}
                else{this.confirmation = 'something went wrong';}
            }
        }
    })
};

socket.init();
<commit_msg>exclude upcomming appointments<commit_after>// lobby.js ~ Copyright 2017 ~ Paul Beaudet MIT license
var socket = {
    io: io(),
    init: function(){
        socket.io.on('lobbyInfo', lobby.app.render);
        socket.io.on('confirm', lobby.app.confirm);
        socket.io.emit('getLobbyInfo', {
            lobbyname: window.location.href.split('/')[4]
        });
    }
};

var time = {
    COVERAGE: 24, // hours of coverage
    compare: {    // return true for available times
        nightToMorning: function(hour, start, end){return (hour <= start || hour > end);},
        morningToNight: function(hour, start, end){return (hour >= start && hour < end);},
    },
    getText: function(hour){ // convert military to civilian time
        if(hour){
            if(hour > 11){
                if(hour === 12){return '12PM';}
                else{return hour - 12 + 'PM';}
            } else {return hour + 'AM';}
        } else {return "12AM";}
    },
    availHours: function(appointments){ // takes array un millis timestampted appointments
        if(appointments.length){        // given that we have any appointments
            availabilityArray = [];
            for(var hour = 0; hour < time.COVERAGE; hour++){
                availabilityArray[hour] = true; // default everything to true
            }
            for(var appointment = 0; appointment < appointments.length; appointment++){
                var busyTime = new Date(appointments[appointment].time).getHours();
                if(busyTime > -1 && busyTime < time.COVERAGE){ // only with in the range of our time coverage
                    availabilityArray[busyTime] = false;       // mark taken times
                }
            }
            return availabilityArray;
        }
    }
};

var lobby = {      // admin controls
    app: new Vue({ // I can only imagine this framework is full of dank memes
        el: '#app',
        data: {
            lobbyname: window.location.href.split('/')[4],
            openTimes: [],
            selectedTime: '',
            hasUserInfo: false,
            onloadTime: new Date(),
            who: '',
            confirmation: 'no hangout pending',
        },
        methods: {
            render: function(data){ // show availability information for this user
                var compare = 'morningToNight'; // default is available morning to night
                var avail = time.availHours(data.appointments);
                if(data.doNotDisturbStart > data.doNotDisturbEnd){compare = 'nightToMorning';}
                for(var hour = this.onloadTime.getHours() + 1; hour < time.COVERAGE; hour++){  // just for today
                    if(time.compare[compare](hour, data.doNotDisturbStart, data.doNotDisturbEnd) && avail[hour]){
                        this.openTimes.push({text:time.getText(hour), value:hour}); // can has, render something
                    }
                }
                this.hasUserInfo = true; // signal that view is ready to be rendered
            },
            appointment: function(){
                if(this.who && this.selectedTime){
                    this.onloadTime.setHours(this.selectedTime);
                    socket.io.emit('appointment', { // first lets just assume for today an work from there
                        lobbyname: this.lobbyname,
                        time: this.onloadTime.getTime(),
                        who: this.who
                    });
                } else {
                    this.confirmation = 'Have to know who you are and when';
                }
            },
            confirm: function(data){  // confirms appointment was made or not
                if(data.ok){this.confirmation = 'hangout pending';}
                else{this.confirmation = 'something went wrong';}
            }
        }
    })
};

socket.init();
<|endoftext|>


<commit_before>'use strict';

var express = require('express'),
    routes = require('./app/routes/index.js'),
    mongoose = require('mongoose'),
    passport = require('passport'),
    Strategy = require('passport-twitter').Strategy;

require('dotenv').load();

// Configure the Twitter strategy for use by Passport.
//
// OAuth 1.0-based strategies require a `verify` function which receives the
// credentials (`token` and `tokenSecret`) for accessing the Twitter API on the
// user's behalf, along with the user's profile.  The function must invoke `cb`
// with a user object, which will be set at `req.user` in route handlers after
// authentication.
passport.use(new Strategy({
    consumerKey: process.env.TWITTER_CONSUMER_KEY,
    consumerSecret: process.env.TWITTER_CONSUMER_SECRET,
    callbackURL: 'http://localhost:8080/login/twitter/return'
  },
  function(token, tokenSecret, profile, cb) {
    // In this example, the user's Twitter profile is supplied as the user
    // record.  In a production-quality application, the Twitter profile should
    // be associated with a user record in the application's database, which
    // allows for account linking and authentication with other identity
    // providers.
    return cb(null, profile);
}));

// Configure Passport authenticated session persistence.
//
// In order to restore authentication state across HTTP requests, Passport needs
// to serialize users into and deserialize users out of the session.  In a
// production-quality application, this would typically be as simple as
// supplying the user ID when serializing, and querying the user record by ID
// from the database when deserializing.  However, due to the fact that this
// example does not have a database, the complete Twitter profile is serialized
// and deserialized.
passport.serializeUser(function(user, cb) {
  cb(null, user);
});

passport.deserializeUser(function(obj, cb) {
  cb(null, obj);
});

var app = express();
console.log(__dirname);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');

var mongo_uri = process.env.MONGO_URI || 'mongodb://localhost/test';
mongoose.connect(mongo_uri);

var db = mongoose.connection;

db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function() {

  // Use application-level middleware for common functionality, including
  // logging, parsing, and session handling.
  // app.use(require('morgan')('combined'));
  app.use(require('cookie-parser')());
  app.use(require('body-parser').urlencoded({ extended: true }));
  app.use(require('express-session')({ secret: 'keyboard cat', resave: true, saveUninitialized: true }));

  // Initialize Passport and restore authentication state, if any, from the
  // session.
  app.use(passport.initialize());
  app.use(passport.session());

  app.use('/public', express.static(process.cwd() + '/public'));
  //app.use('/controllers', express.static(process.cwd() + '/app/controllers'));

  routes(app, passport);

  var port = process.env.PORT || 8080;
  app.listen(port, function () {
    console.log('Node.js listening on port ' + port + '...');
  });

});
<commit_msg>Add APP_URL to Passport config<commit_after>'use strict';

var express = require('express'),
    routes = require('./app/routes/index.js'),
    mongoose = require('mongoose'),
    passport = require('passport'),
    Strategy = require('passport-twitter').Strategy;

require('dotenv').load();

// Configure the Twitter strategy for use by Passport.
//
// OAuth 1.0-based strategies require a `verify` function which receives the
// credentials (`token` and `tokenSecret`) for accessing the Twitter API on the
// user's behalf, along with the user's profile.  The function must invoke `cb`
// with a user object, which will be set at `req.user` in route handlers after
// authentication.
passport.use(new Strategy({
    consumerKey: process.env.TWITTER_CONSUMER_KEY,
    consumerSecret: process.env.TWITTER_CONSUMER_SECRET,
    callbackURL: process.env.APP_URL + '/login/twitter/return'
  },
  function(token, tokenSecret, profile, cb) {
    // In this example, the user's Twitter profile is supplied as the user
    // record.  In a production-quality application, the Twitter profile should
    // be associated with a user record in the application's database, which
    // allows for account linking and authentication with other identity
    // providers.
    return cb(null, profile);
}));

// Configure Passport authenticated session persistence.
//
// In order to restore authentication state across HTTP requests, Passport needs
// to serialize users into and deserialize users out of the session.  In a
// production-quality application, this would typically be as simple as
// supplying the user ID when serializing, and querying the user record by ID
// from the database when deserializing.  However, due to the fact that this
// example does not have a database, the complete Twitter profile is serialized
// and deserialized.
passport.serializeUser(function(user, cb) {
  cb(null, user);
});

passport.deserializeUser(function(obj, cb) {
  cb(null, obj);
});

var app = express();

app.set('views', __dirname + '/views');
app.set('view engine', 'jade');

var mongo_uri = process.env.MONGO_URI || 'mongodb://localhost/test';
mongoose.connect(mongo_uri);

var db = mongoose.connection;

db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function() {

  // Use application-level middleware for common functionality, including
  // logging, parsing, and session handling.
  // app.use(require('morgan')('combined'));
  app.use(require('cookie-parser')());
  app.use(require('body-parser').urlencoded({ extended: true }));
  app.use(require('express-session')({ secret: 'keyboard cat', resave: true, saveUninitialized: true }));

  // Initialize Passport and restore authentication state, if any, from the
  // session.
  app.use(passport.initialize());
  app.use(passport.session());

  app.use('/public', express.static(process.cwd() + '/public'));
  //app.use('/controllers', express.static(process.cwd() + '/app/controllers'));

  routes(app, passport);

  var port = process.env.PORT || 8080;
  app.listen(port, function () {
    console.log('Node.js listening on port ' + port + '...');
  });

});
<|endoftext|>


<commit_before>/* Function to print job to panel in view. */
function printJob(id, title, description, hours, salary, startDate, state, city, percentageMatch){
    var display = document.getElementById("jobs");

    var panel = document.createElement("div");
    panel.className = "panel panel-default";

    var heading = document.createElement("div");
    heading.className = "panel-heading";
    heading.innerHTML += title + " • ";

    var match = document.createElement("strong");
    match.innerHTML = percentageMatch + "%";

    var body = document.createElement("div");
    body.className = "panel-body";

    var p1 = document.createElement("p");
    p1.innerHTML = description;

    var hr1 = document.createElement("hr");

    var p2 = document.createElement("p");
    p2.innerHTML = hours;

    var p3 = document.createElement("p");
    p3.innerHTML = salary;

    var p4 = document.createElement("p");
    p4.innerHTML = startDate;

    var p5 = document.createElement("p");
    p5.innerHTML = state;

    var p6 = document.createElement("p");
    p6.innerHTML = city;

    var hr2 = document.createElement("hr");

    var p7 = document.createElement("p");

    var apply = document.createElement("a");
    apply.className = "btn btn-primary";
    apply.href = "/job/" + id;
    apply.innerHTML = "View";

    panel.appendChild(heading);
    panel.appendChild(body);
    heading.append(match);
    body.append(p1);
    body.append(hr1);
    body.append(p2);
    body.append(p3);
    body.append(p4);
    body.append(p5);
    body.append(p6);
    body.append(hr2);
    body.append(p7);
    p7.append(apply);
    display.appendChild(panel);

    document.getElementById("loading").style.display = "none";
    document.getElementById("nomatches").style.display = "none";
}

/* Function to perform matchmaking. */
function match(){
    /* Get state filter from document. */
    var stateFilter = document.getElementById("state").value;

    /* Arbitrary number; no. of fields compared. */
    var noOfBits = 25;

    /* Input value (needs to be grabbed from user). */
    var input;

    /* Array of job indexes. */
    var jobIndex = [];

    /* Array of bitwise ints to compare. */
    var jobMatch = [];

    /* Array of percentage matches. */
    var percentageMatch = [];

    /* Get current authenticated user data. */
    $.getJSON("api/user/", function(data){
        input = parseInt("" + data.java + data.python + data.c + data.csharp + data.cplus + data.php + data.html + data.css + data.javascript + data.sql + data.unix + data.winserver + data.windesktop + data.linuxdesktop + data.macosdesktop + data.pearl + data.bash + data.batch + data.cisco + data.office + data.r + data.go + data.ruby + data.asp + data.scala, 2);
    }).then(function(){

        /* Populate values into jobIndex, jobMatch and percentageMatch arrays. */
        $.getJSON( "api/jobs/" + stateFilter, function(data){
            var i;
            for(i = 0; i < data.length; i++)
            {
                jobIndex[i] = i;
                jobMatch[i] = parseInt("" + data[i].java + data[i].python + data[i].c + data[i].csharp + data[i].cplus + data[i].php + data[i].html + data[i].css + data[i].javascript + data[i].sql + data[i].unix + data[i].winserver + data[i].windesktop + data[i].linuxdesktop + data[i].macosdesktop + data[i].pearl + data[i].bash + data[i].batch + data[i].cisco + data[i].office + data[i].r + data[i].go + data[i].ruby + data[i].asp + data[i].scala, 2);

                /* Calculate percentage match */
                var matchCalc = ~(input ^ jobMatch[i]);

                var toBinary = (matchCalc).toString(2);

                if(toBinary < 0){
                    toBinary = (matchCalc >>> 0).toString(2);
                    toBinary = toBinary.slice(-noOfBits);
                }

                var count = toBinary.replace(/[^1]/g, "").length;

                percentageMatch[i] = (count / noOfBits) * 100;
            }

            /* Bubble sort. */
            var swapped;

            do{
                swapped = false;

                var i;
                for(i = 0; i < jobIndex.length-1; i++)
                {
                    if(percentageMatch[i] < percentageMatch[i+1])
                    {
                        var tempPer = percentageMatch[i];
                        percentageMatch[i] = percentageMatch[i+1];
                        percentageMatch[i+1] = tempPer;

                        var tempId = jobIndex[i];
                        jobIndex[i] = jobIndex[i+1];
                        jobIndex[i+1] = tempId;

                        var tempJob = jobMatch[i];
                        jobMatch[i] = jobMatch[i+1];
                        jobMatch[i+1] = tempJob;

                        swapped = true;
                    }
                }
            } while(swapped);
        });

        /* Display jobs. */
        $.getJSON( "api/jobs/" + stateFilter, function(data){
            if(data.length > 0){
                var i;
                for(i = 0; i < data.length; i++)
                {
                    var order = jobIndex[i];
                    printJob(data[order].id, data[order].title, data[order].description, data[order].hours, data[order].salary, data[order].startdate, data[order].state, data[order].city, Math.round(percentageMatch[i]));
                }
            }
            else{
                document.getElementById("loading").style.display = "none";
                document.getElementById("nomatches").style.display = "block";
            }
        });
    });
}

/* Initialisation function to test for JavaScript, display loading animation, and call match function. */
function init(){
    document.getElementById("noscript").style.display = "none";
    document.getElementById("loading").style.display = "block";

    document.getElementById("jobs").innerHTML = "";

    match();
}

document.addEventListener('DOMContentLoaded', init);
document.getElementById("state").addEventListener('change', init);
<commit_msg>Usability improvements<commit_after>/* Function to print job to panel in view. */
function printJob(id, title, description, hours, salary, startDate, state, city, percentageMatch){
    var display = document.getElementById("jobs");

    var panel = document.createElement("div");
    panel.className = "panel panel-default";

    var heading = document.createElement("div");
    heading.className = "panel-heading";
    heading.innerHTML += title + " • ";

    var match = document.createElement("strong");
    match.innerHTML = percentageMatch + "%";

    var body = document.createElement("div");
    body.className = "panel-body";

    var p1 = document.createElement("p");
    p1.innerHTML = description;

    var hr1 = document.createElement("hr");

    var p2 = document.createElement("p");
    var p2Title = document.createElement("strong");

    if(hours == "fulltime"){
        p2.innerHTML = "Full time";
    }
    else if(hours == "parttime"){
        p2.innerHTML = "Part time";
    }
    else{
        p2.innerHTML = "Casual";
    }

    p2Title.innerHTML = "Hours: ";

    var p3 = document.createElement("p");
    var p3Title = document.createElement("strong");

    /* https://stackoverflow.com/a/2901298 */
    p3.innerHTML = "$" + salary.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");

    if(hours == "fulltime"){
        p3.innerHTML += " per annum.";
    }
    else{
        p3.innerHTML += " per hour.";
    }

    p3Title.innerHTML = "Salary: ";

    var p4 = document.createElement("p");
    var p4Title = document.createElement("strong");
    p4.innerHTML = startDate;
    p4Title.innerHTML = "Start date: ";

    var p5 = document.createElement("p");
    var p5Title = document.createElement("strong");
    p5.innerHTML = city;

    if(state == "vic"){
        p5.innerHTML += ", Victoria.";
    }
    else if(state == "nsw"){
        p5.innerHTML += ", New South Wales";
    }
    else if(state == "qld"){
        p5.innerHTML += ", Queensland";
    }
    else if(state == "wa"){
        p5.innerHTML += ", Western Australia";
    }
    else if(state == "sa"){
        p5.innerHTML += ", South Australia";
    }
    else if(state == "ta"){
        p5.innerHTML += ", Tasmania";
    }
    else if(state == "act"){
        p5.innerHTML += ", Australian Capital Territory";
    }
    else if(state == "nt"){
        p5.innerHTML += ", Northern Territory";
    }
    else{
        p5.innerHTML += ", Other Australian Region";
    }

    p5Title.innerHTML = "Location: ";

    var hr2 = document.createElement("hr");

    var p6 = document.createElement("p");

    var apply = document.createElement("a");
    apply.className = "btn btn-primary";
    apply.href = "/job/" + id;
    apply.innerHTML = "View";

    panel.appendChild(heading);
    panel.appendChild(body);
    heading.append(match);
    body.append(p1);
    body.append(hr1);
    body.append(p2);
    p2.prepend(p2Title);
    body.append(p3);
    p3.prepend(p3Title);
    body.append(p4);
    p4.prepend(p4Title);
    body.append(p5);
    p5.prepend(p5Title);
    body.append(hr2);
    body.append(p6);
    p6.append(apply);
    display.appendChild(panel);

    document.getElementById("loading").style.display = "none";
    document.getElementById("nomatches").style.display = "none";
}

/* Function to perform matchmaking. */
function match(){
    /* Get state filter from document. */
    var stateFilter = document.getElementById("state").value;

    /* Arbitrary number; no. of fields compared. */
    var noOfBits = 25;

    /* Input value (needs to be grabbed from user). */
    var input;

    /* Array of job indexes. */
    var jobIndex = [];

    /* Array of bitwise ints to compare. */
    var jobMatch = [];

    /* Array of percentage matches. */
    var percentageMatch = [];

    /* Get current authenticated user data. */
    $.getJSON("api/user/", function(data){
        input = parseInt("" + data.java + data.python + data.c + data.csharp + data.cplus + data.php + data.html + data.css + data.javascript + data.sql + data.unix + data.winserver + data.windesktop + data.linuxdesktop + data.macosdesktop + data.pearl + data.bash + data.batch + data.cisco + data.office + data.r + data.go + data.ruby + data.asp + data.scala, 2);
    }).then(function(){

        /* Populate values into jobIndex, jobMatch and percentageMatch arrays. */
        $.getJSON( "api/jobs/" + stateFilter, function(data){
            var i;
            for(i = 0; i < data.length; i++)
            {
                jobIndex[i] = i;
                jobMatch[i] = parseInt("" + data[i].java + data[i].python + data[i].c + data[i].csharp + data[i].cplus + data[i].php + data[i].html + data[i].css + data[i].javascript + data[i].sql + data[i].unix + data[i].winserver + data[i].windesktop + data[i].linuxdesktop + data[i].macosdesktop + data[i].pearl + data[i].bash + data[i].batch + data[i].cisco + data[i].office + data[i].r + data[i].go + data[i].ruby + data[i].asp + data[i].scala, 2);

                /* Calculate percentage match */
                var matchCalc = ~(input ^ jobMatch[i]);

                var toBinary = (matchCalc).toString(2);

                if(toBinary < 0){
                    toBinary = (matchCalc >>> 0).toString(2);
                    toBinary = toBinary.slice(-noOfBits);
                }

                var count = toBinary.replace(/[^1]/g, "").length;

                percentageMatch[i] = (count / noOfBits) * 100;
            }

            /* Bubble sort. */
            var swapped;

            do{
                swapped = false;

                var i;
                for(i = 0; i < jobIndex.length-1; i++)
                {
                    if(percentageMatch[i] < percentageMatch[i+1])
                    {
                        var tempPer = percentageMatch[i];
                        percentageMatch[i] = percentageMatch[i+1];
                        percentageMatch[i+1] = tempPer;

                        var tempId = jobIndex[i];
                        jobIndex[i] = jobIndex[i+1];
                        jobIndex[i+1] = tempId;

                        var tempJob = jobMatch[i];
                        jobMatch[i] = jobMatch[i+1];
                        jobMatch[i+1] = tempJob;

                        swapped = true;
                    }
                }
            } while(swapped);
        });

        /* Display jobs. */
        $.getJSON( "api/jobs/" + stateFilter, function(data){
            if(data.length > 0){
                var i;
                for(i = 0; i < data.length; i++)
                {
                    var order = jobIndex[i];
                    printJob(data[order].id, data[order].title, data[order].description, data[order].hours, data[order].salary, data[order].startdate, data[order].state, data[order].city, Math.round(percentageMatch[i]));
                }
            }
            else{
                document.getElementById("loading").style.display = "none";
                document.getElementById("nomatches").style.display = "block";
            }
        });
    });
}

/* Initialisation function to test for JavaScript, display loading animation, and call match function. */
function init(){
    document.getElementById("noscript").style.display = "none";
    document.getElementById("loading").style.display = "block";

    document.getElementById("jobs").innerHTML = "";

    match();
}

document.addEventListener('DOMContentLoaded', init);
document.getElementById("state").addEventListener('change', init);
<|endoftext|>


<commit_before>/* global Module */

/* MMM-RottenTomatoes.js
 *
 * Magic Mirror
 * Module: MMM-RottenTomatoes
 *
 * Magic Mirror By Michael Teeuw http://michaelteeuw.nl
 * MIT Licensed.
 *
 * Module MMM-RottenTomatoes By Adam Moses http://adammoses.com
 * MIT Licensed.
 */

Module.register("MMM-RottenTomatoes", {
	// setup the default config options
	defaults: {
		// optional
		showHeader: true,
		showOpeningThisWeek: true,
		showBoxOffice: true,
		showComingSoon: true,
		limitOpeningThisWeek: 3,
		limitBoxOffice: 3,
		limitComingSoon: 3,
		boxOfficeAfter: true,
		mergeOpeningAndComingSoon: true,
	},
	// the start function
	start: function() {
		// log starting
		Log.info("Starting module: " + this.name);
		// set refresh rate to 12 hours
		this.config.refreshRate = 12 * 60 * 60 * 1000;
        // set an identier config tag
		this.config.identifier = this.identifier;
		// set loaded, error, and the update to init values
		this.loaded = false;
		this.errorMessage = null;
		// set the header to this place
        if (this.config.showHeader) {
            this.data.header = 'Rotten Tomatoes';
        }
		if (this.config.limitOpeningThisWeek == 0)
			this.config.limitOpeningThisWeek = 100;
		if (this.config.limitBoxOffice == 0)
			this.config.limitBoxOffice = 100;
		if (this.config.limitComingSoon == 0)
			this.config.limitComingSoon = 100;
        // add this config to the helper functions
		this.sendSocketNotification('ROTTEN_TOMATOES_REGISTER_CONFIG', this.config);
	},
	// the socket handler
	socketNotificationReceived: function(notification, payload) {
		// if an update was received
		if (notification === "ROTTEN_TOMATOES_UPDATE") {
			// check this is for this module based on the woeid
			if (payload.identifier === this.identifier)
			{
				// set loaded flag, set the update, and call update dom
				this.rtData = payload.rtData;
                this.loaded = true;
                this.updateDom();
			}
		}
        // if sent error notice
        if (notification === "ROTTEN_TOMATOES_TOO_MANY_ERRORS") {
            this.errorMessage("There was an error.");
            if (this.updateTimer !== null)
                clearTimeout(this.updateTimer);
            this.updateTimer = null;
            this.updateDom();
        }
	},
	//
	cleanScore: function(theScore) {
		if (theScore.indexOf('%') == -1)
			return '--';
		else
			return theScore;
	},
	// the get dom handler
	getDom: function() {
        // if an error, say so
        if (this.errorMessage !== null) {
            var wrapper = document.createElement("div");
			wrapper.className = "small";
			wrapper.innerHTML = this.errorMessage;
			return wrapper;
        }
		// if nothing loaded yet, put in placeholder text
		if (!this.loaded) {
			var wrapper = document.createElement("div");
			wrapper.className = "small";
			wrapper.innerHTML = "Awaiting Update...";
			return wrapper;
		}
		var titleSize = 'xsmall';
		var movieSize = 'xsmall';
		var wrapper = document.createElement("table");
		// do opening this week
		var allOTWandCSRows = [ ];
		if (this.config.showOpeningThisWeek) {
			var otwData = this.rtData.openingThisWeek;
			var otwTitleTR = document.createElement("tr");
			otwTitleTR.className = titleSize;
			var otwTitleTD = document.createElement("td");
			otwTitleTD.innerHTML = "Opening This Week";
			if (this.config.mergeOpeningAndComingSoon)
				otwTitleTD.innerHTML = "Opening / Coming Soon";
			otwTitleTD.colSpan = "3";
			otwTitleTR.appendChild(otwTitleTD);
			allOTWandCSRows.push(otwTitleTR);
			for (var cIndex = 0;
					(cIndex < otwData.length) && (cIndex < this.config.limitOpeningThisWeek);
					cIndex++) {
				var cOTW = otwData[cIndex];
				var otwRowTR = document.createElement("tr");
				otwRowTR.className = movieSize;
				var otwRowMeter = document.createElement("td");
				otwRowMeter.innerHTML = this.cleanScore(cOTW.meter) + "  ";
				otwRowTR.appendChild(otwRowMeter);
				var otwRowTitle = document.createElement("td");
				otwRowTitle.innerHTML = cOTW.title + "  ";
				otwRowTitle.align = 'left';
				otwRowTR.appendChild(otwRowTitle);
				var otwRowDate = document.createElement("td");
				otwRowDate.innerHTML = "  " + cOTW.date;
				otwRowTR.appendChild(otwRowDate);
				allOTWandCSRows.push(otwRowTR);
			}
		}
		// do opening this week
		if (this.config.showOpeningThisWeek) {
			var csData = this.rtData.comingSoon;
			var csTitleTR = document.createElement("tr");
			csTitleTR.className = titleSize;
			var csTitleTD = document.createElement("td");
			csTitleTD.innerHTML = "Coming Soon";
			csTitleTD.colSpan = "3";
			csTitleTR.appendChild(csTitleTD);
			if (!this.config.mergeOpeningAndComingSoon)
				allOTWandCSRows.push(csTitleTR);
			for (var cIndex = 0;
					(cIndex < csData.length) && (cIndex < this.config.limitComingSoon);
					cIndex++) {
				var ccs = csData[cIndex];
				var csRowTR = document.createElement("tr");
				csRowTR.className = movieSize;
				var csRowMeter = document.createElement("td");
				csRowMeter.innerHTML = this.cleanScore(ccs.meter) + "  ";
				csRowTR.appendChild(csRowMeter);
				var csRowTitle = document.createElement("td");
				csRowTitle.innerHTML = ccs.title + "  ";
				csRowTitle.align = 'left';
				csRowTR.appendChild(csRowTitle);
				var csRowDate = document.createElement("td");
				csRowDate.innerHTML = "  " + ccs.date;
				csRowTR.appendChild(csRowDate);
				allOTWandCSRows.push(csRowTR);
			}
		}
		// do box office
		var boRows = [ ];
		if (this.config.showBoxOffice) {
			var boData = this.rtData.boxOffice;
			var boTitleTR = document.createElement("tr");
			boTitleTR.className = titleSize;
			var boTitleTD = document.createElement("td");
			boTitleTD.innerHTML = "Box Office";
			boTitleTD.width = "250px";
			boTitleTD.colSpan = "3";
			boTitleTR.appendChild(boTitleTD);
			boRows.push(boTitleTR);
			for (var cIndex = 0;
					(cIndex < boData.length) && (cIndex < this.config.limitBoxOffice);
					cIndex++) {
				var cbo = boData[cIndex];
				var boRowTR = document.createElement("tr");
				boRowTR.className = movieSize;
				var boRowMeter = document.createElement("td");
				boRowMeter.innerHTML = this.cleanScore(cbo.meter) + "  ";
				boRowTR.appendChild(boRowMeter);
				var boRowTitle = document.createElement("td");
				boRowTitle.innerHTML = cbo.title + "  ";
				boRowTitle.align = 'left';
				boRowTR.appendChild(boRowTitle);
				var boRowGross = document.createElement("td");
				boRowGross.innerHTML = "  " + cbo.gross;
				boRowTR.appendChild(boRowGross);
				boRows.push(boRowTR);
			}
		}
		// build the table
        var allRows = [ ];
        // if set to show box office first, do that
		if (!this.config.boxOfficeAfter)
			allRows = allRows.concat(boRows);
        // add opening this week and coming soon rows
		allRows = allRows.concat(allOTWandCSRows);
        // if set to show box office after, do that
		if (this.config.boxOfficeAfter)
			allRows = allRows.concat(boRows);
        // add all rows to the return table wrapper
		for (var cIndex = 0; cIndex < allRows.length; cIndex++) {
			wrapper.appendChild(allRows[cIndex]);
		}
        // return table wrapper
		return wrapper;
	}
});

// ------------ end -------------
<commit_msg>changed refresh rate<commit_after>/* global Module */

/* MMM-RottenTomatoes.js
 *
 * Magic Mirror
 * Module: MMM-RottenTomatoes
 *
 * Magic Mirror By Michael Teeuw http://michaelteeuw.nl
 * MIT Licensed.
 *
 * Module MMM-RottenTomatoes By Adam Moses http://adammoses.com
 * MIT Licensed.
 */

Module.register("MMM-RottenTomatoes", {
	// setup the default config options
	defaults: {
		// optional
		showHeader: true,
		showOpeningThisWeek: true,
		showBoxOffice: true,
		showComingSoon: true,
		limitOpeningThisWeek: 3,
		limitBoxOffice: 3,
		limitComingSoon: 3,
		boxOfficeAfter: true,
		mergeOpeningAndComingSoon: true,
	},
	// the start function
	start: function() {
		// log starting
		Log.info("Starting module: " + this.name);
		// set refresh rate to 6 hours
		this.config.refreshRate = 6 * 60 * 60 * 1000;
        // set an identier config tag
		this.config.identifier = this.identifier;
		// set loaded, error, and the update to init values
		this.loaded = false;
		this.errorMessage = null;
		// set the header to this place
        if (this.config.showHeader) {
            this.data.header = 'Rotten Tomatoes';
        }
		if (this.config.limitOpeningThisWeek == 0)
			this.config.limitOpeningThisWeek = 100;
		if (this.config.limitBoxOffice == 0)
			this.config.limitBoxOffice = 100;
		if (this.config.limitComingSoon == 0)
			this.config.limitComingSoon = 100;
        // add this config to the helper functions
		this.sendSocketNotification('ROTTEN_TOMATOES_REGISTER_CONFIG', this.config);
	},
	// the socket handler
	socketNotificationReceived: function(notification, payload) {
		// if an update was received
		if (notification === "ROTTEN_TOMATOES_UPDATE") {
			// check this is for this module based on the woeid
			if (payload.identifier === this.identifier)
			{
				// set loaded flag, set the update, and call update dom
				this.rtData = payload.rtData;
                this.loaded = true;
                this.updateDom();
			}
		}
        // if sent error notice
        if (notification === "ROTTEN_TOMATOES_TOO_MANY_ERRORS") {
            this.errorMessage("There was an error.");
            if (this.updateTimer !== null)
                clearTimeout(this.updateTimer);
            this.updateTimer = null;
            this.updateDom();
        }
	},
	//
	cleanScore: function(theScore) {
		if (theScore.indexOf('%') == -1)
			return '--';
		else
			return theScore;
	},
	// the get dom handler
	getDom: function() {
        // if an error, say so
        if (this.errorMessage !== null) {
            var wrapper = document.createElement("div");
			wrapper.className = "small";
			wrapper.innerHTML = this.errorMessage;
			return wrapper;
        }
		// if nothing loaded yet, put in placeholder text
		if (!this.loaded) {
			var wrapper = document.createElement("div");
			wrapper.className = "small";
			wrapper.innerHTML = "Awaiting Update...";
			return wrapper;
		}
		var titleSize = 'xsmall';
		var movieSize = 'xsmall';
		var wrapper = document.createElement("table");
		// do opening this week
		var allOTWandCSRows = [ ];
		if (this.config.showOpeningThisWeek) {
			var otwData = this.rtData.openingThisWeek;
			var otwTitleTR = document.createElement("tr");
			otwTitleTR.className = titleSize;
			var otwTitleTD = document.createElement("td");
			otwTitleTD.innerHTML = "Opening This Week";
			if (this.config.mergeOpeningAndComingSoon)
				otwTitleTD.innerHTML = "Opening / Coming Soon";
			otwTitleTD.colSpan = "3";
			otwTitleTR.appendChild(otwTitleTD);
			allOTWandCSRows.push(otwTitleTR);
			for (var cIndex = 0;
					(cIndex < otwData.length) && (cIndex < this.config.limitOpeningThisWeek);
					cIndex++) {
				var cOTW = otwData[cIndex];
				var otwRowTR = document.createElement("tr");
				otwRowTR.className = movieSize;
				var otwRowMeter = document.createElement("td");
				otwRowMeter.innerHTML = this.cleanScore(cOTW.meter) + "  ";
				otwRowTR.appendChild(otwRowMeter);
				var otwRowTitle = document.createElement("td");
				otwRowTitle.innerHTML = cOTW.title + "  ";
				otwRowTitle.align = 'left';
				otwRowTR.appendChild(otwRowTitle);
				var otwRowDate = document.createElement("td");
				otwRowDate.innerHTML = "  " + cOTW.date;
				otwRowTR.appendChild(otwRowDate);
				allOTWandCSRows.push(otwRowTR);
			}
		}
		// do opening this week
		if (this.config.showOpeningThisWeek) {
			var csData = this.rtData.comingSoon;
			var csTitleTR = document.createElement("tr");
			csTitleTR.className = titleSize;
			var csTitleTD = document.createElement("td");
			csTitleTD.innerHTML = "Coming Soon";
			csTitleTD.colSpan = "3";
			csTitleTR.appendChild(csTitleTD);
			if (!this.config.mergeOpeningAndComingSoon)
				allOTWandCSRows.push(csTitleTR);
			for (var cIndex = 0;
					(cIndex < csData.length) && (cIndex < this.config.limitComingSoon);
					cIndex++) {
				var ccs = csData[cIndex];
				var csRowTR = document.createElement("tr");
				csRowTR.className = movieSize;
				var csRowMeter = document.createElement("td");
				csRowMeter.innerHTML = this.cleanScore(ccs.meter) + "  ";
				csRowTR.appendChild(csRowMeter);
				var csRowTitle = document.createElement("td");
				csRowTitle.innerHTML = ccs.title + "  ";
				csRowTitle.align = 'left';
				csRowTR.appendChild(csRowTitle);
				var csRowDate = document.createElement("td");
				csRowDate.innerHTML = "  " + ccs.date;
				csRowTR.appendChild(csRowDate);
				allOTWandCSRows.push(csRowTR);
			}
		}
		// do box office
		var boRows = [ ];
		if (this.config.showBoxOffice) {
			var boData = this.rtData.boxOffice;
			var boTitleTR = document.createElement("tr");
			boTitleTR.className = titleSize;
			var boTitleTD = document.createElement("td");
			boTitleTD.innerHTML = "Box Office";
			boTitleTD.width = "250px";
			boTitleTD.colSpan = "3";
			boTitleTR.appendChild(boTitleTD);
			boRows.push(boTitleTR);
			for (var cIndex = 0;
					(cIndex < boData.length) && (cIndex < this.config.limitBoxOffice);
					cIndex++) {
				var cbo = boData[cIndex];
				var boRowTR = document.createElement("tr");
				boRowTR.className = movieSize;
				var boRowMeter = document.createElement("td");
				boRowMeter.innerHTML = this.cleanScore(cbo.meter) + "  ";
				boRowTR.appendChild(boRowMeter);
				var boRowTitle = document.createElement("td");
				boRowTitle.innerHTML = cbo.title + "  ";
				boRowTitle.align = 'left';
				boRowTR.appendChild(boRowTitle);
				var boRowGross = document.createElement("td");
				boRowGross.innerHTML = "  " + cbo.gross;
				boRowTR.appendChild(boRowGross);
				boRows.push(boRowTR);
			}
		}
		// build the table
        var allRows = [ ];
        // if set to show box office first, do that
		if (!this.config.boxOfficeAfter)
			allRows = allRows.concat(boRows);
        // add opening this week and coming soon rows
		allRows = allRows.concat(allOTWandCSRows);
        // if set to show box office after, do that
		if (this.config.boxOfficeAfter)
			allRows = allRows.concat(boRows);
        // add all rows to the return table wrapper
		for (var cIndex = 0; cIndex < allRows.length; cIndex++) {
			wrapper.appendChild(allRows[cIndex]);
		}
        // return table wrapper
		return wrapper;
	}
});

// ------------ end -------------
<|endoftext|>


<commit_before>Module.register("MMM-cryptocurrency", {
    result: {},
    defaults: {
        currency: ['bitcoin'],
        conversion: 'USD',
        displayLongNames: false,
        headers: ['change1h', 'change24h', 'change7d']
    },

    start: function () {
        this.getTicker();
        this.scheduleUpdate();
    },

    getStyles: function () {
        return ["MMM-cryptocurrency.css"];
    },

    getTicker: function () {
        var conversion = this.config.conversion;

        // increase the limit at the end to choose from more currencies
        var url = 'https://api.coinmarketcap.com/v1/ticker/?convert='+conversion+'&limit=10';
        this.sendSocketNotification('get_ticker', url);
    },

    scheduleUpdate: function () {
        var self = this;
        // Refresh time should not be less than 5 minutes
        var delay = 300000;
        setInterval(function () {
            self.getTicker();
        }, delay);
    },

    getDom: function () {
        var data = this.result;

        var wrapper = document.createElement("table");
        wrapper.className = 'small mmm-cryptocurrency';

        var tableHeader = document.createElement("tr");
        tableHeader.className = 'header-row';

        var tableHeaderValues = [
            this.translate("CURRENCY"),
            this.translate('PRICE')
        ];
        this.config.headers.includes('change1h') && tableHeaderValues.push(this.translate('CHANGE') + ' (1h)');
        this.config.headers.includes('change24h') && tableHeaderValues.push(this.translate('CHANGE') + ' (24h)');
        this.config.headers.includes('change7d') && tableHeaderValues.push(this.translate('CHANGE') + ' (7d)');

        for (var i = 0; i < tableHeaderValues.length; i++) {
            var tableHeadSetup = document.createElement("th");
            tableHeadSetup.innerHTML = tableHeaderValues[i];
            tableHeader.appendChild(tableHeadSetup);
        }
        wrapper.appendChild(tableHeader);

        for (var i = 0; i < data.length; i++) {
            var currentCurrency = data[i];
            var trWrapper = document.createElement("tr");
            trWrapper.className = 'currency';

            if (this.config.displayLongNames) {
                var name = currentCurrency.name;
            } else {
                name = currentCurrency.symbol;
            }
            // Build object key to get proper rounding
            var rightCurrencyFormat = this.config.conversion.toLowerCase();
            // Round the price and adds the currency string
            var formattedPrice = Math.round(currentCurrency['price_'+rightCurrencyFormat] * 100) / 100+' '+this.config.conversion;

            // Build optional headers using lodash
            var tdValues = [
                name,
                formattedPrice
            ];
            this.config.headers.includes('change24h') && tdValues.push(currentCurrency.percent_change_1h + '%');
            this.config.headers.includes('change24h') && tdValues.push(currentCurrency.percent_change_24h + '%');
            this.config.headers.includes('change24h') && tdValues.push(currentCurrency.percent_change_7d + '%');

            for (var j = 0; j < tdValues.length; j++) {
                var tdWrapper = document.createElement("td");
                tdWrapper.innerHTML = tdValues[j];
                trWrapper.appendChild(tdWrapper);
            }
            wrapper.appendChild(trWrapper);
        }
        return wrapper;
    },

    socketNotificationReceived: function (notification, payload) {
        if (notification === "got_result") {
            this.result = this.getWantedCurrencies(this.config.currency, payload);
            this.updateDom();
        }
    },

    /**
     * Returns configured currencies
     *
     * @param chosenCurrencies
     * @param apiResult
     * @returns {Array}
     */
    getWantedCurrencies: function (chosenCurrencies, apiResult) {
        var filteredCurrencies = [];
        for (var i = 0; i < chosenCurrencies.length; i++){
            for (var j = 0; j < apiResult.length; j++){
                var userCurrency = chosenCurrencies[i];
                var remoteCurrency = apiResult[j];
                if (userCurrency == remoteCurrency.id) {
                    filteredCurrencies.push(remoteCurrency);
                }
            }
        }
        return filteredCurrencies;
    },

    /**
     * Load translations files
     * @returns {{en: string, de: string, it: string}}
     */
    getTranslations: function () {
        return {
            en: "translations/en.json",
            de: "translations/de.json",
            it: "translations/it.json"
        };
    },

});
<commit_msg>Fixed bug of multiple headers<commit_after>Module.register("MMM-cryptocurrency", {
    result: {},
    defaults: {
        currency: ['bitcoin'],
        conversion: 'USD',
        displayLongNames: false,
        headers: ['change1h', 'change24h', 'change7d']
    },

    start: function () {
        this.getTicker();
        this.scheduleUpdate();
    },

    getStyles: function () {
        return ["MMM-cryptocurrency.css"];
    },

    getTicker: function () {
        var conversion = this.config.conversion;

        // increase the limit at the end to choose from more currencies
        var url = 'https://api.coinmarketcap.com/v1/ticker/?convert='+conversion+'&limit=10';
        this.sendSocketNotification('get_ticker', url);
    },

    scheduleUpdate: function () {
        var self = this;
        // Refresh time should not be less than 5 minutes
        var delay = 300000;
        setInterval(function () {
            self.getTicker();
        }, delay);
    },

    getDom: function () {
        var data = this.result;

        var wrapper = document.createElement("table");
        wrapper.className = 'small mmm-cryptocurrency';

        var tableHeader = document.createElement("tr");
        tableHeader.className = 'header-row';

        var tableHeaderValues = [
            this.translate("CURRENCY"),
            this.translate('PRICE')
        ];
        this.config.headers.includes('change1h') && tableHeaderValues.push(this.translate('CHANGE') + ' (1h)');
        this.config.headers.includes('change24h') && tableHeaderValues.push(this.translate('CHANGE') + ' (24h)');
        this.config.headers.includes('change7d') && tableHeaderValues.push(this.translate('CHANGE') + ' (7d)');

        for (var i = 0; i < tableHeaderValues.length; i++) {
            var tableHeadSetup = document.createElement("th");
            tableHeadSetup.innerHTML = tableHeaderValues[i];
            tableHeader.appendChild(tableHeadSetup);
        }
        wrapper.appendChild(tableHeader);

        for (var i = 0; i < data.length; i++) {
            var currentCurrency = data[i];
            var trWrapper = document.createElement("tr");
            trWrapper.className = 'currency';

            if (this.config.displayLongNames) {
                var name = currentCurrency.name;
            } else {
                name = currentCurrency.symbol;
            }
            // Build object key to get proper rounding
            var rightCurrencyFormat = this.config.conversion.toLowerCase();
            // Round the price and adds the currency string
            var formattedPrice = Math.round(currentCurrency['price_'+rightCurrencyFormat] * 100) / 100+' '+this.config.conversion;

            // Build optional headers using lodash
            var tdValues = [
                name,
                formattedPrice
            ];
            this.config.headers.includes('change1h') && tdValues.push(currentCurrency.percent_change_1h + '%');
            this.config.headers.includes('change24h') && tdValues.push(currentCurrency.percent_change_24h + '%');
            this.config.headers.includes('change7d') && tdValues.push(currentCurrency.percent_change_7d + '%');

            for (var j = 0; j < tdValues.length; j++) {
                var tdWrapper = document.createElement("td");
                tdWrapper.innerHTML = tdValues[j];
                trWrapper.appendChild(tdWrapper);
            }
            wrapper.appendChild(trWrapper);
        }
        return wrapper;
    },

    socketNotificationReceived: function (notification, payload) {
        if (notification === "got_result") {
            this.result = this.getWantedCurrencies(this.config.currency, payload);
            this.updateDom();
        }
    },

    /**
     * Returns configured currencies
     *
     * @param chosenCurrencies
     * @param apiResult
     * @returns {Array}
     */
    getWantedCurrencies: function (chosenCurrencies, apiResult) {
        var filteredCurrencies = [];
        for (var i = 0; i < chosenCurrencies.length; i++){
            for (var j = 0; j < apiResult.length; j++){
                var userCurrency = chosenCurrencies[i];
                var remoteCurrency = apiResult[j];
                if (userCurrency == remoteCurrency.id) {
                    filteredCurrencies.push(remoteCurrency);
                }
            }
        }
        return filteredCurrencies;
    },

    /**
     * Load translations files
     * @returns {{en: string, de: string, it: string}}
     */
    getTranslations: function () {
        return {
            en: "translations/en.json",
            de: "translations/de.json",
            it: "translations/it.json"
        };
    },

});
<|endoftext|>


<commit_before>//
//  Place.js
//  -----------
//  Written by AppleBetas and nullpixel. Inspired by Reddit's /r/place.
//

const size = 1000;

var createCanvasController = function(canvas) {
    let ctx = canvas.getContext("2d");
    // Disable image smoothing
    ctx.mozImageSmoothingEnabled = false;
    ctx.webkitImageSmoothingEnabled = false;
    ctx.msImageSmoothingEnabled = false;
    ctx.imageSmoothingEnabled = false;
    // Buffer
    return {
        canvas: canvas,
        ctx: ctx,
        isDisplayDirty: false,

        clearCanvas: function() {
            this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
            this.isDisplayDirty = true;
        },

        drawImage: function(image) {
            this.ctx.drawImage(image, 0, 0, size, size);
            this.isDisplayDirty = true;
        }
    }
}

var place = {
    zoomedIn: false,
    zoomButton: null,
    dragStart: null, isMouseDown: false, panX: 0, panY: 0,

    start: function(canvas, zoomController, cameraController, displayCanvas) {
        this.canvas = canvas;
        this.canvasController = createCanvasController(canvas);
        this.displayCanvas = displayCanvas;
        this.setupDisplayCanvas(this.displayCanvas);

        zoomController.onmousedown = (event) => this.handleMouseDown(event || window.event);
        zoomController.onmouseup = (event) => this.handleMouseUp(event || window.event);
        zoomController.onmousemove = (event) => { if (this.isMouseDown) this.handleMouseDrag(event || window.event); }

        window.onresize = () => this.handleResize();

        this.zoomController = zoomController;
        this.cameraController = cameraController;

        this.loadImage().then((image) => {
            this.canvasController.clearCanvas();
            this.canvasController.drawImage(image);
            this.updateDisplayCanvas();
            this.displayCtx.imageSmoothingEnabled = false;
        });
    },

    handleResize: function() {
        this.displayCanvas.height = window.innerHeight;
        this.displayCanvas.width = window.innerWidth;
        this.displayCtx.mozImageSmoothingEnabled = false;
        this.displayCtx.webkitImageSmoothingEnabled = false;
        this.displayCtx.msImageSmoothingEnabled = false;
        this.displayCtx.imageSmoothingEnabled = false;
        this.updateDisplayCanvas();
    },

    setupDisplayCanvas: function(canvas) {
        this.displayCtx = canvas.getContext("2d");
        console.log("SETUP!!!!");
        this.handleResize();
        this.updateDisplayCanvas();
    },

    updateDisplayCanvas: function() {
        let dcanvas = this.displayCanvas;
        this.displayCtx.clearRect(0, 0, dcanvas.width, dcanvas.height);
        let zoom = this._getZoomMultiplier();
        let mod = size / 2;
        this.displayCtx.drawImage(this.canvas, dcanvas.width / 2 + (this.panX - mod - 0.5) * zoom, dcanvas.height / 2 + (this.panY - mod - 0.5) * zoom, this.canvas.width * zoom, this.canvas.height * zoom);
    },

    _getZoomMultiplier: function() {
        return this.zoomedIn ? 40 : 4;
    },

    loadImage: function() {
        return new Promise((resolve, reject) => {
            image = new Image();
            image.src = "/api/board-image";
            image.onload = () => {
                resolve(image);
            };
        });
    },

    setZoomedIn: function(zoomedIn) {
        this.zoomedIn = zoomedIn;
        if(zoomedIn) $(this.zoomController).addClass("zoomed");
        else $(this.zoomController).removeClass("zoomed");
    },

    toggleZoom: function() {
        this.setZoomedIn(!this.zoomedIn);
        this._adjustZoomButtonText();
    },

    _adjustZoomButtonText: function() {
        if(this.zoomButton) $(this.zoomButton).text(this.zoomedIn ? "Zoom Out" : "Zoom In")
    },

    setZoomButton: function(btn) {
        this.zoomButton = btn;
        this._adjustZoomButtonText();
        var a = this;
        $(btn).click(function() {
            a.toggleZoom();
        });
    },

    moveCamera: function(deltaX, deltaY, animated) {
        if(typeof animated === 'undefined') animated = false;
        let cam = $(this.cameraController);
        let zoomModifier = this._getZoomMultiplier();
        let x = deltaX / zoomModifier, y = deltaY / zoomModifier;
        cam.css({
            top: `+=${y}px`,
            left: `+=${x}px`
        });
        this.panX += x;
        this.panY += y;
        this.updateDisplayCanvas();
    },

    handleMouseDown: function(event) {
        this.isMouseDown = true;
        $(this.zoomController).addClass("grabbing");
        this.dragStart = {x: event.pageX, y: event.pageY};
    },

    handleMouseDrag: function(event) {
        if(this.dragStart) this.moveCamera(event.pageX - this.dragStart.x, event.pageY - this.dragStart.y);
        this.dragStart = {x: event.pageX, y: event.pageY};
    },

    handleMouseUp: function(event) {
        this.isMouseDown = false;
        $(this.zoomController).removeClass("grabbing");
        dragStart = null;
    }
}

place.start($("canvas#place-canvas-draw")[0], $("#zoom-controller")[0], $("#camera-controller")[0], $("canvas#place-canvas")[0]);
place.setZoomButton($("#zoom-button")[0]);

function move(x, y) {
    place.moveCamera(x, y, false);
}<commit_msg>Kinda zoom support<commit_after>//
//  Place.js
//  -----------
//  Written by AppleBetas and nullpixel. Inspired by Reddit's /r/place.
//

const size = 1000;

var createCanvasController = function(canvas) {
    let ctx = canvas.getContext("2d");
    // Disable image smoothing
    ctx.mozImageSmoothingEnabled = false;
    ctx.webkitImageSmoothingEnabled = false;
    ctx.msImageSmoothingEnabled = false;
    ctx.imageSmoothingEnabled = false;
    // Buffer
    return {
        canvas: canvas,
        ctx: ctx,
        isDisplayDirty: false,

        clearCanvas: function() {
            this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
            this.isDisplayDirty = true;
        },

        drawImage: function(image) {
            this.ctx.drawImage(image, 0, 0, size, size);
            this.isDisplayDirty = true;
        }
    }
}

var place = {
    zoomedIn: false,
    zoomButton: null,
    dragStart: null, isMouseDown: false, panX: 0, panY: 0,

    start: function(canvas, zoomController, cameraController, displayCanvas) {
        this.canvas = canvas;
        this.canvasController = createCanvasController(canvas);
        this.displayCanvas = displayCanvas;
        this.setupDisplayCanvas(this.displayCanvas);

        zoomController.onmousedown = (event) => this.handleMouseDown(event || window.event);
        zoomController.onmouseup = (event) => this.handleMouseUp(event || window.event);
        zoomController.onmousemove = (event) => { if (this.isMouseDown) this.handleMouseDrag(event || window.event); }

        window.onresize = () => this.handleResize();

        this.zoomController = zoomController;
        this.cameraController = cameraController;

        this.loadImage().then((image) => {
            this.canvasController.clearCanvas();
            this.canvasController.drawImage(image);
            this.updateDisplayCanvas();
            this.displayCtx.imageSmoothingEnabled = false;
        });
    },

    handleResize: function() {
        this.displayCanvas.height = window.innerHeight;
        this.displayCanvas.width = window.innerWidth;
        this.displayCtx.mozImageSmoothingEnabled = false;
        this.displayCtx.webkitImageSmoothingEnabled = false;
        this.displayCtx.msImageSmoothingEnabled = false;
        this.displayCtx.imageSmoothingEnabled = false;
        this.updateDisplayCanvas();
    },

    setupDisplayCanvas: function(canvas) {
        this.displayCtx = canvas.getContext("2d");
        console.log("SETUP!!!!");
        this.handleResize();
        this.updateDisplayCanvas();
    },

    updateDisplayCanvas: function() {
        let dcanvas = this.displayCanvas;
        this.displayCtx.clearRect(0, 0, dcanvas.width, dcanvas.height);
        let zoom = this._getZoomMultiplier();
        let mod = size / 2;
        this.displayCtx.drawImage(this.canvas, dcanvas.width / 2 + (this.panX - mod - 0.5) * zoom, dcanvas.height / 2 + (this.panY - mod - 0.5) * zoom, this.canvas.width * zoom, this.canvas.height * zoom);
    },

    _getZoomMultiplier: function() {
        return this.zoomedIn ? 40 : 4;
    },

    loadImage: function() {
        return new Promise((resolve, reject) => {
            image = new Image();
            image.src = "/api/board-image";
            image.onload = () => {
                resolve(image);
            };
        });
    },

    setZoomedIn: function(zoomedIn) {
        this.zoomedIn = zoomedIn;
        if(zoomedIn) $(this.zoomController).addClass("zoomed");
        else $(this.zoomController).removeClass("zoomed");
        this.updateDisplayCanvas();
    },

    toggleZoom: function() {
        this.setZoomedIn(!this.zoomedIn);
        this._adjustZoomButtonText();
    },

    _adjustZoomButtonText: function() {
        if(this.zoomButton) $(this.zoomButton).text(this.zoomedIn ? "Zoom Out" : "Zoom In")
    },

    setZoomButton: function(btn) {
        this.zoomButton = btn;
        this._adjustZoomButtonText();
        var a = this;
        $(btn).click(function() {
            a.toggleZoom();
        });
    },

    moveCamera: function(deltaX, deltaY, animated) {
        if(typeof animated === 'undefined') animated = false;
        let cam = $(this.cameraController);
        let zoomModifier = this._getZoomMultiplier();
        let x = deltaX / zoomModifier, y = deltaY / zoomModifier;
        cam.css({
            top: `+=${y}px`,
            left: `+=${x}px`
        });
        this.panX += x;
        this.panY += y;
        this.updateDisplayCanvas();
    },

    handleMouseDown: function(event) {
        this.isMouseDown = true;
        $(this.zoomController).addClass("grabbing");
        this.dragStart = {x: event.pageX, y: event.pageY};
    },

    handleMouseDrag: function(event) {
        if(this.dragStart) this.moveCamera(event.pageX - this.dragStart.x, event.pageY - this.dragStart.y);
        this.dragStart = {x: event.pageX, y: event.pageY};
    },

    handleMouseUp: function(event) {
        this.isMouseDown = false;
        $(this.zoomController).removeClass("grabbing");
        dragStart = null;
    }
}

place.start($("canvas#place-canvas-draw")[0], $("#zoom-controller")[0], $("#camera-controller")[0], $("canvas#place-canvas")[0]);
place.setZoomButton($("#zoom-button")[0]);

function move(x, y) {
    place.moveCamera(x, y, false);
}<|endoftext|>


<commit_before>var connect = require('connect'),
    parse = require('url').parse,
    querystring = require('querystring').parse,
    sessions = { run: {}, log: {} },
    eventid = 0,
    uuid = require('node-uuid');

function remoteServer(app) {
  app.get('/remote/:id?', function (req, res, next) {
    var url = parse(req.url),
        query = querystring(url.query);

    // save a new session id - maybe give it a token back?
    // serve up some JavaScript
    var id = req.params.id || uuid();
    res.writeHead(200, {'Content-Type': 'text/javascript'});
    res.end((query.callback || 'callback') + '("' + id + '");');
  });

  app.get('/remote/:id/log', function (req, res) {
    var id = req.params.id;
    res.writeHead(200, {'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache'});
    res.write('eventId:0\n\n');

    sessions.log[id] = res;
    sessions.log[id].xhr = req.headers['x-requested-with'] == 'XMLHttpRequest';
  });

  app.post('/remote/:id/log', function (req, res) {
    // post made to send log to jsconsole
    var id = req.params.id;
    // passed over to Server Sent Events on jsconsole.com
    if (sessions.log[id]) {
      sessions.log[id].write('data: ' + req.body.data + '\neventId:' + (++eventid) + '\n\n');

      if (sessions.log[id].xhr) {
        sessions.log[id].end(); // lets older browsers finish their xhr request
      }
    }

    res.writeHead(200, { 'Content-Type' : 'text/plain' });
    res.end();
  });

  app.get('/remote/:id/run', function (req, res) {
    var id = req.params.id;
    res.writeHead(200, {'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache'});
    res.write('eventId:0\n\n');
    sessions.run[id] = res;
    sessions.run[id].xhr = req.headers['x-requested-with'] == 'XMLHttpRequest';
  });

  app.post('/remote/:id/run', function (req, res) {
    var id = req.params.id;

    if (sessions.run[id]) {
      sessions.run[id].write('data: ' + req.body.data + '\neventId:' + (++eventid) + '\n\n');

      if (sessions.run[id].xhr) {
        sessions.run[id].end(); // lets older browsers finish their xhr request
      }
    }
    res.writeHead(200, { 'Content-Type' : 'text/plain' });
    res.end();
  });
}

var server = connect.createServer(
  connect.bodyDecoder(),
  connect.logger(),
  connect.staticProvider(__dirname),
  connect.router(remoteServer)
);

console.log('Listening on ' + (process.ARGV[2] || 80));
server.listen(parseInt(process.ARGV[2]) || 80);
<commit_msg>moved to latest Connect<commit_after>var connect = require('connect'),
    parse = require('url').parse,
    querystring = require('querystring').parse,
    sessions = { run: {}, log: {} },
    eventid = 0,
    uuid = require('node-uuid');

function remoteServer(app) {
  app.get('/remote/:id?', function (req, res, next) {
    var url = parse(req.url),
        query = querystring(url.query);

    // save a new session id - maybe give it a token back?
    // serve up some JavaScript
    var id = req.params.id || uuid();
    res.writeHead(200, {'Content-Type': 'text/javascript'});
    res.end((query.callback || 'callback') + '("' + id + '");');
  });

  app.get('/remote/:id/log', function (req, res) {
    var id = req.params.id;
    res.writeHead(200, {'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache'});
    res.write('eventId:0\n\n');

    sessions.log[id] = res;
    sessions.log[id].xhr = req.headers['x-requested-with'] == 'XMLHttpRequest';
  });

  app.post('/remote/:id/log', function (req, res) {
    // post made to send log to jsconsole
    var id = req.params.id;
    // passed over to Server Sent Events on jsconsole.com
    if (sessions.log[id]) {
      sessions.log[id].write('data: ' + req.body.data + '\neventId:' + (++eventid) + '\n\n');

      if (sessions.log[id].xhr) {
        sessions.log[id].end(); // lets older browsers finish their xhr request
      }
    }

    res.writeHead(200, { 'Content-Type' : 'text/plain' });
    res.end();
  });

  app.get('/remote/:id/run', function (req, res) {
    var id = req.params.id;
    res.writeHead(200, {'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache'});
    res.write('eventId:0\n\n');
    sessions.run[id] = res;
    sessions.run[id].xhr = req.headers['x-requested-with'] == 'XMLHttpRequest';
  });

  app.post('/remote/:id/run', function (req, res) {
    var id = req.params.id;

    if (sessions.run[id]) {
      sessions.run[id].write('data: ' + req.body.data + '\neventId:' + (++eventid) + '\n\n');

      if (sessions.run[id].xhr) {
        sessions.run[id].end(); // lets older browsers finish their xhr request
      }
    }
    res.writeHead(200, { 'Content-Type' : 'text/plain' });
    res.end();
  });
}

var server = connect.createServer(
  connect.bodyParser(),
  connect.logger(),
  connect.static(__dirname),
  connect.router(remoteServer)
);

console.log('Listening on ' + (process.ARGV[2] || 80));
server.listen(parseInt(process.ARGV[2]) || 80);
<|endoftext|>


<commit_before>'use strict';
var turf = require('turf');
var users = require('./users.json');

// Filter features touched by list of users defined by users.json
module.exports = function(tileLayers, tile, writeData, done) {
  var layer = tileLayers.osm.osm;
  var result = layer.features.filter(function(val) {
    return users.hasOwnProperty(val.properties._user);
  });

  if (result.length > 0) {
    var fc = turf.featurecollection(result);
    writeData(JSON.stringify(fc) + '\n');
  }

  done(null, null);
};
<commit_msg>update filterUsers<commit_after>'use strict';
var turf = require('turf');
var users = require('./users.json');

// Filter features touched by list of users defined by users.json
module.exports = function(tileLayers, tile, writeData, done) {
  var layer = tileLayers.osm.osm;
  var result = layer.features.filter(function(val) {
    return users.hasOwnProperty(val.properties['@user']);
  });

  if (result.length > 0) {
    var fc = turf.featurecollection(result);
    writeData(JSON.stringify(fc) + '\n');
  }

  done(null, null);
};
<|endoftext|>


<commit_before>window.prefs = {};

window.prefs.openModal = function(feature) {
	$("#prefs-feature-name").text(window.utils.getLongNameForFeature(feature));
	$(".prefs-modal-body").addClass("hidden");
	$("#prefs-" + feature + "-body").removeClass("hidden");
	$("#prefs-modal").attr("data-feature", feature);
	$("#prefs-modal").modal({
		backdrop: "static",
		keyboard: false
	});
};

window.prefs.get = function(name, callback) {
	window.api.get("prefs/get/" + name, function(data) {
		callback(data.val);
	});
};

window.prefs.checkToggle = function($checkbox, prefsId) {
	window.prefs.get(prefsId, function(result) {
		var toggled = false;
		if (parseInt(result) == 1) {
			toggled = true;
		}
		$checkbox.prop("checked", toggled);
		$checkbox.change(function() {
			var prefsVal = ($(this).prop("checked") ? "1" : "0");
			window.api.post("prefs/set", { name: prefsId, value: prefsVal}, function() {});
		});
	});
};
window.prefs.getJSONPref = function(name, callback) {
    window.prefs.get(name, function(val) {
    	if (val === undefined) {
    		callback(undefined);
    	}
        callback(JSON.parse(val));
    });
};
$(document).ready(function() {
	$("#prefs-done").click(function() {
		$("#prefs-modal").modal("hide");
		setPage($("#prefs-modal").attr("data-feature"));
	});
	$("#usr-btn").click(function() {
		var usrname = $("#usr-name").val();
		window.api.post("prefs/setName", { name: usrname }, function() {
			window.page.showLoading();
			window.location.reload();
		});
	});
	window.prefs.getJSONPref("titleOrder", function(val) {
		if(val == undefined) {
			window.api.post("prefs/set", {name: "titleOrder", value:'["HW","Read","Reading","Project","Report","Essay","Paper","Quiz","Test","Final","Exam","Midterm","Lab","Study","DocID","None","NoHW","subjectName"]'}, function() {})
			val = ["HW","Read","Reading","Project","Report","Essay","Paper","Quiz","Test","Final","Exam","Midterm","Lab","Study","DocID","None","NoHW","subjectName"]
		}
		for(title in val) {
			var $titleLi = $('

');
				var $titleWord = $('');
					$titleWord.text(val[title]);
				$titleLi.append($titleWord);
				$titleLi.attr("id", val[title]);
			$("#title-sorting").append($titleLi);
		};
		$("#title-sorting").sortable({
			change: function (event, ui) {
				var setList = JSON.stringify($("#title-sorting").sortable("toArray"));
				window.api.post("prefs/set", {name: "titleOrder", value:setList}, function() {});
			}
		});
		$("#title-sorting").disableSelection();
	});
	// Homework View
	window.prefs.checkToggle($("#prefs-hwView-swap"), "name-subj");
	window.prefs.checkToggle($("#prefs-hwView-color"), "titleclr");
	window.prefs.checkToggle($("#prefs-hwView-checkboxes"), "hwView-checkboxes");
});
<commit_msg>Update prefs.js<commit_after>window.prefs = {};

window.prefs.openModal = function(feature) {
	$("#prefs-feature-name").text(window.utils.getLongNameForFeature(feature));
	$(".prefs-modal-body").addClass("hidden");
	$("#prefs-" + feature + "-body").removeClass("hidden");
	$("#prefs-modal").attr("data-feature", feature);
	$("#prefs-modal").modal({
		backdrop: "static",
		keyboard: false
	});
};

window.prefs.get = function(name, callback) {
	window.api.get("prefs/get/" + name, function(data) {
		callback(data.val);
	});
};

window.prefs.checkToggle = function($checkbox, prefsId) {
	window.prefs.get(prefsId, function(result) {
		var toggled = false;
		if (parseInt(result) == 1) {
			toggled = true;
		}
		$checkbox.prop("checked", toggled);
		$checkbox.change(function() {
			var prefsVal = ($(this).prop("checked") ? "1" : "0");
			window.api.post("prefs/set", { name: prefsId, value: prefsVal}, function() {});
		});
	});
};
window.prefs.getJSONPref = function(name, callback) {
    window.prefs.get(name, function(val) {
    	if (val === undefined) {
    		callback(undefined);
    	}
        callback(JSON.parse(val));
    });
};
$(document).ready(function() {
	$("#prefs-done").click(function() {
		$("#prefs-modal").modal("hide");
		setPage($("#prefs-modal").attr("data-feature"));
	});
	$("#usr-btn").click(function() {
		var usrname = $("#usr-name").val();
		window.api.post("prefs/setName", { name: usrname }, function() {
			window.page.showLoading();
			window.location.reload();
		});
	});
	window.prefs.getJSONPref("titleOrder", function(val) {
		if(val == undefined) {
			val = ["HW","Read","Reading","Project","Report","Essay","Paper","Quiz","Test","Final","Exam","Midterm","Lab","Study","DocID","None","NoHW","subjectName"]
		}
		for(title in val) {
			var $titleLi = $('

');
				$titleLi.attr("id", val[title]);
				var $titleWord = $('');
					$titleWord.text(val[title]);
				$titleLi.append($titleWord);
			$("#title-sorting").append($titleLi);
		};
		$("#title-sorting").sortable({
			change: function (event, ui) {
				var setList = JSON.stringify($("#title-sorting").sortable("toArray"));
				window.api.post("prefs/set", {name: "titleOrder", value:setList}, function() {});
			}
		});
		$("#title-sorting").disableSelection();
	});
	// Homework View
	window.prefs.checkToggle($("#prefs-hwView-swap"), "name-subj");
	window.prefs.checkToggle($("#prefs-hwView-color"), "titleclr");
	window.prefs.checkToggle($("#prefs-hwView-checkboxes"), "hwView-checkboxes");
});
<|endoftext|>