<commit_before>/*
 * Copyright (c) 2014-2015, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * TodoActions
 */

var AppDispatcher = require('../dispatcher/AppDispatcher');
var TodoConstants = require('../constants/TodoConstants');

var TodoActions = {

  /**
   * @param  {string} text
   */
  create: function(text) {
    AppDispatcher.dispatch({
      actionType: TodoConstants.TODO_CREATE,
      text: text
    });
  },

  /**
   * @param  {string} id The ID of the ToDo item
   * @param  {string} text
   */
  updateText: function(id, text) {
    AppDispatcher.dispatch({
      actionType: TodoConstants.TODO_UPDATE_TEXT,
      id: id,
      text: text
    });
  },

  /**
   * Toggle whether a single ToDo is complete
   * @param  {object} todo
   */
  toggleComplete: function(todo) {
    var id = todo.id;
    var todoComplete = todo.complete ?
        TodoConstants.TODO_UNDO_COMPLETE :
        TodoConstants.TODO_COMPLETE;

    AppDispatcher.dispatch({
      actionType: todoComplete,
      id: id
    });
  },

  /**
   * Mark all ToDos as complete
   */
  toggleCompleteAll: function() {
    AppDispatcher.dispatch({
      actionType: TodoConstants.TODO_TOGGLE_COMPLETE_ALL
    });
  },

  /**
   * @param  {string} id
   */
  destroy: function(id) {
    AppDispatcher.dispatch({
      actionType: TodoConstants.TODO_DESTROY,
      id: id
    });
  },

  /**
   * Delete all the completed ToDos
   */
  destroyCompleted: function() {
    AppDispatcher.dispatch({
      actionType: TodoConstants.TODO_DESTROY_COMPLETED
    });
  }

};

module.exports = TodoActions;
<commit_msg>Fixed name to reduce confusion.<commit_after>/*
 * Copyright (c) 2014-2015, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * TodoActions
 */

var AppDispatcher = require('../dispatcher/AppDispatcher');
var TodoConstants = require('../constants/TodoConstants');

var TodoActions = {

  /**
   * @param  {string} text
   */
  create: function(text) {
    AppDispatcher.dispatch({
      actionType: TodoConstants.TODO_CREATE,
      text: text
    });
  },

  /**
   * @param  {string} id The ID of the ToDo item
   * @param  {string} text
   */
  updateText: function(id, text) {
    AppDispatcher.dispatch({
      actionType: TodoConstants.TODO_UPDATE_TEXT,
      id: id,
      text: text
    });
  },

  /**
   * Toggle whether a single ToDo is complete
   * @param  {object} todo
   */
  toggleComplete: function(todo) {
    var id = todo.id;
    var actionType = todo.complete ?
        TodoConstants.TODO_UNDO_COMPLETE :
        TodoConstants.TODO_COMPLETE;

    AppDispatcher.dispatch({
      actionType: actionType,
      id: id
    });
  },

  /**
   * Mark all ToDos as complete
   */
  toggleCompleteAll: function() {
    AppDispatcher.dispatch({
      actionType: TodoConstants.TODO_TOGGLE_COMPLETE_ALL
    });
  },

  /**
   * @param  {string} id
   */
  destroy: function(id) {
    AppDispatcher.dispatch({
      actionType: TodoConstants.TODO_DESTROY,
      id: id
    });
  },

  /**
   * Delete all the completed ToDos
   */
  destroyCompleted: function() {
    AppDispatcher.dispatch({
      actionType: TodoConstants.TODO_DESTROY_COMPLETED
    });
  }

};

module.exports = TodoActions;
<|endoftext|>


<commit_before>/*
 * grunt-writefile
 * https://github.com/lunsdorf/grunt-writefile
 *
 * Copyright (c) 2014 Sören Lünsdorf
 * Licensed under the MIT license.
 */
module.exports = function (grunt) {
    'use strict';

    var fs = require('fs');
    var handlebars = require('handlebars');
    var chalk = require('chalk');

    function provide_template_parser (options) {
        var parser = handlebars.create(); // each task has its own instance
        var p;

        if (options.helpers) {
            for (p in options.helpers) {
                if (options.helpers.hasOwnProperty(p)) {
                    parser.registerHelper(p, options.helpers[p]);
                }
            }
        }

        return parser;
    }

    function provide_template_data (options) {
        var data = {};
        var path, p;

        if ('string' === typeof options.data) {
            data = grunt.file.readJSON(options.data);
        } else {
            data = options.data ? grunt.util._.clone(options.data) : {};
        }

        if (options.paths) {
            data.paths = {};

            for (p in options.paths) {
                if (options.paths.hasOwnProperty(p)) {
                    path = options.paths[p];

                    data.paths[p] = grunt.file.expand.apply(grunt.file, 'string' === typeof path ? [path] : [path, path.src]);
                }
            }
        }

        return data;
    }

    grunt.registerMultiTask('writefile', 'Writes static files using handlebars templates.', function () {
        var options = this.options({
            preserveExtension: false, // ignored if path is expanded
            encoding: grunt.file.defaultEncoding,
            mode: false
        });

        var parser = provide_template_parser(options);
        var data = provide_template_data(options);
        var processed = 0;

        this.files.forEach(function (file) {
            var expandedPath = file.orig.expand || false;

            if (!expandedPath && 1 < file.src.length) {
                grunt.warn('writing multiple sources to single destination: ' + file.dest + '!');
            }

            file.src.forEach(function (src) {
                var encoding = options.encoding;
                var dest, tpl, i;

                if (grunt.file.isFile(src)) {
                    grunt.verbose.writeln('Compiling template file: ' + chalk.cyan(src));

                    tpl = parser.compile(grunt.file.read(src));

                    if (options.preserveExtension || !expandedPath) {
                        dest = file.dest;
                    } else {
                        i = file.dest.lastIndexOf('.');
                        dest = 0 > i ? file.dest : file.dest.substr(0, i);
                    }

                    grunt.file.write(dest, tpl(data), {
                        encoding: options.encoding
                    });

                    if (options.mode !== false) {
                        fs.chmodSync(dest, (options.mode === true) ? fs.lstatSync(src).mode : options.mode);
                    }

                    processed = (processed + 1);
                }
            });
        });

        grunt.log.writeln(chalk.cyan(processed) + (1 < processed ? ' files' : ' file') + ' written.');
    });
};
<commit_msg>Fixed empty destination name if filename has no file extension.<commit_after>/*
 * grunt-writefile
 * https://github.com/lunsdorf/grunt-writefile
 *
 * Copyright (c) 2014 Sören Lünsdorf
 * Licensed under the MIT license.
 */
module.exports = function (grunt) {
    'use strict';

    var chalk = require('chalk');
    var fs = require('fs');
    var handlebars = require('handlebars');
    var path = require('path');

    function provide_template_parser (options) {
        var parser = handlebars.create(); // each task has its own instance
        var p;

        if (options.helpers) {
            for (p in options.helpers) {
                if (options.helpers.hasOwnProperty(p)) {
                    parser.registerHelper(p, options.helpers[p]);
                }
            }
        }

        return parser;
    }

    function provide_template_data (options) {
        var data = {};
        var path, p;

        if ('string' === typeof options.data) {
            data = grunt.file.readJSON(options.data);
        } else {
            data = options.data ? grunt.util._.clone(options.data) : {};
        }

        if (options.paths) {
            data.paths = {};

            for (p in options.paths) {
                if (options.paths.hasOwnProperty(p)) {
                    path = options.paths[p];

                    data.paths[p] = grunt.file.expand.apply(grunt.file, 'string' === typeof path ? [path] : [path, path.src]);
                }
            }
        }

        return data;
    }

    grunt.registerMultiTask('writefile', 'Writes static files using handlebars templates.', function () {
        var options = this.options({
            preserveExtension: false, // ignored if path is expanded
            encoding: grunt.file.defaultEncoding,
            mode: false
        });

        var parser = provide_template_parser(options);
        var data = provide_template_data(options);
        var processed = 0;

        this.files.forEach(function (file) {
            var expandedPath = file.orig.expand || false;

            if (!expandedPath && 1 < file.src.length) {
                grunt.warn('writing multiple sources to single destination: ' + file.dest + '!');
            }

            file.src.forEach(function (src) {
                var encoding = options.encoding;
                var dest, tpl, i;

                if (grunt.file.isFile(src)) {
                    grunt.verbose.writeln('Compiling template file: ' + chalk.cyan(src));

                    tpl = parser.compile(grunt.file.read(src));
                    dest = (options.preserveExtension || !expandedPath) ? file.dest : file.dest.slice(0, (path.extname(file.dest).length * -1));

                    grunt.file.write(dest, tpl(data), {
                        encoding: options.encoding
                    });

                    if (options.mode !== false) {
                        fs.chmodSync(dest, (options.mode === true) ? fs.lstatSync(src).mode : options.mode);
                    }

                    processed = (processed + 1);
                }
            });
        });

        grunt.log.writeln(chalk.cyan(processed) + (1 < processed ? ' files' : ' file') + ' written.');
    });
};
<|endoftext|>


<commit_before>var express = require('express');

exports.plugin = function(mediator)
{

	function init(pull)
	{
		var srv = express.createServer();

		mediator.pull('hook', function(data)
		{
			data.channels.forEach(function(channel)
			{
				srv.get('/'+channel.name, function(req, res)
				{
					var name = channel.name;

					for(var param in req.params)
					{
						name = name.replace(':'+param,req.params[param])
					}

					mediator.pull('-stream ' + name, function(writer)
					{
						writer.pipe(res);
					})
				});
			});

			srv.listen(8032);
		});

	}

	console.log('Server running on port ' + 8032+'. try http://localhost:8032/say/hello/craig');

	mediator.on({
		'push init': init
	});
}<commit_msg>changed output message to correct localhost url<commit_after>var express = require('express');

exports.plugin = function(mediator)
{

	function init(pull)
	{
		var srv = express.createServer();

		mediator.pull('hook', function(data)
		{
			data.channels.forEach(function(channel)
			{
				srv.get('/'+channel.name, function(req, res)
				{
					var name = channel.name;

					for(var param in req.params)
					{
						name = name.replace(':'+param,req.params[param])
					}

					mediator.pull('-stream ' + name, function(writer)
					{
						writer.pipe(res);
					})
				});
			});

			srv.listen(8032);
		});

	}

	console.log('Server running on port ' + 8032+'. try http://localhost:8032/say/hello/1/craig');

	mediator.on({
		'push init': init
	});
}<|endoftext|>


<commit_before>
var firebaseRef = new Firebase('https://firetodo.firebaseio.com/');

