<commit_before>const { ObjectId } = require('mongoose').Types;

const convertToIn = (value) => {
	let list = [];
	if (Array.isArray(value)) {
		list = value;
	} else if (Array.isArray(value.$in)) {
		list = value.$in;
	} else if (typeof value === 'string') {
		list = [value];
	}
	return list;
};

/**
 * Allows to filter a attribute by a value
 * @param {Array} aggregation
 * @param {String} attr - attribute name
 * @param {*} consentStatus - Array, String or Object with a $in
 */
const stageBaseFilter = (aggregation, attr, value) => {
	aggregation.push({
		$match: {
			[attr]: {
				$in: convertToIn(value),
			},
		},
	});
};

/**
 * Convert a "select"-array to an object to handle it with aggregaitons
 * @param {Array} select
 */
const convertSelect = (select) => select.reduce((acc, curr) => ({ ...acc, [curr]: 1 }), {});

/**
 * Creates an reducer to filter parent consent
 * @param {string} type - consent type
 */
const getParentReducer = (type) => ({
	$reduce: {
		input: '$consent.parentConsents',
		initialValue: false,
		in: { $or: ['$$value', `$$this.${type}`] },

	},
});

/**
 * To sort by consentStatus, this stage convert the status message to an number.
 * ConsentStatus is needed to use this stage.
 *
 * missing: 0
 * parentsAgreed: 1
 * ok: 2
 *
 * @param {*} aggregation - current aggregation array
 */
const stageAddConsentSortParam = (aggregation) => {
	aggregation.push({
		$addFields: {
			consentSortParam: {
				$switch: {
					branches: [
						{
							case: {
								$eq: ['$consentStatus', 'missing'],
							},
							then: 0,
						},
						{
							case: {
								$eq: ['$consentStatus', 'parentsAgreed'],
							},
							then: 1,
						},
						{
							case: {
								$eq: ['$consentStatus', 'ok'],
							},
							then: 2,
						},
					],
				},
			},
		},
	});
};

const getConsentStatusSwitch = () => {
	const currentDate = new Date();
	const secondLevel = new Date();
	secondLevel.setFullYear(currentDate.getFullYear() - 16); // TODO: get age from default.conf
	const firstLevel = new Date();
	firstLevel.setFullYear(currentDate.getFullYear() - 14);

	return {
		$switch: {
			branches: [
				{
					case: {
						$or: [
							{
								$and: [
									{ $lte: ['$birthday', secondLevel] },
									{ $eq: ['$consent.userConsent.privacyConsent', true] },
									{ $eq: ['$consent.userConsent.termsOfUseConsent', true] },
								],
							},
							{
								$and: [
									{ $gt: ['$birthday', secondLevel] },
									{ $lte: ['$birthday', firstLevel] },
									{ $eq: ['$consent.userConsent.privacyConsent', true] },
									{ $eq: ['$consent.userConsent.termsOfUseConsent', true] },
									{ $eq: [getParentReducer('privacyConsent'), true] },
									{ $eq: [getParentReducer('termsOfUseConsent'), true] },
								],
							},
							{
								$and: [
									{ $gt: ['$birthday', firstLevel] },
									{ $eq: [getParentReducer('privacyConsent'), true] },
									{ $eq: [getParentReducer('termsOfUseConsent'), true] },
								],
							},

						],
					},
					then: 'ok',

				},
				{
					case: {
						$and: [
							{ $gt: ['$birthday', secondLevel] },
							{ $lte: ['$birthday', firstLevel] },
							{ $eq: [getParentReducer('privacyConsent'), true] },
							{ $eq: [getParentReducer('termsOfUseConsent'), true] },
						],
					},
					then: 'parentsAgreed',
				},
			],
			default: 'missing',
		},
	};
};

const stageAddConsentStatus = (aggregation) => {
	aggregation.push({
		$addFields: {
			consentStatus: getConsentStatusSwitch(),
		},
	});
};

/**
 * Convert Select array to and aggregation Project and adds consentStatus if part of select
 *
 * @param {Array} aggregation
 * @param {Array} select
 */
const stageAddSelectProjectWithConsentCreate = (aggregation, select) => {
	const project = convertSelect(select);

	if (select.includes('consentStatus')) {
		project.consentStatus = getConsentStatusSwitch();
	}

	aggregation.push({
		$project: project,
	});
};

/**
 * Only select fields which are in select
 */
const stageSimpleProject = (aggregation, select) => {
	aggregation.push({
		$project: convertSelect(select),
	});
};

const stageLookupClasses = (aggregation, schoolId, schoolYearId) => {
	aggregation.push({
		$lookup:
		{

			from: 'classes',
			let: { id: '$_id' },
			pipeline: [
				{
					$match: {
						$expr: {
							$and: [
								{ $eq: ['$schoolId', ObjectId(schoolId.toString())] },
								{
									$and: [
										{
											$or: [
												{ $eq: ['$year', ObjectId(schoolYearId.toString())] },
												{ $eq: [{ $type: '$year' }, 'missing'] },
											],
										},
										{
											$or: [
												{ $max: '$gradeLevel' },
												{ $eq: [{ $type: '$gradeLevel' }, 'missing'] },
											],
										},
									],
								},
								{
									$or: [
										{ $in: ['$$id', '$userIds'] },
										{ $in: ['$$id', '$teacherIds'] },
									],
								},
							],
						},
					},
				},
				{
					$sort: {
						year: -1,
						gradeLevel: -1,
						name: 1,
					},
				},
				{
					$project: {
						gradeLevel: {
							$convert: {
								input: '$gradeLevel',
								to: 'string',
								onNull: '',
							},
						},
						name: {
							$convert: {
								input: '$name',
								to: 'string',
								onNull: '',
							},
						},
					},
				},
			],
			as: 'classes',
		},
	});
	aggregation.push({
		$addFields: {
			classesSort: {
				$arrayElemAt: ['$classes', 0],
			},
			classes: {
				$map: {
					input: '$classes',
					as: 'class',
					in: { $concat: ['$$class.gradeLevel', '$$class.name'] },
				},
			},
		},
	});
};

/**
 *	Sorts the output of earlier stage.
 *	To filter by consentStatus, it has to created first
 *
 * @param {Array} aggregation
 * @param {Object} sort
 */
const stageSort = (aggregation, sort) => {
	const mSort = {};
	for (const k in sort) {
		if (({}).hasOwnProperty.call(sort, k)) mSort[k] = Number(sort[k]);
	}

	if (typeof sort === 'object' && ({}).hasOwnProperty.call(sort, 'consentStatus')) {
		mSort.consentSortParam = mSort.consentStatus;
		delete mSort.consentStatus;
		stageAddConsentSortParam(aggregation);
	}

	if (typeof sort === 'object' && ({}).hasOwnProperty.call(sort, 'classes')) {
		mSort['classesSort.gradelevel'] = mSort.classes;
		mSort['classesSort.name'] = mSort.classes;
		delete mSort.classes;
	}

	aggregation.push({
		$sort: mSort,
	});
};

/**
 *	Convert the output to a feathers-mongoose like format:
 *
 * {
 * 	total: Number,
 * 	limit: Number,
 *  skip: Number,
 *  data: Array
 * }
 *
 * @param {Array} aggregation
 * @param {Int} limit
 * @param {Int} skip
 */
const stageFormatWithTotal = (aggregation, limit, skip) => {
	aggregation.push({
		$facet: {
			data: [
				{
					$skip: skip,
				},
				{
					$limit: limit,
				},
			],
			total: [{
				$count: 'count',
			}],
		},
	});

	aggregation.push({
		$project: {
			total: {
				$reduce: {
					input: '$total',
					initialValue: 0,
					in: { $add: ['$$value', '$$this.count'] },
				},
			},
			data: 1,
		},
	});

	aggregation.push({
		$addFields: {
			limit,
			skip,
		},
	});
};

/**
 * Creates an Array for an Aggregation pipeline and can handle, select, sort, limit, skip and matches.
 * To filter or sort by consentStatus, it have also to be seleceted first.
 *
 * @param {{select: Array, sort: Object, limit: Int, skip: Int, ...matches}} param0
 */
const createMultiDocumentAggregation = ({
	select, sort, limit = 25, skip = 0, consentStatus, classes, schoolYearId, ...match
}) => {
	// eslint-disable-next-line no-param-reassign
	limit = Number(limit);
	// eslint-disable-next-line no-param-reassign
	skip = Number(skip);

	const selectSortDiff = Object.getOwnPropertyNames(sort || {}).filter((s) => !select.includes(s));
	const aggregation = [];

	if (match) {
		aggregation.push({
			$match: match,
		});
	}

	if (select) {
		stageAddSelectProjectWithConsentCreate(aggregation, select.concat(selectSortDiff));
		if (select.includes('classes')) stageLookupClasses(aggregation, match.schoolId, schoolYearId);
	} else {
		stageAddConsentStatus(aggregation);
		if (match.schoolId && schoolYearId) stageLookupClasses(aggregation, match.schoolId, schoolYearId);
	}

	if (consentStatus) {
		stageBaseFilter(aggregation, 'consentStatus', consentStatus);
	}

	if (classes) {
		stageBaseFilter(aggregation, 'classes', classes);
	}

	if (sort) {
		stageSort(aggregation, sort);
	}

	// if (selectSortDiff.length !== 0) {
	// TODO: think about doing it after limit and skip
	stageSimpleProject(aggregation, select);
	// }

	stageFormatWithTotal(aggregation, limit, skip);
	return aggregation;
};

module.exports = {
	convertSelect,
	getParentReducer,
	createMultiDocumentAggregation,
};
<commit_msg>naming error<commit_after>const { ObjectId } = require('mongoose').Types;

const convertToIn = (value) => {
	let list = [];
	if (Array.isArray(value)) {
		list = value;
	} else if (Array.isArray(value.$in)) {
		list = value.$in;
	} else if (typeof value === 'string') {
		list = [value];
	}
	return list;
};

/**
 * Allows to filter a attribute by a value
 * @param {Array} aggregation
 * @param {String} attr - attribute name
 * @param {*} consentStatus - Array, String or Object with a $in
 */
const stageBaseFilter = (aggregation, attr, value) => {
	aggregation.push({
		$match: {
			[attr]: {
				$in: convertToIn(value),
			},
		},
	});
};

/**
 * Convert a "select"-array to an object to handle it with aggregaitons
 * @param {Array} select
 */
const convertSelect = (select) => select.reduce((acc, curr) => ({ ...acc, [curr]: 1 }), {});

/**
 * Creates an reducer to filter parent consent
 * @param {string} type - consent type
 */
const getParentReducer = (type) => ({
	$reduce: {
		input: '$consent.parentConsents',
		initialValue: false,
		in: { $or: ['$$value', `$$this.${type}`] },

	},
});

/**
 * To sort by consentStatus, this stage convert the status message to an number.
 * ConsentStatus is needed to use this stage.
 *
 * missing: 0
 * parentsAgreed: 1
 * ok: 2
 *
 * @param {*} aggregation - current aggregation array
 */
const stageAddConsentSortParam = (aggregation) => {
	aggregation.push({
		$addFields: {
			consentSortParam: {
				$switch: {
					branches: [
						{
							case: {
								$eq: ['$consentStatus', 'missing'],
							},
							then: 0,
						},
						{
							case: {
								$eq: ['$consentStatus', 'parentsAgreed'],
							},
							then: 1,
						},
						{
							case: {
								$eq: ['$consentStatus', 'ok'],
							},
							then: 2,
						},
					],
				},
			},
		},
	});
};

