text:
var SGWM =
/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId])
/******/ 			return installedModules[moduleId].exports;
/******/
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			exports: {},
/******/ 			id: moduleId,
/******/ 			loaded: false
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.loaded = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {

	/*
	    Main class (and entry point), unites all the fuctionality
	*/
	'use strict';

	const FixationProcessor = __webpack_require__(1);
	const splitToProgressions = __webpack_require__(4);
	const TextModel = __webpack_require__(6);
	const ProgressionMerger = __webpack_require__(7);
	const WordMapper = __webpack_require__(10);

	const FixationProcessorSettings = __webpack_require__(2);
	const ProgressionMergerSettings = __webpack_require__(9);
	const SplitToProgressionsSettings = __webpack_require__(5);
	const WordMapperSettings = __webpack_require__(11);

	class SGWM {
		constructor( logger ) {
	        this.logger = logger;
		}

		// Arguments:
		//	data ({
		//		fixations (Array of Fixations)
	    //          Fixation = {ts, x, y, duration}
		//		words (Aarray of Word)
	    //          Word = {x, y, width, height, text, row:optional=<line ID starting form 1>}
		//	})
	    map( data ) {
	        let fixations = data.fixations;
	        const words = data.words;

	        if (!fixations || !words) {
	            return;
	        }

	        const text = new TextModel( data.words );

	        fixations = fixations.map( (fixation, i) => Object.assign( { id: i }, fixation ) );

	        const fixationProcessor = new FixationProcessor( this.logger );
	    	fixations = fixationProcessor.filterByLocation( fixations, text.box );
	    	fixations = fixationProcessor.filterByDuration( fixations );

	    	const progressions = splitToProgressions( fixations, text.lineHeight, text.interlineDistance );

			const merger = new ProgressionMerger( text.interlineDistance, this.logger );
	    	const fixationLines = merger.merge( progressions, text.lines );

		    const wordMapper = new WordMapper( this.logger );
		    wordMapper.map( fixationLines, text.lines );
		    wordMapper.clean( fixations, text.words );

	    	return { fixations, text };
	    }

	    static get FixationProcessorSettings() { return FixationProcessorSettings; }
	    static get ProgressionMergerSettings() { return ProgressionMergerSettings; }
	    static get SplitToProgressionsSettings() { return SplitToProgressionsSettings; }
	    static get WordMapperSettings() { return WordMapperSettings; }
	}

	module.exports = SGWM;

/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {

	/*
	    Reprocesses or filter out fixatins based on their properties
	*/
	'use strict';

	const FixationProcessorSettings = __webpack_require__(2);

	const settings = new FixationProcessorSettings();

	let log = () => {}; // Function( module, ...messages )

	class FixationProcessor {
	    // Arguments:
	    //   logger ({ log(...) }): optional logger
	    constructor( logger ) {
	        settings.load();

	        if (logger) {
	            logger.log( '. . . . . . . . .' );
	            log = (...params) => {
	                logger.log( 'FixationProcessor   ', ...params );
	            };
	        }
	    }

	    // Arguments
	    //   fixations (Array of Fixation)
	    //   textbox {left, right, top, bottom}
	    // Returns
	    //   new array with original fixations (Array of Fixation)
	    filterByLocation( fixations, textbox ) {
	        if (!settings.location.enabled || !textbox) {
	            return fixations;
	        }

	        const result = [];

	        for (let i = 0; i < fixations.length; i += 1) {
	            const fixation = fixations[i];
	            if (fixation.x > textbox.left - settings.location.marginX &&
	                    fixation.x < textbox.right + settings.location.marginX &&
	                    fixation.y > textbox.top - settings.location.marginY &&
	                    fixation.y < textbox.bottom + settings.location.marginY) {
	                result.push( fixation );
	            }
	        }

	        return result;
	    }

	    // Arguments:
	    //   fixations (Array of Fixation)
	    // Returns
	    //   new array of fixations
	    //   (the merged fixations have property "merged" = <number of merged fixations>)
	    filterByDuration( fixations ) {
	        if (!settings.duration.enabled) {
	            return fixations;
	        }

	        let fixationCount;
	        let result = fixations;

	        do {
	            fixationCount = result.length;
	            result = joinOrDeleteShortFixations( result );
	        } while (result.length !== fixationCount);

	        return result;
	    }
	}

	// Calculated distance between 2 fixations
	function dist( a, b ) {
	    return Math.sqrt( Math.pow( a.x - b.x, 2 ) + Math.pow( a.y - b.y, 2 ) );
	}

	// Joins 2 fixations, saves the result to the first fixation,
	// and adds property "merged" = <number of merged fixations>
	function join( a, b ) {
	    const totalDuration = a.duration + b.duration;
	    a.x = (a.x * a.duration + b.x * b.duration) / totalDuration;
	    a.y = (a.y * a.duration + b.y * b.duration) / totalDuration;
	    a.duration = totalDuration;
	    a.merged = (a.merged || 1) + (b.merged || 1);
	    log( 'joined', b.id, 'to', a.id );
	    return a;
	}

	// Tries to join the fixation
	// Arguments:
	//      fixation (Fixation)
	//      prev (Fixation)
	//      next (Fixation)
	// Returns
	//      true if the fixation was joined and the original instance should be removed
	function tryJoinFixation( fixation, prev, next ) {
	    const distToPrev = prev ? dist( fixation, prev ) : Number.MAX_VALUE;
	    const distToNext = next ? dist( fixation, next ) : Number.MAX_VALUE;
	    if (distToPrev < settings.duration.mergingDistanceThreshold || distToNext < settings.duration.mergingDistanceThreshold) {
	        if (distToNext < distToPrev) {
	            join( next, fixation );
	        }
	        else {
	            join( prev, fixation );
	        }
	        return true;
	    }
	    else if (fixation.duration < settings.duration.removingDurationThreshold) {
	        log( 'removed', fixation.id );
	        return true;
	    }

	    return false;
	}

	// Cycle all fixations and joins or deletes too short
	// Returns
	//   new sequence of fixations (Array of Fixation)
	function joinOrDeleteShortFixations( fixations ) {
	    const result = [];

	    let prevFix, prevPrevFix;
	    for (let i = 0; i < fixations.length; i += 1) {
	        const fixation = fixations[i];

	        if (prevFix && prevFix.duration < settings.duration.mergingDurationThreshold ) {
	            if (tryJoinFixation( prevFix, prevPrevFix, fixation )) {
	                result.pop();
	                prevFix = prevPrevFix;
	            }
	        }

	        result.push( fixation );

	        prevPrevFix = prevFix;
	        prevFix = fixation;
	    }

	    if (prevFix.duration < settings.duration.mergingDurationThreshold) {
	        if (tryJoinFixation( prevFix, prevPrevFix, null )) {
	          result.pop();
	        }
	    }

	    return result;
	}

	module.exports = FixationProcessor;

/***/ },
/* 2 */
/***/ function(module, exports, __webpack_require__) {

	'use strict';

	const Settings = __webpack_require__(3);

	class FixationProcessorSettings extends Settings {
		constructor() {
			super( 'fixationProcessorSettings' );
			this._location = {
				enabled: false,
				marginX: 200,
				marginY: 200
			};
			this._duration = {
				enabled: false,
				mergingDistanceThreshold: 40,
				mergingDurationThreshold: 100,
				removingDurationThreshold: 80
			};

			super.load();
		}

		get location() { return this._location; }
		set location( value ) { this._location = value; }
		get duration() { return this._duration; }
		set duration( value ) { this._duration = value; }
	}

	module.exports = FixationProcessorSettings;

/***/ },
/* 3 */
/***/ function(module, exports) {

	/*
		Base class for settings
	*/
	'use strict';

	class Settings {
		// Arguments:
		//	 name (string): settings own namespace (without domain which is defined in the class)
		constructor( name ) {
			this._name = name;
			this._domain = 'sgwm';
			this._isInitialized = false;

			this._fullPath = function( name ) {
				return [ this._name, name ].join( '_' );
			};
		}

		load() {
			const hiddenProps = Object.keys(new Settings(''));
			const allSettings = JSON.parse( localStorage.getItem( this._domain ) );
			this._isInitialized = allSettings && allSettings[ this._name ];

			for (let p in this) {
				if (hiddenProps.indexOf( p ) > -1) {
					continue;
				}
				const value = allSettings ? allSettings[ this._fullPath( p ) ] : null;
				if (value !== null) {
					this[p] = value;
				}
			}
		}

		save() {
			const hiddenProps = Object.keys(new Settings(''));
			const allSettings = JSON.parse( localStorage.getItem( this._domain ) ) || {};

			for (let p in this) {
				if (hiddenProps.indexOf( p ) > -1) {
					continue;
				}
				allSettings[ this._fullPath( p ) ] = this[p];
			}

			localStorage.setItem( this._domain, JSON.stringify( allSettings ) );
		}

		get isInitialized() { return this._isInitialized; }
	}

	module.exports = Settings;

/***/ },
/* 4 */
/***/ function(module, exports, __webpack_require__) {

	/*
		Create a set of chunks of progressive reading fixations
	*/
	'use strict';

	const SplitToProgressionsSettings = __webpack_require__(5);

	const settings = new SplitToProgressionsSettings();
	let progressionBox;

	const isProgressiveReadingSaccade = function( dx, dy ) {
		return (
			dx > progressionBox.left &&
			dx < progressionBox.right &&
			Math.abs( dy ) < progressionBox.vertical( dx )
		);
	};

	// Arguments:
	//   fixations (Array of Fixation)
	//   lineHeight (Number): word box height in pixels
	// Returns:
	//   A set of progresions which are the sequences of fixations (Array of (Array of Fixation))
	// Note:
	//   Fixations in the output are referencing to the fixations in the input (no copying)
	module.exports = function( fixations, lineHeight, interlineDistance ) {
	    settings.load();
	    progressionBox = settings.pixelBounds( lineHeight, interlineDistance );

	    const result = [];
	    let currentProgression;

	    const startNewProgression = function( fixation ) {
	        currentProgression = [ fixation ];
	        result.push( currentProgression );
	    };

	    let lastFix = fixations[0];
	    startNewProgression( lastFix );

	    for (let i = 1; i < fixations.length; i += 1) {
	        const fixation = fixations[i];
	        if (!isProgressiveReadingSaccade( fixation.x - lastFix.x, fixation.y - lastFix.y )) {
	            startNewProgression( fixation );
	        }
	        else {
	            currentProgression.push( fixation );
	        }
	        lastFix = fixation;
	    }

	    return result;
	};

/***/ },
/* 5 */
/***/ function(module, exports, __webpack_require__) {

	'use strict';

	const Settings = __webpack_require__(3);

	class ProgressionSplitterSettings extends Settings {
		constructor() {
			super( 'progressionSplitterSettings' );
			this._bounds = {
				left: -0.5,
				right: 10,
				verticalChar: 1.5,	// in characters
				verticalLine: 0.65,	// in interline distances
			};
			this._angle = Math.sin( 15 * Math.PI / 180 );

			super.load();
		}

		get bounds() { return this._bounds; }
		set bounds( value ) { this._bounds = value; }
		get angle() { return this._angle; }
		set angle( value ) { this._angle = value; }
		get left() { return this._bounds.left; }
		set left( value ) { this._bounds.left = value; }
		get right() { return this._bounds.right; }
		set right( value ) { this._bounds.right = value; }
		get verticalLine() { return this._bounds.verticalLine; }
		set verticalLine( value ) { this._bounds.verticalLine = value; }

		pixelBounds( lineHeight, interlineDistance ) {
			const vertical = Math.min(
				this._bounds.verticalChar * lineHeight,
				this._bounds.verticalLine * interlineDistance
			);

			return {
				left: this._bounds.left * lineHeight,
				right: this._bounds.right * lineHeight,
				vertical: dx => vertical + dx * this._angle,
				_vertical: vertical
			};
		}
	}

	module.exports = ProgressionSplitterSettings;

/***/ },
/* 6 */
/***/ function(module, exports) {

	/*
		Creates an array lines, each as array of word boxes
	*/
	'use strict';

	class TextModel {
		// Arguments
		//	 words (Array of {x, y, width, height, text, row:optional=<line ID starting form 1>})
	    // Notes:
	    //   1. each word gets property "id" = <index in the text>
	    //   2. the words in "lines" and "wrods" properties are copied (and shared between these two properties)
		constructor( words ) {
	        const lines = [];
	        const wordList = [];
	        const box = {
	        	left: Number.MAX_VALUE,
	        	top: Number.MAX_VALUE,
	        	right: 0,
	        	bottom: 0
	        };

	        let currentLine;

	        const createNewLine = function( word ) {
	            currentLine = [ word ];
	            currentLine.id = word.row === undefined ? lines.length : word.row - 1;
	            currentLine.y = word.y;
	            lines.push( currentLine );
	        };

	        let currentY = Number.MIN_VALUE;
	        for (let i = 0; i < words.length; i += 1) {
	            const word = Object.assign( {}, words[i] );
	            wordList.push( word );

	            word.id = i;
	            if (word.x < box.left) { box.left = word.x; }
	            if (word.y < box.top) { box.top = word.y; }
	            if (word.x + word.width > box.right) { box.right = word.x + word.width; }
	            if (word.y + word.height > box.bottom) { box.bottom = word.y + word.height; }

	            if (word.y != currentY) {
	                currentY = word.y;
	                createNewLine( word );
	            }
	            else {
	                currentLine.push( word );
	            }
	        }

	        this._box = box;
	        this._lines = lines;
	        this._words = wordList;

	        this._lineHeight = lines[0][0].height;
	        this._interlineDistance = getInterlineDistance( lines );
		}

		get box() { return this._box; }
		get lines() { return this._lines; }
	    get words() { return this._words; }
		get lineHeight() { return this._lineHeight;	}
	    get interlineDistance() { return this._interlineDistance; }
	}

	function getInterlineDistance( lines ) {
	    let interlineDist = 9;
	    if (lines.length > 1) {
	        const interlineDists = [];
	        for (let i = 1; i < lines.length; i += 1) {
	            interlineDists.push( lines[i].y - lines[i - 1].y );
	        }
	        interlineDist = median( interlineDists );
	        /*/
	        for (let i = 1; i < textLines.length; i += 1) {
	            interlineDist += textLines[i].y - textLines[i - 1].y;
	        }
	        interlineDist = interlineDist / (textLines.length - 1);
	        */
	    }
	    else {
	        interlineDist = Number.MAX_VALUE;
	    }

	    return interlineDist;
	}

	function median( array ) {
	    if (array.length <= 5) {
	        return array[ Math.floor( array.length / 2 ) ];
	    }

	    const sets = new Array( Math.floor( array.length / 5 ) + (array.length % 5 ? 1 : 0) );
	    for (let i = 0; i < sets.length; i+=1) {
	        sets[i] = [];
	    }
	    for (let i = 0; i < array.length; i+=1) {
	        sets[ Math.floor( i / 5 ) ].push( array[i] );
	    }

	    let medians = [];
	    sets.forEach( set => {
	        set.sort( (a, b) => {
	            return a - b;
	        });
	        medians.push( set[ Math.floor( set.length / 2 ) ] );
	    });

	    return median( medians );
	}

	module.exports = TextModel;

/***/ },
/* 7 */
/***/ function(module, exports, __webpack_require__) {

	/*
	    Merges progressive reading fixations within the text lines
	*/
	'use strict';

	const regression = __webpack_require__(8);
	const ProgressionMergerSettings = __webpack_require__(9);

	const settings = new ProgressionMergerSettings();

	const SET_TYPE = {
	    LONG: 1,
	    SHORT: 2,
	    ANY: 3
	};

	let interlineDistance = 30;

	let log = () => {}; // Function( module, ...messages )

	class ProgressionMerger {
	    // Arguments:
	    //   interlineDistance (Number): inter-line distance in pixels
	    //   logger ({ log(...) }): optional logger
	    constructor( _interlineDistance, logger ) {
	        interlineDistance = _interlineDistance;

	        settings.load();
	        settings.fitThreshold *= interlineDistance;

	        if (logger) {
	            logger.log( '. . . . . . . . .' );
	            log = (...params) => {
	                logger.log( 'ProgressionMerger   ', ...params );
	            };
	        }
	    }

	    // Arguments:
	    //   progressions (Array of (Array of Fixation))
	    //   textLines (Array of (Array of word))
	    // Returns:
	    //   new sorted array of original and merged progressions (Array of (Array of Fixation))
	    // Notes:
	    //   1. Fixations get property "line", the index of line they land onto.
	    //   2. Merged sets have property "joined" = <number of joined progressions>
	    //   3. Progressions not included in the resulting array and not merged with other have property "removed"
	    merge( progressions, textLines ) {
	        const lineCount = textLines.length;

	        let result = progressions.map( set => set );
	        log( '#0:', result.length, '\n', result.map( set => (set.map( fix => fix.id ))) );

	        // 1. join only long sets
	        result = joinSetsOfType( result, lineCount, SET_TYPE.LONG, SET_TYPE.LONG, settings.minLongSetLength );
	        log( '#1a:', result.length );

	        // 2. join short sets with long sets
	        result = joinSetsOfType( result, lineCount, SET_TYPE.SHORT, SET_TYPE.LONG, settings.minLongSetLength );
	        log( '#1b:', result.length );

	        // 3. join the remaining single-fixation sets with multi-fixation sets
	        const multiFixationSetLength = 2;
	        result = joinSetsOfType( result, lineCount, SET_TYPE.SHORT, SET_TYPE.LONG, multiFixationSetLength );
	        log( '#1c:', result.length );

	        if (result.length > lineCount) {
	            // still too many: join any multi-fixation set with any other multi-fixation set
	            result = joinSetsOfType( result, lineCount, SET_TYPE.LONG, SET_TYPE.LONG, multiFixationSetLength );
	            log( '#2:', result.length );
	        }
	        else if (settings.removeSingleFixationLines) {
	            result = dropShortSets( result, 2 );
	        }

	        if (result.length > lineCount) {
	            // and still too many...
	            // drop short sets
	            result = dropShortSets( result, settings.minLongSetLength );
	            log( '#3a:', result.length );
	        // }

	        // if (result.length > lineCount) {
	            // and still too many...
	            // then force joining the closest sets
	            result = joinSetsOfType( result, lineCount, SET_TYPE.ANY, SET_TYPE.ANY );
	            log( '#3b:', result.length );
	        }

	        align( result, textLines );

	        return result;
	    }
	}

	function joinSetsOfType( fixationsSets, lineCount, primarySetType, secondarySetType, minLongSetLength ) {
	    let result = fixationsSets;
	    const forced = primarySetType === SET_TYPE.ANY && secondarySetType === SET_TYPE.ANY;

	    while (result.length > lineCount) {
	        const pairs = createPairs( result, primarySetType, secondarySetType, minLongSetLength );
	        const updatedSets = findAndJoinClosestPair( result, pairs, forced );

	        if (!updatedSets) {
	            break;
	        }

	        result = updatedSets;
	    }

	    return result;
	}

	/**********************
	    createPairs
	**********************/

	function isValidSet( set, lengthType, lengthTypeThreshold) {
	    if (lengthType === SET_TYPE.LONG && set.length < lengthTypeThreshold) {
	        return false;
	    }
	    else if (lengthType === SET_TYPE.SHORT && set.length >= lengthTypeThreshold) {
	        return false;
	    }
	    return true;
	}

	// function getFittingError( fixations, model ) {
	//     let error = 0;

	//     for (let i = 0; i < fixations.length; i += 1) {
	//         const fixation = fixations[i];
	//         const y = regression.fit( model, fixation.x );
	//         error += (fixation.y - y) * (fixation.y - y);
	//     }

	//     return Math.sqrt( error / fixations.length );
	// }

	function fixationsToArray( fixations ) {
	    const result = [];
	    for (let i = 0; i < fixations.length; i += 1) {
	        const fixation = fixations[i];
	        result.push([ fixation.x, fixation.y ]);
	    }
	    return result;
	}

	function getFitError( fixations ) {
	    const model = regression.model( 'linear', fixationsToArray( fixations ) );

	    let error = 0;

	    for (let i = 0; i < fixations.length; i += 1) {
	        const fixation = fixations[i];
	        const y = regression.fit( model.equation, fixation.x );
	        error += (fixation.y - y) * (fixation.y - y);
	    }

	    return Math.sqrt( error / fixations.length );
	}

	function createPairs( fixationsSets, primarySetType, secondarySetType, setSetTypeThreshold) {
	    const pairs = [];

	    for (let i = 0; i < fixationsSets.length; i += 1) {
	        const set1 = fixationsSets[i];
	        if (!isValidSet( set1, primarySetType, setSetTypeThreshold )) {
	            continue;
	        }

	        // Compute error of fitting to linear model for each pair of sets
	        for (let j = 0; j < fixationsSets.length; j += 1) {
	            if (i === j) {
	                continue;
	            }

	            const set2 = fixationsSets[j];
	            if (!isValidSet( set2, secondarySetType, setSetTypeThreshold )) {
	                continue;
	            }

	            const joinedSets = set1.concat( set2 );

	            pairs.push({
	                set1: i,
	                set2: j,
	                error: getFitError( joinedSets )
	            });
	        }
	    }

	    return pairs;
	}

	/**********************
	    findAndJoinClosestPair
	**********************/

	function joinSets( fixationsSets, id1, id2, forced ) {
	    const maxGradient = forced ? Number.MAX_VALUE : settings.maxLinearGradient;

	    const set1 = fixationsSets[ id1 ];
	    const set2 = fixationsSets[ id2 ];
	    const joinedSet = set1.concat( set2 );
	    joinedSet.joined = (set1.joined || 1) + (set2.joined || 1);

	    const model = regression.model( 'linear', fixationsToArray( joinedSet ) );
	    const gradient = model.equation[1];

	    if (Math.abs( gradient ) < maxGradient) {
	        const minIndex = Math.min( id1, id2 );
	        const maxIndex = Math.max( id1, id2 );

	        fixationsSets.splice( maxIndex, 1 );
	        fixationsSets.splice( minIndex, 1 );
	        fixationsSets.push( joinedSet );

	        log( 'joined', id1 + ' = ' + set1.map( fix => fix.id), ' & ', id2 + ' = ' + set2.map( fix => fix.id) );
	        log( '--->\n', fixationsSets.map( set => set.map( fix => fix.id ) ) );
	        return true;
	    }

	    return false;
	}

	function findAndJoinClosestPair( fixationsSets, pairs, forced ) {
	    let result;

	    const fitThreshold = forced ? Number.MAX_VALUE : settings.fitThreshold;

	    // holds pairs that produce too inclined set
	    const invalidPairs = {};

	    do {
	        // find 2 nearest sets, i.e. the pair with smallest error
	        let minError = Number.MAX_VALUE;
	        let minIndex = -1;
	        for (let i = 0; i < pairs.length; i += 1) {
	            if (invalidPairs[i]) {
	                continue;
	            }
	            const pair = pairs[i];
	            if (pair.error < minError) {
	                minIndex = i;
	                minError = pair.error;
	            }
	        }

	        // if found, try to join them
	        if (minIndex >= 0 && minError < fitThreshold) {
	            const pair = pairs[ minIndex ];
	            const success = joinSets( fixationsSets, pair.set1, pair.set2, forced );
	            if (success) {
	                result = fixationsSets;
	            }
	            else {
	                invalidPairs[ minIndex ] = true;
	            }
	        }
	        else {
	            result = null;
	        }

	        // break only when
	        //  - all pairs have too distant components, or
	        //  - all pairs have very distinctly inclined set of fixations, or
	        //  - a pair of the closest sets was joined
	    } while (result === undefined);

	    return result;
	}

	/**********************
	    align
	**********************/
	function avgY( fixations ) {
	    let sumY = 0;
	    for (let i = 0; i < fixations.length; i += 1) {
	        sumY += fixations[i].y;
	    }
	    return sumY / fixations.length;
	}

	function sortSets( fixationsSets ) {
	    fixationsSets.sort( (set1, set2) => {
	        return avgY( set1 ) - avgY( set2 );
	    });
	    fixationsSets.forEach( set => {
	        set.sort( (fix1, fix2) => {
	            return fix1.ts - fix2.ts;
	        });
	    });
	}

	function computeRange( set, getValue ) {
	    if (!set) {
	        return { min: 0, max: 0, range: 0 };
	    }

	    let min = Number.MAX_SAFE_INTEGER;
	    let max = Number.MIN_SAFE_INTEGER;
	    set.forEach( fixation => {
	        const value = getValue( fixation );
	        if (min > value) {
	            min = value;
	        }
	        if (max < value) {
	            max = value;
	        }
	    });

	    return { min, max, range: max - min };
	}

	function getInitialLine( fixationsSets, textLines ) {
	    if (!settings.intelligentFirstLineMapping || textLines.length < 2) {
	        return 0;
	    }

	    const firstSetLength = computeRange( fixationsSets[0], fixation => fixation.x ).range;
	    const lineLengths = [];
	    textLines.forEach( line => {
	        lineLengths.push( computeRange( line, word => word.x ).range + line[ line.length - 1 ].width );
	    });

	    let lineID = 0;

	    let ratio = lineLengths[ lineID ] / firstSetLength;
	    let threshold = lineLengths[ lineID ] / lineLengths[ lineID + 1 ];
	    while (threshold < 0.7 && ratio < ((threshold + 1) / 2) && lineID <= lineLengths.length / 2 ) {
	        lineID++;
	        ratio = lineLengths[ lineID ] / firstSetLength;
	        threshold = lineLengths[ lineID ] / lineLengths[ lineID + 1 ];
	    }

	    return lineID;
	}

	function align( fixationsSets, textLines ) {

	    sortSets( fixationsSets );

	    const { min: minID, max: maxID } = computeRange( textLines, line => line.id );

	    let currentLineID = getInitialLine( fixationsSets, textLines );
	    let lastSetY;
	    // let lastLineY;

	    for (let i = 0; i < fixationsSets.length; i += 1) {
	        const fixations = fixationsSets[i];
	        const currentSetY = avgY( fixations );
	        // let currentLineY;

	        const initialLineID = currentLineID;
	        if (settings.correctForEmptyLines && i > 0) {
	            /*
	            while (currentLineID < textLines.length) {
	                currentLineY = textLines[ currentLineID ].y;

	                const setDist = currentSetY - lastSetY;
	                const lineDist = currentLineY - lastLineY;

	                if (setDist < settings.emptyLineDetectorFactor * lineDist) {
	                    break;
	                }

	                currentLineID += 1;
	            }
	            */
	            const lineIDsFromMappedSets = [];
	            for (let j = 0; j < i; j += 1) {
	                const prevSet = fixationsSets[j];
	                if (prevSet[0].line === undefined) {
	                    continue;
	                }

	                const prevSetY = avgY( prevSet );
	                lineIDsFromMappedSets.push( prevSet[0].line + (currentSetY - prevSetY) / interlineDistance );
	            }

	            if (lineIDsFromMappedSets.length) {
	                let avgID = lineIDsFromMappedSets.reduce( (acc, id) => (acc + id), 0 ) / lineIDsFromMappedSets.length;
	                if (avgID < currentLineID) {
	                    avgID += settings.currentLineSupportInCorrection;   // if between prev and current line, then support more the current line, thatn the previous
	                }
	                currentLineID = Math.min( maxID, Math.max( minID, Math.round( avgID ) ) );
	            }
	            else {
	                currentLineID += 1;
	            }
	        }
	        // else {
	        //     currentLineY = textLines[ currentLineID ].y;
	        // }

	        if (initialLineID !== currentLineID) {
	            log( `Line advanced: #${initialLineID} => ${currentLineID}` );
	        }

	        for (let j = 0; j < fixations.length; j += 1) {
	            fixations[j].line = currentLineID;
	        }

	        // lastLineY = currentLineY;
	        lastSetY = currentSetY;

	        currentLineID += 1;
	        // if (currentLineID >= textLines.length) {
	        //     break;
	        // }

	        // currentLineY = textLines[ currentLineID ].y;
	    }
	// fixationsSets.forEach( (p, pi) => {
	//     console.log('--',pi,'--');
	//     console.log( p.map( (f, fi) => { return f.line; } ).join(' ') ) ;
	// });
	}

	/**********************
	    dropShortSets
	**********************/
	function dropShortestSet( fixationSets, minSetLength ) {
	    const shortest = fixationSets.reduce(( acc, set, index ) => {
	        if (set.length < acc.length) {
	            return {
	                index: index,
	                length: acc.length
	            };
	        }
	        else {
	            return acc;
	        }
	    }, {index: -1, length: 100} );

	    if (shortest.index >= 0 && shortest.length < minSetLength) {
	        fixationSets.splice( shortest.index, 1 );
	    }

	    return fixationSets;
	}

	function dropAllShortSets( fixationSets, minSetLength ) {
	    const result = [];

	    for (let i = 0; i < fixationSets.length; i += 1) {
	        const fixationSet = fixationSets[i];
	        if (fixationSet.length >= minSetLength) {
	            result.push( fixationSet );
	        }
	        else {
	            fixationSet.removed = true;
	        }
	    }

	    return result;
	}

	function dropShortSets( fixationSets, minSetLength, minSetsCount ) {
	    if (minSetsCount === undefined) {
	        return dropAllShortSets( fixationSets, minSetLength );
	    }
	    else {
	        let result = fixationSets;
	        while (result.length > minSetsCount ) {
	            const shortened = dropShortestSet( result, minSetLength );
	            if (shortened.length === result.length) {
	                break;
	            }
	            result = shortened;
	        }
	        return result;
	    }
	}

	module.exports = ProgressionMerger;

/***/ },
/* 8 */
/***/ function(module, exports, __webpack_require__) {

	/**
	* @license
	*
	* Regression.JS - Regression functions for javascript
	* http://tom-alexander.github.com/regression-js/
	*
	* copyright(c) 2013 Tom Alexander
	* Licensed under the MIT license.
	*
	**/
	/* jshint ignore:start */
	;(function() {
	    'use strict';

	    var gaussianElimination = function(a, o) {
	           var i = 0, j = 0, k = 0, maxrow = 0, tmp = 0, n = a.length - 1, x = new Array(o);
	           for (i = 0; i < n; i++) {
	              maxrow = i;
	              for (j = i + 1; j < n; j++) {
	                 if (Math.abs(a[i][j]) > Math.abs(a[i][maxrow]))
	                    maxrow = j;
	              }
	              for (k = i; k < n + 1; k++) {
	                 tmp = a[k][i];
	                 a[k][i] = a[k][maxrow];
	                 a[k][maxrow] = tmp;
	              }
	              for (j = i + 1; j < n; j++) {
	                 for (k = n; k >= i; k--) {
	                    a[k][j] -= a[k][i] * a[i][j] / a[i][i];
	                 }
	              }
	           }
	           for (j = n - 1; j >= 0; j--) {
	              tmp = 0;
	              for (k = j + 1; k < n; k++)
	                 tmp += a[k][j] * x[k];
	              x[j] = (a[n][j] - tmp) / a[j][j];
	           }
	           return (x);
	    };

	    var methods = {
	        linear: function(data) {
	            var sum = [0, 0, 0, 0, 0], n = 0, results = [];

	            for (; n < data.length; n++) {
	              if (data[n][1] != null) {
	                sum[0] += data[n][0];
	                sum[1] += data[n][1];
	                sum[2] += data[n][0] * data[n][0];
	                sum[3] += data[n][0] * data[n][1];
	                sum[4] += data[n][1] * data[n][1];
	              }
	            }

	            var gradient = (n * sum[3] - sum[0] * sum[1]) / (n * sum[2] - sum[0] * sum[0]);
	            var intercept = (sum[1] / n) - (gradient * sum[0]) / n;
	          //  var correlation = (n * sum[3] - sum[0] * sum[1]) / Math.sqrt((n * sum[2] - sum[0] * sum[0]) * (n * sum[4] - sum[1] * sum[1]));

	            for (var i = 0, len = data.length; i < len; i++) {
	                var coordinate = [data[i][0], data[i][0] * gradient + intercept];
	                results.push(coordinate);
	            }

	            var string = 'y = ' + Math.round(gradient*100) / 100 + 'x + ' + Math.round(intercept*100) / 100;

	            return {equation: [intercept, gradient], points: results, string: string};
	        },

	        linearThroughOrigin: function(data) {
	            var sum = [0, 0], n = 0, results = [];

	            for (; n < data.length; n++) {
	                if (data[n][1] != null) {
	                    sum[0] += data[n][0] * data[n][0]; //sumSqX
	                    sum[1] += data[n][0] * data[n][1]; //sumXY
	                }
	            }

	            var gradient = sum[1] / sum[0];

	            for (var i = 0, len = data.length; i < len; i++) {
	                var coordinate = [data[i][0], data[i][0] * gradient];
	                results.push(coordinate);
	            }

	            var string = 'y = ' + Math.round(gradient*100) / 100 + 'x';

	            return {equation: [0, gradient], points: results, string: string};
	        },

	        exponential: function(data) {
	            var sum = [0, 0, 0, 0, 0, 0], n = 0, results = [];

	            for (len = data.length; n < len; n++) {
	              if (data[n][1] != null) {
	                sum[0] += data[n][0];
	                sum[1] += data[n][1];
	                sum[2] += data[n][0] * data[n][0] * data[n][1];
	                sum[3] += data[n][1] * Math.log(data[n][1]);
	                sum[4] += data[n][0] * data[n][1] * Math.log(data[n][1]);
	                sum[5] += data[n][0] * data[n][1];
	              }
	            }

	            var denominator = (sum[1] * sum[2] - sum[5] * sum[5]);
	            var A = Math.pow(Math.E, (sum[2] * sum[3] - sum[5] * sum[4]) / denominator);
	            var B = (sum[1] * sum[4] - sum[5] * sum[3]) / denominator;

	            for (var i = 0, len = data.length; i < len; i++) {
	                var coordinate = [data[i][0], A * Math.pow(Math.E, B * data[i][0])];
	                results.push(coordinate);
	            }

	            var string = 'y = ' + Math.round(A*100) / 100 + 'e^(' + Math.round(B*100) / 100 + 'x)';

	            return {equation: [A, B], points: results, string: string};
	        },

	        logarithmic: function(data) {
	            var sum = [0, 0, 0, 0], n = 0, results = [];

	            for (len = data.length; n < len; n++) {
	              if (data[n][1] != null) {
	                sum[0] += Math.log(data[n][0]);
	                sum[1] += data[n][1] * Math.log(data[n][0]);
	                sum[2] += data[n][1];
	                sum[3] += Math.pow(Math.log(data[n][0]), 2);
	              }
	            }

	            var B = (n * sum[1] - sum[2] * sum[0]) / (n * sum[3] - sum[0] * sum[0]);
	            var A = (sum[2] - B * sum[0]) / n;

	            for (var i = 0, len = data.length; i < len; i++) {
	                var coordinate = [data[i][0], A + B * Math.log(data[i][0])];
	                results.push(coordinate);
	            }

	            var string = 'y = ' + Math.round(A*100) / 100 + ' + ' + Math.round(B*100) / 100 + ' ln(x)';

	            return {equation: [A, B], points: results, string: string};
	        },

	        power: function(data) {
	            var sum = [0, 0, 0, 0], n = 0, results = [];

	            for (len = data.length; n < len; n++) {
	              if (data[n][1] != null) {
	                sum[0] += Math.log(data[n][0]);
	                sum[1] += Math.log(data[n][1]) * Math.log(data[n][0]);
	                sum[2] += Math.log(data[n][1]);
	                sum[3] += Math.pow(Math.log(data[n][0]), 2);
	              }
	            }

	            var B = (n * sum[1] - sum[2] * sum[0]) / (n * sum[3] - sum[0] * sum[0]);
	            var A = Math.pow(Math.E, (sum[2] - B * sum[0]) / n);

	            for (var i = 0, len = data.length; i < len; i++) {
	                var coordinate = [data[i][0], A * Math.pow(data[i][0] , B)];
	                results.push(coordinate);
	            }

	             var string = 'y = ' + Math.round(A*100) / 100 + 'x^' + Math.round(B*100) / 100;

	            return {equation: [A, B], points: results, string: string};
	        },

	        polynomial: function(data, order) {
	            if(typeof order == 'undefined'){
	                order = 2;
	            }
	             var lhs = [], rhs = [], results = [], a = 0, b = 0, i = 0, k = order + 1;

	                    for (; i < k; i++) {
	                       for (var l = 0, len = data.length; l < len; l++) {
	                          if (data[l][1] != null) {
	                           a += Math.pow(data[l][0], i) * data[l][1];
	                          }
	                        }
	                        lhs.push(a), a = 0;
	                        var c = [];
	                        for (var j = 0; j < k; j++) {
	                           for (var l = 0, len = data.length; l < len; l++) {
	                              if (data[l][1] != null) {
	                               b += Math.pow(data[l][0], i + j);
	                              }
	                            }
	                            c.push(b), b = 0;
	                        }
	                        rhs.push(c);
	                    }
	            rhs.push(lhs);

	           var equation = gaussianElimination(rhs, k);

	                for (var i = 0, len = data.length; i < len; i++) {
	                    var answer = 0;
	                    for (var w = 0; w < equation.length; w++) {
	                        answer += equation[w] * Math.pow(data[i][0], w);
	                    }
	                    results.push([data[i][0], answer]);
	                }

	                var string = 'y = ';

	                for(var i = equation.length-1; i >= 0; i--){
	                  if(i > 1) string += Math.round(equation[i] * Math.pow(10, i)) / Math.pow(10, i)  + 'x^' + i + ' + ';
	                  else if (i == 1) string += Math.round(equation[i]*100) / 100 + 'x' + ' + ';
	                  else string += Math.round(equation[i]*100) / 100;
	                }

	            return {equation: equation, points: results, string: string};
	        },

	        lastvalue: function(data) {
	          var results = [];
	          var lastvalue = null;
	          for (var i = 0; i < data.length; i++) {
	            if (data[i][1]) {
	              lastvalue = data[i][1];
	              results.push([data[i][0], data[i][1]]);
	            }
	            else {
	              results.push([data[i][0], lastvalue]);
	            }
	          }

	          return {equation: [lastvalue], points: results, string: "" + lastvalue};
	        }
	    };

	    var regression = {

	      model: function(method, data, order) {

	        if (typeof method == 'string') {
	          return methods[method](data, order);
	        }
	      },

	      fit: function (equation, x) {
	        var result = 0;
	        var value = 1;

	        if (!equation) {
	          return Number.MAX_VALUE;
	        }

	        for (var i = 0; i < equation.length; ++i) {
	          result += equation[i] * value;
	          value *= x;
	        }

	        return result;
	      }
	    };

	    if (true) {
	        module.exports = regression;
	    } else {
	        window.regression = regression;
	    }

	}());
	/* jshint ignore:end */

/***/ },
/* 9 */
/***/ function(module, exports, __webpack_require__) {

	'use strict';

	const Settings = __webpack_require__(3);

	class ProgressionMergerSettings extends Settings {
		constructor() {
			super( 'progressionMergerSettings' );

			this._minLongSetLength = 3; 	// the minimal length of "long" set
			this._fitThreshold = 0.3;		// fraction of the average inter-line distance
			this._maxLinearGradient = 0.15; // the maximum difference in equation gradients for fixations that can be joined
			this._removeSingleFixationLines = false;
			this._correctForEmptyLines = true;
			this._currentLineSupportInCorrection = 0.0;
			this._emptyLineDetectorFactor = 1.7;	// multiplier to interlineDistance
			this._intelligentFirstLineMapping = false;	// if false, then it  assumes the reading always start from the first line

			super.load();
		}

		get minLongSetLength() { return this._minLongSetLength; }
		set minLongSetLength( value ) { this._minLongSetLength = value; }
		get fitThreshold() { return this._fitThreshold; }
		set fitThreshold( value ) { this._fitThreshold = value; }
		get maxLinearGradient() { return this._maxLinearGradient; }
		set maxLinearGradient( value ) { this._maxLinearGradient = value; }
		get removeSingleFixationLines() { return this._removeSingleFixationLines; }
		set removeSingleFixationLines( value ) { this._removeSingleFixationLines = value; }
		get correctForEmptyLines() { return this._correctForEmptyLines; }
		set correctForEmptyLines( value ) { this._correctForEmptyLines = value; }
		get currentLineSupportInCorrection() { return this._currentLineSupportInCorrection; }
		set currentLineSupportInCorrection( value ) { this._currentLineSupportInCorrection = value; }
		get emptyLineDetectorFactor() { return this._emptyLineDetectorFactor; }
		set emptyLineDetectorFactor( value ) { this._emptyLineDetectorFactor = value; }
		get intelligentFirstLineMapping() { return this._intelligentFirstLineMapping; }
		set intelligentFirstLineMapping( value ) { this._intelligentFirstLineMapping = value; }
	}

	module.exports = ProgressionMergerSettings;

/***/ },
/* 10 */
/***/ function(module, exports, __webpack_require__) {

	/*
		Maps fixations to words within a line
	*/
	'use strict';

	const WordMapperSettings = __webpack_require__(11);

	const settings = new WordMapperSettings();

	let log = () => {}; // Function( module, ...messages )

	class WordMapper {
	    // Arguments:
	    //   logger ({ log(...) }): optional logger
	    constructor( logger ) {
	        settings.load();

	        if (logger) {
	            logger.log( '. . . . . . . . .' );
	            log = (...params) => {
	                logger.log( 'WordMapper   ', ...params );
	            };
	        }
	    }

	    // Arguments:
	    //   fixationLines (Array of (Array of Fixation)): only (all) fixations from one array correspond
	    //      to a text line
	    //      the array is sorted top-to-bottom
	    //   textLines (Array of (Array of Word))
	    // Notes
	    //   1. If rescaling is enabled, then updates fixation.x value and copies the original value to fixation._x
	    //   2, Fixations mapped on a word get property "word" =
	    //      {left, top, right, bottom, index=<index_in_line>, id=<index_in_text>, text }
	    //   3. Words with mapped fixations get property "fixations" = (Array of Fixation)
	    map( fixationLines, textLines ) {
	        for (let i = 0; i < fixationLines.length; i += 1) {
	            const fixations = fixationLines[i];
	            const lineID = fixations[0].line;
	            const textLine = getTextLine( textLines, lineID );

	            if (textLine !== undefined) {
	                if (settings.rescaleFixationX) {
	                    rescaleFixations( fixations, textLine );
	                }
	                mapFixationsWithinLine( fixations, textLine );
	            }
	        }
	    }

	    // Arguments:
	    //   fixations (Array of Fixation): the list of fixations
	    //   words (Array of Word): the Text.words
	    clean( fixations, words ) {
	        if (settings.ignoreTransitions) {
	            removeTransitions( fixations, words );
	        }
	    }
	}

	function getTextLine( textLines, lineID ) {
	    let textLine;
	    for (let i = 0; i < textLines.length; i += 1) {
	        if (lineID === textLines[i].id) {
	            textLine = textLines[i];
	            break;
	        }
	    }
	    return textLine;
	}

	/*****************
	    rescaleFixations
	*****************/

	function getNewLeftMostX( word ) {
	    if (word.text.length > 2 * settings.wordCharSkipStart) {
	        return word.x + Math.floor( settings.wordCharSkipStart / word.text.length * word.width );
	    }
	    else {
	        return word.x + Math.floor( word.width / 2 );
	    }
	}

	function getNewRightMostX( word ) {
	    if (word.text.length > settings.wordCharSkipStart + settings.wordCharSkipEnd) {
	        return word.x + Math.floor( (word.text.length - settings.wordCharSkipEnd) / word.text.length * word.width );
	    }
	    else {
	        return word.x + Math.floor( word.width / 2 );
	    }
	}

	function getFixationsRange( fixations ) {
	    let leftMostX = Number.MAX_VALUE,
	        rightMostX = Number.MIN_VALUE;

	    for (let i = 0; i < fixations.length; i += 1) {
	        let fix = fixations[i];
	        if (fix.x < leftMostX) {
	            leftMostX = fix.x;
	        }
	        else if (fix.x > rightMostX) {
	            rightMostX = fix.x;
	        }
	    }

	    return { leftMostX, rightMostX };
	}

	function computeScale( newRange, oldRange ) {
	    let scale = newRange / oldRange;

	    // limit the scaling factor
	    let newXCorrection = 0;
	    if (scale < settings.scalingDiffLimit) {
	        scale = settings.scalingDiffLimit;
	        newXCorrection = (scale * oldRange - newRange) / 2;
	    }
	    else if (scale > (2 - settings.scalingDiffLimit)) {
	        scale = 2 - settings.scalingDiffLimit;
	        newXCorrection = -(scale * oldRange - newRange) / 2;
	    }

	    return { scale, newXCorrection };
	}

	function rescaleFixations( fixations, words ) {

	    const firstWord = words[0];
	    const lastWord = words[ words.length - 1 ];

	    const leftThreshold = firstWord.x + firstWord.width;
	    const rightThreshold = lastWord.x + lastWord.width;

	    let { leftMostX, rightMostX } = getFixationsRange( fixations );

	    log( 'rescaling...' );
	    log( 'left: ' + leftMostX + ' ' + leftThreshold );
	    log( 'right: ' + rightMostX + ' ' + rightThreshold );

	    if (leftMostX < leftThreshold || rightMostX > rightThreshold) {
	        // Calculate the scaling factor
	        let newLeftMostX = leftMostX < leftThreshold ?  // if the left-most fixation lands left to the 2nd word...
	                        getNewLeftMostX( words[0] ) :   // ...estimate its expected location
	                        leftMostX;                      // otherwise we do not know where it shoud be...
	        let newRightMostX = rightMostX > rightThreshold ?    // if the right-most fixation lands right to the 2nd last word...
	                        getNewRightMostX( lastWord ) :       // ...estimate its expected location
	                        rightMostX;                          // otherwise we do not know where it shoud be...
	        const newRange = newRightMostX - newLeftMostX;
	        const oldRange = rightMostX - leftMostX;
	        const { scale, newXCorrection } = computeScale( newRange, oldRange );
	        log( 'scale', scale );

	        newLeftMostX -= newXCorrection;
	        newRightMostX += newXCorrection;

	        // Recalculate x's
	        for (let i = 0; i < fixations.length; i += 1) {
	            const fixation = fixations[i];
	            fixation._x = fixation.x;
	            fixation.x = newLeftMostX + scale * (fixation.x - leftMostX);
	        }
	    }
	}

	/**************
	    mapFixationsWithinLine
	**************/

	function getClosestWordID( fixation, words ) {
	    let minDist = Number.MAX_VALUE;
	    let minDistWordID = -1;

	    for (let i = 0; i < words.length; i += 1) {
	        const word = words[i];
	        const effectiveWordWidth = word.fixations || word.text.length <= settings.partialLengthMaxWordLength ?
	            settings.effectiveLengthFactor * word.width : word.width;

	        // BUGFIX: effectiveWordWidth =>> word.x + effectiveWordWidth
	        if (fixation.x >= word.x && fixation.x < (word.x + effectiveWordWidth)) {
	            minDistWordID = i;
	            minDist = 0;
	            break;
	        }
	        else {
	            const dist = Math.max( word.x - fixation.x, fixation.x - (word.x + effectiveWordWidth) );
	            if (dist < minDist) {
	                minDist = dist;
	                minDistWordID = i;
	            }
	        }
	    }

	    return minDistWordID;
	}

	function mapFixationsWithinLine( fixations, words ) {
	    log( '== mapping ==' );
	    for (let i = 0; i < fixations.length; i += 1) {
	        const fixation = fixations[i];

	        const closestWordID = getClosestWordID( fixation, words );
	        if (closestWordID < 0) {
	            log( `${fixation.id} => ---` );
	            continue;
	        }

	        const closestWord = words[ closestWordID ];
	        fixation.word = {
	            left: closestWord.x,
	            top: closestWord.y,
	            right: closestWord.x + closestWord.width,
	            bottom: closestWord.y + closestWord.height,
	            index: closestWordID,
	            text: closestWord.text,
	            id: closestWord.id
	        };

	        if (closestWord.fixations) {
	            closestWord.fixations.push( fixation );
	        }
	        else {
	            closestWord.fixations = [ fixation ];
	        }

	        log( `${fixation.id} => ${closestWord.id}` );
	    }
	}

	/***************
	    removeTransitions
	***************/

	function getPrevFixationOnLine( fixations, index ) {
	    let result = null;
	    for (; index > 0; index -= 1) {
	        const fix = fixations[ index ];
	        if (fix.line !== undefined) {
	            result = fix;
	            break;
	        }
	    }

	    return result;
	}

	function getLastChunkSaccade( fixations, index, direction ) {
	    let result = null;
	    for (; index > 0; index -= 1) {
	        const fix = fixations[ index ];
	        if (fix.line === undefined) {
	            continue;
	        }

	        const prevFix = getPrevFixationOnLine( fixations, index - 1 );
	        if (!prevFix) {
	            index = 0;
	            break;
	        }

	        if (direction < 0 ? fix.x < prevFix.x : fix.x >= prevFix.x) {
	            result = fix;
	            break;
	        }
	    }

	    return [ result, index ];
	}

	function removeFixation( fixations, id ) {
	    return fixations.filter( fixation => fixation.id !== id );
	}

	function removeTransitions( fixations, words ) {
	    let index = fixations.length - 1;

	    while (index) {
	        const [ firstProgressionFix, firstProgressionFixIndex ] = getLastChunkSaccade( fixations, index, -1 );
	        if (!firstProgressionFixIndex) {
	            break;
	        }

	        const [ lastProgressionFix, lastProgressionFixIndex ] = getLastChunkSaccade( fixations, firstProgressionFixIndex, 1 );
	        index = lastProgressionFixIndex;
	        if (!lastProgressionFix)  {
	            continue;
	        }

	        if (firstProgressionFix.line !== lastProgressionFix.line) {
	            for (let i = lastProgressionFixIndex + 1; i < firstProgressionFixIndex; i += 1) {
	                const fix = fixations[ i ];
	                if (fix.word) {
	                    const word = words[ fix.word.id ];
	                    if (word.fixations.length === 1) {
	                        delete word.fixations;
	                        log( 'removed @ word #', word.id );
	                    }
	                    else {
	                        word.fixations = removeFixation( word.fixations, fix.id );
	                        log( 'one removed @ word #', word.id );
	                    }

	                    delete fix.word;
	                    delete fix.line;

	                    log( 'removed @ fix #', fix.id );
	                }
	            }
	        }
	    }
	}

	module.exports = WordMapper;

/***/ },
/* 11 */
/***/ function(module, exports, __webpack_require__) {

	'use strict';

	const Settings = __webpack_require__(3);

	class WordMapperSettings extends Settings {
		constructor() {
			super( 'wordMapperSettings' );
			this._wordCharSkipStart = 3;
			this._wordCharSkipEnd = 6;
			this._scalingDiffLimit = 0.9;
			this._rescaleFixationX = true;
			this._partialLengthMaxWordLength = 2;
			this._effectiveLengthFactor = 0.7;
			this._ignoreTransitions = true;

			super.load();
		}

		get wordCharSkipStart() { return this._wordCharSkipStart; }
		set wordCharSkipStart( value ) { this._wordCharSkipStart = value; }
		get wordCharSkipEnd() { return this._wordCharSkipEnd; }
		set wordCharSkipEnd( value ) { this._wordCharSkipEnd = value; }
		get scalingDiffLimit() { return this._scalingDiffLimit; }
		set scalingDiffLimit( value ) { this._scalingDiffLimit = value; }
		get rescaleFixationX() { return this._rescaleFixationX; }
		set rescaleFixationX( value ) { this._rescaleFixationX = value; }
		get partialLengthMaxWordLength() { return this._partialLengthMaxWordLength; }
		set partialLengthMaxWordLength( value ) { this._partialLengthMaxWordLength = value; }
		get effectiveLengthFactor() { return this._effectiveLengthFactor; }
		set effectiveLengthFactor( value ) { this._effectiveLengthFactor = value; }
		get ignoreTransitions() { return this._ignoreTransitions; }
		set ignoreTransitions( value ) { this._ignoreTransitions = value; }
	}

	module.exports = WordMapperSettings;

/***/ }
/******/ ]);
//# sourceMappingURL=sgwm.js.map