$(function() {

// Setup the login button; illustrates making a call to your super secret
// enterprise LDAP database mainframe supercomputer using zero-factor auth.
// This example is designed to show the use of server-side signed tokens; not
// the best practices in authenticating users. The examples makes it easy to
// switch between users by simply swapping out the username in the 'login'
// field.
$('#login').on('click', function() {
  // XXX: If you want to use your own node server, change the URL!
  var url = 'http://misc.firebase.com:22222/?user=' + $('#username').val();
  $.get(url, function(data) {
    if (data != null && data.firebaseAuthToken != null) {
      firebaseRef.unauth();
      firebaseRef.auth(data.firebaseAuthToken);
      // Display the token payload for debug purposes.
      var payload = atob(data.firebaseAuthToken.split('.')[1]);
      $('#token').text('Logged in: ' + payload);
    }
  });
});

// Setup the todo lists.
var names = ['alice', 'bob', 'carol' ];
for (var i = 0; i < names.length; i++) {
  var name = names[i];

  // Register for additions to each user's todo lists.
  firebaseRef.child(name).on('child_added', function(name){
    // Remember, we're in a loop so we need a closure to make sure name is
    // the right value.
    return function(snapshot) {
      var todo = snapshot.val();
      var item = $('<li>').text(todo.todo + ' (' + todo.from + ')');
      item.attr("id", name + '-' + snapshot.name())
      $('#' + name + '-list').append(item);
    }
  }(name));

  // We need to handle remove events when Firebase security rules rollbacks
  // an addition.
  firebaseRef.child(name).on('child_removed', function(name){
    return function(snapshot) {
      $('#' + name + '-' + snapshot.name()).remove();
    }
  }(name));

  // Setup additions to the todo lists. We read from the #username field - this
  // allows us to spoof the 'from' to test the validation rules for Alice and
  // Bob and ensure that the rules we've specified are enforced.
  $('#' + name + '-add').on('click', function(name){
    return function() {
      var item = {
        from: $('#username').val(),
        todo: $('#' + name + '-todo').val()
      };
      firebaseRef.child(name).push(item);
      $('#' + name + '-todo').val('');
    }
  }(name));
}

});
<commit_msg>Changed .get to .getJSON for better support in browsers.<commit_after>
var firebaseRef = new Firebase('https://firetodo.firebaseio.com/');

$(function() {

// Setup the login button; illustrates making a call to your super secret
// enterprise LDAP database mainframe supercomputer using zero-factor auth.
// This example is designed to show the use of server-side signed tokens; not
// the best practices in authenticating users. The examples makes it easy to
// switch between users by simply swapping out the username in the 'login'
// field.
$('#login').on('click', function() {
  // XXX: If you want to use your own node server, change the URL!
  var url = 'http://misc.firebase.com:22222/?user=' + $('#username').val();
  $.getJSON(url, function(data) {
    if (data != null && data.firebaseAuthToken != null) {
      firebaseRef.unauth();
      firebaseRef.auth(data.firebaseAuthToken);
      // Display the token payload for debug purposes.
      var payload = atob(data.firebaseAuthToken.split('.')[1]);
      $('#token').text('Logged in: ' + payload);
    }
  });
});

// Setup the todo lists.
var names = ['alice', 'bob', 'carol' ];
for (var i = 0; i < names.length; i++) {
  var name = names[i];

  // Register for additions to each user's todo lists.
  firebaseRef.child(name).on('child_added', function(name){
    // Remember, we're in a loop so we need a closure to make sure name is
    // the right value.
    return function(snapshot) {
      var todo = snapshot.val();
      var item = $('<li>').text(todo.todo + ' (' + todo.from + ')');
      item.attr("id", name + '-' + snapshot.name())
      $('#' + name + '-list').append(item);
    }
  }(name));

  // We need to handle remove events when Firebase security rules rollbacks
  // an addition.
  firebaseRef.child(name).on('child_removed', function(name){
    return function(snapshot) {
      $('#' + name + '-' + snapshot.name()).remove();
    }
  }(name));

  // Setup additions to the todo lists. We read from the #username field - this
  // allows us to spoof the 'from' to test the validation rules for Alice and
  // Bob and ensure that the rules we've specified are enforced.
  $('#' + name + '-add').on('click', function(name){
    return function() {
      var item = {
        from: $('#username').val(),
        todo: $('#' + name + '-todo').val()
      };
      firebaseRef.child(name).push(item);
      $('#' + name + '-todo').val('');
    }
  }(name));
}

});
<|endoftext|>


<commit_before>const ejs = require("ejs");
const fs = require("fs");
const puppeteer = require("puppeteer");
const ecstatic = require("ecstatic");
const http = require("http");

function findChunkWithName(dependencygraph, name) {
  return Object.values(dependencygraph).find(desc =>
    (desc.facadeModuleId || "").endsWith(name)
  );
}

async function renderEjsFile(inPath, outPath, data) {
  const template = fs.readFileSync(inPath).toString();
  const output = ejs.render(template, data);
  fs.writeFileSync(outPath, output);
}

async function generateShell(file, dependencygraph) {
  await renderEjsFile("src/index.ejs", file, {
    bootstrapFile: findChunkWithName(dependencygraph, "bootstrap.ts").fileName,
    workerFile: findChunkWithName(dependencygraph, "worker.ts").fileName,
    dependencygraph,
    pkg: require("./package.json"),
    fs
  });
}

async function startServer() {
  const app = ecstatic({
    root: "./dist"
  });
  return http.createServer(app).listen();
}

async function grabMarkup(address) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  // TODO Is this good?
  page.viewport(1280, 720);
  await page.goto(address);
  await page.waitFor(1000);
  // Set all input field values as an attribute
  await page.evaluate(() => {
    document.querySelectorAll("input").forEach(el => {
      el.setAttribute("value", el.value);
    });
  });
  const markup = await page.evaluate(() => document.documentElement.outerHTML);
  await browser.close();
  return markup;
}

async function correctMarkup(markup, { port, dependencygraph }) {
  // Make absolute references relative
  markup = markup.replace(new RegExp(`http://localhost:${port}/`, "g"), "./");
  // Remove all dynamically added script tags
  markup = markup.replace(
    /<script src="\.\/chunk-([^"]+)"[^>]+><\/script>/g,
    ""
  );
  return markup;
}

async function run() {
  const dependencygraph = require("./dependencygraph.json");
  await generateShell("dist/index.html", dependencygraph);
  const server = await startServer();
  const port = server.address().port;
  let markup = await grabMarkup(`http://localhost:${port}/?prerender`);
  markup = await correctMarkup(markup, { port, dependencygraph });
  fs.writeFileSync("dist/index.html", markup);
  server.close();

  await renderEjsFile("src/_headers.ejs", "dist/_headers", {});
}
run();
<commit_msg>Add doctype to SSR (#38)<commit_after>const ejs = require("ejs");
const fs = require("fs");
const puppeteer = require("puppeteer");
const ecstatic = require("ecstatic");
const http = require("http");

function findChunkWithName(dependencygraph, name) {
  return Object.values(dependencygraph).find(desc =>
    (desc.facadeModuleId || "").endsWith(name)
  );
}

async function renderEjsFile(inPath, outPath, data) {
  const template = fs.readFileSync(inPath).toString();
  const output = ejs.render(template, data);
  fs.writeFileSync(outPath, output);
}

async function generateShell(file, dependencygraph) {
  await renderEjsFile("src/index.ejs", file, {
    bootstrapFile: findChunkWithName(dependencygraph, "bootstrap.ts").fileName,
    workerFile: findChunkWithName(dependencygraph, "worker.ts").fileName,
    dependencygraph,
    pkg: require("./package.json"),
    fs
  });
}

async function startServer() {
  const app = ecstatic({
    root: "./dist"
  });
  return http.createServer(app).listen();
}

async function grabMarkup(address) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  // TODO Is this good?
  page.viewport(1280, 720);
  await page.goto(address);
  await page.waitFor(1000);
  // Set all input field values as an attribute
  await page.evaluate(() => {
    document.querySelectorAll("input").forEach(el => {
      el.setAttribute("value", el.value);
    });
  });
  const markup = await page.evaluate(() => document.documentElement.outerHTML);
  await browser.close();
  return "<!doctype html>" + markup;
}

async function correctMarkup(markup, { port, dependencygraph }) {
  // Make absolute references relative
  markup = markup.replace(new RegExp(`http://localhost:${port}/`, "g"), "./");
  // Remove all dynamically added script tags
  markup = markup.replace(
    /<script src="\.\/chunk-([^"]+)"[^>]+><\/script>/g,
    ""
  );
  return markup;
}

async function run() {
  const dependencygraph = require("./dependencygraph.json");
  await generateShell("dist/index.html", dependencygraph);
  const server = await startServer();
  const port = server.address().port;
  let markup = await grabMarkup(`http://localhost:${port}/?prerender`);
  markup = await correctMarkup(markup, { port, dependencygraph });
  fs.writeFileSync("dist/index.html", markup);
  server.close();

  await renderEjsFile("src/_headers.ejs", "dist/_headers", {});
}
run();
<|endoftext|>


<commit_before>/**
 * Grafter-memcache is a server component.
 * It sits between Graftwerk and the client to save Graftwerk
 * requests in memory, and also to prevent HTTP timeouts.
 *
 * It takes the Graftwerk query as input, computes a hash and
 * transfer the query to Graftwerk. The client receives a ticket
 * and can fetch the result in a separate query. If the result is
 * not ready after a specified delay, the client is asked to try
 * again. We chosed this behavior in order to prevent HTTP timeouts
 * that can be triggered by many components (CDNs, browser, proxy…).
 *
 * It's a NodeJS software (tested with NodeJS 4.0.0)
 * Some parts are developed using JavaScript ES6.
 */

'use strict';

// The HTTP module is used to contact Graftwerk
var http = require('http');

// In this implementation, the cache is stored using the filesystem
// It facilitates the implementation and use less memory in the NodeJS
// process.
// If the system supports it, the suggest using a tmpfs filesystem.
var fs = require('fs');

// The cryptographic module is used to compute a
// SHA256 hash from the query, used as a ticket number
var crypto = require('crypto');

// Express is a library providing tools to develop HTTP Apis.
var express = require('express');

// Graftwerk queries must be parsed to generate unique hash,
// as a query may contain random values that are not related to the  inputs.
// BusBoy has been selected because it provides a low-level API with streams.
var Busboy = require('busboy');

// Temp is used to generate temporary file paths.
var temp = require('temp');

// HTTP CORS library
var cors = require('cors');

// This component allows compressing the output, to reduce the result size
// when it is sent to the client
var compression = require('compression');

// Morgan is a HTTP logging component.
var morgan = require('morgan');

// Length-stream is used to compute the size of a streams
var lengthStream = require('length-stream');

// Filesize parser is used to convert size such as 200kb to bytes
var filesizeParser = require('filesize-parser');

// Local dependencies
var cleaning = require('./cleaning');
var sin = require('./model.js');

/**
 * All the following settings are pre-configured, but can be overrided using environnement
 * variables.
 *
 * Example:
 *  export HTTP_PORT=8085
 */

// This is the TCP port where this component is listening
var serverPort = process.env.HTTP_PORT || 8082;

// The maximum waiting delay (in ms) is the maximum duration of a fetching request.
// When this delay ends, a HTTP 204 answer is sent to the client. The client must
// send a new request to fetch the data.
var maximumWaitingDelay = parseInt(process.env.MAXIMUM_WAITING_DELAY) || 15000;

// The cache folder contains the results from Graftwerk.
// Each file is identified by a hash (also known as ticked number)
var cacheFolder = process.env.CACHE_FOLDER || './cache/';

