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

/**
 * A scrolling graph component.  Like a seismograph, it has pens on the right hand side that record data, and the paper
 * scrolls to the left.  It is currently sized accordingly to be used in a small draggable sensor, like the ones in Wave
 * Interference, Bending Light or Circuit Construction Kit: AC. It would typically be embedded in a Panel.
 *
 * Please see the demo in http://localhost/griddle/griddle_en.html
 *
 * Moved from wave-interference repo to griddle repo on Wed, Aug 29, 2018.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Jesse Greenberg (PhET Interactive Simulations)
 */

import Emitter from '../../axon/js/Emitter.js';
import Property from '../../axon/js/Property.js';
import Bounds2 from '../../dot/js/Bounds2.js';
import Utils from '../../dot/js/Utils.js';
import Vector2 from '../../dot/js/Vector2.js';
import Shape from '../../kite/js/Shape.js';
import merge from '../../phet-core/js/merge.js';
import ModelViewTransform2 from '../../phetcommon/js/view/ModelViewTransform2.js';
import Node from '../../scenery/js/nodes/Node.js';
import Rectangle from '../../scenery/js/nodes/Rectangle.js';
import Text from '../../scenery/js/nodes/Text.js';
import Tandem from '../../tandem/js/Tandem.js';
import DynamicSeriesNode from './DynamicSeriesNode.js';
import griddle from './griddle.js';
import GridNode from './GridNode.js';

// constants
const LABEL_GRAPH_MARGIN = 3;
const HORIZONTAL_AXIS_LABEL_MARGIN = 4;
const VERTICAL_AXIS_LABEL_MARGIN = 8;

class ScrollingChartNode extends Node {

  /**
   * @param {NumberProperty} valueProperty - indicates the passage of horizontal value in the same units as the model.
   *                                      - This may be seconds or another unit depending on the model.
   * @param {Object} [options]
   */
  constructor( valueProperty, options ) {
    super();

    options = merge( {

      // dimensions for the plot, in view coordinates
      width: 500,
      height: 300,

      // corner radius for the panel containing the plot
      cornerRadius: 5,

      // {Node} - label for the vertical axis, should already be rotated if necessary
      verticalAxisLabelNode: null,

      // {Node} - label for the horizontal axis
      horizontalAxisLabelNode: null,

      // {PlotStyle} - Changes how the DynamicSeries data is drawn on the chart
      plotStyle: DynamicSeriesNode.PlotStyle.LINE,

      // {Object|null} - Options for the Rectangle that contains chart content, including GridNode and
      // DynamicSeriesNodes.
      graphPanelOptions: null, // filled in below because some defaults are based on other options

      // {boolean} - whether or not labels indicating numeric value of major grid lines are shown
      showVerticalGridLabels: true,
      showHorizontalGridLabels: true,

      // {number} - number of decimal places for the labels along grid lines
      verticalGridLabelNumberOfDecimalPlaces: 0,
      horizontalGridLabelNumberOfDecimalPlaces: 0,

      // options passed to both vertical and horizontal label text
      gridLabelOptions: {},

      // line spacing, in model coordinates
      majorVerticalLineSpacing: 1,
      majorHorizontalLineSpacing: 1,

      // options passed to gridNode
      gridNodeOptions: {
        majorLineOptions: {
          stroke: 'lightGray',
          lineWidth: 0.8
        }
      },

      tandem: Tandem.OPTIONAL
    }, options );

    // @private
    this.plotWidth = options.width;
    this.plotHeight = options.height;
    this.showVerticalGridLabels = options.showVerticalGridLabels;
    this.showHorizontalGridLabels = options.showHorizontalGridLabels;
    this.verticalGridLabelNumberOfDecimalPlaces = options.verticalGridLabelNumberOfDecimalPlaces;
    this.horizontalGridLabelNumberOfDecimalPlaces = options.horizontalGridLabelNumberOfDecimalPlaces;
    this.valueProperty = valueProperty;
    this.majorHorizontalLineSpacing = options.majorHorizontalLineSpacing;
    this.majorVerticalLineSpacing = options.majorVerticalLineSpacing;
    this.plotStyle = options.plotStyle;
    this.gridLabelOptions = options.gridLabelOptions;

    // default options to be passed into the graphPanel Rectangle
    options.graphPanelOptions = merge( {
      fill: 'white',
      lineWidth: 1,

      // This stroke is covered by the front panel stroke, only included here to make sure the bounds align
      stroke: 'black',
      right: this.plotWidth,
      pickable: false
    }, options.graphPanelOptions );

    // White panel with gridlines that shows the data
    options.graphPanelOptions = merge( {

      // Prevent data from being plotted outside the graph
      clipArea: Shape.roundedRectangleWithRadii( 0, 0, this.plotWidth, this.plotHeight, {
        topLeft: options.cornerRadius,
        topRight: options.cornerRadius,
        bottomLeft: options.cornerRadius,
        bottomRight: options.cornerRadius
      } )
    }, options.graphPanelOptions );
    const graphPanel = new Rectangle( 0, 0, this.plotWidth, this.plotHeight, options.cornerRadius, options.cornerRadius,
      options.graphPanelOptions
    );

    // @public {Property.<ModelViewTransform2} - Observable model-view transformation for the data, set to
    // transform the plot (zoom or pan data). Default transform puts origin at bottom left of the plot with
    // x ranging from 0-4 and y ranging from -1 to 1
    this.modelViewTransformProperty = options.modelViewTransformProperty || new Property( ModelViewTransform2.createRectangleInvertedYMapping(
      new Bounds2( 0, -1, 4, 1 ),
      new Bounds2( 0, 0, this.plotWidth, this.plotHeight )
    ) );

    const gridNodeOptions = merge( {
      majorHorizontalLineSpacing: this.majorHorizontalLineSpacing,
      majorVerticalLineSpacing: this.majorVerticalLineSpacing,
      modelViewTransformProperty: this.modelViewTransformProperty
    }, options.gridNodeOptions );

    this.gridNode = new GridNode( this.plotWidth, this.plotHeight, gridNodeOptions );

    graphPanel.addChild( this.gridNode );

    // @private {Node} - layers for each of the vertical and horizontal labels along grid lines
    this.verticalGridLabelLayer = new Node();
    this.horizontalGridLabelLayer = new Node();
    this.addChild( this.verticalGridLabelLayer );
    this.addChild( this.horizontalGridLabelLayer );

    const plotWidthWithMargin = this.plotWidth;

    // @private - maps a DynamicSeries -> DynamicSeriesNode so that it can be potentially removed later
    this.dynamicSeriesMap = new Map();

    this.addChild( graphPanel );
    this.graphPanel = graphPanel;

    this.redrawLabels();

    const transformListener = this.redrawLabels.bind( this );
    this.modelViewTransformProperty.link( transformListener );

    // @private - for disposal
    this.scrollingChartNodeDisposeEmitter = new Emitter();

    // Stroke on front panel is on top, so that when the curves go to the edges they do not overlap the border stroke,
    // and so the GridNode appears below the panel stroke as well.
    graphPanel.addChild( new Rectangle( 0, 0, this.plotWidth, this.plotHeight, options.cornerRadius, options.cornerRadius, {
      stroke: graphPanel.stroke,
      lineWidth: graphPanel.lineWidth,
      pickable: false
    } ) );

    /* -------------------------------------------
     * Optional decorations
     * -------------------------------------------*/

    // Position the vertical axis title node
    if ( options.verticalAxisLabelNode ) {
      options.verticalAxisLabelNode.mutate( {
        maxHeight: graphPanel.height,
        right: this.bounds.minX - VERTICAL_AXIS_LABEL_MARGIN, // whether or not there are vertical axis labels, position to the left
        centerY: graphPanel.centerY
      } );
      this.addChild( options.verticalAxisLabelNode );
    }

    // add and position the horizontal axis label
    if ( options.horizontalAxisLabelNode ) {
      this.addChild( options.horizontalAxisLabelNode );

      // For i18n, “Time” will expand symmetrically L/R until it gets too close to the scale bar. Then, the string will
      // expand to the R only, until it reaches the point it must be scaled down in size.
      options.horizontalAxisLabelNode.maxWidth = graphPanel.right - 2 * HORIZONTAL_AXIS_LABEL_MARGIN;

      // Position the horizontal axis title node after its maxWidth is specified
      const labelTop = this.showHorizontalGridLabels ? this.horizontalGridLabelLayer.bottom + LABEL_GRAPH_MARGIN : graphPanel.bottom + LABEL_GRAPH_MARGIN;
      options.horizontalAxisLabelNode.mutate( {
        top: labelTop,
        centerX: plotWidthWithMargin / 2 + graphPanel.bounds.minX
      } );
      if ( options.horizontalAxisLabelNode.left < HORIZONTAL_AXIS_LABEL_MARGIN ) {
        options.horizontalAxisLabelNode.left = HORIZONTAL_AXIS_LABEL_MARGIN;
      }
    }

    this.mutate( options );

    // @private - for dispose
    this.disposeScrollingChartNode = () => {
      this.scrollingChartNodeDisposeEmitter.emit();
      this.scrollingChartNodeDisposeEmitter.dispose();
      this.modelViewTransformProperty.unlink( transformListener );
    };
  }

