'use strict'

var events = require('events')
var once = require('once')

module.exports = stream

function stream(processor) {
  var chunks = []
  var emitter = new events.EventEmitter()
  var ended = false

  emitter.processor = processor
  emitter.readable = true
  emitter.writable = true
  emitter.write = write
  emitter.end = end
  emitter.pipe = pipe

  return emitter

  // Write a chunk into memory.
  function write(chunk, encoding, callback) {
    if (typeof encoding === 'function') {
      callback = encoding
      encoding = null
    }

    if (ended) {
      throw new Error('Did not expect `write` after `end`')
    }

    chunks.push((chunk || '').toString(encoding || 'utf8'))

    if (callback) {
      callback()
    }

    // Signal succesful write.
    return true
  }

  // End the writing.  Passes all arguments to a final `write`.  Starts the
  // process, which will trigger `error`, with a fatal error, if any; `data`,
  // with the generated document in `string` form, if succesful.  If messages
  // are triggered during the process, those are triggerd as `warning`s.
  function end() {
    write.apply(null, arguments)

    ended = true

    processor.process(chunks.join(''), done)

    return true

    function done(err, file) {
      var messages = file ? file.messages : []
      var length = messages.length
      var index = -1

      chunks = null

      // Trigger messages as warnings, except for fatal error.
      while (++index < length) {
        /* istanbul ignore else - shouldn’t happen. */
        if (messages[index] !== err) {
          emitter.emit('warning', messages[index])
        }
      }

      if (err) {
        // Don’t enter an infinite error throwing loop.
        global.setTimeout(function() {
          emitter.emit('error', err)
        }, 4)
      } else {
        emitter.emit('data', file.contents)
        emitter.emit('end')
      }
    }
  }

  // Pipe the processor into a writable stream.  Basically `Stream#pipe`, but
  // inlined and simplified to keep the bundled size down.
  // See <https://github.com/nodejs/node/blob/master/lib/stream.js#L26>.
  function pipe(dest, options) {
    var settings = options || {}
    var onend = once(onended)

    emitter.on('data', ondata)
    emitter.on('error', onerror)
    emitter.on('end', cleanup)
    emitter.on('close', cleanup)

    // If the `end` option is not supplied, `dest.end()` will be called when the
    // `end` or `close` events are received.  Only `dest.end()` once.
    if (!dest._isStdio && settings.end !== false) {
      emitter.on('end', onend)
    }

    dest.on('error', onerror)
    dest.on('close', cleanup)

    dest.emit('pipe', emitter)

    return dest

    // End destination.
    function onended() {
      if (dest.end) {
        dest.end()
      }
    }

    // Handle data.
    function ondata(chunk) {
      if (dest.writable) {
        dest.write(chunk)
      }
    }

    // Clean listeners.
    function cleanup() {
      emitter.removeListener('data', ondata)
      emitter.removeListener('end', onend)
      emitter.removeListener('error', onerror)
      emitter.removeListener('end', cleanup)
      emitter.removeListener('close', cleanup)

      dest.removeListener('error', onerror)
      dest.removeListener('close', cleanup)
    }

    // Close dangling pipes and handle unheard errors.
    function onerror(err) {
      var handlers = emitter._events.error

      cleanup()

      // Cannot use `listenerCount` in node <= 0.12.
      if (!handlers || handlers.length === 0 || handlers === onerror) {
        throw err // Unhandled stream error in pipe.
      }
    }
  }
}


'use strict';

const os = require('os');
const path = require('path');
const fs = require('fs-extra');
const chalk = require('chalk');

// snap rm <names...>
// delete one or more boilerplates
function rm(names) {
  console.log();
  for (const name of names) {
    const bplate = path.join(os.homedir(), '.snap', name);
    if (fs.pathExistsSync(bplate)) {
      fs.removeSync(bplate);
      console.log(`${chalk.green('Success:')} ${chalk.redBright(name)} has been deleted!`);
    } else {
      console.error(`${chalk.red('Error:')} ${chalk.yellow(name)} does not exist.`);
    }
  }
  console.log();
}

module.exports = rm;


'use strict';

const path = require('path');
const os = require('os');
const fs = require('fs-extra');
const chalk = require('chalk');
const shell = require('shelljs');

// snap save <name> [source]
// save a directory or repository provided by [source] to .snap/<name>
function save(name, src = path.resolve(), options) {
  // ensure that the vault exists
  const vault = path.join(os.homedir(), '.snap');
  if (!fs.pathExistsSync(vault)) shell.exec(`node ${path.join(__dirname, 'init.js')}`);

  const root = path.join(vault, name);

  // check if the name is already taken
  if (fs.pathExistsSync(root) && !options.overwrite) {
    console.error(`\n${chalk.red(chalk.underline(name), 'already exists')}\n`);
    return;
  }

  // check if the source provided resolves to a valid path
  const sourcePath = path.resolve(src);
  if (fs.pathExistsSync(sourcePath)) {
    // if --overwrite is passed, remove the existing save first
    if (options.overwrite) fs.removeSync(root);

    // copy the source provided to the new boilerplate in the vault
    fs.copySync(src, root, {
      overwrite: false, // don't want to modify existing boilerplates, -o will remove the old one first
      errorOnExist: true, // shouldn't happen, either the boilerplate is new or has been removed with -o
      filter: pathToCopy => {
        const match = pathToCopy.match(/node_modules$|.git$/);
        if (match) {
          console.log(`${chalk.dim.redBright(match[0])} has been excluded from ${chalk.yellow(name)}`);
        }
        return !match;
      }
    });

    logSuccess(name);
    clean(root, name);
    return;
  }

  // check if the source provided is a valid git url
  const gitUrl = /((git|ssh|http(s)?)|(git@[\w.]+))(:(\/\/)?)([\w.@:/\-~]+)(\.git)(\/)?/;
  if (gitUrl.test(src)) {
    shell.exec(`git clone ${src} ${root}`, { silent: true }, exitCode => {
      if (exitCode !== 0) {
        console.error(`\n${chalk.red('Save failed :(')}  Could not clone from ${chalk.cyan(src)}\n`);
      } else {
        logSuccess(name);
        clean(root, name);
      }
    });
    return;
  }

  // log if the source was invalid
  console.error(chalk.red('\nInvalid [source]'));
  console.log(`Run ${chalk.yellow('snap save -h')} to display help information.\n`);
}

function logSuccess(name) {
  console.log(chalk.green('\nSuccess! ＼(＾O＾)／'));
  console.log(`You can now run ${chalk.yellow('snap', chalk.underline(name), '<project-directory>')} to use your new boilerplate!\n`);
}

// clean out blacklisted content
function clean(root, name) {
  const blacklist = ['.git', 'node_modules'];
  for (const item of blacklist) {
    const pathToItem = path.join(root, item);
    if (fs.pathExistsSync(pathToItem)) {
      fs.removeSync(pathToItem);
      console.log(`${chalk.dim.redBright(item)} has been removed from ${chalk.yellow(name)}`);
    }
  }
}

module.exports = save;


'use strict';

const os = require('os');
const path = require('path');
const shell = require('shelljs');
const chalk = require('chalk');

// snap ls
// list saved boilerplates
function ls() {
  const vault = path.join(os.homedir(), '.snap');
  const list = shell.ls(vault);
  if (!list.length) {
    console.log("\nIt seems you don't have anything saved...");
    console.log(`You can run ${chalk.yellow('snap save')} to save a directory or Git repo for future use!`);
    console.log(`Run ${chalk.yellow('snap save -h')} for more info.\n`);
    return;
  }

  console.log('\nThe following boilerplates have been saved...');
  console.log(`Run ${chalk.yellow('snap <boilerplate-name> <project-directory>')} to get started with your new project!`);
  for (const bplate of list) {
    console.log(`  ○ ${bplate}`);
  }
  console.log();
}

module.exports = ls;


'use strict';

const path = require('path');
const fs = require('fs-extra');
const os = require('os');
const chalk = require('chalk');
const shell = require('shelljs');

// snap <boilerplate-name> <project-directory> [-i]
// create a new project with a boilerplate
function snap(bplateName, projectDir, options) {
  // find the boilerplate
  const bplate = path.join(os.homedir(), '.snap', bplateName);
  if (!fs.pathExistsSync(bplate)) {
    console.log(`\n${chalk.red('Error:')} ${chalk.yellow(bplateName)} does not exist.\n`);
    return;
  }

  const projectPath = path.resolve(projectDir);
  // check if the project directory is already taken
  if (fs.pathExistsSync(projectPath)) {
    console.log(`\n${chalk.red('Error:')} ${chalk.cyan(projectPath)} already exists.\n`);
    return;
  }

  // copy the boilerplate
  console.log('\nCopying...');
  fs.copySync(bplate, projectPath);

  if (options.install) {
    console.log(`Running '${chalk.yellow('npm install')}'...`);
    shell.exec(`cd ${projectDir} && npm install`);
  }

  console.log(chalk.green('\nSuccess! ＼(＾O＾)／'));
  console.log(`You can now ${chalk.yellow(`cd ${projectDir}`)}\n`);
}

module.exports = snap;


'use strict';

const path = require('path');
const os = require('os');
const fs = require('fs-extra');
const chalk = require('chalk');

// snap show <name>
// display the file structure of a boilerplate
function show(name) {
  const root = path.join(os.homedir(), '.snap', name);
  if (!fs.pathExistsSync(root)) {
    console.log(`\n${chalk.red('Error:')} ${chalk.yellow(name)} does not exist.\n`);
    return;
  }

  const tree = walk(root, 0);
  console.log();
  printTree(tree);
  console.log();
}

function printTree(item, parentWasLast, isLast) {
  if (item.level === 0) console.log(chalk.yellow(item.name));
  else {
    // build the item's indent/prefix
    let prefix = '';
    prefix += parentWasLast ? '    '.repeat(item.level - 1) : '│   '.repeat(item.level - 1);
    prefix += isLast ? '└── ' : '├── ';

    console.log(`${prefix}${item.name}`);
  }

  // recursively print children
  if (item.children) {
    for (const child of item.children) {
      const lastChild = child === item.children[item.children.length - 1];
      printTree(child, isLast, lastChild);
    }
  }
}

function walk(currentPath, level) {
  // give the current item a name & level
  const item = { name: path.basename(currentPath), level };
  // get its stats
  const stats = fs.statSync(currentPath);
  // look at the item's contents if it has any
  if (stats.isDirectory()) {
    item.children = fs.readdirSync(currentPath).map(child => walk(path.join(currentPath, child), level + 1));
  }
  return item;
}

module.exports = show;


import { PixelRatio, Dimensions,Navigator } from 'react-native';
// let buildStyleInterpolator = require('buildStyleInterpolator');
let NavigatorSceneConfigs = Navigator.SceneConfigs//转场动画
const Width = Dimensions.get('window').width;
const Height = Dimensions.get("window").height;
const Scale = PixelRatio.get();

const BaseLeftToRightGesture = {
	// If the gesture can end and restart during one continuous touch
	isDetachable: false,

	// How far the swipe must drag to start transitioning
	gestureDetectMovement: 2,

	// Amplitude of release velocity that is considered still
	notMoving: 0.3,

	// Fraction of directional move required.
	directionRatio: 0.66,

	// Velocity to transition with when the gesture release was "not moving"
	snapVelocity: 2,

	// Region that can trigger swipe. iOS default is 30px from the left edge
	edgeHitWidth: 30,

	// Ratio of gesture completion when non-velocity release will cause action
	stillCompletionRatio: 3 / 5,

	fullDistance: Width,
	direction: 'left-to-right',
};

const BaseConfig = {
	// Rebound spring parameters when transitioning FROM this scene
  springFriction: 26,
  springTension: 200,

  // Velocity to start at when transitioning without gesture
  defaultTransitionVelocity: 1.5,
}

const Opacity = {
	opacity: {
		value: 1.0,
		type: 'constant'
	},
}

const BaseScaleIn = {
	...Opacity,
	transformScale:{
		from:{x:0,y:0,z:0},
		to:{x:1,y:1,z:1},
		min:0,
		max:1,
		type:"linear",
		extrapolate:true
	}
}

