<commit_before>/**
 * PowerTip Core
 *
 * @fileoverview  Core variables, plugin object, and API.
 * @link          http://stevenbenner.github.com/jquery-powertip/
 * @author        Steven Benner (http://stevenbenner.com/)
 * @requires      jQuery 1.7+
 */

// useful private variables
var $document = $(document),
	$window = $(window),
	$body = $('body');

// constants
var DATA_DISPLAYCONTROLLER = 'displayController',
	DATA_HASACTIVEHOVER = 'hasActiveHover',
	DATA_HASMOUSEENTER = 'hasMouseEnter',
	DATA_FORCEDOPEN = 'forcedOpen',
	DATA_HASMOUSEMOVE = 'hasMouseMove',
	DATA_MOUSEONTOTIP = 'mouseOnToPopup',
	DATA_ORIGINALTITLE = 'originalTitle',
	DATA_POWERTIP = 'powertip',
	DATA_POWERTIPJQ = 'powertipjq',
	DATA_POWERTIPTARGET = 'powertiptarget',
	EVENT_NAMESPACE = '.powertip',
	RAD2DEG = 180 / Math.PI;

/**
 * Session data
 * Private properties global to all powerTip instances
 */
var session = {
	elements: null,
	tooltips: null,
	isTipOpen: false,
	isFixedTipOpen: false,
	isClosing: false,
	tipOpenImminent: false,
	activeHover: null,
	currentX: 0,
	currentY: 0,
	previousX: 0,
	previousY: 0,
	desyncTimeout: null,
	closeDelayTimeout: null,
	mouseTrackingActive: false,
	delayInProgress: false,
	windowWidth: 0,
	windowHeight: 0,
	scrollTop: 0,
	scrollLeft: 0
};

/**
 * Collision enumeration
 * @enum {number}
 */
var Collision = {
	none: 0,
	top: 1,
	bottom: 2,
	left: 4,
	right: 8
};

/**
 * Display hover tooltips on the matched elements.
 * @param {(Object|string)} opts The options object to use for the plugin, or
 *     the name of a method to invoke on the first matched element.
 * @param {*=} [arg] Argument for an invoked method (optional).
 * @return {jQuery} jQuery object for the matched selectors.
 */
$.fn.powerTip = function(opts, arg) {
	var targetElements = this,
		options,
		tipController;

	// don't do any work if there were no matched elements
	if (!targetElements.length) {
		return targetElements;
	}

	// handle api method calls on the plugin, e.g. powerTip('hide')
	if ($.type(opts) === 'string' && $.powerTip[opts]) {
		return $.powerTip[opts].call(targetElements, targetElements, arg);
	}

	// extend options and instantiate TooltipController
	options = $.extend({}, $.fn.powerTip.defaults, opts);
	tipController = new TooltipController(options);

	// hook mouse and viewport dimension tracking
	initTracking();

	// setup the elements
	targetElements.each(function elementSetup() {
		var $this = $(this),
			dataPowertip = $this.data(DATA_POWERTIP),
			dataElem = $this.data(DATA_POWERTIPJQ),
			dataTarget = $this.data(DATA_POWERTIPTARGET),
			title;

		// handle repeated powerTip calls on the same element by destroying the
		// original instance hooked to it and replacing it with this call
		if ($this.data(DATA_DISPLAYCONTROLLER)) {
			$.powerTip.destroy($this);
		}

		// attempt to use title attribute text if there is no data-powertip,
		// data-powertipjq or data-powertiptarget. If we do use the title
		// attribute, delete the attribute so the browser will not show it
		title = $this.attr('title');
		if (!dataPowertip && !dataTarget && !dataElem && title) {
			$this.data(DATA_POWERTIP, title);
			$this.data(DATA_ORIGINALTITLE, title);
			$this.removeAttr('title');
		}

		// create hover controllers for each element
		$this.data(
			DATA_DISPLAYCONTROLLER,
			new DisplayController($this, options, tipController)
		);
	});

	// attach events to matched elements if the manual option is not enabled
	if (!options.manual) {
		// attach open events
		$.each(options.openEvents, function(idx, evt) {
			if ($.inArray(evt, options.closeEvents) > -1) {
				// event is in both openEvents and closeEvents, so toggle it
				targetElements.on(evt + EVENT_NAMESPACE, function elementToggle(event) {
					$.powerTip.toggle(this, event);
				});
			} else {
				targetElements.on(evt + EVENT_NAMESPACE, function elementOpen(event) {
					$.powerTip.show(this, event);
				});
			}
		});

		// attach close events
		$.each(options.closeEvents, function(idx, evt) {
			if ($.inArray(evt, options.openEvents) < 0) {
				targetElements.on(evt + EVENT_NAMESPACE, function elementClose(event) {
					// set immediate to true for any event without mouse info
					$.powerTip.hide(this, !isMouseEvent(event));
				});
			}
		});

		// attach escape key close event
		targetElements.on('keydown' + EVENT_NAMESPACE, function elementKeyDown(event) {
			// always close tooltip when the escape key is pressed
			if (event.keyCode === 27) {
				$.powerTip.hide(this, true);
			}
		});
	}

	// remember elements that the plugin is attached to
	session.elements = session.elements ? session.elements.add(targetElements) : targetElements;

	return targetElements;
};

/**
 * Default options for the powerTip plugin.
 */
$.fn.powerTip.defaults = {
	fadeInTime: 200,
	fadeOutTime: 100,
	followMouse: false,
	popupId: 'powerTip',
	intentSensitivity: 7,
	intentPollInterval: 100,
	closeDelay: 100,
	placement: 'n',
	smartPlacement: false,
	offset: 10,
	mouseOnToPopup: false,
	manual: false,
	openEvents: [ 'mouseenter', 'focus' ],
	closeEvents: [ 'mouseleave', 'blur' ]
};

/**
 * Default smart placement priority lists.
 * The first item in the array is the highest priority, the last is the lowest.
 * The last item is also the default, which will be used if all previous options
 * do not fit.
 */
$.fn.powerTip.smartPlacementLists = {
	n: ['n', 'ne', 'nw', 's'],
	e: ['e', 'ne', 'se', 'w', 'nw', 'sw', 'n', 's', 'e'],
	s: ['s', 'se', 'sw', 'n'],
	w: ['w', 'nw', 'sw', 'e', 'ne', 'se', 'n', 's', 'w'],
	nw: ['nw', 'w', 'sw', 'n', 's', 'se', 'nw'],
	ne: ['ne', 'e', 'se', 'n', 's', 'sw', 'ne'],
	sw: ['sw', 'w', 'nw', 's', 'n', 'ne', 'sw'],
	se: ['se', 'e', 'ne', 's', 'n', 'nw', 'se'],
	'nw-alt': ['nw-alt', 'n', 'ne-alt', 'sw-alt', 's', 'se-alt', 'w', 'e'],
	'ne-alt': ['ne-alt', 'n', 'nw-alt', 'se-alt', 's', 'sw-alt', 'e', 'w'],
	'sw-alt': ['sw-alt', 's', 'se-alt', 'nw-alt', 'n', 'ne-alt', 'w', 'e'],
	'se-alt': ['se-alt', 's', 'sw-alt', 'ne-alt', 'n', 'nw-alt', 'e', 'w']
};

/**
 * Public API
 */
$.powerTip = {
	/**
	 * Attempts to show the tooltip for the specified element.
	 * @param {jQuery|Element} element The element to open the tooltip for.
	 * @param {jQuery.Event=} event jQuery event for hover intent and mouse
	 *     tracking (optional).
	 * @return {jQuery|Element} The original jQuery object or DOM Element.
	 */
	show: function apiShowTip(element, event) {
		// if we were supplied an event with a pageX property then it is a mouse
		// event with the information necessary to do hover intent testing
		if (isMouseEvent(event)) {
			trackMouse(event);
			session.previousX = event.pageX;
			session.previousY = event.pageY;
			$(element).data(DATA_DISPLAYCONTROLLER).show();
		} else {
			$(element).first().data(DATA_DISPLAYCONTROLLER).show(true, true);
		}
		return element;
	},

	/**
	 * Repositions the tooltip on the element.
	 * @param {jQuery|Element} element The element the tooltip is shown for.
	 * @return {jQuery|Element} The original jQuery object or DOM Element.
	 */
	reposition: function apiResetPosition(element) {
		$(element).first().data(DATA_DISPLAYCONTROLLER).resetPosition();
		return element;
	},

	/**
	 * Attempts to close any open tooltips.
	 * @param {(jQuery|Element)=} element The element with the tooltip that
	 *     should be closed (optional).
	 * @param {boolean=} immediate Disable close delay (optional).
	 * @return {jQuery|Element|undefined} The original jQuery object or DOM
	 *     Element, if one was specified.
	 */
	hide: function apiCloseTip(element, immediate) {
		if (element) {
			$(element).first().data(DATA_DISPLAYCONTROLLER).hide(immediate);
		} else {
			if (session.activeHover) {
				session.activeHover.data(DATA_DISPLAYCONTROLLER).hide(true);
			}
		}
		return element;
	},

	/**
	 * Toggles the tooltip for the specified element. This will open a closed
	 * tooltip, or close and open tooltip.
	 * @param {(jQuery|Element)} element The element with the tooltip that
	 *     should be toggled.
	 * @param {jQuery.Event=} event jQuery event for hover intent and mouse
	 *     tracking (optional).
	 */
	toggle: function apiToggle(element, event) {
		if (session.activeHover && session.activeHover.is(element)) {
			// tooltip for element is active, so close it
			$.powerTip.hide(element, !isMouseEvent(event));
		} else {
			// tooltip for element is not active, so open it
			$.powerTip.show(element, event);
		}
	},

	/**
	 * Destroy and roll back any powerTip() instance on the specified elements.
	 * If no elements are specified then all elements that the plugin is
	 * currently attached to will be rolled back.
	 * @param {(jQuery|Element)=} element The element with the powerTip instance.
	 * @return {jQuery|Element|undefined} The original jQuery object or DOM
	 *     Element, if one was specified.
	 */
	destroy: function apiDestroy(element) {
		var $element = element ? $(element) : session.elements;

		// if the plugin is not hooked to any elements then there is no point
		// trying to destroy anything, or dealing with the possible errors
		if (!session.elements || session.elements.length === 0) {
			return element;
		}

		// unhook events and destroy plugin changes to each element
		$element.off(EVENT_NAMESPACE).each(function destroy() {
			var $this = $(this),
				dataAttributes = [
					DATA_ORIGINALTITLE,
					DATA_DISPLAYCONTROLLER,
					DATA_HASACTIVEHOVER,
					DATA_FORCEDOPEN
				];

			// revert title attribute
			if ($this.data(DATA_ORIGINALTITLE)) {
				$this.attr('title', $this.data(DATA_ORIGINALTITLE));
				dataAttributes.push(DATA_POWERTIP);
			}

			// remove data attributes
			$this.removeData(dataAttributes);
		});

		// remove destroyed element from active elements collection
		session.elements = session.elements.not($element);

		// if there are no active elements left then we will unhook all of the
		// events that we've bound code to and remove the tooltip elements
		if (session.elements.length === 0) {
			$window.off(EVENT_NAMESPACE);
			$document.off(EVENT_NAMESPACE);
			session.mouseTrackingActive = false;
			session.tooltips.remove();
			session.tooltips = null;
		}

		return element;
	}
};

