<commit_before>/**
 * Utility function to generate stories for widgets.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import { storiesOf, Story } from '@storybook/react';

/**
 * Internal dependencies
 */
import { CORE_USER } from '../../assets/js/googlesitekit/datastore/user/constants';
import {
	createTestRegistry,
	WithTestRegistry,
	provideModules,
	provideSiteInfo,
} from '../../tests/js/utils';
import { getWidgetComponentProps } from '../../assets/js/googlesitekit/widgets/util';

/**
 * Generates stories for a report based widget using provided data.
 *
 * @since 1.16.0
 * @private
 *
 * @param {Object}      args                              Widget arguments.
 * @param {Array}       args.moduleSlugs                  List of modules to activate.
 * @param {string}      args.datastore                    Module datastore name.
 * @param {string}      args.group                        Stories group name.
 * @param {Array}       args.data                         Widget data.
 * @param {Object}      args.options                      Arguments for report requests.
 * @param {WPComponent} args.Component                    Widget component.
 * @param {string}      [args.referenceDate]              Reference date string to use, if not today.
 * @param {boolean}     [args.wrapWidget]                 Whether to wrap in default <Widget> component. Default true.
 * @param {Array}       [args.additionalVariants]         Optional. Additional story variants.
 * @param {Array}       [args.additionalVariantCallbacks] Optional. Additional custom callbacks to be run for each of the variants.
 * @param {Function}    [args.setup]                      Optional. Setup function to be run for all Stories being generated.
 * @param {Function}    [args.zeroing]                    Optional. Utility function to be run for Zero Data story to set report values to zero.
 * @param {number}      [args.padding]                    Optional. Can be used to alter padding around component (nomrally to set to 0).
 * @return {Story} Generated story.
 */
export function generateReportBasedWidgetStories( args ) {
	const {
		moduleSlugs,
		datastore,
		group,
		data,
		options,
		Component,
		referenceDate,
		wrapWidget = true,
		additionalVariants = {},
		additionalVariantCallbacks = {},
		setup = () => {},
		zeroing,
		padding,
	} = args;

	const stories = storiesOf( group, module );

	const withRegistry = ( variantName ) => ( StoryComponent ) => {
		const registry = createTestRegistry();
		// Activate the module.
		provideModules(
			registry,
			moduleSlugs.map( ( module ) => {
				return {
					slug: module,
					active: true,
					connected: true,
				};
			} )
		);

		let currentEntityURL = null;
		if ( Array.isArray( options ) && options[ 0 ].url ) {
			currentEntityURL = options[ 0 ].url;
		} else if ( options.url ) {
			currentEntityURL = options.url;
		}

		// Set some site information.
		provideSiteInfo( registry, {
			currentEntityURL,
		} );

		if ( referenceDate ) {
			registry.dispatch( CORE_USER ).setReferenceDate( referenceDate );
		}

		// Call the optional setup function.
		setup( registry, variantName );

		return <StoryComponent registry={ registry } />;
	};

	if ( Array.isArray( options ) ) {
		// 	If options is an array, so must data.
		if ( ! Array.isArray( data ) ) {
			throw new Error( 'options is an array, data must be one too' );
		}
		// Both must have the same length.
		if ( options.length !== data.length ) {
			throw new Error(
				'options and data must have the same number of items'
			);
		}
	}

	const {
		Loaded: additionalLoadedCallback,
		Loading: additionalLoadingCallback,
		DataUnavailable: additionalDataUnavailableCallback,
		ZeroData: additionalZeroDataCallback,
		Error: additionalErrorCallback,
	} = additionalVariantCallbacks;

	// Existing default variants.
	const defaultVariants = {
		Loaded( { dispatch } ) {
			if ( Array.isArray( options ) ) {
				options.forEach( ( option, index ) => {
					dispatch( datastore ).receiveGetReport( data[ index ], {
						options: option,
					} );
				} );
			} else {
				dispatch( datastore ).receiveGetReport( data, { options } );
			}

			// Run additional callback if it exists.
			if ( additionalLoadedCallback ) {
				additionalLoadedCallback( dispatch, data, options );
			}
		},
		Loading( { dispatch } ) {
			if ( Array.isArray( options ) ) {
				options.forEach( ( option, index ) => {
					dispatch( datastore ).receiveGetReport( data[ index ], {
						options: option,
					} );
					dispatch( datastore ).startResolution( 'getReport', [
						option,
					] );
				} );
			} else {
				dispatch( datastore ).receiveGetReport( data, { options } );
				dispatch( datastore ).startResolution( 'getReport', [
					options,
				] );
			}

			// Run additional callback if it exists.
			if ( additionalLoadingCallback ) {
				additionalLoadingCallback( dispatch, data, options );
			}
		},
		DataUnavailable( { dispatch } ) {
			if ( Array.isArray( options ) ) {
				options.forEach( ( option, index ) => {
					const returnType = Array.isArray( data[ index ] ) ? [] : {};
					dispatch( datastore ).receiveGetReport( returnType, {
						options: option,
					} );
				} );
			} else {
				dispatch( datastore ).receiveGetReport( [], { options } );
			}

			// Run additional callback if it exists.
			if ( additionalDataUnavailableCallback ) {
				additionalDataUnavailableCallback( dispatch, data, options );
			}
		},
		ZeroData:
			typeof zeroing === 'function'
				? ( { dispatch } ) => {
						if ( Array.isArray( options ) ) {
							options.forEach( ( option, index ) => {
								dispatch( datastore ).receiveGetReport(
									zeroing( data[ index ], option ),
									{
										options: option,
									}
								);
							} );
						} else {
							dispatch( datastore ).receiveGetReport(
								zeroing( data, options ),
								{
									options,
								}
							);
						}

						// Run additional callback if it exists.
						if ( additionalZeroDataCallback ) {
							additionalZeroDataCallback(
								dispatch,
								data,
								options
							);
						}
				  }
				: undefined,
		Error( { dispatch } ) {
			const error = {
				code: 'missing_required_param',
				message: 'Request parameter is empty: metrics.',
				data: {},
			};

			if ( Array.isArray( options ) ) {
				options.forEach( ( option, index ) => {
					if ( index === 0 ) {
						dispatch( datastore ).receiveError(
							error,
							'getReport',
							[ option ]
						);
						dispatch( datastore ).finishResolution( 'getReport', [
							option,
						] );
					} else {
						dispatch( datastore ).receiveGetReport( data[ index ], {
							options: option,
						} );
					}
				} );
			} else {
				dispatch( datastore ).receiveError( error, 'getReport', [
					options,
				] );
				dispatch( datastore ).finishResolution( 'getReport', [
					options,
				] );
			}

			// Run additional callback if it exists.
			if ( additionalErrorCallback ) {
				additionalErrorCallback( dispatch, data, options );
			}
		},
	};

	// Custom variants.
	const customVariants = {};
	Object.keys( additionalVariants ).forEach( ( name ) => {
		const {
			data: variantData,
			options: variantOptions,
		} = additionalVariants[ name ];

		if ( Array.isArray( variantOptions ) ) {
			// 	If variantOptions is an array, so must variantData.
			if ( ! Array.isArray( variantData ) ) {
				throw new Error(
					`options for variant "${ name }" is an array, data must be one too`
				);
			}
			// Both must have the same length.
			if ( variantOptions.length !== variantData.length ) {
				throw new Error(
					`options and data for variant "${ name }" must have the same number of items`
				);
			}
		}

		customVariants[ name ] = ( { dispatch } ) => {
			if ( Array.isArray( variantOptions ) ) {
				variantOptions.forEach( ( variantOption, index ) => {
					dispatch( datastore ).receiveGetReport(
						variantData[ index ],
						{ options: variantOption }
					);
				} );
			} else {
				dispatch( datastore ).receiveGetReport( variantData, {
					options: variantOptions,
				} );
			}

			// Run additional callback if it exists.
			if ( additionalVariantCallbacks[ name ] ) {
				additionalVariantCallbacks[ name ]( dispatch, data, options );
			}
		};
	} );

	const variants = {
		...defaultVariants,
		...customVariants,
	};

	let widgetElement;

	const slug = moduleSlugs
		.map( ( mapSlug ) => `${ mapSlug }-widget` )
		.join( ' ' );
	const widgetComponentProps = getWidgetComponentProps( slug );

	if ( wrapWidget ) {
		const { Widget } = widgetComponentProps;
		widgetElement = (
			<Widget>
				<Component { ...widgetComponentProps } />
			</Widget>
		);
	} else {
		widgetElement = <Component { ...widgetComponentProps } />;
	}

	Object.keys( variants ).forEach( ( variant ) => {
		if ( ! variants[ variant ] ) {
			return;
		}

		stories.add(
			variant.replace( /([a-z])([A-Z])/, '$1 $2' ),
			( _, { registry } ) => (
				<WithTestRegistry
					registry={ registry }
					callback={ variants[ variant ] }
				>
					{ widgetElement }
				</WithTestRegistry>
			),
			{
				decorators: [ withRegistry( variant ) ],
				padding,
			}
		);
	} );

	return stories;
}

/**
 * Creates and returns a new report data generator using provided factory function.
 *
 * @since 1.28.0
 *
 * @param {Function} factory The factory function.
 * @return {Function} The report data generator.
 */
export function makeReportDataGenerator( factory ) {
	return ( options ) => {
		const results = { options };

		if ( Array.isArray( options ) ) {
			results.data = [];
			for ( let i = 0; i < options.length; i++ ) {
				results.data.push( factory( options[ i ] ) );
			}
		} else {
			results.data = factory( options );
		}

		return results;
	};
}
<commit_msg>Update widget report generator to allow feature flags to be enabled.<commit_after>/**
 * Utility function to generate stories for widgets.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import { storiesOf, Story } from '@storybook/react';

/**
 * Internal dependencies
 */
import { CORE_USER } from '../../assets/js/googlesitekit/datastore/user/constants';
import {
	createTestRegistry,
	WithTestRegistry,
	provideModules,
	provideSiteInfo,
} from '../../tests/js/utils';
import { getWidgetComponentProps } from '../../assets/js/googlesitekit/widgets/util';

/**
 * Generates stories for a report based widget using provided data.
 *
 * @since 1.16.0
 * @private
 *
 * @param {Object}      args                              Widget arguments.
 * @param {Array}       args.moduleSlugs                  List of modules to activate.
 * @param {string}      args.datastore                    Module datastore name.
 * @param {string}      args.group                        Stories group name.
 * @param {Array}       args.data                         Widget data.
 * @param {Object}      args.options                      Arguments for report requests.
 * @param {WPComponent} args.Component                    Widget component.
 * @param {string}      [args.referenceDate]              Reference date string to use, if not today.
 * @param {boolean}     [args.wrapWidget]                 Whether to wrap in default <Widget> component. Default true.
 * @param {Array}       [args.additionalVariants]         Optional. Additional story variants.
 * @param {Array}       [args.additionalVariantCallbacks] Optional. Additional custom callbacks to be run for each of the variants.
 * @param {Function}    [args.setup]                      Optional. Setup function to be run for all Stories being generated.
 * @param {Function}    [args.zeroing]                    Optional. Utility function to be run for Zero Data story to set report values to zero.
 * @param {number}      [args.padding]                    Optional. Can be used to alter padding around component (nomrally to set to 0).
 * @return {Story} Generated story.
 */