const BaseScaleOut = {
	...Opacity,
	transformScale:{
		from:{x:1,y:1,z:1},
		to:{x:0,y:0,z:0},
		min:0,
		max:1,
		type:"linear",
		extrapolate:true
	}
}

const UpLeftIn = {
	...BaseScaleIn,
	transformTranslate: {
		from: {x: -Width, y: -Height, z: 0},
		to: {x: 0, y: 0, z: 1},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	}
}

const UpLeftOut = {
	...BaseScaleOut,
	transformTranslate: {
		from: {x: 0, y: 0, z: 1},
		to: {x: -Width, y: -Height, z: 0},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	}
}

const UpRightIn = {
	...BaseScaleIn,
	transformTranslate: {
		from: {x: Width, y: -Height, z: 0},
		to: {x: 0, y: 0, z: 1},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	}
}

const UpRightOut = {
	...BaseScaleOut,
	transformTranslate: {
		from: {x: 0, y: 0, z: 1},
		to: {x: Width, y: -Height, z: 0},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	}
}

const DownLeftIn = {
	...BaseScaleIn,
	transformTranslate: {
		from: {x: -Width, y: Height, z: 0},
		to: {x: 0, y: 0, z: 1},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	}
}

const DownLeftOut = {
	...BaseScaleOut,
	transformTranslate: {
		from: {x: 0, y: 0, z: 1},
		to: {x: -Width, y: Height, z: 0},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	}
}

const DownRightIn = {
	...BaseScaleIn,
	transformTranslate: {
		from: {x: Width, y: Height, z: 0},
		to: {x: 0, y: 0, z: 1},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	}
}

const DownRightOut = {
	...BaseScaleOut,
	transformTranslate: {
		from: {x: 0, y: 0, z: 1},
		to: {x: Width, y: Height, z: 0},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	}
}

const CenterScaleRightIn = {
	...Opacity,

	transformTranslate: {
		from: {x: Width, y: 0, z: 0},
		to: {x: 0, y: 0, z: 0},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	},
	transformScale: {
		from: {x: 0.4, y: 0.4, z: 0.4},
		to: {x: 1, y: 1, z: 1},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true
	},
};

const CenterScaleLeftIn = {
	...Opacity,

	transformTranslate: {
		from: {x: -Width, y: 0, z: 0},
		to: {x: 0, y: 0, z: 0},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	},
	transformScale: {
		from: {x: 0.4, y: 0.4, z: 0.4},
		to: {x: 1, y: 1, z: 1},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true
	},
};

const CenterScaleLeftOut = {
	...Opacity,
	transformTranslate: {
		from: {x: 0, y: 0, z: 0},
		to: {x: -Width, y: 0, z: 0},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	},
	transformScale: {
		from: {x: 1, y: 1, z: 1},
		to: {x: 0.4, y: 0.4, z: 0.4},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true
	},
}

const CenterScaleRightOut = {
	...Opacity,
	transformTranslate: {
		from: {x: 0, y: 0, z: 0},
		to: {x: Width, y: 0, z: 0},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	},
	transformScale: {
		from: {x: 1, y: 1, z: 1},
		to: {x: 0.4, y: 0.4, z: 0.4},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true
	},
}

const DownScaleIn = {
	...CenterScaleRightIn,
	transformTranslate: {
		from: {x: Math.round(Width/2), y: Math.round(Height/2), z: 0},
		to: {x: 0, y: 0, z: 1},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	},
	transformScale:{
		from:{x:0,y:0,z:0},
		to:{x:1,y:1,z:1},
		min:0,
		max:1,
		type:"linear",
		extrapolate:true
	}
}

const DownScaleOut = {
	...CenterScaleLeftOut,
	transformTranslate: {
		from: {x: 0, y: 0, z: 1},
		to: {x: -Width/2, y: Height/2, z: 0},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	},
	transformScale:{
		from:{x:1,y:1,z:1},
		to:{x:0,y:0,z:0},
		min:0,
		max:1,
		type:"linear",
		extrapolate:true
	},
}

const UpScaleIn = {
	...DownScaleIn,
	transformTranslate: {
		from: {x: Math.round(Width/2), y: -Math.round(Height/2), z: 0},
		to: {x: 0, y: 0, z: 1},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	}
}

const UpScaleOut = {
	...DownScaleOut,
	transformTranslate: {
		from: {x: 0, y: 0, z: 1},
		to: {x: -Width/2, y: -Height/2, z: 0},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	}
}

const RightRotateInDown = {
	transformRotateRadians: {
	  from: {x: 0, y: 0, z: 1, w: 0},
	  to: {x: 0, y: 0, z: 0, w: 1},
	  min: 0,
	  max: 1,
	  type: 'linear',
	  extrapolate: true
	},
	opacity:{
		value:1,
		type:"constant"
	},
	transformTranslate: {
		from: {x: 1, y: 0, z: 0},
		to: {x: 0, y: 0, z: 0},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	},
}

const LeftRotateOutDown = {
	transformRotateRadians: {
	  from: {x: 0, y: 0, z: 0, w: 1},
	  to: {x: 0, y: 0, z: -1, w: 0},
	  min: 0,
	  max: 1,
	  type: 'linear',
	  extrapolate: true
	},
	opacity:{
		value:1,
		type:"constant"
	},
	transformTranslate: {
		from: {x: 0, y: 0, z: 0},
		to: {x: -1, y: 0, z: 0},
		min: 0,
		max: 1,
		type: 'linear',
		extrapolate: true,
		round: Scale
	},
}

// 下面两个是右进左出
const RightRotateInUp = {
	...RightRotateInDown,
	transformRotateRadians: {
	  from: {x: 0, y: 0, z: -1, w: 0.5},
	  to: {x: 0, y: 0, z: 0, w: 1},
	  min: 0,
	  max: 1,
	  type: 'linear',
	  extrapolate: true
	},
}

const LeftRotateOutUp = {
	...LeftRotateOutDown,
	transformRotateRadians: {
	  from: {x: 0, y: 0, z: 0, w: 1},
	  to: {x: 0, y: 0, z: 1, w: 0.5},
	  min: 0,
	  max: 1,
	  type: 'linear',
	  extrapolate: true
	},
}

const LeftRotateYIn = {
	...RightRotateInDown,
	transformRotateRadians: {
	  from: {x: 0, y: -0.8, z: 0, w: 1},
	  to: {x: 0, y: 0, z: 0, w: 1},
	  min: 0,
	  max: 1,
	  type: 'linear',
	  extrapolate: true
	},
}

const RightRotateYOut = {
	...LeftRotateOutDown,
	transformRotateRadians: {
	  from: {x: 0, y: 0, z: 0, w: 1},
	  to: {x: 0, y: -0.8, z: 0, w: 1},
	  min: 0,
	  max: 1,
	  type: 'linear',
	  extrapolate: true
	},
}

const LeftRotateXIn = {
	...RightRotateInDown,
	transformRotateRadians: {
	  from: {x: -1, y: 0, z: 0, w: 1},
	  to: {x: 0, y: 0, z: 0, w: 1},
	  min: 0,
	  max: 1,
	  type: 'linear',
	  extrapolate: true
	},
}

const RightRotateXOut = {
	...LeftRotateOutDown,
	transformRotateRadians: {
	  from: {x: 0, y: 0, z: 0, w: 1},
	  to: {x: -1, y: 0, z: 0, w: 1},
	  min: 0,
	  max: 1,
	  type: 'linear',
	  extrapolate: true
	},
}

// 中间进入退出
const CenterScaleIn = {
	opacity:{
		from:0,
		to:1,
		min:0,
		max:1,
		type:"linear"
	},
	transformScale:{
		from:{x:0.4,y:0.4,z:0.4},
		to:{x:1,y:1,z:1},
		min:0,
		max:1,
		type:"linear",
		extrapolate:true
	},
}

const CenterScaleOut = {
	opacity:{
		from:1,
		to:0,
		min:0,
		max:1,
		type:"linear"
	},
	transformScale:{
		from:{x:1,y:1,z:1},
		to:{x:0.4,y:0.4,z:0.4},
		min:0,
		max:1,
		type:"linear",
		extrapolate:true
	},
}

const Main = {
	opacity:{
		value:1,
		type:"constant"
	}
}

function SwitchMain(init){
	let buildStyleInterpolator = init;
	return Object.assign({},{
		...NavigatorSceneConfigs.FadeAndroid,
		animationInterpolators:{
			into:buildStyleInterpolator({
				...Main,
			}),
			out:buildStyleInterpolator({
				...Main,
			})
		},
		defaultTransitionVelocity:0,
		springTension:1000
	});
}

// 从右边弹出,左边退出
function AnimationScaleInRight(init){
	let buildStyleInterpolator = init;
	return Object.assign({},{
		...NavigatorSceneConfigs.PushFromRight,
		animationInterpolators:{
			into:buildStyleInterpolator({
				...CenterScaleRightIn
			}),
			out:buildStyleInterpolator({
				...CenterScaleLeftOut
			})
		}
	});
}
// const AnimationScaleInRight = Object.assign({},{
// 	...NavigatorSceneConfigs.PushFromRight,
// 	animationInterpolators:{
// 		into:buildStyleInterpolator({
// 			...CenterScaleRightIn
// 		}),
// 		out:buildStyleInterpolator({
// 			...CenterScaleLeftOut
// 		})
// 	}
// });

// 从右下角弹出,左下角退出
function AnimationScaleInRightDown(init){
	let buildStyleInterpolator = init;
	return Object.assign({},{
		...NavigatorSceneConfigs.PushFromRight,
		animationInterpolators:{
			into:buildStyleInterpolator({
				...DownScaleIn
			}),
			out:buildStyleInterpolator({
				...DownScaleOut
			})
		}
	});
}
// const AnimationScaleInRightDown = Object.assign({},{
// 	...NavigatorSceneConfigs.PushFromRight,
// 	animationInterpolators:{
// 		into:buildStyleInterpolator({
// 			...DownScaleIn
// 		}),
// 		out:buildStyleInterpolator({
// 			...DownScaleOut
// 		})
// 	}
// });

// 从右上角弹出,左上角退出
function AnimationScaleInRightUp(init){
	let buildStyleInterpolator = init;
	return Object.assign({},{
		...NavigatorSceneConfigs.PushFromRight,
		animationInterpolators:{
			into:buildStyleInterpolator({
				...UpScaleIn
			}),
			out:buildStyleInterpolator({
				...UpScaleOut
			})
		}
	});
}
// const AnimationScaleInRightUp = Object.assign({},{
// 	...NavigatorSceneConfigs.PushFromRight,
// 	animationInterpolators:{
// 		into:buildStyleInterpolator({
// 			...UpScaleIn
// 		}),
// 		out:buildStyleInterpolator({
// 			...UpScaleOut
// 		})
// 	}
// });

// 右边旋转进入,左边旋转退出
function AnimationRotateInLeft(init){
	let buildStyleInterpolator = init;
	return Object.assign({},{
		...NavigatorSceneConfigs.FadeAndroid,
		animationInterpolators:{
			into:buildStyleInterpolator({
				...RightRotateInDown
			}),
			out:buildStyleInterpolator({
				...LeftRotateOutDown
			})
		}
	});
}
// const AnimationRotateInLeft = Object.assign({},{
// 	...NavigatorSceneConfigs.FadeAndroid,
// 	animationInterpolators:{
// 		into:buildStyleInterpolator({
// 			...RightRotateInDown
// 		}),
// 		out:buildStyleInterpolator({
// 			...LeftRotateOutDown
// 		})
// 	}
// });

// 数据
const AnimationData = {
	upleftin:UpLeftIn,
	upleftout:UpLeftOut,
	uprightin:UpRightIn,
	uprightout:UpRightOut,
	downleftin:DownLeftIn,
	downleftout:DownLeftOut,
	downrightin:DownRightIn,
	downrightout:DownRightOut,
	rightin:CenterScaleRightIn,
	rightout:CenterScaleRightOut,
	leftin:CenterScaleLeftIn,
	leftout:CenterScaleLeftOut,
	rotaterightin:RightRotateInDown,
	rotateleftout:LeftRotateOutDown,
	rotaterightinup:RightRotateInUp,
	rotateleftoutup:LeftRotateOutUp,
	rotateiny:LeftRotateYIn,
	rotateouty:RightRotateYOut,
	rotateinx:LeftRotateXIn,
	rotateoutx:RightRotateXOut,
	centerscalein:CenterScaleIn,
	centerscaleout:CenterScaleOut
}