text:
<select
        name="{{ drName }}"
        class="{{ drClass }}"
        ng-options="item.id as (item.sub_title + item.global_title) for item in dr.categories"
        ng-change="drChange()"
        ng-model="bindModel">
    <option value=""></option>
</select>

redpajama_set_name: RedPajamaGithub


text:
class ExtensionWebRequestTimeTracker {
 public:
  ExtensionWebRequestTimeTracker();
  ~ExtensionWebRequestTimeTracker();

  // Records the time that a request was created.  |has_listener| will be true
  // if there is at least one webRequest listener registered.
  // |has_extra_headers_listener| will be true if there is at least one listener
  // with 'extraHeaders' in the extraInfoSpec.
  void LogRequestStartTime(int64_t request_id,
                           const base::TimeTicks& start_time,
                           bool has_listener,
                           bool has_extra_headers_listener);

  // Records the time that a request either completed or encountered an error.
  void LogRequestEndTime(int64_t request_id, const base::TimeTicks& end_time);

  // Records an additional delay for the given request caused by all extensions
  // combined.
  void IncrementTotalBlockTime(int64_t request_id,
                               const base::TimeDelta& block_time);

  // Called when an extension has canceled the given request.
  void SetRequestCanceled(int64_t request_id);

  // Called when an extension has redirected the given request to another URL.
  void SetRequestRedirected(int64_t request_id);