// The reqs folder contains the queries that must be sent to Graftwerk
// They are saved as a file in order to reduce the memory usage by the component.
// If the server contains a lot of memory, this folder can be mounted as a tmpfs
// filesystem.
var reqsFolder = process.env.REQS_FOLDER || './reqs/';

// The max-age header is used by some proxies and caching system. It is sent when
// the data is fetched by a client.
var cacheMaxAgeHeader = parseInt(process.env.MAX_HAGE) || 86400000;

// Graftwerk hostname and ports.
// This component doesn't include loadbalancing. You can however configure a
// loadbalancer and set the loadbalancer hostname here.
var graftwerkHostname = process.env.GRAFTWERK || 'graftwerk';
var graftwerkPort = process.env.GRAFTWERK_PORT || 8080;

// The maximum amount of storage that can be used in the cache
var maxStorage = filesizeParser(process.env.MAX_STORAGE || '2gb');

// We clear the reqs folder. This folder only contains the requests that are currently
// received from the client or sent to Grafter. It is empty most of the time.
cleaning(reqsFolder);

// The cache folder is cleared after every startup because we don't know the previous status
// (http response code and http content type).
cleaning(cacheFolder);

// The cache map has the SHA256 hash as key, and a Cache object as value.
var cache = new sin.Cache();

// Setting-up the express HTTP server
var app = express();

// Compress the output
app.use(compression());

// Add a HTTP logging using Apache syntax
app.use(morgan('short'));

// Enable CORS for every request
app.use(cors());

/**
 * This is the main API method provided by this component.
 *
 * It listens for HTTP POST queries, no matter what the query
 * path is.
 * A query hash is computed. If a result is not present in the cache,
 * the query is forwarded to Graftwerk.
 *
 * In every case, a ticket number (the hash) is sent to the client serialized in JSON.
 *
 * Example output:
 *   {
 *     "hash": "6710eee6e90e696652222f7c3e71241cefacefea3d9dbfc389099ff189ef1da7",
 *     "processing": true,
 *     "startTime": "2015-09-14T12:44:55.377Z"
 *   }
 */
app.use((req, res, next) => {
  if (req.method !== 'POST') {
    // Calling next allows the other methods to proceed
    return next();
  }

  // We start by cleaning the cache
  cache.clean(maxStorage, cacheFolder);

  // Form parsing initialization
  var bus = new Busboy({
    headers: req.headers
  });

  // Hash stream initialization
  // We chosed SHA256 because it has a good quality.
  // The encoding is set to hex, so a string containing
  // the hexadecimal hash is computed.
  var inputHash = crypto.createHash('sha256');
  inputHash.setEncoding('hex');

  // The url is part of the hash because it can be either graft or pipe
  inputHash.write(req.url);

  // the method is always POST in this version, but it may change
  inputHash.write(req.method);

  // When an uploaded file has been detected in the POST query
  bus.on('file', (fieldname, file, filename, encoding, mimetype) => {
    // We use all these informations to compute the hash
    inputHash.write(fieldname);
    inputHash.write(filename);
    inputHash.write(encoding);
    inputHash.write(mimetype);

    // We also use the file content to compute the hash
    // We ask the pipe to be automaticcaly closed because
    // more files or fields can be received after
    file.pipe(inputHash, {
      end: false
    });
  });

  // When a field (key=value store) has beend detected in the POST query
  bus.on('field', (fieldname, val) => {
    // Both the key and the values are used to compute the hash
    inputHash.write(fieldname);
    inputHash.write(val);
  });

  // The query is saved locally in the filesystem
  // We chosed to do that using a filesystem because a query can be huge
  // (few gigabytes), and we prefer to not have such a big memory usage
  // with the nodejs process.
  // However, if the server has a lot of memory, a tmpfs (a RAM filesystem)
  // maybe used, but we leave this choice to the system administrator.
  var tempPath = temp.path({
    dir: reqsFolder
  });

  var writeStream = fs.createWriteStream(tempPath);

  // The hashkey (or the ticket number) declaration
  var hashKey = 'no-hash-key';

  // When the query is finally completely loaded and saved
  writeStream.on('finish', () => {

    // If the cache already contains the result
    // We just need to remove the query (we do not need it)
    // and to send the informations to the client
    if (cache.has(hashKey)) {
      fs.unlink(tempPath);
      res.json(cache.get(hashKey));
      return;
    }

    // If the cache doesn't exist, we need to create a new entry
    var cacheEntry = new sin.CacheEntry(hashKey);
    cache.set(hashKey, cacheEntry);

    // The informations are directly sent to the client and the request
    // is closed. However we job is not done here for this component,
    // we must foward the query to Grafwerk, save the result and
    // trigger the eventuals event listeners
    res.json(cacheEntry);

    // Create the HTTP query to send to Graftwerk
    var request = http.request({
      hostname: graftwerkHostname,
      port: graftwerkPort,
      method: req.method,
      path: req.url,
      headers: req.headers
    }, (response) => {
      // When we receive the response, we can finally remove the query
      fs.unlink(tempPath);

      // The cache location is a path in the cache filesystem
      var cacheLocation = cacheFolder + hashKey;

      // If Graftwerk returned an error the cache must still be saved
      // so the clients can fetch the errors
      // but the lifetime of the cache entry is short so the user can
      // try again after a delay
      if (response.statusCode === 500) {
        setTimeout(() => {
          // The cache is removed from the collection
          cache.delete(hashKey);

          // and the filesystem data is removed as well
          fs.unlink(cacheLocation);
        }, 1000 * 60 * 15); // after 15 minutes
      }

      // We save the response to the filesystem
      var cacheStream = fs.createWriteStream(cacheLocation);
      response.pipe(cacheStream);

      var cacheStreamLength = 0;
      response.pipe(lengthStream((length) => {
        cacheStreamLength = length;
      }));

      // When the filesystem has saved the response, we can mark it as processed
      // and trigger the eventual event listeners
      cacheStream.on('finish', () => {
        cacheEntry.finalize(response.statusCode, response.headers['content-type'], cacheStreamLength);
        cache.addStreamLength(cacheStreamLength);

        // We save the content-disposition header for some reasons
        if (response.headers['content-disposition']) {
          cacheEntry.contentDisposition = response.headers['content-disposition'];
        }
      });
    });

    // When something goes wrong
    request.on('error', function(err) {
      // We log the problem
      console.log('ERROR: ', err);

      // We cancel the eventual event listeners
      cacheEntry.reject(err);

      // We remove the cache entry
      cache.delete(hashKey);

      // We remove the query file to save memory
      // You can disable this line if you want to keep it for debug purposes
      fs.unlink(tempPath);
    });

    // Read the query from the filesystem...
    var readStream = fs.createReadStream(tempPath);

    // ...and send it to Graftwerk
    readStream.pipe(request);
  });

  // When we have finished the POST query parsing
  bus.on('finish', () => {
    // We close the query file to save it
    // It will trigger the previous event block
    writeStream.end();

    // We compute the hash
    inputHash.end();
    hashKey = inputHash.read();
  });

  // We connect the query to the POST query parsing
  req.pipe(bus);

  // We write the query in a temporary file in the queries folder
  req.pipe(writeStream);
});

// Send a file from a cache entry
// We just use Express tools
var sendFile = (req, res, cacheEntry) => {

  // If the client ask for the status, we send the status
  // instead of the cache content
  if (req.query.status) {
    res.json(cacheEntry);
    return;
  }

  // We send the file with the same status than the one saved
  res.status(cacheEntry.statusCode);

  var headers = {
    'Content-Type': cacheEntry.contentType
  };

  // We put back the content-disposition header if necessary
  if (cacheEntry.contentDisposition) {
    headers['Content-Disposition'] = cacheEntry.contentDisposition;
  }

  // We send the file using express.js
  res.sendFile(cacheEntry.hash, {
    root: cacheFolder,
    maxAge: cacheMaxAgeHeader,
    headers
  }, (err) => {
    if (err) {
      console.log('SendFile error: ', err);

      // We close the client connection
      res.end();
    }
  });
};

/**
 * The client will fetch this endpoint once it received the hash.
 */
app.get('/graftermemcache/:hash', (req, res) => {
  var hashKey = req.params.hash;

  // If the hash is not found in the cache, it's a HTTP 404 not found error
  // This occurs when the client send a wrong hash, or if this component
  // has been restarted after the client received the hash
  // Another possibility is when this component is used behind a loadbalancer
  // and the hash has been sent by another instance. If used behind a loadbalancer,
  // a client should always contact the same instance.
  if (!cache.has(hashKey)) {
    res.status(404).end();
    return;
  }

  var cacheEntry = cache.get(hashKey);

  // If Graftwerk is currently processing the query
  if (cacheEntry.processing) {

    // We get a promise to send the result once the data is received and saved
    var promise = cacheEntry.promise;

    // We however have an internal timeout
    // After waiting the maximum waiting delay, the an empty response is sent to the
    // client. It should interpret it as the necessity to ask again.
    // As explained before, this is done like this to prevent HTTP timeouts that can
    // occurs.
    var timeoutId = setTimeout(function() {
      // Send the empty response with no cache because the status will change
      res.header('Cache-Control', 'no-cache, no-store');
      res.header('Pragma', 'no-cache');
      res.status(204).end();

      // Remove the timeoutID, also used to notify the promise callback that the
      // request has timeout.
      timeoutId = null;

      // Clear variables, it might save memory if a garbage collection occurs before the promise
      // callback. But I am not sure.
      req = null;
      res = null;
    }, maximumWaitingDelay);

    // When the result has been received and saved from Graftwerk
    promise.then(() => {
      // If a timeout hasn't occured yet
      if (timeoutId !== null) {

        // It's time to send the file
        sendFile(req, res, cacheEntry);

        // And to cancel the timeout
        clearTimeout(timeoutId);
      }
    },

    (error) => {
      // Sadly, errors may occur
      res.status(500).json(error);
      clearTimeout(timeoutId);
    });
  } else {
    // If the data is already in the cache and has already been computed
    // we just need to send it
    sendFile(req, res, cacheEntry);
  }

});

// Starting the HTTP server
app.listen(serverPort, () => {
  console.log('Grafter-memcache started on http://localhost:' + serverPort + '/');
  console.log('Graftwerk endpoint: http://' + graftwerkHostname + ':' + graftwerkPort);
});
<commit_msg>BugFix 982810882288<commit_after>/**
 * Grafter-memcache is a server component.
 * It sits between Graftwerk and the client to save Graftwerk
 * requests in memory, and also to prevent HTTP timeouts.
 *
 * It takes the Graftwerk query as input, computes a hash and
 * transfer the query to Graftwerk. The client receives a ticket
 * and can fetch the result in a separate query. If the result is
 * not ready after a specified delay, the client is asked to try
 * again. We chosed this behavior in order to prevent HTTP timeouts
 * that can be triggered by many components (CDNs, browser, proxy…).
 *
 * It's a NodeJS software (tested with NodeJS 4.0.0)
 * Some parts are developed using JavaScript ES6.
 */

'use strict';

// The HTTP module is used to contact Graftwerk
var http = require('http');