const getConsentStatusSwitch = () => {
	const currentDate = new Date();
	const secondLevel = new Date();
	secondLevel.setFullYear(currentDate.getFullYear() - 16); // TODO: get age from default.conf
	const firstLevel = new Date();
	firstLevel.setFullYear(currentDate.getFullYear() - 14);

	return {
		$switch: {
			branches: [
				{
					case: {
						$or: [
							{
								$and: [
									{ $lte: ['$birthday', secondLevel] },
									{ $eq: ['$consent.userConsent.privacyConsent', true] },
									{ $eq: ['$consent.userConsent.termsOfUseConsent', true] },
								],
							},
							{
								$and: [
									{ $gt: ['$birthday', secondLevel] },
									{ $lte: ['$birthday', firstLevel] },
									{ $eq: ['$consent.userConsent.privacyConsent', true] },
									{ $eq: ['$consent.userConsent.termsOfUseConsent', true] },
									{ $eq: [getParentReducer('privacyConsent'), true] },
									{ $eq: [getParentReducer('termsOfUseConsent'), true] },
								],
							},
							{
								$and: [
									{ $gt: ['$birthday', firstLevel] },
									{ $eq: [getParentReducer('privacyConsent'), true] },
									{ $eq: [getParentReducer('termsOfUseConsent'), true] },
								],
							},

						],
					},
					then: 'ok',

				},
				{
					case: {
						$and: [
							{ $gt: ['$birthday', secondLevel] },
							{ $lte: ['$birthday', firstLevel] },
							{ $eq: [getParentReducer('privacyConsent'), true] },
							{ $eq: [getParentReducer('termsOfUseConsent'), true] },
						],
					},
					then: 'parentsAgreed',
				},
			],
			default: 'missing',
		},
	};
};

const stageAddConsentStatus = (aggregation) => {
	aggregation.push({
		$addFields: {
			consentStatus: getConsentStatusSwitch(),
		},
	});
};

/**
 * Convert Select array to and aggregation Project and adds consentStatus if part of select
 *
 * @param {Array} aggregation
 * @param {Array} select
 */
const stageAddSelectProjectWithConsentCreate = (aggregation, select) => {
	const project = convertSelect(select);

	if (select.includes('consentStatus')) {
		project.consentStatus = getConsentStatusSwitch();
	}

	aggregation.push({
		$project: project,
	});
};

/**
 * Only select fields which are in select
 */
const stageSimpleProject = (aggregation, select) => {
	aggregation.push({
		$project: convertSelect(select),
	});
};

const stageLookupClasses = (aggregation, schoolId, schoolYearId) => {
	aggregation.push({
		$lookup:
		{

			from: 'classes',
			let: { id: '$_id' },
			pipeline: [
				{
					$match: {
						$expr: {
							$and: [
								{ $eq: ['$schoolId', ObjectId(schoolId.toString())] },
								{
									$and: [
										{
											$or: [
												{ $eq: ['$year', ObjectId(schoolYearId.toString())] },
												{ $eq: [{ $type: '$year' }, 'missing'] },
											],
										},
										{
											$or: [
												{ $max: '$gradeLevel' },
												{ $eq: [{ $type: '$gradeLevel' }, 'missing'] },
											],
										},
									],
								},
								{
									$or: [
										{ $in: ['$$id', '$userIds'] },
										{ $in: ['$$id', '$teacherIds'] },
									],
								},
							],
						},
					},
				},
				{
					$sort: {
						year: -1,
						gradeLevel: -1,
						name: 1,
					},
				},
				{
					$project: {
						gradeLevel: {
							$convert: {
								input: '$gradeLevel',
								to: 'string',
								onNull: '',
							},
						},
						name: {
							$convert: {
								input: '$name',
								to: 'string',
								onNull: '',
							},
						},
					},
				},
			],
			as: 'classes',
		},
	});
	aggregation.push({
		$addFields: {
			classesSort: {
				$arrayElemAt: ['$classes', 0],
			},
			classes: {
				$map: {
					input: '$classes',
					as: 'class',
					in: { $concat: ['$$class.gradeLevel', '$$class.name'] },
				},
			},
		},
	});
};

/**
 *	Sorts the output of earlier stage.
 *	To filter by consentStatus, it has to created first
 *
 * @param {Array} aggregation
 * @param {Object} sort
 */
const stageSort = (aggregation, sort) => {
	const mSort = {};
	for (const k in sort) {
		if (({}).hasOwnProperty.call(sort, k)) mSort[k] = Number(sort[k]);
	}

	if (typeof sort === 'object' && ({}).hasOwnProperty.call(sort, 'consentStatus')) {
		mSort.consentSortParam = mSort.consentStatus;
		delete mSort.consentStatus;
		stageAddConsentSortParam(aggregation);
	}

	if (typeof sort === 'object' && ({}).hasOwnProperty.call(sort, 'classes')) {
		mSort['classesSort.gradeLevel'] = mSort.classes;
		mSort['classesSort.name'] = mSort.classes;
		delete mSort.classes;
	}

	aggregation.push({
		$sort: mSort,
	});
};

/**
 *	Convert the output to a feathers-mongoose like format:
 *
 * {
 * 	total: Number,
 * 	limit: Number,
 *  skip: Number,
 *  data: Array
 * }
 *
 * @param {Array} aggregation
 * @param {Int} limit
 * @param {Int} skip
 */
const stageFormatWithTotal = (aggregation, limit, skip) => {
	aggregation.push({
		$facet: {
			data: [
				{
					$skip: skip,
				},
				{
					$limit: limit,
				},
			],
			total: [{
				$count: 'count',
			}],
		},
	});

	aggregation.push({
		$project: {
			total: {
				$reduce: {
					input: '$total',
					initialValue: 0,
					in: { $add: ['$$value', '$$this.count'] },
				},
			},
			data: 1,
		},
	});

	aggregation.push({
		$addFields: {
			limit,
			skip,
		},
	});
};

/**
 * Creates an Array for an Aggregation pipeline and can handle, select, sort, limit, skip and matches.
 * To filter or sort by consentStatus, it have also to be seleceted first.
 *
 * @param {{select: Array, sort: Object, limit: Int, skip: Int, ...matches}} param0
 */
const createMultiDocumentAggregation = ({
	select, sort, limit = 25, skip = 0, consentStatus, classes, schoolYearId, ...match
}) => {
	// eslint-disable-next-line no-param-reassign
	limit = Number(limit);
	// eslint-disable-next-line no-param-reassign
	skip = Number(skip);

	const selectSortDiff = Object.getOwnPropertyNames(sort || {}).filter((s) => !select.includes(s));
	const aggregation = [];

	if (match) {
		aggregation.push({
			$match: match,
		});
	}

	if (select) {
		stageAddSelectProjectWithConsentCreate(aggregation, select.concat(selectSortDiff));
		if (select.includes('classes')) stageLookupClasses(aggregation, match.schoolId, schoolYearId);
	} else {
		stageAddConsentStatus(aggregation);
		if (match.schoolId && schoolYearId) stageLookupClasses(aggregation, match.schoolId, schoolYearId);
	}

	if (consentStatus) {
		stageBaseFilter(aggregation, 'consentStatus', consentStatus);
	}

	if (classes) {
		stageBaseFilter(aggregation, 'classes', classes);
	}

	if (sort) {
		stageSort(aggregation, sort);
	}

	// if (selectSortDiff.length !== 0) {
	// TODO: think about doing it after limit and skip
	stageSimpleProject(aggregation, select);
	// }

	stageFormatWithTotal(aggregation, limit, skip);
	return aggregation;
};

module.exports = {
	convertSelect,
	getParentReducer,
	createMultiDocumentAggregation,
};
<|endoftext|>


<commit_before>/*global process*/
const Buffer = require('buffer').Buffer;
const fs = require('fs');
const test = require('tape');
const jws = require('..');

const NODE_VERSION = require('semver').clean(process.version);
const SUPPORTS_ENCRYPTED_KEYS = require('semver').gte(NODE_VERSION, '0.11.8');

function readfile(path) {
  return fs.readFileSync(__dirname + '/' + path).toString();
}

function readstream(path) {
  return fs.createReadStream(__dirname + '/' + path);
}

const rsaPrivateKey = readfile('rsa-private.pem');
const rsaPrivateKeyEncrypted = readfile('rsa-private-encrypted.pem');
const encryptedPassphrase = readfile('encrypted-key-passphrase');
const rsaPublicKey = readfile('rsa-public.pem');
const rsaWrongPublicKey = readfile('rsa-wrong-public.pem');
const ecdsaPrivateKey = {
  '256': readfile('ec256-private.pem'),
  '384': readfile('ec384-private.pem'),
  '512': readfile('ec512-private.pem'),
};
const ecdsaPublicKey = {
  '256': readfile('ec256-public.pem'),
  '384': readfile('ec384-public.pem'),
  '512': readfile('ec512-public.pem'),
};
const ecdsaWrongPublicKey = {
  '256': readfile('ec256-wrong-public.pem'),
  '384': readfile('ec384-wrong-public.pem'),
  '512': readfile('ec512-wrong-public.pem'),
};

const BITS = ['256', '384', '512'];
const CURVES = {
  '256': '256',
  '384': '384',
  '512': '521',
};

const payloadString = 'oh ćhey José!: ¬˚∆ƒå¬ß…©…åˆø˙ˆø´∆¬˚µ…˚¬˜øå…ˆßøˆƒ˜¬';
const payload = {
  name: payloadString,
  value: ['one', 2, 3]
};

BITS.forEach(function (bits) {
  test('HMAC using SHA-'+bits+' hash algorithm', function (t) {
    const alg = 'HS'+bits;
    const header = { alg: alg, typ: 'JWT' };
    const secret = 'sup';
    const jwsObj = jws.sign({
      header: header,
      payload: payload,
      secret: secret,
      encoding: 'utf8',
    });
    const parts = jws.decode(jwsObj);
    t.ok(jws.verify(jwsObj, alg, secret), 'should verify');
    t.notOk(jws.verify(jwsObj, alg, 'something else'), 'should not verify with non-matching secret');
    t.same(parts.payload, payload, 'should match payload');
    t.same(parts.header, header, 'should match header');
    t.end();
  });
});

BITS.forEach(function (bits) {
  test('RSASSA using SHA-'+bits+' hash algorithm', function (t) {
    const alg = 'RS'+bits;
    const header = { alg: alg };
    const privateKey = rsaPrivateKey;
    const publicKey = rsaPublicKey;
    const wrongPublicKey = rsaWrongPublicKey;
    const jwsObj = jws.sign({
      header: header,
      payload: payload,
      privateKey: privateKey
    });
    const parts = jws.decode(jwsObj, { json: true });
    t.ok(jws.verify(jwsObj, alg, publicKey), 'should verify');
    t.notOk(jws.verify(jwsObj, alg, wrongPublicKey), 'should not verify with non-matching public key');
    t.notOk(jws.verify(jwsObj, 'HS'+bits, publicKey), 'should not verify with non-matching algorithm');
    t.same(parts.payload, payload, 'should match payload');
    t.same(parts.header, header, 'should match header');
    t.end();
  });
});

BITS.forEach(function (bits) {
  const curve = CURVES[bits];
  test('ECDSA using P-'+curve+' curve and SHA-'+bits+' hash algorithm', function (t) {
    const alg = 'ES'+bits;
    const header = { alg: alg };
    const privateKey = ecdsaPrivateKey[bits];
    const publicKey = ecdsaPublicKey[bits];
    const wrongPublicKey = ecdsaWrongPublicKey[bits];
    const jwsObj = jws.sign({
      header: header,
      payload: payloadString,
      privateKey: privateKey
    });
    const parts = jws.decode(jwsObj);
    t.ok(jws.verify(jwsObj, alg, publicKey), 'should verify');
    t.notOk(jws.verify(jwsObj, alg, wrongPublicKey), 'should not verify with non-matching public key');
    t.notOk(jws.verify(jwsObj, 'HS'+bits, publicKey), 'should not verify with non-matching algorithm');
    t.same(parts.payload, payloadString, 'should match payload');
    t.same(parts.header, header, 'should match header');
    t.end();
  });
});

