<commit_before>var util = require('util'),
    twitter = require('twitter');
var twit;
var merge = require('merge');
var fs = require('fs')
var colors = require('colors');

var DUMMY_SEARCH = false;
var DUMMY_TWEET = false;
var WAIT_SECONDS = 120;
var QUERY = "";
var TWEET_CALLBACK = null;
var DB = null;

var COUNTDOWN_OUTPUT = false;

var TWITTER_ACCOUNT_NAME = "weathermcr";

var DEFAULT_STREAM_SLEEP_SECONDS = 5;

var TWEET_TYPE = {
  POLLED: "polled",
  STREAMED: "streamed"
}

// --- SQL ---
var sql = [];
sql['createSeenTweets'] = 'CREATE TABLE IF NOT EXISTS seen_tweets ' +
                          '(id INTEGER PRIMARY KEY, time DATETIME DEFAULT current_timestamp, text TEXT, ' +
                            'user_id INTEGER, username TEXT, action_taken TEXT, streamed BOOLEAN DEFAULT 0, ' +
                            'polled BOOLEAN DEFAULT 0, mention BOOLEAN DEFAULT 0)';
sql['createSentTweets'] = 'CREATE TABLE IF NOT EXISTS sent_tweets ' +
                          '(id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT, related_tweet_id INTEGER)';
sql['createAPILogin'] =   'CREATE TABLE IF NOT EXISTS api_login ' +
                          '(id INTEGER PRIMARY KEY AUTOINCREMENT, access_token_key TEXT, access_token_secret TEXT)';

sql['selectNewestAPI'] =     'SELECT * FROM api_login ORDER BY id DESC LIMIT 1';
sql['selectNewestTweet'] =   'SELECT id FROM seen_tweets WHERE streamed = 0 OR polled = 1 ORDER BY id DESC LIMIT 1';
sql['selectExistingTweet'] = "SELECT 1 as 'exist' FROM seen_tweets WHERE id = $tweet_id LIMIT 1";

sql['updateActionTaken'] = 'UPDATE seen_tweets SET action_taken = $action WHERE id = $id';
sql['updatePolled'] =      'UPDATE seen_tweets SET polled = $polled WHERE id = $id';

sql['insertSeenTweet'] = 'INSERT INTO seen_tweets (id, text, user_id, username, streamed, polled, mention) ' +
                         'VALUES ($id, $text, $user_id, $username, $streamed, $polled, $mention)';
sql['logSentTweet'] =    'INSERT INTO sent_tweets (text, related_tweet_id) VALUES ($text, $related_id)';
sql['insertAPILogin'] =  'INSERT INTO api_login (access_token_key, access_token_secret) VALUES ($key, $secret)';

// --- public
exports.config = function(config, wait_seconds, database) {
    twit = new twitter(config);
    WAIT_SECONDS = wait_seconds;
    DB = database;
}

exports.dummy = function(search, tweets) {
    DUMMY_SEARCH = search;
    DUMMY_TWEET = tweets;
    if(DUMMY_SEARCH)
      console.warn("Using dummy twitter search");
    if(DUMMY_TWEET)
      console.warn("Using dummy twitter tweet");
}

exports.start = function(query, tweet_callback, callback) {
    QUERY = query;
    TWEET_CALLBACK = tweet_callback;

    // Ensure the database is ready for use
    DB.serialize(function() {
      DB.run(sql['createSeenTweets']);
      DB.run(sql['createSentTweets']);
      DB.run(sql['createAPILogin']);
    });

    DB.get(sql['selectNewestAPI'], function(err, result) {

      if(result) {
          console.info("Using stored authentication");
          params = { 'since_id': (result.id - 1500) } // for some reason this needs to be done...

          twit.options.access_token_key = result.access_token_key;
          twit.options.access_token_secret = result.access_token_secret;

          start(callback);
      } else {
          console.info("Authentication is needed before bot can start.");
          requireAuthentication(function() {
              //reusable API access
              var insert = DB.prepare(sql['insertAPILogin']);
              insert.run({ $key: twit.options.access_token_key, $secret: twit.options.access_token_secret });

              start(callback);
          });

      }

    });
}

exports.updateActionTaken = function(tweet, actionTaken) {
    var update = DB.prepare(sql['updateActionTaken']);
    update.bind({$id: tweet.id, $action: actionTaken });
    update.run();
}

exports.sendReply = function(reply_to, message, callback, disable_dummy_tweet) {
    if(typeof disable_dummy_tweet === 'undefined')
        disable_dummy_tweet = false;

    // Must include @ for reply, else twitter rejects it
    if(reply_to.user.screen_name)
      message = "@" + reply_to.user.screen_name + " " + message;

    // Twitter rejects us if we provide an incorrect reply_to.id
    params = {}
    if(reply_to.id)
      params.in_reply_to_status_id = reply_to.id;

    if(message.length > 140)
        console.error("Message over 140 letters: " + message);

    console.info("Attempting to send", message);

    if(DUMMY_TWEET && !disable_dummy_tweet) {
      var dummy = { text: message };
      logSentTweet(dummy, params, false);
      callback(dummy);
    } else {
      twit.updateStatus(message, params, function(reply) {
          if(reply.id) {
            logSentTweet(reply, params);
            callback(reply);
          } else {
            console.error("Error sending tweet", reply);
          }
      });
    }

}

// --- private
function start(callback) {
    updateTweets();
    streamWeatherTweets();
    streamUserReplies();
    callback();
}

function requireAuthentication(callback) {
    var express = require('express');
    var app = express();
    var server;

    app.get('/', twit.gatekeeper('/login'), function(req, res){
      res.send('Authentification successfull');
      console.info("Auth complete, closing authentication server.");
      server.close();
      callback();
    });
    app.get('/twauth', twit.login());

    console.info('Listening on 1200 for auth requests to Twitter');
    server = app.listen(1200);
}

function logSentTweet(tweet, params, real) {
    if(typeof params === 'undefined')
      params = {};

    if(typeof real === 'undefined')
      real = true;

    var insert = DB.prepare(sql['logSentTweet']);
    insert.run({ $text: tweet.text, $related_id: params.in_reply_to_status_id });
    console.info("Tweet sent successfully" + (real ? "" : " (Mock)"));
}

function selectTweets(params, callback) {
    defaultParams = {
        'q': QUERY,
        'lang': 'en',
        'result_type': 'recent', //mixed, recent, popular
        'f': 'realtime',
        'include_entities': 'false',
        'count': 100
    };

    params = merge(defaultParams, params);

    if(DUMMY_SEARCH) callback();
    else twit.get('/search/tweets.json', params, callback);
}

function updateTweets() {
    console.info("Querying twitter for updates using polling");
    DB.get(sql['selectNewestTweet'], function(err, result) {
        if(result) params = { 'since_id': (result.id + 1)  }
        else params = {}

        selectTweets(params, function(data) {
            if(!data || !data.statuses) {
                console.warn("No data received from Twitter from search poll");
                if(data) console.error(data);
            }
            else
            {
                var count = data.statuses.length;
                if(count < 1) {
                    console.warn("No tweets received from Twitter from search poll");
                }

                for(i=0;i<count;i++) {
                    var tweet = data.statuses[i];

                    if(isWeatherTweet(data)) {
                        seenTweet(tweet, TWEET_TYPE.POLLED);
                    } else {
                        fs.appendFile('ignoredTweets.txt', data.user.screen_name
                                       + ":\t"
                                       + data.text.replace(/(\r\n|\n|\r)/gm," \\\ ")
                                       + "\n");
                    }

                }
            }

            updateComplete();
        });
    });
}

// Connect to a twitter stream
function streamTweets(type, params, dataCallback, reconnectSleepSeconds) {
    if(typeof reconnectSleepSeconds === 'undefined')
        reconnectSleepSeconds = DEFAULT_STREAM_SLEEP_SECONDS;

    twit.stream(type, params, function(stream) {
        console.info("Connected to twitter "+type+" stream");

        var resetStream = setTimeout(function() {
            console.info("Restarting "+type+" stream as it has been open for 30 minutes");
            stream.destroy();
        }, 30 * 60 * 1000);

        stream.on('data', function(data) {

            // reset done here as 'connect' fires even on rejection
            reconnectSleepSeconds = DEFAULT_STREAM_SLEEP_SECONDS;

            dataCallback(data);

        });

        stream.on('error', function(error) {
            if(error.errorSource) console.error("Twitter "+type+" stream error:", error);
            else console.error("Other Twitter "+type+" stream error");
        });

        stream.on('end', function(end) {
            console.info("Stream "+type+" ended, will attempt to reconnect in "+reconnectSleepSeconds+" seconds");
            clearTimeout(resetStream);
            setTimeout(function() {
              streamTweets(type, params, dataCallback, reconnectSleepSeconds * 2);
            }, reconnectSleepSeconds * 1000);
        });

    });

}

// Twitter side
var weather_keywords = ['weather', 'forecast', 'sunny', ' rain', 'sunshine', 'thunder', 'lightning',
                        'umbrella', ' snow', 'hail', ' warm', 'cold', 'brolly', 'boiling',
                        '#snow', '#rain', '#hail', '#sun', '#thunder'];

// Local
var ignored_users = ['galgateweather', 'mennewsdesk', 'metoffice', 'chadWeather', 'myweather_man',
                     'uk_storms', 'wx_manchester', 'widdop_weather', 'weather', TWITTER_ACCOUNT_NAME,
                     'LordChapman'];

var ignored_keywords = ['rt @', '[Manchester Weather] Your Weekend Forecast', 'weatherspoons',
                        'manchester, nh', 'train', '@MetOffice', '@coldplay', 'Cold cave'];

function isWeatherTweet(tweet) {
    return arrayInString(tweet.text, weather_keywords, true)
           && !arrayInString(tweet.user.screen_name, ignored_users)
           && !arrayInString(tweet.text, ignored_keywords)
}

function highlightMatches(string, array) {
    text = string;
    for(i=0;i<array.length;i++) {
      text = text.replace(new RegExp('(' + array[i] + ')','ig'), "$1".red);
    }
    return text;
}

// Get tweets about the weather
function streamWeatherTweets() {

    // Bounding boxes do not act as filters for other filter parameters.
    // For example track=twitter&locations=-122.75,36.8,-121.75,37.8 would match
    // any tweets containing the term Twitter (even non-geo tweets) OR coming from the San Francisco area.
    track = []

    for(i=0;i<weather_keywords.length;i++) {
      track.push(weather_keywords[i] + ' manchester');
      track.push(weather_keywords[i] + ' #mcr');
    }

    streamTweets('filter',
        {
            track: track,
            locations: '-2.275,53.45,-2.2,53.5' // bottom right, top left - lon/lat
        },
        function(data) {

            // @todo should probably validate data, twitter might return a none tweet
            // the steam may contain "non weather" tweets we must filter first
            if(isWeatherTweet(data)) {

                textWithMatches = highlightMatches(data.text, weather_keywords);
                console.info("Matched streamed weather tweet @", data.user.screen_name, " ", textWithMatches);
                seenTweet(data, TWEET_TYPE.STREAMED);

            } else {
                fs.appendFile('ignoredTweets.txt', data.user.screen_name
                                                    + ":\t"
                                                    + data.text.replace(/(\r\n|\n|\r)/gm," \\ ")
                                                    + "\n");
            }

        }
    );
}

function streamUserReplies() {
    streamTweets('user',
        {
            replies: 'all', // By default @replies are only sent if the current user follows the sender and receiver
            with: 'users' // When set to "users", only messages targeted directly at a user will be delivered
        },
        function(data) {
            // Upon establishing a User Stream connection Twitter will send
            // a preamble before starting regular message delivery
            if(data.friends) {
                return;
            }

            else if(data.text && data.text.toLowerCase().indexOf(TWITTER_ACCOUNT_NAME) != -1) {
                console.info("Recieved mention in user stream @", data.user.screen_name, data.text);

                // Avoid loops, if we sent the message and it was a reply to US
                if(data.in_reply_to_screen_name
                    && data.in_reply_to_screen_name.toLowerCase() == TWITTER_ACCOUNT_NAME
                    && data.user.screen_name.toLowerCase() == TWITTER_ACCOUNT_NAME) {
                    console.warn('Ignored self reply message');
                    return;
                }

                seenTweet(data, TWEET_TYPE.STREAMED, true);
            }
        }
    );
}