// API aliasing
$.powerTip.showTip = $.powerTip.show;
$.powerTip.closeTip = $.powerTip.hide;
<commit_msg>Removed unnecessary parentheses from annotation.<commit_after>/**
 * PowerTip Core
 *
 * @fileoverview  Core variables, plugin object, and API.
 * @link          http://stevenbenner.github.com/jquery-powertip/
 * @author        Steven Benner (http://stevenbenner.com/)
 * @requires      jQuery 1.7+
 */

// useful private variables
var $document = $(document),
	$window = $(window),
	$body = $('body');

// constants
var DATA_DISPLAYCONTROLLER = 'displayController',
	DATA_HASACTIVEHOVER = 'hasActiveHover',
	DATA_HASMOUSEENTER = 'hasMouseEnter',
	DATA_FORCEDOPEN = 'forcedOpen',
	DATA_HASMOUSEMOVE = 'hasMouseMove',
	DATA_MOUSEONTOTIP = 'mouseOnToPopup',
	DATA_ORIGINALTITLE = 'originalTitle',
	DATA_POWERTIP = 'powertip',
	DATA_POWERTIPJQ = 'powertipjq',
	DATA_POWERTIPTARGET = 'powertiptarget',
	EVENT_NAMESPACE = '.powertip',
	RAD2DEG = 180 / Math.PI;

/**
 * Session data
 * Private properties global to all powerTip instances
 */
var session = {
	elements: null,
	tooltips: null,
	isTipOpen: false,
	isFixedTipOpen: false,
	isClosing: false,
	tipOpenImminent: false,
	activeHover: null,
	currentX: 0,
	currentY: 0,
	previousX: 0,
	previousY: 0,
	desyncTimeout: null,
	closeDelayTimeout: null,
	mouseTrackingActive: false,
	delayInProgress: false,
	windowWidth: 0,
	windowHeight: 0,
	scrollTop: 0,
	scrollLeft: 0
};

/**
 * Collision enumeration
 * @enum {number}
 */
var Collision = {
	none: 0,
	top: 1,
	bottom: 2,
	left: 4,
	right: 8
};

/**
 * Display hover tooltips on the matched elements.
 * @param {(Object|string)} opts The options object to use for the plugin, or
 *     the name of a method to invoke on the first matched element.
 * @param {*=} [arg] Argument for an invoked method (optional).
 * @return {jQuery} jQuery object for the matched selectors.
 */
$.fn.powerTip = function(opts, arg) {
	var targetElements = this,
		options,
		tipController;

	// don't do any work if there were no matched elements
	if (!targetElements.length) {
		return targetElements;
	}

	// handle api method calls on the plugin, e.g. powerTip('hide')
	if ($.type(opts) === 'string' && $.powerTip[opts]) {
		return $.powerTip[opts].call(targetElements, targetElements, arg);
	}

	// extend options and instantiate TooltipController
	options = $.extend({}, $.fn.powerTip.defaults, opts);
	tipController = new TooltipController(options);

	// hook mouse and viewport dimension tracking
	initTracking();

	// setup the elements
	targetElements.each(function elementSetup() {
		var $this = $(this),
			dataPowertip = $this.data(DATA_POWERTIP),
			dataElem = $this.data(DATA_POWERTIPJQ),
			dataTarget = $this.data(DATA_POWERTIPTARGET),
			title;

		// handle repeated powerTip calls on the same element by destroying the
		// original instance hooked to it and replacing it with this call
		if ($this.data(DATA_DISPLAYCONTROLLER)) {
			$.powerTip.destroy($this);
		}

		// attempt to use title attribute text if there is no data-powertip,
		// data-powertipjq or data-powertiptarget. If we do use the title
		// attribute, delete the attribute so the browser will not show it
		title = $this.attr('title');
		if (!dataPowertip && !dataTarget && !dataElem && title) {
			$this.data(DATA_POWERTIP, title);
			$this.data(DATA_ORIGINALTITLE, title);
			$this.removeAttr('title');
		}

		// create hover controllers for each element
		$this.data(
			DATA_DISPLAYCONTROLLER,
			new DisplayController($this, options, tipController)
		);
	});

	// attach events to matched elements if the manual option is not enabled
	if (!options.manual) {
		// attach open events
		$.each(options.openEvents, function(idx, evt) {
			if ($.inArray(evt, options.closeEvents) > -1) {
				// event is in both openEvents and closeEvents, so toggle it
				targetElements.on(evt + EVENT_NAMESPACE, function elementToggle(event) {
					$.powerTip.toggle(this, event);
				});
			} else {
				targetElements.on(evt + EVENT_NAMESPACE, function elementOpen(event) {
					$.powerTip.show(this, event);
				});
			}
		});

		// attach close events
		$.each(options.closeEvents, function(idx, evt) {
			if ($.inArray(evt, options.openEvents) < 0) {
				targetElements.on(evt + EVENT_NAMESPACE, function elementClose(event) {
					// set immediate to true for any event without mouse info
					$.powerTip.hide(this, !isMouseEvent(event));
				});
			}
		});

		// attach escape key close event
		targetElements.on('keydown' + EVENT_NAMESPACE, function elementKeyDown(event) {
			// always close tooltip when the escape key is pressed
			if (event.keyCode === 27) {
				$.powerTip.hide(this, true);
			}
		});
	}

	// remember elements that the plugin is attached to
	session.elements = session.elements ? session.elements.add(targetElements) : targetElements;

	return targetElements;
};

/**
 * Default options for the powerTip plugin.
 */
$.fn.powerTip.defaults = {
	fadeInTime: 200,
	fadeOutTime: 100,
	followMouse: false,
	popupId: 'powerTip',
	intentSensitivity: 7,
	intentPollInterval: 100,
	closeDelay: 100,
	placement: 'n',
	smartPlacement: false,
	offset: 10,
	mouseOnToPopup: false,
	manual: false,
	openEvents: [ 'mouseenter', 'focus' ],
	closeEvents: [ 'mouseleave', 'blur' ]
};

/**
 * Default smart placement priority lists.
 * The first item in the array is the highest priority, the last is the lowest.
 * The last item is also the default, which will be used if all previous options
 * do not fit.
 */
$.fn.powerTip.smartPlacementLists = {
	n: ['n', 'ne', 'nw', 's'],
	e: ['e', 'ne', 'se', 'w', 'nw', 'sw', 'n', 's', 'e'],
	s: ['s', 'se', 'sw', 'n'],
	w: ['w', 'nw', 'sw', 'e', 'ne', 'se', 'n', 's', 'w'],
	nw: ['nw', 'w', 'sw', 'n', 's', 'se', 'nw'],
	ne: ['ne', 'e', 'se', 'n', 's', 'sw', 'ne'],
	sw: ['sw', 'w', 'nw', 's', 'n', 'ne', 'sw'],
	se: ['se', 'e', 'ne', 's', 'n', 'nw', 'se'],
	'nw-alt': ['nw-alt', 'n', 'ne-alt', 'sw-alt', 's', 'se-alt', 'w', 'e'],
	'ne-alt': ['ne-alt', 'n', 'nw-alt', 'se-alt', 's', 'sw-alt', 'e', 'w'],
	'sw-alt': ['sw-alt', 's', 'se-alt', 'nw-alt', 'n', 'ne-alt', 'w', 'e'],
	'se-alt': ['se-alt', 's', 'sw-alt', 'ne-alt', 'n', 'nw-alt', 'e', 'w']
};

/**
 * Public API
 */