 private:
  FRIEND_TEST_ALL_PREFIXES(ExtensionWebRequestTimeTrackerTest, Histograms);

  // Timing information for a single request.
  struct RequestTimeLog {
    base::TimeTicks request_start_time;
    base::TimeDelta block_duration;
    bool has_listener = false;
    bool has_extra_headers_listener = false;

    RequestTimeLog();
    ~RequestTimeLog();

   private:
    DISALLOW_COPY_AND_ASSIGN(RequestTimeLog);
  };

  // Records UMA metrics for the given request and its end time.
  void AnalyzeLogRequest(const RequestTimeLog& log,
                         const base::TimeTicks& end_time);

  // A map of current request IDs to timing info for each request.
  std::map<int64_t, RequestTimeLog> request_time_logs_;

  DISALLOW_COPY_AND_ASSIGN(ExtensionWebRequestTimeTracker);
};

#endif  // EXTENSIONS_BROWSER_API_WEB_REQUEST_WEB_REQUEST_TIME_TRACKER_H_


text:
//=================================================================================================

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

#ifndef _BLAZE_MATH_SMP_PARALLELSECTION_H_
#define _BLAZE_MATH_SMP_PARALLELSECTION_H_

//*************************************************************************************************
// Includes
//*************************************************************************************************

#include <blaze/util/Exception.h>
#include <blaze/util/Suffix.h>
#include <atomic>