function arrayInString(string, array, ignore_mentions) {
    if(typeof string !== 'string')
        throw new Error('The term to search must be a string');

    if(typeof ignore_mentions === 'undefined')
        ignore_mentions = false;

    for(i=0;i<array.length;i++) {
        if(string.toLowerCase().indexOf(array[i].toLowerCase()) != -1) {
            if(!ignore_mentions) return true;
            else {
              // note double escape and trim due to trailing spaces
              var re = new RegExp('@\\w*'+array[i].trim(),'i');
              if(!re.test(string))
                  return true;
            }
        }
    }

    return false;

}

function seenTweet(tweet, type, mention) {

    if(typeof mention === 'undefined')
        mention = tweet.text.toLowerCase().indexOf(TWITTER_ACCOUNT_NAME) != -1;

    var select = DB.prepare(sql['selectExistingTweet']);
    select.get({$tweet_id: tweet.id}, function(err, row) {
        if(!row) { // If the result set is empty, the second parameter is undefined
            var insert = DB.prepare(sql['insertSeenTweet']);
            insert.run({
              $id: tweet.id,
              $text: tweet.text,
              $user_id: tweet.user.id,
              $username: tweet.user.screen_name,
              $streamed: type == TWEET_TYPE.STREAMED ? 1 : 0,  // 0/1 as SQLlite has no boolean support
              $polled: type == TWEET_TYPE.POLLED ? 1 : 0,
              $mention: mention ? 1 : 0,
            });
            TWEET_CALLBACK(tweet, mention);
            console.info("Logged: ", tweet.text, tweet.created_at);
        } else {
            console.info("Ignored duplicate: ", tweet.text, tweet.created_at);

            // to prevent the non-stream falling behind if the stream is capturing everything
            if(type == TWEET_TYPE.POLLED) {
                var update = DB.prepare(sql['updatePolled']);
                update.run({ $id: tweet.id, $polled: 1 });
            }
        }
    });
}

function updateComplete() {
    console.info("\n"+'Update complete, waiting '+WAIT_SECONDS+' seconds before the next update');
    countdown(WAIT_SECONDS, updateTweets, "seconds until next search...");
}

function countdown(seconds, callback, message) {
    var countdownInterval;
    var countdownSecondsLeft;
    var countdownCallback;

    countdownInterval = setInterval(function(){
        if(COUNTDOWN_OUTPUT) {
            process.stdout.clearLine();  // clear current text
            process.stdout.cursorTo(0);  // move cursor to beginning of line
            process.stdout.write(countdownSecondsLeft-- + " " + message);
        }

        if(countdownSecondsLeft <= 0) {
            clearInterval(countdownInterval);
            process.stdout.write("\n");
            if(countdownCallback) {
                temp = countdownCallback;
                countdownCallback = null;
                temp();
            }
        }
    }, 1000);

    countdownSecondsLeft = seconds;
    if(typeof callback !== 'undefined') {
        countdownCallback = callback;
    }
}<commit_msg>Log tweaks<commit_after>var util = require('util'),
    twitter = require('twitter');
var twit;
var merge = require('merge');
var fs = require('fs')
var colors = require('colors');

var DUMMY_SEARCH = false;
var DUMMY_TWEET = false;
var WAIT_SECONDS = 120;
var QUERY = "";
var TWEET_CALLBACK = null;
var DB = null;

var COUNTDOWN_OUTPUT = false;

var TWITTER_ACCOUNT_NAME = "weathermcr";

var DEFAULT_STREAM_SLEEP_SECONDS = 5;

var TWEET_TYPE = {
  POLLED: "polled",
  STREAMED: "streamed"
}

// --- SQL ---
var sql = [];
sql['createSeenTweets'] = 'CREATE TABLE IF NOT EXISTS seen_tweets ' +
                          '(id INTEGER PRIMARY KEY, time DATETIME DEFAULT current_timestamp, text TEXT, ' +
                            'user_id INTEGER, username TEXT, action_taken TEXT, streamed BOOLEAN DEFAULT 0, ' +
                            'polled BOOLEAN DEFAULT 0, mention BOOLEAN DEFAULT 0)';
sql['createSentTweets'] = 'CREATE TABLE IF NOT EXISTS sent_tweets ' +
                          '(id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT, related_tweet_id INTEGER)';
sql['createAPILogin'] =   'CREATE TABLE IF NOT EXISTS api_login ' +
                          '(id INTEGER PRIMARY KEY AUTOINCREMENT, access_token_key TEXT, access_token_secret TEXT)';

sql['selectNewestAPI'] =     'SELECT * FROM api_login ORDER BY id DESC LIMIT 1';
sql['selectNewestTweet'] =   'SELECT id FROM seen_tweets WHERE streamed = 0 OR polled = 1 ORDER BY id DESC LIMIT 1';
sql['selectExistingTweet'] = "SELECT 1 as 'exist' FROM seen_tweets WHERE id = $tweet_id LIMIT 1";

sql['updateActionTaken'] = 'UPDATE seen_tweets SET action_taken = $action WHERE id = $id';
sql['updatePolled'] =      'UPDATE seen_tweets SET polled = $polled WHERE id = $id';

sql['insertSeenTweet'] = 'INSERT INTO seen_tweets (id, text, user_id, username, streamed, polled, mention) ' +
                         'VALUES ($id, $text, $user_id, $username, $streamed, $polled, $mention)';
sql['logSentTweet'] =    'INSERT INTO sent_tweets (text, related_tweet_id) VALUES ($text, $related_id)';
sql['insertAPILogin'] =  'INSERT INTO api_login (access_token_key, access_token_secret) VALUES ($key, $secret)';

// --- public
exports.config = function(config, wait_seconds, database) {
    twit = new twitter(config);
    WAIT_SECONDS = wait_seconds;
    DB = database;
}

exports.dummy = function(search, tweets) {
    DUMMY_SEARCH = search;
    DUMMY_TWEET = tweets;
    if(DUMMY_SEARCH)
      console.warn("Using dummy twitter search");
    if(DUMMY_TWEET)
      console.warn("Using dummy twitter tweet");
}

exports.start = function(query, tweet_callback, callback) {
    QUERY = query;
    TWEET_CALLBACK = tweet_callback;

    // Ensure the database is ready for use
    DB.serialize(function() {
      DB.run(sql['createSeenTweets']);
      DB.run(sql['createSentTweets']);
      DB.run(sql['createAPILogin']);
    });

    DB.get(sql['selectNewestAPI'], function(err, result) {

      if(result) {
          console.info("Using stored authentication");
          params = { 'since_id': (result.id - 1500) } // for some reason this needs to be done...

          twit.options.access_token_key = result.access_token_key;
          twit.options.access_token_secret = result.access_token_secret;

          start(callback);
      } else {
          console.info("Authentication is needed before bot can start.");
          requireAuthentication(function() {
              //reusable API access
              var insert = DB.prepare(sql['insertAPILogin']);
              insert.run({ $key: twit.options.access_token_key, $secret: twit.options.access_token_secret });

              start(callback);
          });

      }

    });
}

exports.updateActionTaken = function(tweet, actionTaken) {
    var update = DB.prepare(sql['updateActionTaken']);
    update.bind({$id: tweet.id, $action: actionTaken });
    update.run();
}

exports.sendReply = function(reply_to, message, callback, disable_dummy_tweet) {
    if(typeof disable_dummy_tweet === 'undefined')
        disable_dummy_tweet = false;

    // Must include @ for reply, else twitter rejects it
    if(reply_to.user.screen_name)
      message = "@" + reply_to.user.screen_name + " " + message;

    // Twitter rejects us if we provide an incorrect reply_to.id
    params = {}
    if(reply_to.id)
      params.in_reply_to_status_id = reply_to.id;

    if(message.length > 140)
        console.error("Message over 140 letters: " + message);

    console.info("Attempting to send", message);

    if(DUMMY_TWEET && !disable_dummy_tweet) {
      var dummy = { text: message };
      logSentTweet(dummy, params, false);
      callback(dummy);
    } else {
      twit.updateStatus(message, params, function(reply) {
          if(reply.id) {
            logSentTweet(reply, params);
            callback(reply);
          } else {
            console.error("Error sending tweet", reply);
          }
      });
    }

}

// --- private
function start(callback) {
    updateTweets();
    streamWeatherTweets();
    streamUserReplies();
    callback();
}

function requireAuthentication(callback) {
    var express = require('express');
    var app = express();
    var server;

    app.get('/', twit.gatekeeper('/login'), function(req, res){
      res.send('Authentification successfull');
      console.info("Auth complete, closing authentication server.");
      server.close();
      callback();
    });
    app.get('/twauth', twit.login());

    console.info('Listening on 1200 for auth requests to Twitter');
    server = app.listen(1200);
}

function logSentTweet(tweet, params, real) {
    if(typeof params === 'undefined')
      params = {};

    if(typeof real === 'undefined')
      real = true;

    var insert = DB.prepare(sql['logSentTweet']);
    insert.run({ $text: tweet.text, $related_id: params.in_reply_to_status_id });
    console.info("Tweet sent successfully" + (real ? "" : " (Mock)"));
}

function selectTweets(params, callback) {
    defaultParams = {
        'q': QUERY,
        'lang': 'en',
        'result_type': 'recent', //mixed, recent, popular
        'f': 'realtime',
        'include_entities': 'false',
        'count': 100
    };

    params = merge(defaultParams, params);

    if(DUMMY_SEARCH) callback();
    else twit.get('/search/tweets.json', params, callback);
}

function updateTweets() {
    console.info("Querying twitter for updates using polling");
    DB.get(sql['selectNewestTweet'], function(err, result) {
        if(result) params = { 'since_id': (result.id + 1)  }
        else params = {}

        selectTweets(params, function(data) {
            if(!data || !data.statuses) {
                console.warn("No data received from Twitter from search poll");
                if(data) console.error(data, params);
            }
            else
            {
                var count = data.statuses.length;
                if(count < 1) {
                    console.info("No tweets received from Twitter from search poll");
                }

                for(i=0;i<count;i++) {
                    var tweet = data.statuses[i];

                    if(isWeatherTweet(data)) {
                        seenTweet(tweet, TWEET_TYPE.POLLED);
                    } else {
                        fs.appendFile('ignoredTweets.txt', data.user.screen_name
                                       + ":\t"
                                       + data.text.replace(/(\r\n|\n|\r)/gm," \\\ ")
                                       + "\n");
                    }

                }
            }

            updateComplete();
        });
    });
}

// Connect to a twitter stream
function streamTweets(type, params, dataCallback, reconnectSleepSeconds) {
    if(typeof reconnectSleepSeconds === 'undefined')
        reconnectSleepSeconds = DEFAULT_STREAM_SLEEP_SECONDS;

    twit.stream(type, params, function(stream) {
        console.info("Connected to twitter "+type+" stream");

        var resetStream = setTimeout(function() {
            console.info("Restarting "+type+" stream as it has been open for 30 minutes");
            stream.destroy();
        }, 30 * 60 * 1000);

        stream.on('data', function(data) {

            // reset done here as 'connect' fires even on rejection
            reconnectSleepSeconds = DEFAULT_STREAM_SLEEP_SECONDS;

            dataCallback(data);

        });

        stream.on('error', function(error) {
            if(error.errorSource) console.error("Twitter "+type+" stream error:", error);
            else console.error("Other Twitter "+type+" stream error", error);
        });

        stream.on('end', function(end) {
            console.info("Stream "+type+" ended, will attempt to reconnect in "+reconnectSleepSeconds+" seconds");
            clearTimeout(resetStream);
            setTimeout(function() {
              streamTweets(type, params, dataCallback, reconnectSleepSeconds * 2);
            }, reconnectSleepSeconds * 1000);
        });

    });

}

// Twitter side
var weather_keywords = ['weather', 'forecast', 'sunny', ' rain', 'sunshine', 'thunder', 'lightning',
                        'umbrella', ' snow', 'hail', ' warm', 'cold', 'brolly', 'boiling',
                        '#snow', '#rain', '#hail', '#sun', '#thunder'];

