Source: src/core.js

/* eslint-disable prefer-rest-params */
/**
 * Core module.
 * @module Core
 */
const base = require( './detectors/detector' );
const present = require( 'present' );
const mean = require( './tools/operations' ).mean;
const fs = require( 'fs' );
//The merge.js file contains all the merging strategies
//We'll select this strategies using this function
const applyStrategy = require( './tools/merge' ).applyStrategy;


const realTimeThreshold = 4000;

/**
 * Initialises a detector using the arguments passed. Returns that new detector.
 * @function createDetector
 * @param {string} id - Name of the detector.
 * @param {string} category - Name of the category. E.g. face, voice, etc.
 * @param {Array} media - Type of media that can be processed. E.g. image, video, sound, etc.
 * @param {boolean} realTime - The detector answers in real time.
 * @param {string} url - API/Service URL.
 * @param {Object} otherOptions - Object to save other customized options.
 * @param {Function} initialize - Function to initiliaze the detector (sign into the service, request session token, etc.)
 * @param {Function} extractEmotions - Function to analyse some media.
 * @param {Function} translateToPAD - Function called with the results of the previous function.
 * @return {Detector} New Detector object fully built
 */
function createDetector(
	id,
	category,
	media,
	realTime,
	url,
	otherOptions,
	initialize,
	extractEmotions,
	translateToPAD ) {
	const newDetector = new base.Detector(
		id, category, media, realTime, url, otherOptions );
	newDetector.initialize = initialize;
	newDetector.extractEmotions = extractEmotions;
	newDetector.translateToPAD = translateToPAD;
	return newDetector;
}

/**
 * Handles all the detectors, organized in categories under the same object
 * @constructs DetectorHandler
 * Under each key in its <tt>detectors</tt> attribute there is an array of <tt>Detector</tt> objects
 */
function DetectorHandler( ) {
	this.detectors = {};
}

/**
 * Add a new detector to the DetectorHandler object.
 * @function addDetector
 * @memberof DetectorHandler
 * @param {Object} detectorObj - Detector object. It is added unded the corresponding category.
 * After adding it, the benchmarking process begins.
 * This process also sets the <tt>delay</tt> and <tt>realTime</tt> attributes.
 */
DetectorHandler.prototype.addDetector = function( detectorObj ) {
	if ( this.detectors.hasOwnProperty( detectorObj.category ) ) {
		this.detectors[ detectorObj.category ].push( detectorObj );
	} else {
		this.detectors[ detectorObj.category ] = [ detectorObj ];
	}
	//If there are benchmark-files, the benchmarking task starts
	if ( fs.existsSync( __dirname + '\\detectors\\' + detectorObj.category +
			'\\benchmark-files' ) ) {
		fs.readdirSync( __dirname + '\\detectors\\' + detectorObj.category +
			'\\benchmark-files' ).forEach( function( fileName, index, array ) {
			let startTime = present( );
			const times = [ ];
			const callback = function( data ) {
				times.push( present( ) - startTime );
				startTime = present( );
				if ( index + 1 === array.length ) {
					detectorObj.delay = mean( times );
					detectorObj.realTime = detectorObj.delay < realTimeThreshold;
					detectorObj.cleanResults( );
				}
			};
			console.log( __dirname + '\\detectors\\' + detectorObj.category +
				'\\benchmark-files\\' + fileName );
			detectorObj.extractEmotions( detectorObj, __dirname + '\\detectors\\' + detectorObj.category +
				'\\benchmark-files\\' + fileName, callback );
		} );
	}
};

/**
 * @function setupDetectors
 * @memberof DetectorHandler
 * @param {Object} preferences - JSON object with the setting information (see
 * [/setup]{@link module:API~/setup})
 * @return {number} Number of filtered (removed) detectors.
 */