// 这是自定义动画从哪个角落弹出和哪个角落退出函数
function CheckParams(parms){
	return AnimationData[parms.toLowerCase()];
}

// 定制动画(是上/下,是左/右,是进/出,是否支持手势)
// 第一个参数必须是定义的进入动画,第二个必须是退出的动画
function CustomAnimation(init){
	let buildStyleInterpolator = init;
	return (LeftRightIn,UpDowmOut,Gestures,Base={springFriction:26,springTension:200,defaultTransitionVelocity:1.5})=>{
		return Object.assign({},
			NavigatorSceneConfigs.FadeAndroid,{
			animationInterpolators:{
				into:buildStyleInterpolator({
					...CheckParams(LeftRightIn)
				}),
				out:buildStyleInterpolator({
					...CheckParams(UpDowmOut)
				})
			},
			gestures:Gestures?Gestures:{
				pop:{
					...BaseLeftToRightGesture,
					direction:"left-to-right",
					fullDistance:Width
				}
			},
			...Base
			// springFriction:Base.springFriction,
			// springTension:Base.springTension,
			// defaultTransitionVelocity:Base.defaultTransitionVelocity,
		})
	}
}

export default function AnimationInit(init) {
	return {
		AnimationScaleInRight:AnimationScaleInRight(init),
		AnimationScaleInRightDown:AnimationScaleInRightDown(init),
		AnimationScaleInRightUp:AnimationScaleInRightUp(init),
		AnimationRotateInLeft:AnimationRotateInLeft(init),
		CustomAnimation:CustomAnimation(init),
		BaseLeftToRightGesture:BaseLeftToRightGesture,
		SwitchMain:SwitchMain(init)
	}
}


/*
 * node-analytics
 */

'use strict';

// defaults
const fs = require('fs'),
    path = require('path'),
    http = require('http'),
    crypto = require('crypto');

// installed
const get_ip = require('ipware')().get_ip,
    mongoose = require('mongoose'),
    useragent = require('useragent'),
    maxmind = require('maxmind'),
    cookie = require('cookie'),
    async = require('async'),
    s_io = require('socket.io'),
    chalk = require('chalk'),
    CryptoJS = require('crypto-js'),
    onHeaders = require('on-headers'),
    onFinished = require('on-finished');

// globals
let db,
    geo_lookup,
    io = false;

let log = require('andrao-logger')('n-a');

// -------------------------------------------------------------

const Request_Schema = mongoose.Schema({
    _id: { type: String, unique: true, index: true },
    host: String,
    date: { type: Date, default: Date.now },
    url: { type: String, index: true },
    query: [{ field: String, value: String }],
    ref: { type: String, index: true },
    referrer: String,
    method: String,
    time: Number,
    reaches: [String],
    pauses: [{
        _id: false,
        id: String,
        time: Number
    }],
    clicks: [String]
});
const Session_Schema = mongoose.Schema({
    user: { type: String, index: true },
    name: { type: String, index: true },
    date: { type: Date, default: Date.now },
    last: { type: Date, default: Date.now },
    ip: String,
    is_bot: { type: Boolean, default: true },
    geo: {
        _id: false,
        city:    { type: String, index: true },
        state:   { type: String, index: true },
        country: { type: String, index: true },
        continent: { type: String, index: true },
        time_zone: { type: String, index: true }
    },
    system: {
        os: {
            _id: false,
            name: String,
            version: String
        },
        browser: {
            _id: false,
            name: String,
            version: String
        }
    },
    time: Number,
    resolution: {
        _id: false,
        width: Number,
        height: Number
    },
    reqs: [Request_Schema],
    state: String,
    flash_data: mongoose.Schema.Types.Mixed
});
const Session = mongoose.model('Session', Session_Schema);

module.exports = analytics;
module.exports.sessions = sessions;

const opts = {
    db_host:    '127.0.0.1',
    db_port:    27017,
    db_name:    'node_analytics_db',
    ws_port:    8080,
    ws_server:  false,
    s_io:       false,
    geo_ip:     true,
    mmdb:       'GeoLite2-City.mmdb',
    log:        true,
    log_all:    false,
    error_log:  true,
    secure:     true,
    secret:     'changeMe',
    log_opts:   {
        pre: 'n-a'
    },
    mongoose_params: {
        server: {
            auto_reconnect: true,
            reconnectTries: Number.MAX_VALUE,
            reconnectInterval: 3000,
            keepAlive: 120,
            connectTimeoutMS: 30000,
            socketOptions: {
                keepAlive: 120,
                connectTimeoutMS: 30000,
                reconnectTries: 500,
                reconnectInterval: 3000,
            },
            poolSize: 50
        },
        replset: {
            keepAlive: 1,
            connectTimeoutMS: 30000,
            socketOptions: {
                keepAlive: 1,
                connectTimeoutMS: 30000
            }
        }
    }
};

log("active: wait for MongoDB, GeoIP, & WebSocket");
log("don't forget to copy", chalk.red('node-analytics-client.js'), "to public directory");

function mongoDB(cb){

    // Connect to MongoDB
    const db_url = 'mongodb://' + opts.db_host + ':' + opts.db_port + '/' + opts.db_name;

    db = mongoose.connection;

    const db_connect = setTimeout(() => {
        log(chalk.cyan('mongoose.connect'));
        mongoose.connect(db_url, opts.mongoose_params)
    }, 500);

    db.on('connecting', () => {
        log(chalk.yellow('MongoDB connecting'));
    });
    db.on('error', (err) => {
        log.error(chalk.red('MongoDB error'), err);
    });
    db.on('connected', () => {
        log(chalk.yellow('MongoDB connected:'), 'Wait for open.');

        if(db_connect)
            clearTimeout(db_connect);
    });
    db.once('open', () => {
        log(chalk.green('MongoDB connection open'));
        cb(null);
    });
    db.on('reconnected', () => {
        log(chalk.green('MongoDB reconnected.'));
    });
    db.on('disconnected', () => {
        log.error(chalk.red('MongoDB disconnected!'), 'Attempting reconnect.');
    });
}

function geoDB(cb){
    // Check for mmdb
    if(opts.geo_ip){
        maxmind.open(opts.mmdb, (err, mmdb) => {
            if(err){
                log.error('GeoIP DB open error', opts.mmdb, err);
                return cb(true)
            }

            geo_lookup = mmdb;
            log('GeoIP DB loaded successfully');
            cb(null);
        });
    }
    else {
        log('GeoIP disabled');
        cb(null);
    }
}

function socketInit(cb){

    io = opts.s_io ? opts.s_io : opts.ws_server ? s_io.listen(opts.ws_server) : s_io(opts.ws_port);

    io.of('/node-analytics')
        .use(function(socket, next){
            if(socket.handshake.headers.cookie){
                const cookies = getCookies(socket.handshake.headers.cookie);
                if(cookies && cookies.na_session)
                    next();
            }
            else
                log.error('Socket authentication error; no session cookie');
        })
        .on('connection', socketConnection);

    log('Websocket server established');

    cb(null);
}

// =====================

function socketConnection(socket){

    const cookies = getCookies(socket.handshake.headers.cookie);

    socket.session_start = Date.now();
    socket.blurred = 0;
    socket.blurring = Date.now();
    socket.req_id = cookies.na_req;
    socket.session_id = cookies.na_session;

    // Get session
    if(socket.session_id){
        Session.findById(socket.session_id, function(err, session){
            if(err)
                return log.error('Session find error :: id[socket]', this.session_id, err);
            if(!session)
                return log.error('Session not found :: id[socket]', this.session_id);

            const socket = this;

            // set regional session and request
            socket.session = session;
            if(socket.req_id){
                for(let i = session.reqs.length - 1; i >= 0; i--){
                    if(session.reqs[i]._id.toString() == socket.req_id){
                        socket.req = session.reqs[i];
                        break;
                    }
                }
            }

            // log and initiate socket sensitivity
            if(!socket.req)
                log.error('socket connected; request not found');
            else if(opts.log_all)
                log.session(session, 'socket connected; request:', socket.req._id);

            socketResponse(socket);

        }.bind(socket));
    }

    // =============

    function socketResponse(socket){

        // session updates from the client

        // Trivial not-bot check: socket connects;
        //   Could / should be improved to having done action on page

        if(socket.session.is_bot)
            Update.session(socket.session, { $set: { is_bot: false }});

        if(!socket.session.resolution)
            socket.on('resolution', _socket.resolution.bind(socket));

        // request updates
        socket.on('click', _socket.click.bind(socket));
        socket.on('reach', _socket.reach.bind(socket));
        socket.on('pause', _socket.pause.bind(socket));

        // session timer
        socket.on('blur', _socket.blur.bind(socket));
        socket.on('focus', _socket.focus.bind(socket));

        // Disconnection
        socket.on('disconnect', _socket.disconnect.bind(socket));
    }
}

const _socket = {
    click: function(id){
        if(this.req)
            Update.request(this, { $push: { clicks : id }});

        if(opts.log)
            log.session(this.session, chalk.green('click'), '@', chalk.cyan(id))
    },
    reach: function(id){
        if(this.req)
            Update.request(this, { $push: { reaches: id }});

        if(opts.log)
            log.session(this.session, chalk.yellow('reach'), '@', chalk.cyan(id))
    },
    pause: function(params){
        if(this.req)
            Update.request(this, { $push: { pauses: params }});

        if(opts.log)
            log.session(this.session, chalk.magenta('pause'), `for ${params.time}s @`, chalk.cyan(params.id));
    },

    blur: function(){
        this.blurring = Date.now();
    },

    focus: function(){
        this.blurred += Date.now() - this.blurring;
    },

    resolution: function(params){
        Update.session(this.session, { $set: { resolution: params }});
    },

    disconnect: function(){

        if(!this || !this.req)
            return;

        // request time, sans blurred time
        const t = (Date.now() - this.session_start - this.blurred) / 1000;

        // total session time; begin with this request
        let session_t = t;
        for(let i = 0; i < this.session.reqs.length; i++)
            session_t += this.session.reqs[i].time;

        // update request & session
        this.req.time = t;
        if(this.req)
            Update.request(this, { $set: { time: t }});

        Update.session(this.session, { $set: { session_time: session_t }});

        if(opts.log)
            log.session(this.session, chalk.red(t));
    }
};

// ===============

const session_fields = '_id user name date last flash_data';

function analytics(opts_in){
    for(let k in opts_in)
        opts[k] = opts_in[k];

    if(!opts.log_opts.pre)
        opts.log_opts.pre = 'n-a';

    log = require('andrao-logger')(opts.log_opts);

    async.parallelLimit([
        mongoDB,
        socketInit,
        geoDB,
    ], 2, (err) => {
        if(err)
            return log.error('start-up interrupted');

        log(chalk.green('NODE ANALYTICS READY'));
    });

    // HTTP request:
    return function(req, res, next){

        // Skip cases
        if(req.url.indexOf('/socket.io/?EIO=') == 0)
            return next();

        async.waterfall([
            (cb) => {
                getSession(req, res, cb);
            },
            setCookies,
            sessionData,
            newRequest,
            logRequest,
            sessionSave,
            sessionFlash
        ], (err, session) => {
            if(err){
                log.error(err);
                next();
                return false;
            }
            req.node_analytics = session;
            next();
        });
    }
}

// =====================