export function generateReportBasedWidgetStories( args ) {
	const {
		moduleSlugs,
		datastore,
		group,
		data,
		options,
		Component,
		referenceDate,
		wrapWidget = true,
		additionalVariants = {},
		additionalVariantCallbacks = {},
		setup = () => {},
		zeroing,
		padding,
	} = args;

	const stories = storiesOf( group, module );

	const withRegistry = ( variantName ) => ( StoryComponent ) => {
		const registry = createTestRegistry();
		// Activate the module.
		provideModules(
			registry,
			moduleSlugs.map( ( module ) => {
				return {
					slug: module,
					active: true,
					connected: true,
				};
			} )
		);

		let currentEntityURL = null;
		if ( Array.isArray( options ) && options[ 0 ].url ) {
			currentEntityURL = options[ 0 ].url;
		} else if ( options.url ) {
			currentEntityURL = options.url;
		}

		// Set some site information.
		provideSiteInfo( registry, {
			currentEntityURL,
		} );

		if ( referenceDate ) {
			registry.dispatch( CORE_USER ).setReferenceDate( referenceDate );
		}

		// Call the optional setup function.
		setup( registry, variantName );

		return <StoryComponent registry={ registry } />;
	};

	if ( Array.isArray( options ) ) {
		// 	If options is an array, so must data.
		if ( ! Array.isArray( data ) ) {
			throw new Error( 'options is an array, data must be one too' );
		}
		// Both must have the same length.
		if ( options.length !== data.length ) {
			throw new Error(
				'options and data must have the same number of items'
			);
		}
	}

	const {
		Loaded: additionalLoadedCallback,
		Loading: additionalLoadingCallback,
		DataUnavailable: additionalDataUnavailableCallback,
		ZeroData: additionalZeroDataCallback,
		Error: additionalErrorCallback,
	} = additionalVariantCallbacks;

	// Existing default variants.
	const defaultVariants = {
		Loaded: {
			callback( { dispatch } ) {
				if ( Array.isArray( options ) ) {
					options.forEach( ( option, index ) => {
						dispatch( datastore ).receiveGetReport( data[ index ], {
							options: option,
						} );
					} );
				} else {
					dispatch( datastore ).receiveGetReport( data, { options } );
				}

				// Run additional callback if it exists.
				if ( additionalLoadedCallback ) {
					additionalLoadedCallback( dispatch, data, options );
				}
			},
		},
		Loading: {
			callback( { dispatch } ) {
				if ( Array.isArray( options ) ) {
					options.forEach( ( option, index ) => {
						dispatch( datastore ).receiveGetReport( data[ index ], {
							options: option,
						} );
						dispatch( datastore ).startResolution( 'getReport', [
							option,
						] );
					} );
				} else {
					dispatch( datastore ).receiveGetReport( data, { options } );
					dispatch( datastore ).startResolution( 'getReport', [
						options,
					] );
				}

				// Run additional callback if it exists.
				if ( additionalLoadingCallback ) {
					additionalLoadingCallback( dispatch, data, options );
				}
			},
		},
		DataUnavailable: {
			callback( { dispatch } ) {
				if ( Array.isArray( options ) ) {
					options.forEach( ( option, index ) => {
						const returnType = Array.isArray( data[ index ] )
							? []
							: {};
						dispatch( datastore ).receiveGetReport( returnType, {
							options: option,
						} );
					} );
				} else {
					dispatch( datastore ).receiveGetReport( [], { options } );
				}

				// Run additional callback if it exists.
				if ( additionalDataUnavailableCallback ) {
					additionalDataUnavailableCallback(
						dispatch,
						data,
						options
					);
				}
			},
		},
		ZeroData:
			typeof zeroing === 'function'
				? {
						callback: ( { dispatch } ) => {
							if ( Array.isArray( options ) ) {
								options.forEach( ( option, index ) => {
									dispatch( datastore ).receiveGetReport(
										zeroing( data[ index ], option ),
										{
											options: option,
										}
									);
								} );
							} else {
								dispatch( datastore ).receiveGetReport(
									zeroing( data, options ),
									{
										options,
									}
								);
							}

							// Run additional callback if it exists.
							if ( additionalZeroDataCallback ) {
								additionalZeroDataCallback(
									dispatch,
									data,
									options
								);
							}
						},
				  }
				: undefined,
		Error: {
			callback( { dispatch } ) {
				const error = {
					code: 'missing_required_param',
					message: 'Request parameter is empty: metrics.',
					data: {},
				};

				if ( Array.isArray( options ) ) {
					options.forEach( ( option, index ) => {
						if ( index === 0 ) {
							dispatch( datastore ).receiveError(
								error,
								'getReport',
								[ option ]
							);
							dispatch( datastore ).finishResolution(
								'getReport',
								[ option ]
							);
						} else {
							dispatch( datastore ).receiveGetReport(
								data[ index ],
								{
									options: option,
								}
							);
						}
					} );
				} else {
					dispatch( datastore ).receiveError( error, 'getReport', [
						options,
					] );
					dispatch( datastore ).finishResolution( 'getReport', [
						options,
					] );
				}

				// Run additional callback if it exists.
				if ( additionalErrorCallback ) {
					additionalErrorCallback( dispatch, data, options );
				}
			},
		},
	};

	// Custom variants.
	const customVariants = {};
	Object.keys( additionalVariants ).forEach( ( name ) => {
		const {
			data: variantData,
			options: variantOptions,
			features: variantFeatures,
		} = additionalVariants[ name ];

		if ( Array.isArray( variantOptions ) ) {
			// 	If variantOptions is an array, so must variantData.
			if ( ! Array.isArray( variantData ) ) {
				throw new Error(
					`options for variant "${ name }" is an array, data must be one too`
				);
			}
			// Both must have the same length.
			if ( variantOptions.length !== variantData.length ) {
				throw new Error(
					`options and data for variant "${ name }" must have the same number of items`
				);
			}
		}

		customVariants[ name ] = {
			callback: ( { dispatch } ) => {
				if ( Array.isArray( variantOptions ) ) {
					variantOptions.forEach( ( variantOption, index ) => {
						dispatch( datastore ).receiveGetReport(
							variantData[ index ],
							{ options: variantOption }
						);
					} );
				} else {
					dispatch( datastore ).receiveGetReport( variantData, {
						options: variantOptions,
					} );
				}

				// Run additional callback if it exists.
				if ( additionalVariantCallbacks[ name ] ) {
					additionalVariantCallbacks[ name ](
						dispatch,
						data,
						options
					);
				}
			},
			features: variantFeatures,
		};
	} );

	const variants = {
		...defaultVariants,
		...customVariants,
	};

	let widgetElement;

	const slug = moduleSlugs
		.map( ( mapSlug ) => `${ mapSlug }-widget` )
		.join( ' ' );
	const widgetComponentProps = getWidgetComponentProps( slug );

	if ( wrapWidget ) {
		const { Widget } = widgetComponentProps;
		widgetElement = (
			<Widget>
				<Component { ...widgetComponentProps } />
			</Widget>
		);
	} else {
		widgetElement = <Component { ...widgetComponentProps } />;
	}

	Object.keys( variants ).forEach( ( variant ) => {
		if ( ! variants[ variant ] ) {
			return;
		}

		stories.add(
			variant.replace( /([a-z])([A-Z])/, '$1 $2' ),
			( _, { registry } ) => (
				<WithTestRegistry
					registry={ registry }
					callback={ variants[ variant ].callback }
					features={ variants[ variant ].features || [] }
				>
					{ widgetElement }
				</WithTestRegistry>
			),
			{
				decorators: [ withRegistry( variant ) ],
				padding,
			}
		);
	} );

	return stories;
}

/**
 * Creates and returns a new report data generator using provided factory function.
 *
 * @since 1.28.0
 *
 * @param {Function} factory The factory function.
 * @return {Function} The report data generator.
 */
export function makeReportDataGenerator( factory ) {
	return ( options ) => {
		const results = { options };

		if ( Array.isArray( options ) ) {
			results.data = [];
			for ( let i = 0; i < options.length; i++ ) {
				results.data.push( factory( options[ i ] ) );
			}
		} else {
			results.data = factory( options );
		}

		return results;
	};
}
<|endoftext|>


<commit_before>#!/usr/local/bin/node

var fileNameOut = 'tgn.json';
var source = 'tgn';

var grex = require('grex'),
    argv = require('optimist')
      .usage('Transforms TGN data set into GraphSON format.\nUsage: $0')
      .demand('f')
      .alias('f', 'file')
      .describe('f', 'Load a file')
      .argv
    fs = require('fs'),
    async = require('async'),
    parse = require('csv-parse');

// Transforms TGN object types to HG ontology types
// hg:Place, hg:Polder, hg:Province or hg:Waterway
var objectTypeMap = {
	"inhabited places": "place",
	"national capitals": "place",
	"provincial capitals": "place",
	"cities": "place",
	"island groups": "place",
	"islands (landforms)": "place",
	"villages": "place",
	"towns": "place",
	"regional capitals": "place",
	"capitals (seats of government)": "place",

	"polders": "polder",

	"canals (waterways)": "waterway",
	"marine channels": "waterway",
	"ponds (water)": "waterway",
	"navigation channels": "waterway",
	"lakes (bodies of water)": "waterway",
	"stream channels": "waterway",
	"inlets": "waterway",
	"rivers": "waterway",
	"tidal watercourses": "waterway",
	"seas": "waterway",
	"sections of watercourses": "waterway",
	"distributaries (streams)": "waterway",
	"bays (bodies of water)": "waterway",
	"channels (bodies of water)": "waterway",
	"channels (water bodies)": "waterway",

	"provinces": "province"
};

function containsObject(obj, list) {
    var i;
    for (i = 0; i < list.length; i++) {
        if (list[i] === obj) {
            return true;
        }
    }

    return false;
}

// TGN object types not used in our ontology. TODO Subject to change.
var notUsed = [
	"parks (recreation areas)",
	"docks",
	"ports (settlements)",
	"locales (settlements)",
	"coves (bodies of water)",
	"nature reserves",
	"heaths (landforms)",
	"estates (agricultural)",
	"castles (fortifications)",
	"railroad stations",
	"marshes",
	"forests (cultural landscapes)",
	"points (landforms)",
	"farms",
	"general regions",
	"streams",
	"ruins",
	"first level subdivisions (political entities)",
	"second level subdivisions (political entities)",
	"area (measurement)",
	"bridges (built works)",
	"reservoirs (water distribution structures)",
	"tunnels",
	"dikes",
	"churches (buildings)",
	"landhouses",
	"dunes",
	"straits",
	"locks (hydraulic structures)",
	"dams (hydraulic structures)",
	"flats (landforms)",
	"shoals (landforms)",
	"hills",
	"archaeological sites",
	"forts",
	"lighthouses",
	"beaches",
	"schools (buildings)",
	"banks (landforms)",
	"breakwaters",
	"capes (landforms)",
	"trade centers",
	"museums (buildings)",
	"textile centers",
	"bars (landforms)",
	"concentration camp sites",
	"royal residences",
	"megalithic sites",
	"uplands",
	"anabranches",
	"national parks",
	"palaces",
	"mountains",
	"episcopal sees",
	"battlefields",
	"resorts",
	"oil fields",
	"estuaries",
	"fortified settlements",
	"seaports",
	"naval bases",
	"parts of inhabited places",
	"suburbs",
	"historic sites"
];