test('No digital signature or MAC value included', function (t) {
  const alg = 'none';
  const header = { alg: alg };
  const payload = 'oh hey José!';
  const jwsObj = jws.sign({
    header: header,
    payload: payload,
  });
  const parts = jws.decode(jwsObj);
  t.ok(jws.verify(jwsObj, alg), 'should verify');
  t.ok(jws.verify(jwsObj, alg, 'anything'), 'should still verify');
  t.notOk(jws.verify(jwsObj, 'HS256', 'anything'), 'should not verify with non-matching algorithm');
  t.same(parts.payload, payload, 'should match payload');
  t.same(parts.header, header, 'should match header');
  t.end();
});

test('Streaming sign: HMAC', function (t) {
  const dataStream = readstream('data.txt');
  const secret = 'shhhhh';
  const sig = jws.createSign({
    header: { alg: 'HS256' },
    secret: secret
  });
  dataStream.pipe(sig.payload);
  sig.on('done', function (signature) {
    t.ok(jws.verify(signature, 'HS256', secret), 'should verify');
    t.end();
  });
});

test('Streaming sign: RSA', function (t) {
  const dataStream = readstream('data.txt');
  const privateKeyStream = readstream('rsa-private.pem');
  const publicKey = rsaPublicKey;
  const wrongPublicKey = rsaWrongPublicKey;
  const sig = jws.createSign({
    header: { alg: 'RS256' },
  });
  dataStream.pipe(sig.payload);

  process.nextTick(function () {
    privateKeyStream.pipe(sig.key);
  });

  sig.on('done', function (signature) {
    t.ok(jws.verify(signature, 'RS256', publicKey), 'should verify');
    t.notOk(jws.verify(signature, 'RS256', wrongPublicKey), 'should not verify');
    t.same(jws.decode(signature).payload, readfile('data.txt'), 'got all the data');
    t.end();
  });
});

test('Streaming sign: RSA, predefined streams', function (t) {
  const dataStream = readstream('data.txt');
  const privateKeyStream = readstream('rsa-private.pem');
  const publicKey = rsaPublicKey;
  const wrongPublicKey = rsaWrongPublicKey;
  const sig = jws.createSign({
    header: { alg: 'RS256' },
    payload: dataStream,
    privateKey: privateKeyStream
  });
  sig.on('done', function (signature) {
    t.ok(jws.verify(signature, 'RS256', publicKey), 'should verify');
    t.notOk(jws.verify(signature, 'RS256', wrongPublicKey), 'should not verify');
    t.same(jws.decode(signature).payload, readfile('data.txt'), 'got all the data');
    t.end();
  });
});

test('Streaming verify: ECDSA', function (t) {
  const dataStream = readstream('data.txt');
  const privateKeyStream = readstream('ec512-private.pem');
  const publicKeyStream = readstream('ec512-public.pem');
  const sigStream = jws.createSign({
    header: { alg: 'ES512' },
    payload: dataStream,
    privateKey: privateKeyStream
  });
  const verifier = jws.createVerify({algorithm: 'ES512'});
  sigStream.pipe(verifier.signature);
  publicKeyStream.pipe(verifier.key);
  verifier.on('done', function (valid) {
    t.ok(valid, 'should verify');
    t.end();
  });
});

test('Streaming verify: ECDSA, with invalid key', function (t) {
  const dataStream = readstream('data.txt');
  const privateKeyStream = readstream('ec512-private.pem');
  const publicKeyStream = readstream('ec512-wrong-public.pem');
  const sigStream = jws.createSign({
    header: { alg: 'ES512' },
    payload: dataStream,
    privateKey: privateKeyStream
  });
  const verifier = jws.createVerify({
    algorithm: 'ES512',
    signature: sigStream,
    publicKey: publicKeyStream,
  });
  verifier.on('done', function (valid) {
    t.notOk(valid, 'should not verify');
    t.end();
  });
});

test('Streaming verify: errors during verify should emit as "error"', function (t) {
  const verifierShouldError = jws.createVerify({
    algorithm: 'ES512',
    signature: 'a.b.c', // the short/invalid length signature will make jwa throw
    publicKey: 'invalid-key-will-make-crypto-throw'
  });

  verifierShouldError.on('done', function () {
    t.fail();
    t.end();
  });
  verifierShouldError.on('error', function () {
    t.end()
  });
});

if (SUPPORTS_ENCRYPTED_KEYS) {
  test('Signing: should accept an encrypted key', function (t) {
    const alg = 'RS256';
    const signature = jws.sign({
      header: { alg: alg },
      payload: 'verifyme',
      privateKey: {
        key: rsaPrivateKeyEncrypted,
        passphrase: encryptedPassphrase
      }
    });
    t.ok(jws.verify(signature, 'RS256', rsaPublicKey));
    t.end();
  });

  test('Streaming sign: should accept an encrypted key', function (t) {
    const alg = 'RS256';
    const signer = jws.createSign({
      header: { alg: alg },
      payload: 'verifyme',
      privateKey: {
        key: rsaPrivateKeyEncrypted,
        passphrase: encryptedPassphrase
      }
    });
    const verifier = jws.createVerify({
      algorithm: alg,
      signature: signer,
      publicKey: rsaPublicKey
    });
    verifier.on('done', function (verified) {
      t.ok(verified);
      t.end();
    });
  });
}

test('jws.decode: not a jws signature', function (t) {
  t.same(jws.decode('some garbage string'), null);
  t.same(jws.decode('http://sub.domain.org'), null);
  t.end();
});

test('jws.decode: with a bogus header ', function (t) {
  const header = Buffer('oh hei José!').toString('base64');
  const payload = Buffer('sup').toString('base64');
  const sig = header + '.' + payload + '.';
  const parts = jws.decode(sig);
  t.same(parts, null);
  t.end();
});

test('jws.verify: missing or invalid algorithm', function (t) {
  const header = Buffer('{"something":"not an algo"}').toString('base64');
  const payload = Buffer('sup').toString('base64');
  const sig = header + '.' + payload + '.';
  try { jws.verify(sig) }
  catch (e) {
    t.same(e.code, 'MISSING_ALGORITHM');
  }
  try { jws.verify(sig, 'whatever') }
  catch (e) {
    t.ok(e.message.match('"whatever" is not a valid algorithm.'));
  }
  t.end();
});

test('jws.isValid', function (t) {
  const valid = jws.sign({ header: { alg: 'hs256' }, payload: 'hi', secret: 'shhh' });
  const invalid = (function(){
    const header = Buffer('oh hei José!').toString('base64');
    const payload = Buffer('sup').toString('base64');
    return header + '.' + payload + '.';
  })();
  t.same(jws.isValid('http://sub.domain.org'), false);
  t.same(jws.isValid(invalid), false);
  t.same(jws.isValid(valid), true);
  t.end();
});
<commit_msg>Test for invalid JSON in body<commit_after>/*global process*/
const Buffer = require('buffer').Buffer;
const fs = require('fs');
const test = require('tape');
const jws = require('..');

const NODE_VERSION = require('semver').clean(process.version);
const SUPPORTS_ENCRYPTED_KEYS = require('semver').gte(NODE_VERSION, '0.11.8');

function readfile(path) {
  return fs.readFileSync(__dirname + '/' + path).toString();
}

function readstream(path) {
  return fs.createReadStream(__dirname + '/' + path);
}

const rsaPrivateKey = readfile('rsa-private.pem');
const rsaPrivateKeyEncrypted = readfile('rsa-private-encrypted.pem');
const encryptedPassphrase = readfile('encrypted-key-passphrase');
const rsaPublicKey = readfile('rsa-public.pem');
const rsaWrongPublicKey = readfile('rsa-wrong-public.pem');
const ecdsaPrivateKey = {
  '256': readfile('ec256-private.pem'),
  '384': readfile('ec384-private.pem'),
  '512': readfile('ec512-private.pem'),
};
const ecdsaPublicKey = {
  '256': readfile('ec256-public.pem'),
  '384': readfile('ec384-public.pem'),
  '512': readfile('ec512-public.pem'),
};
const ecdsaWrongPublicKey = {
  '256': readfile('ec256-wrong-public.pem'),
  '384': readfile('ec384-wrong-public.pem'),
  '512': readfile('ec512-wrong-public.pem'),
};

const BITS = ['256', '384', '512'];
const CURVES = {
  '256': '256',
  '384': '384',
  '512': '521',
};

const payloadString = 'oh ćhey José!: ¬˚∆ƒå¬ß…©…åˆø˙ˆø´∆¬˚µ…˚¬˜øå…ˆßøˆƒ˜¬';
const payload = {
  name: payloadString,
  value: ['one', 2, 3]
};

BITS.forEach(function (bits) {
  test('HMAC using SHA-'+bits+' hash algorithm', function (t) {
    const alg = 'HS'+bits;
    const header = { alg: alg, typ: 'JWT' };
    const secret = 'sup';
    const jwsObj = jws.sign({
      header: header,
      payload: payload,
      secret: secret,
      encoding: 'utf8',
    });
    const parts = jws.decode(jwsObj);
    t.ok(jws.verify(jwsObj, alg, secret), 'should verify');
    t.notOk(jws.verify(jwsObj, alg, 'something else'), 'should not verify with non-matching secret');
    t.same(parts.payload, payload, 'should match payload');
    t.same(parts.header, header, 'should match header');
    t.end();
  });
});

BITS.forEach(function (bits) {
  test('RSASSA using SHA-'+bits+' hash algorithm', function (t) {
    const alg = 'RS'+bits;
    const header = { alg: alg };
    const privateKey = rsaPrivateKey;
    const publicKey = rsaPublicKey;
    const wrongPublicKey = rsaWrongPublicKey;
    const jwsObj = jws.sign({
      header: header,
      payload: payload,
      privateKey: privateKey
    });
    const parts = jws.decode(jwsObj, { json: true });
    t.ok(jws.verify(jwsObj, alg, publicKey), 'should verify');
    t.notOk(jws.verify(jwsObj, alg, wrongPublicKey), 'should not verify with non-matching public key');
    t.notOk(jws.verify(jwsObj, 'HS'+bits, publicKey), 'should not verify with non-matching algorithm');
    t.same(parts.payload, payload, 'should match payload');
    t.same(parts.header, header, 'should match header');
    t.end();
  });
});

BITS.forEach(function (bits) {
  const curve = CURVES[bits];
  test('ECDSA using P-'+curve+' curve and SHA-'+bits+' hash algorithm', function (t) {
    const alg = 'ES'+bits;
    const header = { alg: alg };
    const privateKey = ecdsaPrivateKey[bits];
    const publicKey = ecdsaPublicKey[bits];
    const wrongPublicKey = ecdsaWrongPublicKey[bits];
    const jwsObj = jws.sign({
      header: header,
      payload: payloadString,
      privateKey: privateKey
    });
    const parts = jws.decode(jwsObj);
    t.ok(jws.verify(jwsObj, alg, publicKey), 'should verify');
    t.notOk(jws.verify(jwsObj, alg, wrongPublicKey), 'should not verify with non-matching public key');
    t.notOk(jws.verify(jwsObj, 'HS'+bits, publicKey), 'should not verify with non-matching algorithm');
    t.same(parts.payload, payloadString, 'should match payload');
    t.same(parts.header, header, 'should match header');
    t.end();
  });
});

test('No digital signature or MAC value included', function (t) {
  const alg = 'none';
  const header = { alg: alg };
  const payload = 'oh hey José!';
  const jwsObj = jws.sign({
    header: header,
    payload: payload,
  });
  const parts = jws.decode(jwsObj);
  t.ok(jws.verify(jwsObj, alg), 'should verify');
  t.ok(jws.verify(jwsObj, alg, 'anything'), 'should still verify');
  t.notOk(jws.verify(jwsObj, 'HS256', 'anything'), 'should not verify with non-matching algorithm');
  t.same(parts.payload, payload, 'should match payload');
  t.same(parts.header, header, 'should match header');
  t.end();
});