// populate var session; returns boolean on whether newly formed
function getSession(req, res, cb){

    const now = new Date();

    const cookies = getCookies(req.headers.cookie);

    // cookies.na_session  :: session._id
    // cookies.na_user     :: session.user

    // Establish session: new/old session? new/old user?
    if(cookies.na_session){
        if(opts.log_all)
            log('Session cookie found:', cookies.na_session);

        Session.findById(cookies.na_session, session_fields).lean().exec(function(err, session){
            if(err){
                log.error('getSession error', err);
                return cb(err);
            }

            if(!session){
                log.error('Session not found :: id[cookie]:', this.cookies.na_session);

                // send to check if user instead
                if(cookies.na_user)
                    userSession();
                else
                    newSession();
            }
            else {
                Update.session(session, {
                    $set: {
                        last: Date.now()
                    }
                }, (err, session) => {
                    if(err){
                        log.error('establish session / update error');
                        return cb(true);
                    }

                    session.continued = true;
                    cb(err, this.req, this.res, session)
                });
            }

        }.bind({
            cookies: cookies,
            req: req,
            res: res
        }))
    }
    else if(cookies.na_user)
        userSession();
    else
        newSession();

    // ====================

    function userSession(){

        // OLD USER, NEW SESSION

        cb(null, req, res, new Session({
            user: cookies.na_user,
            new_session: true
        }).toObject({ virtuals: true }));

        if(opts.log_all)
            log.timer('getSession 1', now);
    }
    function newSession(){

        // NEW USER, NEW SESSION
        // Initiate session to get _id
        let session = new Session();
        session.user = session._id.toString();
        session.new_user = true;

        session = session.toObject({ virtuals: true });

        cb(null, req, res, session);

        if(opts.log_all)
            log.timer('getSession 2', now);
    }
}

// set cookies
function setCookies(req, res, session, cb){

    const now = new Date();

    // Set cookies
    res.cookie('na_session', AES.encrypt(session._id.toString()), {
        maxAge:     1000 * 60 * 15,              // 15 mins
        httpOnly:   true,
        secure:     opts.secure
    });
    res.cookie('na_user', AES.encrypt(session.user), {
        maxAge:     1000 * 60 * 60 * 24 * 365,   // 1 year
        httpOnly:   true,
        secure:     opts.secure
    });

    cb(null, req, res, session);

    if(opts.log_all)
        log.timer('setCookies', now);
}

// append session data
function sessionData(req, res, session, cb){

    const now = new Date();

    if(session.continued)
        return cb(null, req, res, session);

    async.parallelLimit([
            getIp,
            getLocation,
            getSystem
        ], 2,
        function(err){
            cb(err, this.req, this.res, this.session);

            if(opts.log_all)
                log.timer('sessionData', now);

        }.bind({
            req: req,
            res: res,
            session: session
        })
    );

    // ======================

    // .ip
    function getIp(cb){
        session.ip = get_ip(req).clientIp;
        cb(null)
    }

    // .geo :: .city, .state, .country
    function getLocation(cb){
        if(!geo_lookup)
            return cb(null);

        const loc = geo_lookup.get(session.ip);

        if(!session.geo)
            session.geo = {};

        if(loc){
            try {
                if(loc.city)
                    session.geo.city = loc.city.names.en;
                if(loc.subdivisions)
                    session.geo.state = loc.subdivisions[0].iso_code;
                if(loc.country)
                    session.geo.country = loc.country.iso_code;
                if(loc.continent)
                    session.geo.continent = loc.continent.code;
                if(loc.location)
                    session.geo.time_zone = loc.location.time_zone;
            }
            catch(e){
                log.error('geoIP error:', e);
            }
        }

        cb(null)
    }

    // .system :: .os{, .broswer{ .name, .version
    function getSystem(cb){
        var agent = useragent.parse(req.headers['user-agent']);
        var os = agent.os;

        if(!session.system)
            session.system = {};

        if(!session.system.browser)
            session.system.browser = {};
        if(!session.system.os)
            session.system.os = {};

        session.system.browser.name = agent.family;
        session.system.browser.version = agent.major + '.' + agent.minor + '.' + agent.patch;

        session.system.os.name = os.family;
        session.system.os.version = os.major + '.' + os.minor + '.' + os.patch;

        cb(null)
    }
}

// return new request document, create req cookie
function newRequest(req, res, session, cb){

    const now = new Date();

    const request = {
        _id: `r${crypto.randomBytes(16).toString('hex')}${Date.now()}`,
        host: req.hostname,
        url: req.url,
        method: req.method,
        referrer: req.get('Referrer') || req.get('Referer')
    };

    // populate request query
    for(let field in req.query){
        if(field === 'ref')
            request.ref = req.query[field];
        else {
            if(!request.query)
                request.query = [];

            request.query.push({
                field: field,
                value: req.query[field]
            })
        }
    }

    // add request cookie for communication/association with socket
    res.cookie('na_req', AES.encrypt(request._id), {
        maxAge:     1000 * 60 * 15,             // 15 mins
        httpOnly:   true,
        secure:     opts.secure
    });

    // return request object: will be added at sessionSave();
    cb(null, req, res, session, request);

    if(opts.log_all)
        log.timer('newRequest', now);
}

// log request
function logRequest(req, res, session, request, cb){

    const now = new Date();

    if(opts.log){
        onHeaders(res, log_start.bind(res));
        onFinished(res, req_log.bind({
            req: request,
            ses: session
        }));
    }

    cb(null, session, request);

    if(opts.log_all)
        log.timer('logRequest', now);

    /// ==============

    function log_start(){
        this._log_start = process.hrtime();
    }
    function req_log(){

        const request = this.req;
        const session = this.ses;

        // Status colour
        const sc = res.statusCode < 400 ? 'green' : 'red';

        // Res time
        const ms = nano_time(res._log_start);

        // Referrer
        let ref = request.referrer;
        if(ref){
            ref = ref.replace('http://', '');
            ref = ref.replace('https://', '');
        }

        // Args
        const args = [session, '|', chalk.magenta(request.url), '|', request.method, chalk[sc](res.statusCode), `: ${ms} ms`];

        if(session && session.system){

            args.push('|');

            if(session.system.browser){
                args.push(chalk.grey(session.system.browser.name));
                args.push(chalk.grey(session.system.browser.version));
            }
            if(session.system.os){
                args.push(chalk.grey(session.system.os.name));
                args.push(chalk.grey(session.system.os.version));
            }
        }

        if(ref)
            args.push('|', chalk.grey(`from ${ref}`));

        // Apply
        log.session.apply(log, args);

        // ===

        function nano_time(start){
            let t = conv(process.hrtime()) - conv(start);   // ns
            t = Math.round(t / 1000);                       // µs
            return t / 1000;                                // ms [3 dec]

            // ====

            function conv(t){
                if(!t || typeof t[0] === 'undefined')
                    return 0;

                return t[0] * 1e9 + t[1];
            }
        }
    }
}

// save / update session to DB & proceed to socket
function sessionSave(session, request, cb){

    const now = new Date();

    if(!session.continued){

        session.reqs = [request];

        Update.session(
            session,
            {  $set: session  },
        (err, session) => {
            if(err)
                return cb('db session save error');

            if(opts.log_all)
                log.session(session, 'session active [ new ]');

            cb(null, session);

            if(opts.log_all)
                log.timer('sessionSave 1', now);
        })
    }
    else {
        // an old session: all that needs be updated is request
        Update.session(
            session,
            {  $push: { reqs: request }  },
        (err, session) => {
            if(err){
                log.error('db session update error');
                return cb(true);
            }

            if(opts.log_all)
                log.session(session, 'session active [ updated ]');

            cb(null, session);

            if(opts.log_all)
                log.timer('sessionSave 2', now);
        });
    }
}

function sessionFlash(session, cb){

    const now = new Date();

    // SESSION OBJECT FUNCTIONS
    session.identify = Identify.bind(session);
    session.flash = Flash.bind(session);
    session.save = Update.session_save.bind(session);

    // Expire and clear flash data
    for(let k in session.flash_data){
        if(session.flash_data[k].endurance !== 'indefinite' && (!session.flash_data[k].endurance || !(session.flash_data[k].endurance - 1)))
            delete session.flash_data[k];
        else if(session.flash_data[k].endurance !== 'indefinite')
            session.flash_data[k].endurance--;
    }

    Update.session(session, {
        $set: {
            flash_data: session.flash_data
        }
    }, function(err){
        if(err)
            log.error('sessionFlash update error', err);

        cb(null, this);

        if(opts.log_all)
            log.timer('sessionFlash', now);

    }.bind(session));
}

// =====================

function Identify(name){
    Update.session(this, { $set: { name: name }}, (err) => {
        if(err)
            log.error('session.associate: name save error', err);
    });
}

function Flash(field, value, endurance, cb){

    // Return saved field value
    if(typeof value === 'undefined'){
        if(!this.flash_data || !this.flash_data[field])
            return null;

        return this.flash_data[field].val;
    }

    // Save new field value for next session
    else {

        // Endurance represents number of page loads that this variable will last for
        //  (Helpful in redirect situations)
        if(typeof endurance === 'function'){
            cb = endurance;
            endurance = 1;
        }
        else if(!endurance)
            endurance = 1;

        if(!this.flash_data)
            this.flash_data = {};

        this.flash_data[field] = {
            val: value,
            endurance: endurance
        };
        Update.session(this, { $set: { flash_data: this.flash_data }}, (err, doc) => {
            if(err)
                log.error('flash data save error', err);

            if(cb)
                cb(err);
        });
    }
}

// =====================

const Update = {

    session_save: function(cb){
        const session = this;
        Update.session(session, {
            $set: session
        }, cb);
    },

    session: function(session, upd, cb){

        const keys = Update._keys(upd);

        Session.findByIdAndUpdate(
            session._id,
            upd,
            {
                new: true,
                fields: session_fields,
                upsert: true
            }
        ).lean().exec(function(err, doc){
            if(err)
                log.error('Update.session error [', this, ']', err);
            else if(!doc)
                log.error('Update.session no session found!', session);
            else if(doc && opts.log_all)
                log.session(doc, 'Update.session success [', this, ']');

            if(cb)
                cb(err, doc);

        }.bind(keys))
    },

    request: function(socket, upd_in, cb){

        let upd = {};

        for(let k in upd_in){
            // $push: { clicks: id }
            // -->
            // $push: { reqs.$.clicks: id }

            if(!upd[k])
                upd[k] = {};

            for(let k2 in upd_in[k]){
                let req_key = 'reqs.$.' + k2;
                upd[k][req_key] = upd_in[k][k2];
            }
        }

        let keys = Update._keys(upd);

        Session.update({
            _id: socket.session._id,
            "reqs._id": socket.req._id
        }, upd, function(err, raw){

            const socket = this.socket;

            if(err)
                log.error('Update.request error [', this.keys, ']', socket.session._id, socket.req._id, err);
            else if(raw.n < 1)
                log.error('Update.request not found!', socket.session._id, socket.req._id);
            else if(opts.log_all)
                log.session(socket.session, 'Update.request success [', this.keys, ']');

            if(cb)
                return cb(err);

        }.bind({
            socket: socket,
            keys: keys
        }))
    },

    _keys: function(params){

        let keys = [];

        for(let k in params)
            for(let k2 in params[k])
                keys.push(k2);

        return keys;
    }
};

// =====================

function getCookies(src){
    let cookies = cookie.parse(src || '');
    for(let k in cookies){
        if(k.indexOf('na_') === 0){
            try {
                cookies[k] = AES.decrypt(cookies[k]);
            }
            catch(err){
                log.error('getCookies error', err);
                delete cookies[k];
            }
        }
    }

    return cookies;
}

const AES = {
    encrypt: function(value){
        return CryptoJS.AES.encrypt(value, opts.secret).toString();
    },
    decrypt: function(encrypted){
        return CryptoJS.AES.decrypt(encrypted, opts.secret).toString(CryptoJS.enc.Utf8);
    }
};

// =====================

function sessions(options, cb){

    if(!cb){
        cb = options;
        options = { is_bot: false };
    }

    var n = 32;

    Session.find(options)
        .sort({date: 'desc'})
        .limit(n)
        .exec(function(err, results){
            if(err)
                log.error('Sessions query error:', err);

            cb(err, results)
        });
}


var nools = require("nools");

// Constructor for the message class
var Message = function(packet) {
	this.updatePayload = function(packet) {
		this.p_previous = this.p;
		this.p = packet.payload;
		this.changed = this.p_previous != this.p;
		this.retained = packet.retain;
		this.lastChange = this.currentChange;
		this.currentChange = new Date();
	};

	this.changedFromTo = function(from, to) {
		return this.changed && this.p_previous == from && this.p == to;
	};
	this.changedTo = function(to) {
		return this.changed && this.p == to;
	};
	this.changedFrom = function(from) {
		return this.changed && this.p_previous == from;
	};

	this.t = packet.topic;
	this.updatePayload(packet);
	this.currentChange = new Date();
	this.lastChange = undefined;

	//aliases
	this.payload = this.p;
	this.topic = this.t;
};