namespace blaze {

//=================================================================================================
//
//  CLASS DEFINITION
//
//=================================================================================================

//*************************************************************************************************
/*!\brief Section for the debugging of the shared-memory parallelization.
// \ingroup smp
//
// The ParallelSection class is an auxiliary helper class for the \a BLAZE_PARALLEL_SECTION macro.
// It provides the functionality to detected whether a parallel section has been started and with
// that serves as a utility for debugging the shared-memory parallelization.
*/
template< typename T >
class ParallelSection
{
 public:
   //**Constructor*********************************************************************************
   /*!\name Constructor */
   //@{
   inline ParallelSection( bool activate );
   //@}
   //**********************************************************************************************

   //**Destructor**********************************************************************************
   /*!\name Destructor */
   //@{
   inline ~ParallelSection();
   //@}
   //**********************************************************************************************

   //**Conversion operator*************************************************************************
   /*!\name Conversion operator */
   //@{
   inline operator bool() const;
   //@}
   //**********************************************************************************************

 private:
   //**Member variables****************************************************************************
   /*!\name Member variables */
   //@{
   static std::atomic_int counts_;  //!< Counts for the active parallel section.
                                    /*!< In case a parallel section is active (i.e. the currently
                                         executed code is inside a parallel section), the counts is
                                         increased, otherwise it is decreased. */
   //@}
   //**********************************************************************************************