// In this implementation, the cache is stored using the filesystem
// It facilitates the implementation and use less memory in the NodeJS
// process.
// If the system supports it, the suggest using a tmpfs filesystem.
var fs = require('fs');

// The cryptographic module is used to compute a
// SHA256 hash from the query, used as a ticket number
var crypto = require('crypto');

// Express is a library providing tools to develop HTTP Apis.
var express = require('express');

// Graftwerk queries must be parsed to generate unique hash,
// as a query may contain random values that are not related to the  inputs.
// BusBoy has been selected because it provides a low-level API with streams.
var Busboy = require('busboy');

// Temp is used to generate temporary file paths.
var temp = require('temp');

// HTTP CORS library
var cors = require('cors');

// This component allows compressing the output, to reduce the result size
// when it is sent to the client
var compression = require('compression');

// Morgan is a HTTP logging component.
var morgan = require('morgan');

// Length-stream is used to compute the size of a streams
var lengthStream = require('length-stream');

// Filesize parser is used to convert size such as 200kb to bytes
var filesizeParser = require('filesize-parser');

// Local dependencies
var cleaning = require('./cleaning');
var sin = require('./model.js');

/**
 * All the following settings are pre-configured, but can be overrided using environnement
 * variables.
 *
 * Example:
 *  export HTTP_PORT=8085
 */

// This is the TCP port where this component is listening
var serverPort = process.env.HTTP_PORT || 8082;

// The maximum waiting delay (in ms) is the maximum duration of a fetching request.
// When this delay ends, a HTTP 204 answer is sent to the client. The client must
// send a new request to fetch the data.
var maximumWaitingDelay = parseInt(process.env.MAXIMUM_WAITING_DELAY) || 15000;

// The cache folder contains the results from Graftwerk.
// Each file is identified by a hash (also known as ticked number)
var cacheFolder = process.env.CACHE_FOLDER || './cache/';

// The reqs folder contains the queries that must be sent to Graftwerk
// They are saved as a file in order to reduce the memory usage by the component.
// If the server contains a lot of memory, this folder can be mounted as a tmpfs
// filesystem.
var reqsFolder = process.env.REQS_FOLDER || './reqs/';

// The max-age header is used by some proxies and caching system. It is sent when
// the data is fetched by a client.
var cacheMaxAgeHeader = parseInt(process.env.MAX_HAGE) || 86400000;

// Graftwerk hostname and ports.
// This component doesn't include loadbalancing. You can however configure a
// loadbalancer and set the loadbalancer hostname here.
var graftwerkHostname = process.env.GRAFTWERK || 'graftwerk';
var graftwerkPort = process.env.GRAFTWERK_PORT || 8080;

// The maximum amount of storage that can be used in the cache
var maxStorage = filesizeParser(process.env.MAX_STORAGE || '2gb');

// We clear the reqs folder. This folder only contains the requests that are currently
// received from the client or sent to Grafter. It is empty most of the time.
cleaning(reqsFolder);

// The cache folder is cleared after every startup because we don't know the previous status
// (http response code and http content type).
cleaning(cacheFolder);

// The cache map has the SHA256 hash as key, and a Cache object as value.
var cache = new sin.Cache();

// Setting-up the express HTTP server
var app = express();

// Compress the output
app.use(compression());

// Add a HTTP logging using Apache syntax
app.use(morgan('short'));

// Enable CORS for every request
app.use(cors());

/**
 * This is the main API method provided by this component.
 *
 * It listens for HTTP POST queries, no matter what the query
 * path is.
 * A query hash is computed. If a result is not present in the cache,
 * the query is forwarded to Graftwerk.
 *
 * In every case, a ticket number (the hash) is sent to the client serialized in JSON.
 *
 * Example output:
 *   {
 *     "hash": "6710eee6e90e696652222f7c3e71241cefacefea3d9dbfc389099ff189ef1da7",
 *     "processing": true,
 *     "startTime": "2015-09-14T12:44:55.377Z"
 *   }
 */
app.use((req, res, next) => {
  if (req.method !== 'POST') {
    // Calling next allows the other methods to proceed
    return next();
  }

  // We start by cleaning the cache
  cache.clean(maxStorage, cacheFolder);

  // Form parsing initialization
  var bus = new Busboy({
    headers: req.headers
  });

  // Hash stream initialization
  // We chosed SHA256 because it has a good quality.
  // The encoding is set to hex, so a string containing
  // the hexadecimal hash is computed.
  var inputHash = crypto.createHash('sha256');
  inputHash.setEncoding('hex');

  // The url is part of the hash because it can be either graft or pipe
  inputHash.write(req.url);

  // the method is always POST in this version, but it may change
  inputHash.write(req.method);

  // When an uploaded file has been detected in the POST query
  bus.on('file', (fieldname, file, filename, encoding, mimetype) => {
    // We use all these informations to compute the hash
    inputHash.write(fieldname);
    inputHash.write(filename);
    inputHash.write(encoding);
    inputHash.write(mimetype);

    // We also use the file content to compute the hash
    // We ask the pipe to be automaticcaly closed because
    // more files or fields can be received after
    file.pipe(inputHash, {
      end: false
    });
  });

  // When a field (key=value store) has beend detected in the POST query
  bus.on('field', (fieldname, val) => {
    // Both the key and the values are used to compute the hash
    inputHash.write(fieldname);
    inputHash.write(val);
  });

  // The query is saved locally in the filesystem
  // We chosed to do that using a filesystem because a query can be huge
  // (few gigabytes), and we prefer to not have such a big memory usage
  // with the nodejs process.
  // However, if the server has a lot of memory, a tmpfs (a RAM filesystem)
  // maybe used, but we leave this choice to the system administrator.
  var tempPath = temp.path({
    dir: reqsFolder
  });

  var writeStream = fs.createWriteStream(tempPath);

  // The hashkey (or the ticket number) declaration
  var hashKey = 'no-hash-key';

  // When the query is finally completely loaded and saved
  writeStream.on('finish', () => {

    // If the cache already contains the result
    // We just need to remove the query (we do not need it)
    // and to send the informations to the client
    if (cache.has(hashKey)) {
      fs.unlink(tempPath);
      res.json(cache.get(hashKey));
      return;
    }

    // If the cache doesn't exist, we need to create a new entry
    var cacheEntry = new sin.CacheEntry(hashKey);
    cache.set(hashKey, cacheEntry);

    // The informations are directly sent to the client and the request
    // is closed. However we job is not done here for this component,
    // we must foward the query to Grafwerk, save the result and
    // trigger the eventuals event listeners
    res.json(cacheEntry);

    // Create the HTTP query to send to Graftwerk
    var request = http.request({
      hostname: graftwerkHostname,
      port: graftwerkPort,
      method: req.method,
      path: req.url,
      headers: req.headers
    }, (response) => {
      // When we receive the response, we can finally remove the query
      fs.unlink(tempPath);

      // The cache location is a path in the cache filesystem
      var cacheLocation = cacheFolder + hashKey;

      // If Graftwerk returned an error the cache must still be saved
      // so the clients can fetch the errors
      // but the lifetime of the cache entry is short so the user can
      // try again after a delay
      if (response.statusCode === 500) {
        setTimeout(() => {
          // The cache is removed from the collection
          cache.delete(hashKey);

          // and the filesystem data is removed as well
          fs.unlink(cacheLocation);
        }, 1000 * 60 * 15); // after 15 minutes
      }

      // We save the response to the filesystem
      var cacheStream = fs.createWriteStream(cacheLocation);

      var cacheStreamLength = 0;

      // When the filesystem has saved the response, we can mark it as processed
      // and trigger the eventual event listeners
      cacheStream.on('finish', () => {
        cacheEntry.finalize(response.statusCode, response.headers['content-type'], cacheStreamLength);
        cache.addStreamLength(cacheStreamLength);

        // We save the content-disposition header for some reasons
        if (response.headers['content-disposition']) {
          cacheEntry.contentDisposition = response.headers['content-disposition'];
        }
      });

      response.pipe(lengthStream((length) => {
        cacheStreamLength = length;
      })).pipe(cacheStream);
    });

    // When something goes wrong
    request.on('error', function(err) {
      // We log the problem
      console.log('ERROR: ', err);

      // We cancel the eventual event listeners
      cacheEntry.reject(err);

      // We remove the cache entry
      cache.delete(hashKey);

      // We remove the query file to save memory
      // You can disable this line if you want to keep it for debug purposes
      fs.unlink(tempPath);
    });

    // Read the query from the filesystem...
    var readStream = fs.createReadStream(tempPath);

    // ...and send it to Graftwerk
    readStream.pipe(request);
  });

  // When we have finished the POST query parsing
  bus.on('finish', () => {
    // We close the query file to save it
    // It will trigger the previous event block
    writeStream.end();

    // We compute the hash
    inputHash.end();
    hashKey = inputHash.read();
  });

  // We connect the query to the POST query parsing
  req.pipe(bus);

  // We write the query in a temporary file in the queries folder
  req.pipe(writeStream);
});

// Send a file from a cache entry
// We just use Express tools
var sendFile = (req, res, cacheEntry) => {

  // If the client ask for the status, we send the status
  // instead of the cache content
  if (req.query.status) {
    res.json(cacheEntry);
    return;
  }

  // We send the file with the same status than the one saved
  res.status(cacheEntry.statusCode);

  var headers = {
    'Content-Type': cacheEntry.contentType
  };

  // We put back the content-disposition header if necessary
  if (cacheEntry.contentDisposition) {
    headers['Content-Disposition'] = cacheEntry.contentDisposition;
  }

  // We send the file using express.js
  res.sendFile(cacheEntry.hash, {
    root: cacheFolder,
    maxAge: cacheMaxAgeHeader,
    headers
  }, (err) => {
    if (err) {
      console.log('SendFile error: ', err);

      // We close the client connection
      res.end();
    }
  });
};

/**
 * The client will fetch this endpoint once it received the hash.
 */
app.get('/graftermemcache/:hash', (req, res) => {
  var hashKey = req.params.hash;

  // If the hash is not found in the cache, it's a HTTP 404 not found error
  // This occurs when the client send a wrong hash, or if this component
  // has been restarted after the client received the hash
  // Another possibility is when this component is used behind a loadbalancer
  // and the hash has been sent by another instance. If used behind a loadbalancer,
  // a client should always contact the same instance.
  if (!cache.has(hashKey)) {
    res.status(404).end();
    return;
  }

  var cacheEntry = cache.get(hashKey);

  // If Graftwerk is currently processing the query
  if (cacheEntry.processing) {

    // We get a promise to send the result once the data is received and saved
    var promise = cacheEntry.promise;

    // We however have an internal timeout
    // After waiting the maximum waiting delay, the an empty response is sent to the
    // client. It should interpret it as the necessity to ask again.
    // As explained before, this is done like this to prevent HTTP timeouts that can
    // occurs.
    var timeoutId = setTimeout(function() {
      // Send the empty response with no cache because the status will change
      res.header('Cache-Control', 'no-cache, no-store');
      res.header('Pragma', 'no-cache');
      res.status(204).end();

      // Remove the timeoutID, also used to notify the promise callback that the
      // request has timeout.
      timeoutId = null;

      // Clear variables, it might save memory if a garbage collection occurs before the promise
      // callback. But I am not sure.
      req = null;
      res = null;
    }, maximumWaitingDelay);

    // When the result has been received and saved from Graftwerk
    promise.then(() => {
      // If a timeout hasn't occured yet
      if (timeoutId !== null) {

        // It's time to send the file
        sendFile(req, res, cacheEntry);

        // And to cancel the timeout
        clearTimeout(timeoutId);
      }
    },

    (error) => {
      // Sadly, errors may occur
      res.status(500).json(error);
      clearTimeout(timeoutId);
    });
  } else {
    // If the data is already in the cache and has already been computed
    // we just need to send it
    sendFile(req, res, cacheEntry);
  }

});