// Hard-coded, taken from source file entries for the provinces
var provinceURIs = {
	"NH": "7006951",
	"NB": "7003624",
	"Ut": "7003627",
	"Ze": "7003635",
	"Fl": "7003615",
	"Gr": "7003613",
	"Ge": "7003619",
	"Fr": "7003616",
	"Li": "7003622",
	"Ov": "7003626",
	"Dr": "7003614",
	"ZH": "7003632"
};

var verticesHeader = '{ "graph": { "mode": "NORMAL", "vertices": ',
		edgesHeader = ', "edges": ',
		footer = '} }';

var usedURIs = [];

// VERTICES
function parseVertices(callback) {

	fs.writeFileSync(fileNameOut, verticesHeader);

	parse(fs.readFileSync(argv.file, {encoding: 'utf8'}), {delimiter: ','}, function(err, data) {

		console.log("Parsing vertices...");

		var vertices = [];

		for (var i=1; i<data.length; i++) { // Skip CSV header -- contents hardcoded
			var obj = data[i];
			var objType = obj[7];

			var splitURI = obj[1].split("/");
			var uri = splitURI[splitURI.length - 1];

			if (objectTypeMap.hasOwnProperty(objType)) {
				if (!containsObject(uri, usedURIs)) {

					var vertex = {};
					var objType = objectTypeMap[obj[7]];

					vertex["_id"] = source + "/" + uri;
					vertex["uri"] = source + "/" + uri;

					vertex["name"] = obj[2];
					vertex["source"] = source;
					vertex["type"] = "hg:" + objType.charAt(0).toUpperCase() + objType.slice(1);
					vertex["geometry"] = {"type": "Point", "coordinates": [parseFloat(obj[5]), parseFloat(obj[4])]};
					vertex["startDate"] = "";
					vertex["endDate"] = "";

					vertices.push(vertex);
					usedURIs.push(uri);
				}

			} else if (!containsObject(obj[7], notUsed)) {
				console.log("Property " + obj[7] + " not part of object types map. Skipping...");
			}
		}

		fs.appendFileSync(fileNameOut, JSON.stringify(vertices, null, 4));
		callback(null, true);

	});
}

// EDGES
function parseEdges(callback) {
	fs.appendFileSync(fileNameOut, edgesHeader);

	parse(fs.readFileSync(argv.file, {encoding: 'utf8'}), {delimiter: ','}, function(err, data){
		console.log("Parsing province edges...");

		var edges = [];
		var edgeCounter = 0;

		for (var i=1; i<data.length; i++) { // Skip CSV header -- contents hardcoded
			var obj = data[i];
			var objType = obj[7];

			var splitURI = obj[1].split("/");
			var uri = splitURI[splitURI.length - 1];

			if (containsObject(uri, usedURIs)) {
				if (provinceURIs.hasOwnProperty(obj[3])) {

					var edge = {};
					var objType = objectTypeMap[obj[7]];

					edge["_id"] = source + "/e" + ++edgeCounter;
					edge["_outV"] = source + "/" + uri;
					edge["_inV"] = source + "/" + provinceURIs[obj[3]];
					edge["source"] = source;
					edge["_label"] = "hg:liesIn";
					edge["startDate"] = "";
					edge["endDate"] = "";

					edges.push(edge);
				}
			}
		}

		fs.appendFileSync(fileNameOut, JSON.stringify(edges, null, 4));

		callback(null, true);
	});

}

function doneMsg(callback) {
	fs.appendFileSync(fileNameOut, footer);
	console.log("Done!");
	callback(null, true);
}

async.series([
    parseVertices,
    parseEdges,
		doneMsg
  ]
);<commit_msg>Input directory hg:sameAs Output directory<commit_after>#!/usr/local/bin/node

// Importer script for Getty Thesaurus of Geographic Names (TGN) data set
var fileNameOut = 'tgn.graphson.json';
var source = 'tgn';

var grex = require('grex'),
    argv = require('optimist')
      .usage('Transforms TGN data set into GraphSON format.\nUsage: $0')
      .demand('f')
      .alias('f', 'file')
      .describe('f', 'Load a file')
      .argv
    fs = require('fs'),
    async = require('async'),
    parse = require('csv-parse'),
		path = require('path');

// Transforms TGN object types to HG ontology types
// hg:Place, hg:Polder, hg:Province or hg:Waterway
var objectTypeMap = {
	"inhabited places": "place",
	"national capitals": "place",
	"provincial capitals": "place",
	"cities": "place",
	"island groups": "place",
	"islands (landforms)": "place",
	"villages": "place",
	"towns": "place",
	"regional capitals": "place",
	"capitals (seats of government)": "place",

	"polders": "polder",

	"canals (waterways)": "waterway",
	"marine channels": "waterway",
	"ponds (water)": "waterway",
	"navigation channels": "waterway",
	"lakes (bodies of water)": "waterway",
	"stream channels": "waterway",
	"inlets": "waterway",
	"rivers": "waterway",
	"tidal watercourses": "waterway",
	"seas": "waterway",
	"sections of watercourses": "waterway",
	"distributaries (streams)": "waterway",
	"bays (bodies of water)": "waterway",
	"channels (bodies of water)": "waterway",
	"channels (water bodies)": "waterway",

	"provinces": "province"
};

function containsObject(obj, list) {
    var i;
    for (i = 0; i < list.length; i++) {
        if (list[i] === obj) {
            return true;
        }
    }
    return false;
}

// TGN object types not used in our ontology. TODO Subject to change.
var notUsed = [
	"parks (recreation areas)",
	"docks",
	"ports (settlements)",
	"locales (settlements)",
	"coves (bodies of water)",
	"nature reserves",
	"heaths (landforms)",
	"estates (agricultural)",
	"castles (fortifications)",
	"railroad stations",
	"marshes",
	"forests (cultural landscapes)",
	"points (landforms)",
	"farms",
	"general regions",
	"streams",
	"ruins",
	"first level subdivisions (political entities)",
	"second level subdivisions (political entities)",
	"area (measurement)",
	"bridges (built works)",
	"reservoirs (water distribution structures)",
	"tunnels",
	"dikes",
	"churches (buildings)",
	"landhouses",
	"dunes",
	"straits",
	"locks (hydraulic structures)",
	"dams (hydraulic structures)",
	"flats (landforms)",
	"shoals (landforms)",
	"hills",
	"archaeological sites",
	"forts",
	"lighthouses",
	"beaches",
	"schools (buildings)",
	"banks (landforms)",
	"breakwaters",
	"capes (landforms)",
	"trade centers",
	"museums (buildings)",
	"textile centers",
	"bars (landforms)",
	"concentration camp sites",
	"royal residences",
	"megalithic sites",
	"uplands",
	"anabranches",
	"national parks",
	"palaces",
	"mountains",
	"episcopal sees",
	"battlefields",
	"resorts",
	"oil fields",
	"estuaries",
	"fortified settlements",
	"seaports",
	"naval bases",
	"parts of inhabited places",
	"suburbs",
	"historic sites"
];

// Hard-coded, taken from source file entries for the provinces
var provinceURIs = {
	"NH": "7006951",
	"NB": "7003624",
	"Ut": "7003627",
	"Ze": "7003635",
	"Fl": "7003615",
	"Gr": "7003613",
	"Ge": "7003619",
	"Fr": "7003616",
	"Li": "7003622",
	"Ov": "7003626",
	"Dr": "7003614",
	"ZH": "7003632"
};

var verticesHeader = '{ "graph": { "mode": "NORMAL", "vertices": ',
		edgesHeader = ', "edges": ',
		footer = '} }';

var usedURIs = [];
var fileOut = path.join(path.dirname(path.resolve(argv.file)), fileNameOut);

// VERTICES
function parseVertices(callback) {

	fs.writeFileSync(fileOut, verticesHeader);

	parse(fs.readFileSync(argv.file, {encoding: 'utf8'}), {delimiter: ','}, function(err, data) {

		console.log("Parsing vertices...");

		var vertices = [];

		for (var i=1; i<data.length; i++) { // Skip CSV header -- contents hardcoded
			var obj = data[i];
			var objType = obj[7];

			var splitURI = obj[1].split("/");
			var uri = splitURI[splitURI.length - 1];

			if (objectTypeMap.hasOwnProperty(objType)) {
				if (!containsObject(uri, usedURIs)) {

					var vertex = {};
					var objType = objectTypeMap[obj[7]];

					vertex["_id"] = source + "/" + uri;
					vertex["uri"] = source + "/" + uri;

					vertex["name"] = obj[2];
					vertex["source"] = source;
					vertex["type"] = "hg:" + objType.charAt(0).toUpperCase() + objType.slice(1);
					vertex["geometry"] = {"type": "Point", "coordinates": [parseFloat(obj[5]), parseFloat(obj[4])]};
					vertex["startDate"] = "";
					vertex["endDate"] = "";

					vertices.push(vertex);
					usedURIs.push(uri);
				}

			} else if (!containsObject(obj[7], notUsed)) {
				console.log("Property " + obj[7] + " not part of object types map. Skipping...");
			}
		}

		fs.appendFileSync(fileOut, JSON.stringify(vertices, null, 4));
		callback(null, true);

	});
}

// EDGES
function parseEdges(callback) {
	fs.appendFileSync(fileOut, edgesHeader);

	parse(fs.readFileSync(argv.file, {encoding: 'utf8'}), {delimiter: ','}, function(err, data){
		console.log("Parsing province edges...");

		var edges = [];
		var edgeCounter = 0;

		for (var i=1; i<data.length; i++) { // Skip CSV header -- contents hardcoded
			var obj = data[i];
			var objType = obj[7];

			var splitURI = obj[1].split("/");
			var uri = splitURI[splitURI.length - 1];

			if (containsObject(uri, usedURIs)) {
				if (provinceURIs.hasOwnProperty(obj[3])) {

					var edge = {};
					var objType = objectTypeMap[obj[7]];

					edge["_id"] = source + "/e" + ++edgeCounter;
					edge["_outV"] = source + "/" + uri;
					edge["_inV"] = source + "/" + provinceURIs[obj[3]];
					edge["source"] = source;
					edge["_label"] = "hg:liesIn";
					edge["startDate"] = "";
					edge["endDate"] = "";

					edges.push(edge);
				}
			}
		}

		fs.appendFileSync(fileOut, JSON.stringify(edges, null, 4));
		callback(null, true);
	});

}

function doneMsg(callback) {
	fs.appendFileSync(fileOut, footer);
	console.log("Done!");
	callback(null, true);
}

async.series([
    parseVertices,
    parseEdges,
		doneMsg
  ]
);<|endoftext|>


<commit_before>import React from 'react'
import { View, TouchableHighlight, StyleSheet, Animated,  Easing } from 'react-native'
import { connect } from 'react-redux'
import DefaultText from '../DefaultText'
import marginOffset from '../../util/marginOffset'
import colors from '../../util/colors'
import merge from '../../util/merge'
import commonStyle from './../style.js'
import { bindActionCreators } from 'redux'
import * as actions from '../../store/reducer/login'
import Login from './Login'
import LoginOverlay from './LoginOverlay'
import StatusMessage from '../StatusMessage'