$.powerTip = {
	/**
	 * Attempts to show the tooltip for the specified element.
	 * @param {jQuery|Element} element The element to open the tooltip for.
	 * @param {jQuery.Event=} event jQuery event for hover intent and mouse
	 *     tracking (optional).
	 * @return {jQuery|Element} The original jQuery object or DOM Element.
	 */
	show: function apiShowTip(element, event) {
		// if we were supplied an event with a pageX property then it is a mouse
		// event with the information necessary to do hover intent testing
		if (isMouseEvent(event)) {
			trackMouse(event);
			session.previousX = event.pageX;
			session.previousY = event.pageY;
			$(element).data(DATA_DISPLAYCONTROLLER).show();
		} else {
			$(element).first().data(DATA_DISPLAYCONTROLLER).show(true, true);
		}
		return element;
	},

	/**
	 * Repositions the tooltip on the element.
	 * @param {jQuery|Element} element The element the tooltip is shown for.
	 * @return {jQuery|Element} The original jQuery object or DOM Element.
	 */
	reposition: function apiResetPosition(element) {
		$(element).first().data(DATA_DISPLAYCONTROLLER).resetPosition();
		return element;
	},

	/**
	 * Attempts to close any open tooltips.
	 * @param {(jQuery|Element)=} element The element with the tooltip that
	 *     should be closed (optional).
	 * @param {boolean=} immediate Disable close delay (optional).
	 * @return {jQuery|Element|undefined} The original jQuery object or DOM
	 *     Element, if one was specified.
	 */
	hide: function apiCloseTip(element, immediate) {
		if (element) {
			$(element).first().data(DATA_DISPLAYCONTROLLER).hide(immediate);
		} else {
			if (session.activeHover) {
				session.activeHover.data(DATA_DISPLAYCONTROLLER).hide(true);
			}
		}
		return element;
	},

	/**
	 * Toggles the tooltip for the specified element. This will open a closed
	 * tooltip, or close and open tooltip.
	 * @param {jQuery|Element} element The element with the tooltip that
	 *     should be toggled.
	 * @param {jQuery.Event=} event jQuery event for hover intent and mouse
	 *     tracking (optional).
	 */
	toggle: function apiToggle(element, event) {
		if (session.activeHover && session.activeHover.is(element)) {
			// tooltip for element is active, so close it
			$.powerTip.hide(element, !isMouseEvent(event));
		} else {
			// tooltip for element is not active, so open it
			$.powerTip.show(element, event);
		}
	},

	/**
	 * Destroy and roll back any powerTip() instance on the specified elements.
	 * If no elements are specified then all elements that the plugin is
	 * currently attached to will be rolled back.
	 * @param {(jQuery|Element)=} element The element with the powerTip instance.
	 * @return {jQuery|Element|undefined} The original jQuery object or DOM
	 *     Element, if one was specified.
	 */
	destroy: function apiDestroy(element) {
		var $element = element ? $(element) : session.elements;

		// if the plugin is not hooked to any elements then there is no point
		// trying to destroy anything, or dealing with the possible errors
		if (!session.elements || session.elements.length === 0) {
			return element;
		}

		// unhook events and destroy plugin changes to each element
		$element.off(EVENT_NAMESPACE).each(function destroy() {
			var $this = $(this),
				dataAttributes = [
					DATA_ORIGINALTITLE,
					DATA_DISPLAYCONTROLLER,
					DATA_HASACTIVEHOVER,
					DATA_FORCEDOPEN
				];

			// revert title attribute
			if ($this.data(DATA_ORIGINALTITLE)) {
				$this.attr('title', $this.data(DATA_ORIGINALTITLE));
				dataAttributes.push(DATA_POWERTIP);
			}

			// remove data attributes
			$this.removeData(dataAttributes);
		});

		// remove destroyed element from active elements collection
		session.elements = session.elements.not($element);

		// if there are no active elements left then we will unhook all of the
		// events that we've bound code to and remove the tooltip elements
		if (session.elements.length === 0) {
			$window.off(EVENT_NAMESPACE);
			$document.off(EVENT_NAMESPACE);
			session.mouseTrackingActive = false;
			session.tooltips.remove();
			session.tooltips = null;
		}

		return element;
	}
};

// API aliasing
$.powerTip.showTip = $.powerTip.show;
$.powerTip.closeTip = $.powerTip.hide;
<|endoftext|>


<commit_before>import {MapAreaLayout} from 'app/home/map/mapAreaLayout'
import {VisualizationSelector} from 'app/home/map/imageLayerSource/visualizationSelector'
import {compose} from 'compose'
import {getAllVisualizations, hasTrainingData, preSetVisualizationOptions, supportProbability} from './classificationRecipe'
import {msg} from 'translate'
import {selectFrom} from 'stateUtils'
import {withMapAreaContext} from 'app/home/map/mapAreaContext'
import PropTypes from 'prop-types'
import React from 'react'
import _ from 'lodash'

const defaultLayerConfig = {
    panSharpen: false
}

class _ClassificationImageLayer extends React.Component {
    render() {
        const {layer, map} = this.props
        return (
            <React.Fragment>
                <MapAreaLayout
                    layer={this.canRender() ? layer : null}
                    form={this.renderImageLayerForm()}
                    map={map}
                />
            </React.Fragment>
        )
    }

    renderImageLayerForm() {
        const {recipe, source, layerConfig = {}} = this.props
        const options = this.hasLegend()
            ? [{
                label: msg('process.classification.layers.imageLayer.preSets'),
                options: preSetVisualizationOptions(recipe)
            }]
            : []
        return (
            <VisualizationSelector
                source={source}
                recipe={recipe}
                presetOptions={options}
                selectedVisParams={layerConfig.visParams}
            />
        )
    }

    hasLegend() {
        const {recipe} = this.props
        return !_.isEmpty(selectFrom(recipe, 'model.legend.entries'))
    }

    canRender() {
        const {recipe} = this.props
        const inputImagery = selectFrom(recipe, ['model.inputImagery.images']) || []
        return inputImagery.length && hasTrainingData(recipe)
    }

    componentDidMount() {
        const {recipe, layerConfig: {visParams}} = this.props
        if (!visParams && this.hasLegend()) {
            this.selectVisualization(getAllVisualizations(recipe)[0])
        }
    }

    componentDidUpdate(prevProps) {
        const {layerConfig: {visParams: prevVisParams}} = prevProps
        const {recipe} = this.props
        const allVisualizations = getAllVisualizations(recipe)
        if (prevVisParams) {
            const visParams = allVisualizations.find(({id, bands}) =>
                _.isEqual([id, bands], [prevVisParams.id, prevVisParams.bands])
            )
            if (!visParams) {
                this.selectVisualization(getAllVisualizations(recipe)[0])
            } else if (!_.isEqual(visParams, prevVisParams)) {
                this.selectVisualization(visParams)
            }
        } else {
            allVisualizations.length && this.selectVisualization(allVisualizations[0])
        }
    }

    selectVisualization(visParams) {
        const {mapAreaContext: {updateLayerConfig}} = this.props
        updateLayerConfig({visParams})
    }
}

export const ClassificationImageLayer = compose(
    _ClassificationImageLayer,
    withMapAreaContext()
)

ClassificationImageLayer.defaultProps = {
    layerConfig: defaultLayerConfig
}

ClassificationImageLayer.propTypes = {
    recipe: PropTypes.object.isRequired,
    source: PropTypes.object.isRequired,
    layer: PropTypes.object,
    layerConfig: PropTypes.object,
    map: PropTypes.object
}

export const classificationDataTypes = recipe => {
    const legend = selectFrom(recipe, 'model.legend')
    const entries = _.sortBy(legend.entries, 'value')
    if (!recipe.ui.initialized || !entries.length) {
        return []
    }
    const classifierType = selectFrom(recipe, 'model.classifier.type')
    const min = entries[0].value
    const max = _.last(entries).value
    const dataTypes = {};
    [
        {band: 'class', precision: 'int', min, max},
        {band: 'regression', precision: 'float', min, max},
        {band: 'class_probability', precision: 'int', min: 0, max: 100},
        ...legend.entries.map(({value}) => supportProbability(classifierType) && {
            band: `probability_${value}`, precision: 'int', min: 0, max: 100
        })
    ].filter(o => o).forEach(({band, precision, min, max}) => dataTypes[band] = {precision, min, max})
    return dataTypes
}
<commit_msg>Fixed crash when creating classifications.<commit_after>import {MapAreaLayout} from 'app/home/map/mapAreaLayout'
import {VisualizationSelector} from 'app/home/map/imageLayerSource/visualizationSelector'
import {compose} from 'compose'
import {getAllVisualizations, hasTrainingData, preSetVisualizationOptions, supportProbability} from './classificationRecipe'
import {msg} from 'translate'
import {selectFrom} from 'stateUtils'
import {withMapAreaContext} from 'app/home/map/mapAreaContext'
import PropTypes from 'prop-types'
import React from 'react'
import _ from 'lodash'

const defaultLayerConfig = {
    panSharpen: false
}

class _ClassificationImageLayer extends React.Component {
    render() {
        const {layer, map} = this.props
        return (
            <React.Fragment>
                <MapAreaLayout
                    layer={this.canRender() ? layer : null}
                    form={this.renderImageLayerForm()}
                    map={map}
                />
            </React.Fragment>
        )
    }

    renderImageLayerForm() {
        const {recipe, source, layerConfig = {}} = this.props
        const options = this.hasLegend()
            ? [{
                label: msg('process.classification.layers.imageLayer.preSets'),
                options: preSetVisualizationOptions(recipe)
            }]
            : []
        return (
            <VisualizationSelector
                source={source}
                recipe={recipe}
                presetOptions={options}
                selectedVisParams={layerConfig.visParams}
            />
        )
    }

    hasLegend() {
        const {recipe} = this.props
        return !_.isEmpty(selectFrom(recipe, 'model.legend.entries'))
    }

    canRender() {
        const {recipe} = this.props
        const inputImagery = selectFrom(recipe, ['model.inputImagery.images']) || []
        return inputImagery.length && hasTrainingData(recipe)
    }

    componentDidMount() {
        const {recipe, layerConfig: {visParams}} = this.props
        if (!visParams && this.hasLegend()) {
            this.selectVisualization(getAllVisualizations(recipe)[0])
        }
    }

    componentDidUpdate(prevProps) {
        const {layerConfig: {visParams: prevVisParams}} = prevProps
        const {recipe} = this.props
        const allVisualizations = getAllVisualizations(recipe)
        if (prevVisParams) {
            const visParams = allVisualizations.find(({id, bands}) =>
                _.isEqual([id, bands], [prevVisParams.id, prevVisParams.bands])
            )
            if (!visParams) {
                this.selectVisualization(getAllVisualizations(recipe)[0])
            } else if (!_.isEqual(visParams, prevVisParams)) {
                this.selectVisualization(visParams)
            }
        } else {
            allVisualizations.length && this.selectVisualization(allVisualizations[0])
        }
    }

    selectVisualization(visParams) {
        const {mapAreaContext: {updateLayerConfig}} = this.props
        updateLayerConfig({visParams})
    }
}

export const ClassificationImageLayer = compose(
    _ClassificationImageLayer,
    withMapAreaContext()
)

ClassificationImageLayer.defaultProps = {
    layerConfig: defaultLayerConfig
}

ClassificationImageLayer.propTypes = {
    recipe: PropTypes.object.isRequired,
    source: PropTypes.object.isRequired,
    layer: PropTypes.object,
    layerConfig: PropTypes.object,
    map: PropTypes.object
}