   //**Friend declarations*************************************************************************
   /*! \cond BLAZE_INTERNAL */
   friend bool isParallelSectionActive();
   /*! \endcond */
   //**********************************************************************************************

   //**Friend declarations*************************************************************************
   /*! \cond BLAZE_INTERNAL */
   friend bool isParallelSectionNested();
   /*! \endcond */
   //**********************************************************************************************
};
//*************************************************************************************************

//=================================================================================================
//
//  DEFINITION AND INITIALIZATION OF THE STATIC MEMBER VARIABLES
//
//=================================================================================================

//*************************************************************************************************
/*! \cond BLAZE_INTERNAL */
template< typename T >
std::atomic_int ParallelSection<T>::counts_(0);
/*! \endcond */
//*************************************************************************************************

//=================================================================================================
//
//  CONSTRUCTOR
//
//=================================================================================================

//*************************************************************************************************
/*!\brief Constructor for the ParallelSection class.
//
// \param activate Activation flag for the parallel section.
// \exception std::runtime_error Nested parallel sections detected.
*/
template< typename T >
inline ParallelSection<T>::ParallelSection( bool activate )
{
  ++counts_;
}
//*************************************************************************************************

//=================================================================================================
//
//  DESTRUCTOR
//
//=================================================================================================

//*************************************************************************************************
/*!\brief Destructor of the ParallelSection class.
*/
template< typename T >
inline ParallelSection<T>::~ParallelSection()
{
  --counts_;
}
//*************************************************************************************************

//=================================================================================================
//
//  CONVERSION OPERATOR
//
//=================================================================================================

//*************************************************************************************************
/*!\brief Conversion operator to \a bool.
//
// The conversion operator returns \a true in case a parallel section is active and \a false
// otherwise.
*/
template< typename T >
inline ParallelSection<T>::operator bool() const
{
   return counts_.load() > 0;
}
//*************************************************************************************************

//=================================================================================================
//
//  GLOBAL FUNCTIONS
//
//=================================================================================================

//*************************************************************************************************
/*!\name ParallelSection functions */
//@{
inline bool isParallelSectionActive();
//@}
//*************************************************************************************************

//*************************************************************************************************
/*!\brief Returns whether a parallel section is active or not.
// \ingroup smp
//
// \return \a true if a parallel section is active, \a false if not.
*/
inline bool isParallelSectionActive()
{
   return ParallelSection<int>::counts_.load() > 0;
}
//*************************************************************************************************

//*************************************************************************************************
/*!\name ParallelSection functions */
//@{
inline bool isParallelSectionNested();
//@}
//*************************************************************************************************

//*************************************************************************************************
/*!\brief Returns whether a parallel section is nested or not.
// \ingroup smp
//
// \return \a true if a parallel section is nested, \a false if not.
*/
inline bool isParallelSectionNested()
{
   return ParallelSection<int>::counts_.load() > 1;
}
//*************************************************************************************************

//=================================================================================================
//
//  PARALLEL SECTION MACRO
//
//=================================================================================================

//*************************************************************************************************
/*!\brief Section for the debugging of the shared-memory parallelization.
// \ingroup smp
//
// During the shared-memory parallel (SMP) execution of an operation nested calls to the SMP
// assign functions are conceptually not allowed. In other words, it is not allowed to call a
// SMP assign function from within a non-SMP assign function. The BLAZE_PARALLEL_SECTION macro
// can be used to mark the start of a parallel section and with that detect nested SMP assign
// function calls. In case a nested use of a parallel section is detected, a \a std::runtime_error
// exception is thrown.\n
// Note that this macro is reserved for internal debugging purposes only and therefore must \b NOT
// be used explicitly! Using this macro might result in erroneous results, runtime or compilation
// errors.
*/
#define BLAZE_PARALLEL_SECTION \
   if( blaze::ParallelSection<int> BLAZE_JOIN( parallelSection, __LINE__ ) = true )
//*************************************************************************************************

} // namespace blaze