test('Streaming sign: HMAC', function (t) {
  const dataStream = readstream('data.txt');
  const secret = 'shhhhh';
  const sig = jws.createSign({
    header: { alg: 'HS256' },
    secret: secret
  });
  dataStream.pipe(sig.payload);
  sig.on('done', function (signature) {
    t.ok(jws.verify(signature, 'HS256', secret), 'should verify');
    t.end();
  });
});

test('Streaming sign: RSA', function (t) {
  const dataStream = readstream('data.txt');
  const privateKeyStream = readstream('rsa-private.pem');
  const publicKey = rsaPublicKey;
  const wrongPublicKey = rsaWrongPublicKey;
  const sig = jws.createSign({
    header: { alg: 'RS256' },
  });
  dataStream.pipe(sig.payload);

  process.nextTick(function () {
    privateKeyStream.pipe(sig.key);
  });

  sig.on('done', function (signature) {
    t.ok(jws.verify(signature, 'RS256', publicKey), 'should verify');
    t.notOk(jws.verify(signature, 'RS256', wrongPublicKey), 'should not verify');
    t.same(jws.decode(signature).payload, readfile('data.txt'), 'got all the data');
    t.end();
  });
});

test('Streaming sign: RSA, predefined streams', function (t) {
  const dataStream = readstream('data.txt');
  const privateKeyStream = readstream('rsa-private.pem');
  const publicKey = rsaPublicKey;
  const wrongPublicKey = rsaWrongPublicKey;
  const sig = jws.createSign({
    header: { alg: 'RS256' },
    payload: dataStream,
    privateKey: privateKeyStream
  });
  sig.on('done', function (signature) {
    t.ok(jws.verify(signature, 'RS256', publicKey), 'should verify');
    t.notOk(jws.verify(signature, 'RS256', wrongPublicKey), 'should not verify');
    t.same(jws.decode(signature).payload, readfile('data.txt'), 'got all the data');
    t.end();
  });
});

test('Streaming verify: ECDSA', function (t) {
  const dataStream = readstream('data.txt');
  const privateKeyStream = readstream('ec512-private.pem');
  const publicKeyStream = readstream('ec512-public.pem');
  const sigStream = jws.createSign({
    header: { alg: 'ES512' },
    payload: dataStream,
    privateKey: privateKeyStream
  });
  const verifier = jws.createVerify({algorithm: 'ES512'});
  sigStream.pipe(verifier.signature);
  publicKeyStream.pipe(verifier.key);
  verifier.on('done', function (valid) {
    t.ok(valid, 'should verify');
    t.end();
  });
});

test('Streaming verify: ECDSA, with invalid key', function (t) {
  const dataStream = readstream('data.txt');
  const privateKeyStream = readstream('ec512-private.pem');
  const publicKeyStream = readstream('ec512-wrong-public.pem');
  const sigStream = jws.createSign({
    header: { alg: 'ES512' },
    payload: dataStream,
    privateKey: privateKeyStream
  });
  const verifier = jws.createVerify({
    algorithm: 'ES512',
    signature: sigStream,
    publicKey: publicKeyStream,
  });
  verifier.on('done', function (valid) {
    t.notOk(valid, 'should not verify');
    t.end();
  });
});

test('Streaming verify: errors during verify should emit as "error"', function (t) {
  const verifierShouldError = jws.createVerify({
    algorithm: 'ES512',
    signature: 'a.b.c', // the short/invalid length signature will make jwa throw
    publicKey: 'invalid-key-will-make-crypto-throw'
  });

  verifierShouldError.on('done', function () {
    t.fail();
    t.end();
  });
  verifierShouldError.on('error', function () {
    t.end()
  });
});

if (SUPPORTS_ENCRYPTED_KEYS) {
  test('Signing: should accept an encrypted key', function (t) {
    const alg = 'RS256';
    const signature = jws.sign({
      header: { alg: alg },
      payload: 'verifyme',
      privateKey: {
        key: rsaPrivateKeyEncrypted,
        passphrase: encryptedPassphrase
      }
    });
    t.ok(jws.verify(signature, 'RS256', rsaPublicKey));
    t.end();
  });

  test('Streaming sign: should accept an encrypted key', function (t) {
    const alg = 'RS256';
    const signer = jws.createSign({
      header: { alg: alg },
      payload: 'verifyme',
      privateKey: {
        key: rsaPrivateKeyEncrypted,
        passphrase: encryptedPassphrase
      }
    });
    const verifier = jws.createVerify({
      algorithm: alg,
      signature: signer,
      publicKey: rsaPublicKey
    });
    verifier.on('done', function (verified) {
      t.ok(verified);
      t.end();
    });
  });
}

test('jws.decode: not a jws signature', function (t) {
  t.same(jws.decode('some garbage string'), null);
  t.same(jws.decode('http://sub.domain.org'), null);
  t.end();
});

test('jws.decode: with a bogus header ', function (t) {
  const header = Buffer('oh hei José!').toString('base64');
  const payload = Buffer('sup').toString('base64');
  const sig = header + '.' + payload + '.';
  const parts = jws.decode(sig);
  t.same(parts, null);
  t.end();
});

test('jws.decode: with invalid json in body', function (t) {
  const header = Buffer('{"alg":"HS256","typ":"JWT"}').toString('base64');
  const payload = Buffer('sup').toString('base64');
  const sig = header + '.' + payload + '.';
  var parts;
  t.throws(function () {
    parts = jws.decode(sig);
  })
  t.end();
});

test('jws.verify: missing or invalid algorithm', function (t) {
  const header = Buffer('{"something":"not an algo"}').toString('base64');
  const payload = Buffer('sup').toString('base64');
  const sig = header + '.' + payload + '.';
  try { jws.verify(sig) }
  catch (e) {
    t.same(e.code, 'MISSING_ALGORITHM');
  }
  try { jws.verify(sig, 'whatever') }
  catch (e) {
    t.ok(e.message.match('"whatever" is not a valid algorithm.'));
  }
  t.end();
});

test('jws.isValid', function (t) {
  const valid = jws.sign({ header: { alg: 'hs256' }, payload: 'hi', secret: 'shhh' });
  const invalid = (function(){
    const header = Buffer('oh hei José!').toString('base64');
    const payload = Buffer('sup').toString('base64');
    return header + '.' + payload + '.';
  })();
  t.same(jws.isValid('http://sub.domain.org'), false);
  t.same(jws.isValid(invalid), false);
  t.same(jws.isValid(valid), true);
  t.end();
});
<|endoftext|>


<commit_before>function ImageViewer(imageDivName, img) {
  this.divName = imageDivName;
  this.img = img;
  var that = this;
  $('#' + this.divName).html('');

  var imgWidth = this.img.width;
  var imgHeight = this.img.height;
  var imgCenter = [ imgWidth / 2, -imgHeight / 2 ];
  var url = this.img.url;
  var crossOrigin = 'anonymous';
  var gsd = 2.6; // meter/pix
  // TODO the above gsd is just for example; from metadata, get the real gsd
  var fullSizeMaxGsd = 30;
  var cutoffResolution = fullSizeMaxGsd / gsd;
  var imgWidthMeters = gsd * imgWidth;
  var imgHeightMeters = gsd * imgHeight;
  var originalClipSize = 1000 / gsd;
  var bigImageMaxZoom;

  // Create a 'fake' projection for the openlayers map to use
  var proj = new ol.proj.Projection({
    code : 'ZOOMIFY',
    units : 'pixels',
    extent : [ 0, 0, imgWidth, imgHeight ]
  });

  // Zoomify image source
  var bigImageSource = new ol.source.Zoomify({
    url : url,
    size : [ imgWidth, imgHeight ],
    crossOriginKeyword : crossOrigin,
  });
  bigImageSource.tileGrid.maxZoom = 1;
  // TODO calculate the actual max zoom level for this source

  var littleImageSource = new ol.source.Zoomify({
    url : url,
    size : [ imgWidth, imgHeight ],
    crossOriginKeyword : crossOrigin,
  });

  // Create the big image layer, visible when zoomed all the way out
  var bigImageLayer = new ol.layer.Tile({
    source : bigImageSource,
    //minResolution: cutoffResolution
  });

  // Layer that restricts the image size at higher resolutions
  // (keepin the contract happy)
  var littleImageLayer = new ol.layer.Tile({
    source: littleImageSource,
    //maxResolution: cutoffResolution
  })

  // Vector layer for the planet logo and distribution statement
  var vectorLayer = new ol.layer.Vector({
    source: new ol.source.Vector()
  });

  // Create the map itself
  this.map = new ol.Map({
    interactions : ol.interaction.defaults(),
    layers : [ bigImageLayer, littleImageLayer, vectorLayer ],
    target : this.divName,
    controls : [], // Disable default controls
    view : new ol.View({
      projection : proj,
      center : imgCenter,
      zoom : 1,
      extent: [0, -imgHeight, imgWidth, 0]
    })
  });

  if (true) {  // TODO if it's a planet labs image

    // initialize attribution data variable
    var attributionData = {
        'divName':that.divName,
        'image':loadAttributionImage(attributionMode),
        'mode':attributionMode,
        'location':attributionLocation
    };

    // Affix attribution to bottom right of canvas
    vectorLayer.on('precompose', function(event) {
      var ctx = event.context;
      ctx.save();
      // var canvas_width = $("#" + that.divName).width();
      // var canvas_height = $("#" + that.divName).height();
      // var img = new Image();
      // img.src = bannerUrl;
      // var img_width = img.width;
      // var img_height = img.height;
      // var x = canvas_width - img_width;
      // var y = canvas_height - img_height;
      // ctx.globalAlpha = 0.8;
      // ctx.drawImage(img, x, y);
      // ctx.globalAlpha = 1;
      displayAttribution(ctx,attributionData);
    });

    vectorLayer.on('postcompose', function(event) {
      var ctx = event.context;
      ctx.restore();
    })

    // Restrict the visible window of the image when zoomed to a high res
    littleImageLayer.on('precompose', function(event) {
      var ctx = event.context;
      ctx.save();

      // preferred over "that.map.getView().getResolution()", as viewState
      // contains the exact resolution during a zoom animation
      var currentResolution = event.frameState.viewState.resolution;

      var clipSize = originalClipSize / currentResolution;
      var pixelRatio = event.frameState.pixelRatio;
      var size = that.map.getSize();
      var x = ((size[0]/pixelRatio) - clipSize) / 2;
      var y = ((size[1]/pixelRatio) - clipSize) / 2;

      ctx.beginPath();
      ctx.rect(x,y,clipSize,clipSize)
      ctx.clip();
    });

    littleImageLayer.on('postcompose', function(event) {
      var ctx = event.context;
      ctx.restore();
    });

  }
}

ImageViewer.prototype.blank = function() {
  $("#" + this.divName).html('');
}

function loadAttributionImage(mode) {
    var img = new Image();
    img.onload = function() {
        console.log('ATTRIBUTION IMAGE LOADED');
    }

    switch (mode) {
        case "old":
            img.src = attributionFolderUrl + "banner.png";
            break;
        default:
            img.src = attributionFolderUrl + "planet.svg";
    }
    return img;
}

function displayAttribution(context,attributionData) {
    switch (attributionData.mode) {
        case 'large':
            largeAttribution(context,attributionData);
            break;
        case 'small':
            smallAttribution(context,attributionData);
            break;
        default:
            oldAttribution(context,attributionData);
    }
}

function oldAttribution(context,data) {

    var img_width = data.image.width;
    var img_height = data.image.height;

    var pos = getLeftTop(data.divName,data.location,img_width,img_height);

    context.globalAlpha = 0.8;
    context.drawImage(data.image, pos.left, pos.top, img_width, img_height);
    context.globalAlpha = 1;
}