export const classificationDataTypes = recipe => {
    const legend = selectFrom(recipe, 'model.legend')
    if (!recipe.ui.initialized || !legend.entries.length) {
        return []
    }
    const entries = _.sortBy(legend.entries, 'value')
    const classifierType = selectFrom(recipe, 'model.classifier.type')
    const min = entries[0].value
    const max = _.last(entries).value
    const dataTypes = {};
    [
        {band: 'class', precision: 'int', min, max},
        {band: 'regression', precision: 'float', min, max},
        {band: 'class_probability', precision: 'int', min: 0, max: 100},
        ...legend.entries.map(({value}) => supportProbability(classifierType) && {
            band: `probability_${value}`, precision: 'int', min: 0, max: 100
        })
    ].filter(o => o).forEach(({band, precision, min, max}) => dataTypes[band] = {precision, min, max})
    return dataTypes
}
<|endoftext|>


<commit_before>/*
 @author Zakai Hamilton
 @component ServiceNetMonitor
 @prerequisites npm install pcap2
 */

screens.service.netmonitor = function ServiceNetMonitor(me) {
    me.setup = async function () {
        me.device = null;
        me.packets = [];
        me.timer = null;
        me.runIndex = 0;
        me.streamIndex = 0;
        me.options = { pushPackets: true, collectPackets: false, filterNode: "" };
        me.statistics = {};
        var config = await me.core.service.config(me.id);
        if (config) {
            me.pcap = require('pcap2');
            me.tracker = new me.pcap.TCPTracker();
            me.util = require('util');
            var devices = config.device;
            if (devices && !Array.isArray(devices)) {
                devices = [devices];
            }
            var filter = config.filter;
            if (filter) {
                filter = filter.replace("@port", me.core.http.port);
                me.log("using filter: " + filter);
            }
            for (var device of devices) {
                me.session = null;
                try {
                    me.session = new me.pcap.Session(device, { filter });
                } catch (e) {

                }
                if (me.session) {
                    me.log("connected through device: " + device + " filter: " + filter);
                    me.device = device;
                    break;
                }
            }
            if (me.session) {
                me.session.on('packet', function (rawPacket) {
                    if (!me.options.collectPackets) {
                        return;
                    }
                    var fullPacket = me.pcap.decode.packet(rawPacket);
                    if (filter && filter.includes("tcp")) {
                        me.tracker.track_packet(fullPacket);
                    }
                    var fullPacket = JSON.parse(JSON.stringify(fullPacket));
                    var packet_sec = me.core.json.traverse(fullPacket, "pcap_header.tv_sec").value;
                    var packet_len = me.core.json.traverse(fullPacket, "pcap_header.len").value;
                    var packet_source = me.core.json.traverse(fullPacket, "payload.payload.saddr.addr").value;
                    var packet_target = me.core.json.traverse(fullPacket, "payload.payload.daddr.addr").value;
                    var effects = {};
                    var netcontrol = me.service.netcontrol;
                    if (netcontrol) {
                        var effects = netcontrol.effects;
                    }
                    var packet = {
                        source: packet_source,
                        target: packet_target,
                        size: packet_len,
                        time: packet_sec,
                        runIndex: me.runIndex,
                        streamIndex: me.streamIndex,
                        effects: effects
                    };
                    var addPacket = true;
                    if (me.options.filterNode) {
                        var regex = me.core.string.regex(me.options.filterNode);
                        addPacket = false;
                        if(packet.source && packet.target) {
                            var source = String(packet.source).replace(/,/g, '.');
                            var target = String(packet.target).replace(/,/g, '.');
                            if(source.search(regex) || target.search(regex)) {
                                addPacket = true;
                            }
                        }
                    }
                    if (addPacket) {
                        me.packets.push(packet);
                    }
                    else {
                        if (me.statistics.packetsIgnored) {
                            me.statistics.packetsIgnored++;
                        }
                        else {
                            me.statistics.packetsIgnored = 0;
                        }
                    }
                });
                if (filter && filter.includes("tcp")) {
                    me.tracker.on('start', function (session) {
                        me.log("Start of TCP session between " + session.src_name + " and " + session.dst_name);
                    });
                    me.tracker.on('end', function (session) {
                        me.log("End of TCP session between " + session.src_name + " and " + session.dst_name);
                    });
                }
                setInterval(() => {
                    me.log("there are " + me.packets.length + " packets in queue");
                    me.log("monitor options: " + JSON.stringify(me.options));
                    me.log("monitor statistics: " + JSON.stringify(me.statistics));
                    if (me.options.pushPackets) {
                        var packets = me.packets;
                        me.packets = [];
                        if (packets && packets.length) {
                            await me.manager.packet.push(packets);
                            me.log("sent packets to server");
                        }
                    }
                }, parseInt(config.delay));
            } else {
                me.log("cannot connect through any of the following devices: " + devices);
            }
        }
    };
    me.getOptions = function () {
        return me.options;
    };
    me.setOptions = function (options) {
        me.options = Object.assign({}, me.options, options);
    };
    me.reset = function () {
        me.packets = [];
        me.runIndex = 0;
        me.streamIndex = 0;
        me.statistics = {};
    };
    me.newStream = function () {
        me.streamIndex++;
    };
    me.newRun = function () {
        me.runIndex++;
    }
};
<commit_msg>bug fix<commit_after>/*
 @author Zakai Hamilton
 @component ServiceNetMonitor
 @prerequisites npm install pcap2
 */

screens.service.netmonitor = function ServiceNetMonitor(me) {
    me.setup = async function () {
        me.device = null;
        me.packets = [];
        me.timer = null;
        me.runIndex = 0;
        me.streamIndex = 0;
        me.options = { pushPackets: true, collectPackets: false, filterNode: "" };
        me.statistics = {};
        var config = await me.core.service.config(me.id);
        if (config) {
            me.pcap = require('pcap2');
            me.tracker = new me.pcap.TCPTracker();
            me.util = require('util');
            var devices = config.device;
            if (devices && !Array.isArray(devices)) {
                devices = [devices];
            }
            var filter = config.filter;
            if (filter) {
                filter = filter.replace("@port", me.core.http.port);
                me.log("using filter: " + filter);
            }
            for (var device of devices) {
                me.session = null;
                try {
                    me.session = new me.pcap.Session(device, { filter });
                } catch (e) {

                }
                if (me.session) {
                    me.log("connected through device: " + device + " filter: " + filter);
                    me.device = device;
                    break;
                }
            }
            if (me.session) {
                me.session.on('packet', function (rawPacket) {
                    if (!me.options.collectPackets) {
                        return;
                    }
                    var fullPacket = me.pcap.decode.packet(rawPacket);
                    if (filter && filter.includes("tcp")) {
                        me.tracker.track_packet(fullPacket);
                    }
                    var fullPacket = JSON.parse(JSON.stringify(fullPacket));
                    var packet_sec = me.core.json.traverse(fullPacket, "pcap_header.tv_sec").value;
                    var packet_len = me.core.json.traverse(fullPacket, "pcap_header.len").value;
                    var packet_source = me.core.json.traverse(fullPacket, "payload.payload.saddr.addr").value;
                    var packet_target = me.core.json.traverse(fullPacket, "payload.payload.daddr.addr").value;
                    var effects = {};
                    var netcontrol = me.service.netcontrol;
                    if (netcontrol) {
                        var effects = netcontrol.effects;
                    }
                    var packet = {
                        source: packet_source,
                        target: packet_target,
                        size: packet_len,
                        time: packet_sec,
                        runIndex: me.runIndex,
                        streamIndex: me.streamIndex,
                        effects: effects
                    };
                    var addPacket = true;
                    if (me.options.filterNode) {
                        var regex = me.core.string.regex(me.options.filterNode);
                        addPacket = false;
                        if(packet.source && packet.target) {
                            var source = String(packet.source).replace(/,/g, '.');
                            var target = String(packet.target).replace(/,/g, '.');
                            if(source.search(regex) || target.search(regex)) {
                                addPacket = true;
                            }
                        }
                    }
                    if (addPacket) {
                        me.packets.push(packet);
                    }
                    else {
                        if (me.statistics.packetsIgnored) {
                            me.statistics.packetsIgnored++;
                        }
                        else {
                            me.statistics.packetsIgnored = 0;
                        }
                    }
                });
                if (filter && filter.includes("tcp")) {
                    me.tracker.on('start', function (session) {
                        me.log("Start of TCP session between " + session.src_name + " and " + session.dst_name);
                    });
                    me.tracker.on('end', function (session) {
                        me.log("End of TCP session between " + session.src_name + " and " + session.dst_name);
                    });
                }
                setInterval(async () => {
                    me.log("there are " + me.packets.length + " packets in queue");
                    me.log("monitor options: " + JSON.stringify(me.options));
                    me.log("monitor statistics: " + JSON.stringify(me.statistics));
                    if (me.options.pushPackets) {
                        var packets = me.packets;
                        me.packets = [];
                        if (packets && packets.length) {
                            await me.manager.packet.push(packets);
                            me.log("sent packets to server");
                        }
                    }
                }, parseInt(config.delay));
            } else {
                me.log("cannot connect through any of the following devices: " + devices);
            }
        }
    };
    me.getOptions = function () {
        return me.options;
    };
    me.setOptions = function (options) {
        me.options = Object.assign({}, me.options, options);
    };
    me.reset = function () {
        me.packets = [];
        me.runIndex = 0;
        me.streamIndex = 0;
        me.statistics = {};
    };
    me.newStream = function () {
        me.streamIndex++;
    };
    me.newRun = function () {
        me.runIndex++;
    }
};
<|endoftext|>