DetectorHandler.prototype.setupDetectors = function( preferences ) {
	let detectorsAffected = 0;
	if ( Object.keys( preferences ).length !== 0 ) {
		for ( const propFilter in preferences ) {
			//We use the filter method from DetectorHandler to filter any detector on every channel
			//that doesn't satisfy the requirements from the request's body
			switch ( propFilter ) {
				case 'type':
					//Filter out detectors whose category is not in the setup request
					detectorsAffected += this.filter(
						( det ) => preferences[ propFilter ].indexOf( det.category ) !== -1
					);
					break;
				case 'realTime':
					detectorsAffected += this.filter( ( det ) => det.realTime === preferences[ propFilter ] );
					break;
				case 'delay':
					detectorsAffected += this.filter( ( det ) => det.delay <= preferences[ propFilter ] );
					break;
				default:
					break;
			}
		}
	}
	return detectorsAffected;
};

/**
 * Analyse the media passed as an argument looking for affective data.
 * @function analyseMedia
 * @memberof DetectorHandler
 * @param {string} mediaType - Type of media passed. E.g. image, video, sound or text.
 * @param {Array} lookingFor - List of affective channels that must be analysed. E.g. face, voice, text, signal, etc.
 * @param {string} mediaPath - Path to the media file, local or remote.
 * @return {Promise<number>} Promise with the number of detectors that could attend the request.
 */
DetectorHandler.prototype.analyseMedia = function( mediaType, lookingFor, mediaPath ) {
	return new Promise( ( resolve, reject ) => {
		let analysisRequested = 0;
		for ( const category in this.detectors ) {
			if ( lookingFor.indexOf( category ) !== -1 ) {
				this.detectors[ category ].forEach( ( detector, index, detectorCategory ) => {
					if ( detector.media.indexOf( mediaType ) !== -1 ) {
						detector.extractEmotions( detector, mediaPath );
						analysisRequested++;
					}
				} );
				console.log( analysisRequested );
				resolve( analysisRequested );
			}
		}
		if ( analysisRequested === 0 ) {
			reject( 'No analysis requested' );
		}
	} );
};

/**
 * Remove a whole category of detectors
 * @function quitCategory
 * @memberof DetectorHandler
 * @param {Array|string} types - String of a single type or array of several types.
 * @return {number} Number of detectors deleted
 */
DetectorHandler.prototype.quitCategory = function( types ) {
	let affected = 0;
	//if types is a list of types, i.e. ['face', 'voice', 'physical', 'body']
	if ( Array.isArray( types ) ) {
		types.forEach( ( type ) => {
			affected += this.detectors[ type ].length;
			delete this.detectors[ type ];
		} );
		// if types is just a string, 'face'
	} else if ( typeof( types ) == 'string' ) {
		affected += this.detectors[ types ].length;
		delete this.detectors[ types ];
	}
	return affected;
};

/**
 * Filter all detectors by a certain criteria
 * @function filter
 * @memberof DetectorHandler
 * @param {Function} filteringFunction - Function to apply in a filter.
 * @return {number} Number of detectors filtered
 */
DetectorHandler.prototype.filter = function( filteringFunction ) {
	let affected = 0;
	for ( const category in this.detectors ) {
		const oLength = this.detectors[ category ].length;
		this.detectors[ category ] = this.detectors[ category ].filter( filteringFunction );
		affected += oLength - this.detectors[ category ].length;
		if ( this.detectors[ category ].length === 0 ) {
			delete this.detectors[ category ];
		}
	}
	return affected;
};

/**
 * Get results from a specific channel in a specific format
 * @function getChannelResults
 * @memberof DetectorHandler
 * @param {string} channel - Name of the channel from which the results are requested.
 * @param {string} resulsType - Format desired for the results. E.g. pad or raw.
 * @return {Array|Object} Array with the results or a object with a string holding the error message.
 */