// Local
var ignored_users = ['galgateweather', 'mennewsdesk', 'metoffice', 'chadWeather', 'myweather_man',
                     'uk_storms', 'wx_manchester', 'widdop_weather', 'weather', TWITTER_ACCOUNT_NAME,
                     'LordChapman'];

var ignored_keywords = ['rt @', '[Manchester Weather] Your Weekend Forecast', 'weatherspoons',
                        'manchester, nh', 'train', '@MetOffice', '@coldplay', 'Cold cave'];

function isWeatherTweet(tweet) {
    return arrayInString(tweet.text, weather_keywords, true)
           && !arrayInString(tweet.user.screen_name, ignored_users)
           && !arrayInString(tweet.text, ignored_keywords)
}

function highlightMatches(string, array) {
    text = string;
    for(i=0;i<array.length;i++) {
      text = text.replace(new RegExp('(' + array[i] + ')','ig'), "$1".red);
    }
    return text;
}

// Get tweets about the weather
function streamWeatherTweets() {

    // Bounding boxes do not act as filters for other filter parameters.
    // For example track=twitter&locations=-122.75,36.8,-121.75,37.8 would match
    // any tweets containing the term Twitter (even non-geo tweets) OR coming from the San Francisco area.
    track = []

    for(i=0;i<weather_keywords.length;i++) {
      track.push(weather_keywords[i] + ' manchester');
      track.push(weather_keywords[i] + ' #mcr');
    }

    streamTweets('filter',
        {
            track: track,
            locations: '-2.275,53.45,-2.2,53.5' // bottom right, top left - lon/lat
        },
        function(data) {

            // @todo should probably validate data, twitter might return a none tweet
            // the steam may contain "non weather" tweets we must filter first
            if(isWeatherTweet(data)) {

                textWithMatches = highlightMatches(data.text, weather_keywords);
                console.info("Matched streamed weather tweet @", data.user.screen_name, " ", textWithMatches);
                seenTweet(data, TWEET_TYPE.STREAMED);

            } else {
                fs.appendFile('ignoredTweets.txt', data.user.screen_name
                                                    + ":\t"
                                                    + data.text.replace(/(\r\n|\n|\r)/gm," \\ ")
                                                    + "\n");
            }

        }
    );
}

function streamUserReplies() {
    streamTweets('user',
        {
            replies: 'all', // By default @replies are only sent if the current user follows the sender and receiver
            with: 'users' // When set to "users", only messages targeted directly at a user will be delivered
        },
        function(data) {
            // Upon establishing a User Stream connection Twitter will send
            // a preamble before starting regular message delivery
            if(data.friends) {
                return;
            }

            else if(data.text && data.text.toLowerCase().indexOf(TWITTER_ACCOUNT_NAME) != -1) {
                console.info("Recieved mention in user stream @", data.user.screen_name, data.text);

                // Avoid loops, if we sent the message and it was a reply to US
                if(data.in_reply_to_screen_name
                    && data.in_reply_to_screen_name.toLowerCase() == TWITTER_ACCOUNT_NAME
                    && data.user.screen_name.toLowerCase() == TWITTER_ACCOUNT_NAME) {
                    console.warn('Ignored self reply message');
                    return;
                }

                seenTweet(data, TWEET_TYPE.STREAMED, true);
            }
        }
    );
}

function arrayInString(string, array, ignore_mentions) {
    if(typeof string !== 'string')
        throw new Error('The term to search must be a string');

    if(typeof ignore_mentions === 'undefined')
        ignore_mentions = false;

    for(i=0;i<array.length;i++) {
        if(string.toLowerCase().indexOf(array[i].toLowerCase()) != -1) {
            if(!ignore_mentions) return true;
            else {
              // note double escape and trim due to trailing spaces
              var re = new RegExp('@\\w*'+array[i].trim(),'i');
              if(!re.test(string))
                  return true;
            }
        }
    }

    return false;

}

function seenTweet(tweet, type, mention) {

    if(typeof mention === 'undefined')
        mention = tweet.text.toLowerCase().indexOf(TWITTER_ACCOUNT_NAME) != -1;

    var select = DB.prepare(sql['selectExistingTweet']);
    select.get({$tweet_id: tweet.id}, function(err, row) {
        if(!row) { // If the result set is empty, the second parameter is undefined
            var insert = DB.prepare(sql['insertSeenTweet']);
            insert.run({
              $id: tweet.id,
              $text: tweet.text,
              $user_id: tweet.user.id,
              $username: tweet.user.screen_name,
              $streamed: type == TWEET_TYPE.STREAMED ? 1 : 0,  // 0/1 as SQLlite has no boolean support
              $polled: type == TWEET_TYPE.POLLED ? 1 : 0,
              $mention: mention ? 1 : 0,
            });
            TWEET_CALLBACK(tweet, mention);
            console.info("Logged: ", tweet.text, tweet.created_at);
        } else {
            console.info("Ignored duplicate: ", tweet.text, tweet.created_at);

            // to prevent the non-stream falling behind if the stream is capturing everything
            if(type == TWEET_TYPE.POLLED) {
                var update = DB.prepare(sql['updatePolled']);
                update.run({ $id: tweet.id, $polled: 1 });
            }
        }
    });
}

function updateComplete() {
    console.info("\n"+'Update complete, waiting '+WAIT_SECONDS+' seconds before the next update');
    countdown(WAIT_SECONDS, updateTweets, "seconds until next search...");
}

function countdown(seconds, callback, message) {
    var countdownInterval;
    var countdownSecondsLeft;
    var countdownCallback;

    countdownInterval = setInterval(function(){
        if(COUNTDOWN_OUTPUT) {
            process.stdout.clearLine();  // clear current text
            process.stdout.cursorTo(0);  // move cursor to beginning of line
            process.stdout.write(countdownSecondsLeft-- + " " + message);
        }

        if(countdownSecondsLeft <= 0) {
            clearInterval(countdownInterval);
            process.stdout.write("\n");
            if(countdownCallback) {
                temp = countdownCallback;
                countdownCallback = null;
                temp();
            }
        }
    }, 1000);

    countdownSecondsLeft = seconds;
    if(typeof callback !== 'undefined') {
        countdownCallback = callback;
    }
}<|endoftext|>


<commit_before>import React from 'react';
import { UncontrolledTooltip } from 'reactstrap';
import PropTypes from 'prop-types';

const ImportServersBtn = (serversImporter) => class ImportServersBtn extends React.Component {
  static defaultProps = {
    onImport: () => ({}),
  };
  static propTypes = {
    onImport: PropTypes.func,
    createServers: PropTypes.func,
    fileRef: PropTypes.oneOfType([ PropTypes.object, PropTypes.node ]),
  };

  constructor(props) {
    super(props);
    this.fileRef = props.fileRef || React.createRef();
  }

  render() {
    const { importServersFromFile } = serversImporter;
    const { onImport, createServers } = this.props;
    const onChange = ({ target }) =>
      importServersFromFile(target.files[0])
        .then(createServers)
        .then(onImport)
        .then(() => {
          // Reset input after processing file
          target.value = null;
        });

    return (
      <React.Fragment>
        <button
          type="button"
          className="btn btn-outline-secondary mr-2"
          id="importBtn"
          onClick={() => this.fileRef.current.click()}
        >
          Import from file
        </button>
        <UncontrolledTooltip placement="top" target="importBtn">
          You can create servers by importing a CSV file with columns name, apiKey and url
        </UncontrolledTooltip>

        <input
          type="file"
          accept="text/csv"
          className="create-server__csv-select"
          ref={this.fileRef}
          onChange={onChange}
        />
      </React.Fragment>
    );
  }
};

export default ImportServersBtn;
<commit_msg>Updated ImportServersBtn to be a functional component<commit_after>import React, { useRef } from 'react';
import { UncontrolledTooltip } from 'reactstrap';
import PropTypes from 'prop-types';

const propTypes = {
  onImport: PropTypes.func,
  createServers: PropTypes.func,
  fileRef: PropTypes.oneOfType([ PropTypes.object, PropTypes.node ]),
};

// FIXME Replace with typescript: (ServersImporter)
const ImportServersBtn = ({ importServersFromFile }) => {
  const ImportServersBtnComp = ({ createServers, fileRef, onImport = () => {} }) => {
    const ref = fileRef || useRef();
    const onChange = ({ target }) =>
      importServersFromFile(target.files[0])
        .then(createServers)
        .then(onImport)
        .then(() => {
          // Reset input after processing file
          target.value = null;
        });

    return (
      <React.Fragment>
        <button
          type="button"
          className="btn btn-outline-secondary mr-2"
          id="importBtn"
          onClick={() => ref.current.click()}
        >
          Import from file
        </button>
        <UncontrolledTooltip placement="top" target="importBtn">
          You can create servers by importing a CSV file with columns name, apiKey and url.
        </UncontrolledTooltip>

        <input type="file" accept="text/csv" className="create-server__csv-select" ref={ref} onChange={onChange} />
      </React.Fragment>
    );
  };

  ImportServersBtnComp.propTypes = propTypes;

  return ImportServersBtnComp;
};

export default ImportServersBtn;
<|endoftext|>