function smallAttribution(context,data) {

    // sizing parameters
    var fontsize = 10; //pixels
    var margin = 3;
    var lineheight = 2+fontsize;

    // attribution statement
    var attribution = "Distribution Statement C \u00B7 Imagery \u00A92016 Planet \u00B7 All rights reserved";

    // font display parameters
    context.font = fontsize.toString() + 'px Segoe UI';
    context.textAlign = 'left';
    context.textBaseline = 'bottom'

    // attrib. block width for 1 line attribution
    var metrics = context.measureText(attribution);
    var block_width = metrics.width + 2*margin;

    // expand block if necessary
    var canvas_size = getCanvasSize(data.divName);
    block_width = Math.min(block_width,canvas_size.width)

    // wrap text to rectangle width
    var attribution_lines = wrapText(context,attribution,canvas_size.width-2*margin);

    // attrib. block height
    var block_height = 2*margin + fontsize + (attribution_lines.length-1)*lineheight;

    // left & top of full attrib. block
    var pos = getLeftTop(data.divName,data.location,block_width,block_height);

    // background rectangle
    context.globalAlpha=0.5;
    context.fillStyle = '#000';
    context.fillRect(pos.left,pos.top,block_width,block_height);

    // display all text lines
    context.globalAlpha=1.0;
    context.fillStyle = '#FFF';

    xtmp = pos.left+margin;
    ytmp = pos.top+margin+fontsize;
    for (var k = 0; k < attribution_lines.length; k++) {
        context.fillText(attribution_lines[k],xtmp,ytmp)
        ytmp += lineheight;
    }

    //cleanup
    context.globalAlpha=1.0;
}

function largeAttribution(context,data) {

    // sizing parameters
    var fontsize = 10; //pixels
    var lineheight = 2+fontsize;
    var margin = 5;
    var block_width = 400;
    var img_height = 20;

    // copyright & distribution statement text
    var copytext = 'Imagery \u00A92016 Planet \u00B7 All rights reserved'
    var disttext = 'DISTRIBUTION STATEMENT C: Distribution authorized to U.S. Government Agencies and their contractors (Administrative or Operational Use). Other requests for this document shall be referred to the AFRL, Wright-Patterson Air Force Base, OH 45433-7321.';

    // font display parameters
    context.font = fontsize.toString() + 'px Segoe UI';
    context.textAlign = 'left';
    context.textBaseline = 'bottom'

    // reduce attrib. block width if necessary
    var canvas_size = getCanvasSize(data.divName);
    block_width = Math.min(block_width,canvas_size.width);

    // scaled image width (accomodate missing/not yet loaded image)
    var img_width;
    if (data.image.width==0) {
        img_height = 0;
        img_width = 0;
    } else {
        img_width = img_height * (data.image.width/data.image.height);
    }

    // copyright lines
    // if wrapping is required, eliminate the icon
    var metrics = context.measureText(copytext);

    var copylines;
    if (metrics.width <= block_width-3*margin-img_width) {
        copylines = [copytext];
    } else {
        img_width = 0;
        img_height = 0;
        copylines = wrapText(context,copytext,block_width);
    }

    // expand block width if necessary
    //block_width = Math.max(block_width,4*margin+img_width+copy_width)

    // wrapped distribution statement
    var distlines = wrapText(context, disttext, block_width-2*margin);

    // block height
    var copylines_height = fontsize + (copylines.length-1)*lineheight;
    var copy_height = Math.max(img_height,copylines_height);
    var dist_height = fontsize + (distlines.length-1)*lineheight;
    var block_height = 3*margin+copy_height+dist_height;

    // left & top of full attribution block
    var pos = getLeftTop(data.divName,data.location,block_width,block_height);

    // draw rectangle
    context.globalAlpha=0.5;
    context.fillStyle = '#000';
    context.fillRect(pos.left,pos.top,block_width,block_height);

    // initialize text drawing variables
    var xtmp,ytmp;

    // draw image
    if (img_width>0) {
        context.globalAlpha=1;
        xtmp = pos.left + margin;
        ytmp = pos.top + margin + (copy_height-img_height)/2;
        context.drawImage(data.image,xtmp,ytmp, img_width, img_height);
    }

    // draw copyright text
    context.globalAlpha=1;
    context.fillStyle = '#FFF';

    if (img_width==0) {
        xtmp = pos.left+margin;
    } else {
        xtmp = pos.left+img_width+2*margin;
    }
    ytmp = pos.top + margin + (copy_height-copylines_height)/2 + fontsize;

    for (var k = 0; k < copylines.length; k++) {
        context.fillText(copylines[k],xtmp,ytmp)
        ytmp += lineheight;
    }

    // draw distribution statement text
    context.globalAlpha=1;
    context.fillStyle = '#FFF';

    xtmp = pos.left+margin;
    ytmp = pos.top+2*margin+copy_height+fontsize;
    for (var k = 0; k < distlines.length; k++) {
        context.fillText(distlines[k],xtmp,ytmp)
        ytmp += lineheight;
    }

    // cleanup
    context.globalAlpha=1;

}

function wrapText(context, text, maxWidth) {
    var words = text.split(' ');
    var lines = [];

    var line = '';
    for(var n = 0; n < words.length; n++) {
        var testLine = line + words[n] + ' ';
        var metrics = context.measureText(testLine);
        var testWidth = metrics.width;
        if (testWidth > maxWidth && n > 0) {
            lines.push(line);
            line = words[n] + ' ';
        }
        else {
            line = testLine;
        }
    }
    lines.push(line);
    return lines;
}

function getCanvasSize(divName) {
    var width = $("#" + divName).width();
    var height = $("#" + divName).height();
    return {'width': width,'height':height};
}

function getLeftTop(divName,location,obj_width,obj_height) {
    var canvas_size = getCanvasSize(divName);

    var left,top;
    switch (location) {
        case 'topleft':
            left = 0;
            top = 0;
            break;
        case 'topright':
            left = canvas_size.width - obj_width;
            top = 0;
            break;
        case 'bottomleft':
            left = 0;
            top = canvas_size.height - obj_height;
            break;
        default:
            left = canvas_size.width - obj_width;
            top = canvas_size.height - obj_height;
    }

    return {'left':left,'top':top};
}<commit_msg>Changed image view so high-res window now follows the mouse<commit_after>function ImageViewer(imageDivName, img) {
  this.divName = imageDivName;
  this.img = img;
  var that = this;
  $('#' + this.divName).html('');

  var imgWidth = this.img.width;
  var imgHeight = this.img.height;
  var imgCenter = [ imgWidth / 2, -imgHeight / 2 ];
  var url = this.img.url;
  var crossOrigin = 'anonymous';
  var gsd = 2.6; // meter/pix
  // TODO the above gsd is just for example; from metadata, get the real gsd
  var fullSizeMaxGsd = 30;
  var cutoffResolution = fullSizeMaxGsd / gsd;
  var imgWidthMeters = gsd * imgWidth;
  var imgHeightMeters = gsd * imgHeight;
  var originalClipSize = 1000 / gsd;
  var bigImageMaxZoom;

  // Create a 'fake' projection for the openlayers map to use
  var proj = new ol.proj.Projection({
    code : 'ZOOMIFY',
    units : 'pixels',
    extent : [ 0, 0, imgWidth, imgHeight ]
  });

  // Zoomify image source
  var bigImageSource = new ol.source.Zoomify({
    url : url,
    size : [ imgWidth, imgHeight ],
    crossOriginKeyword : crossOrigin,
  });
  bigImageSource.tileGrid.maxZoom = 1;
  // TODO calculate the actual max zoom level for this source

  var littleImageSource = new ol.source.Zoomify({
    url : url,
    size : [ imgWidth, imgHeight ],
    crossOriginKeyword : crossOrigin,
  });

  // Create the big image layer, visible when zoomed all the way out
  var bigImageLayer = new ol.layer.Tile({
    source : bigImageSource,
  });

  // Layer that restricts the image size at higher resolutions
  // (keepin the contract happy)
  var littleImageLayer = new ol.layer.Tile({
    source: littleImageSource,
  })

  // Vector layer for the planet logo and distribution statement
  var vectorLayer = new ol.layer.Vector({
    source: new ol.source.Vector()
  });

  // Create the map itself
  this.map = new ol.Map({
    interactions : ol.interaction.defaults(),
    layers : [ bigImageLayer, littleImageLayer, vectorLayer ],
    target : this.divName,
    controls : [], // Disable default controls
    view : new ol.View({
      projection : proj,
      center : imgCenter,
      zoom : 1,
      extent: [0, -imgHeight, imgWidth, 0]
    })
  });

  // Check in the db whether it's a planet image, and only set the attribution
  // logo and zoom restrictions if it is.
  $.get("/meta/rest/auto/image/"+img.id, setAttribution, 'json');

  function setAttribution(data) {
    if (data._attributes !== "") {
      var attributes = JSON.parse(data._attributes);
    } else {
      var attributes = {};
    }

    if (attributes.planet_rest_response) {

      // initialize attribution data variable
      var attributionData = {
          'divName':that.divName,
          'image':loadAttributionImage(attributionMode),
          'mode':attributionMode,
          'location':attributionLocation
      };

      vectorLayer.on('precompose', function(event) {
        var ctx = event.context;
        ctx.save();
        displayAttribution(ctx,attributionData);
      });

      vectorLayer.on('postcompose', function(event) {
        var ctx = event.context;
        ctx.restore();
      })

      var x; var y;

      // Restrict the visible window of the image when zoomed to a high res
      littleImageLayer.on('precompose', function(event) {
        var ctx = event.context;
        ctx.save();

        // preferred over "that.map.getView().getResolution()", as viewState
        // contains the exact resolution during a zoom animation
        var currentResolution = event.frameState.viewState.resolution;

        var clipSize = originalClipSize / currentResolution;
        var pixelRatio = event.frameState.pixelRatio;
        var size = that.map.getSize();

        if (mousePosition) {
          x = mousePosition[0] - (clipSize / 2);
          y = mousePosition[1] - (clipSize / 2);
        } else {
          // var x = ((size[0]/pixelRatio) - clipSize) / 2;
          // var y = ((size[1]/pixelRatio) - clipSize) / 2;
        }

        ctx.beginPath();
        ctx.rect(x,y,clipSize,clipSize)
        ctx.clip();
      });

      littleImageLayer.on('postcompose', function(event) {
        var ctx = event.context;
        ctx.restore();
      });

      var mousePosition = null;
      var container = document.getElementById(that.divName);

      container.addEventListener('mousemove', function(event) {
        mousePosition = that.map.getEventPixel(event);
        that.map.render();
      });

      container.addEventListener('mouseout', function(event) {
        mousePosition = null;
        that.map.render();
      });
    }
  }
}

ImageViewer.prototype.blank = function() {
  $("#" + this.divName).html('');
}

function loadAttributionImage(mode) {
    var img = new Image();
    img.onload = function() {
        console.log('ATTRIBUTION IMAGE LOADED');
    }

    switch (mode) {
        case "old":
            img.src = attributionFolderUrl + "banner.png";
            break;
        default:
            img.src = attributionFolderUrl + "planet.svg";
    }
    return img;
}

function displayAttribution(context,attributionData) {
    switch (attributionData.mode) {
        case 'large':
            largeAttribution(context,attributionData);
            break;
        case 'small':
            smallAttribution(context,attributionData);
            break;
        default:
            oldAttribution(context,attributionData);
    }
}

function oldAttribution(context,data) {

    var img_width = data.image.width;
    var img_height = data.image.height;

    var pos = getLeftTop(data.divName,data.location,img_width,img_height);

    context.globalAlpha = 0.8;
    context.drawImage(data.image, pos.left, pos.top, img_width, img_height);
    context.globalAlpha = 1;
}