#endif


text:
/**
 * Helper library for benchmarking small pieces of code
 * Uses rdtsc to get the clock counter. Note potential problems on multicore systems.
 *
 * See header file for details.
 *
 * v1.2 2015-11-25 / 2017-11-22 Pirmin Schmid, MIT License
 */

#include "benchmark.h"

#include <assert.h>
#include <float.h>
#include <inttypes.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//--- private data ---------------------------------------------------------------------------------
//    some internal variables

static uint64_t baseline_ = 0;
static uint64_t baseline_backup_ = 0; // used to handle baseline reset by development_map_values()

// raw data stored from measurement
static uint64_t *data_ = NULL;

// temporary internal data for outlier removal
// allocated at the beginning to avoid lots of mallocs() later
static uint64_t *data_without_outliers_ = NULL;

// additional temporary internal data for outlier removal (histogram method)
// allocated at the beginning to avoid mallocs() later
static uint64_t *data_working_temp_ = NULL;

static size_t cap_ = 0;
static size_t count_ = 0;
static size_t denominator_ = 1;

static enum testbench_outlier_detection_mode outlier_detection_mode_ = TESTBENCH_OUTLIER_DETECTION_OFF;

// default unit
static struct testbench_time_unit cycles_ = {
    .name = "cycles",
    .cycles_per_unit = 1
};

// values of t-distribution (two-tailed) for 100*(1-alpha)% = 95% (alpha level 0.05)
// from https://www.medcalc.org/manual/t-distribution.php
// abbreviated use here
struct distr {
    size_t df;
    double t_value;
};

// arbitrarily set
#define T_TABLE_INFINITY 1024

static struct distr t_table_[] = {
    {T_TABLE_INFINITY, 1.960},
    {300, 1.968},
    {100, 1.984},
    {80, 1.990},
    {60, 2.000},
    {50, 2.009},
    {40, 2.021},
    {30, 2.042},
    {20, 2.086},
    {18, 2.101},
    {16, 2.120},
    {14, 2.145},
    {12, 2.179},
    {10, 2.228},
    {9, 2.262},
    {8, 2.306},
    {7, 2.365},
    {6, 2.447},
    {5, 2.571},
    {4, 2.776},
    {3, 3.182},
    {2, 4.303},
    {1, 12.706}
};

static size_t t_table_n_ = sizeof(t_table_) / sizeof(t_table_[0]);

//--- private helpers ------------------------------------------------------------------------------

static double get_t_value(size_t n)
{
    assert(n > 1);

    size_t df = n - 1;
    for (size_t i = 0; i < t_table_n_; i++) {
        if (df >= t_table_[i].df) {
            return t_table_[i].t_value;
        }
    }

    // will not happen
    assert(false);
    return 0.0;
}

static int cmp_uint64_t(const void *a, const void *b)
{
    uint64_t a2 = *((uint64_t *)a);
    uint64_t b2 = *((uint64_t *)b);
    if (a2 < b2) {
        return -1;
    }
    if (a2 > b2) {
        return 1;
    }
    return 0;
}

/**
 * note, the expected fractional parts for r_frac will be 0.0, 0.25, 0.5, 0.75
 * thus, no need to test for machine precision if( r_frac <= (DBL_EPSILON * r_p) )
 * PERC_ATOL is currently chosen to allow for some "reserve" if get_percentile is chosen
 * to be used for smaller percentiles, too. The conditional is kept to avoid rounding errors
 * with calculations close to 0.0.
 */
#define PERC_ATOL 0.001

/**
 * calculates percentile value as described in
 * https://www.medcalc.org/manual/summary_statistics.php
 * see Lentner C (ed). Geigy Scientific Tables, 8th edition, Volume 2. Basel: Ciba-Geigy Limited, 1982
 *     Schoonjans F, De Bacquer D, Schmid P. Estimation of population percentiles. Epidemiology 2011;22:750-751.
 * percentile must be in range (0,1) here -- note: not (0,100).
 * modified to include the denominator, too.
 */
static double get_percentile(uint64_t *sorted_values, size_t n_values, double percentile, size_t denominator)
{
    double n = (double)n_values;
    assert(1.0/n <= percentile && percentile <= (n - 1.0)/n);
    double r_p = 0.5 + percentile * n;
    // note: only valid if 1/n <= percentile <= (n-1)/n
    // i.e. n must be at very least 4 for percentiles == 0.25 (quartile 1) and 0.75 (quartile 3)
    //                              2 for percentile == 0.5 (median)

    double r_floor = floor(r_p);
    double r_frac = r_p - r_floor; // in range [0,1); also == abs(r_floor - r_p)

    double result = 0.0;
    if (r_frac < PERC_ATOL) {
        // integer number, use (r_floor - 1) as array index and return value
        size_t r_ind = (size_t)r_floor - 1;
        assert(r_ind < n_values);
        result = (double)sorted_values[r_ind];
    }
    else {
        // use linear interpolation for fractional part
        // benefits and limitations -> see mentioned papers
        size_t r_ind = (size_t)r_floor - 1;
        size_t r_ind2 = r_ind + 1;
        assert(r_ind2 < n_values);
        result = (1.0 - r_frac) * (double)sorted_values[r_ind];  // the closer to 0.0 the more weight
        result += r_frac * (double)sorted_values[r_ind2];        // the closer to 1.0 the more weight
    }

    return result / ((double)denominator);
}