<commit_before>$(function(){
    // re-create the bootstrap collapse... but slide from left
    $(document).on('click', '[data-toggle="slide-collapse"]', function(event) {
        event.preventDefault(event);
        var collapsed = this.classList.contains('collapsed');
        $btn = $(this);
        target = $btn.data('target');
        $('[data-toggle="slide-collapse"][data-target="' + target + '"]').toggleClass('collapsed', collapsed);
        if (!collapsed) {
            hide_sidebar();
        } else {
            show_sidebar();
        }
    });
    const activeClass = 'active';

    // checks for sidebar items that match the current page
    // uses the 'active' property of the *_menu.php files to match

    // highlight active time on page load
    checkActiveHashLinks()

    // highlight active item on browser hash change
    window.addEventListener("hashchange", function(event) {
        checkActiveHashLinks()
    }, false);

    // clear active item on click
    $(document).on('click', '#sidebar [data-active]', function(event) {
        $(event.currentTarget).parents('ul').first().find('li').removeClass(activeClass)
    });

    /**
     * checks the sidebar for any links that match the current url and adds the active class to it's parent <li>
     */
    function checkActiveHashLinks() {
        var links_with_active = document.querySelectorAll('#sidebar [data-active]')
        if(links_with_active) {
            for(n in Object.values(links_with_active)) {
                let link = links_with_active[n]
                // mark active link on load
                if(link.dataset.active === window.location.href) {
                    link.parentNode.classList.add(activeClass)
                }
            }
        }
    }

    // open sidebar if active page link clicked
    $('#left-nav li a').on('click', function(event) {
        // if the link has a [data-is-link] attribute navigate to the link
        if(!event.currentTarget.dataset.isLink) {
            event.preventDefault();
        } else {
            var href = event.currentTarget.href;
            window.location.href = href;
            return false;
        }

        const $link = $(this);
        const $sidebar_inner = $($link.data('sidebar')); // (.sidebar_inner)

        // show 2nd level - if on 3rd level
        let secondlevel_menuitems = $('.sidebar-menu > li.collapse').length;
        let open_secondlevel_menuitems = $('.sidebar-menu > li.collapse.in').length
        let thirdLevelOpen = secondlevel_menuitems !== open_secondlevel_menuitems;

        // alter tab states
        $link.parent().addClass(activeClass).siblings().removeClass(activeClass);

        // hide if not sidebar found
        if ($sidebar_inner.length == 0) {
            hide_sidebar();
        } else {
            if ($('body').hasClass('collapsed')) {
                // closed sidebar
                show_sidebar();
                $sidebar_inner.addClass(activeClass).siblings().removeClass(activeClass)
                if ($('body').hasClass('auto')) $('body').toggleClass('auto manual')

            } else {
                // already open sidebar
                if ($sidebar_inner.hasClass(activeClass)) {
                    // hide sidebar if clicked item already active and on 2nd level
                    if(!thirdLevelOpen) {
                        hide_sidebar();
                    } else {
                        hideMenuItems(event);
                        // @todo: make the sidebar show 2nd level and not hide_sidebar()
                        hide_sidebar();
                    }
                    if ($('body').hasClass('auto')) $('body').toggleClass('auto manual')
                } else {
                    // enable correct sidebar inner based on clicked tab
                    $sidebar_inner.addClass(activeClass).find('li a').each(function(){
                        $(this)[0].tabIndex = "0";
                    })
                    $sidebar_inner.siblings('.sidebar-inner').each(function(i,n) {
                        $(this).removeClass(activeClass);
                        $(this).find('li a').each(function(j,m){
                            $(this)[0].tabIndex = "-1";
                        })
                    })
                }
            }
        }
    });

    // on trigger sidebar hide/show
    $('#sidebar').on('hide.sidebar.collapse show.sidebar.collapse', function(event){
        // resize after slight delay
        var interval = setInterval(function(){
            if (typeof graph_resize === 'function'){
                graph_resize();
            }
            if (typeof graph_draw === 'function'){
                graph_draw();
            }
        }, 75);
        // stop resizing
        setTimeout(function(){
            clearInterval(interval);
        }, 300);
    });

    // on finish sidebar hide/show
    $('#sidebar').on('hidden.sidebar.collapse shown.sidebar.collapse', function(event){
        // resize once finished animating
        if (typeof graph_resize === 'function'){
            graph_resize();
        }
        if (typeof graph_draw === 'function'){
            graph_draw();
        }
        if (typeof resize === 'function'){
            resize();
        }
        if (typeof app_resize === 'function'){
            app_resize();
        }
        if (typeof vis_resize === 'function'){
            vis_resize();
        }
    });

    // hide sidebar on smaller devices
    window.addEventListener('resize', function(event) {

        if ($('body').hasClass('auto')) {
            if ($(window).width() < 870) {
                hide_sidebar();
                document.body.classList.add('narrow');
            }
            if ($(window).width() >= 870 && $(document.body).hasClass('collapsed')) {
                show_sidebar();
                document.body.classList.remove('narrow');
            }
        } else {
            if (!$(document.body).hasClass('collapsed')) {
                if ($(window).width() < 870) {
                    $(".content-container").css("margin","2.7rem 0 0 0");
                } else {
                    $('body').toggleClass('manual auto')
                    $(".content-container").css("margin","2.7rem 0 0 15rem");
                }
            }
        }
    })

    // hide sidebar on load on narrow devices
    if ($(window).width() < 870) {
        document.body.classList.add('narrow','collapsed');
        $('#sidebar').trigger('hidden.sidebar.collapse');
        hide_sidebar();
        // allow narrow screens to expand sidebar after delay to avoid animation of hiding sidebar
        setTimeout(function(){
            document.body.classList.add('has-animation');
        }, 500);
    }
    /**
     * If menu 3rd level menu shown shrink 2nd level entries
     *
     * @param {object} [event] mouse event if triggered by click
     */
    function hideMenuItems(event) {
        if (!event) return;

        var $menu = $(event.target).parents('ul').first();
        var clicked = false;
        if (typeof event !== 'undefined') {
            event.preventDefault();
            event.stopPropagation();
            clicked = true;
        }
        let link = $menu.find('li.active a');
        if (clicked) {
            // hide the back arrow from active third level links - on click
            $(event.target).parents('.nav').first().find('.active .third-level-indicator').toggleClass('hidden');
        }
        let active_menu = link.parents('.sidebar-menu').first();
        if(active_menu.length !== 1) return; // no menu found

        let active_menu_name = active_menu.attr('id').split('-');
        active_menu_name.shift();

        path = window.path; // e.g: http://localhost/emoncms
        if(typeof path === 'undefined') {
            console.log("Sidebar error: path undefined");
            path = '';
        }
        let relative_path = window.location.href.replace(path,''); // eg subtracts http://localhost/emoncms from http://localhost/emoncms/feed/list
        let controller = relative_path.split('/')[0]; // eg. feed
        let include_id = [active_menu_name,controller,'sidebar','include'].join('-'); // eg. setup-feed-sidebar-include
        let include = $('#' + include_id);

        if (include.length == 1 && clicked) {
            // show 3rd level menu
            include.toggleClass('in');
            // hide 2nd level menu items
            $menu.find('li').not('.active').toggleClass('in');
        }
    }
    function getQueryStringValue (key) {
        return decodeURIComponent(window.location.search.replace(new RegExp("^(?:.*[&\\?]" + encodeURIComponent(key).replace(/[\.\+\*]/g, "\\$&") + "(?:\\=([^&]*))?)?.*$", "i"), "$1"));
    }
    // add current page to user's bookmark list
    $('#set-bookmark, #remove-bookmark').click(function(event){
        event.preventDefault();
        var bookmarks = [];
        var $nav = $('#footer_nav');
        var currentPage = window.location.href.replace(path,'');
        var key = 'bookmarks';
        var $button = $(this);
        var $icon = $button.find('.icon');
        var remove = $icon.is('.star');
        var currentTitle = $('#sidebar .sidebar-menu li.active a').first().text();
        if(currentTitle.length==0) currentTitle = $('h1').first().text();
        if(currentTitle.length==0) currentTitle = $('h2').first().text();
        if(currentTitle.length==0) currentTitle = $('h3').first().text();
        if(currentTitle.length==0) currentTitle = document.title;
        if(getQueryStringValue("name")) {
            currentTitle = decodeURI(getQueryStringValue("name").replace('+',' '));
        }
        // get current bookmarks
        $.get(path+'user/preferences.json',{preferences:key})
        .done(function(data){
            // catch json parsing errors
            bookmarks = data || [];
            if(!remove) {
            // remove current page to bookmarks
                var unique = false;
                for(b in bookmarks) {
                    if (bookmarks[b]['path'] && bookmarks[b]['path'] !== currentPage){
                        unique = true;
                    }
                }
                if(unique || bookmarks.length === 0){
                    bookmarks.push({
                        'path': currentPage,
                        'text': currentTitle
                    })
                }
            } else {
            // remove current page from bookmarks
                var newBookmarks = [];
                for(b in bookmarks) {
                    if (bookmarks[b]['path'] && bookmarks[b]['path'] !== currentPage){
                        newBookmarks.push(bookmarks[b]);
                    }
                }
                bookmarks = newBookmarks;
            }
            // save new user preferences
            $.post(path+'user/preferences.json', {
                preferences: {
                    bookmarks: JSON.stringify(bookmarks)
                }
            }, function(data) {
                var $menu = $('#sidebar_bookmarks');
                var $template = $('#bookmark_link');
                if(data.success) {
                    // saved successfully change icon
                    $('#set-bookmark, #remove-bookmark').parent().toggleClass('d-none');
                    if(!remove) {
                        // add new item to menu
                        $($template.html()).appendTo($menu)
                        .find('a').attr({
                            href: path+currentPage,
                            title: currentTitle
                        })
                        .text(currentTitle).hide().fadeIn();
                        $menu.trigger('bookmarks:updated');
                        $nav.fadeIn();

                    } else {
                        // remove entry from menu
                        $.each($menu.find('li'), function(n, elem){
                            $li = $(elem);
                            var relative = $li.find('a').attr('href').replace(path,'');
                            if(relative === currentPage) {
                                $li.animate({height:0},function(){
                                    $(this).remove();
                                    $menu.trigger('bookmarks:updated');
                                    setTimeout(function(){
                                        if($menu.find('li').length == 0) {
                                            $nav.fadeOut();
                                        }
                                        // $nav.toggleClass('d-none', $menu.find('li').length == 0);
                                    },400)
                                });
                            }
                        });
                    }
                }
            });
        });
    });

    // show/hide sidebar includes
    $(document).on('click', '#menu-emoncms li.active a', hideMenuItems);
    // show hide 2nd / 3rd menu items
    // setTimeout(hideMenuItems, 100);

    // save a cookie to remember user's choice to hide or show the bookmarks
    $('#sidebar_bookmarks').on('show hide', function(event) {
        var bookmarks_collapsed = event.type !== 'show';
        docCookies.setItem('bookmarks_collapsed', bookmarks_collapsed)
    })

    $(document).keyup(function(e) {
        if (e.keyCode == 27) { // escape key maps to keycode `27`
            $('.navbar .dropdown').removeClass('open')
        }
    });

}); // end of jquery ready()