  /**
   * Adds a DynamicSeriesNode to this ScrollingChartNode.
   * @public
   *
   * @param {DynamicSeries} dynamicSeries
   */
  addDynamicSeries( dynamicSeries ) {
    const dynamicSeriesNode = new DynamicSeriesNode(
      dynamicSeries,
      this.plotWidth,
      new Bounds2( 0, 0, this.plotWidth, this.plotHeight ),
      this.valueProperty,
      this.modelViewTransformProperty
    );
    this.dynamicSeriesMap.set( dynamicSeries, dynamicSeriesNode );
    this.graphPanel.addChild( dynamicSeriesNode );
    this.scrollingChartNodeDisposeEmitter.addListener( () => dynamicSeriesNode.dispose() );
  }

  /**
   * Adds serveral DynamicSeries at once, for convenience.
   * @public
   *
   * @param {DynamicSeries[]} dynamicSeriesArray
   */
  addDynamicSeriesArray( dynamicSeriesArray ) {
    dynamicSeriesArray.forEach( this.addDynamicSeries.bind( this ) );
  }

  /**
   * Remove a DynamicSeries (and its DynamicSeriesNode)_from this plot.
   * @public
   *
   * @param {DynamicSeries} dynamicSeries
   */
  removeDynamicSeries( dynamicSeries ) {
    assert && assert( this.dynamicSeriesMap.has( dynamicSeries ), 'trying to remove DynamicSeriesNode when one does not exist.' );
    this.graphPanel.removeChild( this.dynamicSeriesMap.get( dynamicSeries ) );
    this.dynamicSeriesMap.delete( dynamicSeries );
  }

  /**
   * Set line spacings for the grid and labels.
   * @public
   *
   * @param {number} majorVerticalLineSpacing
   * @param {number} majorHorizontalLineSpacing
   * @param {number} minorVerticalLineSpacing
   * @param {number} minorHorizontalLineSpacing
   */
  setLineSpacings( majorVerticalLineSpacing, majorHorizontalLineSpacing, minorVerticalLineSpacing, minorHorizontalLineSpacing ) {
    this.majorHorizontalLineSpacing = majorHorizontalLineSpacing;
    this.majorVerticalLineSpacing = majorVerticalLineSpacing;

    this.gridNode.setLineSpacings( majorVerticalLineSpacing, majorHorizontalLineSpacing, minorVerticalLineSpacing, minorHorizontalLineSpacing );
    this.redrawLabels();
  }

  /**
   * Redraws labels for when line spacing or transform changes.
   *
   * @protected
   */
  redrawLabels() {
    if ( this.showVerticalGridLabels ) {
      const verticalLabelChildren = [];
      const yPositions = this.gridNode.getHorizontalLinePositionsInGrid( GridNode.LineType.MAJOR_HORIZONTAL );
      yPositions.forEach( yPosition => {
        const viewY = this.modelViewTransformProperty.get().modelToViewY( yPosition );
        const labelPoint = this.graphPanel.localToParentPoint( new Vector2( this.gridNode.bounds.left, viewY ) );

        const labelText = new Text( Utils.toFixed( yPosition, this.verticalGridLabelNumberOfDecimalPlaces ), merge( {
          rightCenter: labelPoint.plusXY( -3, 0 )
        }, this.gridLabelOptions ) );

        verticalLabelChildren.push( labelText );
      } );
      this.verticalGridLabelLayer.children = verticalLabelChildren;
    }

    if ( this.showHorizontalGridLabels ) {

      // draw labels along the horizontal lines
      const horizontalLabelChildren = [];
      const xPositions = this.gridNode.getVerticalLinePositionsInGrid( GridNode.LineType.MAJOR_VERTICAL );
      xPositions.forEach( xPosition => {
        const viewX = this.modelViewTransformProperty.get().modelToViewX( xPosition );
        const labelPoint = this.graphPanel.localToParentPoint( new Vector2( viewX, this.gridNode.bounds.bottom ) );

        const labelText = new Text( Utils.toFixed( xPosition, this.horizontalGridLabelNumberOfDecimalPlaces ), merge( {
          centerTop: labelPoint.plusXY( 0, 3 )
        }, this.gridLabelOptions ) );

        horizontalLabelChildren.push( labelText );
      } );
      this.horizontalGridLabelLayer.children = horizontalLabelChildren;
    }
  }

  /**
   * Set the plot style for the graph, to be drawn as a line graph or a scatter plot.
   *
   * @param {XYDataSeriesNode.PlotStyle} plotStyle - one of plotStyle
   * @public
   */
  setPlotStyle( plotStyle ) {
    this.plotStyle = plotStyle;

    this.dynamicSeriesMap.forEach( dynamicSeriesNode => {
      dynamicSeriesNode.setPlotStyle( plotStyle );
    } );
  }

  /**
   * Releases resources when no longer used.
   * @public
   */
  dispose() {
    this.disposeScrollingChartNode();
    super.dispose();
  }
}

griddle.register( 'ScrollingChartNode', ScrollingChartNode );
export default ScrollingChartNode;<commit_msg>clean up options and provide a function to create a typical ModelViewTransformProperty for ScrollingChartNode, see #46<commit_after>// Copyright 2018-2020, University of Colorado Boulder

/**
 * A graph that supports "scrolling" of model data with a ModelViewTransform2 so you can display
 * different ranges of data.
 *
 * Please see the demo in http://localhost/griddle/griddle_en.html
 *
 * Moved from wave-interference repo to griddle repo on Wed, Aug 29, 2018.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Jesse Greenberg (PhET Interactive Simulations)
 */

import Emitter from '../../axon/js/Emitter.js';
import Property from '../../axon/js/Property.js';
import Bounds2 from '../../dot/js/Bounds2.js';
import Range from '../../dot/js/Range.js';
import Utils from '../../dot/js/Utils.js';
import Vector2 from '../../dot/js/Vector2.js';
import Shape from '../../kite/js/Shape.js';
import merge from '../../phet-core/js/merge.js';
import ModelViewTransform2 from '../../phetcommon/js/view/ModelViewTransform2.js';
import Node from '../../scenery/js/nodes/Node.js';
import Rectangle from '../../scenery/js/nodes/Rectangle.js';
import Text from '../../scenery/js/nodes/Text.js';
import Tandem from '../../tandem/js/Tandem.js';
import DynamicSeriesNode from './DynamicSeriesNode.js';
import griddle from './griddle.js';
import GridNode from './GridNode.js';

// constants
const LABEL_GRAPH_MARGIN = 3;
const HORIZONTAL_AXIS_LABEL_MARGIN = 4;
const VERTICAL_AXIS_LABEL_MARGIN = 8;

class ScrollingChartNode extends Node {