static struct testbench_statistics calc_statistics(uint64_t *values, size_t n_values);

static bool fprint_testbench_statistics_including_outliers(FILE *stream, const char *title,
                                                           const struct testbench_statistics *stat,
                                                           const struct testbench_time_unit *unit,
                                                           size_t removed_outliers);

static struct testbench_statistics fprint_histogram_and_remove_outliers(FILE *stream, const char *title,
                                                                        const struct testbench_statistics *stat,
                                                                        const struct testbench_time_unit *unit,
                                                                        uint64_t *values, size_t n_values,
                                                                        bool test_for_outliers,
                                                                        bool *ret_ok);

//--- implementation of the public API -------------------------------------------------------------
//    see header file for information about the functions

bool create_testbench(size_t capacity)
{
    uint64_t start = 0;
    uint64_t stop = 0;

    if (capacity < 1) {
        goto error_wrong_capacity;
    }

    if (data_) {
        delete_testbench();
    }

    data_ = malloc(capacity * sizeof(*data_));
    if (!data_) {
        goto error_malloc_data;
    }

    data_without_outliers_ = malloc(capacity * sizeof(*data_without_outliers_));
    if (!data_without_outliers_) {
        goto error_malloc_data_without_outliers;
    }

    data_working_temp_ = malloc(capacity * sizeof(*data_working_temp_));
    if (!data_working_temp_) {
        goto error_malloc_data_working_temp;
    }

    cap_ = capacity;
    denominator_ = 1;

    // establish baseline
    // have 2 full dry runs of size cap_ (warming up) and then one measurement
    for (size_t i = 0; i < 3; i++) {
        for (size_t j = 0; j < cap_; j++) {
            RDTSC_START(start);
            // nothing
            RDTSC_STOP(stop);
            data_[j] = stop - start;
        }
    }

    count_ = cap_;
    struct testbench_statistics baseline_stat = calc_statistics(data_, count_);
    print_testbench_statistics("baseline", &baseline_stat, NULL);
    print_histogram("baseline", &baseline_stat, NULL);
    baseline_ = baseline_stat.absMin;
    baseline_backup_ = baseline_;
    printf("Benchmark library: %" PRIu64 " cycles will be used as baseline.\n", baseline_);
    count_ = 0;
    denominator_ = TESTBENCH_STD_DENOMINATOR;
    return true;

    // error handling
//error_next:
    free(data_working_temp_);
    data_working_temp_ = NULL;
error_malloc_data_working_temp:
    free(data_without_outliers_);
    data_without_outliers_ = NULL;
error_malloc_data_without_outliers:
    free(data_);
    data_ = NULL;
error_malloc_data:
error_wrong_capacity:
    return false;
}

void set_denominator(size_t denominator)
{
    if (denominator < 1) {
        return;
    }

    denominator_ = denominator;
}

void set_outlier_detection_mode(enum testbench_outlier_detection_mode mode)
{
    outlier_detection_mode_ = mode;
}

void reset_testbench(void)
{
    count_ = 0;
    baseline_ = baseline_backup_;
}

void delete_testbench(void)
{
    if (data_working_temp_) {
        free(data_working_temp_);
        data_working_temp_ = NULL;
    }

    if (data_without_outliers_) {
        free(data_without_outliers_);
        data_without_outliers_ = NULL;
    }

    if (data_) {
        free(data_);
        data_ = NULL;
        cap_ = 0;
        count_ = 0;
        baseline_ = 0;
        denominator_ = TESTBENCH_STD_DENOMINATOR;
    }
}

void add_measurement(uint64_t start, uint64_t stop)
{
    // no array index check here
    uint64_t delta = stop - start - baseline_;
    data_[count_++] = ((int64_t)delta) < 0 ? 0 : delta;
}

//--- testbench_get_statistics() with associated private function ----------------------------------

/**
 * calculates the statistics for an array values[] of size n_values
 * this internal function is used by testbench_get_statistics() and print_histogram()
 */
static struct testbench_statistics calc_statistics(uint64_t *values, size_t n_values)
{
    // note: min/max deliberately not stored while adding measurements to avoid any
    // unnecessary cache interruption of the program to be measured
    struct testbench_statistics result = {0, 0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
    result.denominator = denominator_;
    result.baseline = baseline_;
    if (n_values == 0) {
        return result;
    }

    result.count = n_values;

    // mean, min, max
    uint64_t sum = 0;
    uint64_t min = UINT64_MAX;
    uint64_t max = 0;
    for (size_t i = 0; i < n_values; i++) {
        const uint64_t value = values[i];
        sum += value;

        if (value < min) {
            min = value;
        }

        if (value > max) {
            max = value;
        }
    }

    const double mean = (double)sum / ((double)denominator_ * (double)n_values);
    result.mean = mean;
    result.absMin = min;
    result.min = (double)min / (double)denominator_;
    result.absMax = max;
    result.max = (double)max / (double)denominator_;

    // robust
    if (n_values > 1) {
        qsort(values, n_values, sizeof(*values), cmp_uint64_t);
        result.median = get_percentile(values, n_values, 0.5, denominator_);

        if (n_values > 3) {
            // just the bare minimum to work
            // of course these quartiles will have large 95% confidence intervals by themselves
            // thus: choose larger n_values for meaningful results, of course
            result.q1 = get_percentile(values, n_values, 0.25, denominator_);
            result.q3 = get_percentile(values, n_values, 0.75, denominator_);
        }
        else {
            result.q1 = result.min;
            result.q3 = result.max;
        }
    }
    else {
        result.median = result.min;
        result.q1 = result.min;
        result.q3 = result.max;
    }

    // parametric (assuming normal distribution)
    if (n_values > 1) {
        // this is just the barely minimum to avoid div/0
        // of course, one should use a meaningful count / n_values
        double s2 = 0.0;
        for (size_t i = 0; i < n_values; i++) {
            const double delta = (double)values[i] / (double)denominator_ - mean;
            s2 += delta * delta;
        }

        s2 = s2 / (double)(n_values - 1);
        s2 = sqrt(s2);
        result.sd = s2;
        double sem = s2 / sqrt((double)n_values);

        double ci95_delta = get_t_value(n_values) * sem;
        result.ci95_a = mean - ci95_delta;
        result.ci95_b = mean + ci95_delta;
    }

    return result;
}

struct testbench_statistics testbench_get_statistics(void)
{
    // note: min/max deliberately not stored while adding measurements to avoid any
    // unnecessary cache interruption of the program to be measured
    return calc_statistics(data_, count_);
}

//--- fprint_testbench_values() --------------------------------------------------------------------

bool fprint_testbench_values(FILE *stream, const char *title, const struct testbench_time_unit *unit)
{
    assert(stream);
    assert(title);
    // unit is optional

    int ret = fprintf(stream, "# %s (n=%zu)\n", title, count_);
    if (ret < 0) {
        return false;
    }

    if (unit) {
        ret = fprintf(stream, "# unit: %s with %" PRIu64 " cycles / unit\n", unit->name, unit->cycles_per_unit);
        if (ret < 0) {
            return false;
        }

        const double cpu = unit->cycles_per_unit;
        for (size_t i = 0; i < count_; i++) {
            const double value = (double)data_[i] / cpu;
            ret = fprintf(stream, "%f\n", value);
            if (ret < 0) {
                return false;
            }
        }
    }
    else {
        ret = fprintf(stream, "# unit: cycles\n");
        if (ret < 0) {
            return false;
        }

        for (size_t i = 0; i < count_; i++) {
            ret = fprintf(stream, "%" PRIu64 "\n", data_[i]);
            if (ret < 0) {
                return false;
            }
        }
    }

    return true;
}

//--- print_testbench_statistics() with associated private function --------------------------------

static struct testbench_statistics convert_stats(const struct testbench_statistics *stat, const struct testbench_time_unit *unit)
{
    uint64_t cpu_i = unit->cycles_per_unit;
    double cpu_d = (double)unit->cycles_per_unit;