<commit_before>dc.pieChart = function(parent, chartGroup) {
    var DEFAULT_MIN_ANGLE_FOR_LABEL = 0.5;

    var _sliceCssClass = "pie-slice";

    var _radius = 90, _innerRadius = 0;

    var _g;

    var _minAngleForLabel = DEFAULT_MIN_ANGLE_FOR_LABEL;

    var _chart = dc.singleSelectionChart(dc.colorChart(dc.baseChart({})));

    _chart.label(function(d) {
        return _chart.keyAccessor()(d.data);
    });

    _chart.renderLabel(true);

    _chart.title(function(d) {
        return _chart.keyAccessor()(d.data) + ": " + _chart.valueAccessor()(d.data);
    });

    _chart.transitionDuration(350);

    _chart.doRender = function() {
        _chart.resetSvg();

        _g = _chart.svg()
            .append("g")
            .attr("transform", "translate(" + _chart.cx() + "," + _chart.cy() + ")");

        drawChart();

        return _chart;
    };

    function drawChart() {
        if (_chart.dataSet()) {
            var pie = calculateDataPie();

            var arc = _chart.buildArcs();

            var pieData = pie(_chart.orderedGroup().top(Infinity));

            var slices = _g.selectAll("g." + _sliceCssClass)
                .data(pieData);

            createElements(slices, arc, pieData);

            updateElements(pieData, arc);

            removeElements(slices);

            highlightFilter();
        }
    }

    function createElements(slices, arc, pieData) {
        var slicesEnter = createSliceNodes(slices);

        createSlicePath(slicesEnter, arc);

        createTitles(slicesEnter);

        createLabels(pieData, arc);
    }

    function createSliceNodes(slices) {
        var slicesEnter = slices
            .enter()
            .append("g")
            .attr("class", function(d, i) {
                return _sliceCssClass + " _" + i;
            });
        return slicesEnter;
    }

    function createSlicePath(slicesEnter, arc) {
        var slicePath = slicesEnter.append("path")
            .attr("fill", function(d, i) {
                return _chart.getColor(d, i);
            })
            .on("click", onClick)
            .attr("d", function(d, i) {
                return safeArc(d, i, arc);
            });
        slicePath.transition()
            .duration(_chart.transitionDuration())
            .attrTween("d", tweenPie);
    }

    function createTitles(slicesEnter) {
        if (_chart.renderTitle()) {
            slicesEnter.append("title").text(function(d) {
                return _chart.title()(d);
            });
        }
    }

    function createLabels(pieData, arc) {
        if (_chart.renderLabel()) {
            var labels = _g.selectAll("text." + _sliceCssClass)
                .data(pieData);

            var labelsEnter = labels
                .enter()
                .append("text")
                .attr("class", function(d, i) {
                    return _sliceCssClass + " _" + i;
                })
                .on("click", onClick);
            dc.transition(labelsEnter, _chart.transitionDuration())
                .attr("transform", function(d) {
                    d.innerRadius = _chart.innerRadius();
                    d.outerRadius = _radius;
                    var centroid = arc.centroid(d);
                    if (isNaN(centroid[0]) || isNaN(centroid[1])) {
                        return "translate(0,0)";
                    } else {
                        return "translate(" + centroid + ")";
                    }
                })
                .attr("text-anchor", "middle")
                .text(function(d) {
                    var data = d.data;
                    if (sliceHasNoData(data) || sliceTooSmall(d))
                        return "";
                    return _chart.label()(d);
                });
        }
    }

    function updateElements(pieData, arc) {
        updateSlicePaths(pieData, arc);
        updateLabels(pieData, arc);
        updateTitles(pieData);
    }

    function updateSlicePaths(pieData, arc) {
        var slicePaths = _g.selectAll("g." + _sliceCssClass)
            .data(pieData)
            .select("path")
            .attr("d", function(d, i) {
                return safeArc(d, i, arc);
            });
        dc.transition(slicePaths, _chart.transitionDuration(),
            function(s) {
                s.attrTween("d", tweenPie);
            }).attr("fill", function(d, i) {
                return _chart.getColor(d, i);
            });
    }

    function updateLabels(pieData, arc) {
        if (_chart.renderLabel()) {
            var labels = _g.selectAll("text." + _sliceCssClass)
                .data(pieData);
            dc.transition(labels, _chart.transitionDuration())
                .attr("transform", function(d) {
                    d.innerRadius = _chart.innerRadius();
                    d.outerRadius = _radius;
                    var centroid = arc.centroid(d);
                    if (isNaN(centroid[0]) || isNaN(centroid[1])) {
                        return "translate(0,0)";
                    } else {
                        return "translate(" + centroid + ")";
                    }
                })
                .attr("text-anchor", "middle")
                .text(function(d) {
                    var data = d.data;
                    if (sliceHasNoData(data) || sliceTooSmall(d))
                        return "";
                    return _chart.label()(d);
                });
        }
    }

    function updateTitles(pieData) {
        if (_chart.renderTitle()) {
            _g.selectAll("g." + _sliceCssClass)
                .data(pieData)
                .select("title")
                .text(function(d) {
                    return _chart.title()(d);
                });
        }
    }

    function removeElements(slices) {
        slices.exit().remove();
    }

    function highlightFilter() {
        if (_chart.hasFilter()) {
            _chart.selectAll("g." + _sliceCssClass).each(function(d) {
                if (_chart.isSelectedSlice(d)) {
                    _chart.highlightSelected(this);
                } else {
                    _chart.fadeDeselected(this);
                }
            });
        } else {
            _chart.selectAll("g." + _sliceCssClass).each(function(d) {
                _chart.resetHighlight(this);
            });
        }
    }

    _chart.innerRadius = function(r) {
        if (!arguments.length) return _innerRadius;
        _innerRadius = r;
        return _chart;
    };

    _chart.radius = function(r) {
        if (!arguments.length) return _radius;
        _radius = r;
        return _chart;
    };

    _chart.cx = function() {
        return _chart.width() / 2;
    };

    _chart.cy = function() {
        return _chart.height() / 2;
    };

    _chart.buildArcs = function() {
        return d3.svg.arc().outerRadius(_radius).innerRadius(_innerRadius);
    };

    _chart.isSelectedSlice = function(d) {
        return _chart.filter() == _chart.keyAccessor()(d.data);
    };

    _chart.doRedraw = function() {
        drawChart();
        return _chart;
    };

    _chart.minAngleForLabel = function(_) {
        if (!arguments.length) return _minAngleForLabel;
        _minAngleForLabel = _;
        return _chart;
    };

    function calculateDataPie() {
        return d3.layout.pie().sort(null).value(function(d) {
            return _chart.valueAccessor()(d);
        });
    }

    function sliceTooSmall(d) {
        var angle = (d.endAngle - d.startAngle);
        return isNaN(angle) || angle < _minAngleForLabel;
    }

    function sliceHasNoData(data) {
        return _chart.valueAccessor()(data) == 0;
    }

    function tweenPie(b) {
        b.innerRadius = _chart.innerRadius();
        var current = this._current;
        if (isOffCanvas(current))
            current = {startAngle: 0, endAngle: 0};
        var i = d3.interpolate(current, b);
        this._current = i(0);
        return function(t) {
            return safeArc(i(t), 0, _chart.buildArcs());
        };
    }

    function isOffCanvas(current) {
        return current == null || isNaN(current.startAngle) || isNaN(current.endAngle);
    }

    function onClick(d) {
        _chart.onClick(d.data);
    }

    function safeArc(d, i, arc) {
        var path = arc(d, i);
        if(path.indexOf("NaN") >= 0)
            path = "M0,0";
        return path;
    }

    return _chart.anchor(parent, chartGroup);
};
<commit_msg>Pie charts can now be configured to display top N and the rest as one 'Others' slice<commit_after>dc.pieChart = function(parent, chartGroup, howMany, titleOther) {
    var DEFAULT_MIN_ANGLE_FOR_LABEL = 0.5;

    var _sliceCssClass = "pie-slice";

    var _radius = 90, _innerRadius = 0;

    var _g;

    var _minAngleForLabel = DEFAULT_MIN_ANGLE_FOR_LABEL;

    var _chart = dc.singleSelectionChart(dc.colorChart(dc.baseChart({})));

    var _howMany = typeof howMany != 'undefined' ? howMany : Infinity;
    var _titleOther = typeof titleOther != 'undefined' ? titleOther : "Others";

    function assemblePieData() {
        if (_howMany == Infinity) {
            return _chart.orderedGroup().top(_howMany); // ordered by keys
        } else {
            var topRows = _chart.group().top(_howMany); // ordered by value

            var allRowsSum = 0.0, topRowsSum = 0.0;
            for (var i in topRows) {
                topRowsSum += topRows[i].value;
            }
            var allRows = _chart.group().all();
            for (var i in allRows) {
                allRowsSum += allRows[i].value;
            }
            topRows.push({"key" : _titleOther, "value" : allRowsSum - topRowsSum });
            return topRows;
        }
    }

    _chart.label(function(d) {
        return _chart.keyAccessor()(d.data);
    });

    _chart.renderLabel(true);

    _chart.title(function(d) {
        return _chart.keyAccessor()(d.data) + ": " + _chart.valueAccessor()(d.data);
    });

    _chart.transitionDuration(350);

    _chart.doRender = function() {
        _chart.resetSvg();

        _g = _chart.svg()
            .append("g")
            .attr("transform", "translate(" + _chart.cx() + "," + _chart.cy() + ")");

        drawChart();

        return _chart;
    };

    function drawChart() {
        if (_chart.dataSet()) {
            var pie = calculateDataPie();

            var arc = _chart.buildArcs();

            var pieData = pie(assemblePieData());

            var slices = _g.selectAll("g." + _sliceCssClass)
                .data(pieData);

            createElements(slices, arc, pieData);

            updateElements(pieData, arc);

            removeElements(slices);

            highlightFilter();
        }
    }

    function createElements(slices, arc, pieData) {
        var slicesEnter = createSliceNodes(slices);

        createSlicePath(slicesEnter, arc);

        createTitles(slicesEnter);

        createLabels(pieData, arc);
    }

    function createSliceNodes(slices) {
        var slicesEnter = slices
            .enter()
            .append("g")
            .attr("class", function(d, i) {
                return _sliceCssClass + " _" + i;
            });
        return slicesEnter;
    }

    function createSlicePath(slicesEnter, arc) {
        var slicePath = slicesEnter.append("path")
            .attr("fill", function(d, i) {
                return _chart.getColor(d, i);
            })
            .on("click", onClick)
            .attr("d", function(d, i) {
                return safeArc(d, i, arc);
            });
        slicePath.transition()
            .duration(_chart.transitionDuration())
            .attrTween("d", tweenPie);
    }

    function createTitles(slicesEnter) {
        if (_chart.renderTitle()) {
            slicesEnter.append("title").text(function(d) {
                return _chart.title()(d);
            });
        }
    }

    function createLabels(pieData, arc) {
        if (_chart.renderLabel()) {
            var labels = _g.selectAll("text." + _sliceCssClass)
                .data(pieData);

            var labelsEnter = labels
                .enter()
                .append("text")
                .attr("class", function(d, i) {
                    return _sliceCssClass + " _" + i;
                })
                .on("click", onClick);
            dc.transition(labelsEnter, _chart.transitionDuration())
                .attr("transform", function(d) {
                    d.innerRadius = _chart.innerRadius();
                    d.outerRadius = _radius;
                    var centroid = arc.centroid(d);
                    if (isNaN(centroid[0]) || isNaN(centroid[1])) {
                        return "translate(0,0)";
                    } else {
                        return "translate(" + centroid + ")";
                    }
                })
                .attr("text-anchor", "middle")
                .text(function(d) {
                    var data = d.data;
                    if (sliceHasNoData(data) || sliceTooSmall(d))
                        return "";
                    return _chart.label()(d);
                });
        }
    }

    function updateElements(pieData, arc) {
        updateSlicePaths(pieData, arc);
        updateLabels(pieData, arc);
        updateTitles(pieData);
    }

    function updateSlicePaths(pieData, arc) {
        var slicePaths = _g.selectAll("g." + _sliceCssClass)
            .data(pieData)
            .select("path")
            .attr("d", function(d, i) {
                return safeArc(d, i, arc);
            });
        dc.transition(slicePaths, _chart.transitionDuration(),
            function(s) {
                s.attrTween("d", tweenPie);
            }).attr("fill", function(d, i) {
                return _chart.getColor(d, i);
            });
    }

    function updateLabels(pieData, arc) {
        if (_chart.renderLabel()) {
            var labels = _g.selectAll("text." + _sliceCssClass)
                .data(pieData);
            dc.transition(labels, _chart.transitionDuration())
                .attr("transform", function(d) {
                    d.innerRadius = _chart.innerRadius();
                    d.outerRadius = _radius;
                    var centroid = arc.centroid(d);
                    if (isNaN(centroid[0]) || isNaN(centroid[1])) {
                        return "translate(0,0)";
                    } else {
                        return "translate(" + centroid + ")";
                    }
                })
                .attr("text-anchor", "middle")
                .text(function(d) {
                    var data = d.data;
                    if (sliceHasNoData(data) || sliceTooSmall(d))
                        return "";
                    return _chart.label()(d);
                });
        }
    }

    function updateTitles(pieData) {
        if (_chart.renderTitle()) {
            _g.selectAll("g." + _sliceCssClass)
                .data(pieData)
                .select("title")
                .text(function(d) {
                    return _chart.title()(d);
                });
        }
    }

    function removeElements(slices) {
        slices.exit().remove();
    }

    function highlightFilter() {
        if (_chart.hasFilter()) {
            _chart.selectAll("g." + _sliceCssClass).each(function(d) {
                if (_chart.isSelectedSlice(d)) {
                    _chart.highlightSelected(this);
                } else {
                    _chart.fadeDeselected(this);
                }
            });
        } else {
            _chart.selectAll("g." + _sliceCssClass).each(function(d) {
                _chart.resetHighlight(this);
            });
        }
    }

    _chart.innerRadius = function(r) {
        if (!arguments.length) return _innerRadius;
        _innerRadius = r;
        return _chart;
    };

    _chart.radius = function(r) {
        if (!arguments.length) return _radius;
        _radius = r;
        return _chart;
    };

    _chart.cx = function() {
        return _chart.width() / 2;
    };

    _chart.cy = function() {
        return _chart.height() / 2;
    };

    _chart.buildArcs = function() {
        return d3.svg.arc().outerRadius(_radius).innerRadius(_innerRadius);
    };

    _chart.isSelectedSlice = function(d) {
        return _chart.filter() == _chart.keyAccessor()(d.data);
    };

    _chart.doRedraw = function() {
        drawChart();
        return _chart;
    };

    _chart.minAngleForLabel = function(_) {
        if (!arguments.length) return _minAngleForLabel;
        _minAngleForLabel = _;
        return _chart;
    };

    function calculateDataPie() {
        return d3.layout.pie().sort(null).value(function(d) {
            return _chart.valueAccessor()(d);
        });
    }

    function sliceTooSmall(d) {
        var angle = (d.endAngle - d.startAngle);
        return isNaN(angle) || angle < _minAngleForLabel;
    }

    function sliceHasNoData(data) {
        return _chart.valueAccessor()(data) == 0;
    }

    function tweenPie(b) {
        b.innerRadius = _chart.innerRadius();
        var current = this._current;
        if (isOffCanvas(current))
            current = {startAngle: 0, endAngle: 0};
        var i = d3.interpolate(current, b);
        this._current = i(0);
        return function(t) {
            return safeArc(i(t), 0, _chart.buildArcs());
        };
    }

    function isOffCanvas(current) {
        return current == null || isNaN(current.startAngle) || isNaN(current.endAngle);
    }

    function onClick(d) {
        _chart.onClick(d.data);
    }

    function safeArc(d, i, arc) {
        var path = arc(d, i);
        if(path.indexOf("NaN") >= 0)
            path = "M0,0";
        return path;
    }

    return _chart.anchor(parent, chartGroup);
};
<|endoftext|>