  /**
   * @param {NumberProperty} valueProperty - indicates the passage of horizontal value in the same units as the model.
   *                                      - This may be seconds or another unit depending on the model.
   * @param {Object} [options]
   */
  constructor( valueProperty, options ) {
    super();

    options = merge( {

      // dimensions for the plot, in view coordinates
      width: 500,
      height: 300,

      // {Property.<ModelViewTransform> - model-view transform for the data, null because default transform
      // is determined by default optional ranges defaultModelXRange and defaultModelYRange. Plot data is
      // in model coordinates and you can scale or translate the plot by modifying this Property. See
      // createRectangularModelViewTransform for typical and default chart transform.
      modelViewTransformProperty: null,

      // Default ranges in model coordinates for the model-view transform - the plot will display
      // these ranges of data within the view dimensions of width and height specified above.
      // These have no impact if you provide your own modelViewTransformProperty.
      defaultModelXRange: new Range( 0, 4 ),
      defaultModelYRange: new Range( -1, 1 ),

      // corner radius for the panel containing the plot
      cornerRadius: 5,

      // {Node} - label for the vertical axis, should already be rotated if necessary
      verticalAxisLabelNode: null,

      // {Node} - label for the horizontal axis
      horizontalAxisLabelNode: null,

      // {PlotStyle} - Changes how the DynamicSeries data is drawn on the chart
      plotStyle: DynamicSeriesNode.PlotStyle.LINE,

      // {Object|null} - Options for the Rectangle that contains chart content, including GridNode and
      // DynamicSeriesNodes.
      graphPanelOptions: null, // filled in below because some defaults are based on other options

      // {boolean} - whether or not labels indicating numeric value of major grid lines are shown
      showVerticalGridLabels: true,
      showHorizontalGridLabels: true,

      // {number} - number of decimal places for the labels along grid lines
      verticalGridLabelNumberOfDecimalPlaces: 0,
      horizontalGridLabelNumberOfDecimalPlaces: 0,

      // options passed to both vertical and horizontal label text
      gridLabelOptions: {},

      // line spacing, in model coordinates
      majorVerticalLineSpacing: 1,
      majorHorizontalLineSpacing: 1,

      // options passed to gridNode
      gridNodeOptions: {
        majorLineOptions: {
          stroke: 'lightGray',
          lineWidth: 0.8
        }
      },

      tandem: Tandem.OPTIONAL
    }, options );

    // @private
    this.plotWidth = options.width;
    this.plotHeight = options.height;
    this.showVerticalGridLabels = options.showVerticalGridLabels;
    this.showHorizontalGridLabels = options.showHorizontalGridLabels;
    this.verticalGridLabelNumberOfDecimalPlaces = options.verticalGridLabelNumberOfDecimalPlaces;
    this.horizontalGridLabelNumberOfDecimalPlaces = options.horizontalGridLabelNumberOfDecimalPlaces;
    this.valueProperty = valueProperty;
    this.majorHorizontalLineSpacing = options.majorHorizontalLineSpacing;
    this.majorVerticalLineSpacing = options.majorVerticalLineSpacing;
    this.plotStyle = options.plotStyle;
    this.gridLabelOptions = options.gridLabelOptions;

    // default options to be passed into the graphPanel Rectangle
    options.graphPanelOptions = merge( {
      fill: 'white',
      lineWidth: 1,

      // This stroke is covered by the front panel stroke, only included here to make sure the bounds align
      stroke: 'black',
      right: this.plotWidth,
      pickable: false
    }, options.graphPanelOptions );

    // White panel with gridlines that shows the data
    options.graphPanelOptions = merge( {

      // Prevent data from being plotted outside the graph
      clipArea: Shape.roundedRectangleWithRadii( 0, 0, this.plotWidth, this.plotHeight, {
        topLeft: options.cornerRadius,
        topRight: options.cornerRadius,
        bottomLeft: options.cornerRadius,
        bottomRight: options.cornerRadius
      } )
    }, options.graphPanelOptions );
    const graphPanel = new Rectangle( 0, 0, this.plotWidth, this.plotHeight, options.cornerRadius, options.cornerRadius,
      options.graphPanelOptions
    );

    // @public {Property.<ModelViewTransform2} - Observable model-view transformation for the data, set to
    // transform the plot (zoom or pan data). Default transform puts origin at bottom left of the plot with
    // x ranging from 0-4 and y ranging from -1 to 1
    this.modelViewTransformProperty = options.modelViewTransformProperty || new Property( this.createRectangularModelViewTransform( options.defaultModelXRange, options.defaultModelYRange ) );

    const gridNodeOptions = merge( {
      majorHorizontalLineSpacing: this.majorHorizontalLineSpacing,
      majorVerticalLineSpacing: this.majorVerticalLineSpacing,
      modelViewTransformProperty: this.modelViewTransformProperty
    }, options.gridNodeOptions );

    this.gridNode = new GridNode( this.plotWidth, this.plotHeight, gridNodeOptions );

    graphPanel.addChild( this.gridNode );

    // @private {Node} - layers for each of the vertical and horizontal labels along grid lines
    this.verticalGridLabelLayer = new Node();
    this.horizontalGridLabelLayer = new Node();
    this.addChild( this.verticalGridLabelLayer );
    this.addChild( this.horizontalGridLabelLayer );

    const plotWidthWithMargin = this.plotWidth;

    // @private - maps a DynamicSeries -> DynamicSeriesNode so that it can be potentially removed later
    this.dynamicSeriesMap = new Map();

    this.addChild( graphPanel );
    this.graphPanel = graphPanel;

    this.redrawLabels();

    const transformListener = this.redrawLabels.bind( this );
    this.modelViewTransformProperty.link( transformListener );

    // @private - for disposal
    this.scrollingChartNodeDisposeEmitter = new Emitter();

    // Stroke on front panel is on top, so that when the curves go to the edges they do not overlap the border stroke,
    // and so the GridNode appears below the panel stroke as well.
    graphPanel.addChild( new Rectangle( 0, 0, this.plotWidth, this.plotHeight, options.cornerRadius, options.cornerRadius, {
      stroke: graphPanel.stroke,
      lineWidth: graphPanel.lineWidth,
      pickable: false
    } ) );

    /* -------------------------------------------
     * Optional decorations
     * -------------------------------------------*/

    // Position the vertical axis title node
    if ( options.verticalAxisLabelNode ) {
      options.verticalAxisLabelNode.mutate( {
        maxHeight: graphPanel.height,
        right: this.bounds.minX - VERTICAL_AXIS_LABEL_MARGIN, // whether or not there are vertical axis labels, position to the left
        centerY: graphPanel.centerY
      } );
      this.addChild( options.verticalAxisLabelNode );
    }

    // add and position the horizontal axis label
    if ( options.horizontalAxisLabelNode ) {
      this.addChild( options.horizontalAxisLabelNode );

      // For i18n, “Time” will expand symmetrically L/R until it gets too close to the scale bar. Then, the string will
      // expand to the R only, until it reaches the point it must be scaled down in size.
      options.horizontalAxisLabelNode.maxWidth = graphPanel.right - 2 * HORIZONTAL_AXIS_LABEL_MARGIN;

      // Position the horizontal axis title node after its maxWidth is specified
      const labelTop = this.showHorizontalGridLabels ? this.horizontalGridLabelLayer.bottom + LABEL_GRAPH_MARGIN : graphPanel.bottom + LABEL_GRAPH_MARGIN;
      options.horizontalAxisLabelNode.mutate( {
        top: labelTop,
        centerX: plotWidthWithMargin / 2 + graphPanel.bounds.minX
      } );
      if ( options.horizontalAxisLabelNode.left < HORIZONTAL_AXIS_LABEL_MARGIN ) {
        options.horizontalAxisLabelNode.left = HORIZONTAL_AXIS_LABEL_MARGIN;
      }
    }

    this.mutate( options );

    // @private - for dispose
    this.disposeScrollingChartNode = () => {
      this.scrollingChartNodeDisposeEmitter.emit();
      this.scrollingChartNodeDisposeEmitter.dispose();
      this.modelViewTransformProperty.unlink( transformListener );
    };
  }

  /**
   * Adds a DynamicSeriesNode to this ScrollingChartNode.
   * @public
   *
   * @param {DynamicSeries} dynamicSeries
   */
  addDynamicSeries( dynamicSeries ) {
    const dynamicSeriesNode = new DynamicSeriesNode(
      dynamicSeries,
      this.plotWidth,
      new Bounds2( 0, 0, this.plotWidth, this.plotHeight ),
      this.valueProperty,
      this.modelViewTransformProperty
    );
    this.dynamicSeriesMap.set( dynamicSeries, dynamicSeriesNode );
    this.graphPanel.addChild( dynamicSeriesNode );
    this.scrollingChartNodeDisposeEmitter.addListener( () => dynamicSeriesNode.dispose() );
  }

  /**
   * Adds serveral DynamicSeries at once, for convenience.
   * @public
   *
   * @param {DynamicSeries[]} dynamicSeriesArray
   */
  addDynamicSeriesArray( dynamicSeriesArray ) {
    dynamicSeriesArray.forEach( this.addDynamicSeries.bind( this ) );
  }

  /**
   * Remove a DynamicSeries (and its DynamicSeriesNode)_from this plot.
   * @public
   *
   * @param {DynamicSeries} dynamicSeries
   */
  removeDynamicSeries( dynamicSeries ) {
    assert && assert( this.dynamicSeriesMap.has( dynamicSeries ), 'trying to remove DynamicSeriesNode when one does not exist.' );
    this.graphPanel.removeChild( this.dynamicSeriesMap.get( dynamicSeries ) );
    this.dynamicSeriesMap.delete( dynamicSeries );
  }

  /**
   * Set line spacings for the grid and labels.
   * @public
   *
   * @param {number} majorVerticalLineSpacing
   * @param {number} majorHorizontalLineSpacing
   * @param {number} minorVerticalLineSpacing
   * @param {number} minorHorizontalLineSpacing
   */
  setLineSpacings( majorVerticalLineSpacing, majorHorizontalLineSpacing, minorVerticalLineSpacing, minorHorizontalLineSpacing ) {
    this.majorHorizontalLineSpacing = majorHorizontalLineSpacing;
    this.majorVerticalLineSpacing = majorVerticalLineSpacing;

    this.gridNode.setLineSpacings( majorVerticalLineSpacing, majorHorizontalLineSpacing, minorVerticalLineSpacing, minorHorizontalLineSpacing );
    this.redrawLabels();
  }

