<commit_before>import DatasetController from '../core/core.datasetController';
import {isArray, valueOrDefault} from '../helpers/helpers.core';
import {toRadians, PI, TAU, HALF_PI} from '../helpers/helpers.math';

/**
 * @typedef { import("../core/core.controller").default } Chart
 */

function getRatioAndOffset(rotation, circumference, cutout) {
	let ratioX = 1;
	let ratioY = 1;
	let offsetX = 0;
	let offsetY = 0;
	// If the chart's circumference isn't a full circle, calculate size as a ratio of the width/height of the arc
	if (circumference < TAU) {
		let startAngle = rotation % TAU;
		startAngle += startAngle >= PI ? -TAU : startAngle < -PI ? TAU : 0;
		const endAngle = startAngle + circumference;
		const startX = Math.cos(startAngle);
		const startY = Math.sin(startAngle);
		const endX = Math.cos(endAngle);
		const endY = Math.sin(endAngle);
		const contains0 = (startAngle <= 0 && endAngle >= 0) || endAngle >= TAU;
		const contains90 = (startAngle <= HALF_PI && endAngle >= HALF_PI) || endAngle >= TAU + HALF_PI;
		const contains180 = startAngle === -PI || endAngle >= PI;
		const contains270 = (startAngle <= -HALF_PI && endAngle >= -HALF_PI) || endAngle >= PI + HALF_PI;
		const minX = contains180 ? -1 : Math.min(startX, startX * cutout, endX, endX * cutout);
		const minY = contains270 ? -1 : Math.min(startY, startY * cutout, endY, endY * cutout);
		const maxX = contains0 ? 1 : Math.max(startX, startX * cutout, endX, endX * cutout);
		const maxY = contains90 ? 1 : Math.max(startY, startY * cutout, endY, endY * cutout);
		ratioX = (maxX - minX) / 2;
		ratioY = (maxY - minY) / 2;
		offsetX = -(maxX + minX) / 2;
		offsetY = -(maxY + minY) / 2;
	}
	return {ratioX, ratioY, offsetX, offsetY};
}

export default class DoughnutController extends DatasetController {

	constructor(chart, datasetIndex) {
		super(chart, datasetIndex);

		this.enableOptionSharing = true;
		this.innerRadius = undefined;
		this.outerRadius = undefined;
		this.offsetX = undefined;
		this.offsetY = undefined;
	}

	linkScales() {}

	/**
	 * Override data parsing, since we are not using scales
	 */
	parse(start, count) {
		const data = this.getDataset().data;
		const meta = this._cachedMeta;
		let i, ilen;
		for (i = start, ilen = start + count; i < ilen; ++i) {
			meta._parsed[i] = +data[i];
		}
	}

	// Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly
	getRingIndex(datasetIndex) {
		let ringIndex = 0;

		for (let j = 0; j < datasetIndex; ++j) {
			if (this.chart.isDatasetVisible(j)) {
				++ringIndex;
			}
		}

		return ringIndex;
	}

	/**
	 * Get the maximal rotation & circumference extents
	 * across all visible datasets.
	 */
	_getRotationExtents() {
		let min = TAU;
		let max = -TAU;

		const me = this;
		const opts = me.chart.options;

		for (let i = 0; i < me.chart.data.datasets.length; ++i) {
			if (me.chart.isDatasetVisible(i)) {
				const dataset = me.chart.data.datasets[i];
				const rotation = toRadians(valueOrDefault(dataset.rotation, opts.rotation) - 90);
				const circumference = toRadians(valueOrDefault(dataset.circumference, opts.circumference));

				min = Math.min(min, rotation);
				max = Math.max(max, rotation + circumference);
			}
		}

		return {
			rotation: min,
			circumference: max - min,
		};
	}

	/**
	 * @param {string} mode
	 */
	update(mode) {
		const me = this;
		const chart = me.chart;
		const {chartArea, options} = chart;
		const meta = me._cachedMeta;
		const arcs = meta.data;
		const cutout = options.cutoutPercentage / 100 || 0;
		const chartWeight = me._getRingWeight(me.index);

		// Compute the maximal rotation & circumference limits.
		// If we only consider our dataset, this can cause problems when two datasets
		// are both less than a circle with different rotations (starting angles)
		const {circumference, rotation} = me._getRotationExtents();
		const {ratioX, ratioY, offsetX, offsetY} = getRatioAndOffset(rotation, circumference, cutout);
		const spacing = me.getMaxBorderWidth() + me.getMaxOffset(arcs);
		const maxWidth = (chartArea.right - chartArea.left - spacing) / ratioX;
		const maxHeight = (chartArea.bottom - chartArea.top - spacing) / ratioY;
		const outerRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0);
		const innerRadius = Math.max(outerRadius * cutout, 0);
		const radiusLength = (outerRadius - innerRadius) / me._getVisibleDatasetWeightTotal();
		me.offsetX = offsetX * outerRadius;
		me.offsetY = offsetY * outerRadius;

		meta.total = me.calculateTotal();

		me.outerRadius = outerRadius - radiusLength * me._getRingWeightOffset(me.index);
		me.innerRadius = Math.max(me.outerRadius - radiusLength * chartWeight, 0);

		me.updateElements(arcs, 0, arcs.length, mode);
	}

	/**
	 * @private
	 */
	_circumference(i, reset) {
		const me = this;
		const opts = me.chart.options;
		const meta = me._cachedMeta;
		const circumference = toRadians(valueOrDefault(me._config.circumference, opts.circumference));
		return reset && opts.animation.animateRotate ? 0 : this.chart.getDataVisibility(i) ? me.calculateCircumference(meta._parsed[i] * circumference / TAU) : 0;
	}

	updateElements(arcs, start, count, mode) {
		const me = this;
		const reset = mode === 'reset';
		const chart = me.chart;
		const chartArea = chart.chartArea;
		const opts = chart.options;
		const animationOpts = opts.animation;
		const centerX = (chartArea.left + chartArea.right) / 2;
		const centerY = (chartArea.top + chartArea.bottom) / 2;
		const animateScale = reset && animationOpts.animateScale;
		const innerRadius = animateScale ? 0 : me.innerRadius;
		const outerRadius = animateScale ? 0 : me.outerRadius;
		const firstOpts = me.resolveDataElementOptions(start, mode);
		const sharedOptions = me.getSharedOptions(firstOpts);
		const includeOptions = me.includeOptions(mode, sharedOptions);
		let startAngle = toRadians(valueOrDefault(me._config.rotation, opts.rotation) - 90);
		let i;

		for (i = 0; i < start; ++i) {
			startAngle += me._circumference(i, reset);
		}

		for (i = start; i < start + count; ++i) {
			const circumference = me._circumference(i, reset);
			const arc = arcs[i];
			const properties = {
				x: centerX + me.offsetX,
				y: centerY + me.offsetY,
				startAngle,
				endAngle: startAngle + circumference,
				circumference,
				outerRadius,
				innerRadius
			};
			if (includeOptions) {
				properties.options = sharedOptions || me.resolveDataElementOptions(i, mode);
			}
			startAngle += circumference;

			me.updateElement(arc, i, properties, mode);
		}
		me.updateSharedOptions(sharedOptions, mode, firstOpts);
	}

	calculateTotal() {
		const meta = this._cachedMeta;
		const metaData = meta.data;
		let total = 0;
		let i;

		for (i = 0; i < metaData.length; i++) {
			const value = meta._parsed[i];
			if (!isNaN(value) && this.chart.getDataVisibility(i)) {
				total += Math.abs(value);
			}
		}

		return total;
	}

	calculateCircumference(value) {
		const total = this._cachedMeta.total;
		if (total > 0 && !isNaN(value)) {
			return TAU * (Math.abs(value) / total);
		}
		return 0;
	}

	getLabelAndValue(index) {
		const me = this;
		const meta = me._cachedMeta;
		const chart = me.chart;
		const labels = chart.data.labels || [];

		return {
			label: labels[index] || '',
			value: meta._parsed[index],
		};
	}

	getMaxBorderWidth(arcs) {
		const me = this;
		let max = 0;
		const chart = me.chart;
		let i, ilen, meta, controller, options;

		if (!arcs) {
			// Find the outmost visible dataset
			for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) {
				if (chart.isDatasetVisible(i)) {
					meta = chart.getDatasetMeta(i);
					arcs = meta.data;
					controller = meta.controller;
					if (controller !== me) {
						controller.configure();
					}
					break;
				}
			}
		}

		if (!arcs) {
			return 0;
		}

		for (i = 0, ilen = arcs.length; i < ilen; ++i) {
			options = controller.resolveDataElementOptions(i);
			if (options.borderAlign !== 'inner') {
				max = Math.max(max, options.borderWidth || 0, options.hoverBorderWidth || 0);
			}
		}
		return max;
	}

	getMaxOffset(arcs) {
		let max = 0;

		for (let i = 0, ilen = arcs.length; i < ilen; ++i) {
			const options = this.resolveDataElementOptions(i);
			max = Math.max(max, options.offset || 0, options.hoverOffset || 0);
		}
		return max;
	}

	/**
	 * Get radius length offset of the dataset in relation to the visible datasets weights. This allows determining the inner and outer radius correctly
	 * @private
	 */
	_getRingWeightOffset(datasetIndex) {
		let ringWeightOffset = 0;

		for (let i = 0; i < datasetIndex; ++i) {
			if (this.chart.isDatasetVisible(i)) {
				ringWeightOffset += this._getRingWeight(i);
			}
		}

		return ringWeightOffset;
	}

	/**
	 * @private
	 */
	_getRingWeight(datasetIndex) {
		return Math.max(valueOrDefault(this.chart.data.datasets[datasetIndex].weight, 1), 0);
	}

	/**
	 * Returns the sum of all visible data set weights.
	 * @private
	 */
	_getVisibleDatasetWeightTotal() {
		return this._getRingWeightOffset(this.chart.data.datasets.length) || 1;
	}
}

DoughnutController.id = 'doughnut';

/**
 * @type {any}
 */