<commit_before>/**
 * User: Martin Martimeo
 * Date: 13.08.13
 * Time: 18:30
 *
 * Extension to backbone_forms
 */

require(["jquery", "underscore", "backbone", "backbone_forms"],function ($, _, Backbone, BackboneForms) {
    var self = this.Tornado || {};
    var Tornado = this.Tornado = self;

    Tornado.BackboneForm = function (element, options) {
        this.$element = $(element);
        this.options = $.extend({}, Tornado.BackboneForm.DEFAULTS, options);

        if (this.options.model) {
            if (typeof this.options.model == "string") {
                this.model = this.options.model = new window[this.options.model]();
            } else {
                this.model = this.options.model;
            }
        }

        this.form = new Backbone.Form(this.options);
    };
    Tornado.BackboneForm.DEFAULTS = {};

    /**
     * (re)render the form
     *
     * Based on backbone-forms.js/render by Charles Davison
     */
    Tornado.BackboneForm.prototype.render = function () {
        var self = this.form,
            $form = this.$element,
            fields = this.form.fields;

        console.log(self);

        //Render standalone editors
        $form.find('[data-editors]').add($form).each(function (i, el) {
            var $container = $(el),
                selection = $container.attr('data-editors');

            if (_.isUndefined(selection)) {
                return;
            }

            //Work out which fields to include
            var keys = (selection == '*')
                ? self.selectedFields || _.keys(fields)
                : selection.split(',');

            //Add them
            _.each(keys, function (key) {
                var field = fields[key];

                $container.append(field.editor.render().el);
            });
        });

        //Render standalone fields
        $form.find('[data-fields]').add($form).each(function (i, el) {
            var $container = $(el),
                selection = $container.attr('data-fields');

            console.log($container);

            if (_.isUndefined(selection)) {
                return;
            }

            //Work out which fields to include
            var keys = (selection == '*')
                ? self.selectedFields || _.keys(fields)
                : selection.split(',');

            console.log(fields);

            //Add them
            _.each(keys, function (key) {
                var field = fields[key];

                $container.append(field.render().el);
            });
        });

        //Render fieldsets
        $form.find('[data-fieldsets]').add($form).each(function (i, el) {
            var $container = $(el),
                selection = $container.attr('data-fieldsets');

            if (_.isUndefined(selection)) return;

            _.each(self.fieldsets, function (fieldset) {
                $container.append(fieldset.render().el);
            });
        });

        //Set the main element
        self.setElement($form);

        //Set class
        $form.addClass(self.className);

        return self;
    };

    /**
     * Allows to facility html elements with backbone-forms or model functionality
     *
     * @param option
     */
    $.fn.backbone = function (option) {
        return this.each(function () {
            var $this = $(this);
            var data = $this.data('tb.backbone');

            var options = typeof option == 'object' && option;

            if (!data) {
                $this.data('tb.backbone', (data = new Tornado.BackboneForm(this, options)));
                $this.data('tb.backbone').render();
            }
            if (typeof option == 'string') {
                data[option]();
            }
        });
    };
    $.fn.backbone.Constructor = Tornado.BackboneForm;

    // Facile elements with backbone-forms
    $(window).on('load', function () {
        $('[data-model][data-require]').each(function () {
            var $form = $(this);

            require([$(this).data('require')], function () {
                $form.backbone($form.data())
            });
        });
        $('[data-model]:not([data-require])').each(function () {
            var $form = $(this);
            $form.backbone($form.data());
        });
    });

    return Tornado;
}).call(window);
<commit_msg>Remove debug output<commit_after>/**
 * User: Martin Martimeo
 * Date: 13.08.13
 * Time: 18:30
 *
 * Extension to backbone_forms
 */

require(["jquery", "underscore", "backbone", "backbone_forms"],function ($, _, Backbone, BackboneForm) {
    var self = this.Tornado || {};
    var Tornado = this.Tornado = self;

    Tornado.BackboneForm = function (element, options) {
        this.$element = $(element);
        this.options = $.extend({}, Tornado.BackboneForm.DEFAULTS, options);

        if (this.options.model) {
            if (typeof this.options.model == "string") {
                this.model = this.options.model = new window[this.options.model]();
            } else {
                this.model = this.options.model;
            }
        }

        this.form = new Backbone.Form(this.options);
    };
    Tornado.BackboneForm.DEFAULTS = {};

    /**
     * (re)render the form
     *
     * Based on backbone-forms.js/render by Charles Davison
     */
    Tornado.BackboneForm.prototype.render = function () {
        var self = this.form,
            $form = this.$element,
            fields = this.form.fields;

        //Render standalone editors
        $form.find('[data-editors]').add($form).each(function (i, el) {
            var $container = $(el),
                selection = $container.attr('data-editors');

            if (_.isUndefined(selection)) {
                return;
            }

            //Work out which fields to include
            var keys = (selection == '*')
                ? self.selectedFields || _.keys(fields)
                : selection.split(',');

            //Add them
            _.each(keys, function (key) {
                var field = fields[key];

                $container.append(field.editor.render().el);
            });
        });

        //Render standalone fields
        $form.find('[data-fields]').add($form).each(function (i, el) {
            var $container = $(el),
                selection = $container.attr('data-fields');

            if (_.isUndefined(selection)) {
                return;
            }

            //Work out which fields to include
            var keys = (selection == '*')
                ? self.selectedFields || _.keys(fields)
                : selection.split(',');

            //Add them
            _.each(keys, function (key) {
                var field = fields[key];

                $container.append(field.render().el);
            });
        });

        //Render fieldsets
        $form.find('[data-fieldsets]').add($form).each(function (i, el) {
            var $container = $(el),
                selection = $container.attr('data-fieldsets');

            if (_.isUndefined(selection)) return;

            _.each(self.fieldsets, function (fieldset) {
                $container.append(fieldset.render().el);
            });
        });

        //Set the main element
        self.setElement($form);

        //Set class
        $form.addClass(self.className);

        return self;
    };

    /**
     * Allows to facility html elements with backbone-forms or model functionality
     *
     * @param option
     */
    $.fn.backbone = function (option) {
        return this.each(function () {
            var $this = $(this);
            var data = $this.data('tb.backbone');

            var options = typeof option == 'object' && option;

            if (!data) {
                $this.data('tb.backbone', (data = new Tornado.BackboneForm(this, options)));
                $this.data('tb.backbone').render();
            }
            if (typeof option == 'string') {
                data[option]();
            }
        });
    };
    $.fn.backbone.Constructor = Tornado.BackboneForm;

    // Facile elements with backbone-forms
    $(window).on('load', function () {
        $('[data-model][data-require]').each(function () {
            var $form = $(this);

            require([$(this).data('require')], function () {
                $form.backbone($form.data())
            });
        });
        $('[data-model]:not([data-require])').each(function () {
            var $form = $(this);
            $form.backbone($form.data());
        });
    });

    return Tornado;
}).call(window);
<|endoftext|>