  /**
   * Redraws labels for when line spacing or transform changes. Labels are only drawn along the major
   * grid lines.
   *
   * @protected
   */
  redrawLabels() {
    if ( this.showVerticalGridLabels ) {
      const verticalLabelChildren = [];
      const yPositions = this.gridNode.getHorizontalLinePositionsInGrid( GridNode.LineType.MAJOR_HORIZONTAL );
      yPositions.forEach( yPosition => {
        const viewY = this.modelViewTransformProperty.get().modelToViewY( yPosition );
        const labelPoint = this.graphPanel.localToParentPoint( new Vector2( this.gridNode.bounds.left, viewY ) );

        const labelText = new Text( Utils.toFixed( yPosition, this.verticalGridLabelNumberOfDecimalPlaces ), merge( {
          rightCenter: labelPoint.plusXY( -3, 0 )
        }, this.gridLabelOptions ) );

        verticalLabelChildren.push( labelText );
      } );
      this.verticalGridLabelLayer.children = verticalLabelChildren;
    }

    if ( this.showHorizontalGridLabels ) {

      // draw labels along the horizontal lines
      const horizontalLabelChildren = [];
      const xPositions = this.gridNode.getVerticalLinePositionsInGrid( GridNode.LineType.MAJOR_VERTICAL );
      xPositions.forEach( xPosition => {
        const viewX = this.modelViewTransformProperty.get().modelToViewX( xPosition );
        const labelPoint = this.graphPanel.localToParentPoint( new Vector2( viewX, this.gridNode.bounds.bottom ) );

        const labelText = new Text( Utils.toFixed( xPosition, this.horizontalGridLabelNumberOfDecimalPlaces ), merge( {
          centerTop: labelPoint.plusXY( 0, 3 )
        }, this.gridLabelOptions ) );

        horizontalLabelChildren.push( labelText );
      } );
      this.horizontalGridLabelLayer.children = horizontalLabelChildren;
    }
  }

  /**
   * Set the plot style for the graph, to be drawn as a line graph or a scatter plot.
   *
   * @param {XYDataSeriesNode.PlotStyle} plotStyle - one of plotStyle
   * @public
   */
  setPlotStyle( plotStyle ) {
    this.plotStyle = plotStyle;

    this.dynamicSeriesMap.forEach( dynamicSeriesNode => {
      dynamicSeriesNode.setPlotStyle( plotStyle );
    } );
  }

  /**
   * Create a typical ModelViewTransform2 for the plot that spans the widthRange and heightRange in model coordinates
   * so that those ranges are contained within and fill the ScrollingChartNode view bounds. Also inverts y so that
   * +y points up on the chart. Other transformms may be used, but this is the most common.
   * @public
   *
   * @param {Range} widthRange - in model coordinates
   * @param {Range} heightRange - in model coordinates
   * @returns {ModelViewTransform2}
   */
  createRectangularModelViewTransform( widthRange, heightRange ) {
    return ModelViewTransform2.createRectangleInvertedYMapping(
      new Bounds2( widthRange.min, heightRange.min, widthRange.max, heightRange.max ),
      new Bounds2( 0, 0, this.plotWidth, this.plotHeight )
    );
  }

  /**
   * Releases resources when no longer used.
   * @public
   */
  dispose() {
    this.disposeScrollingChartNode();
    super.dispose();
  }
}

griddle.register( 'ScrollingChartNode', ScrollingChartNode );
export default ScrollingChartNode;<|endoftext|>


<commit_before>/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher';
import EditorModel from '../../../editor/model';
import HistoryManager from '../../../editor/history';
import {setCaretPosition} from '../../../editor/caret';
import {getCaretOffsetAndText} from '../../../editor/dom';
import Autocomplete from '../rooms/Autocomplete';
import {autoCompleteCreator} from '../../../editor/parts';
import {renderModel} from '../../../editor/render';
import {Room} from 'matrix-js-sdk';

const IS_MAC = navigator.platform.indexOf("Mac") !== -1;

function cloneSelection(selection) {
    return {
        anchorNode: selection.anchorNode,
        anchorOffset: selection.anchorOffset,
        focusNode: selection.focusNode,
        focusOffset: selection.focusOffset,
        isCollapsed: selection.isCollapsed,
        rangeCount: selection.rangeCount,
        type: selection.type,
    };
}

function selectionEquals(a: Selection, b: Selection): boolean {
    return a.anchorNode === b.anchorNode &&
        a.anchorOffset === b.anchorOffset &&
        a.focusNode === b.focusNode &&
        a.focusOffset === b.focusOffset &&
        a.isCollapsed === b.isCollapsed &&
        a.rangeCount === b.rangeCount &&
        a.type === b.type;
}

export default class BasicMessageEditor extends React.Component {
    static propTypes = {
        model: PropTypes.instanceOf(EditorModel).isRequired,
        room: PropTypes.instanceOf(Room).isRequired,
        placeholder: PropTypes.string,
        label: PropTypes.string,    // the aria label
        initialCaret: PropTypes.object, // See DocumentPosition in editor/model.js
    };

    constructor(props, context) {
        super(props, context);
        this.state = {
            autoComplete: null,
        };
        this._editorRef = null;
        this._autocompleteRef = null;
        this._modifiedFlag = false;
    }

    _updateEditorState = (caret, inputType, diff) => {
        renderModel(this._editorRef, this.props.model);
        if (caret) {
            try {
                setCaretPosition(this._editorRef, this.props.model, caret);
            } catch (err) {
                console.error(err);
            }
        }
        if (this.props.placeholder) {
            const {isEmpty} = this.props.model;
            if (isEmpty) {
                this._editorRef.style.setProperty("--placeholder", `'${this.props.placeholder}'`);
                this._editorRef.classList.add("mx_BasicMessageComposer_inputEmpty");
            } else {
                this._editorRef.classList.remove("mx_BasicMessageComposer_inputEmpty");
                this._editorRef.style.removeProperty("--placeholder");
            }
        }
        this.setState({autoComplete: this.props.model.autoComplete});
        this.historyManager.tryPush(this.props.model, caret, inputType, diff);
    }

    _onInput = (event) => {
        this._modifiedFlag = true;
        const sel = document.getSelection();
        const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
        this._setLastCaret(caret, text, sel);
        this.props.model.update(text, event.inputType, caret);
    }