    struct testbench_statistics s;
    s.count = stat->count;
    s.denominator = stat->denominator;
    s.baseline = stat->baseline / cpu_i;
    s.absMin = stat->absMin / cpu_i;
    s.absMax = stat->absMax / cpu_i;
    s.min = stat->min / cpu_d;
    s.q1 = stat->q1 / cpu_d;
    s.median = stat->median / cpu_d;
    s.q3 = stat->q3 / cpu_d;
    s.max = stat->max / cpu_d;
    s.mean = stat->mean / cpu_d;
    s.sd = stat->sd / cpu_d;
    s.ci95_a = stat->ci95_a / cpu_d;
    s.ci95_b = stat->ci95_b / cpu_d;
    return s;
}

static bool fprint_testbench_statistics_including_outliers(FILE *stream, const char *title,
                                                           const struct testbench_statistics *stat,
                                                           const struct testbench_time_unit *unit,
                                                           size_t removed_outliers)
{
    int ret = 0;
    if (title) {
        ret = fprintf(stream, "\n%s:\n", title);
        if (ret < 0) {
            return false;
        }
    }

    struct testbench_statistics s;
    if (unit) {
        s = convert_stats(stat, unit);
    }
    else {
        unit = &cycles_;
        s = *stat;
    }

    if (stat->count > 3) {
        // just the bare minimum to somewhat make sense
        // better use larger counts, of course
        ret = fprintf(stream, "- robust:       median %.1f %s, IQR [%.1f, %.1f], min %.1f, max %.1f, n=%zu [%zu outlier(s) removed], denominator=%zu, baseline=%" PRIu64 "\n",
                      s.median, unit->name, s.q1, s.q3, s.min, s.max, s.count, removed_outliers, s.denominator, s.baseline);
        if (ret < 0) {
            return false;
        }

        ret = fprintf(stream, "- normal dist.: %.1f ± %.1f %s (mean ± sd), 95%% CI for the mean [%.1f, %.1f], min %.1f, max %.1f, n=%zu [%zu outlier(s) removed], denominator=%zu, baseline=%" PRIu64 "\n",
                      s.mean, s.sd, unit->name, s.ci95_a, s.ci95_b, s.min, s.max, s.count, removed_outliers, s.denominator, s.baseline);
        if (ret < 0) {
            return false;
        }
    }
    else {
        // there is not much that should be reported with such low counts
        ret = fprintf(stream, "mean %.1f %s, median %.1f, min %.1f, max %.1f, n=%zu [%zu outlier(s) removed], denominator=%zu, baseline=%" PRIu64 "; use n >= 4 for more detailed descriptive statistics.\n",
                      s.mean, unit->name, s.median, s.min, s.max, s.count, removed_outliers, s.denominator, s.baseline);
        if (ret < 0) {
            return false;
        }
    }

    return true;
}

bool fprint_testbench_statistics(FILE *stream, const char *title,
                                 const struct testbench_statistics *stat,
                                 const struct testbench_time_unit *unit)
{
    assert(stream);
    assert(stat);
    // title and unit are optional

    int ret = 0;
    if (title) {
        ret = fprintf(stream, "\n%s:\n", title);
        if (ret < 0) {
            return false;
        }
    }

    struct testbench_statistics s;
    if (unit) {
        s = convert_stats(stat, unit);
    }
    else {
        unit = &cycles_;
        s = *stat;
    }

    if (stat->count > 3) {
        // just the bare minimum to somewhat make sense
        // better use larger counts, of course
        ret = fprintf(stream, "- robust:       median %.1f %s, IQR [%.1f, %.1f], min %.1f, max %.1f, n=%zu, denominator=%zu, baseline=%" PRIu64 "\n",
                      s.median, unit->name, s.q1, s.q3, s.min, s.max, s.count, s.denominator, s.baseline);
        if (ret < 0) {
            return false;
        }

        ret = fprintf(stream, "- normal dist.: %.1f ± %.1f %s (mean ± sd), 95%% CI for the mean [%.1f, %.1f], min %.1f, max %.1f, n=%zu, denominator=%zu, baseline=%" PRIu64 "\n",
                      s.mean, s.sd, unit->name, s.ci95_a, s.ci95_b, s.min, s.max, s.count, s.denominator, s.baseline);
        if (ret < 0) {
            return false;
        }
    }
    else {
        // there is not much that should be reported with such low counts
        ret = fprintf(stream, "mean %.1f %s, median %.1f, min %.1f, max %.1f, n=%zu, denominator=%zu, baseline=%" PRIu64 "; use n >= 4 for more detailed descriptive statistics.\n",
                      s.mean, unit->name, s.median, s.min, s.max, s.count, s.denominator, s.baseline);
        if (ret < 0) {
            return false;
        }
    }

    return true;
}

//--- fprint_histogram() with associated private function ------------------------------------------

static struct testbench_statistics fprint_histogram_and_remove_outliers(FILE *stream, const char *title,
                                                                        const struct testbench_statistics *stat,
                                                                        const struct testbench_time_unit *unit,
                                                                        uint64_t *values, size_t n_values,
                                                                        bool test_for_outliers,
                                                                        bool *ret_ok)
{
    *ret_ok = true;

    // some checks
    if (stat->max < stat->min) {
        return *stat;
    }

    if (stat->count < 1) {
        return *stat;
    }

    uint64_t d = denominator_;
    if (unit) {
        d *= unit->cycles_per_unit;
    }

    uint64_t min = stat->absMin / d;
    uint64_t max = stat->absMax / d;

    size_t delta = (size_t)(max - min);
    size_t bins = delta + 1;
    size_t size = 1;
    while (bins > TESTBENCH_MAX_BINS) {
        // arbitrary limit
        size <<= 1;
        bins = delta/size + 1;
    }

    int ret = 0;
    if (title) {
        ret = fprintf(stream, "%s (%zu bins of size %zu)\n", title, bins, size);
        if (ret < 0) {
            goto fprintf_error_return;
        }
    }
    else {
        ret = fprintf(stream, "(%zu bins of size %zu)\n", bins, size);
        if (ret < 0) {
            goto fprintf_error_return;
        }
    }

    size_t histogram[TESTBENCH_MAX_BINS];
    for (size_t i = 0; i < bins; i++) {
        histogram[i] = 0;
    }

    for (size_t i = 0; i < n_values; i++) {
        histogram[ ((size_t)(values[i] / d - min)) / size ]++;
    }

    for (size_t i = 0; i < bins; i++) {
        size_t j = (histogram[i] * 200) / n_values; // 200 is used to have 0.5 % resolution
        if (size == 1) {
            ret = fprintf(stream, "%4" PRIu64 " [%3zu]: ", i + min, histogram[i]);
            if (ret < 0) {
                goto fprintf_error_return;
            }
        }
        else {
            uint64_t offset = (i * size) + min;
            ret = fprintf(stream, "%4" PRIu64 " - %4" PRIu64 " [%3zu]: ", offset, offset + size - 1, histogram[i]);
            if (ret < 0) {
                goto fprintf_error_return;
            }
        }

        while (j >= 2) {
            ret = fprintf(stream, "*");
            if (ret < 0) {
                goto fprintf_error_return;
            }
            j-=2;
        }
        if (j == 1) {
            ret = fprintf(stream, ".");
            if (ret < 0) {
                goto fprintf_error_return;
            }
        }
        ret = fprintf(stream, "\n");
        if (ret < 0) {
            goto fprintf_error_return;
        }
    }

    if (outlier_detection_mode_ == TESTBENCH_OUTLIER_DETECTION_OFF) {
        return *stat;
    }

    if (!test_for_outliers) {
        return *stat;
    }

    // start outlier detection
    size_t count_without_outliers = 0;

    if (outlier_detection_mode_ == TESTBENCH_OUTLIER_DETECTION_SD) {
        if (n_values < TESTBENCH_OUTLIER_DETECTION_SD_MIN_N) {
            return *stat;
        }

        double diff = TESTBENCH_OUTLIER_DETECTION_SD_MIN_SD * stat->sd;
        double low = stat->mean - diff;
        double high = stat->mean + diff;
        for (size_t i = 0; i < n_values; i++) {
            uint64_t vi = values[i];
            double vd = (double)vi;
            if (vd < low) {
                continue;
            }
            if (vd > high) {
                continue;
            }
            data_without_outliers_[count_without_outliers++] = vi;
        }
    }
    else if (outlier_detection_mode_ == TESTBENCH_OUTLIER_DETECTION_HISTOGRAM) {
        if (n_values < TESTBENCH_OUTLIER_DETECTION_HISTOGRAM_MIN_N) {
            return *stat;
        }

        size_t cutoff = TESTBENCH_STD_CUTOFF;

        // the following algorithm is a compromise of speed and performance
        // outlier detection using the histogram is currently defined as:
        // keep only the values that have been measured more often than defined by cutoff,
        // typically set to 1. An easy question to answer for histograms with bin size 1.
        // Since some test results needed bins of wider size for reasonable optical representation
        // of the histogram, a separate test is needed. Potentially a large amount of memory
        // could be needed to build this array with bins of size 1 then. Thus an alternative method is used that
        // runs in O(n^2) for n=number of value, which is fine for typically small n
        // Stable memory requirement that can be allocated already during initialization
        // of the bench for all tests.
        // And after introduction of the denominator, it is also easier to use the alternative
        // version for histograms of bin size 1 to avoid back and forth calculation of
        // denominators with rounding errors.

        // alternative histogram version as described
        // 1) copy data
        memcpy(data_working_temp_, values, n_values * sizeof(*values));

        // 2) walk thru the list and check the # of occurences of a value
        //    if > cutoff -> copy all of them
        //    otherwise ignore
        //    note: all checked positions will be set to 0 to avoid re-checking
        //    of these values again and again
        // Advantage:    no additional memory needed;
        // Disadvantage: run time O(n^2) as discussed above
        for (size_t i = 0; i < n_values; i++) {
            uint64_t v = data_working_temp_[i];
            if (v == 0) {
                continue;
            }

            size_t counter = 1;
            for (size_t j = i+1; j < n_values; j++) {
                if (data_working_temp_[j] == v) {
                    data_working_temp_[j] = 0;
                    counter++;
                }
            }

            // 3) add values to the correct list, if needed
            if (counter > cutoff) {
                for (size_t j = 0; j < counter; j++) {
                    data_without_outliers_[count_without_outliers++] = v;
                }
            }
        }
    }
    else {
        assert(false);
    }

    struct testbench_statistics no_outliers = calc_statistics(data_without_outliers_, count_without_outliers);
    ret = fprintf(stream, "\nAfter outlier removal (method ");
    if (ret < 0) {
        goto fprintf_error_return;
    }
    switch (outlier_detection_mode_) {
        case TESTBENCH_OUTLIER_DETECTION_SD: {
            ret = fprintf(stream, "standard deviation, cutoff at %d SD):", TESTBENCH_OUTLIER_DETECTION_SD_MIN_SD);
            if (ret < 0) {
                goto fprintf_error_return;
            }
            break;
        }

        case TESTBENCH_OUTLIER_DETECTION_HISTOGRAM: {
            ret = fprintf(stream, "histogram, cutoff %d):", TESTBENCH_STD_CUTOFF);
            if (ret < 0) {
                goto fprintf_error_return;
            }
            break;
        }

        default:
            assert(false);
    }
    if (!fprint_testbench_statistics_including_outliers(stream, title, &no_outliers, unit, stat->count - count_without_outliers)) {
        goto fprintf_error_return;
    }
    fprint_histogram_and_remove_outliers(stream, NULL, &no_outliers, unit, data_without_outliers_, count_without_outliers, false, ret_ok);
    if (!*ret_ok) {
        goto fprintf_error_return;
    }
    ret = fprintf(stream, "\n");
    if (ret < 0) {
        goto fprintf_error_return;
    }

    // all OK
    return no_outliers;

fprintf_error_return:
    *ret_ok = false;
    return *stat;
}

struct testbench_statistics fprint_histogram(FILE *stream, const char *title,
                                             const struct testbench_statistics *stat,
                                             const struct testbench_time_unit *unit,
                                             bool *ret_ok)
{
    assert(stream);
    assert(stat);
    assert(ret_ok);
    // title and unit are optional

    return fprint_histogram_and_remove_outliers(stream, title, stat, unit, data_, count_, true, ret_ok);
}

//--- development helpers ------------------------------------------------------

bool development_load_raw_values(const uint64_t *values, size_t n_values)
{
    assert(values);

    if (n_values > cap_) {
        return false;
    }

    memcpy(data_, values, n_values * sizeof(*values));
    count_ = n_values;
    return true;
}

bool development_get_raw_values(uint64_t *values_buffer, size_t n_values_capacity, size_t *ret_n_values)
{
    assert(values_buffer);
    assert(ret_n_values);

    if (n_values_capacity < count_) {
        return false;
    }

    *ret_n_values = count_;
    memcpy(values_buffer, data_, count_ * sizeof(*values_buffer));
    return true;
}

bool development_map_values(testbench_lambda_function_t lambda)
{
    assert(lambda);

    if (denominator_ != 1) {
        return false;
    }

    baseline_ = 0;

    for (size_t i = 0; i < count_; i++) {
        data_[i] = lambda(data_[i]);
    }

    return true;
}

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