<commit_before>$( document ).ready(function() {
	var ctx;
	var WIDTH;
	var HEIGHT;
	var intervalId = 0;
	var frame = 0;

	var emflake = new Image();
	emflake.src = 'img/emflake.png';
	var x = x0 = 100;
	var y = y0 = -299;
	var dx = 0;
	var dy = 2;
	var w = 306;
	var h = 299;

	var mikeflake = new Image();
	mikeflake.src = 'img/mikeflake.png';
	var mx = mx0 = 300;
	var my = my0 = -641;
	var dmx = 0;
	var dmy = 2;
	var mw = 261;
	var mh = 292;

	var lightning = new Image();
	lightning.src = 'img/lightning.png';
	var lx = lx0 = -500;
	var ly = ly0 = 100;
	var dlx = 2;
	var dly = 0;
	var lw = 414;
	var lh = 173;

	var fry = new Image();
	fry.src = 'img/fry.png';
	var fx = fx0 = -900;
	var fy = fy0 = 200;
	var dfx = 2;
	var dfy = 0;
	var fw = 364;
	var fh = 116;

	var snowFadeInRate = .003;
	var snow1alpha = snow2alpha = snow3alpha = 0;

	function init(){
		//set context
		ctx = $("canvas#card")[0].getContext('2d');

		WIDTH = $("canvas#card").width()
		HEIGHT = $("canvas#card").height()

		intervalId = setInterval(draw, 10);
	}

	function draw(){
		frame ++;
		clear();

		drawCharacter(emflake, x, y, w, h);
		dx = getRandomInt(-1,1);
		x += dx;
		y += dy;
		if (y > HEIGHT)	y = y0;

		drawCharacter(mikeflake, mx, my, mw, mh);
		dmx = getRandomInt(-1,1);
		mx += dmx;
		my += dmy;
		if (my > HEIGHT) my = my0;

		drawCharacter(lightning, lx, ly, lw, lh);
		dly = getRandomInt(-1, 1);
		lx += dlx;
		ly += dly;
		if (lx > WIDTH) lx = lx0;

		drawCharacter(fry, fx, fy, fw, fh);
		dfy = getRandomInt(-1,1);
		fx += dfx;
		fy += dfy;
		if (fx > WIDTH) fx = fx0;

		if (frame > 300 && snow2alpha < 1) {
			//draw snow 1
			drawSnow1(snow1alpha);
			if (snow1alpha < 1) snow1alpha += snowFadeInRate;
		}

		if (snow1alpha >= 1 && snow3alpha < 1){
			//draw snow 2
			drawSnow2(snow2alpha);
			if (snow2alpha < 1) snow2alpha += snowFadeInRate;
		}

		if (snow2alpha >= 1){
			//draw snow 3
			drawSnow3(snow3alpha);
			if (snow3alpha < 1) snow3alpha += snowFadeInRate;
		}

	}

	function drawCharacter(name, x, y, w, h){
		ctx.drawImage(name, x, y, w, h);
	}

	function clear() {
	  ctx.clearRect(0, 0, WIDTH, HEIGHT);
	}

	function getRandomInt (min, max) {
	    return Math.floor(Math.random() * (max - min + 1)) + min;
	}

	function drawRect(){
		/* draw a rectangle just for debugging purposes */
		ctx.fillRect(100, 100, 10, 10);
	}

	function drawSnow1(snow1alpha){
		ctx.save();
		ctx.beginPath();
		ctx.moveTo(37.1, 810.9);
		ctx.bezierCurveTo(104.4, 810.9, 119.7, 760.9, 186.0, 762.0);
		ctx.bezierCurveTo(243.8, 762.9, 298.0, 810.9, 318.7, 812.0);
		ctx.bezierCurveTo(339.3, 813.0, 414.0, 782.6, 435.0, 782.6);
		ctx.bezierCurveTo(477.3, 782.6, 496.8, 810.2, 526.3, 810.9);
		ctx.bezierCurveTo(577.3, 812.0, 731.6, 807.6, 770.8, 807.6);
		ctx.bezierCurveTo(826.3, 807.6, 970.8, 759.8, 1018.7, 729.3);
		ctx.bezierCurveTo(1048.0, 710.7, 1063.2, 768.5, 1063.2, 768.5);
		ctx.lineTo(1006.7, 829.3);
		ctx.lineTo(33.9, 843.5);
		ctx.lineTo(37.1, 810.9);
		ctx.closePath();
		ctx.fillStyle = "rgba(255, 255, 255, "+snow1alpha +")";
		ctx.fill();
		ctx.restore();
	}

	function drawSnow2(snow2alpha){
		ctx.save();
		ctx.beginPath();
		ctx.moveTo(14.4, 747.0);
		ctx.bezierCurveTo(90.4, 749.0, 97.1, 726.6, 190.5, 727.1);
		ctx.bezierCurveTo(225.0, 727.2, 264.0, 740.6, 318.4, 739.0);
		ctx.bezierCurveTo(354.6, 738.0, 416.4, 681.0, 480.4, 687.0);
		ctx.bezierCurveTo(544.4, 693.0, 568.4, 709.0, 636.4, 717.0);
		ctx.bezierCurveTo(704.4, 725.0, 730.4, 753.0, 798.4, 749.0);
		ctx.bezierCurveTo(866.4, 745.0, 928.4, 677.0, 972.4, 681.0);
		ctx.bezierCurveTo(1016.4, 685.0, 1042.4, 693.0, 1042.4, 693.0);
		ctx.lineTo(1036.4, 813.0);
		ctx.lineTo(0, 811.0);
		ctx.lineTo(0, 747.0);
		ctx.closePath();
		ctx.fillStyle = "rgba(255, 255, 255, "+snow2alpha +")";
		ctx.fill();
		ctx.restore();
	}

	function drawSnow3(snow3alpha){
		ctx.save();
		ctx.beginPath();
		ctx.moveTo(3.0, 695.6);
		ctx.bezierCurveTo(34.5, 700.1, 108.0, 694.1, 160.5, 695.6);
		ctx.bezierCurveTo(202.5, 696.8, 244.5, 702.4, 309.0, 700.1);
		ctx.bezierCurveTo(351.0, 698.6, 408.0, 665.6, 460.5, 665.6);
		ctx.bezierCurveTo(513.0, 665.6, 600.0, 679.1, 631.5, 680.6);
		ctx.bezierCurveTo(663.0, 682.1, 691.5, 688.1, 744.0, 683.6);
		ctx.bezierCurveTo(796.5, 679.1, 824.9, 657.7, 882.0, 656.6);
		ctx.bezierCurveTo(958.5, 655.1, 1027.4, 667.1, 1046.9, 664.1);
		ctx.lineTo(1040.9, 812.6);
		ctx.lineTo(0.0, 815.6);
		ctx.lineTo(0.0, 695.6);
		ctx.closePath();
		ctx.fillStyle = "rgba(255, 255, 255, "+snow3alpha +")";
		ctx.fill();
		ctx.restore();
	}

	init();

//end document ready
});<commit_msg>multiple mikeflakes<commit_after>$( document ).ready(function() {
	var ctx;
	var WIDTH;
	var HEIGHT;
	var intervalId = 0;
	var frame = 0;

	var emflakes = [];
	var emflake = new Image();
	emflake.src = 'img/emflake.png';
	var x = x0 = 100;
	var y = y0 = -299;
	var dx = 0;
	var dy = 2;
	var w = 306;
	var h = 299;

	var mikeflakes = [];
	var mikeflake = new Image();
	mikeflake.src = 'img/mikeflake.png';
	var mx = mx0 = 300;
	var my = my0 = -641;
	var dmx = 0;
	var dmy = 2;
	var mw = 261;
	var mh = 292;

	var lightning = new Image();
	lightning.src = 'img/lightning.png';
	var lx = lx0 = -500;
	var ly = ly0 = 100;
	var dlx = 2;
	var dly = 0;
	var lw = 414;
	var lh = 173;

	var fry = new Image();
	fry.src = 'img/fry.png';
	var fx = fx0 = -900;
	var fy = fy0 = 200;
	var dfx = 2;
	var dfy = 0;
	var fw = 364;
	var fh = 116;

	var snowFadeInRate = .003;
	var snow1alpha = snow2alpha = snow3alpha = 0;

	function init(){
		//set context
		ctx = $("canvas#card")[0].getContext('2d');

		WIDTH = $("canvas#card").width()
		HEIGHT = $("canvas#card").height()

		intervalId = setInterval(draw, 10);
	}

	function draw(){
		frame ++;
		clear();

		if (getRandomInt(1,60) == 1){
			//var mikeflakeLength = mikeflakes.length;
			mikeflakex = getRandomInt(0, WIDTH);
			mikeflakew = getRandomInt(50, 261);
			mikeflakeh = (mikeflakew * 292 / 261);
			mikeflakes.push({
				thisx: mikeflakex,
				thisy: my0,
				thisw: mikeflakew,
				thish: mikeflakeh
			});
			//console.log(mikeflakes);
		}

		$.each(mikeflakes, function (index, value){
			drawCharacter(mikeflake, value.thisx, value.thisy, value.thisw, value.thish);
			value.thisy += dmy;
		});

		/*
		drawCharacter(emflake, x, y, w, h);
		dx = getRandomInt(-1,1);
		x += dx;
		y += dy;
		if (y > HEIGHT)	y = y0;

		drawCharacter(mikeflake, mx, my, mw, mh);
		dmx = getRandomInt(-1,1);
		mx += dmx;
		my += dmy;
		if (my > HEIGHT) my = my0;
		*/

		/* Drawing snow builds */
		if (frame > 300 && snow2alpha < 1) {
			//draw snow 1
			drawSnow1(snow1alpha);
			if (snow1alpha < 1) snow1alpha += snowFadeInRate;
		}

		if (snow1alpha >= 1 && snow3alpha < 1){
			//draw snow 2
			drawSnow2(snow2alpha);
			if (snow2alpha < 1) snow2alpha += snowFadeInRate;
		}

		if (snow2alpha >= 1){
			//draw snow 3
			drawSnow3(snow3alpha);
			if (snow3alpha < 1) snow3alpha += snowFadeInRate;
		}

		//only when snow build is done, draw the lightning
		if (snow3alpha >= 1){

			drawCharacter(lightning, lx, ly, lw, lh);
			dly = getRandomInt(-1, 1);
			lx += dlx;
			ly += dly;
			if (lx > WIDTH) lx = lx0;

			drawCharacter(fry, fx, fy, fw, fh);
			dfy = getRandomInt(-1,1);
			fx += dfx;
			fy += dfy;
			if (fx > WIDTH) fx = fx0;
		}

	}

	function drawCharacter(name, x, y, w, h){
		ctx.drawImage(name, x, y, w, h);
	}

	function clear() {
	  ctx.clearRect(0, 0, WIDTH, HEIGHT);
	}

	function getRandomInt (min, max) {
	    return Math.floor(Math.random() * (max - min + 1)) + min;
	}

	function drawRect(){
		/* draw a rectangle just for debugging purposes */
		ctx.fillRect(100, 100, 10, 10);
	}

	function drawSnow1(snow1alpha){
		ctx.save();
		ctx.beginPath();
		ctx.moveTo(37.1, 810.9);
		ctx.bezierCurveTo(104.4, 810.9, 119.7, 760.9, 186.0, 762.0);
		ctx.bezierCurveTo(243.8, 762.9, 298.0, 810.9, 318.7, 812.0);
		ctx.bezierCurveTo(339.3, 813.0, 414.0, 782.6, 435.0, 782.6);
		ctx.bezierCurveTo(477.3, 782.6, 496.8, 810.2, 526.3, 810.9);
		ctx.bezierCurveTo(577.3, 812.0, 731.6, 807.6, 770.8, 807.6);
		ctx.bezierCurveTo(826.3, 807.6, 970.8, 759.8, 1018.7, 729.3);
		ctx.bezierCurveTo(1048.0, 710.7, 1063.2, 768.5, 1063.2, 768.5);
		ctx.lineTo(1006.7, 829.3);
		ctx.lineTo(33.9, 843.5);
		ctx.lineTo(37.1, 810.9);
		ctx.closePath();
		ctx.fillStyle = "rgba(255, 255, 255, "+snow1alpha +")";
		ctx.fill();
		ctx.restore();
	}

	function drawSnow2(snow2alpha){
		ctx.save();
		ctx.beginPath();
		ctx.moveTo(14.4, 747.0);
		ctx.bezierCurveTo(90.4, 749.0, 97.1, 726.6, 190.5, 727.1);
		ctx.bezierCurveTo(225.0, 727.2, 264.0, 740.6, 318.4, 739.0);
		ctx.bezierCurveTo(354.6, 738.0, 416.4, 681.0, 480.4, 687.0);
		ctx.bezierCurveTo(544.4, 693.0, 568.4, 709.0, 636.4, 717.0);
		ctx.bezierCurveTo(704.4, 725.0, 730.4, 753.0, 798.4, 749.0);
		ctx.bezierCurveTo(866.4, 745.0, 928.4, 677.0, 972.4, 681.0);
		ctx.bezierCurveTo(1016.4, 685.0, 1042.4, 693.0, 1042.4, 693.0);
		ctx.lineTo(1036.4, 813.0);
		ctx.lineTo(0, 811.0);
		ctx.lineTo(0, 747.0);
		ctx.closePath();
		ctx.fillStyle = "rgba(255, 255, 255, "+snow2alpha +")";
		ctx.fill();
		ctx.restore();
	}

	function drawSnow3(snow3alpha){
		ctx.save();
		ctx.beginPath();
		ctx.moveTo(3.0, 695.6);
		ctx.bezierCurveTo(34.5, 700.1, 108.0, 694.1, 160.5, 695.6);
		ctx.bezierCurveTo(202.5, 696.8, 244.5, 702.4, 309.0, 700.1);
		ctx.bezierCurveTo(351.0, 698.6, 408.0, 665.6, 460.5, 665.6);
		ctx.bezierCurveTo(513.0, 665.6, 600.0, 679.1, 631.5, 680.6);
		ctx.bezierCurveTo(663.0, 682.1, 691.5, 688.1, 744.0, 683.6);
		ctx.bezierCurveTo(796.5, 679.1, 824.9, 657.7, 882.0, 656.6);
		ctx.bezierCurveTo(958.5, 655.1, 1027.4, 667.1, 1046.9, 664.1);
		ctx.lineTo(1040.9, 812.6);
		ctx.lineTo(0.0, 815.6);
		ctx.lineTo(0.0, 695.6);
		ctx.closePath();
		ctx.fillStyle = "rgba(255, 255, 255, "+snow3alpha +")";
		ctx.fill();
		ctx.restore();
	}

	init();

//end document ready
});<|endoftext|>


<commit_before>import { Point } from '@pixi/math';

/**
 * Builds a line to draw
 *
 * Ignored from docs since it is not directly exposed.
 *
 * @ignore
 * @private
 * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties
 * @param {object} webGLData - an object containing all the WebGL-specific information to create this shape
 * @param {object} webGLDataNativeLines - an object containing all the WebGL-specific information to create nativeLines
 */
export default function (graphicsData, graphicsGeometry)
{
    if (graphicsData.lineStyle.native)
    {
        buildNativeLine(graphicsData, graphicsGeometry);
    }
    else
    {
        buildLine(graphicsData, graphicsGeometry);
    }
}

/**
 * Builds a line to draw using the polygon method.
 *
 * Ignored from docs since it is not directly exposed.
 *
 * @ignore
 * @private
 * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties
 * @param {object} webGLData - an object containing all the WebGL-specific information to create this shape
 */