// trigger the events to allow module js scripts to attach actions to the events
function show_sidebar(options) {
    // @note: assumes the css animation takes 300ms
    $('#sidebar').trigger('show.sidebar.collapse');
    setTimeout(function(){
        $('#sidebar').trigger('shown.sidebar.collapse');
    }, 350);
    $('body').removeClass('collapsed').addClass('expanded');

    if ($(window).width() < 870) {
        $(".content-container").css("margin","2.7rem 0 0 0");
    } else {
        $(".content-container").css("margin","2.7rem 0 0 15rem");
    }
}
function hide_sidebar(options) {
    // @note: assumes the css animation takes 300ms
    $('#sidebar').trigger('hide.sidebar.collapse');
    setTimeout(function(){
        $('#sidebar').trigger('hidden.sidebar.collapse');
    }, 350);
    $('body').addClass('collapsed').removeClass('expanded');

    $(".content-container").css("margin","2.7rem auto 0 auto");
}

// backward compatible empty function
if(typeof init_sidebar !== 'function') var init_sidebar = function(){}

// get/set document cookies
var docCookies = {
    getItem: function (sKey) {
      if (!sKey) { return null; }
      return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
    },
    setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
      if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return false; }
      var sExpires = "";
      if (vEnd) {
        switch (vEnd.constructor) {
          case Number:
          //   sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd;
            sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; expires=" + (new Date(vEnd * 1e3 + Date.now())).toUTCString();
            break;
          case String:
            sExpires = "; expires=" + vEnd;
            break;
          case Date:
            sExpires = "; expires=" + vEnd.toUTCString();
            break;
        }
      }
      document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : "");
      return true;
    },
    removeItem: function (sKey, sPath, sDomain) {
      if (!this.hasItem(sKey)) { return false; }
      document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "");
      return true;
    },
    hasItem: function (sKey) {
      if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return false; }
      return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
    },
    keys: function () {
      var aKeys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, "").split(/\s*(?:\=[^;]*)?;\s*/);
      for (var nLen = aKeys.length, nIdx = 0; nIdx < nLen; nIdx++) { aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]); }
      return aKeys;
    }
  };

<commit_msg>fixed 3 level 'back' button<commit_after>$(function(){
    // re-create the bootstrap collapse... but slide from left
    $(document).on('click', '[data-toggle="slide-collapse"]', function(event) {
        event.preventDefault(event);
        var collapsed = this.classList.contains('collapsed');
        $btn = $(this);
        target = $btn.data('target');
        $('[data-toggle="slide-collapse"][data-target="' + target + '"]').toggleClass('collapsed', collapsed);
        if (!collapsed) {
            hide_sidebar();
        } else {
            show_sidebar();
        }
    });
    const activeClass = 'active';

    // checks for sidebar items that match the current page
    // uses the 'active' property of the *_menu.php files to match

    // highlight active time on page load
    checkActiveHashLinks()

    // highlight active item on browser hash change
    window.addEventListener("hashchange", function(event) {
        checkActiveHashLinks()
    }, false);

    // clear active item on click
    $(document).on('click', '#sidebar [data-active]:not([data-sidebar])', function(event) {
        $(event.currentTarget).parents('ul').first().find('li').removeClass(activeClass)
    });

    /**
     * checks the sidebar for any links that match the current url and adds the active class to it's parent <li>
     */
    function checkActiveHashLinks() {
        var links_with_active = document.querySelectorAll('#sidebar [data-active]')
        if(links_with_active) {
            for(n in Object.values(links_with_active)) {
                let link = links_with_active[n]
                // mark active link on load
                if(link.dataset.active === window.location.href) {
                    link.parentNode.classList.add(activeClass)
                }
            }
        }
    }

    // open sidebar if active page link clicked
    $('#left-nav li a').on('click', function(event) {
        // if the link has a [data-is-link] attribute navigate to the link
        if(!event.currentTarget.dataset.isLink) {
            event.preventDefault();
        } else {
            var href = event.currentTarget.href;
            window.location.href = href;
            return false;
        }

        const $link = $(this);
        const $sidebar_inner = $($link.data('sidebar')); // (.sidebar_inner)

        // show 2nd level - if on 3rd level
        let secondlevel_menuitems = $('.sidebar-menu > li.collapse').length;
        let open_secondlevel_menuitems = $('.sidebar-menu > li.collapse.in').length
        let thirdLevelOpen = secondlevel_menuitems !== open_secondlevel_menuitems;

        // alter tab states
        $link.parent().addClass(activeClass).siblings().removeClass(activeClass);

        // hide if not sidebar found
        if ($sidebar_inner.length == 0) {
            hide_sidebar();
        } else {
            if ($('body').hasClass('collapsed')) {
                // closed sidebar
                show_sidebar();
                $sidebar_inner.addClass(activeClass).siblings().removeClass(activeClass)
                if ($('body').hasClass('auto')) $('body').toggleClass('auto manual')

            } else {
                // already open sidebar
                if ($sidebar_inner.hasClass(activeClass)) {
                    // hide sidebar if clicked item already active and on 2nd level
                    if(!thirdLevelOpen) {
                        hide_sidebar();
                    } else {
                        hideMenuItems(event);
                        // @todo: make the sidebar show 2nd level and not hide_sidebar()
                        hide_sidebar();
                    }
                    if ($('body').hasClass('auto')) $('body').toggleClass('auto manual')
                } else {
                    // enable correct sidebar inner based on clicked tab
                    $sidebar_inner.addClass(activeClass).find('li a').each(function(){
                        $(this)[0].tabIndex = "0";
                    })
                    $sidebar_inner.siblings('.sidebar-inner').each(function(i,n) {
                        $(this).removeClass(activeClass);
                        $(this).find('li a').each(function(j,m){
                            $(this)[0].tabIndex = "-1";
                        })
                    })
                }
            }
        }
    });

    // on trigger sidebar hide/show
    $('#sidebar').on('hide.sidebar.collapse show.sidebar.collapse', function(event){
        // resize after slight delay
        var interval = setInterval(function(){
            if (typeof graph_resize === 'function'){
                graph_resize();
            }
            if (typeof graph_draw === 'function'){
                graph_draw();
            }
        }, 75);
        // stop resizing
        setTimeout(function(){
            clearInterval(interval);
        }, 300);
    });

    // on finish sidebar hide/show
    $('#sidebar').on('hidden.sidebar.collapse shown.sidebar.collapse', function(event){
        // resize once finished animating
        if (typeof graph_resize === 'function'){
            graph_resize();
        }
        if (typeof graph_draw === 'function'){
            graph_draw();
        }
        if (typeof resize === 'function'){
            resize();
        }
        if (typeof app_resize === 'function'){
            app_resize();
        }
        if (typeof vis_resize === 'function'){
            vis_resize();
        }
    });

    // hide sidebar on smaller devices
    window.addEventListener('resize', function(event) {

        if ($('body').hasClass('auto')) {
            if ($(window).width() < 870) {
                hide_sidebar();
                document.body.classList.add('narrow');
            }
            if ($(window).width() >= 870 && $(document.body).hasClass('collapsed')) {
                show_sidebar();
                document.body.classList.remove('narrow');
            }
        } else {
            if (!$(document.body).hasClass('collapsed')) {
                if ($(window).width() < 870) {
                    $(".content-container").css("margin","2.7rem 0 0 0");
                } else {
                    $('body').toggleClass('manual auto')
                    $(".content-container").css("margin","2.7rem 0 0 15rem");
                }
            }
        }
    })

    // hide sidebar on load on narrow devices
    if ($(window).width() < 870) {
        document.body.classList.add('narrow','collapsed');
        $('#sidebar').trigger('hidden.sidebar.collapse');
        hide_sidebar();
        // allow narrow screens to expand sidebar after delay to avoid animation of hiding sidebar
        setTimeout(function(){
            document.body.classList.add('has-animation');
        }, 500);
    }
    /**
     * If menu 3rd level menu shown shrink 2nd level entries
     *
     * @param {object} [event] mouse event if triggered by click
     */
    function hideMenuItems(event) {
        if (!event) return;

        var $menu = $(event.target).parents('ul').first();
        var clicked = false;
        if (typeof event !== 'undefined') {
            event.preventDefault();
            event.stopPropagation();
            clicked = true;
        }
        let link = $menu.find('li.active a');
        if (clicked) {
            // hide the back arrow from active third level links - on click
            $(event.target).parents('.nav').first().find('.active .third-level-indicator').toggleClass('hidden');
        }
        let active_menu = link.parents('.sidebar-menu').first();
        if(active_menu.length !== 1) return; // no menu found

        let active_menu_name = active_menu.attr('id').split('-');
        active_menu_name.shift();

        path = window.path; // e.g: http://localhost/emoncms
        if(typeof path === 'undefined') {
            console.log("Sidebar error: path undefined");
            path = '';
        }
        let relative_path = window.location.href.replace(path,''); // eg subtracts http://localhost/emoncms from http://localhost/emoncms/feed/list
        let controller = relative_path.split('/')[0]; // eg. feed
        let include_id = [active_menu_name,controller,'sidebar','include'].join('-'); // eg. setup-feed-sidebar-include
        let include = $('#' + include_id);

        if (include.length == 1 && clicked) {
            // show 3rd level menu
            include.toggleClass('in');
            // hide 2nd level menu items
            $menu.find('li').not('.active').toggleClass('in');
        }
    }
    function getQueryStringValue (key) {
        return decodeURIComponent(window.location.search.replace(new RegExp("^(?:.*[&\\?]" + encodeURIComponent(key).replace(/[\.\+\*]/g, "\\$&") + "(?:\\=([^&]*))?)?.*$", "i"), "$1"));
    }
    // add current page to user's bookmark list
    $('#set-bookmark, #remove-bookmark').click(function(event){
        event.preventDefault();
        var bookmarks = [];
        var $nav = $('#footer_nav');
        var currentPage = window.location.href.replace(path,'');
        var key = 'bookmarks';
        var $button = $(this);
        var $icon = $button.find('.icon');
        var remove = $icon.is('.star');
        var currentTitle = $('#sidebar .sidebar-menu li.active a').first().text();
        if(currentTitle.length==0) currentTitle = $('h1').first().text();
        if(currentTitle.length==0) currentTitle = $('h2').first().text();
        if(currentTitle.length==0) currentTitle = $('h3').first().text();
        if(currentTitle.length==0) currentTitle = document.title;
        if(getQueryStringValue("name")) {
            currentTitle = decodeURI(getQueryStringValue("name").replace('+',' '));
        }
        // get current bookmarks
        $.get(path+'user/preferences.json',{preferences:key})
        .done(function(data){
            // catch json parsing errors
            bookmarks = data || [];
            if(!remove) {
            // remove current page to bookmarks
                var unique = false;
                for(b in bookmarks) {
                    if (bookmarks[b]['path'] && bookmarks[b]['path'] !== currentPage){
                        unique = true;
                    }
                }
                if(unique || bookmarks.length === 0){
                    bookmarks.push({
                        'path': currentPage,
                        'text': currentTitle
                    })
                }
            } else {
            // remove current page from bookmarks
                var newBookmarks = [];
                for(b in bookmarks) {
                    if (bookmarks[b]['path'] && bookmarks[b]['path'] !== currentPage){
                        newBookmarks.push(bookmarks[b]);
                    }
                }
                bookmarks = newBookmarks;
            }
            // save new user preferences
            $.post(path+'user/preferences.json', {
                preferences: {
                    bookmarks: JSON.stringify(bookmarks)
                }
            }, function(data) {
                var $menu = $('#sidebar_bookmarks');
                var $template = $('#bookmark_link');
                if(data.success) {
                    // saved successfully change icon
                    $('#set-bookmark, #remove-bookmark').parent().toggleClass('d-none');
                    if(!remove) {
                        // add new item to menu
                        $($template.html()).appendTo($menu)
                        .find('a').attr({
                            href: path+currentPage,
                            title: currentTitle
                        })
                        .text(currentTitle).hide().fadeIn();
                        $menu.trigger('bookmarks:updated');
                        $nav.fadeIn();

                    } else {
                        // remove entry from menu
                        $.each($menu.find('li'), function(n, elem){
                            $li = $(elem);
                            var relative = $li.find('a').attr('href').replace(path,'');
                            if(relative === currentPage) {
                                $li.animate({height:0},function(){
                                    $(this).remove();
                                    $menu.trigger('bookmarks:updated');
                                    setTimeout(function(){
                                        if($menu.find('li').length == 0) {
                                            $nav.fadeOut();
                                        }
                                        // $nav.toggleClass('d-none', $menu.find('li').length == 0);
                                    },400)
                                });
                            }
                        });
                    }
                }
            });
        });
    });

    // show/hide sidebar includes
    $(document).on('click', '#menu-emoncms li.active a', hideMenuItems);
    // show hide 2nd / 3rd menu items
    // setTimeout(hideMenuItems, 100);

    // save a cookie to remember user's choice to hide or show the bookmarks
    $('#sidebar_bookmarks').on('show hide', function(event) {
        var bookmarks_collapsed = event.type !== 'show';
        docCookies.setItem('bookmarks_collapsed', bookmarks_collapsed)
    })

    $(document).keyup(function(e) {
        if (e.keyCode == 27) { // escape key maps to keycode `27`
            $('.navbar .dropdown').removeClass('open')
        }
    });

}); // end of jquery ready()