// Constructor for the clock class
var Clock = function(){
    this.date = new Date();

    this.getHours = function() {
        return this.date.getHours();
    };

    this.getMinutes = function() {
        return this.date.getMinutes();
    };

    this.hoursIsBetween = function(a, b) {
			if(a <= b) return this.date.getHours() >= a && this.date.getHours() <=b;
			else return this.date.getHours() >= a || this.date.getHours() <= b;
    };

    this.step = function(){
        this.date = new Date();
        this.isMorning = this.hoursIsBetween(6, 11);
        this.isNoon = this.hoursIsBetween(12, 14);
        this.isAfternoon = this.hoursIsBetween(15, 17);
        this.isEvening = this.hoursIsBetween(18, 23);
        this.isNight = this.hoursIsBetween(0,5);
        return this;
    };
};

module.exports = function(RED) {

	function NoolsAssert(n) {
		RED.nodes.createNode(this,n);
		var node = this;

		node.session = RED.nodes.getNode(n.session).session;
		node.messages = RED.nodes.getNode(n.session).messages;
		node.clock = RED.nodes.getNode(n.session).clock;

		node.on("input", function(msg) {
			if(!msg.topic) {
				node.warn("Topic must be defined!");
				return;
			}

			if(msg.topic in node.messages) {
				var m = node.messages[msg.topic];
				if(msg.payload !== undefined) {
					m.updatePayload(msg);
					node.session.modify(m);
				} else {
					node.session.retract(m);
				}

			} else {
				if(!msg.payload) {
					return;
				}
				var m = new Message(msg);
				node.messages[msg.topic] = m;
				node.session.assert(m);
			}
			node.session.modify(node.clock);
			node.session.match();
		});
	}
	RED.nodes.registerType("nools-assert", NoolsAssert);

////////////////////////////////////////////////////////////////////////////////
// NoolsFire: Output node of a nools flow                                     //
////////////////////////////////////////////////////////////////////////////////

	function NoolsFire(n) {
		RED.nodes.createNode(this,n);
		var node = this;

		node.name = n.name;
		node.topic = n.topic;

		node.session = RED.nodes.getNode(n.session).session;
		node.messages = RED.nodes.getNode(n.session).messages;
		RED.nodes.getNode(n.session).on("publish", function(msg) {
			if( !node.topic || node.topic === msg.topic) {
				node.send(msg);
			}
		});

		node.session.on("fire", function(name, rule) {
			node.send([null, {
				topic: node.topic,
				payload: name,
				facts: node.session.getFacts(),
				name: name
			}]);
		});
	}
	RED.nodes.registerType("nools-fire", NoolsFire);

////////////////////////////////////////////////////////////////////////////////
// NoolsFlowNode: Configuration node containing the flow and session          //
////////////////////////////////////////////////////////////////////////////////

	function NoolsFlowNode(n) {
		RED.nodes.createNode(this,n);
		var node = this;

		// node.messages contains all messages received by any assert node
		node.messages = {};
		node.clock = new Clock();

		var publish = function(msg) {
			node.emit("publish", msg);
		};

		node.flow = nools.compile(n.flow, {
			name: n.id,
			define: {
				Message: Message,
				Clock: Clock,
				publish: publish
			}
		});
		node.session = node.flow.getSession();
		node.session.assert(node.clock);

		//Run once for init
		node.session.match();

		node.on("close", function() {
			node.session.dispose();
			nools.deleteFlow(n.id);
		});

	}
	RED.nodes.registerType("nools-flow", NoolsFlowNode);
};


/*
 * Copyright (c) 2016 Will Shown. All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

'use strict';

const π = Math.PI,
      { acos, asin, atan2, cos, sin, sqrt, pow } = Math;

const PEELS = 5;

/**
 * ONLY USES VALUES IN RADIANS.
 * LONGITUDE AND LATITUDE ARE LIKE EARTH'S.
 */

const L = acos(sqrt(5) / 5); // the spherical arclength of the icosahedron's edges.

/**
 * Returns the arc length between two positions.
 *
 * @return {number} arc length
 */
function distance(f1_φ, f1_λ, f2_φ, f2_λ) {
  return 2 * asin(sqrt(
      pow(sin((f1_φ - f2_φ) / 2), 2) +
      cos(f1_φ) * cos(f2_φ) * pow(sin((f1_λ - f2_λ) / 2), 2)
    ));
}

/**
 * Returns the course between two positions.
 *
 * @param {object} pos1
 *
 * @param {object} pos2
 *
 * @returns {object} course
 * @returns {number} course.a - the heading at pos1 in radians clockwise from the local meridian
 * @returns {number} course.d - the distance traveled
 */
function course(pos1, pos2) {

  var f1_φ = pos1[0],
      f1_λ = pos1[1],
      f2_φ = pos2[0],
      f2_λ = pos2[1];

  var d = distance(f1_φ, f1_λ, f2_φ, f2_λ), a, course = {};

  if (sin(f2_λ - f1_λ) < 0) {
    a = acos((sin(f2_φ) - sin(f1_φ) * cos(d)) / (sin(d) * cos(f1_φ)));
  } else {
    a = 2 * π - acos((sin(f2_φ) - sin(f1_φ) * cos(d)) / (sin(d) * cos(f1_φ)));
  }

  course.d = d;
  course.a = a;

  return course;
}

/**
 * Returns the position halfway between two positions.
 * Chiefly used to test `interpolate`.
 *
 * @param {object} pos1
 * @param {number} pos1.φ - first latitude in radians
 * @param {number} pos1.λ - first longitude in radians
 *
 * @param {object} pos2
 * @param {number} pos2.φ - second latitude in radians
 * @param {number} pos2.λ - second longitude in radians
 *
 * @return {object} pos3 - result
 * @return {number} pos3.φ - result latitude in radians
 * @return {number} pos3.λ - result longitude in radians
 */
function midpoint(pos1, pos2) {
  var Bx = Math.cos(pos2.φ) * Math.cos(pos2.λ - pos1.λ);
  var By = Math.cos(pos2.φ) * Math.sin(pos2.λ - pos1.λ);

  return {
    φ: atan2(sin(pos1.φ) + sin(pos2.φ),
      sqrt((cos(pos1.φ) + Bx) * (cos(pos1.φ) + Bx) + By * By)),
    λ: pos1.λ + atan2(By, cos(pos1.φ) + Bx)
  };
}

/**
 * Populates buffer `buf` with `d - 1` evenly-spaced positions between two points.
 *
 * @param {number} f1_φ - first position's φ value
 * @param {number} f1_λ - first position's λ value
 *
 * @param {number} f2_φ - second position's φ value
 * @param {number} f2_λ - second position's λ value
 *
 * @param {number} d - number of times to divide the segment between the positions
 * @param {ArrayBuffer} buf - buffer in which to store the result
 */
function interpolate(f1_φ, f1_λ, f2_φ, f2_λ, d, buf) {

  for (var i = 1; i < d; i += 1) {

    let f = i / d,
        Δ = distance(f1_φ, f1_λ, f2_φ, f2_λ);

    let A = sin((1 - f) * Δ) / sin(Δ),
        B = sin(f * Δ) / sin(Δ);

    let x = A * cos(f1_φ) * cos(f1_λ) + B * cos(f2_φ) * cos(f2_λ),
        z = A * cos(f1_φ) * sin(f1_λ) + B * cos(f2_φ) * sin(f2_λ),
        y = A * sin(f1_φ) + B * sin(f2_φ);

    let φ = atan2(y, sqrt(pow(x, 2) + pow(z, 2))),
        λ = atan2(z, x);

    buf[2 * (i - 1) + 0] = φ;
    buf[2 * (i - 1) + 1] = λ;

  }

}

/**
 * Populates buffer `buf` from offset `b` with the center point of positions provided
 *
 * @param buf - buffer in which to store the result
 * @param b - offset in `buf` from which to begin storing the result
 * @param p - an even number of remaining arguments as pairs of coordinates for each position
 */
function centroid(buf, b, ...p) {

  var n = p.length / 2;

  var sum_x = 0,
      sum_z = 0,
      sum_y = 0;

  for (let i = 0; i < n; i += 1) {

    let i_φ = p[2 * i + 0],
        i_λ = p[2 * i + 1];

    sum_x += cos(i_φ) * cos(i_λ);
    sum_z += cos(i_φ) * sin(i_λ);
    sum_y += sin(i_φ);

  }

  var x = sum_x / n,
      z = sum_z / n,
      y = sum_y / n;

  var r = sqrt(
    x * x + z * z + y * y
  );

  var φ = asin(y / r),
      λ = atan2(z, x);

  buf[b + 0] = φ;
  buf[b + 1] = λ;

}

/**
 * Sets the barycenter position of every field on a Sphere.
 * @this {Sphere}
 */
function populate() {

  var d     = this._divisions,
      max_x = 2 * d - 1,
      buf   = new Float64Array((d - 1) * 2);

  this._positions = new Float64Array(( 5 * 2 * d * d + 2 ) * 2);

  // Determine position for polar and tropical fields using only arithmetic.

  this._Fields[0]._setPosition(π / 2, 0);
  this._Fields[1]._setPosition(π / -2, 0);

  for (let s = 0; s < PEELS; s += 1) {

    let λNorth = s * 2 / 5 * π,
        λSouth = s * 2 / 5 * π + π / 5;

    this.get(s, d - 1, 0)._setPosition(π / 2 - L, λNorth);
    this.get(s, max_x, 0)._setPosition(π / -2 + L, λSouth);

  }

  // Determine positions for the fields along the edges using arc interpolation.

  if ((d - 1) > 0) { // d must be at least 2 for there to be fields between vertices.

    for (let s = 0; s < PEELS; s += 1) {

      let p = (s + 4) % PEELS;

      let snP = 0,
          ssP = 1,
          cnT = this.get(s, d - 1, 0)._i,
          pnT = this.get(p, d - 1, 0)._i,
          csT = this.get(s, max_x, 0)._i,
          psT = this.get(p, max_x, 0)._i;

      // north pole to current north tropical pentagon

      interpolate(
        this._positions[2 * snP + 0],
        this._positions[2 * snP + 1],
        this._positions[2 * cnT + 0],
        this._positions[2 * cnT + 1],
        d,
        buf
      );

      for (let i = 1; i < d; i += 1)
        this.get(s, i - 1, 0)
          ._setPosition(buf[2 * (i - 1) + 0], buf[2 * (i - 1) + 1]);

      // current north tropical pentagon to previous north tropical pentagon

      interpolate(
        this._positions[2 * cnT + 0],
        this._positions[2 * cnT + 1],
        this._positions[2 * pnT + 0],
        this._positions[2 * pnT + 1],
        d,
        buf
      );

      for (let i = 1; i < d; i += 1)
        this.get(s, d - 1 - i, i)
          ._setPosition(buf[2 * (i - 1) + 0], buf[2 * (i - 1) + 1]);

      // current north tropical pentagon to previous south tropical pentagon

      interpolate(
        this._positions[2 * cnT + 0],
        this._positions[2 * cnT + 1],
        this._positions[2 * psT + 0],
        this._positions[2 * psT + 1],
        d,
        buf
      );

      for (let i = 1; i < d; i += 1)
        this.get(s, d - 1, i)
          ._setPosition(buf[2 * (i - 1) + 0], buf[2 * (i - 1) + 1]);

      // current north tropical pentagon to current south tropical pentagon

      interpolate(
        this._positions[2 * cnT + 0],
        this._positions[2 * cnT + 1],
        this._positions[2 * csT + 0],
        this._positions[2 * csT + 1],
        d,
        buf
      );

      for (let i = 1; i < d; i += 1)
        this.get(s, d - 1 + i, 0)
          ._setPosition(buf[2 * (i - 1) + 0], buf[2 * (i - 1) + 1]);

      // current south tropical pentagon to previous south tropical pentagon

      interpolate(
        this._positions[2 * csT + 0],
        this._positions[2 * csT + 1],
        this._positions[2 * psT + 0],
        this._positions[2 * psT + 1],
        d,
        buf
      );

      for (let i = 1; i < d; i += 1)
        this.get(s, max_x - i, i)
          ._setPosition(buf[2 * (i - 1) + 0], buf[2 * (i - 1) + 1]);

      // current south tropical pentagon to south pole

      interpolate(
        this._positions[2 * csT + 0],
        this._positions[2 * csT + 1],
        this._positions[2 * ssP + 0],
        this._positions[2 * ssP + 1],
        d,
        buf
      );

      for (let i = 1; i < d; i += 1)
        this.get(s, max_x, i)
          ._setPosition(buf[2 * (i - 1) + 0], buf[2 * (i - 1) + 1]);

    }

  }

  // Determine positions for fields between edges using interpolation.

  if ((d - 2) > 0) { // d must be at least 3 for there to be fields not along edges.

    for (let s = 0; s < PEELS; s += 1) {

      for (let x = 0; x < d * 2; x += 1) {

        // for each column, fill in values for fields between edge fields,
        // whose positions were defined in the previous block.

        if ((x + 1) % d > 0) { // ignore the columns that are edges.

          let j  = d - ((x + 1) % d), // the y index of the field in this column that is along a diagonal edge
              n1 = j - 1, // the number of unpositioned fields before j
              n2 = d - 1 - j, // the number of unpositioned fields after j
              f1 = this.get(s, x, 0)._i, // the field along the early edge
              f2 = this.get(s, x, j)._i, // the field along the diagonal edge
              f3 = this.get(s, x, d - 1)._adjacentFields[2]._i; // the field along the later edge,
                                                                // which will necessarily belong to
                                                                // another section.

          interpolate(
            this._positions[2 * f1 + 0],
            this._positions[2 * f1 + 1],
            this._positions[2 * f2 + 0],
            this._positions[2 * f2 + 1],
            n1 + 1,
            buf
          );

          for (let i = 1; i < j; i += 1)
            this.get(s, x, i)
              ._setPosition(buf[2 * (i - 1) + 0], buf[2 * (i - 1) + 1]);

          interpolate(
            this._positions[2 * f2 + 0],
            this._positions[2 * f2 + 1],
            this._positions[2 * f3 + 0],
            this._positions[2 * f3 + 1],
            n2 + 1,
            buf
          );

          for (let i = j + 1; i < d; i += 1)
            this.get(s, x, i)
              ._setPosition(buf[2 * (i - j - 1) + 0], buf[2 * (i - j - 1) + 1]);

        }

      }

    }

  }

}