DetectorHandler.prototype.getChannelResults = function( channel, resulsType ) {
	if ( this.detectors.hasOwnProperty( channel ) ) {
		return [
			this.detectors[ channel ].map( function( detector ) {
				const results = detector.getResults( resulsType );
				return results ? results : [ ];
			} )
		];
	} else {
		return {
			error: 'Non existing channel'
		};
	}
};

/**
 * Return all the detectors in a single array
 * @function getDetectors
 * @memberof DetectorHandler
 * @return {Array} Array with all the detectors in DetectorHandler.
 */
DetectorHandler.prototype.getDetectors = function( ) {
	return [ ].concat( ...Object.values( this.detectors ) );
};

/**
 * Return total number of detectors
 * @function lengthDetectors
 * @memberof DetectorHandler
 * @return {number} Total number of detectors.
 */
DetectorHandler.prototype.lengthDetectors = function( ) {
	return this.getDetectors( ).length;
};

/**
 * Return array of channels
 * @function getChannelsKeys
 * @memberof DetectorHandler
 * @return {Array} Array of channels
 */
DetectorHandler.prototype.getChannelsKeys = function( ) {
	return Object.keys( this.detectors );
};

/**
 * Return array of channels
 * @function getChannels
 * @memberof DetectorHandler
 * @param {Array} channelNames - Array of strings of channel names.
 * @return {Array.<Detector>} Array of channels (being a channel an array of Detector)
 */
DetectorHandler.prototype.getChannels = function( channelNames ) {
	if ( channelNames.length === 0 ) {
		return [ ];
	} else {
		const channelArray = channelNames.map( ( channelName ) => {
			if ( this.detectors.hasOwnProperty( channelName ) ) {
				return this.getChannelResults( channelName, 'pad' );
			} else {
				return [ ];
			}
		} );
		return channelArray;
	}
};

/**
 * Return detectors of a channel
 * @memberof DetectorHandler
 * @param {String} channelName - Name of a channel.
 * @return {Array.<Detector>} Array of channels (being a channel an array of Detector)
 */
DetectorHandler.prototype.getChannelDetectors = function( channelName ) {
	if ( this.detectors.hasOwnProperty( channelName ) ) {
		return this.detectors[ channelName ];
	} else {
		return [ ];
	}
};

/**
 * Merge results from specified channels
 * @memberof DetectorHandler
 * @param {String|Array} channel - String 'all' or array of channel names
 * @param {String} localStrategy - Name of local strategy. This strategy can be found in <code>tools/merge.js</code>
 * @param {String} globalStrategy - Name of global strategy. This strategy can be found in <code>tools/merge.js</code>
 * @return {Array} Triplet of aggregated data.
 */
DetectorHandler.prototype.mergeResults = function( channel, localStrategy, globalStrategy ) {
	let channelsToMerge = undefined;
	//TODO: comprobar si es un string y luego comprobar si es 'all' o el nombre
	//de un canal
	if ( channel === 'all' ) {
		//Return all available channels
		channelsToMerge = this.getChannelsKeys( );
	} else if ( typeof channel !== 'Array' ) {
		//channel can be either undefined or a string
		channelsToMerge = [ channel ];
	}
	//Map the array of channel names: ['face', 'voice', 'other', ...]
	//so every detector in every channel (contained in the array)
	//aggregates its results in just one triplet
	const channelMergedResults = channelsToMerge.map( ( channelName ) => {
		//Get the detectors of each channel
		const detectors = this.getChannelDetectors( channelName );
		//Each detector applies the strategy to his own result
		//We get an array of triplets per detector
		//The strategies can be undefined at this point, and this will be handled in the
		//./tool/merge.js file.
		const aggregatedDetectorResults = detectors.map( ( det ) => det.applyStrategy( localStrategy ) );
		return applyStrategy( localStrategy, aggregatedDetectorResults );
	} );
	//we apply the global strategy to these locally aggregated data
	return applyStrategy( globalStrategy, channelMergedResults );
};

module.exports.createDetector = createDetector;
module.exports.DetectorHandler = DetectorHandler;