function smallAttribution(context,data) {

    // sizing parameters
    var fontsize = 10; //pixels
    var margin = 3;
    var lineheight = 2+fontsize;

    // attribution statement
    var attribution = "Distribution Statement C \u00B7 Imagery \u00A92016 Planet \u00B7 All rights reserved";

    // font display parameters
    context.font = fontsize.toString() + 'px Segoe UI';
    context.textAlign = 'left';
    context.textBaseline = 'bottom'

    // attrib. block width for 1 line attribution
    var metrics = context.measureText(attribution);
    var block_width = metrics.width + 2*margin;

    // expand block if necessary
    var canvas_size = getCanvasSize(data.divName);
    block_width = Math.min(block_width,canvas_size.width)

    // wrap text to rectangle width
    var attribution_lines = wrapText(context,attribution,canvas_size.width-2*margin);

    // attrib. block height
    var block_height = 2*margin + fontsize + (attribution_lines.length-1)*lineheight;

    // left & top of full attrib. block
    var pos = getLeftTop(data.divName,data.location,block_width,block_height);

    // background rectangle
    context.globalAlpha=0.5;
    context.fillStyle = '#000';
    context.fillRect(pos.left,pos.top,block_width,block_height);

    // display all text lines
    context.globalAlpha=1.0;
    context.fillStyle = '#FFF';

    xtmp = pos.left+margin;
    ytmp = pos.top+margin+fontsize;
    for (var k = 0; k < attribution_lines.length; k++) {
        context.fillText(attribution_lines[k],xtmp,ytmp)
        ytmp += lineheight;
    }

    //cleanup
    context.globalAlpha=1.0;
}

function largeAttribution(context,data) {

    // sizing parameters
    var fontsize = 10; //pixels
    var lineheight = 2+fontsize;
    var margin = 5;
    var block_width = 400;
    var img_height = 20;

    // copyright & distribution statement text
    var copytext = 'Imagery \u00A92016 Planet \u00B7 All rights reserved'
    var disttext = 'DISTRIBUTION STATEMENT C: Distribution authorized to U.S. Government Agencies and their contractors (Administrative or Operational Use). Other requests for this document shall be referred to the AFRL, Wright-Patterson Air Force Base, OH 45433-7321.';

    // font display parameters
    context.font = fontsize.toString() + 'px Segoe UI';
    context.textAlign = 'left';
    context.textBaseline = 'bottom'

    // reduce attrib. block width if necessary
    var canvas_size = getCanvasSize(data.divName);
    block_width = Math.min(block_width,canvas_size.width);

    // scaled image width (accomodate missing/not yet loaded image)
    var img_width;
    if (data.image.width==0) {
        img_height = 0;
        img_width = 0;
    } else {
        img_width = img_height * (data.image.width/data.image.height);
    }

    // copyright lines
    // if wrapping is required, eliminate the icon
    var metrics = context.measureText(copytext);

    var copylines;
    if (metrics.width <= block_width-3*margin-img_width) {
        copylines = [copytext];
    } else {
        img_width = 0;
        img_height = 0;
        copylines = wrapText(context,copytext,block_width);
    }

    // expand block width if necessary
    //block_width = Math.max(block_width,4*margin+img_width+copy_width)

    // wrapped distribution statement
    var distlines = wrapText(context, disttext, block_width-2*margin);

    // block height
    var copylines_height = fontsize + (copylines.length-1)*lineheight;
    var copy_height = Math.max(img_height,copylines_height);
    var dist_height = fontsize + (distlines.length-1)*lineheight;
    var block_height = 3*margin+copy_height+dist_height;

    // left & top of full attribution block
    var pos = getLeftTop(data.divName,data.location,block_width,block_height);

    // draw rectangle
    context.globalAlpha=0.5;
    context.fillStyle = '#000';
    context.fillRect(pos.left,pos.top,block_width,block_height);

    // initialize text drawing variables
    var xtmp,ytmp;

    // draw image
    if (img_width>0) {
        context.globalAlpha=1;
        xtmp = pos.left + margin;
        ytmp = pos.top + margin + (copy_height-img_height)/2;
        context.drawImage(data.image,xtmp,ytmp, img_width, img_height);
    }

    // draw copyright text
    context.globalAlpha=1;
    context.fillStyle = '#FFF';

    if (img_width==0) {
        xtmp = pos.left+margin;
    } else {
        xtmp = pos.left+img_width+2*margin;
    }
    ytmp = pos.top + margin + (copy_height-copylines_height)/2 + fontsize;

    for (var k = 0; k < copylines.length; k++) {
        context.fillText(copylines[k],xtmp,ytmp)
        ytmp += lineheight;
    }

    // draw distribution statement text
    context.globalAlpha=1;
    context.fillStyle = '#FFF';

    xtmp = pos.left+margin;
    ytmp = pos.top+2*margin+copy_height+fontsize;
    for (var k = 0; k < distlines.length; k++) {
        context.fillText(distlines[k],xtmp,ytmp)
        ytmp += lineheight;
    }

    // cleanup
    context.globalAlpha=1;

}

function wrapText(context, text, maxWidth) {
    var words = text.split(' ');
    var lines = [];

    var line = '';
    for(var n = 0; n < words.length; n++) {
        var testLine = line + words[n] + ' ';
        var metrics = context.measureText(testLine);
        var testWidth = metrics.width;
        if (testWidth > maxWidth && n > 0) {
            lines.push(line);
            line = words[n] + ' ';
        }
        else {
            line = testLine;
        }
    }
    lines.push(line);
    return lines;
}

function getCanvasSize(divName) {
    var width = $("#" + divName).width();
    var height = $("#" + divName).height();
    return {'width': width,'height':height};
}

function getLeftTop(divName,location,obj_width,obj_height) {
    var canvas_size = getCanvasSize(divName);

    var left,top;
    switch (location) {
        case 'topleft':
            left = 0;
            top = 0;
            break;
        case 'topright':
            left = canvas_size.width - obj_width;
            top = 0;
            break;
        case 'bottomleft':
            left = 0;
            top = canvas_size.height - obj_height;
            break;
        default:
            left = canvas_size.width - obj_width;
            top = canvas_size.height - obj_height;
    }

    return {'left':left,'top':top};
}<|endoftext|>


<commit_before>/* global Player, Projectile, Zombie, Explosion, ObjectPoolMaker */

(function () {
  'use strict';

  var Game = {
    init: function () {
      Game.canvas = document.getElementById('game-canvas');
      Game.context = Game.canvas.getContext('2d');

      Game.loadBackground();
      Game.bind();
      Game.reset();
    },

    bind: function () {
      document.addEventListener('keydown', function (e) {
        Game.keysDown[e.keyCode] = true;
      }, false);

      document.addEventListener('keyup', function (e) {
        delete Game.keysDown[e.keyCode];
      }, false);

      document.getElementById('play-again').addEventListener('click', function (e) {
        Game.reset();
      });
    },

    reset: function () {
      // Current game difficulty
      Game.difficulty = 0.01;

      // Max difficulty that Game can reach at any given time
      // It represents the chance percentage of spawning a new Zombie
      Game.maxDifficulty = 0.2;

      // How much difficulty increases after its cooldown elapsed
      Game.difficultyIncrement = 0.005;

      // How long it takes (in seconds) to difficulty to increase
      Game.difficultyCooldown = 3;

      // Last time difficulty was increased
      Game.lastDifficultyIncrease = 0;

      // Time elapsed since Game began
      Game.gameTime = 0;

      // Total score from killing zombies
      Game.score = 0;

      // Maps which keys as currently pressed by e.keyCode
      Game.keysDown = {};

      // Last time a projectile was shot by the player
      Game.lastProjectileTime = 0;

      // How long it takes (in ms) for another projectile to be fired
      Game.projectileCooldown = 100;

      // How many pixels Player and Zombies are constrained by vertically/horizontally
      // (this is interesting according to the background image used)
      Game.verticalBoundary = 35;
      Game.horizontalBoundary = 5;

      Game.isGameOver = false;

      Game.player = new Player(
        Game.canvas.width / 2 - Player.width / 2,
        Game.canvas.height / 2 - Player.height / 2
      );

      // Store pool of objects to be reused for each Entity
      Game.zombiePool = new ObjectPoolMaker(Zombie, 100);
      Game.projectilePool = new ObjectPoolMaker(Projectile, 100);

      // Got lazy here and just used an array.. soon to be updated :p
      Game.explosions = [];

      document.getElementById('game-over-overlay').style.display = 'none';
      document.getElementById('game-over').style.display = 'none';
    },

    loadBackground: function () {
      Game.backgroundReady = false;

      Game.backgroundImg = new Image();
      Game.backgroundImg.src = 'public/images/background.png';

      Game.backgroundImg.onload = function () {
        Game.backgroundReady = true;
      };
    },

    update: function (modifier) {
      Game.gameTime += modifier;

      Game.handleInput(modifier);
      Game.handleDifficulty();
      Game.spawnZombies();
      Game.updateEntities(modifier);
    },

    handleInput: function (modifier) {
      if (Game.isGameOver) {
        return;
      }

      var now = Date.now();

      // UP
      if (Game.keysDown[38] && Game.player.y > Game.verticalBoundary) {
        Game.player.setY(Game.player.y - (Game.player.speed * modifier));
      }

      // DOWN
      if (Game.keysDown[40] && Game.player.y < Game.canvas.height - Game.verticalBoundary - Player.height) {
        Game.player.setY(Game.player.y + (Game.player.speed * modifier));
      }

      // RIGHT
      if (Game.keysDown[39] && Game.player.x < Game.canvas.width - Game.horizontalBoundary - Player.width) {
        Game.player.setX(Game.player.x + (Game.player.speed * modifier));
      }

      // LEFT
      if (Game.keysDown[37] && Game.player.x > Game.horizontalBoundary) {
        Game.player.setX(Game.player.x - (Game.player.speed * modifier));
      }

      // SPACE - shoot!
      if (Game.keysDown[32]) {
        // prevent spamming projectiles
        if (now - Game.lastProjectileTime > Game.projectileCooldown) {
          Game.projectilePool.create(
            Game.player.x,
            Game.player.y + (Player.height / 2) - (Projectile.height / 2)
          );
          Game.lastProjectileTime = now;
        }
      }
    },

    handleDifficulty: function () {
      // It gets harder as time goes by..
      if (Game.difficulty < Game.maxDifficulty &&
          Game.gameTime - Game.lastDifficultyIncrease > Game.difficultyCooldown) {
        Game.difficulty += Game.difficultyIncrement;
        Game.lastDifficultyIncrease = Game.gameTime;
      }
    },

    spawnZombies: function () {
      // Create some enemies
      if (Math.random() < Game.difficulty) {
        // resets x coordinate
        // randomize new y coordinate (constrained in available map space)
        Game.zombiePool.create(
          Game.canvas.width,
          Math.random() * (Game.canvas.height - Zombie.height - (2 * Game.verticalBoundary)) + Game.verticalBoundary
        );
      }
    },

    updateEntities: function (modifier) {
      Game.updateProjectilesAndKillZombies(modifier);
      Game.updateZombies(modifier);
      Game.updateExplosions();
      Game.updatePlayer();
    },

    updateProjectilesAndKillZombies: function (modifier) {
      var i,
          j,
          zombie,
          projectile;

      // update all projectiles
      for (i = 0; i < Game.projectilePool.size(); i++) {
        projectile = Game.projectilePool.objectPool()[i];

        projectile.setX(projectile.x + (projectile.speed * modifier));

        // delete projectile if out of the scene
        if (projectile.x > Game.canvas.width) {
          Game.projectilePool.destroy(projectile);
          i--;
        }
        else {
          // kill zombies!
          for (j = 0; j < Game.zombiePool.size(); j++) {
            zombie = Game.zombiePool.objectPool()[j];
            if (projectile.isCollided(zombie)) {
              Game.explosions.push(new Explosion(zombie.x, zombie.y));

              // hundred points for each zombie!
              Game.score += 100;

              Game.projectilePool.destroy(projectile);
              i--;
              Game.zombiePool.destroy(zombie);
              j--;

              break;
            }
          }
        }
      }
    },

    updateZombies: function (modifier) {
      var zombie,
          i,
          now = Date.now();

      // update all enemies
      for (i = 0; i < Game.zombiePool.size(); i++) {
        zombie = Game.zombiePool.objectPool()[i];
        zombie.update(now);
        zombie.setX(zombie.x - (zombie.speed * modifier));

        // delete zombie if out of the scene
        if (zombie.x + Zombie.width < 0) {
          Game.zombiePool.destroy(zombie);
          i--;
        }
      }
    },

    updateExplosions: function () {
      var i;

      for (i = 0; i < Game.explosions.length; ++i) {
        Game.explosions[i].update();
        if (Game.explosions[i].currentFrame >= Explosion.framesPosition.length - 1) {
          Game.explosions.splice(i--, 1);
        }
      }
    },

    updatePlayer: function () {
      var zombie,
          i;

      if (!Game.isGameOver) {
        for (i = 0; i < Game.zombiePool.size(); ++i) {
          zombie = Game.zombiePool.objectPool()[i];
          if (zombie.isCollided(Game.player)) {
            Game.gameOver();
            break;
          }
        }
      }
    },

    gameOver: function () {
      document.getElementById('game-over-overlay').style.display = 'block';
      document.getElementById('game-over').style.display = 'block';
      Game.isGameOver = true;
    },

    render: function () {
      var i;

      // Render Background
      if (Game.backgroundReady) {
        Game.context.drawImage(Game.backgroundImg, 0, 0);
      }

      // Render Projectiles
      for (i = 0; i < Game.projectilePool.size(); ++i) {
        Game.projectilePool.objectPool()[i].render(Game.context);
      }

      // Render Explosions
      for (i = 0; i < Game.explosions.length; ++i) {
        Game.explosions[i].render(Game.context);
      }

      // Render Zombies
      for (i = 0; i < Game.zombiePool.size(); ++i) {
        Game.zombiePool.objectPool()[i].render(Game.context);
      }

      // Render Player
      if (!Game.isGameOver) {
        Game.player.render(Game.context);
      }

      // Render score
      Game.renderStatus();
    },

    renderStatus: function () {
      Game.context.fillStyle = '#fff';
      Game.context.font = '20px Helvetica';
      Game.context.textAlign = 'left';
      Game.context.textBaseline = 'top';

      Game.context.fillText('Score: ' + Game.score, 10, 5);
      Game.context.fillText('Level: ' + Game.player.level, Game.canvas.width - 110, 5);
    }
  };

  // expose Game globally
  window.Game = Game;
}());
<commit_msg>JsHint was complaining that "e" argument was never used - and I don't like complains :p<commit_after>/* global Player, Projectile, Zombie, Explosion, ObjectPoolMaker */