DoughnutController.defaults = {
	datasetElementType: false,
	dataElementType: 'arc',
	dataElementOptions: [
		'backgroundColor',
		'borderColor',
		'borderWidth',
		'borderAlign',
		'offset'
	],
	animation: {
		numbers: {
			type: 'number',
			properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y', 'offset', 'borderWidth']
		},
		// Boolean - Whether we animate the rotation of the Doughnut
		animateRotate: true,
		// Boolean - Whether we animate scaling the Doughnut from the centre
		animateScale: false
	},
	aspectRatio: 1,
	legend: {
		labels: {
			generateLabels(chart) {
				const data = chart.data;
				if (data.labels.length && data.datasets.length) {
					return data.labels.map((label, i) => {
						const meta = chart.getDatasetMeta(0);
						const style = meta.controller.getStyle(i);

						return {
							text: label,
							fillStyle: style.backgroundColor,
							strokeStyle: style.borderColor,
							lineWidth: style.borderWidth,
							hidden: !chart.getDataVisibility(i),

							// Extra data used for toggling the correct item
							index: i
						};
					});
				}
				return [];
			}
		},

		onClick(e, legendItem, legend) {
			legend.chart.toggleDataVisibility(legendItem.index);
			legend.chart.update();
		}
	},

	// The percentage of the chart that we cut out of the middle.
	cutoutPercentage: 50,

	// The rotation of the chart, where the first data arc begins.
	rotation: 0,

	// The total circumference of the chart.
	circumference: 360,

	// Need to override these to give a nice default
	tooltips: {
		callbacks: {
			title() {
				return '';
			},
			label(tooltipItem) {
				let dataLabel = tooltipItem.label;
				const value = ': ' + tooltipItem.formattedValue;

				if (isArray(dataLabel)) {
					// show value on first line of multiline label
					// need to clone because we are changing the value
					dataLabel = dataLabel.slice();
					dataLabel[0] += value;
				} else {
					dataLabel += value;
				}

				return dataLabel;
			}
		}
	}
};
<commit_msg>Normalize rotation and circumference resolution (#8015)<commit_after>import DatasetController from '../core/core.datasetController';
import {isArray, valueOrDefault} from '../helpers/helpers.core';
import {toRadians, PI, TAU, HALF_PI} from '../helpers/helpers.math';

/**
 * @typedef { import("../core/core.controller").default } Chart
 */

function getRatioAndOffset(rotation, circumference, cutout) {
	let ratioX = 1;
	let ratioY = 1;
	let offsetX = 0;
	let offsetY = 0;
	// If the chart's circumference isn't a full circle, calculate size as a ratio of the width/height of the arc
	if (circumference < TAU) {
		let startAngle = rotation % TAU;
		startAngle += startAngle >= PI ? -TAU : startAngle < -PI ? TAU : 0;
		const endAngle = startAngle + circumference;
		const startX = Math.cos(startAngle);
		const startY = Math.sin(startAngle);
		const endX = Math.cos(endAngle);
		const endY = Math.sin(endAngle);
		const contains0 = (startAngle <= 0 && endAngle >= 0) || endAngle >= TAU;
		const contains90 = (startAngle <= HALF_PI && endAngle >= HALF_PI) || endAngle >= TAU + HALF_PI;
		const contains180 = startAngle === -PI || endAngle >= PI;
		const contains270 = (startAngle <= -HALF_PI && endAngle >= -HALF_PI) || endAngle >= PI + HALF_PI;
		const minX = contains180 ? -1 : Math.min(startX, startX * cutout, endX, endX * cutout);
		const minY = contains270 ? -1 : Math.min(startY, startY * cutout, endY, endY * cutout);
		const maxX = contains0 ? 1 : Math.max(startX, startX * cutout, endX, endX * cutout);
		const maxY = contains90 ? 1 : Math.max(startY, startY * cutout, endY, endY * cutout);
		ratioX = (maxX - minX) / 2;
		ratioY = (maxY - minY) / 2;
		offsetX = -(maxX + minX) / 2;
		offsetY = -(maxY + minY) / 2;
	}
	return {ratioX, ratioY, offsetX, offsetY};
}

export default class DoughnutController extends DatasetController {

	constructor(chart, datasetIndex) {
		super(chart, datasetIndex);

		this.enableOptionSharing = true;
		this.innerRadius = undefined;
		this.outerRadius = undefined;
		this.offsetX = undefined;
		this.offsetY = undefined;
	}

	linkScales() {}

	/**
	 * Override data parsing, since we are not using scales
	 */
	parse(start, count) {
		const data = this.getDataset().data;
		const meta = this._cachedMeta;
		let i, ilen;
		for (i = start, ilen = start + count; i < ilen; ++i) {
			meta._parsed[i] = +data[i];
		}
	}

	// Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly
	getRingIndex(datasetIndex) {
		let ringIndex = 0;

		for (let j = 0; j < datasetIndex; ++j) {
			if (this.chart.isDatasetVisible(j)) {
				++ringIndex;
			}
		}

		return ringIndex;
	}

	/**
	 * @private
	 */
	_getRotation() {
		return toRadians(valueOrDefault(this._config.rotation, this.chart.options.rotation) - 90);
	}

	/**
	 * @private
	 */
	_getCircumference() {
		return toRadians(valueOrDefault(this._config.circumference, this.chart.options.circumference));
	}

	/**
	 * Get the maximal rotation & circumference extents
	 * across all visible datasets.
	 */
	_getRotationExtents() {
		let min = TAU;
		let max = -TAU;

		const me = this;

		for (let i = 0; i < me.chart.data.datasets.length; ++i) {
			if (me.chart.isDatasetVisible(i)) {
				const controller = me.chart.getDatasetMeta(i).controller;
				const rotation = controller._getRotation();
				const circumference = controller._getCircumference();

				min = Math.min(min, rotation);
				max = Math.max(max, rotation + circumference);
			}
		}

		return {
			rotation: min,
			circumference: max - min,
		};
	}

	/**
	 * @param {string} mode
	 */
	update(mode) {
		const me = this;
		const chart = me.chart;
		const {chartArea, options} = chart;
		const meta = me._cachedMeta;
		const arcs = meta.data;
		const cutout = options.cutoutPercentage / 100 || 0;
		const chartWeight = me._getRingWeight(me.index);

		// Compute the maximal rotation & circumference limits.
		// If we only consider our dataset, this can cause problems when two datasets
		// are both less than a circle with different rotations (starting angles)
		const {circumference, rotation} = me._getRotationExtents();
		const {ratioX, ratioY, offsetX, offsetY} = getRatioAndOffset(rotation, circumference, cutout);
		const spacing = me.getMaxBorderWidth() + me.getMaxOffset(arcs);
		const maxWidth = (chartArea.right - chartArea.left - spacing) / ratioX;
		const maxHeight = (chartArea.bottom - chartArea.top - spacing) / ratioY;
		const outerRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0);
		const innerRadius = Math.max(outerRadius * cutout, 0);
		const radiusLength = (outerRadius - innerRadius) / me._getVisibleDatasetWeightTotal();
		me.offsetX = offsetX * outerRadius;
		me.offsetY = offsetY * outerRadius;

		meta.total = me.calculateTotal();

		me.outerRadius = outerRadius - radiusLength * me._getRingWeightOffset(me.index);
		me.innerRadius = Math.max(me.outerRadius - radiusLength * chartWeight, 0);

		me.updateElements(arcs, 0, arcs.length, mode);
	}

	/**
	 * @private
	 */
	_circumference(i, reset) {
		const me = this;
		const opts = me.chart.options;
		const meta = me._cachedMeta;
		const circumference = me._getCircumference();
		return reset && opts.animation.animateRotate ? 0 : this.chart.getDataVisibility(i) ? me.calculateCircumference(meta._parsed[i] * circumference / TAU) : 0;
	}

	updateElements(arcs, start, count, mode) {
		const me = this;
		const reset = mode === 'reset';
		const chart = me.chart;
		const chartArea = chart.chartArea;
		const opts = chart.options;
		const animationOpts = opts.animation;
		const centerX = (chartArea.left + chartArea.right) / 2;
		const centerY = (chartArea.top + chartArea.bottom) / 2;
		const animateScale = reset && animationOpts.animateScale;
		const innerRadius = animateScale ? 0 : me.innerRadius;
		const outerRadius = animateScale ? 0 : me.outerRadius;
		const firstOpts = me.resolveDataElementOptions(start, mode);
		const sharedOptions = me.getSharedOptions(firstOpts);
		const includeOptions = me.includeOptions(mode, sharedOptions);
		let startAngle = me._getRotation();
		let i;

		for (i = 0; i < start; ++i) {
			startAngle += me._circumference(i, reset);
		}

		for (i = start; i < start + count; ++i) {
			const circumference = me._circumference(i, reset);
			const arc = arcs[i];
			const properties = {
				x: centerX + me.offsetX,
				y: centerY + me.offsetY,
				startAngle,
				endAngle: startAngle + circumference,
				circumference,
				outerRadius,
				innerRadius
			};
			if (includeOptions) {
				properties.options = sharedOptions || me.resolveDataElementOptions(i, mode);
			}
			startAngle += circumference;

			me.updateElement(arc, i, properties, mode);
		}
		me.updateSharedOptions(sharedOptions, mode, firstOpts);
	}

	calculateTotal() {
		const meta = this._cachedMeta;
		const metaData = meta.data;
		let total = 0;
		let i;

		for (i = 0; i < metaData.length; i++) {
			const value = meta._parsed[i];
			if (!isNaN(value) && this.chart.getDataVisibility(i)) {
				total += Math.abs(value);
			}
		}

		return total;
	}

	calculateCircumference(value) {
		const total = this._cachedMeta.total;
		if (total > 0 && !isNaN(value)) {
			return TAU * (Math.abs(value) / total);
		}
		return 0;
	}

	getLabelAndValue(index) {
		const me = this;
		const meta = me._cachedMeta;
		const chart = me.chart;
		const labels = chart.data.labels || [];

		return {
			label: labels[index] || '',
			value: meta._parsed[index],
		};
	}

	getMaxBorderWidth(arcs) {
		const me = this;
		let max = 0;
		const chart = me.chart;
		let i, ilen, meta, controller, options;

		if (!arcs) {
			// Find the outmost visible dataset
			for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) {
				if (chart.isDatasetVisible(i)) {
					meta = chart.getDatasetMeta(i);
					arcs = meta.data;
					controller = meta.controller;
					if (controller !== me) {
						controller.configure();
					}
					break;
				}
			}
		}

		if (!arcs) {
			return 0;
		}

		for (i = 0, ilen = arcs.length; i < ilen; ++i) {
			options = controller.resolveDataElementOptions(i);
			if (options.borderAlign !== 'inner') {
				max = Math.max(max, options.borderWidth || 0, options.hoverBorderWidth || 0);
			}
		}
		return max;
	}

	getMaxOffset(arcs) {
		let max = 0;

		for (let i = 0, ilen = arcs.length; i < ilen; ++i) {
			const options = this.resolveDataElementOptions(i);
			max = Math.max(max, options.offset || 0, options.hoverOffset || 0);
		}
		return max;
	}

	/**
	 * Get radius length offset of the dataset in relation to the visible datasets weights. This allows determining the inner and outer radius correctly
	 * @private
	 */
	_getRingWeightOffset(datasetIndex) {
		let ringWeightOffset = 0;

		for (let i = 0; i < datasetIndex; ++i) {
			if (this.chart.isDatasetVisible(i)) {
				ringWeightOffset += this._getRingWeight(i);
			}
		}

		return ringWeightOffset;
	}

	/**
	 * @private
	 */
	_getRingWeight(datasetIndex) {
		return Math.max(valueOrDefault(this.chart.data.datasets[datasetIndex].weight, 1), 0);
	}

	/**
	 * Returns the sum of all visible data set weights.
	 * @private
	 */
	_getVisibleDatasetWeightTotal() {
		return this._getRingWeightOffset(this.chart.data.datasets.length) || 1;
	}
}

DoughnutController.id = 'doughnut';

/**
 * @type {any}
 */