module.exports = {
  populate,
  centroid,
  midpoint,
  interpolate,
  course,
  distance
};


"use strict";

var React           = require('react');
var invariant       = require('react/lib/invariant');
var Base            = require('react-app-controller');
var request         = require('react-app-controller/request');
var cloneDeep       = require('lodash.clonedeep');
var assign          = require('lodash.assign');

var ControllerInterface = assign({}, Base.ControllerInterface, {

  /**
   * Override request process so it first fetches the data needed for a page
   * transition.
   */
  process: function(req, cb) {
    req = request.normalizeRequest(req);

    var page;
    try {
      page = this.createPageForRequest(req);
    } catch(err) {
      if (cb)
        return cb(err)
      else
        throw err;
    }

    var needData = typeof page.fetchData === 'function' && !this.state.request.data;

    if (request.isEqual(this.state.request, req) && !needData)
      return;

    fetchDataForRequest(this, page, req, function(err, req) {
      if (err) {
        if (cb)
          return cb(err)
        else
          throw err;
      }
      this.setState({request: req, page: page});
    }.bind(this));
  }
});

var ControllerRenderingInterface = assign({}, Base.ControllerRenderingInterface, {
  renderToString: function(req, cb) {
    invariant(
      typeof cb === 'function',
      'provide callback as a last argument to renderToString(...)'
    );

    req = request.normalizeRequest(req);

    var controller, page;
    try {
      controller = this({request: request.normalizeRequest(req)});
      page = controller.createPageForRequest(req);
    } catch(err) {
      return cb(err);
    }

    fetchDataForRequest(controller, page, req, function(err, req) {
      if (err) return cb(err);

      try {
        React.renderComponentToString(controller, function(markup) {
          cb(null, {markup: markup, request: req, controller: controller});
        });
      } catch (e) {
        cb(e);
      }
    });
  }
});

function fetchDataForRequest(controller, page, req, cb) {
  if (req.data || typeof page.fetchData !== 'function') {
    cb(null, req, page);
  } else {
    page.fetchData(req, function(err, data) {
      if (err) return cb(err);
      req.data = cloneDeep(data);
      cb(null, req, page);
    });
  }
}

function createController(spec) {
  return Base.createController(spec,
    ControllerInterface,
    ControllerRenderingInterface);
}

module.exports = createController;


/* @flow */
'use strict'
/**
 * Gets a set of OGR Parameters for an export
 *
 * @param {object} options - potentially contains a fields object
 */
function create (format, options) {
  const ogrFormat = ogrFormats[format]
  // shapefiles cannot be streamed out of ogr2ogr
  const output = format === 'zip' ? `${options.path || '.'}/${options.name}` : '/vsistdout/'
  const input = options.input || 'layer.vrt'

  let cmd = ['--config', 'SHAPE_ENCODING', 'UTF-8', '-f', ogrFormat, output, input]

  options.geometry = options.geometry && options.geometry.toUpperCase() || 'NONE'
  if (format === 'csv') cmd = csvParams(cmd, options)
  if (format === 'zip') cmd = shapefileParams(cmd, options)
  if (format === 'georss') cmd = georssParams(cmd, options)
  return finishOgrParams(cmd)
}

/**
 * Add parameters specific to a csv export
 *
 * @param {array} cmd - an array of OGR command parts to modify
 * @param {object} options - may contain fields
 * @return {array}
 * @private
 */
function csvParams (cmd, options) {
  cmd.push('-lco', 'WRITE_BOM=YES')
  const hasPointGeom = options.geometry === 'POINT'
  const fields = options.fields.join('|').toLowerCase().split('|')
  const hasXY = fields.indexOf('x') > -1 && fields.indexOf('y') > -1
  if (hasPointGeom && !hasXY) cmd = cmd.concat(['-lco', 'GEOMETRY=AS_XY'])
  return cmd
}

/**
 * Add parameters specific to a GeoRSS export
 *
 * @param {array} cmd - an array of OGR command parts to modify
 * @param {object} options - may contain fields
 * @return {array}
 * @private
 */
function georssParams (cmd, options) {
  cmd.push('-dsco', 'USE_EXTENSIONS=YES')
  return cmd
}

/**
 * Add parameters specific to a shapefile export
 *
 * @param {string} cmd - an array of OGR command parts to modify
 * @param {object} options - may contain a wkid or srs
 * @param {function} callback - calls back back with a modified command array or an error
 * @private
 */
function shapefileParams (cmd, options) {
  // make sure geometries are still written even if the first is null
  if (options.geometry !== 'NONE') cmd.push('-nlt', options.geometry.toUpperCase())
  cmd.push('-fieldmap', 'identity')
  if (!options.ignoreShpLimit) cmd.push('-lco', '2GB_LIMIT=yes')
  if (options.srs) cmd.push('-t_srs', options.srs)
  return cmd
}

/**
 * Adds final parameters to the OGR function call
 * @param {array} cmd - an array of OGR command parts to modify
 * @return {string} the final OGR command
 */
function finishOgrParams (cmd) {
  cmd.push('-append')
  cmd.push('-skipfailures')
  cmd.push('-lco', 'ENCODING=UTF-8')
  return cmd
}

const ogrFormats = {
  kml: 'KML',
  zip: 'ESRI Shapefile',
  csv: 'CSV',
  json: 'GeoJSON',
  geojson: 'GeoJSON',
  gpkg: 'GPKG',
  georss: 'GeoRSS'
}

module.exports = {create}


/*
 * Copyright (c) 2016 Will Shown. All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

'use strict';

const populateInterfieldData = require('./inter-field'),
      intArr                 = require('./int-arr');

const { cos, sin } = Math;

/**
 * Creates edges **between field barycenters** and returns an array of vertices
 * and a corresponding array of faces to use in three.js. Bear in mind, this is
 * not necessarily the most accurate representation of the model.
 *
 * @param sphere
 * @param [options]
 * @param done
 */
function barycenterVerticesAndFaces(sphere, options, done) {

  var n         = sphere._Fields.length,
      positions = new Float32Array(n * 3),
      indices   = sphere._interfieldTriangles,
      colors    = new Float32Array(indices.length * 3);

  for (let f = 0; f < sphere._Fields.length; f += 1) {

    let field = sphere._Fields[f],
        f_φ   = sphere._positions[2 * f + 0],
        f_λ   = sphere._positions[2 * f + 1],
        color = options.colorFn.call(field);

    positions[f * 3 + 0] = cos(f_φ) * cos(f_λ); // x
    positions[f * 3 + 2] = cos(f_φ) * sin(f_λ); // z
    positions[f * 3 + 1] = sin(f_φ); // y

    colors[f * 3 + 0] = color.r;
    colors[f * 3 + 1] = color.g;
    colors[f * 3 + 2] = color.b;

  }

  // normals are exactly positions, as long as radius is 1
  var normals = positions.slice(0);

  if (done) done.call(null, null, {
    positions,
    normals,
    indices,
    colors
  });

}

/**
 * Creates edges between actual fields and returns an array of vertices and a
 * corresponding array of faces to use in three.js. This representation of the
 * model is popularly depicted in the reference materials.
 *
 * @param sphere
 * @param options
 * @param done
 */
function fieldVerticesAndFaces(sphere, options, done) {

  // counter-clockwise face-vertex orders

  const PENT_FACES    = [0, 2, 1, /**/ 0, 4, 2, /**/ 4, 3, 2],
        HEX_FACES     = [0, 2, 1, /**/ 0, 3, 2, /**/ 0, 5, 3, /**/ 5, 4, 3],

        PENT_FACES_CW = [1, 2, 0, /**/ 2, 4, 0, /**/ 2, 3, 4];

  var interfieldTriangles = sphere._interfieldTriangles,
      nPolyEdgeVerts      = interfieldTriangles.length / 3,
      nPolys              = sphere._Fields.length;

  var nDistinctVertices = nPolys * 6 - 12, // 6 vertices for each poly (-12 vertices for the 12 pentagons)
      nTriangles        = nPolys * 4 - 12; // 4 triangles to each hexagon, 3 to each pentagon of which there are 12.

  // support maps
  var fieldPosMap      = intArr(nDistinctVertices, nPolys),
      indexedPositions = new Float32Array(nPolyEdgeVerts * 3);

  // maps for the GPU
  var indices   = intArr(nDistinctVertices, nTriangles * 3),
      positions = new Float32Array(nDistinctVertices * 3),
      normals   = new Float32Array(nDistinctVertices * 3),
      colors    = new Float32Array(nDistinctVertices * 3);

  // populate the cartesian coordinates of positions

  for (let v = 0; v < nPolyEdgeVerts; v += 1) {

    let c_φ = sphere._interfieldCentroids[2 * v + 0],
        c_λ = sphere._interfieldCentroids[2 * v + 1];

    indexedPositions[3 * v + 0] = cos(c_φ) * cos(c_λ); // x
    indexedPositions[3 * v + 2] = cos(c_φ) * sin(c_λ); // z
    indexedPositions[3 * v + 1] = sin(c_φ); // y

  }

  var c = 0, t = 0;

  for (let f = 0; f < nPolys; f += 1) {

    let field          = sphere._Fields[f],
        sides          = field._adjacentFields.length,
        color          = options.colorFn.call(field),
        f_φ            = sphere._positions[2 * f + 0],
        f_λ            = sphere._positions[2 * f + 1],
        polyPosIndices = [];

    fieldPosMap[f] = c;

    for (let s = 0; s < sides; s += 1) {

      let cc = (c + s),
          fi = sphere._interfieldIndices[6 * f + s];

      polyPosIndices.push(fi);

      positions[cc * 3 + 0] = indexedPositions[fi * 3 + 0];
      positions[cc * 3 + 1] = indexedPositions[fi * 3 + 1];
      positions[cc * 3 + 2] = indexedPositions[fi * 3 + 2];

      colors[cc * 3 + 0] = color.r;
      colors[cc * 3 + 1] = color.g;
      colors[cc * 3 + 2] = color.b;

      normals[cc * 3 + 0] = cos(f_φ) * cos(f_λ);
      normals[cc * 3 + 2] = cos(f_φ) * sin(f_λ);
      normals[cc * 3 + 1] = sin(f_φ);

    }

    let faces;

    if (f === 1) {
      faces = PENT_FACES_CW;
    } else {
      faces = sides === 5 ? PENT_FACES : HEX_FACES;
    }

    for (let v = 0; v < faces.length; v += 1) {

      let tt = (t + v);

      indices[tt] = c + faces[v];

    }

    c += sides;
    t += faces.length;

  }

  done.call(null, null, {
    positions,
    indices,
    normals,
    colors
  });

}