    _insertText(textToInsert, inputType = "insertText") {
        const sel = document.getSelection();
        const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
        const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset);
        caret.offset += textToInsert.length;
        this.props.model.update(newText, inputType, caret);
    }

    // this is used later to see if we need to recalculate the caret
    // on selectionchange. If it is just a consequence of typing
    // we don't need to. But if the user is navigating the caret without input
    // we need to recalculate it, to be able to know where to insert content after
    // losing focus
    _setLastCaret(caret, text, selection) {
        this._lastSelection = cloneSelection(selection);
        this._lastCaret = caret;
        this._lastTextLength = text.length;
    }

    _refreshLastCaretIfNeeded() {
        // TODO: needed when going up and down in editing messages ... not sure why yet
        // because the editors should stop doing this when when blurred ...
        // maybe it's on focus and the _editorRef isn't available yet or something.
        if (!this._editorRef) {
            return;
        }
        const selection = document.getSelection();
        if (!this._lastSelection || !selectionEquals(this._lastSelection, selection)) {
            this._lastSelection = cloneSelection(selection);
            const {caret, text} = getCaretOffsetAndText(this._editorRef, selection);
            this._lastCaret = caret;
            this._lastTextLength = text.length;
        }
        return this._lastCaret;
    }

    clearUndoHistory() {
        this.historyManager.clear();
    }

    getCaret() {
        return this._lastCaret;
    }

    isCaretAtStart() {
        return this.getCaret().offset === 0;
    }

    isCaretAtEnd() {
        return this.getCaret().offset === this._lastTextLength;
    }

    _onBlur = () => {
        document.removeEventListener("selectionchange", this._onSelectionChange);
    }

    _onFocus = () => {
        document.addEventListener("selectionchange", this._onSelectionChange);
        // force to recalculate
        this._lastSelection = null;
        this._refreshLastCaretIfNeeded();
    }

    _onSelectionChange = () => {
        this._refreshLastCaretIfNeeded();
    }

    _onKeyDown = (event) => {
        const model = this.props.model;
        const modKey = IS_MAC ? event.metaKey : event.ctrlKey;
        let handled = false;
        // undo
        if (modKey && event.key === "z") {
            if (this.historyManager.canUndo()) {
                const {parts, caret} = this.historyManager.undo(this.props.model);
                // pass matching inputType so historyManager doesn't push echo
                // when invoked from rerender callback.
                model.reset(parts, caret, "historyUndo");
            }
            handled = true;
        // redo
        } else if (modKey && event.key === "y") {
            if (this.historyManager.canRedo()) {
                const {parts, caret} = this.historyManager.redo();
                // pass matching inputType so historyManager doesn't push echo
                // when invoked from rerender callback.
                model.reset(parts, caret, "historyRedo");
            }
            handled = true;
        // insert newline on Shift+Enter
        } else if (event.key === "Enter" && (event.shiftKey || (IS_MAC && event.altKey))) {
            this._insertText("\n");
            handled = true;
        // autocomplete or enter to send below shouldn't have any modifier keys pressed.
        } else if (!(event.metaKey || event.altKey || event.shiftKey)) {
            if (model.autoComplete) {
                const autoComplete = model.autoComplete;
                switch (event.key) {
                    case "Enter":
                        autoComplete.onEnter(event); break;
                    case "ArrowUp":
                        autoComplete.onUpArrow(event); break;
                    case "ArrowDown":
                        autoComplete.onDownArrow(event); break;
                    case "Tab":
                        autoComplete.onTab(event); break;
                    case "Escape":
                        autoComplete.onEscape(event); break;
                    default:
                        return; // don't preventDefault on anything else
                }
                handled = true;
            }
        }
        if (handled) {
            event.preventDefault();
            event.stopPropagation();
        }
    }

    _cancelEdit = () => {
        dis.dispatch({action: "edit_event", event: null});
        dis.dispatch({action: 'focus_composer'});
    }

    isModified() {
        return this._modifiedFlag;
    }

    _onAutoCompleteConfirm = (completion) => {
        this.props.model.autoComplete.onComponentConfirm(completion);
    }

    _onAutoCompleteSelectionChange = (completion) => {
        this.props.model.autoComplete.onComponentSelectionChange(completion);
    }

    componentWillUnmount() {
        this._editorRef.removeEventListener("input", this._onInput, true);
    }

    componentDidMount() {
        const model = this.props.model;
        model.setUpdateCallback(this._updateEditorState);
        const partCreator = model.partCreator;
        // TODO: does this allow us to get rid of EditorStateTransfer?
        // not really, but we could not serialize the parts, and just change the autoCompleter
        partCreator.setAutoCompleteCreator(autoCompleteCreator(
            () => this._autocompleteRef,
            query => this.setState({query}),
        ));
        this.historyManager = new HistoryManager(partCreator);
        // initial render of model
        this._updateEditorState(this._getInitialCaretPosition());
        // attach input listener by hand so React doesn't proxy the events,
        // as the proxied event doesn't support inputType, which we need.
        this._editorRef.addEventListener("input", this._onInput, true);
        this._editorRef.focus();
    }

    _getInitialCaretPosition() {
        let caretPosition;
        if (this.props.initialCaret) {
            // if restoring state from a previous editor,
            // restore caret position from the state
            const caret = this.props.initialCaret;
            caretPosition = this.props.model.positionForOffset(caret.offset, caret.atNodeEnd);
        } else {
            // otherwise, set it at the end
            caretPosition = this.props.model.getPositionAtEnd();
        }
        return caretPosition;
    }

    render() {
        let autoComplete;
        if (this.state.autoComplete) {
            const query = this.state.query;
            const queryLen = query.length;
            autoComplete = (

                <Autocomplete
                    ref={ref => this._autocompleteRef = ref}
                    query={query}
                    onConfirm={this._onAutoCompleteConfirm}
                    onSelectionChange={this._onAutoCompleteSelectionChange}
                    selection={{beginning: true, end: queryLen, start: queryLen}}
                    room={this.props.room}
                />

);
        }
        return (

            { autoComplete }

 this._editorRef = ref}
                aria-label={this.props.label}
            >

);
    }

    focus() {
        this._editorRef.focus();
    }
}
<commit_msg>remove dead code<commit_after>/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher';
import EditorModel from '../../../editor/model';
import HistoryManager from '../../../editor/history';
import {setCaretPosition} from '../../../editor/caret';
import {getCaretOffsetAndText} from '../../../editor/dom';
import Autocomplete from '../rooms/Autocomplete';
import {autoCompleteCreator} from '../../../editor/parts';
import {renderModel} from '../../../editor/render';
import {Room} from 'matrix-js-sdk';

const IS_MAC = navigator.platform.indexOf("Mac") !== -1;

function cloneSelection(selection) {
    return {
        anchorNode: selection.anchorNode,
        anchorOffset: selection.anchorOffset,
        focusNode: selection.focusNode,
        focusOffset: selection.focusOffset,
        isCollapsed: selection.isCollapsed,
        rangeCount: selection.rangeCount,
        type: selection.type,
    };
}

function selectionEquals(a: Selection, b: Selection): boolean {
    return a.anchorNode === b.anchorNode &&
        a.anchorOffset === b.anchorOffset &&
        a.focusNode === b.focusNode &&
        a.focusOffset === b.focusOffset &&
        a.isCollapsed === b.isCollapsed &&
        a.rangeCount === b.rangeCount &&
        a.type === b.type;
}

export default class BasicMessageEditor extends React.Component {
    static propTypes = {
        model: PropTypes.instanceOf(EditorModel).isRequired,
        room: PropTypes.instanceOf(Room).isRequired,
        placeholder: PropTypes.string,
        label: PropTypes.string,    // the aria label
        initialCaret: PropTypes.object, // See DocumentPosition in editor/model.js
    };

    constructor(props, context) {
        super(props, context);
        this.state = {
            autoComplete: null,
        };
        this._editorRef = null;
        this._autocompleteRef = null;
        this._modifiedFlag = false;
    }

    _updateEditorState = (caret, inputType, diff) => {
        renderModel(this._editorRef, this.props.model);
        if (caret) {
            try {
                setCaretPosition(this._editorRef, this.props.model, caret);
            } catch (err) {
                console.error(err);
            }
        }
        if (this.props.placeholder) {
            const {isEmpty} = this.props.model;
            if (isEmpty) {
                this._editorRef.style.setProperty("--placeholder", `'${this.props.placeholder}'`);
                this._editorRef.classList.add("mx_BasicMessageComposer_inputEmpty");
            } else {
                this._editorRef.classList.remove("mx_BasicMessageComposer_inputEmpty");
                this._editorRef.style.removeProperty("--placeholder");
            }
        }
        this.setState({autoComplete: this.props.model.autoComplete});
        this.historyManager.tryPush(this.props.model, caret, inputType, diff);
    }

    _onInput = (event) => {
        this._modifiedFlag = true;
        const sel = document.getSelection();
        const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
        this._setLastCaret(caret, text, sel);
        this.props.model.update(text, event.inputType, caret);
    }