// Starting the HTTP server
app.listen(serverPort, () => {
  console.log('Grafter-memcache started on http://localhost:' + serverPort + '/');
  console.log('Graftwerk endpoint: http://' + graftwerkHostname + ':' + graftwerkPort);
});
<|endoftext|>


<commit_before>'use strict';
var turf = require('turf');
var _ = require('underscore');
var rbush = require('rbush');
var flatten = require('geojson-flatten');

module.exports = function(tileLayers, tile, writeData, done) {
  var layer = tileLayers.osm.osm;
  var bboxLayer = turf.bboxPolygon(turf.extent(layer));
  bboxLayer.geometry.type = 'LineString';
  bboxLayer.geometry.coordinates = bboxLayer.geometry.coordinates[0];
  var bufferLayer = turf.buffer(bboxLayer, 0.01, 'miles').features[0];
  var preserveType = {
    //major
    'motorway': true,
    'trunk': true,
    'primary': true,
    'secondary': true,
    'tertiary': true,
    'unclassified': true,
    'residential': true,
    'service': true,
    'motorway_link': true,
    'trunk_link': true,
    'primary_link': true,
    'secondary_link': true,
    'tertiary_link': true,
    'living_street': true,
    'pedestrian': true,
    'road': true,
    //minor
    'track': true,
    'footway': true,
    'path': true
  };
  var unit = 'meters';
  var distance = 5;
  var highways = {};
  var bboxes = [];
  var output = {};
  var osmlint = 'nodeendingnearhighway';

  for (var i = 0; i < layer.features.length; i++) {
    var val = layer.features[i];
    // Linestring evaluation
    if (val.geometry.type === 'LineString' && preserveType[val.properties.highway] && val.properties.layer === undefined) {
      var bboxL = turf.extent(val);
      bboxL.push(val.properties._osm_way_id + 'L');
      bboxes.push(bboxL);
      highways[val.properties._osm_way_id + 'L'] = {
        highway: val,
        buffer: turf.buffer(val, distance, unit).features[0]
      };
    } else if (val.geometry.type === 'MultiLineString' && preserveType[val.properties.highway] && val.properties.layer === undefined) { //MultiLineString evaluation
      var flat = flatten(val);
      var id = val.properties._osm_way_id + 'L';
      for (var f = 0; f < flat.length; f++) {
        if (flat[f].geometry.type === 'LineString') {
          var bboxM = turf.extent(flat[f]);
          var idFlat = id + 'M' + f;
          bboxM.push(idFlat);
          bboxes.push(bboxM);
          highways[idFlat] = {
            highway: flat[f],
            buffer: turf.buffer(flat[f], distance, unit).features[0]
          };
        }
      }
    }
  }

  var highwaysTree = rbush(bboxes.length);
  highwaysTree.load(bboxes);

  for (var m = 0; m < bboxes.length; m++) {
    var valueBbox = bboxes[m];
    //obtaining first and last coordinates
    var valueHighway = highways[valueBbox[4]].highway;
    var firstCoord = valueHighway.geometry.coordinates[0];
    var firstPoint = turf.point(firstCoord);
    var endCoord = valueHighway.geometry.coordinates[valueHighway.geometry.coordinates.length - 1];
    var endPoint = turf.point(endCoord);
    var overlapsFirstPoint = [];
    if (!turf.inside(firstPoint, bufferLayer)) {
      overlapsFirstPoint = highwaysTree.search(turf.extent(turf.buffer(firstPoint, distance, unit)));
    }
    var overlapsEndPoint = [];
    if (!turf.inside(endPoint, bufferLayer)) {
      overlapsEndPoint = highwaysTree.search(turf.extent(turf.buffer(endPoint, distance, unit)));
    }
    var overlapBboxes = overlapsFirstPoint.concat(overlapsEndPoint);
    if (!_.isEqual(firstCoord, endCoord)) {
      var arrayCorrd = [];
      for (var j = 0; j < overlapBboxes.length; j++) {
        var overlapBbox = overlapBboxes[j];
        if (valueBbox[4] !== overlapBbox[4]) {
          arrayCorrd = arrayCorrd.concat(_.flatten(highways[overlapBbox[4]].highway.geometry.coordinates));
        }
      }

      var type;
      if (valueHighway.properties.highway === 'track' || valueHighway.properties.highway === 'footway' || valueHighway.properties.highway === 'path') {
        type = 'minor';
      } else {
        type = 'major';
      }
      var props = {
        wayA: valueHighway.properties._osm_way_id,
        _osmlint: osmlint,
        type: type
      };
      valueHighway.properties.type = type;
      for (var k = 0; k < overlapsFirstPoint.length; k++) {
        var overlapPointFirst = overlapsFirstPoint[k];
        var overlapHighwayF = highways[overlapPointFirst[4]].highway;

        if (valueBbox[4] !== overlapPointFirst[4] && (arrayCorrd.indexOf(firstCoord[0]) === -1 || arrayCorrd.indexOf(firstCoord[1]) === -1)) {
          if (turf.inside(firstPoint, highways[overlapPointFirst[4]].buffer)) {
            props.wayB = overlapHighwayF.properties._osm_way_id;
            firstPoint.properties = props;
            //Check out whether the streets are connected at some point
            var coordinatesF = valueHighway.geometry.coordinates;
            var valueCoorF = _.flatten([coordinatesF[1], coordinatesF[2]]);
            var overlapCoorF = _.flatten(overlapHighwayF.geometry.coordinates);
            if (_.intersection(valueCoorF, overlapCoorF).length < 2) {
              output[valueBbox[4]] = valueHighway;
              output[valueBbox[4]].properties._osmlint = osmlint;
              output[overlapPointFirst[4]] = overlapHighwayF;
              output[overlapPointFirst[4]].properties._osmlint = osmlint;
              if (valueHighway.properties._osm_way_id > overlapHighwayF.properties._osm_way_id) {
                output[valueBbox[4].toString().concat(overlapPointFirst[4])] = firstPoint;
              } else {
                output[overlapPointFirst[4].toString().concat(valueBbox[4])] = firstPoint;
              }
            }
          }
        }
      }
      for (var l = 0; l < overlapsEndPoint.length; l++) {
        var overlapPointEnd = overlapsEndPoint[l];
        var overlapHighwayE = highways[overlapPointEnd[4]].highway;
        if (valueBbox[4] !== overlapPointEnd[4] && (arrayCorrd.indexOf(endCoord[0]) === -1 || arrayCorrd.indexOf(endCoord[1]) === -1)) {
          if (turf.inside(endPoint, highways[overlapPointEnd[4]].buffer)) {
            props.wayB = overlapHighwayE.properties._osm_way_id;
            endPoint.properties = props;
            //Check out whether the streets are connected at some point
            var coordinatesE = valueHighway.geometry.coordinates;
            var valueCoorE = _.flatten([coordinatesE[coordinatesE.length - 1], coordinatesE[coordinatesE.length - 2]]);
            var overlapCoorE = _.flatten(overlapHighwayE.geometry.coordinates);
            if (_.intersection(valueCoorE, overlapCoorE).length < 2) {
              output[valueBbox[4]] = valueHighway;
              output[valueBbox[4]].properties._osmlint = osmlint;
              output[overlapPointEnd[4]] = overlapHighwayE;
              output[overlapPointEnd[4]].properties._osmlint = osmlint;
              if (valueHighway.properties._osm_way_id > overlapHighwayE.properties._osm_way_id) {
                output[valueBbox[4].toString().concat(overlapPointEnd[4])] = endPoint;
              } else {
                output[overlapPointEnd[4].toString().concat(valueBbox[4])] = endPoint;
              }
            }
          }
        }
      }
    }
  }

  var result = _.values(output);

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

  done(null, null);

};
<commit_msg>update nimor roads<commit_after>'use strict';
var turf = require('turf');
var _ = require('underscore');
var rbush = require('rbush');
var flatten = require('geojson-flatten');