// trigger the events to allow module js scripts to attach actions to the events
function show_sidebar(options) {
    // @note: assumes the css animation takes 300ms
    $('#sidebar').trigger('show.sidebar.collapse');
    setTimeout(function(){
        $('#sidebar').trigger('shown.sidebar.collapse');
    }, 350);
    $('body').removeClass('collapsed').addClass('expanded');

    if ($(window).width() < 870) {
        $(".content-container").css("margin","2.7rem 0 0 0");
    } else {
        $(".content-container").css("margin","2.7rem 0 0 15rem");
    }
}
function hide_sidebar(options) {
    // @note: assumes the css animation takes 300ms
    $('#sidebar').trigger('hide.sidebar.collapse');
    setTimeout(function(){
        $('#sidebar').trigger('hidden.sidebar.collapse');
    }, 350);
    $('body').addClass('collapsed').removeClass('expanded');

    $(".content-container").css("margin","2.7rem auto 0 auto");
}

// backward compatible empty function
if(typeof init_sidebar !== 'function') var init_sidebar = function(){}

// get/set document cookies
var docCookies = {
    getItem: function (sKey) {
      if (!sKey) { return null; }
      return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
    },
    setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
      if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return false; }
      var sExpires = "";
      if (vEnd) {
        switch (vEnd.constructor) {
          case Number:
          //   sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd;
            sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; expires=" + (new Date(vEnd * 1e3 + Date.now())).toUTCString();
            break;
          case String:
            sExpires = "; expires=" + vEnd;
            break;
          case Date:
            sExpires = "; expires=" + vEnd.toUTCString();
            break;
        }
      }
      document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : "");
      return true;
    },
    removeItem: function (sKey, sPath, sDomain) {
      if (!this.hasItem(sKey)) { return false; }
      document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "");
      return true;
    },
    hasItem: function (sKey) {
      if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return false; }
      return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
    },
    keys: function () {
      var aKeys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, "").split(/\s*(?:\=[^;]*)?;\s*/);
      for (var nLen = aKeys.length, nIdx = 0; nIdx < nLen; nIdx++) { aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]); }
      return aKeys;
    }
  };

<|endoftext|>


<commit_before>// Copyright 2015-2020, University of Colorado Boulder

/**
 * An asynchronous (async.queue) queue with a concurrency of one (meaning that only one task is executed at a time)
 * that is used to serialize submissions of strings for a translation.
 *
 * @author Aaron Davis
 * @author John Blanco
 */

'use strict';

// modules
const _ = require( 'lodash' ); // eslint-disable-line
const longTermStringStorage = require( './longTermStringStorage' );
const { Pool } = require( 'pg' ); // eslint-disable-line
const requestBuild = require( './requestBuild' );
const RosettaConstants = require( './RosettaConstants' );
const winston = require( 'winston' );

// for debug purposes, it is possible to configure Rosetta to skip sending build requests to the build server
const SEND_BUILD_REQUESTS = global.config.sendBuildRequests === undefined ? true : global.config.sendBuildRequests;

/**
 * task queue into which translation requests are pushed
 * @public
 */
module.exports.stringSubmissionQueue = async ( req, res ) => {
  const { targetLocale } = req.params;
  const { simName } = req.params;
  const userId = ( req.session.userId ) ? req.session.userId : 0;

  // Define an object that will contain the sets of strings that need to be stored.  The keys for this object will be
  // the sim or library name, aka the 'repo', and the strings will be in the form needed for long term string storage.
  // Multiple sims and/or libraries may need to be committed as part of a single translation due to shared code.
  const stringSets = {};

  // flags for checking if strings have changed for a given sim or lib
  const stringsChangedFlags = {};

  // Extract the string sets from the submitted translation.  The body of the request (req.body) contains an object with
  // all of the strings submitted through the POST request from the translation utility. The keys are in the form
  // "[repository] [key]" and the values are the strings, for example: "chains chains.title": "Chains".
  _.keys( await req.body ).forEach( submittedStringKey => {

    // data submitted is in the form "[repository] [key]", for example "area-builder area-builder.title"
    const repoAndStringKey = submittedStringKey.split( ' ' );
    const repo = repoAndStringKey[ 0 ];
    const stringKey = repoAndStringKey[ 1 ];

    // if this sim or lib isn't represent yet, add it
    if ( !stringSets[ repo ] ) {
      stringSets[ repo ] = {};
    }

    // get the value of the string
    let stringValue = req.body[ submittedStringKey ];

    // Handle any embedded line feeds. There is some history here, please see
    // https://github.com/phetsims/rosetta/issues/144 and https://github.com/phetsims/rosetta/issues/207. The best
    // thing to do going forward is to always use
 in multi-line strings instead of '\n', this code is here to
    // handle older sims in which '\n' was used in multi-line strings.
    if ( stringValue.indexOf( '\\n' ) !== -1 ) {
      winston.info( 'replacing line feed sequence in string from repo ' + repo + ' with key ' + stringKey );
      stringValue = stringValue.replace( /\\n/g, '\n' );
    }

    // check if the string is already in translatedStrings to get the history if it exists
    const translatedString = req.session.translatedStrings[ targetLocale ] &&
                             req.session.translatedStrings[ targetLocale ][ repo ] &&
                             req.session.translatedStrings[ targetLocale ][ repo ][ stringKey ];
    let history = translatedString && translatedString.history;
    const oldValue = ( history && history.length ) ? history[ history.length - 1 ].newValue : '';

    // only add the update if the value has changed
    if ( oldValue !== stringValue ) {
      const newHistoryEntry = {
        userId: ( req.session.userId ) ? req.session.userId : 'phet-test',
        timestamp: Date.now(),
        oldValue: oldValue,
        newValue: stringValue
      };

      if ( history ) {
        history.push( newHistoryEntry );
      }
      else {
        history = [ newHistoryEntry ];
      }
      stringSets[ repo ][ stringKey ] = {
        value: stringValue,
        history: history
      };

      // set flag indicating that a string has changed
      if ( !stringsChangedFlags[ repo ] ) {
        stringsChangedFlags[ repo ] = true;
      }
    }
    else if ( translatedString ) {
      stringSets[ repo ][ stringKey ] = translatedString;
    }
  } );

  // make a list of only the string sets that have changed
  const changedStringSets = _.pick( stringSets, _.keys( stringsChangedFlags ) );

  // save the modified strings to long-term storage
  const stringSavePromises = [];
  _.keys( changedStringSets ).forEach( simOrLibName => {
    stringSavePromises.push(
      longTermStringStorage.saveTranslatedStrings( simOrLibName, targetLocale, changedStringSets[ simOrLibName ] )
    );
  } );

  // wait for all the save operations to complete
  try {

    // wait for all the save operations to complete
    await Promise.all( stringSavePromises );

  }
  catch( err ) {

    winston.error( 'error while submitting strings, err = ' + err );
    winston.info( 'rendering error page' );

    // render an error page
    res.render( 'error.html', {
      title: 'Translation Utility Error',
      message: 'An error occurred when trying to save the translated strings.',
      errorDetails: err,
      timestamp: new Date().getTime()
    } );

    // bail out
    return;
  }

  try {

    // request a build of the new translation (if enabled)
    if ( SEND_BUILD_REQUESTS ) {
      await requestBuild( simName, targetLocale, userId );
    }
    else {
      winston.info( 'build requests for translations are disabled, skipping' );
    }

    // clear out short-term storage, since these strings have now been successfully stored in the long-term area
    await deleteStringsFromDB( userId, targetLocale, _.keys( stringSets ) ).catch();

    // render the page that indicates that the translation was successfully submitted
    res.render( 'translation-submit.html', {
      title: 'Translation submitted',
      simName: simName,
      targetLocale: targetLocale
    } );
  }
  catch( err ) {

    winston.error( 'error while requesting build and clearing out DB, err = ' + err );
    winston.info( 'rendering error page' );

    // render an error page
    res.render( 'error.html', {
      title: 'Translation Utility Error',
      message: 'An error occurred when trying to build the new simulation.',
      errorDetails: err,
      timestamp: new Date().getTime()
    } );
  }

};