function buildLine(graphicsData, graphicsGeometry)
{
    // TODO OPTIMISE!
    let points = graphicsData.points || graphicsData.shape.points.slice();

    if (points.length === 0)
    {
        return;
    }
    // if the line width is an odd number add 0.5 to align to a whole pixel
    // commenting this out fixes #711 and #1620
    // if (graphicsData.lineWidth%2)
    // {
    //     for (i = 0; i < points.length; i++)
    //     {
    //         points[i] += 0.5;
    //     }
    // }

    const style = graphicsData.lineStyle;

    // get first and last point.. figure out the middle!
    const firstPoint = new Point(points[0], points[1]);
    let lastPoint = new Point(points[points.length - 2], points[points.length - 1]);

    // if the first point is the last point - gonna have issues :)
    if (firstPoint.x === lastPoint.x && firstPoint.y === lastPoint.y)
    {
        // need to clone as we are going to slightly modify the shape..
        points = points.slice();

        points.pop();
        points.pop();

        lastPoint = new Point(points[points.length - 2], points[points.length - 1]);

        const midPointX = lastPoint.x + ((firstPoint.x - lastPoint.x) * 0.5);
        const midPointY = lastPoint.y + ((firstPoint.y - lastPoint.y) * 0.5);

        points.unshift(midPointX, midPointY);
        points.push(midPointX, midPointY);
    }

    const verts = graphicsGeometry.points;
    const length = points.length / 2;
    let indexCount = points.length;
    let indexStart = verts.length / 2;

    // DRAW the Line
    const width = style.width / 2;

    // sort color
    let p1x = points[0];
    let p1y = points[1];
    let p2x = points[2];
    let p2y = points[3];
    let p3x = 0;
    let p3y = 0;

    let perpx = -(p1y - p2y);
    let perpy = p1x - p2x;
    let perp2x = 0;
    let perp2y = 0;
    let perp3x = 0;
    let perp3y = 0;

    let dist = Math.sqrt((perpx * perpx) + (perpy * perpy));

    perpx /= dist;
    perpy /= dist;
    perpx *= width;
    perpy *= width;

    const ratio = style.alignment;// 0.5;
    const r1 = (1 - ratio) * 2;
    const r2 = ratio * 2;

    // start
    verts.push(
        p1x - (perpx * r1),
        p1y - (perpy * r1));

    verts.push(
        p1x + (perpx * r2),
        p1y + (perpy * r2));

    for (let i = 1; i < length - 1; ++i)
    {
        p1x = points[(i - 1) * 2];
        p1y = points[((i - 1) * 2) + 1];

        p2x = points[i * 2];
        p2y = points[(i * 2) + 1];

        p3x = points[(i + 1) * 2];
        p3y = points[((i + 1) * 2) + 1];

        perpx = -(p1y - p2y);
        perpy = p1x - p2x;

        dist = Math.sqrt((perpx * perpx) + (perpy * perpy));
        perpx /= dist;
        perpy /= dist;
        perpx *= width;
        perpy *= width;

        perp2x = -(p2y - p3y);
        perp2y = p2x - p3x;

        dist = Math.sqrt((perp2x * perp2x) + (perp2y * perp2y));
        perp2x /= dist;
        perp2y /= dist;
        perp2x *= width;
        perp2y *= width;

        const a1 = (-perpy + p1y) - (-perpy + p2y);
        const b1 = (-perpx + p2x) - (-perpx + p1x);
        const c1 = ((-perpx + p1x) * (-perpy + p2y)) - ((-perpx + p2x) * (-perpy + p1y));
        const a2 = (-perp2y + p3y) - (-perp2y + p2y);
        const b2 = (-perp2x + p2x) - (-perp2x + p3x);
        const c2 = ((-perp2x + p3x) * (-perp2y + p2y)) - ((-perp2x + p2x) * (-perp2y + p3y));

        let denom = (a1 * b2) - (a2 * b1);

        if (Math.abs(denom) < 0.1)
        {
            denom += 10.1;
            verts.push(
                p2x - (perpx * r1),
                p2y - (perpy * r1));

            verts.push(
                p2x + (perpx * r2),
                p2y + (perpy * r2));

            continue;
        }

        const px = ((b1 * c2) - (b2 * c1)) / denom;
        const py = ((a2 * c1) - (a1 * c2)) / denom;
        const pdist = ((px - p2x) * (px - p2x)) + ((py - p2y) * (py - p2y));

        if (pdist > (196 * width * width))
        {
            perp3x = perpx - perp2x;
            perp3y = perpy - perp2y;

            dist = Math.sqrt((perp3x * perp3x) + (perp3y * perp3y));
            perp3x /= dist;
            perp3y /= dist;
            perp3x *= width;
            perp3y *= width;

            verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1));

            verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2));

            verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1));

            indexCount++;
        }
        else
        {
            verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1));

            verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2));
        }
    }

    p1x = points[(length - 2) * 2];
    p1y = points[((length - 2) * 2) + 1];

    p2x = points[(length - 1) * 2];
    p2y = points[((length - 1) * 2) + 1];

    perpx = -(p1y - p2y);
    perpy = p1x - p2x;

    dist = Math.sqrt((perpx * perpx) + (perpy * perpy));
    perpx /= dist;
    perpy /= dist;
    perpx *= width;
    perpy *= width;

    verts.push(p2x - (perpx * r1), p2y - (perpy * r1));

    verts.push(p2x + (perpx * r2), p2y + (perpy * r2));

    const indices = graphicsGeometry.indices;

    // indices.push(indexStart);

    for (let i = 0; i < indexCount - 2; ++i)
    {
        indices.push(indexStart, indexStart + 1, indexStart + 2);

        indexStart++;
    }
}

/**
 * Builds a line to draw using the gl.drawArrays(gl.LINES) method
 *
 * Ignored from docs since it is not directly exposed.
 *
 * @ignore
 * @private
 * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties
 * @param {object} webGLData - an object containing all the WebGL-specific information to create this shape
 */