const style = {
  container: {
    flex: 1,
    alignItems: 'center',
    paddingLeft: 30,
    paddingRight: 30,
    paddingBottom: 24
  },
  bottomContainer : {
    alignItems: 'center',
    alignSelf: 'stretch'
  },
  background: {
    ...StyleSheet.absoluteFillObject
  },
  welcome: {
    container: {
      flex: 1,
      justifyContent: 'flex-start',
      alignItems: 'center',
      marginTop: marginOffset(254),
      marginBottom: 10,
    },
  },
  loginButton: {
    container: {
      backgroundColor: colors.white,
      borderRadius: 10,
      alignSelf: 'stretch',
      height: 48,
      alignItems: 'center',
      justifyContent: 'center',
      marginBottom: 10
    },
    text: {
      color: colors.bristolBlue2,
      fontSize: 22,
      fontFamily: commonStyle.museo500
    }
  },
  skipButton: {
    container: {
      backgroundColor: colors.transparent,
      borderRadius: 10,
      borderWidth: 1.5,
      borderColor: colors.white,
      alignSelf: 'stretch',
      height: 48,
      alignItems: 'center',
      justifyContent: 'center',
      marginBottom: 18
    },
    text: {
      color: colors.white,
      fontSize: 18,
      fontFamily: commonStyle.museo500
    }
  }
}

class Splash  extends React.Component {
  constructor() {
    super()
    this.state = {
      backgroundOffset: new Animated.Value(0)
    }
  }

  componentDidMount() {
    this.animateBackground()
  }

  animateBackground() {
    const animateBackgroundTo = toValue =>
      Animated.timing(this.state.backgroundOffset, {
        toValue,
        duration: 240000,
        easing: Easing.linear
      })

    // create a looping animation for the background
    Animated.sequence([
      // background image is 7900px wide. Assuming the widest screem is <1080 (6 plus)
      animateBackgroundTo(-(7900 - 1080)),
      animateBackgroundTo(0)
    ]).start(event => {
      if (event.finished) {
        this.animateBackground()
      }
    })
  }

  render()  {
    return (
      <View style={style.container}>
        <Animated.Image
          style={merge(style.background, {
            transform: [
              {translateX: this.state.backgroundOffset}
            ]
          })}
          source={require('../background.jpg')}/>
        <View style={style.welcome.container}>
          { this.props.renderWelcomeMessage(this.props) }
        </View>
        { this.props.loginFormOpen
          ? undefined
          : <View style={style.bottomContainer}>
              <TouchableHighlight
                  style={style.loginButton.container}
                  onPress={() => this.props.openLoginForm(true)}
                  underlayColor={colors.offWhite}>
                <DefaultText style={style.loginButton.text}>{this.props.loginButtonText}</DefaultText>
              </TouchableHighlight>
              <TouchableHighlight
                  style={style.skipButton.container}
                  onPress={this.props.logout}
                  underlayColor={colors.bristolBlue3}>
                <DefaultText style={style.skipButton.text}>{this.props.logoutButtonText}</DefaultText>
              </TouchableHighlight>
              { this.props.renderInfoText(this.props) }
            </View>
        }
        <LoginOverlay/>
        <Login hideUsernameInput={this.props.hideUsernameInput}/>
        <StatusMessage/>
      </View>
    )
  }
}

const mapStateToProps = (state) => ({
    ...state.login
})

const mapDispatchToProps = (dispatch) =>
  bindActionCreators(actions, dispatch)

export default connect(mapStateToProps, mapDispatchToProps)(Splash)
<commit_msg>Splash for iPhone 7 Plus<commit_after>import React from 'react'
import { View, TouchableHighlight, StyleSheet, Animated,  Easing, Dimensions } from 'react-native'
import { connect } from 'react-redux'
import DefaultText from '../DefaultText'
import marginOffset from '../../util/marginOffset'
import colors from '../../util/colors'
import merge from '../../util/merge'
import commonStyle from './../style.js'
import { bindActionCreators } from 'redux'
import * as actions from '../../store/reducer/login'
import Login from './Login'
import LoginOverlay from './LoginOverlay'
import StatusMessage from '../StatusMessage'

const style = {
  container: {
    flex: 1,
    alignItems: 'center',
    paddingLeft: 30,
    paddingRight: 30,
    paddingBottom: 24
  },
  bottomContainer : {
    alignItems: 'center',
    alignSelf: 'stretch'
  },
  background: {
    position: 'absolute',
    height: Dimensions.get('window').height,
    left: 0,
  },
  welcome: {
    container: {
      flex: 1,
      justifyContent: 'flex-start',
      alignItems: 'center',
      marginTop: marginOffset(254),
      marginBottom: 10,
    },
  },
  loginButton: {
    container: {
      backgroundColor: colors.white,
      borderRadius: 10,
      alignSelf: 'stretch',
      height: 48,
      alignItems: 'center',
      justifyContent: 'center',
      marginBottom: 10
    },
    text: {
      color: colors.bristolBlue2,
      fontSize: 22,
      fontFamily: commonStyle.museo500
    }
  },
  skipButton: {
    container: {
      backgroundColor: colors.transparent,
      borderRadius: 10,
      borderWidth: 1.5,
      borderColor: colors.white,
      alignSelf: 'stretch',
      height: 48,
      alignItems: 'center',
      justifyContent: 'center',
      marginBottom: 18
    },
    text: {
      color: colors.white,
      fontSize: 18,
      fontFamily: commonStyle.museo500
    }
  }
}

class Splash  extends React.Component {
  constructor() {
    super()
    this.state = {
      backgroundOffset: new Animated.Value(0)
    }
  }

  componentDidMount() {
    this.animateBackground()
  }

  animateBackground() {
    const animateBackgroundTo = toValue =>
      Animated.timing(this.state.backgroundOffset, {
        toValue,
        duration: 240000,
        easing: Easing.linear
      })

    // create a looping animation for the background
    Animated.sequence([
      // background image is 7900px wide. Assuming the widest screem is <1080 (6 plus)
      animateBackgroundTo(-(7900 - 1080)),
      animateBackgroundTo(0)
    ]).start(event => {
      if (event.finished) {
        this.animateBackground()
      }
    })
  }

  render()  {
    return (
      <View style={style.container}>
        <Animated.Image
          style={merge(style.background, {
            transform: [
              {translateX: this.state.backgroundOffset}
            ]
          })}
          resizeMode='stretch'
          source={require('../background.jpg')}/>
        <View style={style.welcome.container}>
          { this.props.renderWelcomeMessage(this.props) }
        </View>
        { this.props.loginFormOpen
          ? undefined
          : <View style={style.bottomContainer}>
              <TouchableHighlight
                  style={style.loginButton.container}
                  onPress={() => this.props.openLoginForm(true)}
                  underlayColor={colors.offWhite}>
                <DefaultText style={style.loginButton.text}>{this.props.loginButtonText}</DefaultText>
              </TouchableHighlight>
              <TouchableHighlight
                  style={style.skipButton.container}
                  onPress={this.props.logout}
                  underlayColor={colors.bristolBlue3}>
                <DefaultText style={style.skipButton.text}>{this.props.logoutButtonText}</DefaultText>
              </TouchableHighlight>
              { this.props.renderInfoText(this.props) }
            </View>
        }
        <LoginOverlay/>
        <Login hideUsernameInput={this.props.hideUsernameInput}/>
        <StatusMessage/>
      </View>
    )
  }
}

const mapStateToProps = (state) => ({
    ...state.login
})

const mapDispatchToProps = (dispatch) =>
  bindActionCreators(actions, dispatch)

export default connect(mapStateToProps, mapDispatchToProps)(Splash)
<|endoftext|>


<commit_before>/*
 * Handles start time display
 *
 * It process `Contract.startDates` in case of forward
 * starting contracts and populate the start time select
 * box
 */

var StartDates = (function(){
    'use strict';

    var hasNow = 0;
    var displayed = 0;

    var compareStartDate = function(a,b) {
        if (a.date < b.date)
            return -1;
        if (a.date > b.date)
            return 1;
        return 0;
    };

    var getElement = function(){
        return document.getElementById('date_start');
    };

    var displayStartDates = function() {

        var startDates = Contract.startDates();

        if (startDates && startDates.list.length) {

            var target= getElement(),
                fragment =  document.createDocumentFragment(),
                row = document.getElementById('date_start_row');

            row.style.display = 'flex';

            while (target && target.firstChild) {
                target.removeChild(target.firstChild);
            }

            if(startDates.has_spot){
                var option = document.createElement('option');
                var content = document.createTextNode(Content.localize().textNow);
                option.setAttribute('value', 'now');
                option.appendChild(content);
                fragment.appendChild(option);
                hasNow = 1;
            }
            else{
                hasNow = 0;
            }

            startDates.list.sort(compareStartDate);

            var first;
            startDates.list.forEach(function (start_date) {
                var a = moment.unix(start_date.open).utc();
                var b = moment.unix(start_date.close).utc();

                var ROUNDING = 5 * 60 * 1000;
                var start = moment.utc();

                if(moment(start).isAfter(moment(a))){
                    a = start;
                }

                a = moment(Math.ceil((+a) / ROUNDING) * ROUNDING).utc();

                while(a.isBefore(b)) {
                    if(a.unix()-start.unix()>5*60){
                        option = document.createElement('option');
                        option.setAttribute('value', a.utc().unix());
                        if(typeof first === 'undefined' && !hasNow){
                            first = a.utc().unix();
                        }
                        content = document.createTextNode(a.format('HH:mm ddd'));
                        if(option.value === Defaults.get('date_start')) {
                            option.setAttribute('selected', 'selected');
                        }
                        option.appendChild(content);
                        fragment.appendChild(option);
                    }
                    a.add(5, 'minutes');
                }
            });
            target.appendChild(fragment);
            Defaults.set('date_start', target.value);
            displayed = 1;
            if(first){
                TradingEvents.onStartDateChange(first);
            }
        } else {
            displayed = 0;
            document.getElementById('date_start_row').style.display = 'none';
            Defaults.remove('date_start');
        }
    };

    var setNow = function(){
        if(hasNow){
            var element = getElement();
            element.value = 'now';
            $('#date_start').removeClass('light-yellow-background');
            Defaults.set('date_start', 'now');
        }
    } ;

    return {
        display: displayStartDates,
        node: getElement,
        setNow: setNow,
        displayed: function(){ return displayed; }
    };

})();
<commit_msg>Add checking everywhere<commit_after>/*
 * Handles start time display
 *
 * It process `Contract.startDates` in case of forward
 * starting contracts and populate the start time select
 * box
 */