function getVerticesAndFaces(options, done) {
  populateInterfieldData.call(this);
  // will route to different algorithms depending on options, but only does barycenter technique for now.
  switch (options.type) {
    case 'poly-per-field':
      return fieldVerticesAndFaces(this, options, done);
      break;
    case 'vertex-per-field':
    default:
      return barycenterVerticesAndFaces(this, options, done);
      break;
  }
}

module.exports = getVerticesAndFaces;


/*
 * Copyright (c) 2016 Will Shown. All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

'use strict';

const inside = require('point-in-polygon');

import { Bitmap } from 'geof-util';

const populateInterfieldData = require('./inter-field');

const π = Math.PI,
      τ = 2 * Math.PI;

const { min, max, ceil, floor } = Math;

/**
 * Returns a field's edge vertices and its bounds. Latitudinal coordinates may be
 * greater than π if the field straddles the meridian across from 0.
 *
 * @this Field
 * @returns {{min_φ: Number, max_φ: number, min_λ: Number, max_λ: number, vertices: Array}}
 * @private
 */
function _fieldGeometry() {

  const ifi = this._parent._interfieldIndices,
        ifc = this._parent._interfieldCentroids,
        i   = this._i;

  var max_φ    = -Infinity,
      min_φ    = Infinity,
      max_λ    = -Infinity,
      min_λ    = Infinity,
      mid_λ    = this._parent._positions[2 * i + 1],
      vertices = [];

  for (let v = 0; v < this._adjacentFields.length; v += 1) {

    let φ = ifc[2 * ifi[6 * i + v] + 0],
        λ = ifc[2 * ifi[6 * i + v] + 1];

    max_φ = max(max_φ, φ);
    min_φ = min(min_φ, φ);

    max_λ = max(max_λ, λ);
    min_λ = min(min_λ, λ);

    vertices.push([λ, φ]);

  }

  if (i === 0) {
    max_φ = π / 2;
  }

  if (i === 1) {
    min_φ = π / -2;
  }

  if (i < 2) {
    min_λ = -π;
    max_λ = π;
    vertices = [
      [min_λ, max_φ],
      [max_λ, max_φ],
      [max_λ, min_φ],
      [min_λ, min_φ]
    ];
  } else if (max_λ > 0 && min_λ < 0 && (mid_λ < π / -2 || mid_λ > π / 2)) {
    // this spans the meridian, so shift negative λ values past π and recalculate latitudinal bounds

    max_λ = -Infinity;
    min_λ = Infinity;

    for (let v = 0; v < vertices.length; v += 1) {

      if (vertices[v][0] < 0) vertices[v][0] += τ;

      max_λ = max(max_λ, vertices[v][0]);
      min_λ = min(min_λ, vertices[v][0]);

    }

  }

  return {
    min_φ,
    max_φ,
    min_λ,
    max_λ,
    vertices
  };

}

/**
 * Writes booleans indicating which pixel corners lay inside a field's edges to a bitmap.
 * Returns the bounds within which writing took place.
 *
 * @param bmp - the Bitmap instance containing the grid of values
 * @param geo - result of `_fieldGeometry` analysis
 * @param w - width of the dataset
 * @param h - height of the dataset
 * @returns {{min_x: number, max_x: number, min_y: number, max_y: number}}
 * @private
 */
function _populateSelectionGrid(bmp, geo, w, h) {

  var w_rect = τ / w,
      h_rect = π / h;

  var min_x = floor(geo.min_λ / w_rect + w / 2),
      max_x = ceil(geo.max_λ / w_rect + w / 2),
      min_y = floor(geo.min_φ / h_rect + h / 2),
      max_y = ceil(geo.max_φ / h_rect + h / 2);

  for (let x = min_x; x <= max_x; x += 1) {

    for (let y = min_y; y <= max_y; y += 1) {

      bmp.write(x % w, y, inside(
        [x * w_rect - π, y * h_rect - π / 2],
        geo.vertices
      ));

    }

  }

  return {
    min_x,
    max_x,
    min_y,
    max_y
  };

}

/**
 * Returns a value from 0 to 4 indicating how many corners of a pixel lay within a
 * Field's edges.
 *
 * @param bmp – Bitmap instance
 * @param x - cartesian x-position of the pixel
 * @param y – cartesian y-position of the pixel
 * @returns {number}
 * @private
 */
function _testPoints(bmp, x, y) {

  var result = 0,
      w      = bmp._w;

  if (bmp.get((x + 0) % w, y + 0)) result++;
  if (bmp.get((x + 0) % w, y + 1)) result++;
  if (bmp.get((x + 1) % w, y + 0)) result++;
  if (bmp.get((x + 1) % w, y + 1)) result++;

  return result;

}

/**
 * A function that sets field data based on raster data. A flat array of numbers, `data`, is
 * supplied representing `height` rows of `width` identical rectangles, each rectangle comprising
 * `depth` points of data. A plate carrée projection is assumed.
 *
 * `map` is a function that receives `depth` arguments, each the weighted mean value of the data in
 * that channel, and called with each field's context. It can't be asynchronous.
 *
 * `done` is a Node-style completion callback called when setting field data is complete.
 *
 * @this {Sphere}
 *
 * @param {ArrayBuffer} data
 * @param {number} width
 * @param {number} height
 * @param {number} depth
 * @param {function} map
 * @param {function} done
 */
function fromRaster(data, width, height, depth, map, done) {

  var sphere = this;

  populateInterfieldData.call(sphere);

  var bmp = new Bitmap(width, height);

  sphere._Fields.forEach(function (field) {

    var geo       = _fieldGeometry.call(field),
        selection = _populateSelectionGrid(bmp, geo, width, height);

    var valSums   = [],
        weightSum = 0;

    for (let z = 0; z < depth; z += 1) {
      valSums[z] = 0;
    }

    for (let x = selection.min_x; x < selection.max_x; x += 1) {

      for (let y = selection.min_y; y < selection.max_y; y += 1) {

        let w = _testPoints(bmp, x, y) / 4;

        for (let z = 0; z < depth; z += 1) {
          valSums[z] += data[(height - y - 1) * width * depth + (width - x - 1) * depth + z] * w;
        }

        weightSum += w;

      }

    }

    if (weightSum <= 0) debugger; // weight sum should never be non-positive

    map.apply(field, valSums.map((val) => {
      return val / weightSum
    }));

    bmp.clear(
      selection.max_x - selection.min_x,
      selection.max_y - selection.min_y,
      selection.min_x,
      selection.min_y
    );

  });

  done();

}

module.exports = fromRaster;


function keyValue(args, getter, setter){
    var attrs = {},
        keys,
        key = args[0],
        value = args[1];

    if(typeof key === 'object'){
        attrs = key;
    }else if(args.length === 1){
        return this[0] ? getter(this[0]) : null;
    }else{
        attrs[key] = value;
    };

    keys = Object.keys(attrs);

    return this.each(function(el){
        keys.forEach(function(key){
            setter(el, key, attrs);
        });
    });
};

// 查找节点，返回一个可操作的节点数组
function tethys(selector, context){

    var nodes = [];

    // 把参数转换为包含Node的数组
    if(selector.each && selector.on){
        // tethys 对象
        return selector;
    }else if(typeof selector === 'string'){
        // html代码或选择器
        if(selector.match(/^[^\b\B]*\</)){
            // html代码
            nodes = tethys.parseHtml(selector);
        }else{
            // 选择器
            nodes = (context || document).querySelectorAll(selector);
        };
    }else if(Array.isArray(selector) || selector.constructor === NodeList){
        // 包含节点的数组或NodeList
        nodes = selector;
    }else{
        // 节点
        nodes = [selector];
    };

    // 当Node被appendChild方法添加到其它元素中后，该Node会被从它所在的NodeList中移除
    // 为了避免这种情况，我们要把NodeList转换成包含Node的数组
    nodes = Array.prototype.map.call(nodes, function(n){
        return n;
    });

    // 给数组添加dom操作方法
    tethys.extend(nodes, tethys.fn);

    return nodes;
};

// 扩展
tethys.extend = function(){
    var args = arguments,
        deep = false,
        dest,
        prop = Array.prototype;

    if (typeof args[0] === 'boolean') {
        deep = prop.shift.call(args);
    };

    dest = prop.shift.call(args);

    prop.forEach.call(args, function (src) {
        Object.keys(src).forEach(function (key) {
            if (deep && typeof src[key] === 'object' && typeof dest[key] === 'object') {
                extend(true, dest[key], src[key]);
            } else if (typeof src[key] !== 'undefined') {
                dest[key] = src[key];
            };
        });
    });
    return dest;
};

// 合并数组
tethys.merge = function(ary1, ary2){
    (ary2 || []).forEach(function(n){
        ary1.push(n);
    });
};

// 把html代码转换成NodeList
tethys.parseHtml = function(str){
    var div = document.createElement('DIV');
    div.innerHTML = str;
    return div.childNodes;
};

// 微型模板
tethys.tpl = function(s, o) {
    var SUBREGEX = /\{\s*([^|}]+?)\s*(?:\|([^}]*))?\s*\}/g;
    return s.replace ? s.replace(SUBREGEX, function (match, key) {
        return typeof o[key] === 'undefined' ? match : o[key];
    }) : s;
};

//
tethys.fn = {

    // 遍历
    each: function(fn){

        Array.prototype.forEach.call(this || [], fn);

        return this;
    },

    // 绑定事件
    on: function(events, fn){

        events = events.split(/\s*\,\s*/);

        return this.each(function(el){

            fn = fn.bind(el);

            events.forEach(function(event){
                el.addEventListener(event, fn);
            });
        });
    },

    // 设置css
    // css('color', 'red')
    // css({ color: 'red' })
    css: function(key, value){

        var format = function(key){
            return key.replace(/(-([a-z]))/g, function(s, s1, s2){
                return s2.toUpperCase();
            });
        };

        return keyValue.call(this, arguments, function(el){
            return el.style[format(key)];
        }, function(el, key, attrs){
            el.style[format(key)] = attrs[key] + '';
        });
    },

    // 设置或者返回属性
    attr: function(key, value){

        return keyValue.call(this, arguments, function(el){
            return el.getAttribute(key);
        }, function(el, key, attrs){
            el.setAttribute(key, attrs[key] + '');
        });
    },

    // 检查是否有class
    hasClass: function(cls){
        var has = false, reg = new RegExp('\\b' + cls + '\\b');

        this.each(function(el){
            has = has || !!el.className.match(reg);
        });

        return has;
    },

    // 添加class
    addClass: function(cls, type){
        var reg = new RegExp('\\b' + cls + '\\b');

        // 为所有节点添加或删除class
        return this.each(function(el){
            var name = el.className;

            if(typeof name !== 'string') return;

            if(type === 'remove'){
                // remove
                if(name.match(reg)) {
                    el.className = name.replace(reg, '');
                }
            }else{
                // add
                if(!name.match(reg)) {
                    el.className += ' ' + cls;
                }
            }
        });
    },

    // 删除class
    removeClass: function(cls){
        return this.addClass(cls, 'remove');
    },

    // 设置html
    html: function(html){
        return this.each(function(el){
            el.innerHTML = html;
        });
    },

    // 显示
    show: function(){
        return this.each(function(el){
            if(el.style.display === 'none'){
                el.style.display = el.getAttribute('o-d') || '';
            };
        });
    },

    // 隐藏
    hide: function(){
        return this.each(function(el){
            if(el.style.display !== 'none') {
                el.setAttribute('o-d', el.style.display);
                el.style.display = 'none';
            };
        });
    },

    // 切换显示隐藏
    toggle: function(){
        return this.each(function(el){
            var e = $(el);
            e.css("display") == "none" ? e.show() : e.hide();
        });
    },

    // 追加节点
    append: function(child){

        var children = tethys(child);

        return this.each(function(el){
            children.each(function(child, i){
                el.appendChild(child);
            });
        });
    },

    // 查找
    find: function(selector){
        var nodes = [];

        this.each(function(el){
            tethys(selector, el).each(function(node){
                nodes.push(node);
            });
        });

        return tethys(nodes);
    }

};