module.exports = function(tileLayers, tile, writeData, done) {
  var layer = tileLayers.osm.osm;
  var bboxLayer = turf.bboxPolygon(turf.extent(layer));
  bboxLayer.geometry.type = 'LineString';
  bboxLayer.geometry.coordinates = bboxLayer.geometry.coordinates[0];
  var bufferLayer = turf.buffer(bboxLayer, 0.01, 'miles').features[0];
  var preserveType = {
    //major
    'motorway': true,
    'trunk': true,
    'primary': true,
    'secondary': true,
    'tertiary': true,
    'unclassified': true,
    'residential': true,
    'motorway_link': true,
    'trunk_link': true,
    'primary_link': true,
    'secondary_link': true,
    'tertiary_link': true,
    'living_street': true,
    'pedestrian': true,
    'road': true,
    //minor
    'service': true,
    'track': true,
    'footway': true,
    'path': true

  };
  var unit = 'meters';
  var distance = 5;
  var highways = {};
  var bboxes = [];
  var output = {};
  var osmlint = 'nodeendingnearhighway';

  for (var i = 0; i < layer.features.length; i++) {
    var val = layer.features[i];
    // Linestring evaluation
    if (val.geometry.type === 'LineString' && preserveType[val.properties.highway]) {
      var bboxL = turf.extent(val);
      bboxL.push(val.properties._osm_way_id + 'L');
      bboxes.push(bboxL);
      highways[val.properties._osm_way_id + 'L'] = {
        highway: val,
        buffer: turf.buffer(val, distance, unit).features[0]
      };
    } else if (val.geometry.type === 'MultiLineString' && preserveType[val.properties.highway]) { //MultiLineString evaluation
      var flat = flatten(val);
      var id = val.properties._osm_way_id + 'L';
      for (var f = 0; f < flat.length; f++) {
        if (flat[f].geometry.type === 'LineString') {
          var bboxM = turf.extent(flat[f]);
          var idFlat = id + 'M' + f;
          bboxM.push(idFlat);
          bboxes.push(bboxM);
          highways[idFlat] = {
            highway: flat[f],
            buffer: turf.buffer(flat[f], distance, unit).features[0]
          };
        }
      }
    }
  }

  var highwaysTree = rbush(bboxes.length);
  highwaysTree.load(bboxes);

  for (var m = 0; m < bboxes.length; m++) {
    var valueBbox = bboxes[m];
    //obtaining first and last coordinates
    var valueHighway = highways[valueBbox[4]].highway;
    var firstCoord = valueHighway.geometry.coordinates[0];
    var firstPoint = turf.point(firstCoord);
    var endCoord = valueHighway.geometry.coordinates[valueHighway.geometry.coordinates.length - 1];
    var endPoint = turf.point(endCoord);
    var overlapsFirstPoint = [];
    if (!turf.inside(firstPoint, bufferLayer)) {
      overlapsFirstPoint = highwaysTree.search(turf.extent(turf.buffer(firstPoint, distance, unit)));
    }
    var overlapsEndPoint = [];
    if (!turf.inside(endPoint, bufferLayer)) {
      overlapsEndPoint = highwaysTree.search(turf.extent(turf.buffer(endPoint, distance, unit)));
    }
    var overlapBboxes = overlapsFirstPoint.concat(overlapsEndPoint);
    if (!_.isEqual(firstCoord, endCoord)) {
      var arrayCorrd = [];
      for (var j = 0; j < overlapBboxes.length; j++) {
        var overlapBbox = overlapBboxes[j];
        if (valueBbox[4] !== overlapBbox[4]) {
          arrayCorrd = arrayCorrd.concat(_.flatten(highways[overlapBbox[4]].highway.geometry.coordinates));
        }
      }

      var type;
      if (valueHighway.properties.highway === 'service' || valueHighway.properties.highway === 'track' || valueHighway.properties.highway === 'footway' || valueHighway.properties.highway === 'path') {
        type = 'minor';
      } else {
        type = 'major';
      }
      var props = {
        wayA: valueHighway.properties._osm_way_id,
        _osmlint: osmlint,
        type: type
      };
      valueHighway.properties.type = type;
      for (var k = 0; k < overlapsFirstPoint.length; k++) {
        var overlapPointFirst = overlapsFirstPoint[k];
        var overlapHighwayF = highways[overlapPointFirst[4]].highway;

        if (valueBbox[4] !== overlapPointFirst[4] && (arrayCorrd.indexOf(firstCoord[0]) === -1 || arrayCorrd.indexOf(firstCoord[1]) === -1)) {
          if (turf.inside(firstPoint, highways[overlapPointFirst[4]].buffer)) {
            props.wayB = overlapHighwayF.properties._osm_way_id;
            firstPoint.properties = props;
            //Check out whether the streets are connected at some point
            var coordinatesF = valueHighway.geometry.coordinates;
            var valueCoorF = _.flatten([coordinatesF[1], coordinatesF[2]]);
            var overlapCoorF = _.flatten(overlapHighwayF.geometry.coordinates);
            if (_.intersection(valueCoorF, overlapCoorF).length < 2) {
              output[valueBbox[4]] = valueHighway;
              output[valueBbox[4]].properties._osmlint = osmlint;
              output[overlapPointFirst[4]] = overlapHighwayF;
              output[overlapPointFirst[4]].properties._osmlint = osmlint;
              if (valueHighway.properties._osm_way_id > overlapHighwayF.properties._osm_way_id) {
                output[valueBbox[4].toString().concat(overlapPointFirst[4])] = firstPoint;
              } else {
                output[overlapPointFirst[4].toString().concat(valueBbox[4])] = firstPoint;
              }
            }
          }
        }
      }
      for (var l = 0; l < overlapsEndPoint.length; l++) {
        var overlapPointEnd = overlapsEndPoint[l];
        var overlapHighwayE = highways[overlapPointEnd[4]].highway;
        if (valueBbox[4] !== overlapPointEnd[4] && (arrayCorrd.indexOf(endCoord[0]) === -1 || arrayCorrd.indexOf(endCoord[1]) === -1)) {
          if (turf.inside(endPoint, highways[overlapPointEnd[4]].buffer)) {
            props.wayB = overlapHighwayE.properties._osm_way_id;
            endPoint.properties = props;
            //Check out whether the streets are connected at some point
            var coordinatesE = valueHighway.geometry.coordinates;
            var valueCoorE = _.flatten([coordinatesE[coordinatesE.length - 1], coordinatesE[coordinatesE.length - 2]]);
            var overlapCoorE = _.flatten(overlapHighwayE.geometry.coordinates);
            if (_.intersection(valueCoorE, overlapCoorE).length < 2) {
              output[valueBbox[4]] = valueHighway;
              output[valueBbox[4]].properties._osmlint = osmlint;
              output[overlapPointEnd[4]] = overlapHighwayE;
              output[overlapPointEnd[4]].properties._osmlint = osmlint;
              if (valueHighway.properties._osm_way_id > overlapHighwayE.properties._osm_way_id) {
                output[valueBbox[4].toString().concat(overlapPointEnd[4])] = endPoint;
              } else {
                output[overlapPointEnd[4].toString().concat(valueBbox[4])] = endPoint;
              }
            }
          }
        }
      }
    }
  }

  var result = _.values(output);

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

  done(null, null);

};<|endoftext|>


<commit_before>exports.productionVersionId = "ab8db7709f2e";<commit_msg>Incrementing Production Version.<commit_after>exports.productionVersionId = "d5fedf04149e";<|endoftext|>


<commit_before>/* TODO:
 * when video moves it loses title
 * pause / play
 * spin
 * fast
 * free photo API that auto embeded first search result??
 * Add sort bot
 * bump
 * bot responses
 */

/*
 * Globals
 */
var admin = "geoffkeighley";
// Note: must add mod to channel first
var mods = ["Fitzthistlewits"];

var isMod = function(usr) {
  return mods.indexOf(usr) > -1 || usr === admin;
};

var colourMap = {
  "sadweeaboo2" : "red",
  "RyanGoslingTwerk" : "#ff66cc"
};

var msgColours = {
  "success": "#0f8c1f",
  "error": "red",
  "general": "yellow",
};

var addVideoTitleToTarget = function(targ) {
  if (targ.querySelector('.qe_title') !== null && targ.title !== undefined && targ.querySelector('.qe_title').innerText.length > 0 && !targ.className.includes('ui-')) {
    var videoTitle = targ.querySelector('.qe_title').innerText;
    if (!videoTitle.includes(targ.title)) {
      targ.querySelector('.qe_title').innerText += " " + targ.title;
    }
  }
};

var addBotMsg = function(msg, colour) {
  var element = document.createElement('div');
  element.classList.add('server-msg-reconnect'); // Spoofs a server message
  element.style.color = colour;
  element.style.border = "1px solid " + colour;
  element.innerText = msg;
  document.querySelector('#messagebuffer').appendChild(element);
  document.querySelector('#messagebuffer').scrollTop = document.querySelector('#messagebuffer').scrollHeight;
};

var addNameToTarget = function(targ) {
  var username = targ.className.substring("chat-msg-".length, targ.className.length);
  if (targ.childNodes.length < 3 && targ.className.indexOf("server-msg") < 0 && targ.className.indexOf("\\$server\\$") < 0) {
    var span = document.createElement("SPAN");
    span.innerHTML = "" + username + ": ";

    if (username === admin) {
      span.className += "userlist_owner";
    } else if (isMod(username)) {
      span.className += "userlist_op";
    }

    targ.insertBefore(span, targ.childNodes[1]);
  }

  if (colourMap[username] !== undefined) {
    targ.childNodes[1].style.color = colourMap[username];
  }
};

var sendMsg = function(msg) {
  document.querySelector('#chatline').value = msg;
  // Due to a bug outside of my control, this is how a enter press must be dispatched
  var enterEvent = new Event('keydown');
  enterEvent.keyCode = 13;
  document.querySelector('#chatline').dispatchEvent(enterEvent);
};

// isInit (optional) -> defaults to false
//                   -> true: will ignore certain checks (such as 'roll)
var checkForOptions = function(targ, isInit) {
  if (isInit === undefined) isInit = false;

  // Check vanilla message
  if (targ.childNodes[2] !== undefined) {
    var msg = targ.childNodes[2].innerText;
    var username = targ.className.substring(("chat-msg-").length, targ.className.length);

    var w = document.querySelector('#welcome');
    var scriptUser = (w === null) ? undefined : w.innerText.substring(w.innerText.indexOf(',')+2);

    // Cannot be init as this will create a feedback loop
    if (msg.indexOf("'roll") === 0) {
      if (username === scriptUser && !isInit) {
        var maxRoll = msg.substr("'roll ".length);
        console.log("Roll: " + parseInt(maxRoll));
        if (!isNaN(maxRoll) && maxRoll >= 1 && maxRoll <= 10000) {
          maxRoll = Math.trunc(maxRoll);
          var roll = Math.random() * (maxRoll - 1) + 1;
          roll = Math.round(roll);
          targ.remove();
          addBotMsg(username + " rolled a " + roll + " on a d" + maxRoll, msgColours.success);
          sendMsg(JSON.stringify({'roll': roll, 'maxRoll': maxRoll}));
        }
      } else {
        targ.remove();
      }
    } else if (msg.indexOf("'lead") === 0 && isMod(username)) {
      var userList = document.querySelector('#userlist');
      if (!isInit && scriptUser === username) {
        for (let i = 0; i < userList.childNodes.length; i++) {
          if (userList.childNodes[i].innerText === username) {
            var btnClicked = userList.childNodes[i].querySelectorAll('button')[1].innerText;
            userList.childNodes[i].querySelectorAll('button')[1].click();
            var m = (btnClicked.includes("Give")) ? "has taken lead" : "has given up lead";
            sendMsg(JSON.stringify({msg: username + " " + m, colour: msgColours.general}));
            break;
          }
        }
      }

      targ.remove();
    } else if (msg.indexOf("'skip") === 0 && isMod(username)) {
      var videoList = document.querySelector('#queue');
      if (!isInit && scriptUser === username && videoList.childNodes.length > 1) {
        var nextVideo = document.querySelector('.queue_active').nextSibling;
        if (nextVideo === null) {
          nextVideo = document.querySelector('#queue').childNodes[0];
        }

        var btnClicked = nextVideo.querySelector('.qbtn-play');
        // Possible buttons are hidden
        if (btnClicked !== null) {
          btnClicked.click();
          sendMsg(JSON.stringify({msg: username + " skipped video", colour: msgColours.general}));
        } else {
          sendMsg(JSON.stringify({msg: username + " failed to skip - are buttons hidden?", colour: msgColours.failed}));
        }
      }

      targ.remove();
    } else if (msg.indexOf("'img") === 0) {
      var url = msg.substr("'img".length);

      var buffer = document.querySelector('#messagebuffer');

      var image = new Image();
      image.onload = function() { buffer.scrollTop = buffer.scrollHeight; }
      image.src = url;

      var imgElement = document.createElement('img');
      imgElement.src = image.src;
      imgElement.style["max-width"] = "350px";
      imgElement.style["max-height"] = "200px";
      imgElement.style.cursor = "zoom-in";
      imgElement.classList.add('user-img');
      targ.appendChild(imgElement);
      targ.childNodes[2].remove();

      var imgDiv = document.createElement('div');
      imgDiv.style.position = "fixed";
      imgDiv.style.top = 0;
      imgDiv.style.left = 0;
      imgDiv.style.width = document.body.offsetWidth + "px";
      imgDiv.style.height = document.body.offsetHeight + "px";
      imgDiv.style["background-color"] = "rgba(23, 23, 23, 0.75)";
      imgDiv.style["z-index"] = 2000; // Has to be above the fixed header
      imgDiv.style.visibility = "hidden";
      imgDiv.style.cursor = "zoom-out";
      var imgPreview = document.createElement('img');
      imgPreview.src = image.src;
      imgPreview.style.visibility = "hidden";
      imgPreview.style.position = "fixed";
      imgPreview.style.top = "0px";
      imgPreview.style.left = "0px";
      imgPreview.style["z-index"] = 25;
      imgPreview.style.cursor = "zoom-out";

      imgDiv.appendChild(imgPreview);
      targ.appendChild(imgDiv);

      var clickShow = function(e) {
        // Must be called here as offsetWidth/Height may change
        imgDiv.style.width = window.innerWidth + "px";
        imgDiv.style.height = window.innerHeight + "px";
        imgDiv.style.visibility = "visible";

        imgPreview.style["max-width"] = window.innerWidth*0.8 + "px";
        imgPreview.style["max-height"] = window.innerHeight*0.98 + "px";
        imgPreview.style.top = ((window.innerHeight - imgPreview.height) / 2) + "px";
        imgPreview.style.left = ((window.innerWidth - imgPreview.width) / 2) + "px";
        imgPreview.style.visibility = "visible";
      };

      var clickHide = function(e) {
        imgPreview.style.visibility = "hidden";
        imgDiv.style.visibility = "hidden";
      };

      imgElement.addEventListener('click', clickShow);

      imgDiv.addEventListener('click',  clickHide);
      imgPreview.addEventListener('click',  clickHide);

      window.onresize = function() {
        imgDiv.style.width = window.innerWidth + "px";
        imgDiv.style.height = window.innerHeight + "px";

        imgPreview.style["max-width"] = window.innerWidth*0.8 + "px";
        imgPreview.style["max-height"] = window.innerHeight*0.98 + "px";
        imgPreview.style.top = ((window.innerHeight - imgPreview.height) / 2) + "px";
        imgPreview.style.left = ((window.innerWidth - imgPreview.width) / 2) + "px";
      };
    }

    // Returns an object with the JSON and the process type
    // or undefined if not valid JSON
    var validateJSON = function(j) {
      try {
        var json = JSON.parse(j);
      } catch (error) {
        return undefined;
      }

      json = {'json': json, 'type': null};

      if (json.json.roll !== undefined && json.json.maxRoll !== undefined) {
        json.type = 'roll';
      } else if (json.json.msg !== undefined && json.json.colour !== undefined) {
        json.type = 'msg';
      }

      return json;
    };

    var json = validateJSON(msg);
    if (json !== undefined && json.type !== null) {
      if (!isInit) {
        if (json.type === "roll") {
          if (username !== scriptUser) {
            addBotMsg(username + " rolled a " + json.json.roll + " on a d" + json.json.maxRoll, msgColours.success);
          }

          targ.remove();
        } else if (json.type === 'msg') {
          addBotMsg(json.json.msg, json.json.colour);
          targ.remove();
        }
      } else {
        targ.remove();
      }
    }
  }
};