(function () {
  'use strict';

  var Game = {
    init: function () {
      Game.canvas = document.getElementById('game-canvas');
      Game.context = Game.canvas.getContext('2d');

      Game.loadBackground();
      Game.bind();
      Game.reset();
    },

    bind: function () {
      document.addEventListener('keydown', function (e) {
        Game.keysDown[e.keyCode] = true;
      }, false);

      document.addEventListener('keyup', function (e) {
        delete Game.keysDown[e.keyCode];
      }, false);

      document.getElementById('play-again').addEventListener('click', function () {
        Game.reset();
      });
    },

    reset: function () {
      // Current game difficulty
      Game.difficulty = 0.01;

      // Max difficulty that Game can reach at any given time
      // It represents the chance percentage of spawning a new Zombie
      Game.maxDifficulty = 0.2;

      // How much difficulty increases after its cooldown elapsed
      Game.difficultyIncrement = 0.005;

      // How long it takes (in seconds) to difficulty to increase
      Game.difficultyCooldown = 3;

      // Last time difficulty was increased
      Game.lastDifficultyIncrease = 0;

      // Time elapsed since Game began
      Game.gameTime = 0;

      // Total score from killing zombies
      Game.score = 0;

      // Maps which keys as currently pressed by e.keyCode
      Game.keysDown = {};

      // Last time a projectile was shot by the player
      Game.lastProjectileTime = 0;

      // How long it takes (in ms) for another projectile to be fired
      Game.projectileCooldown = 100;

      // How many pixels Player and Zombies are constrained by vertically/horizontally
      // (this is interesting according to the background image used)
      Game.verticalBoundary = 35;
      Game.horizontalBoundary = 5;

      Game.isGameOver = false;

      Game.player = new Player(
        Game.canvas.width / 2 - Player.width / 2,
        Game.canvas.height / 2 - Player.height / 2
      );

      // Store pool of objects to be reused for each Entity
      Game.zombiePool = new ObjectPoolMaker(Zombie, 100);
      Game.projectilePool = new ObjectPoolMaker(Projectile, 100);

      // Got lazy here and just used an array.. soon to be updated :p
      Game.explosions = [];

      document.getElementById('game-over-overlay').style.display = 'none';
      document.getElementById('game-over').style.display = 'none';
    },

    loadBackground: function () {
      Game.backgroundReady = false;

      Game.backgroundImg = new Image();
      Game.backgroundImg.src = 'public/images/background.png';

      Game.backgroundImg.onload = function () {
        Game.backgroundReady = true;
      };
    },

    update: function (modifier) {
      Game.gameTime += modifier;

      Game.handleInput(modifier);
      Game.handleDifficulty();
      Game.spawnZombies();
      Game.updateEntities(modifier);
    },

    handleInput: function (modifier) {
      if (Game.isGameOver) {
        return;
      }

      var now = Date.now();

      // UP
      if (Game.keysDown[38] && Game.player.y > Game.verticalBoundary) {
        Game.player.setY(Game.player.y - (Game.player.speed * modifier));
      }

      // DOWN
      if (Game.keysDown[40] && Game.player.y < Game.canvas.height - Game.verticalBoundary - Player.height) {
        Game.player.setY(Game.player.y + (Game.player.speed * modifier));
      }

      // RIGHT
      if (Game.keysDown[39] && Game.player.x < Game.canvas.width - Game.horizontalBoundary - Player.width) {
        Game.player.setX(Game.player.x + (Game.player.speed * modifier));
      }

      // LEFT
      if (Game.keysDown[37] && Game.player.x > Game.horizontalBoundary) {
        Game.player.setX(Game.player.x - (Game.player.speed * modifier));
      }

      // SPACE - shoot!
      if (Game.keysDown[32]) {
        // prevent spamming projectiles
        if (now - Game.lastProjectileTime > Game.projectileCooldown) {
          Game.projectilePool.create(
            Game.player.x,
            Game.player.y + (Player.height / 2) - (Projectile.height / 2)
          );
          Game.lastProjectileTime = now;
        }
      }
    },

    handleDifficulty: function () {
      // It gets harder as time goes by..
      if (Game.difficulty < Game.maxDifficulty &&
          Game.gameTime - Game.lastDifficultyIncrease > Game.difficultyCooldown) {
        Game.difficulty += Game.difficultyIncrement;
        Game.lastDifficultyIncrease = Game.gameTime;
      }
    },

    spawnZombies: function () {
      // Create some enemies
      if (Math.random() < Game.difficulty) {
        // resets x coordinate
        // randomize new y coordinate (constrained in available map space)
        Game.zombiePool.create(
          Game.canvas.width,
          Math.random() * (Game.canvas.height - Zombie.height - (2 * Game.verticalBoundary)) + Game.verticalBoundary
        );
      }
    },

    updateEntities: function (modifier) {
      Game.updateProjectilesAndKillZombies(modifier);
      Game.updateZombies(modifier);
      Game.updateExplosions();
      Game.updatePlayer();
    },

    updateProjectilesAndKillZombies: function (modifier) {
      var i,
          j,
          zombie,
          projectile;

      // update all projectiles
      for (i = 0; i < Game.projectilePool.size(); i++) {
        projectile = Game.projectilePool.objectPool()[i];

        projectile.setX(projectile.x + (projectile.speed * modifier));

        // delete projectile if out of the scene
        if (projectile.x > Game.canvas.width) {
          Game.projectilePool.destroy(projectile);
          i--;
        }
        else {
          // kill zombies!
          for (j = 0; j < Game.zombiePool.size(); j++) {
            zombie = Game.zombiePool.objectPool()[j];
            if (projectile.isCollided(zombie)) {
              Game.explosions.push(new Explosion(zombie.x, zombie.y));

              // hundred points for each zombie!
              Game.score += 100;

              Game.projectilePool.destroy(projectile);
              i--;
              Game.zombiePool.destroy(zombie);
              j--;

              break;
            }
          }
        }
      }
    },

    updateZombies: function (modifier) {
      var zombie,
          i,
          now = Date.now();

      // update all enemies
      for (i = 0; i < Game.zombiePool.size(); i++) {
        zombie = Game.zombiePool.objectPool()[i];
        zombie.update(now);
        zombie.setX(zombie.x - (zombie.speed * modifier));

        // delete zombie if out of the scene
        if (zombie.x + Zombie.width < 0) {
          Game.zombiePool.destroy(zombie);
          i--;
        }
      }
    },

    updateExplosions: function () {
      var i;

      for (i = 0; i < Game.explosions.length; ++i) {
        Game.explosions[i].update();
        if (Game.explosions[i].currentFrame >= Explosion.framesPosition.length - 1) {
          Game.explosions.splice(i--, 1);
        }
      }
    },

    updatePlayer: function () {
      var zombie,
          i;

      if (!Game.isGameOver) {
        for (i = 0; i < Game.zombiePool.size(); ++i) {
          zombie = Game.zombiePool.objectPool()[i];
          if (zombie.isCollided(Game.player)) {
            Game.gameOver();
            break;
          }
        }
      }
    },

    gameOver: function () {
      document.getElementById('game-over-overlay').style.display = 'block';
      document.getElementById('game-over').style.display = 'block';
      Game.isGameOver = true;
    },

    render: function () {
      var i;

      // Render Background
      if (Game.backgroundReady) {
        Game.context.drawImage(Game.backgroundImg, 0, 0);
      }

      // Render Projectiles
      for (i = 0; i < Game.projectilePool.size(); ++i) {
        Game.projectilePool.objectPool()[i].render(Game.context);
      }

      // Render Explosions
      for (i = 0; i < Game.explosions.length; ++i) {
        Game.explosions[i].render(Game.context);
      }

      // Render Zombies
      for (i = 0; i < Game.zombiePool.size(); ++i) {
        Game.zombiePool.objectPool()[i].render(Game.context);
      }

      // Render Player
      if (!Game.isGameOver) {
        Game.player.render(Game.context);
      }

      // Render score
      Game.renderStatus();
    },

    renderStatus: function () {
      Game.context.fillStyle = '#fff';
      Game.context.font = '20px Helvetica';
      Game.context.textAlign = 'left';
      Game.context.textBaseline = 'top';

      Game.context.fillText('Score: ' + Game.score, 10, 5);
      Game.context.fillText('Level: ' + Game.player.level, Game.canvas.width - 110, 5);
    }
  };

  // expose Game globally
  window.Game = Game;
}());
<|endoftext|>


<commit_before>/**
 * The SidebarNavView view.
 */