export default tethys;


/**
 * Returns a new Babel Transformer that uses the supplied schema to transform
 * template strings tagged with `Relay.QL` into an internal representation of
 * GraphQL queries.
 */

const fs = require('fs');

const log = (msg) => {
  console.log('[transform-relay-hot]', msg);
};

// file changes watcher
function watcherFn(schemaFilepath, watchInterval, reinitBabelRelayPlugin, prevMtime) {
  try {
    let stats;
    try {
      stats = fs.statSync(schemaFilepath);
    } catch (e) {
      // no problem
    }
    if (stats) {
      if (!prevMtime) prevMtime = stats.mtime;
      if (stats.mtime.getTime() !== prevMtime.getTime()) {
        prevMtime = stats.mtime;
        reinitBabelRelayPlugin();
      }
    }
    setTimeout(
      () => {
        watcherFn(schemaFilepath, watchInterval, reinitBabelRelayPlugin, prevMtime);
      },
      watchInterval
    ).unref(); // fs.watch blocks babel from exit, so using `setTimeout` with `unref`
  } catch (e) {
    log(e);
  }
}

// babelRelayPlugin initializer
function initBabelRelayPlugin(pluginOptions, babel, ref) {
  const verbose = !!pluginOptions.verbose;
  const schemaFilepath = pluginOptions.schema || '';
  let schema;

  try {
    schema = fs.readFileSync(schemaFilepath, 'utf8');
  } catch (e) {
    schema = null;
    log('Cannot load GraphQL Schema from file \''
                 + schemaFilepath + '\': ' + e);
  }

  if (schema) {
    if (verbose) {
      log('GraphQL Schema loaded successfully from \'' + schemaFilepath + '\'');
    }
    ref.babelRelayPlugin = require('babel-plugin-relay')(babel);
  } else {
    // empty Plugin
    log('Relay.QL will not be transformed, cause `schema.data` is empty.');
    ref.babelRelayPlugin = {
      visitor: {
        Program: function () {},
        TaggedTemplateExpression: function () {},
      },
    };
  }
}

function initProxy(babel, state) {
  const ref = {};
  const pluginOptions = state.opts || {};

  if (pluginOptions.schemaJsonFilepath) {
    log(
      '[transform-relay-hot] Please rename `schemaJsonFilepath` option in .babelrc:'
      + '\n   {'
      + '\n     "plugins": ['
      + '\n       ["transform-relay-hot", {'
      + '\n 🛑 from:  "schemaJsonFilepath": "./pathToSchema.json",'
      + '\n 👌   to:  "schema": "./pathToSchema.json",'
      + '\n       }]'
      + '\n     ]'
      + '\n   }'
    );
    pluginOptions.schema = pluginOptions.schemaJsonFilepath;
  }

  if (!pluginOptions.schema || pluginOptions.schema === '') {
    log(
      '[transform-relay-hot] You should provide `schema` option in .babelrc:'
      + '\n   {'
      + '\n     "plugins": ['
      + '\n       ["transform-relay-hot", {'
      + '\n         "schema": "./pathToSchema.json",'
      + '\n         "watchInterval": 2000'
      + '\n       }]'
      + '\n     ]'
      + '\n   }'
    );
  }

  // HACK obtain/update babelRelayPlugin by reference
  initBabelRelayPlugin(pluginOptions, babel, ref);

  const watchInterval = pluginOptions && pluginOptions.watchInterval
    ? pluginOptions.watchInterval
    : 2000;
  if (watchInterval > 0 && pluginOptions.schema) {
    const reinitBabelRelayPlugin = () => {
      log('Re-init babel-plugin-relay');
      // decache all babel-plugin-relay modules
      Object.keys(require.cache).forEach((key) => {
        if (key.indexOf('babel-plugin-relay') > 0) {
          delete require.cache[key];
        }
      });
      Object.keys(module.constructor._pathCache).forEach((key) => {
        if (key.indexOf('babel-plugin-relay') > 0) {
          delete module.constructor._pathCache[key];
        }
      });
      initBabelRelayPlugin(pluginOptions, babel, ref);
    };
    watcherFn(pluginOptions.schema, watchInterval, reinitBabelRelayPlugin);
  }

  return ref;
}

// babel plugin proxy
module.exports = function (babel) {
  let ref;
  return {
    visitor: {
      /**
       * Extract the module name from `@providesModule`.
       */
      Program(path, state) {
        // HACK ONLY ONCE obtain plugin configs form .babelrc and init our proxy
        if (!ref) ref = initProxy(babel, state);

        const fn = ref.babelRelayPlugin.visitor.Program;
        if (fn) fn(path, state);
      },

      /**
       * Transform Relay.QL`...`.
       */
      TaggedTemplateExpression(path, state) {
        const fn = ref.babelRelayPlugin.visitor.TaggedTemplateExpression;
        if (fn) fn(path, state);
      },
    },
  };
};


/*!
* tap.js
* Copyright (c) 2015 Alex Gibson
* https://github.com/alexgibson/tap.js/
* Released under MIT license
*/

function Tap(el) {
    this.el = typeof el === 'object' ? el : document.getElementById(el);
    this.moved = false; //flags if the finger has moved
    this.startX = 0; //starting x coordinate
    this.startY = 0; //starting y coordinate
    this.hasTouchEventOccured = false; //flag touch event
    this.el.addEventListener('touchstart', this, false);
    this.el.addEventListener('mousedown', this, false);
}

// return true if left click is in the event, handle many browsers
Tap.prototype.leftButton = function(event) {
    // modern & preferred:  MSIE>=9, Firefox(all)
    if ('buttons' in event) {
        // https://developer.mozilla.org/docs/Web/API/MouseEvent/buttons
        return event.buttons === 1;
    } else {
        return 'which' in event ?
            // 'which' is well defined (and doesn't exist on MSIE<=8)
            // https://developer.mozilla.org/docs/Web/API/MouseEvent/which
            event.which === 1 :
            // for MSIE<=8 button is 1=left (0 on all other browsers)
            // https://developer.mozilla.org/docs/Web/API/MouseEvent/button
            event.button === 1;
    }
};

Tap.prototype.start = function(e) {
    if (e.type === 'touchstart') {

        this.hasTouchEventOccured = true;
        this.el.addEventListener('touchmove', this, false);
        this.el.addEventListener('touchend', this, false);
        this.el.addEventListener('touchcancel', this, false);

    } else if (e.type === 'mousedown' && this.leftButton(e)) {

        this.el.addEventListener('mousemove', this, false);
        this.el.addEventListener('mouseup', this, false);
    }

    this.moved = false;
    this.startX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
    this.startY = e.type === 'touchstart' ? e.touches[0].clientY : e.clientY;
};

Tap.prototype.move = function(e) {
    //if finger moves more than 10px flag to cancel
    var x = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
    var y = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY;
    if (Math.abs(x - this.startX) > 10 || Math.abs(y - this.startY) > 10) {
        this.moved = true;
    }
};

Tap.prototype.end = function(e) {
    var evt;

    this.el.removeEventListener('touchmove', this, false);
    this.el.removeEventListener('touchend', this, false);
    this.el.removeEventListener('touchcancel', this, false);
    this.el.removeEventListener('mouseup', this, false);
    this.el.removeEventListener('mousemove', this, false);

    if (!this.moved) {
        //create custom event
        try {
            evt = new window.CustomEvent('tap', {
                bubbles: true,
                cancelable: true
            });
        } catch (e) {
            evt = document.createEvent('Event');
            evt.initEvent('tap', true, true);
        }

        //prevent touchend from propagating to any parent
        //nodes that may have a tap.js listener attached
        e.stopPropagation();

        // dispatchEvent returns false if any handler calls preventDefault,
        if (!e.target.dispatchEvent(evt)) {
            // in which case we want to prevent clicks from firing.
            e.preventDefault();
        }
    }
};

Tap.prototype.cancel = function() {
    this.hasTouchEventOccured = false;
    this.moved = false;
    this.startX = 0;
    this.startY = 0;
};

Tap.prototype.destroy = function() {
    this.el.removeEventListener('touchstart', this, false);
    this.el.removeEventListener('touchmove', this, false);
    this.el.removeEventListener('touchend', this, false);
    this.el.removeEventListener('touchcancel', this, false);
    this.el.removeEventListener('mousedown', this, false);
    this.el.removeEventListener('mouseup', this, false);
    this.el.removeEventListener('mousemove', this, false);
};

Tap.prototype.handleEvent = function(e) {
    switch (e.type) {
        case 'touchstart': this.start(e); break;
        case 'touchmove': this.move(e); break;
        case 'touchend': this.end(e); break;
        case 'touchcancel': this.cancel(e); break;
        case 'mousedown': this.start(e); break;
        case 'mouseup': this.end(e); break;
        case 'mousemove': this.move(e); break;
    }
};

export default Tap;


import './action-sheet.css'
import $ from '../lib/tethys.js'
import Tap from '../lib/tap.js'

const tpl =
    '
\

\

\

';

const buttonTpl = '
{text}
';

var ActionSheet = function(opt){

    // 默认参数
    opt = $.extend({
        buttons: {},
        inTime: 500,
        outTime: 500
    }, opt);

    // 渲染
    this.render().update(opt.buttons);
};

// 绑定点击事件
function bindTapEvent(el, fn){
    new Tap(el);
    el.addEventListener('tap', fn, false);
}

ActionSheet.prototype = {

    // 初始化渲染
    render: function(){
        var doc = document.documentElement;

        this.el = $(tpl);

        this.el.hide().css({
            width: doc.clientWidth + 'px',
            height: doc.clientHeight + 'px'
        });

        bindTapEvent(this.el.find('.as-cover')[0], this.hide.bind(this));

        $('body').append(this.el);

        return this;
    },

    // 显示
    show: function(){

        this.el.show();
        this.el.addClass('as-in');

        setTimeout(function(){
            this.el.removeClass('as-in');
        }.bind(this), 350);

        return this;
    },

    // 隐藏
    hide: function(){

        this.el.addClass('as-out');

        setTimeout(function(){
            this.el.removeClass('as-out').hide();
        }.bind(this), 300);

        return this;
    },

    // 更新按钮
    update: function(buttons){
        var buttonContainer = this.el.find('.as-buttons');

        // 清空按钮容器
        buttonContainer.html('');

        // 添加取消按钮
        buttons['取消'] = this.hide.bind(this);

        // 遍历创建按钮
        Object.keys(buttons).forEach(function(key){
            var n = buttons[key],
                btn = $($.tpl(buttonTpl, {
                    text: key
                }));

            // 绑定tap事件
            bindTapEvent(btn[0], function(e){

                e.stopPropagation();
                e.preventDefault();

                // 如果参数是函数则调用
                // 如果是字符串则认为是url直接跳转
                if(typeof this.action === 'function'){
                    this.action.call(this.context, e);
                }else if(typeof this.action === 'string'){
                    location.href = this.action;
                };
            }.bind({action: n, context: this}));

            // 触摸反馈
            btn.on('touchstart', function(e){
                $(e.target).addClass('as-active');
            }).on('touchend', function(e){
                $(e.target).removeClass('as-active');
            });

            // 添加到按钮容器
            buttonContainer.append(btn);
        }.bind(this));

        return this;
    }
};

export default ActionSheet;