    _insertText(textToInsert, inputType = "insertText") {
        const sel = document.getSelection();
        const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
        const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset);
        caret.offset += textToInsert.length;
        this.props.model.update(newText, inputType, caret);
    }

    // this is used later to see if we need to recalculate the caret
    // on selectionchange. If it is just a consequence of typing
    // we don't need to. But if the user is navigating the caret without input
    // we need to recalculate it, to be able to know where to insert content after
    // losing focus
    _setLastCaret(caret, text, selection) {
        this._lastSelection = cloneSelection(selection);
        this._lastCaret = caret;
        this._lastTextLength = text.length;
    }

    _refreshLastCaretIfNeeded() {
        // TODO: needed when going up and down in editing messages ... not sure why yet
        // because the editors should stop doing this when when blurred ...
        // maybe it's on focus and the _editorRef isn't available yet or something.
        if (!this._editorRef) {
            return;
        }
        const selection = document.getSelection();
        if (!this._lastSelection || !selectionEquals(this._lastSelection, selection)) {
            this._lastSelection = cloneSelection(selection);
            const {caret, text} = getCaretOffsetAndText(this._editorRef, selection);
            this._lastCaret = caret;
            this._lastTextLength = text.length;
        }
        return this._lastCaret;
    }

    clearUndoHistory() {
        this.historyManager.clear();
    }

    getCaret() {
        return this._lastCaret;
    }

    isCaretAtStart() {
        return this.getCaret().offset === 0;
    }

    isCaretAtEnd() {
        return this.getCaret().offset === this._lastTextLength;
    }

    _onBlur = () => {
        document.removeEventListener("selectionchange", this._onSelectionChange);
    }

    _onFocus = () => {
        document.addEventListener("selectionchange", this._onSelectionChange);
        // force to recalculate
        this._lastSelection = null;
        this._refreshLastCaretIfNeeded();
    }

    _onSelectionChange = () => {
        this._refreshLastCaretIfNeeded();
    }

    _onKeyDown = (event) => {
        const model = this.props.model;
        const modKey = IS_MAC ? event.metaKey : event.ctrlKey;
        let handled = false;
        // undo
        if (modKey && event.key === "z") {
            if (this.historyManager.canUndo()) {
                const {parts, caret} = this.historyManager.undo(this.props.model);
                // pass matching inputType so historyManager doesn't push echo
                // when invoked from rerender callback.
                model.reset(parts, caret, "historyUndo");
            }
            handled = true;
        // redo
        } else if (modKey && event.key === "y") {
            if (this.historyManager.canRedo()) {
                const {parts, caret} = this.historyManager.redo();
                // pass matching inputType so historyManager doesn't push echo
                // when invoked from rerender callback.
                model.reset(parts, caret, "historyRedo");
            }
            handled = true;
        // insert newline on Shift+Enter
        } else if (event.key === "Enter" && (event.shiftKey || (IS_MAC && event.altKey))) {
            this._insertText("\n");
            handled = true;
        // autocomplete or enter to send below shouldn't have any modifier keys pressed.
        } else if (!(event.metaKey || event.altKey || event.shiftKey)) {
            if (model.autoComplete) {
                const autoComplete = model.autoComplete;
                switch (event.key) {
                    case "Enter":
                        autoComplete.onEnter(event); break;
                    case "ArrowUp":
                        autoComplete.onUpArrow(event); break;
                    case "ArrowDown":
                        autoComplete.onDownArrow(event); break;
                    case "Tab":
                        autoComplete.onTab(event); break;
                    case "Escape":
                        autoComplete.onEscape(event); break;
                    default:
                        return; // don't preventDefault on anything else
                }
                handled = true;
            }
        }
        if (handled) {
            event.preventDefault();
            event.stopPropagation();
        }
    }

    isModified() {
        return this._modifiedFlag;
    }

    _onAutoCompleteConfirm = (completion) => {
        this.props.model.autoComplete.onComponentConfirm(completion);
    }

    _onAutoCompleteSelectionChange = (completion) => {
        this.props.model.autoComplete.onComponentSelectionChange(completion);
    }

    componentWillUnmount() {
        this._editorRef.removeEventListener("input", this._onInput, true);
    }

    componentDidMount() {
        const model = this.props.model;
        model.setUpdateCallback(this._updateEditorState);
        const partCreator = model.partCreator;
        // TODO: does this allow us to get rid of EditorStateTransfer?
        // not really, but we could not serialize the parts, and just change the autoCompleter
        partCreator.setAutoCompleteCreator(autoCompleteCreator(
            () => this._autocompleteRef,
            query => this.setState({query}),
        ));
        this.historyManager = new HistoryManager(partCreator);
        // initial render of model
        this._updateEditorState(this._getInitialCaretPosition());
        // attach input listener by hand so React doesn't proxy the events,
        // as the proxied event doesn't support inputType, which we need.
        this._editorRef.addEventListener("input", this._onInput, true);
        this._editorRef.focus();
    }

    _getInitialCaretPosition() {
        let caretPosition;
        if (this.props.initialCaret) {
            // if restoring state from a previous editor,
            // restore caret position from the state
            const caret = this.props.initialCaret;
            caretPosition = this.props.model.positionForOffset(caret.offset, caret.atNodeEnd);
        } else {
            // otherwise, set it at the end
            caretPosition = this.props.model.getPositionAtEnd();
        }
        return caretPosition;
    }

    render() {
        let autoComplete;
        if (this.state.autoComplete) {
            const query = this.state.query;
            const queryLen = query.length;
            autoComplete = (

                <Autocomplete
                    ref={ref => this._autocompleteRef = ref}
                    query={query}
                    onConfirm={this._onAutoCompleteConfirm}
                    onSelectionChange={this._onAutoCompleteSelectionChange}
                    selection={{beginning: true, end: queryLen, start: queryLen}}
                    room={this.props.room}
                />

);
        }
        return (

            { autoComplete }

 this._editorRef = ref}
                aria-label={this.props.label}
            >

);
    }

    focus() {
        this._editorRef.focus();
    }
}
<|endoftext|>


<commit_before>
var coge_markers;
define( [
             'dojo/_base/declare',
             'dijit/Dialog',
            'JBrowse/View/Track/HTMLFeatures'
         ],

         function(
             declare,
             Dialog,
             HTMLFeatures
         ) {
return declare( [ HTMLFeatures ], {
    constructor: function() {
        this.inherited(arguments); // call superclass constructor
        coge_markers = this;
        this.browser = arguments[0].browser;
    },

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

    _trackMenuOptions: function() {
        var options = this.inherited(arguments);
        var track = this;

        if (track.config.coge.type == 'notebook')
            return options;

        options.push({ type: 'dijit/MenuSeparator' });

		if (track.config.coge.type == 'experiment')
			options.push({
				label: 'ExperimentView',
				onClick: function(){window.open( 'ExperimentView.pl?eid=' + track.config.coge.id );}
			});

        if (track.config.coge.type != 'search')  {
            options.push({
                label: 'Find Data that Overlaps Features',
                onClick: function(){coge_plugin.features_overlap_search_dialog(track, 'Markers', 'markers');}
            });
        }
        options.push({
            label: 'Export Track Data',
            onClick: function(){coge_plugin.export_dialog(track);}
        });
        if (track.config.coge.search)
            options.push({
                label: 'Merge Markers',
                onClick: function(){coge_plugin.markers_merge_dialog(track)}
            });
            options.push({
                label: 'Save Results as New Experiment',
                onClick: function(){coge_plugin.save_as_experiment_dialog(track)}
            });
        return options;
    },

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

    updateStaticElements: function( coords ) {
        this.inherited( arguments );
        coge_plugin.adjust_nav(this.config.coge.id)
    }
});
});

<commit_msg>fixing detached git branch<commit_after>
var coge_markers;
define([
    'dojo/_base/declare',
    'dijit/Dialog',
    'CoGe/View/ColorDialog',
    'JBrowse/View/Track/HTMLFeatures'
],
function(declare, Dialog, ColorDialog, HTMLFeatures) {
    return declare( [ HTMLFeatures ], {
    constructor: function() {
        this.inherited(arguments); // call superclass constructor
        coge_markers = this;
        this.browser = arguments[0].browser;
    },

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

    _trackMenuOptions: function() {
        var options = this.inherited(arguments);
        var track = this;
        var config = track.config;

        if (config.coge.type == 'notebook')
            return options;

        options.push({ type: 'dijit/MenuSeparator' });

		if (config.coge.type == 'experiment')
			options.push({
				label: 'ExperimentView',
				onClick: function(){window.open( 'ExperimentView.pl?eid=' + config.coge.id );}
            });

		options.push({
            label: 'Change color',
            onClick: function(event) {
                if (!track._color_dialog) {
					var feature_color = {};
					if (config.style.featureCss)
						feature_color[config.coge.id] = config.style.featureCss.substring(17);
                    track._color_dialog = new ColorDialog({
                        title: "Change colors",
                        style: { width: '230px' },
                        track: track,
                        items: config.coge.experiments || [{value: config.coge.id, label: config.coge.name}],
                        featureColor: feature_color,
                        callback: function(id, color) {
                            var curColor = config.style.featureCss;
                            if (!curColor || curColor.substring(curColor.indexOf(':') + 1).toLowerCase() != color.toLowerCase()) {
                                config.style.featureCss = 'background-color:' + color;
								track.updateUserStyles({ featureCss : config.style.featureCss });
                                track.changed();
                                coge_track_list.setTrackColor(config.track, id, color);
                            }
                        }
                    });
                }
                track._color_dialog.show();
            }
        });

        if (config.coge.type != 'search')  {
            options.push({
                label: 'Find Data that Overlaps Features',
                onClick: function(){coge_plugin.features_overlap_search_dialog(track, 'Markers', 'markers');}
            });
        }
        options.push({
            label: 'Export Track Data',
            onClick: function(){coge_plugin.export_dialog(track);}
        });
        if (config.coge.search)
            options.push({
                label: 'Merge Markers',
                onClick: function(){coge_plugin.markers_merge_dialog(track)}
            });
            options.push({
                label: 'Save Results as New Experiment',
                onClick: function(){coge_plugin.save_as_experiment_dialog(track)}
            });
        return options;
    },

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

    updateStaticElements: function( coords ) {
        this.inherited( arguments );
        coge_plugin.adjust_nav(this.config.coge.id)
    }
});
});
<|endoftext|>


<commit_before>function pageViewModel(gvm) {
    // Page specific i18n bindings
    gvm.title = ko.computed(function(){i18n.setLocale(gvm.lang()); return gvm.app() + ' - ' + i18n.__("AdminPage");}, gvm);
    gvm.pageHeaderEditUser = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("UserEditTitle");}, gvm);

    gvm.edituserid = $("#usereditHeader").data('value');

    gvm.userName = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("UserName");}, gvm);
    gvm.firstName = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("Firstname");}, gvm);
    gvm.lastName = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("Lastname");}, gvm);
    gvm.userStatus = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("UserStatus");}, gvm);

    gvm.permissionRole = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("PermissionRole");}, gvm);
    gvm.permissionDescription = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("PermissionDescription");}, gvm);

    getAllUserDataById(gvm.edituserid());

    gvm.rights = ko.observableArray([]);
    gvm.user = ko.observableArray([]);

    gvm.updateUser = function(user)
    {
        gvm.user.push(user);
    },

    gvm.updatePermissions = function(permission)
    {
        gvm.rights.push(user);
    },

    gvm.removeUser = function(user) {
        gvm.user.remove(user);
        removeUser(user);
    },

    gvm.clearStructure = function() {
        gvm.rights.destroyAll();
        gvm.user.destroyAll();
    }
}

function initPage() {
    getAllUserDataById();
}