define([
  'jquery',
  'backbone',
  'underscore',
  'mps'
], function($,Backbone, _,mps) {

  'use strict';

  var SidebarNavModel = Backbone.Model.extend({
    defaults: {
      offset: null,
      height: null
    }
  });

  var SidebarNavView = Backbone.View.extend({

    el: '#sidebarNavView',

    events: {
      'click .nav-item' : 'updateSource',
      'click .nav-title' : 'scrollTo',
      'click #back-btn' : 'returnBack',
      'click .source_header' : 'toggleSources',
      'click .source_dropdown_header' : 'toggleDropdown',
      'click .source_dropdown_menu a' : 'showSubContent'
    },

    initialize: function() {
      if (!this.$el.length) {
        return
      }
      //CACHE
      this.$window = $(window);
      this.$document = $(document);
      this.$htmlbody = $('html,body');
      this.$headerH1 = $('#headerView').find('h1');
      this.$backBtn = $('#back-btn');
      this.$navItem = this.$el.find('.nav-item');
      this.$sideBarAside = $('#sidebarAside');
      this.$sideBarBox = $('#sources-box');
      this.$sourceArticle = $('.source-article');
      this.$sourceHeader = $('.source_header');
      this.$sourceBody = $('.source_body');
      this.$cut = $('#cut');
      this.$sourceSpinner = $('#sources-spinner');

      //VARS
      this.padding = 40;
      this.first = true;
      this.mobile = (this.$window.width() > 850) ? false : true;

      //INIT
      this.setListeners();
    },

    setListeners: function(){
      this.calculateOffsets();
      this.scrollDocument();
      this.$document.on('scroll',_.bind(this.scrollDocument,this));
      this.$window.on('resize',_.bind(this.calculateOffsets,this));

      mps.subscribe('SourceStatic/change',_.bind(this.changeSource,this));
      mps.subscribe('SubItem/change',_.bind(this.calculateOffsets,this));
    },

    toggleDropdown: function(e){
      e && e.preventDefault();
      $(e.currentTarget).parents('.source_dropdown').find('.source_dropdown_menu').toggle(0);
    },

    showSubContent:function(e){
      e && e.preventDefault();
      $(e.currentTarget).parents('.source_dropdown').find('.source_dropdown_menu').hide(0);

      var text = $(e.currentTarget).text();
      var id = $(e.currentTarget).data('slug');
      $(e.currentTarget).parents('.source_dropdown').find('.source_dropdown_header').find('.overview_title').children('span').text(text);

      $('.source_dropdown_body').hide(0);
      $('#'+id).show(0);

      this.calculateOffsets();

    },

    toggleSources: function(e){
      this.$sourceBody.hide(0);
      this.$sourceHeader.removeClass('active');

      if(!$(e.currentTarget).hasClass('active')){
        $(e.currentTarget).addClass('active');
        $(e.currentTarget).parent().children('.source_body').show(0);
      }

      this.$htmlbody.animate({ scrollTop: this.$sideBarBox.offset().top - this.padding },0);

      setTimeout(_.bind(function(){
        this.calculateOffsets();
      },this),50);
    },

    calculateOffsets: function(){
      this.$sideBarBox.css({'min-height': this.$sideBarAside.height() });
      this.offset = this.$el.offset().top + parseInt(this.$el.css('paddingTop'), 10);
      this.offsetBottom = this.$cut.offset().top - this.$sideBarAside.height() - this.padding;
    },

    scrollDocument: function(e){
      var scrollTop = this.$document.scrollTop();
      if (scrollTop > this.offset) {
        this.$sideBarBox.addClass('fixed');
        this.firstFixed = false;
        if(scrollTop < this.offsetBottom) {
          this.$sideBarAside.removeClass('bottom').addClass('fixed');
        }else{
          this.$sideBarAside.removeClass('fixed').addClass('bottom');
        }
      }else{
        this.$sideBarAside.removeClass('fixed bottom');
        this.$sideBarBox.removeClass('fixed');
        this.firstFixed = true;
      }
    },

    updateSource: function(e){
      e && e.preventDefault();

      var params = {
        section: $(e.currentTarget).data('slug'),
        interesting: $(e.currentTarget).data('interesting')
      }

      mps.publish('SourceStatic/update',[params]);
    },

    changeSource: function(params){
      //spinner
      this.$sourceSpinner.removeClass('start');

      if (params.section) {
        var posY = (this.mobile) ? this.$document.scrollTop() : this.$sideBarBox.offset().top - this.padding;
        this.$htmlbody.animate({ scrollTop: posY },0, _.bind(function(){
            this.changeHelper(params.section);
        },this));
        mps.publish('Interesting/update',[params.interesting]);
      }else{
        if (!this.mobile) {
          var section = this.$navItem.eq(0).data('slug');
          this.changeHelper(section);
        }
        var interesting = this.$navItem.eq(0).data('interesting');
        mps.publish('Interesting/update',[interesting]);
      }

    },

    changeHelper: function(section){
      this.$sideBarBox.addClass('active');
      this.$backBtn.addClass('active');
      //aside
      this.$navItem.removeClass('selected');
      $('.'+section).addClass('selected');

      //section
      this.$sourceArticle.removeClass('selected');
      $('#'+section).addClass('selected');

      if(this.mobile) {
        this.$sideBarBox.animate({ scrollTop: 0 },0);
        this.$headerH1.addClass('active');
      }

      setTimeout(_.bind(function(){
        this.calculateOffsets();
      },this),50);

      setTimeout(_.bind(function(){

        //htmlbody
        if(this.mobile) {
          this.$sideBarBox.addClass('animate');
          this.$htmlbody.addClass('active');
        }
      },this),500);
    },

    returnBack: function(e){
      e && e.preventDefault();

      this.$headerH1.removeClass('active');
      this.$sideBarBox.removeClass('active');
      this.$backBtn.removeClass('active');
      this.$navItem.removeClass('selected');

      this.$htmlbody.removeClass('active').animate({ scrollTop: this.$sideBarAside.offset().top },0);

    },

    scrollTo: function(e){
      e && e.preventDefault();
      this.$htmlbody.animate({ scrollTop: 0 },500);
    }

  });

  return SidebarNavView;

});
<commit_msg>fix accordion<commit_after>/**
 * The SidebarNavView view.
 */
define([
  'jquery',
  'backbone',
  'underscore',
  'mps'
], function($,Backbone, _,mps) {

  'use strict';

  var SidebarNavModel = Backbone.Model.extend({
    defaults: {
      offset: null,
      height: null
    }
  });

  var SidebarNavView = Backbone.View.extend({

    el: '#sidebarNavView',

    events: {
      'click .nav-item' : 'updateSource',
      'click .nav-title' : 'scrollTo',
      'click #back-btn' : 'returnBack',
      'click .source_header' : 'toggleSources',
      'click .source_dropdown_header' : 'toggleDropdown',
      'click .source_dropdown_menu a' : 'showSubContent'
    },

    initialize: function() {
      if (!this.$el.length) {
        return
      }
      //CACHE
      this.$window = $(window);
      this.$document = $(document);
      this.$htmlbody = $('html,body');
      this.$headerH1 = $('#headerView').find('h1');
      this.$backBtn = $('#back-btn');
      this.$navItem = this.$el.find('.nav-item');
      this.$sideBarAside = $('#sidebarAside');
      this.$sideBarBox = $('#sources-box');
      this.$sourceArticle = $('.source-article');
      this.$sourceHeader = $('.source_header');
      this.$sourceBody = $('.source_body');
      this.$cut = $('#cut');
      this.$sourceSpinner = $('#sources-spinner');

      //VARS
      this.padding = 40;
      this.first = true;
      this.mobile = (this.$window.width() > 850) ? false : true;

      //INIT
      this.setListeners();
    },

    setListeners: function(){
      this.calculateOffsets();
      this.scrollDocument();
      this.$document.on('scroll',_.bind(this.scrollDocument,this));
      this.$window.on('resize',_.bind(this.calculateOffsets,this));

      mps.subscribe('SourceStatic/change',_.bind(this.changeSource,this));
      mps.subscribe('SubItem/change',_.bind(this.calculateOffsets,this));
    },

    toggleDropdown: function(e){
      e && e.preventDefault();
      $(e.currentTarget).parents('.source_dropdown').find('.source_dropdown_menu').toggle(0);
    },

    showSubContent:function(e){
      e && e.preventDefault();
      $(e.currentTarget).parents('.source_dropdown').find('.source_dropdown_menu').hide(0);

      var text = $(e.currentTarget).text();
      var id = $(e.currentTarget).data('slug');
      $(e.currentTarget).parents('.source_dropdown').find('.source_dropdown_header').find('.overview_title').children('span').text(text);

      $('.source_dropdown_body').hide(0);
      $('#'+id).show(0);

      this.calculateOffsets();

    },

    toggleSources: function(e){
      this.$sourceBody.hide(0);

      if ($(e.currentTarget).hasClass('active')) {
        this.$sourceHeader.removeClass('active');
      } else {
        $(e.currentTarget).addClass('active');
        $(e.currentTarget).parent().children('.source_body').show(0);
      }

      this.$htmlbody.animate({ scrollTop: this.$sideBarBox.offset().top - this.padding },0);

      setTimeout(_.bind(function(){
        this.calculateOffsets();
      },this),50);
    },

    calculateOffsets: function(){
      this.$sideBarBox.css({'min-height': this.$sideBarAside.height() });
      this.offset = this.$el.offset().top + parseInt(this.$el.css('paddingTop'), 10);
      this.offsetBottom = this.$cut.offset().top - this.$sideBarAside.height() - this.padding;
    },

    scrollDocument: function(e){
      var scrollTop = this.$document.scrollTop();
      if (scrollTop > this.offset) {
        this.$sideBarBox.addClass('fixed');
        this.firstFixed = false;
        if(scrollTop < this.offsetBottom) {
          this.$sideBarAside.removeClass('bottom').addClass('fixed');
        }else{
          this.$sideBarAside.removeClass('fixed').addClass('bottom');
        }
      }else{
        this.$sideBarAside.removeClass('fixed bottom');
        this.$sideBarBox.removeClass('fixed');
        this.firstFixed = true;
      }
    },

    updateSource: function(e){
      e && e.preventDefault();

      var params = {
        section: $(e.currentTarget).data('slug'),
        interesting: $(e.currentTarget).data('interesting')
      }

      mps.publish('SourceStatic/update',[params]);
    },

    changeSource: function(params){
      //spinner
      this.$sourceSpinner.removeClass('start');

      if (params.section) {
        var posY = (this.mobile) ? this.$document.scrollTop() : this.$sideBarBox.offset().top - this.padding;
        this.$htmlbody.animate({ scrollTop: posY },0, _.bind(function(){
            this.changeHelper(params.section);
        },this));
        mps.publish('Interesting/update',[params.interesting]);
      }else{
        if (!this.mobile) {
          var section = this.$navItem.eq(0).data('slug');
          this.changeHelper(section);
        }
        var interesting = this.$navItem.eq(0).data('interesting');
        mps.publish('Interesting/update',[interesting]);
      }

    },

    changeHelper: function(section){
      this.$sideBarBox.addClass('active');
      this.$backBtn.addClass('active');
      //aside
      this.$navItem.removeClass('selected');
      $('.'+section).addClass('selected');

      //section
      this.$sourceArticle.removeClass('selected');
      $('#'+section).addClass('selected');

      if(this.mobile) {
        this.$sideBarBox.animate({ scrollTop: 0 },0);
        this.$headerH1.addClass('active');
      }

      setTimeout(_.bind(function(){
        this.calculateOffsets();
      },this),50);

      setTimeout(_.bind(function(){

        //htmlbody
        if(this.mobile) {
          this.$sideBarBox.addClass('animate');
          this.$htmlbody.addClass('active');
        }
      },this),500);
    },

    returnBack: function(e){
      e && e.preventDefault();

      this.$headerH1.removeClass('active');
      this.$sideBarBox.removeClass('active');
      this.$backBtn.removeClass('active');
      this.$navItem.removeClass('selected');

      this.$htmlbody.removeClass('active').animate({ scrollTop: this.$sideBarAside.offset().top },0);

    },

    scrollTo: function(e){
      e && e.preventDefault();
      this.$htmlbody.animate({ scrollTop: 0 },500);
    }

  });

  return SidebarNavView;

});
<|endoftext|>