function buildNativeLine(graphicsData, graphicsGeometry)
{
    let i = 0;

    const points = graphicsData.points || graphicsData.shape.points;

    if (points.length === 0) return;

    const verts = graphicsGeometry.points;
    const indices = graphicsGeometry.indices;
    const length = points.length / 2;

    let indexStart = verts.length / 2;
    // sort color

    for (i = 1; i < length; i++)
    {
        const p1x = points[(i - 1) * 2];
        const p1y = points[((i - 1) * 2) + 1];

        const p2x = points[i * 2];
        const p2y = points[(i * 2) + 1];

        verts.push(p1x, p1y);

        verts.push(p2x, p2y);

        indices.push(indexStart++, indexStart++);
    }
}
<commit_msg>Fixes detecting close stroke shapes (#5453)<commit_after>import { Point, SHAPES } from '@pixi/math';

/**
 * Builds a line to draw
 *
 * Ignored from docs since it is not directly exposed.
 *
 * @ignore
 * @private
 * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties
 * @param {object} webGLData - an object containing all the WebGL-specific information to create this shape
 * @param {object} webGLDataNativeLines - an object containing all the WebGL-specific information to create nativeLines
 */
export default function (graphicsData, graphicsGeometry)
{
    if (graphicsData.lineStyle.native)
    {
        buildNativeLine(graphicsData, graphicsGeometry);
    }
    else
    {
        buildLine(graphicsData, graphicsGeometry);
    }
}

/**
 * Builds a line to draw using the polygon method.
 *
 * Ignored from docs since it is not directly exposed.
 *
 * @ignore
 * @private
 * @param {PIXI.GraphicsData} graphicsData - The graphics object containing all the necessary properties
 * @param {PIXI.GraphicsGeometry} graphicsGeometry - Geometry where to append output
 */
function buildLine(graphicsData, graphicsGeometry)
{
    const shape = graphicsData.shape;
    let points = graphicsData.points || shape.points.slice();

    if (points.length === 0)
    {
        return;
    }
    // if the line width is an odd number add 0.5 to align to a whole pixel
    // commenting this out fixes #711 and #1620
    // if (graphicsData.lineWidth%2)
    // {
    //     for (i = 0; i < points.length; i++)
    //     {
    //         points[i] += 0.5;
    //     }
    // }

    const style = graphicsData.lineStyle;

    // get first and last point.. figure out the middle!
    const firstPoint = new Point(points[0], points[1]);
    const lastPoint = new Point(points[points.length - 2], points[points.length - 1]);
    const closedShape = shape.type !== SHAPES.POLY;
    const closedPath = firstPoint.x === lastPoint.x && firstPoint.y === lastPoint.y;

    // if the first point is the last point - gonna have issues :)
    if (closedPath || closedShape)
    {
        // need to clone as we are going to slightly modify the shape..
        points = points.slice();

        if (closedPath)
        {
            points.pop();
            points.pop();
            lastPoint.set(points[points.length - 2], points[points.length - 1]);
        }

        const midPointX = lastPoint.x + ((firstPoint.x - lastPoint.x) * 0.5);
        const midPointY = lastPoint.y + ((firstPoint.y - lastPoint.y) * 0.5);

        points.unshift(midPointX, midPointY);
        points.push(midPointX, midPointY);
    }

    const verts = graphicsGeometry.points;
    const length = points.length / 2;
    let indexCount = points.length;
    let indexStart = verts.length / 2;

    // DRAW the Line
    const width = style.width / 2;

    // sort color
    let p1x = points[0];
    let p1y = points[1];
    let p2x = points[2];
    let p2y = points[3];
    let p3x = 0;
    let p3y = 0;

    let perpx = -(p1y - p2y);
    let perpy = p1x - p2x;
    let perp2x = 0;
    let perp2y = 0;
    let perp3x = 0;
    let perp3y = 0;

    let dist = Math.sqrt((perpx * perpx) + (perpy * perpy));

    perpx /= dist;
    perpy /= dist;
    perpx *= width;
    perpy *= width;

    const ratio = style.alignment;// 0.5;
    const r1 = (1 - ratio) * 2;
    const r2 = ratio * 2;

    // start
    verts.push(
        p1x - (perpx * r1),
        p1y - (perpy * r1));

    verts.push(
        p1x + (perpx * r2),
        p1y + (perpy * r2));

    for (let i = 1; i < length - 1; ++i)
    {
        p1x = points[(i - 1) * 2];
        p1y = points[((i - 1) * 2) + 1];

        p2x = points[i * 2];
        p2y = points[(i * 2) + 1];

        p3x = points[(i + 1) * 2];
        p3y = points[((i + 1) * 2) + 1];

        perpx = -(p1y - p2y);
        perpy = p1x - p2x;

        dist = Math.sqrt((perpx * perpx) + (perpy * perpy));
        perpx /= dist;
        perpy /= dist;
        perpx *= width;
        perpy *= width;

        perp2x = -(p2y - p3y);
        perp2y = p2x - p3x;

        dist = Math.sqrt((perp2x * perp2x) + (perp2y * perp2y));
        perp2x /= dist;
        perp2y /= dist;
        perp2x *= width;
        perp2y *= width;

        const a1 = (-perpy + p1y) - (-perpy + p2y);
        const b1 = (-perpx + p2x) - (-perpx + p1x);
        const c1 = ((-perpx + p1x) * (-perpy + p2y)) - ((-perpx + p2x) * (-perpy + p1y));
        const a2 = (-perp2y + p3y) - (-perp2y + p2y);
        const b2 = (-perp2x + p2x) - (-perp2x + p3x);
        const c2 = ((-perp2x + p3x) * (-perp2y + p2y)) - ((-perp2x + p2x) * (-perp2y + p3y));

        let denom = (a1 * b2) - (a2 * b1);

        if (Math.abs(denom) < 0.1)
        {
            denom += 10.1;
            verts.push(
                p2x - (perpx * r1),
                p2y - (perpy * r1));

            verts.push(
                p2x + (perpx * r2),
                p2y + (perpy * r2));

            continue;
        }

        const px = ((b1 * c2) - (b2 * c1)) / denom;
        const py = ((a2 * c1) - (a1 * c2)) / denom;
        const pdist = ((px - p2x) * (px - p2x)) + ((py - p2y) * (py - p2y));

        if (pdist > (196 * width * width))
        {
            perp3x = perpx - perp2x;
            perp3y = perpy - perp2y;

            dist = Math.sqrt((perp3x * perp3x) + (perp3y * perp3y));
            perp3x /= dist;
            perp3y /= dist;
            perp3x *= width;
            perp3y *= width;

            verts.push(p2x - (perp3x * r1), p2y - (perp3y * r1));

            verts.push(p2x + (perp3x * r2), p2y + (perp3y * r2));

            verts.push(p2x - (perp3x * r2 * r1), p2y - (perp3y * r1));

            indexCount++;
        }
        else
        {
            verts.push(p2x + ((px - p2x) * r1), p2y + ((py - p2y) * r1));

            verts.push(p2x - ((px - p2x) * r2), p2y - ((py - p2y) * r2));
        }
    }

    p1x = points[(length - 2) * 2];
    p1y = points[((length - 2) * 2) + 1];

    p2x = points[(length - 1) * 2];
    p2y = points[((length - 1) * 2) + 1];

    perpx = -(p1y - p2y);
    perpy = p1x - p2x;

    dist = Math.sqrt((perpx * perpx) + (perpy * perpy));
    perpx /= dist;
    perpy /= dist;
    perpx *= width;
    perpy *= width;

    verts.push(p2x - (perpx * r1), p2y - (perpy * r1));

    verts.push(p2x + (perpx * r2), p2y + (perpy * r2));

    const indices = graphicsGeometry.indices;

    // indices.push(indexStart);

    for (let i = 0; i < indexCount - 2; ++i)
    {
        indices.push(indexStart, indexStart + 1, indexStart + 2);

        indexStart++;
    }
}

/**
 * Builds a line to draw using the gl.drawArrays(gl.LINES) method
 *
 * Ignored from docs since it is not directly exposed.
 *
 * @ignore
 * @private
 * @param {PIXI.WebGLGraphicsData} graphicsData - The graphics object containing all the necessary properties
 * @param {object} webGLData - an object containing all the WebGL-specific information to create this shape
 */
function buildNativeLine(graphicsData, graphicsGeometry)
{
    let i = 0;

    const points = graphicsData.points || graphicsData.shape.points;

    if (points.length === 0) return;

    const verts = graphicsGeometry.points;
    const indices = graphicsGeometry.indices;
    const length = points.length / 2;

    let indexStart = verts.length / 2;
    // sort color

    for (i = 1; i < length; i++)
    {
        const p1x = points[(i - 1) * 2];
        const p1y = points[((i - 1) * 2) + 1];

        const p2x = points[i * 2];
        const p2y = points[(i * 2) + 1];

        verts.push(p1x, p1y);

        verts.push(p2x, p2y);

        indices.push(indexStart++, indexStart++);
    }
}
<|endoftext|>


<commit_before>/**
 * @file In order to load huge volumes of polygon data into memory without
 * breaking Node (due to its low heap-size limits), the package forks a
 * worker process per polygon layer/shapefile. This module contains
 * functions for initializing them and searching them.
 */

'use strict';

const path = require('path');
const childProcess = require( 'child_process' );
const logger = require( 'pelias-logger' ).get( 'wof-pip-service:master' );
const async = require('async');
const _ = require('lodash');
const fs = require('fs');

let requestCount = 0;
// worker processes keyed on layer
const workers = {};

const responseQueue = {};
const wofData = {};

const defaultLayers = [
  'neighbourhood',
  'borough',
  'locality',
  'localadmin',
  'county',
  'macrocounty',
  'macroregion',
  'region',
  'dependency',
  'country',
  'marinearea',
  'ocean'
];

module.exports.create = function createPIPService(datapath, layers, localizedAdminNames, callback) {
  // take the intersection to keep order in decreasing granularity
  // ie - _.intersection([1, 2, 3], [3, 1]) === [1, 3]
  layers = _.intersection(defaultLayers, _.isEmpty(layers) ? defaultLayers : layers);

  // load all workers
  async.forEach(layers, (layer, done) => {
      startWorker(datapath, layer, localizedAdminNames, function (err, worker) {
        workers[layer] = worker;
        done();
      });
    },
    function end() {
      logger.info('PIP Service Loading Completed!!!');

      callback(null, {
        end: killAllWorkers,
        lookup: function (latitude, longitude, search_layers, responseCallback) {
          if (search_layers === undefined) {
            search_layers = layers;
          } else {
            // take the intersection of the valid layers and the layers sent in
            // so that if any layers are manually disabled for development
            // everything still works. this also means invalid layers
            // are silently ignored
            search_layers = _.intersection(layers, search_layers);
          }

          const id = requestCount++;

          if (search_layers.length === 0) {
            return responseCallback(null, []);
          }

          if (responseQueue.hasOwnProperty(id)) {
            const msg = `Tried to create responseQueue item with id ${id} that is already present`;
            logger.error(msg);
            return responseCallback(null, []);
          }

          // bookkeeping object that tracks the progress of the request
          responseQueue[id] = {
            latLon: {latitude: latitude, longitude: longitude},
            // copy of layers to search
            search_layers: search_layers.slice(),
            responseCallback: responseCallback
          };

          // call the worker for the first layer
          searchWorker(
            id,
            workers[responseQueue[id].search_layers.shift()],
            responseQueue[id].latLon);

        }
      });
    }
  );
};

function killAllWorkers() {
  Object.keys(workers).forEach(function (layer) {
    workers[layer].kill();
  });
}

function startWorker(datapath, layer, localizedAdminNames, callback) {
  const worker = childProcess.fork(path.join(__dirname, 'worker'));

  worker.on('message', msg => {
    if (msg.type === 'loaded') {
      const layerSpecificWofData = JSON.parse(fs.readFileSync(`wof-${layer}-data.json`));
      fs.unlinkSync(`wof-${layer}-data.json`);

      logger.info(`${msg.layer} worker loaded ${_.size(layerSpecificWofData)} features in ${msg.seconds} seconds`);

      // add all layer-specific WOF data to the big WOF data
      _.assign(wofData, layerSpecificWofData);
      callback(null, worker);
    }

    if (msg.type === 'results') {
      handleResults(msg);
    }
  });

  worker.send({
    type: 'load',
    layer: layer,
    datapath: datapath,
    localizedAdminNames: localizedAdminNames
  });
}

function searchWorker(id, worker, coords) {
  worker.send({
    type: 'search',
    id: id,
    coords: coords
  });
}

function handleResults(msg) {
  // logger.info('RESULTS:', JSON.stringify(msg, null, 2));

  if (!responseQueue.hasOwnProperty(msg.id)) {
    logger.error(`tried to handle results for missing id ${msg.id}`);
    return;
  }

  // console.log(JSON.stringify(msg.results));

  // first, handle the case where there was a miss
  if (_.isEmpty(msg.results)) {
    if (!_.isEmpty(responseQueue[msg.id].search_layers)) {
      // if there are no results, then call the next layer but only if there are more layer to call
      searchWorker(msg.id, workers[responseQueue[msg.id].search_layers.shift()], responseQueue[msg.id].latLon);
    } else {
      // no layers left to search, so return an empty array
      responseQueue[msg.id].responseCallback(null, []);
    }

  } else {
    // there was a hit, so find the hierachy and assemble all the pieces
    const hierarchy = wofData[msg.results.Id].Hierarchy;

    if (!_.isEmpty(hierarchy)) {
      const results = _.compact(_.values(hierarchy[0]).map(id => wofData[id]));

      responseQueue[msg.id].responseCallback(null, results);

    } else {
      // some records like oceans and marine areas don't have hierarchies, so use the raw WOF record
      responseQueue[msg.id].responseCallback(null, [wofData[msg.results.Id]]);

    }

    delete responseQueue[msg.id];
  }

}
<commit_msg>moved variable for scope<commit_after>/**
 * @file In order to load huge volumes of polygon data into memory without
 * breaking Node (due to its low heap-size limits), the package forks a
 * worker process per polygon layer/shapefile. This module contains
 * functions for initializing them and searching them.
 */

'use strict';

const path = require('path');
const childProcess = require( 'child_process' );
const logger = require( 'pelias-logger' ).get( 'wof-pip-service:master' );
const async = require('async');
const _ = require('lodash');
const fs = require('fs');

let requestCount = 0;
// worker processes keyed on layer
const workers = {};

const responseQueue = {};
const wofData = {};

const defaultLayers = [
  'neighbourhood',
  'borough',
  'locality',
  'localadmin',
  'county',
  'macrocounty',
  'macroregion',
  'region',
  'dependency',
  'country',
  'marinearea',
  'ocean'
];

module.exports.create = function createPIPService(datapath, layers, localizedAdminNames, callback) {
  // take the intersection to keep order in decreasing granularity
  // ie - _.intersection([1, 2, 3], [3, 1]) === [1, 3]
  layers = _.intersection(defaultLayers, _.isEmpty(layers) ? defaultLayers : layers);

  // load all workers
  async.forEach(layers, (layer, done) => {
      startWorker(datapath, layer, localizedAdminNames, function (err, worker) {
        workers[layer] = worker;
        done();
      });
    },
    function end() {
      logger.info('PIP Service Loading Completed!!!');

      callback(null, {
        end: killAllWorkers,
        lookup: function (latitude, longitude, search_layers, responseCallback) {
          if (search_layers === undefined) {
            search_layers = layers;
          } else {
            // take the intersection of the valid layers and the layers sent in
            // so that if any layers are manually disabled for development
            // everything still works. this also means invalid layers
            // are silently ignored
            search_layers = _.intersection(layers, search_layers);
          }

          if (search_layers.length === 0) {
            return responseCallback(null, []);
          }

          const id = requestCount++;

          if (responseQueue.hasOwnProperty(id)) {
            const msg = `Tried to create responseQueue item with id ${id} that is already present`;
            logger.error(msg);
            return responseCallback(null, []);
          }

          // bookkeeping object that tracks the progress of the request
          responseQueue[id] = {
            latLon: {latitude: latitude, longitude: longitude},
            // copy of layers to search
            search_layers: search_layers.slice(),
            responseCallback: responseCallback
          };

          // call the worker for the first layer
          searchWorker(
            id,
            workers[responseQueue[id].search_layers.shift()],
            responseQueue[id].latLon);

        }
      });
    }
  );
};

function killAllWorkers() {
  Object.keys(workers).forEach(function (layer) {
    workers[layer].kill();
  });
}

function startWorker(datapath, layer, localizedAdminNames, callback) {
  const worker = childProcess.fork(path.join(__dirname, 'worker'));

  worker.on('message', msg => {
    if (msg.type === 'loaded') {
      const layerSpecificWofData = JSON.parse(fs.readFileSync(`wof-${layer}-data.json`));
      fs.unlinkSync(`wof-${layer}-data.json`);

      logger.info(`${msg.layer} worker loaded ${_.size(layerSpecificWofData)} features in ${msg.seconds} seconds`);

      // add all layer-specific WOF data to the big WOF data
      _.assign(wofData, layerSpecificWofData);
      callback(null, worker);
    }

    if (msg.type === 'results') {
      handleResults(msg);
    }
  });

  worker.send({
    type: 'load',
    layer: layer,
    datapath: datapath,
    localizedAdminNames: localizedAdminNames
  });
}

function searchWorker(id, worker, coords) {
  worker.send({
    type: 'search',
    id: id,
    coords: coords
  });
}

function handleResults(msg) {
  // logger.info('RESULTS:', JSON.stringify(msg, null, 2));

  if (!responseQueue.hasOwnProperty(msg.id)) {
    logger.error(`tried to handle results for missing id ${msg.id}`);
    return;
  }

  // console.log(JSON.stringify(msg.results));

  // first, handle the case where there was a miss
  if (_.isEmpty(msg.results)) {
    if (!_.isEmpty(responseQueue[msg.id].search_layers)) {
      // if there are no results, then call the next layer but only if there are more layer to call
      searchWorker(msg.id, workers[responseQueue[msg.id].search_layers.shift()], responseQueue[msg.id].latLon);
    } else {
      // no layers left to search, so return an empty array
      responseQueue[msg.id].responseCallback(null, []);
    }

  } else {
    // there was a hit, so find the hierachy and assemble all the pieces
    const hierarchy = wofData[msg.results.Id].Hierarchy;

    if (!_.isEmpty(hierarchy)) {
      const results = _.compact(_.values(hierarchy[0]).map(id => wofData[id]));

      responseQueue[msg.id].responseCallback(null, results);

    } else {
      // some records like oceans and marine areas don't have hierarchies, so use the raw WOF record
      responseQueue[msg.id].responseCallback(null, [wofData[msg.results.Id]]);

    }

    delete responseQueue[msg.id];
  }

}
<|endoftext|>