DoughnutController.defaults = {
	datasetElementType: false,
	dataElementType: 'arc',
	dataElementOptions: [
		'backgroundColor',
		'borderColor',
		'borderWidth',
		'borderAlign',
		'offset'
	],
	animation: {
		numbers: {
			type: 'number',
			properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y', 'offset', 'borderWidth']
		},
		// Boolean - Whether we animate the rotation of the Doughnut
		animateRotate: true,
		// Boolean - Whether we animate scaling the Doughnut from the centre
		animateScale: false
	},
	aspectRatio: 1,
	legend: {
		labels: {
			generateLabels(chart) {
				const data = chart.data;
				if (data.labels.length && data.datasets.length) {
					return data.labels.map((label, i) => {
						const meta = chart.getDatasetMeta(0);
						const style = meta.controller.getStyle(i);

						return {
							text: label,
							fillStyle: style.backgroundColor,
							strokeStyle: style.borderColor,
							lineWidth: style.borderWidth,
							hidden: !chart.getDataVisibility(i),

							// Extra data used for toggling the correct item
							index: i
						};
					});
				}
				return [];
			}
		},

		onClick(e, legendItem, legend) {
			legend.chart.toggleDataVisibility(legendItem.index);
			legend.chart.update();
		}
	},

	// The percentage of the chart that we cut out of the middle.
	cutoutPercentage: 50,

	// The rotation of the chart, where the first data arc begins.
	rotation: 0,

	// The total circumference of the chart.
	circumference: 360,

	// Need to override these to give a nice default
	tooltips: {
		callbacks: {
			title() {
				return '';
			},
			label(tooltipItem) {
				let dataLabel = tooltipItem.label;
				const value = ': ' + tooltipItem.formattedValue;

				if (isArray(dataLabel)) {
					// show value on first line of multiline label
					// need to clone because we are changing the value
					dataLabel = dataLabel.slice();
					dataLabel[0] += value;
				} else {
					dataLabel += value;
				}

				return dataLabel;
			}
		}
	}
};
<|endoftext|>


<commit_before><commit_msg>add test for recent calc fix<commit_after><|endoftext|>


<commit_before>/*
 * Copyright 2015 the Mimu Authors (Dan Bornstein et alia).
 * Licensed AS IS and WITHOUT WARRANTY under the Apache License,
 * Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0>
 */

"use strict";