var StartDates = (function(){
    'use strict';

    var hasNow = 0;
    var displayed = 0;

    var compareStartDate = function(a,b) {
        if (a.date < b.date)
            return -1;
        if (a.date > b.date)
            return 1;
        return 0;
    };

    var getElement = function(){
        return document.getElementById('date_start');
    };

    var displayStartDates = function() {

        var startDates = Contract.startDates();

        if (startDates && startDates.list.length) {

            var target= getElement(),
                fragment =  document.createDocumentFragment(),
                row = document.getElementById('date_start_row');

            row.style.display = 'flex';

            while (target && target.firstChild) {
                target.removeChild(target.firstChild);
            }

            if(startDates.has_spot){
                var option = document.createElement('option');
                var content = document.createTextNode(Content.localize().textNow);
                option.setAttribute('value', 'now');
                $('#date_start').removeClass('light-yellow-background');
                option.appendChild(content);
                fragment.appendChild(option);
                hasNow = 1;
            }
            else{
                hasNow = 0;
            }

            startDates.list.sort(compareStartDate);

            var first;
            startDates.list.forEach(function (start_date) {
                var a = moment.unix(start_date.open).utc();
                var b = moment.unix(start_date.close).utc();

                var ROUNDING = 5 * 60 * 1000;
                var start = moment.utc();

                if(moment(start).isAfter(moment(a))){
                    a = start;
                }

                a = moment(Math.ceil((+a) / ROUNDING) * ROUNDING).utc();

                while(a.isBefore(b)) {
                    if(a.unix()-start.unix()>5*60){
                        option = document.createElement('option');
                        option.setAttribute('value', a.utc().unix());
                        if(typeof first === 'undefined' && !hasNow){
                            first = a.utc().unix();
                        }
                        content = document.createTextNode(a.format('HH:mm ddd'));
                        if(option.value === Defaults.get('date_start')) {
                            option.setAttribute('selected', 'selected');
                        }
                        option.appendChild(content);
                        fragment.appendChild(option);
                    }
                    a.add(5, 'minutes');
                }
            });
            target.appendChild(fragment);
            Defaults.set('date_start', target.value);
            displayed = 1;
            if(first){
                TradingEvents.onStartDateChange(first);
            }
        } else {
            displayed = 0;
            document.getElementById('date_start_row').style.display = 'none';
            Defaults.remove('date_start');
        }
    };

    var setNow = function(){
        if(hasNow){
            var element = getElement();
            element.value = 'now';
            $('#date_start').removeClass('light-yellow-background');
            Defaults.set('date_start', 'now');
        }
    } ;

    return {
        display: displayStartDates,
        node: getElement,
        setNow: setNow,
        displayed: function(){ return displayed; }
    };

})();
<|endoftext|>


<commit_before>var _ = require('underscore');
var GMapsLayerView = require('./gmaps-layer-view');

var GMapsPlainLayerView = function(layerModel, gmapsMap) {
  this.color = layerModel.get('color')
   GMapsLayerView.call(this, layerModel, gmapsMap);
};

_.extend(
  GMapsPlainLayerView.prototype,
  GMapsLayerView.prototype, {

  _update: function() {
    this.color = this.model.get('color')
    this.refreshView();
  },

  getTile: function(coord, zoom, ownerDocument) {
      var div = document.createElement('div');
      div.style.width = this.tileSize.x;
      div.style.height = this.tileSize.y;
      div['background-color'] = this.color;
      return div;
  },

  tileSize: new google.maps.Size(256,256),
  maxZoom: 100,
  minZoom: 0,
  name:"plain layer",
  alt: "plain layer"
});

module.exports = GMapsPlainLayerView;
<commit_msg>Whitespace<commit_after>var _ = require('underscore');
var GMapsLayerView = require('./gmaps-layer-view');

var GMapsPlainLayerView = function(layerModel, gmapsMap) {
  this.color = layerModel.get('color')
  GMapsLayerView.call(this, layerModel, gmapsMap);
};

_.extend(
  GMapsPlainLayerView.prototype,
  GMapsLayerView.prototype, {

  _update: function() {
    this.color = this.model.get('color')
    this.refreshView();
  },

  getTile: function(coord, zoom, ownerDocument) {
      var div = document.createElement('div');
      div.style.width = this.tileSize.x;
      div.style.height = this.tileSize.y;
      div['background-color'] = this.color;
      return div;
  },

  tileSize: new google.maps.Size(256,256),
  maxZoom: 100,
  minZoom: 0,
  name:"plain layer",
  alt: "plain layer"
});

module.exports = GMapsPlainLayerView;
<|endoftext|>


<commit_before>var firebase = new Firebase('https://thebluealliance.firebaseio.com/notifications/');
var maxNumCards = 100;
var numToAnimate = 3; // How many cards to animate
var earliestKey = null;
var visibleTypes = {'favorite_teams_only': true};  // default = visible

$(window).load(function() {
    $('#ticker-filter').click(function() {
        $('#ticker-filter-options').slideToggle();
    });

    firebase.limitToLast(maxNumCards).on('child_added', function(childSnapshot) {
        var card = buildNotificationCard(childSnapshot.val());
        $('#ticker-notifications').prepend(card);
        var len = $('#ticker-notifications').length;
        if (len > maxNumCards) {
            $('#ticker-notifications').children()[len-1].remove();
        }
        updateVisibility(card, card.attr('data-type'));

        if (earliestKey == null || childSnapshot.key() < earliestKey) {
            earliestKey = childSnapshot.key();
        }
    });

    // Show/hide notification types based on checkboxes
    $('input[type="checkbox"]').click(function(){
        var type = $(this).attr("value");
        if (type in visibleTypes) {
            visibleTypes[type] = !visibleTypes[type];
        } else {
            visibleTypes[type] = false;
        }
        updateAllTickerCards();
    });
});

function favoriteTeamsOff() {
    visibleTypes['favorite_teams_only'] = false;
    $('#favorite-teams-only').attr('checked', false);
}

function updateFavMatchOutline() {
    var favTeamNums = getFavoriteTeamNums();
    $('.team-cell').each(function (){
        var hasFav = false;
        for (var i=0; i<favTeamNums.length; i++) {
            if (favTeamNums[i] == $(this).text()) {
                if ($(this).hasClass('red')) {
                    $(this).addClass('red-favorite');
                } else {
                    $(this).addClass('blue-favorite');
                }
                hasFav = true;
                break;
            }
        }
        if (!hasFav) {
            $(this).removeClass('red-favorite');
            $(this).removeClass('blue-favorite');
        }
    });
}

function updateAllTickerCards() {
    var favTeamNums = getFavoriteTeamNums();
    var count = 0;
    $('#ticker-notifications').children().each(function () {
        var dataType = $(this).attr('data-type');
        if (!(dataType in visibleTypes) || visibleTypes[dataType]) {
            if (visibleTypes['favorite_teams_only'] && (dataType == 'upcoming_match' || dataType == 'match_score')) {
                var isFav = false;
                for (var i=0; i<favTeamNums.length; i++) {
                    if ($(this).attr('data-team-nums').split(',').indexOf(favTeamNums[i]) != -1) {
                        isFav = true;
                        break;
                    }
                }
                if (!isFav) {
                    count = hideHelper($(this), count);
                    return;
                }
            }
            count = showHelper($(this), count);
        } else {
            count = hideHelper($(this), count);
            return;
        }
    });
}

function hideHelper(e, count) {
    // Helper for hiding cards
    var new_count = count;
    if ($(this).is(':visible')) {
        new_count++;
    }
    if (count < numToAnimate) {
        e.slideUp();
    } else {
        e.hide();
    }
    return new_count
}

function showHelper(e, count) {
    // Helper for showing cards
    var new_count = count;
    if (!$(this).is(':visible')) {
        new_count++;
    }
    if (count < numToAnimate) {
        e.slideDown();
    } else {
        e.show();
    }
    return new_count
}

function updateVisibility(e, type) {
    // New cads are always hidden. This chooses whether or not to show it
    if (!(type in visibleTypes) || visibleTypes[type]) {
        if (visibleTypes['favorite_teams_only'] && (type == 'match_score' || type == 'upcoming_match')) {
            var favTeamNums = getFavoriteTeamNums();
            for (var i=0; i<favTeamNums.length; i++) {
                if (e.attr('data-team-nums').split(',').indexOf(favTeamNums[i]) != -1) {
                    e.slideDown();
                }
            }
        } else {
            e.slideDown();
        }
    }
}