function getAllUserDataById(edituserid){
    console.log("get " + edituserid);
    $.getJSON("/api/edituser/" + edituserid, function(data)
    {
        console.log("get user data");
        var addedUsername = "";
        $.each(data, function(i, item){
            console.log(item.username);

            var current = item.username;
            $.each(data, function(i, item)
            {
                if(item.username == current){
                    viewModel.updatePermissions(new Permission(item.roleid, item.role))
                    console.log(item.roleid + " " + item.role );
                }
            });

            permissions = permissions.substr(0, permissions.length - 3);

            if (addedUsername != current){
                addedUsername = item.username;
                viewModel.updateUser(new User(item.userid, item.username, item.firstname, item.lastname, viewModel.rights()));
            }
        });
    });
    console.log(edituserid)
    console.log("end")
}

function Permission(id, permissions) {
    return {
        id: ko.observable(id),
        permissions: ko.observable(permissions)
    };
}

function User(id, username, firstname, lastname, status) {
    return {
        id: ko.observable(id),
        username: ko.observable(username),
        firstname: ko.observable(firstname),
        lastname: ko.observable(lastname),
        status: ko.observable(status),

        removeThisUser: function() {
            if(confirm('Are you sure you want to remove this user?'))
            {
                viewModel.removeUser(this);
            }
        },

        changeStatus: function() {
            if (this.status() == "ACTIVE"){
                this.status("DISABLED");
            } else if (this.status() == "DISABLED") {
                this.status("ACTIVE");
            } else {
                this.status("WAIT_ACTIVATION");
            }
            updateUserStatus(this);
        }
    };
}<commit_msg>Admin edit user<commit_after>function pageViewModel(gvm) {
    // Page specific i18n bindings
    gvm.title = ko.computed(function(){i18n.setLocale(gvm.lang()); return gvm.app() + ' - ' + i18n.__("AdminPage");}, gvm);
    gvm.pageHeaderEditUser = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("UserEditTitle");}, gvm);

    gvm.edituserid = $("#usereditHeader").data('value');

    gvm.userName = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("UserName");}, gvm);
    gvm.firstName = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("Firstname");}, gvm);
    gvm.lastName = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("Lastname");}, gvm);
    gvm.userStatus = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("UserStatus");}, gvm);

    gvm.permissionRole = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("PermissionRole");}, gvm);
    gvm.permissionDescription = ko.computed(function(){i18n.setLocale(gvm.lang()); return i18n.__("PermissionDescription");}, gvm);

    getAllUserDataById(gvm.edituserid);

    gvm.rights = ko.observableArray([]);
    gvm.user = ko.observableArray([]);

    gvm.updateUser = function(user)
    {
        gvm.user.push(user);
    },

    gvm.updatePermissions = function(permission)
    {
        gvm.rights.push(user);
    },

    gvm.removeUser = function(user) {
        gvm.user.remove(user);
        removeUser(user);
    },

    gvm.clearStructure = function() {
        gvm.rights.destroyAll();
        gvm.user.destroyAll();
    }
}

function initPage() {
    getAllUserDataById();
}

function getAllUserDataById(edituserid){
    console.log("get " + edituserid);
    $.getJSON("/api/edituser/" + edituserid, function(data)
    {
        console.log("get user data");
        var addedUsername = "";
        $.each(data, function(i, item){
            console.log(item.username);

            var current = item.username;
            $.each(data, function(i, item)
            {
                if(item.username == current){
                    viewModel.updatePermissions(new Permission(item.roleid, item.role))
                    console.log(item.roleid + " " + item.role );
                }
            });

            permissions = permissions.substr(0, permissions.length - 3);

            if (addedUsername != current){
                addedUsername = item.username;
                viewModel.updateUser(new User(item.userid, item.username, item.firstname, item.lastname, viewModel.rights()));
            }
        });
    });
    console.log(edituserid)
    console.log("end")
}

function Permission(id, permissions) {
    return {
        id: ko.observable(id),
        permissions: ko.observable(permissions)
    };
}

function User(id, username, firstname, lastname, status) {
    return {
        id: ko.observable(id),
        username: ko.observable(username),
        firstname: ko.observable(firstname),
        lastname: ko.observable(lastname),
        status: ko.observable(status),

        removeThisUser: function() {
            if(confirm('Are you sure you want to remove this user?'))
            {
                viewModel.removeUser(this);
            }
        },

        changeStatus: function() {
            if (this.status() == "ACTIVE"){
                this.status("DISABLED");
            } else if (this.status() == "DISABLED") {
                this.status("ACTIVE");
            } else {
                this.status("WAIT_ACTIVATION");
            }
            updateUserStatus(this);
        }
    };
}<|endoftext|>


<commit_before>'use strict';