define(["./StyleUtil"], function(StyleUtil) {

/** Fraction of the canvas width to take as the width of a thin line. */
var THIN_LINE_AMT = 0.002;

/** Fraction of the canvas width to take as the width of a thick line. */
var THICK_LINE_AMT = 0.00275;

/**
 * Canvas-drawn widget shell. This class provides the basic mechanism for
 * a complex widget to be built out within a specified container node
 * (typically a `div`). Drawing is assumed to be done by layering a foreground
 * and background canvas, where the former is more actively modified than the
 * latter. Subclasses are expected to fill in rendering functions and event
 * handling as desired.
 *
 * This class takes care of creating the foreground and background canvases,
 * sizing them such that when rendered they have square pixels, and adding them
 * to the DOM. It also has a few utilities (e.g. accessing the style) to help
 * simplify subclasses.
 */
class CanvasWidget {
    /**
     * Takes a DOM `node` to insert into. The node should be an empty container.
     * `cssClass` is the class that will be assigned to a `div` which will be
     * used as the source for style information.
     */
    constructor(node, cssClass) {
        var doc = node.ownerDocument;
        var style;

        // Make a `div` to hold the two canvases. It has to be set for
        // explicit relative positioning, otherwise the child nodes "leak"
        // out.
        this._divNode = doc.createElement("div");
        style = this._divNode.style;
        style.position = "relative";
        style.width = "100%";
        style.height = "100%";
        node.appendChild(this._divNode);

        /** The node which is consulted for CSS style info. */
        this._styleNode = doc.createElement("span");
        this._styleNode.className = cssClass;
        this._styleNode.display = "hidden";
        this._divNode.appendChild(this._styleNode);

        /** The fixed background canvas. */
        this._background = doc.createElement("canvas");
        style = this._background.style;
        style.position = "absolute";
        style.top = "0";
        style.left = "0";
        style.width = "100%";
        style.height = "100%";
        style.zIndex = "1";

        /** The main display canvas. */
        this._foreground = doc.createElement("canvas");
        style = this._foreground.style;
        style.position = "absolute";
        style.top = "0";
        style.left = "0";
        style.width = "100%";
        style.height = "100%";
        style.zIndex = "2";

        this._adjustCanvasSizes();
        this._autoAdjustCanvasSizes();
        this._autoRefresh();
        this._divNode.appendChild(this.background);
        this._divNode.appendChild(this.foreground);
    }

    /**
     * Sets up periodic auto-refresh of the canvases. This is mostly of use
     * as a hackish way to be able to react to style changes (including notably
     * font loading) without getting too fancy.
     */
    _autoRefresh() {
        var outerThis = this;
        var view = this.defaultView;

        function refresh() {
            // We add a little bit of randomness to the delay time to avoid
            // having a massive global heartbeat.
            outerThis.render();
            view.setTimeout(refresh, 1000 + (Math.random() * 500));
        }
        refresh();
    }

    /**
     * Sets up a listener to automatically adjust the canvas sizes if the
     * view gets resized. This attempts to only do a canvas resize after the
     * user is done adjusting the window.
     */
    _autoAdjustCanvasSizes() {
        var outerThis = this;
        var view = this.defaultView;
        var pending = 0;

        view.addEventListener("resize", function() {
            pending++;
            view.setTimeout(function() {
                pending--;
                if (pending === 0) {
                    outerThis._adjustCanvasSizes();
                }
            }, 500);
        });
    }

    /**
     * Sets up the dimensions of the canvases.
     */
    _adjustCanvasSizes() {
        // Copy the dimensions of the canvas from the `div`, which ensures the
        // aspect ratio remains the same. Thus, the canvases will have square
        // pixels.
        var node = this._divNode;
        var fg = this._foreground;
        var bg = this._background;
        var width = node.clientWidth;
        var height = node.clientHeight;

        if ((width != fg.width) || (height != fg.height)) {
            fg.width = width;
            fg.height = height;
            bg.width = width;
            bg.height = height;
            this.render();
        }
    }

    /**
     * Sets the opacity of the foreground. By default it is 100% opaque.
     */
    set foregroundOpacity(value) {
        this._foreground.style.opacity = value;
    }

    /**
     * Gets the "default view" (typically the window of the document).
     */
    get defaultView() {
        return this._divNode.ownerDocument.defaultView;
    }

    /**
     * Gets the foreground canvas.
     */
    get foreground() {
        return this._foreground;
    }

    /**
     * Gets the background canvas.
     */
    get background() {
        return this._background;
    }

    /**
     * Gets the line width to use for "thin" lines.
     */
    get thinLineWidth() {
        return Math.max(1, Math.floor(this._foreground.width * THIN_LINE_AMT));
    }

    /**
     * Gets the line width to use for "thick" lines.
     */
    get thickLineWidth() {
        return Math.max(1, Math.floor(this._foreground.width * THICK_LINE_AMT));
    }

    /**
     * Gets the computed style. The result will be read-only.
     */
    getComputedStyle() {
        return StyleUtil.getComputedStyle(this._styleNode);
    }

    /**
     * Gets a modifiable clone of the computed style.
     */
    cloneComputedStyle() {
        return StyleUtil.cloneComputedStyle(this._styleNode);
    }

    /**
     * Renders the background. Subclasses are expected to override this.
     */
    renderBackground() {
        var canvas = this.background;
        var style = StyleUtil.cloneComputedStyle(this._styleNode);
        var ctx = canvas.getContext("2d");

        ctx.fillStyle = Color.parse(style.backgroundColor);
        ctx.fillRect(0, 0, canvas.width, canvas.height);
    }

    /**
     * Renders the foreground. Subclasses are expected to override this.
     */
    renderForeground() {
        var canvas = this.foreground;
        var ctx = canvas.getContext("2d");

        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

    /**
     * Renders both background and foreground. Subclasses should not override
     * this.
     */
    render() {
        this.renderBackground();
        this.renderForeground();
    }
}

return CanvasWidget;
});
<commit_msg>Tweak docs.<commit_after>/*
 * Copyright 2015 the Mimu Authors (Dan Bornstein et alia).
 * Licensed AS IS and WITHOUT WARRANTY under the Apache License,
 * Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0>
 */

"use strict";

define(["./StyleUtil"], function(StyleUtil) {

/** Fraction of the canvas width to take as the width of a thin line. */
var THIN_LINE_AMT = 0.002;

/** Fraction of the canvas width to take as the width of a thick line. */
var THICK_LINE_AMT = 0.00275;

/**
 * Canvas-drawn widget shell. This class provides the basic mechanism for
 * a complex widget to be built out within a specified container node
 * (typically an empty `div` or `td`). Drawing is assumed to be done by
 * layering a foreground and background canvas, where the former is more
 * actively modified than the latter. Subclasses are expected to fill in
 * rendering functions and event handling as desired.
 *
 * This class takes care of creating the foreground and background canvases,
 * sizing them such that when rendered they have square pixels, and adding them
 * to the DOM. It also has a few utilities (e.g. accessing the style) to help
 * simplify subclasses.
 */
class CanvasWidget {
    /**
     * Takes a DOM `node` to insert into. The node should be an empty container.
     * `cssClass` is the class that will be assigned to a `div` which will be
     * used as the source for style information.
     */
    constructor(node, cssClass) {
        var doc = node.ownerDocument;
        var style;

        // Make a `div` to hold the two canvases. It has to be set for
        // explicit relative positioning, otherwise the child nodes "leak"
        // out.
        this._divNode = doc.createElement("div");
        style = this._divNode.style;
        style.position = "relative";
        style.width = "100%";
        style.height = "100%";
        node.appendChild(this._divNode);

        /** The node which is consulted for CSS style info. */
        this._styleNode = doc.createElement("span");
        this._styleNode.className = cssClass;
        this._styleNode.display = "hidden";
        this._divNode.appendChild(this._styleNode);

        /** The fixed background canvas. */
        this._background = doc.createElement("canvas");
        style = this._background.style;
        style.position = "absolute";
        style.top = "0";
        style.left = "0";
        style.width = "100%";
        style.height = "100%";
        style.zIndex = "1";

        /** The main display canvas. */
        this._foreground = doc.createElement("canvas");
        style = this._foreground.style;
        style.position = "absolute";
        style.top = "0";
        style.left = "0";
        style.width = "100%";
        style.height = "100%";
        style.zIndex = "2";

        this._adjustCanvasSizes();
        this._autoAdjustCanvasSizes();
        this._autoRefresh();
        this._divNode.appendChild(this.background);
        this._divNode.appendChild(this.foreground);
    }

    /**
     * Sets up periodic auto-refresh of the canvases. This is mostly of use
     * as a hackish way to be able to react to style changes (including notably
     * font loading) without getting too fancy.
     */
    _autoRefresh() {
        var outerThis = this;
        var view = this.defaultView;

        function refresh() {
            // We add a little bit of randomness to the delay time to avoid
            // having a massive global heartbeat.
            outerThis.render();
            view.setTimeout(refresh, 1000 + (Math.random() * 500));
        }
        refresh();
    }

    /**
     * Sets up a listener to automatically adjust the canvas sizes if the
     * view gets resized. This attempts to only do a canvas resize after the
     * user is done adjusting the window.
     */
    _autoAdjustCanvasSizes() {
        var outerThis = this;
        var view = this.defaultView;
        var pending = 0;

        view.addEventListener("resize", function() {
            pending++;
            view.setTimeout(function() {
                pending--;
                if (pending === 0) {
                    outerThis._adjustCanvasSizes();
                }
            }, 500);
        });
    }

    /**
     * Sets up the dimensions of the canvases.
     */
    _adjustCanvasSizes() {
        // Copy the dimensions of the canvas from the `div`, which ensures the
        // aspect ratio remains the same. Thus, the canvases will have square
        // pixels.
        var node = this._divNode;
        var fg = this._foreground;
        var bg = this._background;
        var width = node.clientWidth;
        var height = node.clientHeight;

        if ((width != fg.width) || (height != fg.height)) {
            fg.width = width;
            fg.height = height;
            bg.width = width;
            bg.height = height;
            this.render();
        }
    }

    /**
     * Sets the opacity of the foreground. By default it is 100% opaque.
     */
    set foregroundOpacity(value) {
        this._foreground.style.opacity = value;
    }

    /**
     * Gets the "default view" (typically the window of the document).
     */
    get defaultView() {
        return this._divNode.ownerDocument.defaultView;
    }

    /**
     * Gets the foreground canvas.
     */
    get foreground() {
        return this._foreground;
    }

    /**
     * Gets the background canvas.
     */
    get background() {
        return this._background;
    }

    /**
     * Gets the line width to use for "thin" lines.
     */
    get thinLineWidth() {
        return Math.max(1, Math.floor(this._foreground.width * THIN_LINE_AMT));
    }

    /**
     * Gets the line width to use for "thick" lines.
     */
    get thickLineWidth() {
        return Math.max(1, Math.floor(this._foreground.width * THICK_LINE_AMT));
    }

    /**
     * Gets the computed style. The result will be read-only.
     */
    getComputedStyle() {
        return StyleUtil.getComputedStyle(this._styleNode);
    }

    /**
     * Gets a modifiable clone of the computed style.
     */
    cloneComputedStyle() {
        return StyleUtil.cloneComputedStyle(this._styleNode);
    }

    /**
     * Renders the background. Subclasses are expected to override this.
     */
    renderBackground() {
        var canvas = this.background;
        var style = StyleUtil.cloneComputedStyle(this._styleNode);
        var ctx = canvas.getContext("2d");

        ctx.fillStyle = Color.parse(style.backgroundColor);
        ctx.fillRect(0, 0, canvas.width, canvas.height);
    }

    /**
     * Renders the foreground. Subclasses are expected to override this.
     */
    renderForeground() {
        var canvas = this.foreground;
        var ctx = canvas.getContext("2d");

        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

    /**
     * Renders both background and foreground. Subclasses should not override
     * this.
     */
    render() {
        this.renderBackground();
        this.renderForeground();
    }
}

return CanvasWidget;
});
<|endoftext|>


<commit_before>describe('/research behaviour', () => {

    afterEach(() => {
        cy.visitPage('/logout')
    })

    it('Should redirect if the user has not logged in', () => {
      cy.visitPage('/research')
      cy.url().should('include', 'login')
    })

    it('Should be accesible if the user is an admin', () => {
      cy.adminSignIn()
      cy.visitPage('/research')
      cy.url().should('include', 'research')
    })

    it('Should be accesible if the user is not an admin', () => {
      cy.userSignIn()
      cy.visitPage('/research')
      cy.url().should('include', 'research')
    })

    it('Should be a form with an input', () => {
        cy.userSignIn()
        cy.visitPage('/research')
        cy.get('form[role="search"]')
          .find('input')
    })

    it.skip('Should have an input text as a valid stock symbol', () => {
        const stockSymbol = "AAPL"
        cy.adminSignIn()
        cy.visitPage('/research')
        cy.get('.form-control')
          .clear()
          .type(stockSymbol)

        cy.get('form').then(form$ => {
            form$.on('submit', e => {
                e.preventDefault()
            })
        })

        cy.get('button[type="submit"]')
          .first()
          .click()

        cy.get('form')
          .should('have.attr', 'method', 'get')
          .should('have.attr', 'action', '/research')

        cy.get('input[name="url"]')
          .should('have.attr', 'value', 'https://finance.yahoo.com/quote/')

        cy.get('input[name="symbol"]')
          .should('have.attr', 'value', stockSymbol)

      })
})

<commit_msg>Redefined test assertion<commit_after>describe('/research behaviour', () => {

    afterEach(() => {
        cy.visitPage('/logout')
    })

    it('Should redirect if the user has not logged in', () => {
      cy.visitPage('/research')
      cy.url().should('include', 'login')
    })

    it('Should be accesible if the user is an admin', () => {
      cy.adminSignIn()
      cy.visitPage('/research')
      cy.url().should('include', 'research')
    })

    it('Should be accesible if the user is not an admin', () => {
      cy.userSignIn()
      cy.visitPage('/research')
      cy.url().should('include', 'research')
    })

    it('Should be a form with an input', () => {
        cy.userSignIn()
        cy.visitPage('/research')
        cy.get('form[role="search"]')
          .find('input')
    })

    it('Should have an input text as a valid stock symbol', () => {
        const stockSymbol = "AAPL"
        cy.adminSignIn()
        cy.visitPage('/research')
        cy.get('.form-control')
          .clear()
          .type(stockSymbol)

        cy.get('form')
          .should('have.attr', 'action', '/research')
          .invoke('attr', 'action', '/skip')

        cy.get('button[type="submit"]')
          .first()
          .click()

        cy.url().should('include', 'https%3A%2F%2Ffinance.yahoo.com%2Fquote%2F&symbol=AAPL')
      })
})

<|endoftext|>


<commit_before>'use strict';

var async = require('async');
var strategy = require('./strategy');

var Config = require('./Config');

/**
 * ConfigLoader
 * Represents a configuration loader that loads a configuration through a list of strategies.
 *
 * @constructor
 */
function ConfigLoader(strategies, logger) {
  this.strategies = [];

  if (logger) {
    if (typeof logger.debug !== 'function') {
      throw new Error('Provided logger is required to have method debug().');
    }
    this.logger = logger;
  }

  if (strategies && !(strategies instanceof Array)) {
    throw new Error("Argument 'strategies' must be an array.");
  }

  for (var i = 0; i < strategies.length; i++) {
    this.add(strategies[i]);
  }
}

ConfigLoader.prototype.add = function (item) {
  if (!item || typeof item.process !== 'function') {
    throw new Error("Unable to add strategy. Strategy is either empty or missing required method 'process'.");
  }

  if (this.logger) {
    if (typeof item.setLogger === 'function') {
      item.setLogger(this.logger);
    }
    this.strategies.push(new strategy.DebugConfigStrategy(this.logger, 'Before:' + item.constructor.name));
  }

  this.strategies.push(item);
};

ConfigLoader.prototype.prepend = function (item) {
  if (!item || typeof item.process !== 'function') {
    throw new Error("Unable to prepend strategy. Strategy is either empty or missing required method 'process'.");
  }

  if (this.logger) {
    if (typeof item.setLogger === 'function') {
      item.setLogger(this.logger);
    }
    this.strategies.unshift(new strategy.DebugConfigStrategy(this.logger, 'Before:' + item.constructor.name));
  }

  this.strategies.unshift(item);
};

ConfigLoader.prototype.load = function (callback) {
  var logger = this.logger;
  var strategies = this.strategies.slice();

  // Ensure that we always start out with a empty config object.
  strategies.unshift(new strategy.NullConfigStrategy(new Config()));

  var tasks = [];

  for (var i = 0; i < strategies.length; i++) {
    tasks.push(strategies[i].process.bind(strategies[i]));
  }

  async.waterfall(tasks, function (err, result) {
    if (logger) {
      if (err) {
        logger.debug("Error:\n" + JSON.stringify(err, null, 4) + '\n');
      } else {
        logger.debug("Result:\n" + JSON.stringify(result, null, 4) + '\n');
      }
    }
    callback(err, result);
  });
};

module.exports = ConfigLoader;
<commit_msg>use single quotes instead of double quotes<commit_after>'use strict';

var async = require('async');
var strategy = require('./strategy');

var Config = require('./Config');

/**
 * ConfigLoader
 * Represents a configuration loader that loads a configuration through a list of strategies.
 *
 * @constructor
 */
function ConfigLoader(strategies, logger) {
  this.strategies = [];

  if (logger) {
    if (typeof logger.debug !== 'function') {
      throw new Error('Provided logger is required to have method debug().');
    }
    this.logger = logger;
  }

  if (strategies && !(strategies instanceof Array)) {
    throw new Error('Argument \'strategies\' must be an array.');
  }

  for (var i = 0; i < strategies.length; i++) {
    this.add(strategies[i]);
  }
}

ConfigLoader.prototype.add = function (item) {
  if (!item || typeof item.process !== 'function') {
    throw new Error('Unable to add strategy. Strategy is either empty or missing required method \'process\'.');
  }

  if (this.logger) {
    if (typeof item.setLogger === 'function') {
      item.setLogger(this.logger);
    }
    this.strategies.push(new strategy.DebugConfigStrategy(this.logger, 'Before:' + item.constructor.name));
  }

  this.strategies.push(item);
};

ConfigLoader.prototype.prepend = function (item) {
  if (!item || typeof item.process !== 'function') {
    throw new Error('Unable to prepend strategy. Strategy is either empty or missing required method \'process\'.');
  }

  if (this.logger) {
    if (typeof item.setLogger === 'function') {
      item.setLogger(this.logger);
    }
    this.strategies.unshift(new strategy.DebugConfigStrategy(this.logger, 'Before:' + item.constructor.name));
  }

  this.strategies.unshift(item);
};

ConfigLoader.prototype.load = function (callback) {
  var logger = this.logger;
  var strategies = this.strategies.slice();

  // Ensure that we always start out with a empty config object.
  strategies.unshift(new strategy.NullConfigStrategy(new Config()));

  var tasks = [];

  for (var i = 0; i < strategies.length; i++) {
    tasks.push(strategies[i].process.bind(strategies[i]));
  }

  async.waterfall(tasks, function (err, result) {
    if (logger) {
      if (err) {
        logger.debug('Error:\n' + JSON.stringify(err, null, 4) + '\n');
      } else {
        logger.debug('Result:\n' + JSON.stringify(result, null, 4) + '\n');
      }
    }
    callback(err, result);
  });
};

module.exports = ConfigLoader;
<|endoftext|>


<commit_before>console.log("=================================");
console.log("Starting Resist! server v0.1");
console.log("---------------------------------");

var sys = require("sys"),
	path = require("path"),
	url = require("url"),
	app = require('http').createServer(handler),
  	io = require('socket.io').listen(app),
  	fs = require('fs');

io.set('log level', 1); // reduce logging
app.listen(8080);

function handler (request, response) {
	var my_path = url.parse(request.url).pathname;
    var full_path = path.join(process.cwd(),my_path);

    if (full_path.indexOf("index.html") >= 0 || request.url === "/") {
    	fs.readFile(__dirname + '/client/index.html',
		  function (err, data) {
		    response.writeHead(200);
		    response.end(data);
		  });
    }
    else {
	    fs.exists(full_path,function(exists){

	        if(!exists){
	            response.writeHeader(404, {"Content-Type": "text/plain"});
	            response.write("404 Not Found\n");
	            response.end();
	        }
	        else{
	            fs.readFile(full_path, "binary", function(err, file) {
	                 if(err) {
	                     response.writeHeader(500, {"Content-Type": "text/plain"});
	                     response.write(err + "\n");
	                     response.end();
	                 }
	                 else{
	                    response.writeHeader(200);
	                    response.write(file, "binary");
	                    response.end();
	                }

	            });
	        }
	    });
	}
}

// =================================================
// Game logic
// -------------------------------------------------

var Map = require('./lib/resist/Map/Map'),
	MapLoader = require('./lib/resist/Map/MapLoader'),
	ctxMap = new Map();

MapLoader('./client/maps/town.json', ctxMap);

var ResistUnit = require("./lib/resist/ResistUnit");
var NetworkConnector = require("./lib/resist/NetworkConnector");
var Sprite = require("./lib/resist/sprite/Sprite");
var Wave = require("./lib/resist/Wave");
var UnitManager = require("./lib/resist/UnitManager");

var unitManager = new UnitManager();
var time;
var currentWave = null;
var waveNumber = 1;
var maxWaveNumber = 1;

io.sockets.on('connection', function (socket) {

	// When connecting, send credentials and notice other players
	socket.on("send-credentials", function (data) {
		var player = unitManager.createPlayerUnit(socket, data);

		socket.emit("credentials-result", {
			id : player.id
		});

		socket.on('disconnect', function () {
			unitManager.deleteUnit(player.id);
		});
	});
});

/*
 * Must dispatch an event to players and client players
 */
function receiveEvent(eventType, spriteDestId, data) {
	for (var i = 0; i < playerList.length; i++) {
		playerList[i].toDigestEventList.push(new Array(eventType, spriteDestId, data));
		playerList[i].connector.sendEvent(eventType, spriteDestId, data);
	}
}

// -------------------------------------------------
// Game loop
// -------------------------------------------------
setInterval(loop, 25);

function loop() {
    if (unitManager.getRealPlayerCount() == 0) {
    	if (currentWave != null) {
    		currentWave.destroy();
    		currentWave = null;
    		waveNumber = 1;
    	}
    }
    else {
    	if (currentWave == null) {
    		currentWave = new Wave(576, 576, unitManager, 5, waveNumber, maxWaveNumber);
    		currentWave.initialize();
    	}

    	if (currentWave.tick()) {
    		currentWave = null;
    		waveNumber++;

    		if (waveNumber > maxWaveNumber) {
    			maxWaveNumber = waveNumber;
    		}
    	}
    	else if (unitManager.getRealAlivePlayerCount() == 0) {
    		currentWave.destroy();
    		currentWave = null;
    	}

    	unitManager.tick(ctxMap);
    }
}<commit_msg>Fixed lowercase folder name<commit_after>console.log("=================================");
console.log("Starting Resist! server v0.1");
console.log("---------------------------------");

var sys = require("sys"),
	path = require("path"),
	url = require("url"),
	app = require('http').createServer(handler),
  	io = require('socket.io').listen(app),
  	fs = require('fs');

io.set('log level', 1); // reduce logging
app.listen(8080);

function handler (request, response) {
	var my_path = url.parse(request.url).pathname;
    var full_path = path.join(process.cwd(),my_path);

    if (full_path.indexOf("index.html") >= 0 || request.url === "/") {
    	fs.readFile(__dirname + '/client/index.html',
		  function (err, data) {
		    response.writeHead(200);
		    response.end(data);
		  });
    }
    else {
	    fs.exists(full_path,function(exists){

	        if(!exists){
	            response.writeHeader(404, {"Content-Type": "text/plain"});
	            response.write("404 Not Found\n");
	            response.end();
	        }
	        else{
	            fs.readFile(full_path, "binary", function(err, file) {
	                 if(err) {
	                     response.writeHeader(500, {"Content-Type": "text/plain"});
	                     response.write(err + "\n");
	                     response.end();
	                 }
	                 else{
	                    response.writeHeader(200);
	                    response.write(file, "binary");
	                    response.end();
	                }

	            });
	        }
	    });
	}
}

// =================================================
// Game logic
// -------------------------------------------------

var Map = require('./lib/resist/map/Map'),
	MapLoader = require('./lib/resist/map/MapLoader'),
	ctxMap = new Map();

MapLoader('./client/maps/town.json', ctxMap);

var ResistUnit = require("./lib/resist/ResistUnit");
var NetworkConnector = require("./lib/resist/NetworkConnector");
var Sprite = require("./lib/resist/sprite/Sprite");
var Wave = require("./lib/resist/Wave");
var UnitManager = require("./lib/resist/UnitManager");

var unitManager = new UnitManager();
var time;
var currentWave = null;
var waveNumber = 1;
var maxWaveNumber = 1;

io.sockets.on('connection', function (socket) {

	// When connecting, send credentials and notice other players
	socket.on("send-credentials", function (data) {
		var player = unitManager.createPlayerUnit(socket, data);

		socket.emit("credentials-result", {
			id : player.id
		});

		socket.on('disconnect', function () {
			unitManager.deleteUnit(player.id);
		});
	});
});

/*
 * Must dispatch an event to players and client players
 */
function receiveEvent(eventType, spriteDestId, data) {
	for (var i = 0; i < playerList.length; i++) {
		playerList[i].toDigestEventList.push(new Array(eventType, spriteDestId, data));
		playerList[i].connector.sendEvent(eventType, spriteDestId, data);
	}
}

// -------------------------------------------------
// Game loop
// -------------------------------------------------
setInterval(loop, 25);

function loop() {
    if (unitManager.getRealPlayerCount() == 0) {
    	if (currentWave != null) {
    		currentWave.destroy();
    		currentWave = null;
    		waveNumber = 1;
    	}
    }
    else {
    	if (currentWave == null) {
    		currentWave = new Wave(576, 576, unitManager, 5, waveNumber, maxWaveNumber);
    		currentWave.initialize();
    	}

    	if (currentWave.tick()) {
    		currentWave = null;
    		waveNumber++;

    		if (waveNumber > maxWaveNumber) {
    			maxWaveNumber = waveNumber;
    		}
    	}
    	else if (unitManager.getRealAlivePlayerCount() == 0) {
    		currentWave.destroy();
    		currentWave = null;
    	}

    	unitManager.tick(ctxMap);
    }
}<|endoftext|>


<commit_before>'use babel';

import {Point, Range} from 'atom';

export default class EventHandler {
  constructor(doc, editor, path, router) {
    this.doc = doc;
    this.editor = editor;
    this.path = path;
    this.router = router;
    this.clientId = atom.config.get('coeditor.clientId');
    this.push = true;
    this.buffer = this.editor.getBuffer();
    this.indicatorMap = new Map();
    this.highlightMap = new Map();

    this.doc.subscribe( (error) => {
      if (error) throw error;
      if (this.doc.type === null) {
        this.doc.create(this.buffer.getText(), 'text');
      } else {
        let data = this.doc.data;
        this.doBufferChange( () => {
          this.buffer.setText(data);
        })
      }

      let data = {
        a: 'meta',
        type: 'addTab',
        uri: path // TextEditor's uri is it's absolute path, when sending data, we send relative path, then when we receive it, we will convert to absolute path
      };
      setTimeout( () => {
        this.router.sendSocketMsg(JSON.stringify(data));
      }, 0);
    });
  }

  listen() {
    // remote OT events
    this.doc.on('op', (op, source) => {
      if (source) {
        return;
      }
      console.log('on OP:');
      console.log(op);

      var pos = 0;
      for (var i = 0; i < op.length; i++) {
        var component = op[i];
        switch (typeof component) {
          case 'number':
            pos += component;
            break;
          case 'string':
            this.onInsert(pos, component);
            pos += component.length;
            break;
          case 'object':
            this.onRemove(pos, component.d);
            break;
          default:
            break;
        }
      }
    });

    // local events
    let disposables = [];

    disposables.push(this.editor.onDidChangeGrammar( () => {

      let grammar = this.editor.getGrammar();
      let data = {
        a: 'meta',
        type: 'grammar',
        path: this.path,
        grammar: grammar.scopeName
      }

      setTimeout( () => {
        this.router.sendSocketMsg(JSON.stringify(data));
      }, 0);
    }));

    disposables.push(this.editor.onDidSave( (event) => {
      if (!this.push) {
        return;
      }

      let data = {
        a: 'meta',
        type: 'save',
        path: this.path
        // path: atom.project.relativizePath(event.path)[1]
      };

      setTimeout( () => {
        this.router.sendSocketMsg(JSON.stringify(data));
      }, 0);
    }));

    disposables.push(this.editor.onDidDestroy( () => {
      let data = {
        a: 'meta',
        type: 'editorClosed',
        path: this.path,
        clientId: this.clientId
      };

      setTimeout( () => {
        this.router.sendSocketMsg(JSON.stringify(data));
      }, 0);

    }));

    disposables.push(this.editor.onDidChangeSelectionRange( (event) => {
      if (!this.push) {
        return;
      }
      let data = {
        a: 'meta',
        type: 'highlight',
        path: this.path,
        newRange: event.newBufferRange,
        clientId: this.clientId
      };

      setTimeout( () => {
        this.router.sendSocketMsg(JSON.stringify(data));
      }, 0);
    }));

    disposables.push(this.editor.onDidChangeCursorPosition( (event) => {
      console.log('on did change cursor position');
      let data = {
        a: 'meta',
        type: 'cursorMoved',
        path: this.path,
        indicatorColor: atom.config.get('coeditor.indicatorBackgroundColor'),
        newPosition: event.newBufferPosition,
        clientId: this.clientId
      };

      setTimeout( () => {
        this.router.sendSocketMsg(JSON.stringify(data));
      }, 0);

      let position = this.buffer.characterIndexForPosition(event.newBufferPosition);
      let op = [position, {d: 0}];
      setTimeout( () => {
        this.doc.submitOp(op);
      }, 0);
    }));

    disposables.push(this.buffer.onDidChange((event) => {
      if (!this.push) {
        return;
      }
      console.log('on did change text');
      // remove text event
      if (event['oldText'].length !== 0) {
        let oldText = event['oldText'];
        if (oldText.includes('\n')) {
          let lineEnding = this.buffer.lineEndingForRow(0);
          oldText = lineEnding === '\r\n' ?
                                   oldText.replace(/\r?\n/g, "\r\n") : // CRLF
                                   oldText; // LF
        }
        console.log(oldText.length);
        let startPoint = event['oldRange'].start;
        let position = this.buffer.characterIndexForPosition(startPoint);
        let op = [position, {d: oldText.length}];
        this.doc.submitOp(op);
      }
      // insert text event
      if (event['newText'].length !== 0) {
        let startPoint = event['newRange'].start;
        let position = this.buffer.characterIndexForPosition(startPoint);
        let op = [position, event['newText']];
        this.doc.submitOp(op);
      }
    }));

    return disposables;
  }

  on(msg) {
    let data = JSON.parse(msg);
    switch (data.type) {
      case 'grammar':
        this.onGrammar(data.grammar);
        break;
      case 'cursorMoved':
        // setTimeout( () => {
          this.onCursorMoved(data.newPosition, data.clientId, data.indicatorColor);
        // }, 1000);
        break;
      case 'highlight':
        this.onHighlight(data.newRange, data.clientId);
        break;
      case 'save':
        this.onSave(data.path);
        break;
      default:
    }
  }

  onGrammar(grammar) {
    this.editor.setGrammar(atom.grammars.grammarForScopeName(grammar));
  }

  onCursorMoved(positionObj, clientId, indicatorColor) {
    // type = overlay
    let overlayMarker = this.indicatorMap.resetMarker(clientId);

    overlayMarker = this.editor.markBufferPosition(Point.fromObject(positionObj), {
      'invalidate': 'never'
    });

    this.indicatorMap.set(clientId, overlayMarker);

    if (overlayMarker.isDestroyed()) {
      return;
    }
    this.editor.decorateMarker(overlayMarker, {
      type: 'overlay',
      position: 'head',
      item: this.getIndicator(clientId, indicatorColor)
    });
    console.log('onCursorMoved: ');
    console.log(positionObj);
  }

  getIndicator(text, indicatorColor) {
    let indicatorDiv = document.createElement('div');
    Object.assign(indicatorDiv.style, {
      transform: 'translate(0, -100%)',
      display: 'flex'
    });

    let cursorDiv = document.createElement('div');
    Object.assign(cursorDiv.style, {
      alignItems: 'stretch',
      borderLeft: '2px solid ' + indicatorColor
      // borderLeft: '2px solid rgba(31, 161, 93, 1)'
    });
    cursorDiv.animate([
      {opacity: '1'},
      {opacity: '0'}
    ], {
      duration: 300,
      iterations: 'Infinity',
      direction: 'alternate'
    });
    indicatorDiv.appendChild(cursorDiv);

    let textDiv = document.createElement('div');
    textDiv.textContent = text;
    let style = {
      alignItems: 'stretch',
      backgroundColor: indicatorColor,
      paddingLeft: '1px',
      fontFamily: 'comic sans, comic sans ms, cursive, verdana, arial, sans-serif'
    }
    Object.assign(textDiv.style, style);
    indicatorDiv.appendChild(textDiv);

    return indicatorDiv;
  }

  onHighlight(rangeObj, clientId) {

    let marker = this.highlightMap.resetMarker(clientId);

    if (Point.fromObject(rangeObj.start).isEqual(Point.fromObject(rangeObj.end))) {
      return;
    }
    marker = this.editor.markBufferRange(Range.fromObject(rangeObj), {
      invalidate: 'never'
    });
    this.highlightMap.set(clientId, marker);
    this.editor.decorateMarker(marker, {
      type: 'highlight',
      class: 'selection'
    });
  }

  onSave(relativePath) {
    // atom.workspace.getActivePane().saveItem(atom.workspace.getActivePaneItem());
    this.push = false;
    console.log('on save: ');
    console.log(atom.project.getPaths());
    if (typeof this.editor.getPath() !== 'undefined') {
      this.editor.save();
    }
    this.push = true;
    // if (typeof this.editor.getPath() !== 'undefined') {
    //   this.editor.save();
    // } else {
    //   let absolutePath = path.join(atom.project.getPaths()[0], relativePath);
    //   this.editor.saveAs(absolutePath);
    // }
  }

  onInsert(position, text) {
    this.doBufferChange( () => {
      let from = this.buffer.positionForCharacterIndex(position);
      this.buffer.insert(from, text);
    })
  }

  onRemove(position, length) {
    this.doBufferChange( () => {
      let from = this.buffer.positionForCharacterIndex(position);
      let to = this.buffer.positionForCharacterIndex(position + length);
      this.buffer.delete([from.toArray(), to.toArray()]);
    })
  }

  doBufferChange(callback) {
    this.push = false;
    callback();
    this.push = true;
  }

  resetMarker(clientId) {
    this.highlightMap.resetMarker(clientId);
    this.indicatorMap.resetMarker(clientId);
  }

  destroy() {
    this.editor.getDefaultMarkerLayer().clear();
  }
}

Map.prototype.resetMarker = function(clientId) {
  let marker = this.get(clientId);
  if (typeof marker !== 'undefined') {
    marker.destroy();
  }
  return marker;
}
<commit_msg>Revert "replace line ending in oldText if file's line ending is CRLF"<commit_after>'use babel';

import {Point, Range} from 'atom';

export default class EventHandler {
  constructor(doc, editor, path, router) {
    this.doc = doc;
    this.editor = editor;
    this.path = path;
    this.router = router;
    this.clientId = atom.config.get('coeditor.clientId');
    this.push = true;
    this.buffer = this.editor.getBuffer();
    this.indicatorMap = new Map();
    this.highlightMap = new Map();

    this.doc.subscribe( (error) => {
      if (error) throw error;
      if (this.doc.type === null) {
        this.doc.create(this.buffer.getText(), 'text');
      } else {
        let data = this.doc.data;
        this.doBufferChange( () => {
          this.buffer.setText(data);
        })
      }

      let data = {
        a: 'meta',
        type: 'addTab',
        uri: path // TextEditor's uri is it's absolute path, when sending data, we send relative path, then when we receive it, we will convert to absolute path
      };
      setTimeout( () => {
        this.router.sendSocketMsg(JSON.stringify(data));
      }, 0);
    });
  }

  listen() {
    // remote OT events
    this.doc.on('op', (op, source) => {
      if (source) {
        return;
      }
      console.log('on OP:');
      console.log(op);

      var pos = 0;
      for (var i = 0; i < op.length; i++) {
        var component = op[i];
        switch (typeof component) {
          case 'number':
            pos += component;
            break;
          case 'string':
            this.onInsert(pos, component);
            pos += component.length;
            break;
          case 'object':
            this.onRemove(pos, component.d);
            break;
          default:
            break;
        }
      }
    });

    // local events
    let disposables = [];

    disposables.push(this.editor.onDidChangeGrammar( () => {

      let grammar = this.editor.getGrammar();
      let data = {
        a: 'meta',
        type: 'grammar',
        path: this.path,
        grammar: grammar.scopeName
      }

      setTimeout( () => {
        this.router.sendSocketMsg(JSON.stringify(data));
      }, 0);
    }));

    disposables.push(this.editor.onDidSave( (event) => {
      if (!this.push) {
        return;
      }

      let data = {
        a: 'meta',
        type: 'save',
        path: this.path
        // path: atom.project.relativizePath(event.path)[1]
      };

      setTimeout( () => {
        this.router.sendSocketMsg(JSON.stringify(data));
      }, 0);
    }));

    disposables.push(this.editor.onDidDestroy( () => {
      let data = {
        a: 'meta',
        type: 'editorClosed',
        path: this.path,
        clientId: this.clientId
      };

      setTimeout( () => {
        this.router.sendSocketMsg(JSON.stringify(data));
      }, 0);

    }));

    disposables.push(this.editor.onDidChangeSelectionRange( (event) => {
      if (!this.push) {
        return;
      }
      let data = {
        a: 'meta',
        type: 'highlight',
        path: this.path,
        newRange: event.newBufferRange,
        clientId: this.clientId
      };

      setTimeout( () => {
        this.router.sendSocketMsg(JSON.stringify(data));
      }, 0);
    }));

    disposables.push(this.editor.onDidChangeCursorPosition( (event) => {
      console.log('on did change cursor position');
      let data = {
        a: 'meta',
        type: 'cursorMoved',
        path: this.path,
        indicatorColor: atom.config.get('coeditor.indicatorBackgroundColor'),
        newPosition: event.newBufferPosition,
        clientId: this.clientId
      };

      setTimeout( () => {
        this.router.sendSocketMsg(JSON.stringify(data));
      }, 0);

      let position = this.buffer.characterIndexForPosition(event.newBufferPosition);
      let op = [position, {d: 0}];
      setTimeout( () => {
        this.doc.submitOp(op);
      }, 0);
    }));

    disposables.push(this.buffer.onDidChange((event) => {
      if (!this.push) {
        return;
      }
      console.log('on did change text');
      // remove text event
      if (event['oldText'].length !== 0) {
        let lineEnding = atom.config.get('coeditor.lineEnding');
        let oldText = lineEnding === 'CRLF' ?
                      event['oldText'].replace(/\r?\n/g, "\r\n") :
                      event['oldText'];
        console.log(oldText.length);
        let startPoint = event['oldRange'].start;
        let position = this.buffer.characterIndexForPosition(startPoint);
        let op = [position, {d: oldText.length}];
        this.doc.submitOp(op);
      }
      // insert text event
      if (event['newText'].length !== 0) {
        let startPoint = event['newRange'].start;
        let position = this.buffer.characterIndexForPosition(startPoint);
        let op = [position, event['newText']];
        this.doc.submitOp(op);
      }
    }));

    return disposables;
  }

  on(msg) {
    let data = JSON.parse(msg);
    switch (data.type) {
      case 'grammar':
        this.onGrammar(data.grammar);
        break;
      case 'cursorMoved':
        // setTimeout( () => {
          this.onCursorMoved(data.newPosition, data.clientId, data.indicatorColor);
        // }, 1000);
        break;
      case 'highlight':
        this.onHighlight(data.newRange, data.clientId);
        break;
      case 'save':
        this.onSave(data.path);
        break;
      default:
    }
  }

  onGrammar(grammar) {
    this.editor.setGrammar(atom.grammars.grammarForScopeName(grammar));
  }

  onCursorMoved(positionObj, clientId, indicatorColor) {
    // type = overlay
    let overlayMarker = this.indicatorMap.resetMarker(clientId);

    overlayMarker = this.editor.markBufferPosition(Point.fromObject(positionObj), {
      'invalidate': 'never'
    });

    this.indicatorMap.set(clientId, overlayMarker);

    if (overlayMarker.isDestroyed()) {
      return;
    }
    this.editor.decorateMarker(overlayMarker, {
      type: 'overlay',
      position: 'head',
      item: this.getIndicator(clientId, indicatorColor)
    });
    console.log('onCursorMoved: ');
    console.log(positionObj);
  }

  getIndicator(text, indicatorColor) {
    let indicatorDiv = document.createElement('div');
    Object.assign(indicatorDiv.style, {
      transform: 'translate(0, -100%)',
      display: 'flex'
    });

    let cursorDiv = document.createElement('div');
    Object.assign(cursorDiv.style, {
      alignItems: 'stretch',
      borderLeft: '2px solid ' + indicatorColor
      // borderLeft: '2px solid rgba(31, 161, 93, 1)'
    });
    cursorDiv.animate([
      {opacity: '1'},
      {opacity: '0'}
    ], {
      duration: 300,
      iterations: 'Infinity',
      direction: 'alternate'
    });
    indicatorDiv.appendChild(cursorDiv);

    let textDiv = document.createElement('div');
    textDiv.textContent = text;
    let style = {
      alignItems: 'stretch',
      backgroundColor: indicatorColor,
      paddingLeft: '1px',
      fontFamily: 'comic sans, comic sans ms, cursive, verdana, arial, sans-serif'
    }
    Object.assign(textDiv.style, style);
    indicatorDiv.appendChild(textDiv);

    return indicatorDiv;
  }

  onHighlight(rangeObj, clientId) {

    let marker = this.highlightMap.resetMarker(clientId);

    if (Point.fromObject(rangeObj.start).isEqual(Point.fromObject(rangeObj.end))) {
      return;
    }
    marker = this.editor.markBufferRange(Range.fromObject(rangeObj), {
      invalidate: 'never'
    });
    this.highlightMap.set(clientId, marker);
    this.editor.decorateMarker(marker, {
      type: 'highlight',
      class: 'selection'
    });
  }

  onSave(relativePath) {
    // atom.workspace.getActivePane().saveItem(atom.workspace.getActivePaneItem());
    this.push = false;
    console.log('on save: ');
    console.log(atom.project.getPaths());
    if (typeof this.editor.getPath() !== 'undefined') {
      this.editor.save();
    }
    this.push = true;
    // if (typeof this.editor.getPath() !== 'undefined') {
    //   this.editor.save();
    // } else {
    //   let absolutePath = path.join(atom.project.getPaths()[0], relativePath);
    //   this.editor.saveAs(absolutePath);
    // }
  }

  onInsert(position, text) {
    this.doBufferChange( () => {
      let from = this.buffer.positionForCharacterIndex(position);
      this.buffer.insert(from, text);
    })
  }

  onRemove(position, length) {
    this.doBufferChange( () => {
      let from = this.buffer.positionForCharacterIndex(position);
      let to = this.buffer.positionForCharacterIndex(position + length);
      this.buffer.delete([from.toArray(), to.toArray()]);
    })
  }

  doBufferChange(callback) {
    this.push = false;
    callback();
    this.push = true;
  }

  resetMarker(clientId) {
    this.highlightMap.resetMarker(clientId);
    this.indicatorMap.resetMarker(clientId);
  }

  destroy() {
    this.editor.getDefaultMarkerLayer().clear();
  }
}

Map.prototype.resetMarker = function(clientId) {
  let marker = this.get(clientId);
  if (typeof marker !== 'undefined') {
    marker.destroy();
  }
  return marker;
}
<|endoftext|>


<commit_before>var kue = require('kue'),
  fs = require('node-fs'),
  mv = require('mv'),
  async = require('async'),
  koop = require('./'),
  exec = require('child_process').exec;

// get the config from stdin
config = JSON.parse(process.argv[2]);

// set up koop with the thing we need like logging and the cache
koop.config = config;
koop.log = new koop.Logger( config );
koop.Cache = new koop.DataCache( koop );
koop.files = new koop.Files( koop );

// Start the Cache DB with the conn string from config
if ( config && config.db ) {
  if ( config.db.postgis ) {
    koop.Cache.db = koop.PostGIS.connect( config.db.postgis.conn );
  } else if ( config && config.db.sqlite ) {
    koop.Cache.db = koop.SQLite.connect( config.db.sqlite );
  }
  koop.Cache.db.log = koop.log;
} else if (config && !config.db){
  process.exit();
}

// create the job queue
jobs = kue.createQueue({
  prefix: config.export_workers.redis.prefix,
  redis: {
    port: config.export_workers.redis.port,
    host: config.export_workers.redis.host
  }
});

// catch crashes and save the current jobs
process.once( 'SIGINT', function ( sig ) {
  jobs.active(function(err, ids){
    if ( ids.length ){
      ids.forEach( function( id ) {
        kue.Job.get( id, function( err, job ) {
          job.inactive();
          jobs.active(function(err, activeIds){
            if (!activeIds.length){
             jobs.shutdown(function(err) {
               process.exit( 0 );
             }, 5000 );
            }
          });
        });
      });
    } else {
      jobs.shutdown(function(err) {
        process.exit( 0 );
      }, 5000 );
    }
  });
});

// process 2 jobs at a time
// TODO think about scaling this and how to do we start many workers
//  - ie can this be distributed across more machines?
//  - does it need to on the same server always?
jobs.process('exports', 2, function(job, done){
  if (fs.existsSync(job.data.files.rootVrtFile) && !job.data.options.ignore_cache){
    // since we have a VRT file locally
    // we just want to create the export and complete the job
    koop.Cache.getInfo(job.data.table, function(err, info){
      info.status = 'processing';
      info.generating = true;
      koop.Cache.updateInfo(job.data.table, info, function(err, res){
        // create large file from vrt
        var cmd = ['ogr2ogr', '-f', job.data.ogrFormat, '-update', '-append', ( job.data.format == 'zip' ) ? job.data.files.rootNewFileTmp.replace('zip','shp') : job.data.files.rootNewFileTmp, job.data.files.rootVrtFile];

        callOGR(job.data.format, job.data.files.rootNewFileTmp, cmd, job.data.options, job.data.files, function(err, formatFile){
          // remove the processing state and return the job
          delete info.status;
          delete info.generating;
          koop.Cache.updateInfo(job.data.table, info, function(e, res){
            if (err) {
              return done(err);
            } else {
              return done();
            }
          });
        });
      });
    });
  } else {
    fs.mkdir( job.data.files.base, '0777', true, function(){
      koop.Cache.getCount(job.data.table, job.data.options, function(err, count){
        var pages = Math.ceil(count / job.data.options.limit);
        for (var i=0; i < pages; i++){
            var offset = i * (job.data.options.limit);
            var chunk = { file: job.data.files.base+'/part.' + i + '.json', offset: offset };
            job.data.pages.push( chunk );
        }
        createFiles( job, done );
      });
    });
  }
});

function createFiles(job, done){
  console.log('starting job ', job.id, job.data);
  var vrt = '<OGRVRTDataSource>';

  var pageLen = job.data.pages.length,
    completed = 0;

  var workerQ = async.queue(function(task, cb){
    var opts = {
      layer: task.options.layer,
      limit: task.options.limit,
      where: task.options.where,
      geometry: task.options.geometry,
      offset: task.offset,
      bypassProcessing: true
    };

    var filePart = task.file;

    koop.Cache.db.select(task.dbkey, opts, function(err, json){
      koop.Cache.getInfo(task.table, function(err, info){
        if ( json && json[0] && json[0].info ){
          delete json[0].info;
          json = json[0];
        }
        var exists = fs.existsSync( filePart );
        if ( exists ){
          fs.unlinkSync( filePart );
        }
        fs.writeFile(filePart, JSON.stringify(json), function(){
          // tick up the number of complete pages
          completed++;
          job.progress( completed, pageLen );

          vrt += '<OGRVRTLayer name="OGRGeoJSON"><SrcDataSource>';
          vrt += filePart;
          vrt += '</SrcDataSource></OGRVRTLayer>';

          if (completed == pageLen){
            vrt += '</OGRVRTDataSource>';
            fs.writeFile( task.files.rootVrtFile, vrt, function(){
              // CALL OGR
              cmd = [];
              cmd.push('ogr2ogr');
              cmd.push('-f');
              cmd.push(task.ogrFormat);
              cmd.push('-update');
              cmd.push('-append');
              cmd.push(( task.format == 'zip' ) ? task.files.rootNewFileTmp.replace('zip', 'shp') : task.files.rootNewFileTmp);
              cmd.push( task.files.rootVrtFile );

              callOGR(task.format, task.files.rootNewFileTmp, cmd, task.options, task.files, function(err, formatFile){
                  delete info.status;
                  delete info.generating;
                  koop.Cache.updateInfo( task.table, info, function(err, res){
                    done();
                    cb();
                  });
              });
            });
          } else {
            cb();
          }
        });
      });
    });

  },4);

  job.data.pages.forEach(function(page,i){
    var task = {
      options: job.data.options,
      file: page.file,
      offset: page.offset,
      dbkey: job.data.dbkey,
      table: job.data.table,
      format: job.data.format,
      ogrFormat: job.data.ogrFormat,
      files: job.data.files
    };
    workerQ.push(task, function(err){ if (err) console.log(err); });
  });

}

function callOGR( format, outFile, cmd, options, files, callback ){
  if ( format == 'csv' && options.geomType && options.geomType == 'esriGeometryPoint' ) {
    cmd.push('-lco');
    cmd.push('GEOMETRY=AS_XY');
  } else if (format == 'zip' || format == 'shp'){
    cmd.push('-lco');
    cmd.push('ENCODING=UTF-8');
    if ( options.geomType && options.geomType == 'esriGeometryPoint' ){
      //cmd.push('-where');
      //cmd.push("'OGR_GEOMETRY=\"POINT\"'");
    } else if (options.geomType && options.geomType == 'esriGeometryPolygon') {
      cmd.push('-lco');
      cmd.push("SHPT=POLYGON");
    }
    cmd.push('-fieldmap');
    cmd.push('identity');
  }

  var base = files.base;

  koop.log.info('calling ogr2ogr: %s', cmd.join(' '));
  exec(cmd.join(' '), function (err, stdout, stderr) {
    koop.log.info('ogr2ogr done', err, stdout, stderr);
    if ( format == 'zip' || format == 'shp'){
      // mkdir for base path (dir + key) to store shp
      fs.mkdir( base, '0777', true, function(){
        var shp = outFile.replace('zip','shp');
        var dbf = outFile.replace('zip','dbf');
        var shx = outFile.replace('zip','shx');
        var prj = outFile.replace('zip','prj');
        if ( options.name ){
            // cp each file into dir with new name
            var shpdir = base + files.tmpName + '.shp';
            mv(shpdir+'/OGRGeoJSON.shp', base+'/' + options.name + '.shp', function(err) {
              mv(shpdir+'/OGRGeoJSON.dbf', base+'/' + options.name + '.dbf', function(err) {
                mv(shpdir+'/OGRGeoJSON.shx', base+'/' + options.name + '.shx', function(err) {
                  mv(shpdir+'/OGRGeoJSON.prj', base+'/' + options.name + '.prj', function(err) {
                    mv(shpdir+'/OGRGeoJSON.cpg', base+'/' + options.name + '.cpg', function(err) {
                      // zip all and return the new zip
                      var newZipTmp = base + '/' + options.name + files.tmpName + '.zip';
                      var newZip = base + '/' + options.name + '.zip';
                      var zipCMD = ['zip', '-rj', '"'+newZipTmp+'"', base+'/', '--exclude=*.json*'].join(' ');
                      koop.log.debug('creating a new zip: %s', zipCMD);
                      exec(zipCMD, function(err, stdout, stderr){
                        mv(newZipTmp, newZip, function(err) {
                          if ( koop.files.s3 ) {
                            var stream = fs.createReadStream(newZip);
                            koop.files.write( files.path+'/'+options.key, options.name + '.zip', stream, function( err ){
                              callback(null, newZip);
                            });
                          } else {
                            callback(null, newZip);
                          }
                        });
                      });
                    });
                  });
                });
              });
            });
        } else {
          child = exec(['zip', '-j', outFile, shp, dbf, shx, prj].join(' '), function (err, stdout, stderr) {
            mv(outFile, files.rootNewFile, function(err) {
              if ( koop.files.s3 ) {
                var stream = fs.createReadStream(files.rootNewFile);
                koop.files.write( files.path+'/'+options.key, files.newFile, stream, function( err ){
                  callback(null, files.rootNewFile);
                });
              } else {
                callback(null, files.rootNewFile);
              }
            });
          });
        }
      });
    } else {
      mv(outFile, files.rootNewFile, function(err) {
        console.log('ogr2ogr done');
        if ( koop.files.s3 ) {
          var stream = fs.createReadStream(files.rootNewFile);
          koop.files.write( files.path+'/'+options.key, files.newFile, stream, function( err ){
            callback(null, files.rootNewFile);
          });
        } else {
          callback(null, files.rootNewFile);
        }
      });
    }
  });
}
<commit_msg>changing worker config lookup<commit_after>var kue = require('kue'),
  fs = require('node-fs'),
  mv = require('mv'),
  async = require('async'),
  koop = require('./'),
  cluster = require('cluster'),
  path = require('path'),
  exec = require('child_process').exec;

// get the config from a stdin given dir
try {
  config = require(path.join(__dirname, process.argv[2]));
} catch(e){
  config = require('config');
}

if (!config.export_workers){
  console.log('No worker configutation found, exiting.');
  process.exit();
}

// set up koop with the thing we need like logging and the cache
koop.config = config;
koop.log = new koop.Logger( config );
koop.Cache = new koop.DataCache( koop );
koop.files = new koop.Files( koop );

// Start the Cache DB with the conn string from config
if ( config && config.db ) {
  if ( config.db.postgis ) {
    koop.Cache.db = koop.PostGIS.connect( config.db.postgis.conn );
  } else if ( config && config.db.sqlite ) {
    koop.Cache.db = koop.SQLite.connect( config.db.sqlite );
  }
  koop.Cache.db.log = koop.log;
} else if (config && !config.db){
  process.exit();
}

// create the job queue
jobs = kue.createQueue({
  prefix: config.export_workers.redis.prefix,
  redis: {
    port: config.export_workers.redis.port,
    host: config.export_workers.redis.host
  }
});

// TODO make this configurable
var clusterWorkerSize = 4;

if (cluster.isMaster) {
  kue.app.listen(process.env.PORT || 3000);
  for (var i = 0; i < clusterWorkerSize; i++) {
    cluster.fork();
  }
  // catch crashes and save the current jobs
  process.once( 'SIGINT', function ( sig ) {
    jobs.active(function(err, ids){
      if ( ids.length ){
        ids.forEach( function( id ) {
          kue.Job.get( id, function( err, job ) {
            job.inactive();
            jobs.active(function(err, activeIds){
              if (!activeIds.length){
               jobs.shutdown(function(err) {
                 process.exit( 0 );
               }, 5000 );
              }
            });
          });
        });
      } else {
        jobs.shutdown(function(err) {
          process.exit( 0 );
        }, 5000 );
      }
    });
  });

} else {

  // process 2 jobs at a time
  // TODO think about scaling this and how to do we start many workers
  //  - ie can this be distributed across more machines?
  //  - does it need to on the same server always?
  jobs.process('exports', 2, function(job, done){
    if (fs.existsSync(job.data.files.rootVrtFile) && !job.data.options.ignore_cache){
      // since we have a VRT file locally
      // we just want to create the export and complete the job
      koop.Cache.getInfo(job.data.table, function(err, info){
        info.status = 'processing';
        info.generating = true;
        koop.Cache.updateInfo(job.data.table, info, function(err, res){
          // create large file from vrt
          var cmd = ['ogr2ogr', '-f', job.data.ogrFormat, '-update', '-append', ( job.data.format == 'zip' ) ? job.data.files.rootNewFileTmp.replace('zip','shp') : job.data.files.rootNewFileTmp, job.data.files.rootVrtFile];

          callOGR(job.data.format, job.data.files.rootNewFileTmp, cmd, job.data.options, job.data.files, function(err, formatFile){
            // remove the processing state and return the job
            delete info.status;
            delete info.generating;
            koop.Cache.updateInfo(job.data.table, info, function(e, res){
              if (err) {
                return done(err);
              } else {
                return done();
              }
            });
          });
        });
      });
    } else {
      fs.mkdir( job.data.files.base, '0777', true, function(){
        koop.Cache.getCount(job.data.table, job.data.options, function(err, count){
          var pages = Math.ceil(count / job.data.options.limit);
          for (var i=0; i < pages; i++){
              var offset = i * (job.data.options.limit);
              var chunk = { file: job.data.files.base+'/part.' + i + '.json', offset: offset };
              job.data.pages.push( chunk );
          }
          createFiles( job, done );
        });
      });
    }
  });

}

function createFiles(job, done){
  console.log('starting job ', job.id, job.data);
  var vrt = '<OGRVRTDataSource>';

  var pageLen = job.data.pages.length,
    completed = 0;

  var workerQ = async.queue(function(task, cb){
    var opts = {
      layer: task.options.layer,
      limit: task.options.limit,
      where: task.options.where,
      geometry: task.options.geometry,
      offset: task.offset,
      bypassProcessing: true
    };

    var filePart = task.file;

    koop.Cache.db.select(task.dbkey, opts, function(err, json){
      koop.Cache.getInfo(task.table, function(err, info){
        if ( json && json[0] && json[0].info ){
          delete json[0].info;
          json = json[0];
        }
        var exists = fs.existsSync( filePart );
        if ( exists ){
          fs.unlinkSync( filePart );
        }
        fs.writeFile(filePart, JSON.stringify(json), function(){
          // tick up the number of complete pages
          completed++;
          job.progress( completed, pageLen );

          vrt += '<OGRVRTLayer name="OGRGeoJSON"><SrcDataSource>';
          vrt += filePart;
          vrt += '</SrcDataSource></OGRVRTLayer>';

          if (completed == pageLen){
            vrt += '</OGRVRTDataSource>';
            fs.writeFile( task.files.rootVrtFile, vrt, function(){
              // CALL OGR
              cmd = [];
              cmd.push('ogr2ogr');
              cmd.push('-f');
              cmd.push(task.ogrFormat);
              cmd.push('-update');
              cmd.push('-append');
              cmd.push(( task.format == 'zip' ) ? task.files.rootNewFileTmp.replace('zip', 'shp') : task.files.rootNewFileTmp);
              cmd.push( task.files.rootVrtFile );

              callOGR(task.format, task.files.rootNewFileTmp, cmd, task.options, task.files, function(err, formatFile){
                  delete info.status;
                  delete info.generating;
                  koop.Cache.updateInfo( task.table, info, function(err, res){
                    done();
                    cb();
                  });
              });
            });
          } else {
            cb();
          }
        });
      });
    });

  },4);

  job.data.pages.forEach(function(page,i){
    var task = {
      options: job.data.options,
      file: page.file,
      offset: page.offset,
      dbkey: job.data.dbkey,
      table: job.data.table,
      format: job.data.format,
      ogrFormat: job.data.ogrFormat,
      files: job.data.files
    };
    workerQ.push(task, function(err){ if (err) console.log(err); });
  });

}

function callOGR( format, outFile, cmd, options, files, callback ){
  if ( format == 'csv' && options.geomType && options.geomType == 'esriGeometryPoint' ) {
    cmd.push('-lco');
    cmd.push('GEOMETRY=AS_XY');
  } else if (format == 'zip' || format == 'shp'){
    cmd.push('-lco');
    cmd.push('ENCODING=UTF-8');
    if ( options.geomType && options.geomType == 'esriGeometryPoint' ){
      //cmd.push('-where');
      //cmd.push("'OGR_GEOMETRY=\"POINT\"'");
    } else if (options.geomType && options.geomType == 'esriGeometryPolygon') {
      cmd.push('-lco');
      cmd.push("SHPT=POLYGON");
    }
    cmd.push('-fieldmap');
    cmd.push('identity');
  }

  var base = files.base;

  koop.log.info('calling ogr2ogr: %s', cmd.join(' '));
  exec(cmd.join(' '), function (err, stdout, stderr) {
    koop.log.info('ogr2ogr done', err, stdout, stderr);
    if ( format == 'zip' || format == 'shp'){
      // mkdir for base path (dir + key) to store shp
      fs.mkdir( base, '0777', true, function(){
        var shp = outFile.replace('zip','shp');
        var dbf = outFile.replace('zip','dbf');
        var shx = outFile.replace('zip','shx');
        var prj = outFile.replace('zip','prj');
        if ( options.name ){
            // cp each file into dir with new name
            var shpdir = base + files.tmpName + '.shp';
            mv(shpdir+'/OGRGeoJSON.shp', base+'/' + options.name + '.shp', function(err) {
              mv(shpdir+'/OGRGeoJSON.dbf', base+'/' + options.name + '.dbf', function(err) {
                mv(shpdir+'/OGRGeoJSON.shx', base+'/' + options.name + '.shx', function(err) {
                  mv(shpdir+'/OGRGeoJSON.prj', base+'/' + options.name + '.prj', function(err) {
                    mv(shpdir+'/OGRGeoJSON.cpg', base+'/' + options.name + '.cpg', function(err) {
                      // zip all and return the new zip
                      var newZipTmp = base + '/' + options.name + files.tmpName + '.zip';
                      var newZip = base + '/' + options.name + '.zip';
                      var zipCMD = ['zip', '-rj', '"'+newZipTmp+'"', base+'/', '--exclude=*.json*'].join(' ');
                      koop.log.debug('creating a new zip: %s', zipCMD);
                      exec(zipCMD, function(err, stdout, stderr){
                        mv(newZipTmp, newZip, function(err) {
                          if ( koop.files.s3 ) {
                            var stream = fs.createReadStream(newZip);
                            koop.files.write( files.path+'/'+options.key, options.name + '.zip', stream, function( err ){
                              callback(null, newZip);
                            });
                          } else {
                            callback(null, newZip);
                          }
                        });
                      });
                    });
                  });
                });
              });
            });
        } else {
          child = exec(['zip', '-j', outFile, shp, dbf, shx, prj].join(' '), function (err, stdout, stderr) {
            mv(outFile, files.rootNewFile, function(err) {
              if ( koop.files.s3 ) {
                var stream = fs.createReadStream(files.rootNewFile);
                koop.files.write( files.path+'/'+options.key, files.newFile, stream, function( err ){
                  callback(null, files.rootNewFile);
                });
              } else {
                callback(null, files.rootNewFile);
              }
            });
          });
        }
      });
    } else {
      mv(outFile, files.rootNewFile, function(err) {
        console.log('ogr2ogr done');
        if ( koop.files.s3 ) {
          var stream = fs.createReadStream(files.rootNewFile);
          koop.files.write( files.path+'/'+options.key, files.newFile, stream, function( err ){
            callback(null, files.rootNewFile);
          });
        } else {
          callback(null, files.rootNewFile);
        }
      });
    }
  });
}
<|endoftext|>