function buildNotificationCard(data){
    var payload = data['payload'];
    if(payload == null){
        return;
    }
    var timeString = data['time'];
    var timeFormatted = new Date(timeString+"+00:00").toLocaleString();
    var messageData = JSON.stringify(payload['message_data'], null, 2);
    var messageType = payload['message_type'];

    var card = $('
', {'class': 'panel'});
    card.hide();  // default with cards hidden
    card.attr('data-type', messageType);
    var body = $('
', {'class': 'panel-body'});
    var eventKey = 'XXXX????';
    switch(messageType) {
        case 'alliance_selection':
            card.addClass('panel-material-red');
            eventKey = payload['message_data']['event_key'];

            body.append($('', {text: 'Alliance Selection Results: TODO'}));
            break;
        case 'awards_posted':
            card.addClass('panel-material-green');
            eventKey = payload['message_data']['event_key'];

            body.append($('', {text: 'Awards Posted'}));
            break;
        case 'match_score':
            card.addClass('panel-material-indigo');
            eventKey = payload['message_data']['match']['event_key'];

            body.append($('', {text: 'Match Result: ' + matchKeyToReadableName(payload['message_data']['match']['key'])}));

            var redTeams = payload['message_data']['match']['alliances']['red']['teams'];
            var blueTeams = payload['message_data']['match']['alliances']['blue']['teams'];
            var redScore = payload['message_data']['match']['alliances']['red']['score'];
            var blueScore = payload['message_data']['match']['alliances']['blue']['score'];
            body.append(constructMatchTable(redTeams, blueTeams, redScore, blueScore));
            card.attr('data-team-nums', redTeams.concat(blueTeams));
            break;
        case 'schedule_updated':
            card.addClass('panel-material-light-blue');
            eventKey = payload['message_data']['event_key'];

            body.append($('').append($('', {'href': 'http://www.thebluealliance.com/event/'+eventKey, 'target': '_blank', text: 'Schedule Updated'})));
            break;
        case 'starting_comp_level':
            card.addClass('panel-material-purple');
            eventKey = payload['message_data']['event_key'];

            levels = {
                'qm': 'Qualification',
                'qf': 'Quarterfinal',
                'sf': 'Semifinal',
                'f': 'Final',
            }

            body.append($('', {text: levels[payload['message_data']['comp_level']] + ' matches starting'}));
            break;
        case 'upcoming_match':
            card.addClass('panel-material-orange');
            eventKey = payload['message_data']['match_key'].split('_')[0];

            body.append($('', {text: 'Upcoming Match: ' + matchKeyToReadableName(payload['message_data']['match_key'])}));

            var redTeams = payload['message_data']['team_keys'].slice(0, 3);
            var blueTeams = payload['message_data']['team_keys'].slice(3, 6);
            body.append(constructMatchTable(redTeams, blueTeams, null, null));
            card.attr('data-team-nums', redTeams.concat(blueTeams));
            break;
        default:
            body.append($('', {text: messageType}));
            body.append($('
', {text: messageData}));
            break;
    }
    var eventName = payload['message_data']['event_name'];
    // Strip things out like "FIM District - " This is crude and may break
    var splitEventName = eventName.split(' - ');
    if (splitEventName.length == 1) {
        eventName = splitEventName[0];
    } else {
        eventName = splitEventName[1];
    }
    var heading = $('
', {
        'class': 'panel-heading',
        html: ""+eventName+" ["+eventKey.toUpperCase().substring(4)+"]"
    });

    var footer = $('
', {'class': 'panel-footer'});
    var time = $('
', {'class': 'pull-right'});
    time.append($('', {text: timeFormatted}));
    footer.append(time)
    return card.append(heading).append(body).append(footer);
}

function constructMatchTable(redTeams, blueTeams, redScore, blueScore) {
    for (var i=0; i<redTeams.length; i++) {
        redTeams[i] = redTeams[i].substring(3);
    }
    for (var i=0; i<blueTeams.length; i++) {
        blueTeams[i] = blueTeams[i].substring(3);
    }

    var favTeamNums = getFavoriteTeamNums();
    var matchTable = $('
', {'class': 'match-table'});

    var redRow = $('
');
    for (var i in redTeams) {
        var td = $('', {'class': 'red team-cell'}).append($('', {'href': '/team/'+redTeams[i], 'target': '_blank', text: redTeams[i]}));
        if (favTeamNums.indexOf(redTeams[i]) != -1) {
            td.addClass('red-favorite');
        }
        redRow.append(td)
    }
    if (redScore != null) {
        redRow.append($('', {'class': 'redScore', text: redScore}));
    }
    var blueRow = $('
');
    for (var i in blueTeams) {
        var td = $('', {'class': 'blue team-cell'}).append($('', {'href': '/team/'+blueTeams[i], 'target': '_blank', text: blueTeams[i]}));
        if (favTeamNums.indexOf(blueTeams[i]) != -1) {
            td.addClass('blue-favorite');
        }
        blueRow.append(td)
    }
    if (blueScore != null) {
        blueRow.append($('', {'class': 'blueScore', text: blueScore}));
    }

    var tbody = $('
');
    tbody.append(redRow);
    tbody.append(blueRow);
    matchTable.append(tbody)
    return matchTable
}

function matchKeyToReadableName(matchKey) {
    return matchKey.split('_')[1];
}
<commit_msg>fix max cards bug<commit_after>var firebase = new Firebase('https://thebluealliance.firebaseio.com/notifications/');
var maxNumCards = 100;
var numToAnimate = 3; // How many cards to animate
var earliestKey = null;
var visibleTypes = {'favorite_teams_only': true};  // default = visible

$(window).load(function() {
    $('#ticker-filter').click(function() {
        $('#ticker-filter-options').slideToggle();
    });

    firebase.limitToLast(maxNumCards).on('child_added', function(childSnapshot) {
        var card = buildNotificationCard(childSnapshot.val());
        $('#ticker-notifications').prepend(card);
        var len = $('#ticker-notifications').children().length;
        if (len > maxNumCards) {
            $('#ticker-notifications').children()[len-1].remove();
        }
        updateVisibility(card, card.attr('data-type'));

        if (earliestKey == null || childSnapshot.key() < earliestKey) {
            earliestKey = childSnapshot.key();
        }
    });

    // Show/hide notification types based on checkboxes
    $('input[type="checkbox"]').click(function(){
        var type = $(this).attr("value");
        if (type in visibleTypes) {
            visibleTypes[type] = !visibleTypes[type];
        } else {
            visibleTypes[type] = false;
        }
        updateAllTickerCards();
    });
});

function favoriteTeamsOff() {
    visibleTypes['favorite_teams_only'] = false;
    $('#favorite-teams-only').attr('checked', false);
}

function updateFavMatchOutline() {
    var favTeamNums = getFavoriteTeamNums();
    $('.team-cell').each(function (){
        var hasFav = false;
        for (var i=0; i<favTeamNums.length; i++) {
            if (favTeamNums[i] == $(this).text()) {
                if ($(this).hasClass('red')) {
                    $(this).addClass('red-favorite');
                } else {
                    $(this).addClass('blue-favorite');
                }
                hasFav = true;
                break;
            }
        }
        if (!hasFav) {
            $(this).removeClass('red-favorite');
            $(this).removeClass('blue-favorite');
        }
    });
}

function updateAllTickerCards() {
    var favTeamNums = getFavoriteTeamNums();
    var count = 0;
    $('#ticker-notifications').children().each(function () {
        var dataType = $(this).attr('data-type');
        if (!(dataType in visibleTypes) || visibleTypes[dataType]) {
            if (visibleTypes['favorite_teams_only'] && (dataType == 'upcoming_match' || dataType == 'match_score')) {
                var isFav = false;
                for (var i=0; i<favTeamNums.length; i++) {
                    if ($(this).attr('data-team-nums').split(',').indexOf(favTeamNums[i]) != -1) {
                        isFav = true;
                        break;
                    }
                }
                if (!isFav) {
                    count = hideHelper($(this), count);
                    return;
                }
            }
            count = showHelper($(this), count);
        } else {
            count = hideHelper($(this), count);
            return;
        }
    });
}

function hideHelper(e, count) {
    // Helper for hiding cards
    var new_count = count;
    if ($(this).is(':visible')) {
        new_count++;
    }
    if (count < numToAnimate) {
        e.slideUp();
    } else {
        e.hide();
    }
    return new_count
}

function showHelper(e, count) {
    // Helper for showing cards
    var new_count = count;
    if (!$(this).is(':visible')) {
        new_count++;
    }
    if (count < numToAnimate) {
        e.slideDown();
    } else {
        e.show();
    }
    return new_count
}

function updateVisibility(e, type) {
    // New cads are always hidden. This chooses whether or not to show it
    if (!(type in visibleTypes) || visibleTypes[type]) {
        if (visibleTypes['favorite_teams_only'] && (type == 'match_score' || type == 'upcoming_match')) {
            var favTeamNums = getFavoriteTeamNums();
            for (var i=0; i<favTeamNums.length; i++) {
                if (e.attr('data-team-nums').split(',').indexOf(favTeamNums[i]) != -1) {
                    e.slideDown();
                }
            }
        } else {
            e.slideDown();
        }
    }
}

function buildNotificationCard(data){
    var payload = data['payload'];
    if(payload == null){
        return;
    }
    var timeString = data['time'];
    var timeFormatted = new Date(timeString+"+00:00").toLocaleString();
    var messageData = JSON.stringify(payload['message_data'], null, 2);
    var messageType = payload['message_type'];

    var card = $('
', {'class': 'panel'});
    card.hide();  // default with cards hidden
    card.attr('data-type', messageType);
    var body = $('
', {'class': 'panel-body'});
    var eventKey = 'XXXX????';
    switch(messageType) {
        case 'alliance_selection':
            card.addClass('panel-material-red');
            eventKey = payload['message_data']['event_key'];

            body.append($('', {text: 'Alliance Selection Results: TODO'}));
            break;
        case 'awards_posted':
            card.addClass('panel-material-green');
            eventKey = payload['message_data']['event_key'];

            body.append($('', {text: 'Awards Posted'}));
            break;
        case 'match_score':
            card.addClass('panel-material-indigo');
            eventKey = payload['message_data']['match']['event_key'];

            body.append($('', {text: 'Match Result: ' + matchKeyToReadableName(payload['message_data']['match']['key'])}));

            var redTeams = payload['message_data']['match']['alliances']['red']['teams'];
            var blueTeams = payload['message_data']['match']['alliances']['blue']['teams'];
            var redScore = payload['message_data']['match']['alliances']['red']['score'];
            var blueScore = payload['message_data']['match']['alliances']['blue']['score'];
            body.append(constructMatchTable(redTeams, blueTeams, redScore, blueScore));
            card.attr('data-team-nums', redTeams.concat(blueTeams));
            break;
        case 'schedule_updated':
            card.addClass('panel-material-light-blue');
            eventKey = payload['message_data']['event_key'];

            body.append($('').append($('', {'href': 'http://www.thebluealliance.com/event/'+eventKey, 'target': '_blank', text: 'Schedule Updated'})));
            break;
        case 'starting_comp_level':
            card.addClass('panel-material-purple');
            eventKey = payload['message_data']['event_key'];

            levels = {
                'qm': 'Qualification',
                'qf': 'Quarterfinal',
                'sf': 'Semifinal',
                'f': 'Final',
            }

            body.append($('', {text: levels[payload['message_data']['comp_level']] + ' matches starting'}));
            break;
        case 'upcoming_match':
            card.addClass('panel-material-orange');
            eventKey = payload['message_data']['match_key'].split('_')[0];

            body.append($('', {text: 'Upcoming Match: ' + matchKeyToReadableName(payload['message_data']['match_key'])}));

            var redTeams = payload['message_data']['team_keys'].slice(0, 3);
            var blueTeams = payload['message_data']['team_keys'].slice(3, 6);
            body.append(constructMatchTable(redTeams, blueTeams, null, null));
            card.attr('data-team-nums', redTeams.concat(blueTeams));
            break;
        default:
            body.append($('', {text: messageType}));
            body.append($('
', {text: messageData}));
            break;
    }
    var eventName = payload['message_data']['event_name'];
    // Strip things out like "FIM District - " This is crude and may break
    var splitEventName = eventName.split(' - ');
    if (splitEventName.length == 1) {
        eventName = splitEventName[0];
    } else {
        eventName = splitEventName[1];
    }
    var heading = $('
', {
        'class': 'panel-heading',
        html: ""+eventName+" ["+eventKey.toUpperCase().substring(4)+"]"
    });

    var footer = $('
', {'class': 'panel-footer'});
    var time = $('
', {'class': 'pull-right'});
    time.append($('', {text: timeFormatted}));
    footer.append(time)
    return card.append(heading).append(body).append(footer);
}

function constructMatchTable(redTeams, blueTeams, redScore, blueScore) {
    for (var i=0; i<redTeams.length; i++) {
        redTeams[i] = redTeams[i].substring(3);
    }
    for (var i=0; i<blueTeams.length; i++) {
        blueTeams[i] = blueTeams[i].substring(3);
    }

    var favTeamNums = getFavoriteTeamNums();
    var matchTable = $('
', {'class': 'match-table'});

    var redRow = $('
');
    for (var i in redTeams) {
        var td = $('', {'class': 'red team-cell'}).append($('', {'href': '/team/'+redTeams[i], 'target': '_blank', text: redTeams[i]}));
        if (favTeamNums.indexOf(redTeams[i]) != -1) {
            td.addClass('red-favorite');
        }
        redRow.append(td)
    }
    if (redScore != null) {
        redRow.append($('', {'class': 'redScore', text: redScore}));
    }
    var blueRow = $('
');
    for (var i in blueTeams) {
        var td = $('', {'class': 'blue team-cell'}).append($('', {'href': '/team/'+blueTeams[i], 'target': '_blank', text: blueTeams[i]}));
        if (favTeamNums.indexOf(blueTeams[i]) != -1) {
            td.addClass('blue-favorite');
        }
        blueRow.append(td)
    }
    if (blueScore != null) {
        blueRow.append($('', {'class': 'blueScore', text: blueScore}));
    }

    var tbody = $('
');
    tbody.append(redRow);
    tbody.append(blueRow);
    matchTable.append(tbody)
    return matchTable
}

function matchKeyToReadableName(matchKey) {
    return matchKey.split('_')[1];
}
<|endoftext|>


<commit_before>/**
 * A Topic is an Agenda Topic which can
 * have multiple sub-items called InfoItem.
 */
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { subElementsHelper } from '/imports/helpers/subElements';
import { Minutes } from './minutes';
import { MeetingSeries } from './meetingseries';
import { InfoItemFactory } from './InfoItemFactory';
import { InfoItem } from './infoitem';
import { _ } from 'meteor/underscore';

import './helpers/promisedMethods';
import './collections/minutes_private';

function resolveParentElement(parent) {
    if (typeof parent === 'string') {
        let parentId = parent;
        parent =  MeetingSeries.findOne(parentId);
        if (!parent) parent = Minutes.findOne(parentId);
        return parent;
    }

    if ((typeof parent === 'object') && ( typeof parent.upsertTopic === 'function' )) {
        return parent;
    }

    throw new Meteor.Error('Runtime error, illegal parent element');
}

function resolveTopic(parentElement, source) {
    if (typeof source === 'string') {
        if (typeof parentElement.findTopic !== 'function') {
            throw new Meteor.Error('Runtime error, illegal parent element');
        }

        source = parentElement.findTopic(source);
        if (!source) {
            throw new Meteor.Error('Runtime Error, could not find topic!');
        }
    }

    _.defaults(source, {
        isOpen: true,
        isNew: true,
        isRecurring: false,
        labels: [],
        isSkipped: false
    });

    return source;
}

export class Topic {

    /**
     *
     * @param parentElement {string|object} is either the id of the parent minute or parent meeting series
     *                      or the parent object which has at least the methods upsertTopic() and findTopic().
     *                      So the parent object could be both a minute or a meeting series.
     * @param source        {string|object} topic_id then the document will be fetched from the parentMinute
     *                      or a topic object
     */
    constructor(parentElement, source) {
        if (!parentElement || !source) {
            return;
        }

        this._parentMinutes = resolveParentElement(parentElement);
        if (!this._parentMinutes) {
            return;
        }

        this._topicDoc = resolveTopic(this._parentMinutes, source);

        if (!Array.isArray(this._topicDoc.infoItems)) {
            this._topicDoc.infoItems = [];
        }
    }

    // ################### static methods
    static findTopicIndexInArray(id, topics) {
        return subElementsHelper.findIndexById(id, topics);
    }

    /**
     * Checks if the given topic document
     * has at least one open ActionItem.
     *
     * @param topicDoc document of a topic
     * @returns {boolean}
     */
    static hasOpenActionItem(topicDoc) {
        let infoItemDocs = topicDoc.infoItems;
        let i;
        for (i = 0; i < infoItemDocs.length; i++) {
            if (infoItemDocs[i].isOpen) {
                return true;
            }
        }
        return false;
    }

    // ################### object methods
    toString () {
        return 'Topic: '+JSON.stringify(this._topicDoc, null, 4);
    }

    log () {
        console.log(this.toString());
    }

    invalidateIsNewFlag() {
        this._topicDoc.isNew = false;
        this._topicDoc.infoItems.forEach(infoItemDoc => {
            let infoItem = InfoItemFactory.createInfoItem(this, infoItemDoc);
            infoItem.invalidateIsNewFlag();
        });
    }

    /**
     * A topic is finally completed (and will not show up in future minutes) if it is
     *    - not checked as dicussed and
     *    - has no more open AIs and
     *    - is not marked as recurring
     * @returns {boolean}
     */
    isFinallyCompleted() {
        return (!this.getDocument().isOpen &&
                !this.hasOpenActionItem() &&
                !this.isRecurring());
    }

    isDeleteAllowed() {
        return (this.getDocument().createdInMinute === this._parentMinutes._id);
    }

    isRecurring() {
        return this.getDocument().isRecurring;
    }

    toggleRecurring() {
        this.getDocument().isRecurring = !this.isRecurring();
    }

    isSkipped() {
        return this.getDocument().isSkipped;
    }

    toggleSkip(forceOpenTopic = true) {
        this.getDocument().isSkipped = !this.isSkipped();
        if (forceOpenTopic) {
            if (this.isSkipped() && (!this._topicDoc.isOpen)) { // topic has been set to skip, so it will be automatically set as open
                this.toggleState();
            }
        }
    }

    async upsertInfoItem(topicItemDoc, saveChanges, insertPlacementTop = true) {
        if (saveChanges === undefined) {
            saveChanges = true;
        }
        let i = undefined;
        if (! topicItemDoc._id) {             // brand-new topicItem
            topicItemDoc._id = Random.id();   // create our own local _id here!
        } else {
            i = subElementsHelper.findIndexById(topicItemDoc._id, this.getInfoItems());
        }
        if (i === undefined) {                      // topicItem not in array
            if (insertPlacementTop) {
                this.getInfoItems().unshift(topicItemDoc);
            }  else {
                this.getInfoItems().push(topicItemDoc);
            }
        } else {
            this.getInfoItems()[i] = topicItemDoc;      // overwrite in place
        }

        if (saveChanges) {
            try {
                await this.save();
            } catch(e) {
                throw e;
            }
        }
        return topicItemDoc._id;
    }

    async removeInfoItem(id) {
        let index = subElementsHelper.findIndexById(id, this.getInfoItems());
        let item = this.getInfoItems()[index];
        if (InfoItem.isActionItem(item) && !InfoItem.isCreatedInMinutes(item, this._parentMinutes._id)) {
            throw new Meteor.Error('Cannot remove item', 'It is not allowed to remove an action item which was not ' +
                'created within the current minutes');
        }

        if (index !== undefined) {
            this.getInfoItems().splice(index, 1);
            return this.save();
        }
    }

    /**
     * Removes all fire-and-forget elements as well
     * as closed AIs from this topic (info items which are
     * no action items)
     */
    tailorTopic() {
        this._topicDoc.infoItems = this._topicDoc.infoItems.filter((infoItemDoc) => {
            let infoItem = InfoItemFactory.createInfoItem(this, infoItemDoc);
            return infoItem.isSticky();
        });
    }

    /**
     * Finds the InfoItem identified by its
     * id.
     *
     * @param id
     * @returns {undefined|InfoItem|ActionItem}
     */
    findInfoItem(id) {
        let i = subElementsHelper.findIndexById(id, this.getInfoItems());
        if (i !== undefined) {
            return InfoItemFactory.createInfoItem(this, this.getInfoItems()[i]);
        }
        return undefined;
    }

    getInfoItems() {
        return this._topicDoc.infoItems;
    }

    getOnlyInfoItems() {
        return this.getInfoItems().filter((item) => {
            return !InfoItem.isActionItem(item);
        });
    }

    getOnlyActionItems() {
        return this._topicDoc.infoItems.filter((infoItemDoc) => {
            return InfoItem.isActionItem(infoItemDoc);
        });
    }

    getOpenActionItems() {
        return this._topicDoc.infoItems.filter((infoItemDoc) => {
            return InfoItem.isActionItem(infoItemDoc) && infoItemDoc.isOpen;
        });
    }

    setItems(items) {
        this._topicDoc.infoItems = items;
    }

    setSubject(subject) {
        this._topicDoc.subject = subject;
    }

    getSubject() {
        return this._topicDoc.subject;
    }

    async save() {
        return this._parentMinutes.upsertTopic(this._topicDoc);
    }

    async saveAtBottom() {
        return this._parentMinutes.upsertTopic(this._topicDoc, false);
    }

    async toggleState () {    // open/close
        this._topicDoc.isOpen = !this._topicDoc.isOpen;
        return await Meteor.callPromise('minutes.updateTopic', this._topicDoc._id, { isOpen: this._topicDoc.isOpen });
    }

    async closeTopicAndAllOpenActionItems() {
        this._topicDoc.isOpen = false;
        this._topicDoc.isRecurring = false;
        this.getOpenActionItems().forEach(item => {
            item.isOpen = false;
        });
        await this.save();
    }

    hasOpenActionItem() {
        return Topic.hasOpenActionItem(this._topicDoc);
    }

    getDocument() {
        return this._topicDoc;
    }

    addLabelsByIds(labelIds) {
        labelIds.forEach(id => {
            if (!this.hasLabelWithId(id)) {
                this._topicDoc.labels.push(id);
            }
        });
    }

    hasLabelWithId(labelId) {
        let i;
        for (i = 0; i < this._topicDoc.labels.length; i++) {
            if (this._topicDoc.labels[i] === labelId) {
                return true;
            }
        }
        return false;
    }

    getLabelsRawArray() {
        if (!this._topicDoc.labels) {
            return [];
        }
        return this._topicDoc.labels;
    }

    /**
     * Checks whether this topic has associated responsibles
     * or not. This method must have the same name as the
     * actionItem.hasResponsibles method.
     *
     * @return {boolean}
     */
    hasResponsibles () {
        let responsibles = this._topicDoc.responsibles;
        return (responsibles && responsibles.length > 0);
    }

    /**
     * Returns all responsibles associated with this
     * topic. This method must have the same name as the
     * actionItem.getResponsibles method.
     *
     * @return {Array}
     */
    getResponsibles() {
        return this._topicDoc.responsibles;
    }

}
<commit_msg>check condition hasOpenActionItem() fn with itemType attribute<commit_after>/**
 * A Topic is an Agenda Topic which can
 * have multiple sub-items called InfoItem.
 */
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { subElementsHelper } from '/imports/helpers/subElements';
import { Minutes } from './minutes';
import { MeetingSeries } from './meetingseries';
import { InfoItemFactory } from './InfoItemFactory';
import { InfoItem } from './infoitem';
import { _ } from 'meteor/underscore';

import './helpers/promisedMethods';
import './collections/minutes_private';

function resolveParentElement(parent) {
    if (typeof parent === 'string') {
        let parentId = parent;
        parent =  MeetingSeries.findOne(parentId);
        if (!parent) parent = Minutes.findOne(parentId);
        return parent;
    }

    if ((typeof parent === 'object') && ( typeof parent.upsertTopic === 'function' )) {
        return parent;
    }

    throw new Meteor.Error('Runtime error, illegal parent element');
}

function resolveTopic(parentElement, source) {
    if (typeof source === 'string') {
        if (typeof parentElement.findTopic !== 'function') {
            throw new Meteor.Error('Runtime error, illegal parent element');
        }

        source = parentElement.findTopic(source);
        if (!source) {
            throw new Meteor.Error('Runtime Error, could not find topic!');
        }
    }

    _.defaults(source, {
        isOpen: true,
        isNew: true,
        isRecurring: false,
        labels: [],
        isSkipped: false
    });

    return source;
}

export class Topic {

    /**
     *
     * @param parentElement {string|object} is either the id of the parent minute or parent meeting series
     *                      or the parent object which has at least the methods upsertTopic() and findTopic().
     *                      So the parent object could be both a minute or a meeting series.
     * @param source        {string|object} topic_id then the document will be fetched from the parentMinute
     *                      or a topic object
     */
    constructor(parentElement, source) {
        if (!parentElement || !source) {
            return;
        }

        this._parentMinutes = resolveParentElement(parentElement);
        if (!this._parentMinutes) {
            return;
        }

        this._topicDoc = resolveTopic(this._parentMinutes, source);

        if (!Array.isArray(this._topicDoc.infoItems)) {
            this._topicDoc.infoItems = [];
        }
    }

    // ################### static methods
    static findTopicIndexInArray(id, topics) {
        return subElementsHelper.findIndexById(id, topics);
    }

    /**
     * Checks if the given topic document
     * has at least one open ActionItem.
     *
     * @param topicDoc document of a topic
     * @returns {boolean}
     */
    static hasOpenActionItem(topicDoc) {
        let infoItemDocs = topicDoc.infoItems;
        let i;
        for (i = 0; i < infoItemDocs.length; i++) {
            if (infoItemDocs[i].itemType === 'actionItem' &&  infoItemDocs[i].isOpen) {
                return true;
            }
        }
        return false;
    }

    // ################### object methods
    toString () {
        return 'Topic: '+JSON.stringify(this._topicDoc, null, 4);
    }

    log () {
        console.log(this.toString());
    }

    invalidateIsNewFlag() {
        this._topicDoc.isNew = false;
        this._topicDoc.infoItems.forEach(infoItemDoc => {
            let infoItem = InfoItemFactory.createInfoItem(this, infoItemDoc);
            infoItem.invalidateIsNewFlag();
        });
    }

    /**
     * A topic is finally completed (and will not show up in future minutes) if it is
     *    - not checked as dicussed and
     *    - has no more open AIs and
     *    - is not marked as recurring
     * @returns {boolean}
     */
    isFinallyCompleted() {
        return (!this.getDocument().isOpen &&
                !this.hasOpenActionItem() &&
                !this.isRecurring());
    }

    isDeleteAllowed() {
        return (this.getDocument().createdInMinute === this._parentMinutes._id);
    }

    isRecurring() {
        return this.getDocument().isRecurring;
    }

    toggleRecurring() {
        this.getDocument().isRecurring = !this.isRecurring();
    }

    isSkipped() {
        return this.getDocument().isSkipped;
    }

    toggleSkip(forceOpenTopic = true) {
        this.getDocument().isSkipped = !this.isSkipped();
        if (forceOpenTopic) {
            if (this.isSkipped() && (!this._topicDoc.isOpen)) { // topic has been set to skip, so it will be automatically set as open
                this.toggleState();
            }
        }
    }

    async upsertInfoItem(topicItemDoc, saveChanges, insertPlacementTop = true) {
        if (saveChanges === undefined) {
            saveChanges = true;
        }
        let i = undefined;
        if (! topicItemDoc._id) {             // brand-new topicItem
            topicItemDoc._id = Random.id();   // create our own local _id here!
        } else {
            i = subElementsHelper.findIndexById(topicItemDoc._id, this.getInfoItems());
        }
        if (i === undefined) {                      // topicItem not in array
            if (insertPlacementTop) {
                this.getInfoItems().unshift(topicItemDoc);
            }  else {
                this.getInfoItems().push(topicItemDoc);
            }
        } else {
            this.getInfoItems()[i] = topicItemDoc;      // overwrite in place
        }

        if (saveChanges) {
            try {
                await this.save();
            } catch(e) {
                throw e;
            }
        }
        return topicItemDoc._id;
    }

    async removeInfoItem(id) {
        let index = subElementsHelper.findIndexById(id, this.getInfoItems());
        let item = this.getInfoItems()[index];
        if (InfoItem.isActionItem(item) && !InfoItem.isCreatedInMinutes(item, this._parentMinutes._id)) {
            throw new Meteor.Error('Cannot remove item', 'It is not allowed to remove an action item which was not ' +
                'created within the current minutes');
        }

        if (index !== undefined) {
            this.getInfoItems().splice(index, 1);
            return this.save();
        }
    }

    /**
     * Removes all fire-and-forget elements as well
     * as closed AIs from this topic (info items which are
     * no action items)
     */
    tailorTopic() {
        this._topicDoc.infoItems = this._topicDoc.infoItems.filter((infoItemDoc) => {
            let infoItem = InfoItemFactory.createInfoItem(this, infoItemDoc);
            return infoItem.isSticky();
        });
    }

    /**
     * Finds the InfoItem identified by its
     * id.
     *
     * @param id
     * @returns {undefined|InfoItem|ActionItem}
     */
    findInfoItem(id) {
        let i = subElementsHelper.findIndexById(id, this.getInfoItems());
        if (i !== undefined) {
            return InfoItemFactory.createInfoItem(this, this.getInfoItems()[i]);
        }
        return undefined;
    }

    getInfoItems() {
        return this._topicDoc.infoItems;
    }

    getOnlyInfoItems() {
        return this.getInfoItems().filter((item) => {
            return !InfoItem.isActionItem(item);
        });
    }

    getOnlyActionItems() {
        return this._topicDoc.infoItems.filter((infoItemDoc) => {
            return InfoItem.isActionItem(infoItemDoc);
        });
    }

    getOpenActionItems() {
        return this._topicDoc.infoItems.filter((infoItemDoc) => {
            return InfoItem.isActionItem(infoItemDoc) && infoItemDoc.isOpen;
        });
    }

    setItems(items) {
        this._topicDoc.infoItems = items;
    }

    setSubject(subject) {
        this._topicDoc.subject = subject;
    }

    getSubject() {
        return this._topicDoc.subject;
    }

    async save() {
        return this._parentMinutes.upsertTopic(this._topicDoc);
    }

    async saveAtBottom() {
        return this._parentMinutes.upsertTopic(this._topicDoc, false);
    }

    async toggleState () {    // open/close
        this._topicDoc.isOpen = !this._topicDoc.isOpen;
        return await Meteor.callPromise('minutes.updateTopic', this._topicDoc._id, { isOpen: this._topicDoc.isOpen });
    }

    async closeTopicAndAllOpenActionItems() {
        this._topicDoc.isOpen = false;
        this._topicDoc.isRecurring = false;
        this.getOpenActionItems().forEach(item => {
            item.isOpen = false;
        });
        await this.save();
    }

    hasOpenActionItem() {
        return Topic.hasOpenActionItem(this._topicDoc);
    }

    getDocument() {
        return this._topicDoc;
    }

    addLabelsByIds(labelIds) {
        labelIds.forEach(id => {
            if (!this.hasLabelWithId(id)) {
                this._topicDoc.labels.push(id);
            }
        });
    }

    hasLabelWithId(labelId) {
        let i;
        for (i = 0; i < this._topicDoc.labels.length; i++) {
            if (this._topicDoc.labels[i] === labelId) {
                return true;
            }
        }
        return false;
    }

    getLabelsRawArray() {
        if (!this._topicDoc.labels) {
            return [];
        }
        return this._topicDoc.labels;
    }

    /**
     * Checks whether this topic has associated responsibles
     * or not. This method must have the same name as the
     * actionItem.hasResponsibles method.
     *
     * @return {boolean}
     */
    hasResponsibles () {
        let responsibles = this._topicDoc.responsibles;
        return (responsibles && responsibles.length > 0);
    }

    /**
     * Returns all responsibles associated with this
     * topic. This method must have the same name as the
     * actionItem.getResponsibles method.
     *
     * @return {Array}
     */
    getResponsibles() {
        return this._topicDoc.responsibles;
    }

}
<|endoftext|>


<commit_before>var tswcalc = tswcalc || {};
tswcalc.data = tswcalc.data || {};

tswcalc.data.woodcutters = {
    'neck': {
        healer: {
            name: 'The Woodcutter\'s Sorrow',
            fixed: true,
            glyph: {
                ql: '10.5',
                primary: {
                    stat: 'critical-power',
                    dist: 4
                },
                secondary: {
                    stat: 'none',
                    dist: 0
                }
            },
            signet: {
                id: 90,
                fixed: true,
                name: 'The Woodcutter\'s Sorrow',
                description: 'Whenever you directly heal an ally, you trigger Maternal Band: If your ally has an existing barrier, they are healed for twice the strength of that barrier. Maternal Band can only occur once per ally every 4 seconds.',
                cooldown: 0,
                quality: {
                    heroic: 0
                },
                icon: 'woodcutter_sorrow'
            }
        },
        tank: {
            name: 'The Woodcutter\'s Regret',
            fixed: true,
            glyph: {
                ql: '10.5',
                primary: {
                    stat: 'magical-protection',
                    dist: 2
                },
                secondary: {
                    stat: 'physical-protection',
                    dist: 2
                }
            },
            signet: {
                id: 91,
                name: 'The Woodcutter\'s Regret',
                description: 'Whenever you are hit, you have a 33% chance to trigger Guardian Spirit: Increases the lowest of your Evade, Defence and Block Rating by 300 for 5 seconds. This effect will not trigger if two or more of these stats are of equal value.',
                cooldown: 0,
                quality: {
                    heroic: 0
                },
                icon: 'woodcutter_regret'
            }
        },
        dps: {
            name: 'The Woodcutter\'s Wrath',
            fixed: true,
            glyph: {
                ql: '10.5',
                primary: {
                    stat: 'critical-power',
                    dist: 4
                },
                secondary: {
                    stat: 'none',
                    dist: 0
                }
            },
            signet: {
                id: 92,
                name: 'The Woodcutter\'s Wrath',
                description: 'Whenever an attack fails to penetrate, you gain a Mother\'s Wrath counter. When you reach 5 counters, they are removed and you gain a beneficial effect which increases your Penetration Chance by 50% for 3 seconds. Whenever you penetrate, all Mother\'s Wrath counters are removed.',
                cooldown: 0,
                quality: {
                    heroic: 0
                },
                icon: 'woodcutter_wrath'
            }
        }
    }
};<commit_msg>woodcutter dps nerf - only tooltip<commit_after>var tswcalc = tswcalc || {};
tswcalc.data = tswcalc.data || {};

tswcalc.data.woodcutters = {
    'neck': {
        healer: {
            name: 'The Woodcutter\'s Sorrow',
            fixed: true,
            glyph: {
                ql: '10.5',
                primary: {
                    stat: 'critical-power',
                    dist: 4
                },
                secondary: {
                    stat: 'none',
                    dist: 0
                }
            },
            signet: {
                id: 90,
                fixed: true,
                name: 'The Woodcutter\'s Sorrow',
                description: 'Whenever you directly heal an ally, you trigger Maternal Band: If your ally has an existing barrier, they are healed for twice the strength of that barrier. Maternal Band can only occur once per ally every 4 seconds.',
                cooldown: 0,
                quality: {
                    heroic: 0
                },
                icon: 'woodcutter_sorrow'
            }
        },
        tank: {
            name: 'The Woodcutter\'s Regret',
            fixed: true,
            glyph: {
                ql: '10.5',
                primary: {
                    stat: 'magical-protection',
                    dist: 2
                },
                secondary: {
                    stat: 'physical-protection',
                    dist: 2
                }
            },
            signet: {
                id: 91,
                name: 'The Woodcutter\'s Regret',
                description: 'Whenever you are hit, you have a 33% chance to trigger Guardian Spirit: Increases the lowest of your Evade, Defence and Block Rating by 300 for 5 seconds. This effect will not trigger if two or more of these stats are of equal value.',
                cooldown: 0,
                quality: {
                    heroic: 0
                },
                icon: 'woodcutter_regret'
            }
        },
        dps: {
            name: 'The Woodcutter\'s Wrath',
            fixed: true,
            glyph: {
                ql: '10.5',
                primary: {
                    stat: 'critical-power',
                    dist: 4
                },
                secondary: {
                    stat: 'none',
                    dist: 0
                }
            },
            signet: {
                id: 92,
                name: 'The Woodcutter\'s Wrath',
                description: 'Whenever an attack fails to penetrate, you gain a Mother\'s Wrath counter. When you reach 5 counters, they are removed and you gain a beneficial effect which increases your Penetration Chance by 40% for 3 seconds. This effect can only occur once every 6 seconds. Whenever you penetrate, all Mother\'s Wrath counters are removed.',
                cooldown: 0,
                quality: {
                    heroic: 0
                },
                icon: 'woodcutter_wrath'
            }
        }
    }
};<|endoftext|>