/**
 * delete the strings that are stored in short-term storage
 * {string} userID
 * {string} locale
 * {string[]} simOrLibNames
 * @private
 */
async function deleteStringsFromDB( userID, locale, simOrLibNames ) {

  winston.info(
    'removing strings from short term storage for userID = ' + userID + ', sim/libs = ' + simOrLibNames + ', locale = ' + locale
  );

  // create a string with all the repo names that can be used in the SQL query
  let simOrLibNamesString = '';
  simOrLibNames.forEach( ( simOrLibName, index ) => {
    simOrLibNamesString += 'repository = ' + '\'' + simOrLibName + '\'';
    if ( index < simOrLibNames.length - 1 ) {
      simOrLibNamesString += ' OR ';
    }
  } );

  const deleteQuery = 'DELETE FROM saved_translations WHERE user_id = $1 AND locale = $2 AND (' + simOrLibNamesString + ')';
  const pool = new Pool( {
    connectionTimeoutMillis: RosettaConstants.DATABASE_QUERY_TIMEOUT
  } );
  try {
    winston.info( 'attempting query: ' + deleteQuery );
    await pool.query( deleteQuery, [ userID, locale ] );
    winston.info( 'deletion of strings succeeded' );
  }
  catch( err ) {
    winston.error( 'deletion of strings failed, err = ' + err );
  }
}<commit_msg>Enable "no-useless-concat" rule, see https://github.com/phetsims/chipper/issues/814<commit_after>// Copyright 2015-2020, University of Colorado Boulder

/**
 * An asynchronous (async.queue) queue with a concurrency of one (meaning that only one task is executed at a time)
 * that is used to serialize submissions of strings for a translation.
 *
 * @author Aaron Davis
 * @author John Blanco
 */

'use strict';

// modules
const _ = require( 'lodash' ); // eslint-disable-line
const longTermStringStorage = require( './longTermStringStorage' );
const { Pool } = require( 'pg' ); // eslint-disable-line
const requestBuild = require( './requestBuild' );
const RosettaConstants = require( './RosettaConstants' );
const winston = require( 'winston' );

// for debug purposes, it is possible to configure Rosetta to skip sending build requests to the build server
const SEND_BUILD_REQUESTS = global.config.sendBuildRequests === undefined ? true : global.config.sendBuildRequests;

/**
 * task queue into which translation requests are pushed
 * @public
 */
module.exports.stringSubmissionQueue = async ( req, res ) => {
  const { targetLocale } = req.params;
  const { simName } = req.params;
  const userId = ( req.session.userId ) ? req.session.userId : 0;

  // Define an object that will contain the sets of strings that need to be stored.  The keys for this object will be
  // the sim or library name, aka the 'repo', and the strings will be in the form needed for long term string storage.
  // Multiple sims and/or libraries may need to be committed as part of a single translation due to shared code.
  const stringSets = {};

  // flags for checking if strings have changed for a given sim or lib
  const stringsChangedFlags = {};

  // Extract the string sets from the submitted translation.  The body of the request (req.body) contains an object with
  // all of the strings submitted through the POST request from the translation utility. The keys are in the form
  // "[repository] [key]" and the values are the strings, for example: "chains chains.title": "Chains".
  _.keys( await req.body ).forEach( submittedStringKey => {

    // data submitted is in the form "[repository] [key]", for example "area-builder area-builder.title"
    const repoAndStringKey = submittedStringKey.split( ' ' );
    const repo = repoAndStringKey[ 0 ];
    const stringKey = repoAndStringKey[ 1 ];

    // if this sim or lib isn't represent yet, add it
    if ( !stringSets[ repo ] ) {
      stringSets[ repo ] = {};
    }

    // get the value of the string
    let stringValue = req.body[ submittedStringKey ];

    // Handle any embedded line feeds. There is some history here, please see
    // https://github.com/phetsims/rosetta/issues/144 and https://github.com/phetsims/rosetta/issues/207. The best
    // thing to do going forward is to always use
 in multi-line strings instead of '\n', this code is here to
    // handle older sims in which '\n' was used in multi-line strings.
    if ( stringValue.indexOf( '\\n' ) !== -1 ) {
      winston.info( 'replacing line feed sequence in string from repo ' + repo + ' with key ' + stringKey );
      stringValue = stringValue.replace( /\\n/g, '\n' );
    }

    // check if the string is already in translatedStrings to get the history if it exists
    const translatedString = req.session.translatedStrings[ targetLocale ] &&
                             req.session.translatedStrings[ targetLocale ][ repo ] &&
                             req.session.translatedStrings[ targetLocale ][ repo ][ stringKey ];
    let history = translatedString && translatedString.history;
    const oldValue = ( history && history.length ) ? history[ history.length - 1 ].newValue : '';

    // only add the update if the value has changed
    if ( oldValue !== stringValue ) {
      const newHistoryEntry = {
        userId: ( req.session.userId ) ? req.session.userId : 'phet-test',
        timestamp: Date.now(),
        oldValue: oldValue,
        newValue: stringValue
      };

      if ( history ) {
        history.push( newHistoryEntry );
      }
      else {
        history = [ newHistoryEntry ];
      }
      stringSets[ repo ][ stringKey ] = {
        value: stringValue,
        history: history
      };

      // set flag indicating that a string has changed
      if ( !stringsChangedFlags[ repo ] ) {
        stringsChangedFlags[ repo ] = true;
      }
    }
    else if ( translatedString ) {
      stringSets[ repo ][ stringKey ] = translatedString;
    }
  } );

  // make a list of only the string sets that have changed
  const changedStringSets = _.pick( stringSets, _.keys( stringsChangedFlags ) );

  // save the modified strings to long-term storage
  const stringSavePromises = [];
  _.keys( changedStringSets ).forEach( simOrLibName => {
    stringSavePromises.push(
      longTermStringStorage.saveTranslatedStrings( simOrLibName, targetLocale, changedStringSets[ simOrLibName ] )
    );
  } );

  // wait for all the save operations to complete
  try {

    // wait for all the save operations to complete
    await Promise.all( stringSavePromises );

  }
  catch( err ) {

    winston.error( 'error while submitting strings, err = ' + err );
    winston.info( 'rendering error page' );

    // render an error page
    res.render( 'error.html', {
      title: 'Translation Utility Error',
      message: 'An error occurred when trying to save the translated strings.',
      errorDetails: err,
      timestamp: new Date().getTime()
    } );

    // bail out
    return;
  }

  try {

    // request a build of the new translation (if enabled)
    if ( SEND_BUILD_REQUESTS ) {
      await requestBuild( simName, targetLocale, userId );
    }
    else {
      winston.info( 'build requests for translations are disabled, skipping' );
    }

    // clear out short-term storage, since these strings have now been successfully stored in the long-term area
    await deleteStringsFromDB( userId, targetLocale, _.keys( stringSets ) ).catch();

    // render the page that indicates that the translation was successfully submitted
    res.render( 'translation-submit.html', {
      title: 'Translation submitted',
      simName: simName,
      targetLocale: targetLocale
    } );
  }
  catch( err ) {

    winston.error( 'error while requesting build and clearing out DB, err = ' + err );
    winston.info( 'rendering error page' );

    // render an error page
    res.render( 'error.html', {
      title: 'Translation Utility Error',
      message: 'An error occurred when trying to build the new simulation.',
      errorDetails: err,
      timestamp: new Date().getTime()
    } );
  }

};

/**
 * delete the strings that are stored in short-term storage
 * {string} userID
 * {string} locale
 * {string[]} simOrLibNames
 * @private
 */
async function deleteStringsFromDB( userID, locale, simOrLibNames ) {

  winston.info(
    'removing strings from short term storage for userID = ' + userID + ', sim/libs = ' + simOrLibNames + ', locale = ' + locale
  );

  // create a string with all the repo names that can be used in the SQL query
  let simOrLibNamesString = '';
  simOrLibNames.forEach( ( simOrLibName, index ) => {
    simOrLibNamesString += 'repository = \'' + simOrLibName + '\'';
    if ( index < simOrLibNames.length - 1 ) {
      simOrLibNamesString += ' OR ';
    }
  } );

  const deleteQuery = 'DELETE FROM saved_translations WHERE user_id = $1 AND locale = $2 AND (' + simOrLibNamesString + ')';
  const pool = new Pool( {
    connectionTimeoutMillis: RosettaConstants.DATABASE_QUERY_TIMEOUT
  } );
  try {
    winston.info( 'attempting query: ' + deleteQuery );
    await pool.query( deleteQuery, [ userID, locale ] );
    winston.info( 'deletion of strings succeeded' );
  }
  catch( err ) {
    winston.error( 'deletion of strings failed, err = ' + err );
  }
}<|endoftext|>