(function(factory) {
  if (typeof define == 'function' && define.amd) {
    // AMD. Register as an anonymous module
    define(['jquery'], factory);
  }
  else if (typeof exports == 'object') {
    // Node/CommonJS
    module.exports = factory(require('jquery'));
  }
  else {
    // Browser globals
    factory(jQuery);
  }
})(function($) {
  function Checkboxpicker(element, options) {
    this.element = element;
    this.$element = $(element);

    this.options = $.extend({}, $.fn.checkboxpicker.defaults, options, this.$element.data());

    if (this.$element.closest('label').length) {
      console.warn(this.options.warningMessage);

      return;
    }

    this.$group = $('
');

    // .btn-group-justified works with  elements as the <button> doesn't pick up the styles
    this.$buttons = $('').addClass('btn');

    // === '': <... data-reverse>
    var reverse = this.options.reverse || this.options.reverse === '';

    this.$off = this.$buttons.eq(reverse ? 1 : 0);
    this.$on = this.$buttons.eq(reverse ? 0 : 1);

    this.init();
  }

  Checkboxpicker.prototype = {
    init: function() {
      // For IE 9/10 use $.fn.hide() instead $.fn.prop('hidden', true)
      this.$element.hide();

      if (this.options.offLabel) {
        this.$off.html(this.options.offLabel);
      }

      if (this.options.onLabel) {
        this.$on.html(this.options.onLabel);
      }

      if (this.options.offIconClass) {
        this.$off.prepend('');
      }

      if (this.options.onIconClass) {
        this.$on.prepend('');
      }

      if (this.element.checked) {
        this.$on.addClass('active ' + this.options.onClass);
        this.$off.addClass(this.options.defaultClass);
      }
      else {
        this.$off.addClass('active ' + this.options.offClass);
        this.$on.addClass(this.options.defaultClass);
      }

      if (this.options.style) {
        this.$group.addClass(this.options.style);
      }

      if (this.element.title) {
        this.$group.attr('title', this.element.title);
      }
      else {
        // Attribute title (offTitle, onTitle) on this.$buttons not work (native) if this.element.disabled, fine!
        if (this.options.offTitle) {
          this.$off.attr('title', this.options.offTitle);
        }

        if (this.options.onTitle) {
          this.$on.attr('title', this.options.onTitle);
        }
      }

      // Keydown event only trigger if set tabindex, fine!
      this.$group.keydown(this.keydown.bind(this));

      // Don't trigger if  element has .disabled class, fine!
      this.$group.on('click', 'a:not(.active)', this.click.bind(this));

      this.$element.change(this.toggle_checked.bind(this));

      if (this.element.id) {
        $('label[for="' + this.element.id + '"]').click(this.focus.bind(this));
      }

      $(this.element.form).on('reset', this.reset.bind(this));

      this.$group.append(this.$buttons).insertAfter(this.element);

      // Necessarily after this.$group.append() (autofocus)
      if (this.element.disabled) {
        this.$buttons.addClass('disabled');

        if (this.options.disabledCursor) {
          this.$group.css('cursor', this.options.disabledCursor);
        }
      }
      else {
        this.$group.attr('tabindex', this.element.tabIndex);

        if (this.element.autofocus) {
          this.focus();
        }
      }
    },
    toggle_checked: function() {
      // this.$group not focus (incorrect on form reset)
      this.$buttons.toggleClass('active ' + this.options.defaultClass);
      this.$off.toggleClass(this.options.offClass);
      this.$on.toggleClass(this.options.onClass);
    },
    toggle_disabled: function() {
      this.$buttons.toggleClass('disabled');

      if (this.element.disabled) {
        this.$group.attr('tabindex', this.element.tabIndex);
        this.$group.css('cursor', '');
      }
      else {
        this.$group.removeAttr('tabindex');

        if (this.options.disabledCursor) {
          this.$group.css('cursor', this.options.disabledCursor);
        }
      }
    },
    focus: function() {
      // Original behavior
      this.$group.focus();
    },
    click: function() {
      this.$element.prop('checked', !this.element.checked);
    },
    keydown: function(event) {
      // 13: Return, 32: Spacebar

      // Off vertical scrolling
      if (event.keyCode == 32) {
        event.preventDefault();
      }

      if (/^(13|32)$/.test(event.keyCode)) {
        this.click();
      }
    },
    reset: function() {
      // this.element.checked not used (incorect on large number of form elements)
      if ((this.element.defaultChecked && this.$off.hasClass('active')) || (!this.element.defaultChecked && this.$on.hasClass('active'))) {
        this.$element.change();
      }
    }
  };

  // Be hooks friendly
  var oldPropHooks = $.extend({}, $.propHooks);

  // Support $.fn.prop setter (checked, disabled)
  $.extend($.propHooks, {
    checked: {
      set: function(elem, value) {
        var data = $.data(elem, 'bs.checkbox');

        if (data && elem.checked != value) {
          data.$element.change();
        }

        if (oldPropHooks.checked && oldPropHooks.checked.set) {
          oldPropHooks.checked.set(elem, value);
        }
      }
    },
    disabled: {
      set: function(elem, value) {
        var data = $.data(elem, 'bs.checkbox');

        if (data && elem.disabled != value) {
          data.toggle_disabled();
        }

        if (oldPropHooks.disabled && oldPropHooks.disabled.set) {
          oldPropHooks.disabled.set(elem, value);
        }
      }
    }
  });

  // For AMD/Node/CommonJS used elements (optional)
  // http://learn.jquery.com/jquery-ui/environments/amd/
  $.fn.checkboxpicker = function(options, elements) {
    var $elements;

    if (this instanceof $) {
      $elements = this;
    }
    else if (typeof options == 'string') {
      $elements = $(options);
    }
    else {
      $elements = $(elements);
    }

    return $elements.each(function() {
      var data = $.data(this, 'bs.checkbox');

      if (!data) {
        data = new Checkboxpicker(this, options);

        $.data(this, 'bs.checkbox', data);
      }
    });
  };

  // HTML5 data-*.
  // <input data-on-label="43"> --> $('input').data('onLabel') == '43'.
  $.fn.checkboxpicker.defaults = {
    style: false,
    defaultClass: 'btn-default',
    disabledCursor: 'not-allowed',
    offClass: 'btn-danger',
    onClass: 'btn-success',
    offLabel: 'No',
    onLabel: 'Yes',
    offTitle: false,
    onTitle: false,
    warningMessage: 'Please do not use Bootstrap-checkbox element in label element.'
  };

  return $.fn.checkboxpicker;
});
<commit_msg>Fix change event.<commit_after>'use strict';

(function(factory) {
  if (typeof define == 'function' && define.amd) {
    // AMD. Register as an anonymous module
    define(['jquery'], factory);
  }
  else if (typeof exports == 'object') {
    // Node/CommonJS
    module.exports = factory(require('jquery'));
  }
  else {
    // Browser globals
    factory(jQuery);
  }
})(function($) {
  function Checkboxpicker(element, options) {
    this.element = element;
    this.$element = $(element);

    this.options = $.extend({}, $.fn.checkboxpicker.defaults, options, this.$element.data());

    if (this.$element.closest('label').length) {
      console.warn(this.options.warningMessage);

      return;
    }

    this.$group = $('
');

    // .btn-group-justified works with  elements as the <button> doesn't pick up the styles
    this.$buttons = $('').addClass('btn');

    // === '': <... data-reverse>
    var reverse = this.options.reverse || this.options.reverse === '';

    this.$off = this.$buttons.eq(reverse ? 1 : 0);
    this.$on = this.$buttons.eq(reverse ? 0 : 1);

    this.init();
  }

  Checkboxpicker.prototype = {
    init: function() {
      // For IE 9/10 use $.fn.hide() instead $.fn.prop('hidden', true)
      this.$element.hide();

      if (this.options.offLabel) {
        this.$off.html(this.options.offLabel);
      }

      if (this.options.onLabel) {
        this.$on.html(this.options.onLabel);
      }

      if (this.options.offIconClass) {
        this.$off.prepend('');
      }

      if (this.options.onIconClass) {
        this.$on.prepend('');
      }

      if (this.element.checked) {
        this.$on.addClass('active ' + this.options.onClass);
        this.$off.addClass(this.options.defaultClass);
      }
      else {
        this.$off.addClass('active ' + this.options.offClass);
        this.$on.addClass(this.options.defaultClass);
      }

      if (this.options.style) {
        this.$group.addClass(this.options.style);
      }

      if (this.element.title) {
        this.$group.attr('title', this.element.title);
      }
      else {
        // Attribute title (offTitle, onTitle) on this.$buttons not work (native) if this.element.disabled, fine!
        if (this.options.offTitle) {
          this.$off.attr('title', this.options.offTitle);
        }

        if (this.options.onTitle) {
          this.$on.attr('title', this.options.onTitle);
        }
      }

      // Keydown event only trigger if set tabindex, fine!
      this.$group.keydown(this.keydown.bind(this));

      // Don't trigger if  element has .disabled class, fine!
      this.$group.on('click', 'a:not(.active)', this.click.bind(this));

      this.$element.change(this.toggle_checked.bind(this));

      if (this.element.id) {
        $('label[for="' + this.element.id + '"]').click(this.focus.bind(this));
      }

      $(this.element.form).on('reset', this.reset.bind(this));

      this.$group.append(this.$buttons).insertAfter(this.element);

      // Necessarily after this.$group.append() (autofocus)
      if (this.element.disabled) {
        this.$buttons.addClass('disabled');

        if (this.options.disabledCursor) {
          this.$group.css('cursor', this.options.disabledCursor);
        }
      }
      else {
        this.$group.attr('tabindex', this.element.tabIndex);

        if (this.element.autofocus) {
          this.focus();
        }
      }
    },
    toggle_checked: function() {
      // this.$group not focus (incorrect on form reset)
      this.$buttons.toggleClass('active ' + this.options.defaultClass);
      this.$off.toggleClass(this.options.offClass);
      this.$on.toggleClass(this.options.onClass);
    },
    toggle_disabled: function() {
      this.$buttons.toggleClass('disabled');

      if (this.element.disabled) {
        this.$group.attr('tabindex', this.element.tabIndex);
        this.$group.css('cursor', '');
      }
      else {
        this.$group.removeAttr('tabindex');

        if (this.options.disabledCursor) {
          this.$group.css('cursor', this.options.disabledCursor);
        }
      }
    },
    focus: function() {
      // Original behavior
      this.$group.focus();
    },
    click: function() {
      this.$element.prop('checked', !this.element.checked);
    },
    change: function(value) {
      // Fix #12
      this.element.checked = value;

      this.$element.change();
    },
    keydown: function(event) {
      // 13: Return, 32: Spacebar

      // Off vertical scrolling
      if (event.keyCode == 32) {
        event.preventDefault();
      }

      if (/^(13|32)$/.test(event.keyCode)) {
        this.click();
      }
    },
    reset: function() {
      // this.element.checked not used (incorect on large number of form elements)
      if ((this.element.defaultChecked && this.$off.hasClass('active')) || (!this.element.defaultChecked && this.$on.hasClass('active'))) {
        this.change(this.element.defaultChecked);
      }
    }
  };

  // Be hooks friendly
  var oldPropHooks = $.extend({}, $.propHooks);

  // Support $.fn.prop setter (checked, disabled)
  $.extend($.propHooks, {
    checked: {
      set: function(elem, value) {
        var data = $.data(elem, 'bs.checkbox');

        if (data && elem.checked != value) {
          data.change(value);
        }

        if (oldPropHooks.checked && oldPropHooks.checked.set) {
          oldPropHooks.checked.set(elem, value);
        }
      }
    },
    disabled: {
      set: function(elem, value) {
        var data = $.data(elem, 'bs.checkbox');

        if (data && elem.disabled != value) {
          data.toggle_disabled();
        }

        if (oldPropHooks.disabled && oldPropHooks.disabled.set) {
          oldPropHooks.disabled.set(elem, value);
        }
      }
    }
  });

  // For AMD/Node/CommonJS used elements (optional)
  // http://learn.jquery.com/jquery-ui/environments/amd/
  $.fn.checkboxpicker = function(options, elements) {
    var $elements;

    if (this instanceof $) {
      $elements = this;
    }
    else if (typeof options == 'string') {
      $elements = $(options);
    }
    else {
      $elements = $(elements);
    }

    return $elements.each(function() {
      var data = $.data(this, 'bs.checkbox');

      if (!data) {
        data = new Checkboxpicker(this, options);

        $.data(this, 'bs.checkbox', data);
      }
    });
  };

  // HTML5 data-*.
  // <input data-on-label="43"> --> $('input').data('onLabel') == '43'.
  $.fn.checkboxpicker.defaults = {
    style: false,
    defaultClass: 'btn-default',
    disabledCursor: 'not-allowed',
    offClass: 'btn-danger',
    onClass: 'btn-success',
    offLabel: 'No',
    onLabel: 'Yes',
    offTitle: false,
    onTitle: false,
    warningMessage: 'Please do not use Bootstrap-checkbox element in label element.'
  };

  return $.fn.checkboxpicker;
});
<|endoftext|>