var setUserColour = function(targ) {
  if (colourMap[targ.childNodes[1].innerText] !== undefined) {
    targ.childNodes[1].style.color = colourMap[targ.childNodes[1].innerText];
    targ.childNodes[1].className = "";
  }
};

var main = function() {
  var videoTitleTarget = document.getElementById('queue');
  var msgTarget = document.getElementById("messagebuffer");
  var usrTarget = document.getElementById("userlist");

  var videoList = videoTitleTarget.getElementsByTagName("li");
  for (var i = 0; i < videoList.length; i++) {
    addVideoTitleToTarget(videoList[i]);
  }

  var msgList = msgTarget.getElementsByTagName("div");
  for (var i = 0; i < msgList.length; i++) {
    addNameToTarget(msgList[i]);
  }

  var userList = usrTarget.getElementsByClassName("userlist_item");
  for (var i = 0; i < userList.length; i++) {
    setUserColour(userList[i]);
  }

  // create an observer instance
  var videoTitleObserver = new MutationObserver(function(mutations) {
    // Which mutation is used is not important
    // var videoItem = mutations[0].target.getElementsByTagName("li");

    // for (var i = 0; i < mutations.length; i++) {
      // Modifies the last x (mutations.length) video titles
      // addVideoTitleToTarget(videoItem[videoItem.length-1-i]);
    // }

    var videos = document.querySelectorAll('.queue_entry');
    for (let i = 0; i < videos.length; i++) {
      addVideoTitleToTarget(videos[i]);
    }
  });

  var nameMsgObserver = new MutationObserver(function(mutations) {
    var msgItem = mutations[0].target.getElementsByTagName("div");
    for (var i = 0; i < mutations.length; i++) {
      // Modifies the last x (mutations.length)
      // Visual / DOM changes
      addNameToTarget(msgItem[msgItem.length-1-i]);
      // Functionality changes
      checkForOptions(msgItem[msgItem.length-1-i]);
    }
  });

  var usrListObserver = new MutationObserver(function(mutations) {
    var usrItem = mutations[0].target.getElementsByClassName("userlist_item");
    usrItem = usrItem[usrItem.length-1];
    setUserColour(usrItem);
  });

  // configuration of the observer:
  var videoTitleConfig = {childList: true};
  var nameMsgConfig = {childList : true};
  var usrListConfig = {childList : true};

  // Check all messages in history
  // May delete some children, check for this
  for (let i = 0; i < msgTarget.childNodes.length;) {
    var len = msgTarget.childNodes.length;
    checkForOptions(msgTarget.childNodes[i], true);
    if (msgTarget.childNodes.length === len) {
      i += 1;
    }
  }

  // pass in the target node, as well as the observer options
  videoTitleObserver.observe(videoTitleTarget, videoTitleConfig);
  nameMsgObserver.observe(msgTarget, nameMsgConfig);
  usrListObserver.observe(usrTarget, usrListConfig);

  // Remove old test area
  if (document.querySelector('.test-area') !== null) {
    document.querySelector('.test-area').remove();
  }

  var testArea = document.createElement('div');
  testArea.classList.add('test-area');
  testArea.innerText = "Test Area. DO NOT MOUSE OVER.";
  var testImg = document.createElement('img');
  testImg.src = "https://cdn.instructables.com/FB2/DLT8/I86UGV9U/FB2DLT8I86UGV9U.MEDIUM.gif";
  testImg.style.visibility = "hidden";
  testImg.style["z-index"] = 100;

  testArea.addEventListener('mouseenter', function(e) {
    testImg.style.visibility = "visible";
    testArea.innerText = "";
    testArea.appendChild(testImg);
    testImg.style.width = testArea.offsetWidth + "px";
    testImg.style.height = testArea.offsetHeight + "px";
    testArea.style.border = "none";
  });

  testImg.addEventListener('mouseout', function(e) {
    testImg.style.visibility = "hidden";
    testArea.innerText = "Test Area. DO NOT MOUSE OVER.";
    testArea.style.border = "2px dotted white";
  });

  document.querySelector('#leftpane').appendChild(testArea);

  // Removed for now
  // var bannerDiv = document.createElement('div');
  // bannerDiv.classList.add('banner');
  // var bannerImg = document.createElement('img');
  // bannerImg.src = "http://i.imgur.com/b2O7hQq.png";
  // bannerDiv.appendChild(bannerImg);
  // document.querySelector('.container-fluid').insertBefore(bannerDiv, document.querySelector('#announcements'));

  // Add any custom CSS
  // Used over cytube CSS editor so you only have to update one file
  var css = `.user-img:hover {
    filter: grayscale(40%);
  }
  .test-area {
    width: 100%;
    height: 115px;
    border: 2px dotted white;
    text-align: center;
    font-size: 24px;
  }
  .banner {
    width: 100%;
    text-align: center;
    margin-bottom: 5px;
  }
  .banner img {
    width: 100%;
    height: 180px;
  }`;

  var cssTag = document.createElement('style');
  if (cssTag.styleSheet) {
    cssTag.styleSheet.cssText = css;
  } else {
    cssTag.appendChild(document.createTextNode(css));
  }

  document.querySelector('head').appendChild(cssTag);
}();
<commit_msg>Reverted video list observer code - fix still in place<commit_after>/* TODO:
 * when video moves it loses title
 * pause / play
 * spin
 * fast
 * free photo API that auto embeded first search result??
 * Add sort bot
 * bump
 * bot responses
 */

/*
 * Globals
 */
var admin = "geoffkeighley";
// Note: must add mod to channel first
var mods = ["Fitzthistlewits"];

var isMod = function(usr) {
  return mods.indexOf(usr) > -1 || usr === admin;
};

var colourMap = {
  "sadweeaboo2" : "red",
  "RyanGoslingTwerk" : "#ff66cc"
};

var msgColours = {
  "success": "#0f8c1f",
  "error": "red",
  "general": "yellow",
};

var addVideoTitleToTarget = function(targ) {
  if (targ.querySelector('.qe_title') !== null && targ.title !== undefined && targ.querySelector('.qe_title').innerText.length > 0 && !targ.className.includes('ui-')) {
    var videoTitle = targ.querySelector('.qe_title').innerText;
    if (!videoTitle.includes(targ.title)) {
      targ.querySelector('.qe_title').innerText += " " + targ.title;
    }
  }
};

var addBotMsg = function(msg, colour) {
  var element = document.createElement('div');
  element.classList.add('server-msg-reconnect'); // Spoofs a server message
  element.style.color = colour;
  element.style.border = "1px solid " + colour;
  element.innerText = msg;
  document.querySelector('#messagebuffer').appendChild(element);
  document.querySelector('#messagebuffer').scrollTop = document.querySelector('#messagebuffer').scrollHeight;
};

var addNameToTarget = function(targ) {
  var username = targ.className.substring("chat-msg-".length, targ.className.length);
  if (targ.childNodes.length < 3 && targ.className.indexOf("server-msg") < 0 && targ.className.indexOf("\\$server\\$") < 0) {
    var span = document.createElement("SPAN");
    span.innerHTML = "" + username + ": ";

    if (username === admin) {
      span.className += "userlist_owner";
    } else if (isMod(username)) {
      span.className += "userlist_op";
    }

    targ.insertBefore(span, targ.childNodes[1]);
  }

  if (colourMap[username] !== undefined) {
    targ.childNodes[1].style.color = colourMap[username];
  }
};

var sendMsg = function(msg) {
  document.querySelector('#chatline').value = msg;
  // Due to a bug outside of my control, this is how a enter press must be dispatched
  var enterEvent = new Event('keydown');
  enterEvent.keyCode = 13;
  document.querySelector('#chatline').dispatchEvent(enterEvent);
};

// isInit (optional) -> defaults to false
//                   -> true: will ignore certain checks (such as 'roll)
var checkForOptions = function(targ, isInit) {
  if (isInit === undefined) isInit = false;

  // Check vanilla message
  if (targ.childNodes[2] !== undefined) {
    var msg = targ.childNodes[2].innerText;
    var username = targ.className.substring(("chat-msg-").length, targ.className.length);

    var w = document.querySelector('#welcome');
    var scriptUser = (w === null) ? undefined : w.innerText.substring(w.innerText.indexOf(',')+2);

    // Cannot be init as this will create a feedback loop
    if (msg.indexOf("'roll") === 0) {
      if (username === scriptUser && !isInit) {
        var maxRoll = msg.substr("'roll ".length);
        console.log("Roll: " + parseInt(maxRoll));
        if (!isNaN(maxRoll) && maxRoll >= 1 && maxRoll <= 10000) {
          maxRoll = Math.trunc(maxRoll);
          var roll = Math.random() * (maxRoll - 1) + 1;
          roll = Math.round(roll);
          targ.remove();
          addBotMsg(username + " rolled a " + roll + " on a d" + maxRoll, msgColours.success);
          sendMsg(JSON.stringify({'roll': roll, 'maxRoll': maxRoll}));
        }
      } else {
        targ.remove();
      }
    } else if (msg.indexOf("'lead") === 0 && isMod(username)) {
      var userList = document.querySelector('#userlist');
      if (!isInit && scriptUser === username) {
        for (let i = 0; i < userList.childNodes.length; i++) {
          if (userList.childNodes[i].innerText === username) {
            var btnClicked = userList.childNodes[i].querySelectorAll('button')[1].innerText;
            userList.childNodes[i].querySelectorAll('button')[1].click();
            var m = (btnClicked.includes("Give")) ? "has taken lead" : "has given up lead";
            sendMsg(JSON.stringify({msg: username + " " + m, colour: msgColours.general}));
            break;
          }
        }
      }

      targ.remove();
    } else if (msg.indexOf("'skip") === 0 && isMod(username)) {
      var videoList = document.querySelector('#queue');
      if (!isInit && scriptUser === username && videoList.childNodes.length > 1) {
        var nextVideo = document.querySelector('.queue_active').nextSibling;
        if (nextVideo === null) {
          nextVideo = document.querySelector('#queue').childNodes[0];
        }

        var btnClicked = nextVideo.querySelector('.qbtn-play');
        // Possible buttons are hidden
        if (btnClicked !== null) {
          btnClicked.click();
          sendMsg(JSON.stringify({msg: username + " skipped video", colour: msgColours.general}));
        } else {
          sendMsg(JSON.stringify({msg: username + " failed to skip - are buttons hidden?", colour: msgColours.failed}));
        }
      }

      targ.remove();
    } else if (msg.indexOf("'img") === 0) {
      var url = msg.substr("'img".length);

      var buffer = document.querySelector('#messagebuffer');

      var image = new Image();
      image.onload = function() { buffer.scrollTop = buffer.scrollHeight; }
      image.src = url;

      var imgElement = document.createElement('img');
      imgElement.src = image.src;
      imgElement.style["max-width"] = "350px";
      imgElement.style["max-height"] = "200px";
      imgElement.style.cursor = "zoom-in";
      imgElement.classList.add('user-img');
      targ.appendChild(imgElement);
      targ.childNodes[2].remove();

      var imgDiv = document.createElement('div');
      imgDiv.style.position = "fixed";
      imgDiv.style.top = 0;
      imgDiv.style.left = 0;
      imgDiv.style.width = document.body.offsetWidth + "px";
      imgDiv.style.height = document.body.offsetHeight + "px";
      imgDiv.style["background-color"] = "rgba(23, 23, 23, 0.75)";
      imgDiv.style["z-index"] = 2000; // Has to be above the fixed header
      imgDiv.style.visibility = "hidden";
      imgDiv.style.cursor = "zoom-out";
      var imgPreview = document.createElement('img');
      imgPreview.src = image.src;
      imgPreview.style.visibility = "hidden";
      imgPreview.style.position = "fixed";
      imgPreview.style.top = "0px";
      imgPreview.style.left = "0px";
      imgPreview.style["z-index"] = 25;
      imgPreview.style.cursor = "zoom-out";

      imgDiv.appendChild(imgPreview);
      targ.appendChild(imgDiv);

      var clickShow = function(e) {
        // Must be called here as offsetWidth/Height may change
        imgDiv.style.width = window.innerWidth + "px";
        imgDiv.style.height = window.innerHeight + "px";
        imgDiv.style.visibility = "visible";

        imgPreview.style["max-width"] = window.innerWidth*0.8 + "px";
        imgPreview.style["max-height"] = window.innerHeight*0.98 + "px";
        imgPreview.style.top = ((window.innerHeight - imgPreview.height) / 2) + "px";
        imgPreview.style.left = ((window.innerWidth - imgPreview.width) / 2) + "px";
        imgPreview.style.visibility = "visible";
      };

      var clickHide = function(e) {
        imgPreview.style.visibility = "hidden";
        imgDiv.style.visibility = "hidden";
      };

      imgElement.addEventListener('click', clickShow);

      imgDiv.addEventListener('click',  clickHide);
      imgPreview.addEventListener('click',  clickHide);

      window.onresize = function() {
        imgDiv.style.width = window.innerWidth + "px";
        imgDiv.style.height = window.innerHeight + "px";

        imgPreview.style["max-width"] = window.innerWidth*0.8 + "px";
        imgPreview.style["max-height"] = window.innerHeight*0.98 + "px";
        imgPreview.style.top = ((window.innerHeight - imgPreview.height) / 2) + "px";
        imgPreview.style.left = ((window.innerWidth - imgPreview.width) / 2) + "px";
      };
    }

    // Returns an object with the JSON and the process type
    // or undefined if not valid JSON
    var validateJSON = function(j) {
      try {
        var json = JSON.parse(j);
      } catch (error) {
        return undefined;
      }

      json = {'json': json, 'type': null};

      if (json.json.roll !== undefined && json.json.maxRoll !== undefined) {
        json.type = 'roll';
      } else if (json.json.msg !== undefined && json.json.colour !== undefined) {
        json.type = 'msg';
      }

      return json;
    };

    var json = validateJSON(msg);
    if (json !== undefined && json.type !== null) {
      if (!isInit) {
        if (json.type === "roll") {
          if (username !== scriptUser) {
            addBotMsg(username + " rolled a " + json.json.roll + " on a d" + json.json.maxRoll, msgColours.success);
          }

          targ.remove();
        } else if (json.type === 'msg') {
          addBotMsg(json.json.msg, json.json.colour);
          targ.remove();
        }
      } else {
        targ.remove();
      }
    }
  }
};

var setUserColour = function(targ) {
  if (colourMap[targ.childNodes[1].innerText] !== undefined) {
    targ.childNodes[1].style.color = colourMap[targ.childNodes[1].innerText];
    targ.childNodes[1].className = "";
  }
};

var main = function() {
  var videoTitleTarget = document.getElementById('queue');
  var msgTarget = document.getElementById("messagebuffer");
  var usrTarget = document.getElementById("userlist");

  var videoList = videoTitleTarget.getElementsByTagName("li");
  for (var i = 0; i < videoList.length; i++) {
    addVideoTitleToTarget(videoList[i]);
  }

  var msgList = msgTarget.getElementsByTagName("div");
  for (var i = 0; i < msgList.length; i++) {
    addNameToTarget(msgList[i]);
  }

  var userList = usrTarget.getElementsByClassName("userlist_item");
  for (var i = 0; i < userList.length; i++) {
    setUserColour(userList[i]);
  }

  var videoTitleObserver = new MutationObserver(function(mutations) {
    // Which mutation is used is not important
    var videoItem = mutations[0].target.getElementsByTagName("li");

    for (var i = 0; i < mutations.length; i++) {
      // Modifies the last x (mutations.length) video titles
      addVideoTitleToTarget(videoItem[videoItem.length-1-i]);
    }
  });

  var nameMsgObserver = new MutationObserver(function(mutations) {
    var msgItem = mutations[0].target.getElementsByTagName("div");
    for (var i = 0; i < mutations.length; i++) {
      // Modifies the last x (mutations.length)
      // Visual / DOM changes
      addNameToTarget(msgItem[msgItem.length-1-i]);
      // Functionality changes
      checkForOptions(msgItem[msgItem.length-1-i]);
    }
  });

  var usrListObserver = new MutationObserver(function(mutations) {
    var usrItem = mutations[0].target.getElementsByClassName("userlist_item");
    usrItem = usrItem[usrItem.length-1];
    setUserColour(usrItem);
  });

  // configuration of the observer:
  var videoTitleConfig = {childList: true};
  var nameMsgConfig = {childList : true};
  var usrListConfig = {childList : true};

  // Check all messages in history
  // May delete some children, check for this
  for (let i = 0; i < msgTarget.childNodes.length;) {
    var len = msgTarget.childNodes.length;
    checkForOptions(msgTarget.childNodes[i], true);
    if (msgTarget.childNodes.length === len) {
      i += 1;
    }
  }

  // pass in the target node, as well as the observer options
  videoTitleObserver.observe(videoTitleTarget, videoTitleConfig);
  nameMsgObserver.observe(msgTarget, nameMsgConfig);
  usrListObserver.observe(usrTarget, usrListConfig);

  // Remove old test area
  if (document.querySelector('.test-area') !== null) {
    document.querySelector('.test-area').remove();
  }

  var testArea = document.createElement('div');
  testArea.classList.add('test-area');
  testArea.innerText = "Test Area. DO NOT MOUSE OVER.";
  var testImg = document.createElement('img');
  testImg.src = "https://cdn.instructables.com/FB2/DLT8/I86UGV9U/FB2DLT8I86UGV9U.MEDIUM.gif";
  testImg.style.visibility = "hidden";
  testImg.style["z-index"] = 100;

  testArea.addEventListener('mouseenter', function(e) {
    testImg.style.visibility = "visible";
    testArea.innerText = "";
    testArea.appendChild(testImg);
    testImg.style.width = testArea.offsetWidth + "px";
    testImg.style.height = testArea.offsetHeight + "px";
    testArea.style.border = "none";
  });

  testImg.addEventListener('mouseout', function(e) {
    testImg.style.visibility = "hidden";
    testArea.innerText = "Test Area. DO NOT MOUSE OVER.";
    testArea.style.border = "2px dotted white";
  });

  document.querySelector('#leftpane').appendChild(testArea);

  // Removed for now
  // var bannerDiv = document.createElement('div');
  // bannerDiv.classList.add('banner');
  // var bannerImg = document.createElement('img');
  // bannerImg.src = "http://i.imgur.com/b2O7hQq.png";
  // bannerDiv.appendChild(bannerImg);
  // document.querySelector('.container-fluid').insertBefore(bannerDiv, document.querySelector('#announcements'));

  // Add any custom CSS
  // Used over cytube CSS editor so you only have to update one file
  var css = `.user-img:hover {
    filter: grayscale(40%);
  }
  .test-area {
    width: 100%;
    height: 115px;
    border: 2px dotted white;
    text-align: center;
    font-size: 24px;
  }
  .banner {
    width: 100%;
    text-align: center;
    margin-bottom: 5px;
  }
  .banner img {
    width: 100%;
    height: 180px;
  }`;

  var cssTag = document.createElement('style');
  if (cssTag.styleSheet) {
    cssTag.styleSheet.cssText = css;
  } else {
    cssTag.appendChild(document.createTextNode(css));
  }

  document.querySelector('head').appendChild(cssTag);
}();
<|endoftext|>