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

/**
 * Scenery node that shows a handle, which is made of two parts: the "grip" which is where you would grab it and the
 * "attachment" which are elbow-shaped bars that attach the handle to another surface.
 *
 * @author Chris Klusendorf (PhET Interactive Simulations)
 * @author Sam Reid (PhET Interactive Simulations)
 */
define( function( require ) {
  'use strict';

  // modules
  var Color = require( 'SCENERY/util/Color' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );
  var Matrix3 = require( 'DOT/Matrix3' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' );
  var Shape = require( 'KITE/Shape' );

  // constants
  // grip shape vars
  var GRIP_WIDTH = 100;
  var GRIP_HEIGHT = 42;
  var GRIP_CORNER_RADIUS = GRIP_WIDTH * 0.03;
  var GRIP_END_PAD = GRIP_WIDTH * 0.03; // horizontal line between the edge of the grip and the cubic curves
  var GRIP_SINGLE_FINGER_INDENT_DEPTH = GRIP_HEIGHT * 0.11;
  var GRIP_SINGLE_FINGER_INDENT_HALF_WIDTH = ( GRIP_WIDTH - GRIP_CORNER_RADIUS * 2 - GRIP_END_PAD * 2 ) / 8;
  var DEFAULT_GRIP_BASE_COLOR = new Color( 183, 184, 185 );

  /**
   * @param {Object} [options]
   * @constructor
   */
  function HandleNode( options ) {

    options = _.extend( {
      gripBaseColor: DEFAULT_GRIP_BASE_COLOR, // {Color|string} base color of gradient on the grip
      gripStroke: 'black', // {Color|string} stroke color of the grip
      gripLineWidth: 3,
      attachmentFill: 'gray', // {Color|string} solid fill color for the attachments
      attachmentStroke: 'black', // {Color|string} stroke color of the attachments
      attachmentLineWidth: 3,
      hasLeftAttachment: true,
      hasRightAttachment: true
    }, options );

    assert && assert( options.hasLeftAttachment || options.hasRightAttachment, 'at least one attachment is required' );

    // control points for cubic curve shape on grip
    // each single-finger indent is made of two cubic curves that are mirrored over the y-axis
    var controlPoint1X = GRIP_SINGLE_FINGER_INDENT_HALF_WIDTH / 2;
    var controlPoint1Y = 0;
    var controlPoint2X = GRIP_SINGLE_FINGER_INDENT_HALF_WIDTH / 4;
    var controlPoint2Y = GRIP_SINGLE_FINGER_INDENT_DEPTH;

    /**
     * Add an "up/down" combination to either the top or bottom of the grip.
     * @param {Shape} shape - the shape to append to
     * @param {number} sign - +1 for top side of grip, -1 for bottom side of grip
     */
    function addGripIndent( shape, sign ) {

      // this is a grip indent
      shape.cubicCurveToRelative(
        sign * controlPoint1X,
        sign * controlPoint1Y,
        sign * controlPoint2X,
        sign * controlPoint2Y,
        sign * GRIP_SINGLE_FINGER_INDENT_HALF_WIDTH,
        sign * GRIP_SINGLE_FINGER_INDENT_DEPTH )
        .cubicCurveToRelative(
          sign * ( GRIP_SINGLE_FINGER_INDENT_HALF_WIDTH - controlPoint2X ),
          sign * -controlPoint1Y,
          sign * ( GRIP_SINGLE_FINGER_INDENT_HALF_WIDTH - controlPoint1X ),
          sign * -controlPoint2Y,
          sign * GRIP_SINGLE_FINGER_INDENT_HALF_WIDTH,
          sign * -GRIP_SINGLE_FINGER_INDENT_DEPTH );
    }

    // the shape begins on the left edge, middle y
    // this is the upper left corner before grip indents start
    var gripShape = new Shape()
      .moveTo( 0, GRIP_HEIGHT / 2 )
      .lineTo( 0, GRIP_CORNER_RADIUS )
      .arc( GRIP_CORNER_RADIUS, GRIP_CORNER_RADIUS, GRIP_CORNER_RADIUS, Math.PI, Math.PI * 1.5, false )
      .lineToRelative( GRIP_END_PAD, 0 );

    // these are the top grip indents
    addGripIndent( gripShape, 1 );
    addGripIndent( gripShape, 1 );
    addGripIndent( gripShape, 1 );
    addGripIndent( gripShape, 1 );

    // this is the whole right edge
    gripShape.lineToRelative( GRIP_END_PAD, 0 )
      .arc( GRIP_WIDTH - GRIP_CORNER_RADIUS, GRIP_CORNER_RADIUS, GRIP_CORNER_RADIUS, Math.PI * 1.5, 0, false )
      .lineToRelative( 0, GRIP_HEIGHT - ( GRIP_CORNER_RADIUS * 2 ) )
      .arc( GRIP_WIDTH - GRIP_CORNER_RADIUS, GRIP_HEIGHT - GRIP_CORNER_RADIUS, GRIP_CORNER_RADIUS, 0, Math.PI / 2, false )
      .lineToRelative( -GRIP_END_PAD, 0 );

    // these are the bottom grip indents
    addGripIndent( gripShape, -1 );
    addGripIndent( gripShape, -1 );
    addGripIndent( gripShape, -1 );
    addGripIndent( gripShape, -1 );

    // this is the lower left hand corner
    gripShape.lineToRelative( -GRIP_END_PAD, 0 )
      .arc( GRIP_CORNER_RADIUS, GRIP_HEIGHT - GRIP_CORNER_RADIUS, GRIP_CORNER_RADIUS, Math.PI / 2, Math.PI, false )
      .lineTo( 0, GRIP_HEIGHT / 2 )
      .close();

    // add handle grip shape
    var gradientBaseColor = Color.toColor( options.gripBaseColor );
    var gripPath = new Path( gripShape, {
      lineWidth: options.gripLineWidth,
      stroke: options.gripStroke,
      fill: new LinearGradient( 0, 0, 0, GRIP_HEIGHT )
        .addColorStop( 0, gradientBaseColor )
        .addColorStop( 0.4, gradientBaseColor.brighterColor( 0.5 ) )
        .addColorStop( 0.7, gradientBaseColor )
        .addColorStop( 1.0, gradientBaseColor.darkerColor( 0.6 ) )
    } );

    assert && assert( !options.hasOwnProperty( 'children' ), 'HandleNode sets children' );
    options = _.extend( {
      children: [ gripPath ]
    }, options );

    // handle attachment shape vars
    var attachmentShaftWidth = GRIP_HEIGHT * 0.35;
    var attachmentHeight = GRIP_HEIGHT * 1.15;
    var attachmentBaseNubWidth = attachmentShaftWidth * 0.4;
    var attachmentBaseNubHeight = attachmentHeight * 0.2;
    var attachmentMiddleHeight = attachmentHeight * 0.5;
    var attachmentSmallArcRadius = attachmentShaftWidth * 0.5;

    var attachmentOptions = {
      fill: options.attachmentFill,
      stroke: options.attachmentStroke,
      lineWidth: options.attachmentLineWidth,
      lineJoin: 'round',
      top: gripPath.centerY - attachmentShaftWidth / 2
    };

    var leftAttachmentShape = new Shape()

    // Starts at bottom-left and proceeds clockwise
      .moveTo( 0, attachmentHeight )
      .lineToRelative( attachmentBaseNubWidth, -attachmentBaseNubHeight )
      .lineToRelative( 0, -attachmentMiddleHeight )

      // The main arc at the top left of the attachment
      .arc(
        attachmentShaftWidth + attachmentBaseNubWidth + attachmentSmallArcRadius,
        attachmentHeight - attachmentBaseNubHeight - attachmentMiddleHeight,
        attachmentShaftWidth + attachmentSmallArcRadius,
        Math.PI,
        Math.PI * 1.5,
        false
      )
      .lineToRelative( 0, attachmentShaftWidth )

      // This is the smaller arc on the underside of the attachment
      .arc(
        attachmentShaftWidth + attachmentBaseNubWidth + attachmentSmallArcRadius,
        attachmentHeight - attachmentBaseNubHeight - attachmentMiddleHeight,
        attachmentSmallArcRadius,
        Math.PI * 1.5,
        Math.PI,
        true
      )
      .lineToRelative( 0, attachmentMiddleHeight )
      .lineToRelative( attachmentBaseNubWidth, attachmentBaseNubHeight )
      .lineToRelative( -attachmentShaftWidth - ( attachmentBaseNubWidth * 2 ), 0 )
      .close();

    // left attachment
    if ( options.hasLeftAttachment ) {

      var leftAttachmentPath = new Path( leftAttachmentShape, _.extend( {
        right: gripPath.left + options.gripLineWidth
      }, attachmentOptions ) );

      options.children.unshift( leftAttachmentPath ); // prepend so that attachment is behind grip
    }

    // right attachment, a mirror image of the left
    if ( options.hasRightAttachment ) {

      var rightAttachmentShape = leftAttachmentShape.transformed( Matrix3.scaling( -1, 1 ) );

      // handle right attachment
      var rightAttachmentPath = new Path( rightAttachmentShape, _.extend( {
        left: gripPath.right - options.gripLineWidth
      }, attachmentOptions ) );

      options.children.unshift( rightAttachmentPath );  // prepend so that attachment is behind grip
    }

    Node.call( this, options );
  }

  sceneryPhet.register( 'HandleNode', HandleNode );

  return inherit( Node, HandleNode );
} );<commit_msg>move addGripIndent out of the constructor, #477<commit_after>// Copyright 2018, University of Colorado Boulder

/**
 * Scenery node that shows a handle, which is made of two parts: the "grip" which is where you would grab it and the
 * "attachment" which are elbow-shaped bars that attach the handle to another surface.
 *
 * @author Chris Klusendorf (PhET Interactive Simulations)
 * @author Sam Reid (PhET Interactive Simulations)
 */
define( function( require ) {
  'use strict';

  // modules
  var Color = require( 'SCENERY/util/Color' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );
  var Matrix3 = require( 'DOT/Matrix3' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' );
  var Shape = require( 'KITE/Shape' );

  // constants
  // grip shape vars
  var GRIP_WIDTH = 100;
  var GRIP_HEIGHT = 42;
  var GRIP_CORNER_RADIUS = GRIP_WIDTH * 0.03;
  var GRIP_END_PAD = GRIP_WIDTH * 0.03; // horizontal line between the edge of the grip and the cubic curves
  var GRIP_SINGLE_FINGER_INDENT_DEPTH = GRIP_HEIGHT * 0.11;
  var GRIP_SINGLE_FINGER_INDENT_HALF_WIDTH = ( GRIP_WIDTH - GRIP_CORNER_RADIUS * 2 - GRIP_END_PAD * 2 ) / 8;
  var DEFAULT_GRIP_BASE_COLOR = new Color( 183, 184, 185 );

  /**
   * @param {Object} [options]
   * @constructor
   */
  function HandleNode( options ) {

    options = _.extend( {
      gripBaseColor: DEFAULT_GRIP_BASE_COLOR, // {Color|string} base color of gradient on the grip
      gripStroke: 'black', // {Color|string} stroke color of the grip
      gripLineWidth: 3,
      attachmentFill: 'gray', // {Color|string} solid fill color for the attachments
      attachmentStroke: 'black', // {Color|string} stroke color of the attachments
      attachmentLineWidth: 3,
      hasLeftAttachment: true,
      hasRightAttachment: true
    }, options );

    assert && assert( options.hasLeftAttachment || options.hasRightAttachment, 'at least one attachment is required' );

    // the grip shape begins on the left edge, middle y
    // this is the upper left corner before grip indents start
    var gripShape = new Shape()
      .moveTo( 0, GRIP_HEIGHT / 2 )
      .lineTo( 0, GRIP_CORNER_RADIUS )
      .arc( GRIP_CORNER_RADIUS, GRIP_CORNER_RADIUS, GRIP_CORNER_RADIUS, Math.PI, Math.PI * 1.5, false )
      .lineToRelative( GRIP_END_PAD, 0 );

    // these are the top grip indents
    addGripIndent( gripShape, 1 );
    addGripIndent( gripShape, 1 );
    addGripIndent( gripShape, 1 );
    addGripIndent( gripShape, 1 );

    // this is the whole right edge
    gripShape.lineToRelative( GRIP_END_PAD, 0 )
      .arc( GRIP_WIDTH - GRIP_CORNER_RADIUS, GRIP_CORNER_RADIUS, GRIP_CORNER_RADIUS, Math.PI * 1.5, 0, false )
      .lineToRelative( 0, GRIP_HEIGHT - ( GRIP_CORNER_RADIUS * 2 ) )
      .arc( GRIP_WIDTH - GRIP_CORNER_RADIUS, GRIP_HEIGHT - GRIP_CORNER_RADIUS, GRIP_CORNER_RADIUS, 0, Math.PI / 2, false )
      .lineToRelative( -GRIP_END_PAD, 0 );

    // these are the bottom grip indents
    addGripIndent( gripShape, -1 );
    addGripIndent( gripShape, -1 );
    addGripIndent( gripShape, -1 );
    addGripIndent( gripShape, -1 );

    // this is the lower left hand corner
    gripShape.lineToRelative( -GRIP_END_PAD, 0 )
      .arc( GRIP_CORNER_RADIUS, GRIP_HEIGHT - GRIP_CORNER_RADIUS, GRIP_CORNER_RADIUS, Math.PI / 2, Math.PI, false )
      .lineTo( 0, GRIP_HEIGHT / 2 )
      .close();

    // add handle grip shape
    var gradientBaseColor = Color.toColor( options.gripBaseColor );
    var gripPath = new Path( gripShape, {
      lineWidth: options.gripLineWidth,
      stroke: options.gripStroke,
      fill: new LinearGradient( 0, 0, 0, GRIP_HEIGHT )
        .addColorStop( 0, gradientBaseColor )
        .addColorStop( 0.4, gradientBaseColor.brighterColor( 0.5 ) )
        .addColorStop( 0.7, gradientBaseColor )
        .addColorStop( 1.0, gradientBaseColor.darkerColor( 0.6 ) )
    } );

    assert && assert( !options.hasOwnProperty( 'children' ), 'HandleNode sets children' );
    options = _.extend( {
      children: [ gripPath ]
    }, options );

    // handle attachment shape vars
    var attachmentShaftWidth = GRIP_HEIGHT * 0.35;
    var attachmentHeight = GRIP_HEIGHT * 1.15;
    var attachmentBaseNubWidth = attachmentShaftWidth * 0.4;
    var attachmentBaseNubHeight = attachmentHeight * 0.2;
    var attachmentMiddleHeight = attachmentHeight * 0.5;
    var attachmentSmallArcRadius = attachmentShaftWidth * 0.5;

    var attachmentOptions = {
      fill: options.attachmentFill,
      stroke: options.attachmentStroke,
      lineWidth: options.attachmentLineWidth,
      lineJoin: 'round',
      top: gripPath.centerY - attachmentShaftWidth / 2
    };

    var leftAttachmentShape = new Shape()

    // Starts at bottom-left and proceeds clockwise
      .moveTo( 0, attachmentHeight )
      .lineToRelative( attachmentBaseNubWidth, -attachmentBaseNubHeight )
      .lineToRelative( 0, -attachmentMiddleHeight )

      // The main arc at the top left of the attachment
      .arc(
        attachmentShaftWidth + attachmentBaseNubWidth + attachmentSmallArcRadius,
        attachmentHeight - attachmentBaseNubHeight - attachmentMiddleHeight,
        attachmentShaftWidth + attachmentSmallArcRadius,
        Math.PI,
        Math.PI * 1.5,
        false
      )
      .lineToRelative( 0, attachmentShaftWidth )

      // This is the smaller arc on the underside of the attachment
      .arc(
        attachmentShaftWidth + attachmentBaseNubWidth + attachmentSmallArcRadius,
        attachmentHeight - attachmentBaseNubHeight - attachmentMiddleHeight,
        attachmentSmallArcRadius,
        Math.PI * 1.5,
        Math.PI,
        true
      )
      .lineToRelative( 0, attachmentMiddleHeight )
      .lineToRelative( attachmentBaseNubWidth, attachmentBaseNubHeight )
      .lineToRelative( -attachmentShaftWidth - ( attachmentBaseNubWidth * 2 ), 0 )
      .close();

    // left attachment
    if ( options.hasLeftAttachment ) {

      var leftAttachmentPath = new Path( leftAttachmentShape, _.extend( {
        right: gripPath.left + options.gripLineWidth
      }, attachmentOptions ) );

      options.children.unshift( leftAttachmentPath ); // prepend so that attachment is behind grip
    }

    // right attachment, a mirror image of the left
    if ( options.hasRightAttachment ) {

      var rightAttachmentShape = leftAttachmentShape.transformed( Matrix3.scaling( -1, 1 ) );

      // handle right attachment
      var rightAttachmentPath = new Path( rightAttachmentShape, _.extend( {
        left: gripPath.right - options.gripLineWidth
      }, attachmentOptions ) );

      options.children.unshift( rightAttachmentPath );  // prepend so that attachment is behind grip
    }

    Node.call( this, options );
  }

  sceneryPhet.register( 'HandleNode', HandleNode );

  /**
   * Add an "up/down" combination to either the top or bottom of the grip.
   * @param {Shape} shape - the shape to append to
   * @param {number} sign - +1 for top side of grip, -1 for bottom side of grip
   */
  function addGripIndent( shape, sign ) {

    // control points for cubic curve shape on grip
    // each single-finger indent is made of two cubic curves that are mirrored over the y-axis
    var controlPoint1X = GRIP_SINGLE_FINGER_INDENT_HALF_WIDTH / 2;
    var controlPoint1Y = 0;
    var controlPoint2X = GRIP_SINGLE_FINGER_INDENT_HALF_WIDTH / 4;
    var controlPoint2Y = GRIP_SINGLE_FINGER_INDENT_DEPTH;

    // this is a grip indent
    shape.cubicCurveToRelative(
      sign * controlPoint1X,
      sign * controlPoint1Y,
      sign * controlPoint2X,
      sign * controlPoint2Y,
      sign * GRIP_SINGLE_FINGER_INDENT_HALF_WIDTH,
      sign * GRIP_SINGLE_FINGER_INDENT_DEPTH )
      .cubicCurveToRelative(
        sign * ( GRIP_SINGLE_FINGER_INDENT_HALF_WIDTH - controlPoint2X ),
        sign * -controlPoint1Y,
        sign * ( GRIP_SINGLE_FINGER_INDENT_HALF_WIDTH - controlPoint1X ),
        sign * -controlPoint2Y,
        sign * GRIP_SINGLE_FINGER_INDENT_HALF_WIDTH,
        sign * -GRIP_SINGLE_FINGER_INDENT_DEPTH );
  }

  return inherit( Node, HandleNode );
} );<|endoftext|>


<commit_before>var Enoki = require('choo-dat-hypha/lib')
var enoki = new Enoki()

module.exports = docs

function docs (state, emitter, app) {
  state.docs = {
    loaded: false,
    content: { },
    site: { }
  }

  state.events.DOCS_LOAD = 'docs:load'

  emitter.on(state.events.DOCS_LOAD, onLoad)

  async function onLoad () {
    await enoki.load()
    state.docs.content = await enoki.readContent()
    state.docs.loaded = true
    emitter.emit(state.events.RENDER)
  }
}
<commit_msg>enoki<commit_after>var Enoki = require('enoki/lib')
var enoki = new Enoki()

module.exports = docs

function docs (state, emitter, app) {
  state.docs = {
    loaded: false,
    content: { },
    site: { }
  }

  state.events.DOCS_LOAD = 'docs:load'

  emitter.on(state.events.DOCS_LOAD, onLoad)

  async function onLoad () {
    await enoki.load()
    state.docs.content = await enoki.readContent()
    state.docs.loaded = true
    emitter.emit(state.events.RENDER)
  }
}
<|endoftext|>


<commit_before>function ImageTrack(trackMeta, url, refSeq, browserParams) {
    Track.call(this, trackMeta.label, trackMeta.key,
               false, browserParams.changeCallback);
    this.refSeq = refSeq;
    this.tileToImage = {};
    this.zoomCache = {};
    this.baseUrl = (browserParams.baseUrl ? browserParams.baseUrl : "");
    this.load(this.baseUrl + url);

    this.imgErrorHandler = function(ev) {
        var img = ev.target || ev.srcElement;
        img.src="";
    };
}

ImageTrack.prototype = new Track("");

ImageTrack.prototype.loadSuccess = function(o) {
    //tileWidth: width, in pixels, of the tiles
    this.tileWidth = o.tileWidth;
    //zoomLevels: array of {basesPerTile, scale, height, urlPrefix} hashes
    this.zoomLevels = o.zoomLevels;
    this.setLoaded();
};

ImageTrack.prototype.setViewInfo = function(numBlocks, trackDiv, labelDiv,
                                            widthPct, widthPx, scale) {
    Track.prototype.setViewInfo.apply(this, [numBlocks, trackDiv, labelDiv,
                                             widthPct, widthPx, scale]);
    this.setLabel(this.key);
};

ImageTrack.prototype.getZoom = function(scale) {
    var result = this.zoomCache[scale];
    if (result) return result;

    result = this.zoomLevels[0];
    var desiredBases = this.tileWidth / scale;
    for (i = 1; i < this.zoomLevels.length; i++) {
        if (Math.abs(this.zoomLevels[i].basesPerTile - desiredBases)
            < Math.abs(result.basesPerTile - desiredBases))
            result = this.zoomLevels[i];
    }

    this.zoomCache[scale] = result;
    return result;
};

ImageTrack.prototype.getImages = function(zoom, startBase, endBase) {
    //var startTile = ((startBase - this.refSeq.start) / zoom.basesPerTile) | 0;
    //var endTile = ((endBase - this.refSeq.start) / zoom.basesPerTile) | 0;
    var startTile = (startBase / zoom.basesPerTile) | 0;
    var endTile = (endBase / zoom.basesPerTile) | 0;
    startTile = Math.max(startTile, 0);
    var result = [];
    var im;
    for (var i = startTile; i <= endTile; i++) {
	im = this.tileToImage[i];
	if (!im) {
	    im = document.createElement("img");
            im.onerror = this.imgErrorHandler;
            //prepend this.baseUrl if zoom.urlPrefix is relative
            im.src = (zoom.urlPrefix.match(/^(([^/]+:)|\/)/) ? "" : this.baseUrl)
                     + zoom.urlPrefix + i + ".png";
            //TODO: need image coord systems that don't start at 0?
	    im.startBase = (i * zoom.basesPerTile); // + this.refSeq.start;
	    im.baseWidth = zoom.basesPerTile;
	    im.tileNum = i;
	    this.tileToImage[i] = im;
	}
	result.push(im);
    }
    return result;
};

ImageTrack.prototype.fillBlock = function(block, leftBlock, rightBlock, leftBase, rightBase, scale, stripeWidth) {
    var zoom = this.getZoom(scale);
    var blockWidth = rightBase - leftBase;
    var images = this.getImages(zoom, leftBase, rightBase);
    var im;

    for (var i = 0; i < images.length; i++) {
	im = images[i];
	if (!(im.parentNode && im.parentNode.parentNode)) {
	    im.style.cssText = "position: absolute; left: " + (100 * ((im.startBase - leftBase) / blockWidth)) + "%; width: " + (100 * (im.baseWidth / blockWidth)) + "%; top: 0px; height: " + zoom.height + "px;";
            block.appendChild(im);
	}
    }

    return zoom.height;
};

ImageTrack.prototype.startZoom = function(destScale, destStart, destEnd) {
    if (this.empty) return;
    this.tileToImage = {};
    this.getImages(this.getZoom(destScale), destStart, destEnd);
};

ImageTrack.prototype.endZoom = function(destScale, destBlockBases) {
    Track.prototype.clear.apply(this);
};

ImageTrack.prototype.clear = function() {
    Track.prototype.clear.apply(this);
    this.tileToImage = {};
};

ImageTrack.prototype.transfer = function(sourceBlock, destBlock) {
    if (!(sourceBlock && destBlock)) return;

    var children = sourceBlock.childNodes;
    var destLeft = destBlock.startBase;
    var destRight = destBlock.endBase;
    var im;
    for (var i = 0; i < children.length; i++) {
	im = children[i];
	if ("startBase" in im) {
	    //if sourceBlock contains an image that overlaps destBlock,
	    if ((im.startBase < destRight)
		&& ((im.startBase + im.baseWidth) > destLeft)) {
		//move image from sourceBlock to destBlock
		im.style.left = (100 * ((im.startBase - destLeft) / (destRight - destLeft))) + "%";
		destBlock.appendChild(im);
	    } else {
		delete this.tileToImage[im.tileNum];
	    }
	}
    }
};

/*

Copyright (c) 2007-2009 The Evolutionary Software Foundation

Created by Mitchell Skinner <mitch_skinner@berkeley.edu>

This package and its accompanying libraries are free software; you can
redistribute it and/or modify it under the terms of the LGPL (either
version 2.1, or at your option, any later version) or the Artistic
License 2.0.  Refer to LICENSE for the full license text.

*/
<commit_msg>actually make the image 404 handler work properly<commit_after>function ImageTrack(trackMeta, url, refSeq, browserParams) {
    Track.call(this, trackMeta.label, trackMeta.key,
               false, browserParams.changeCallback);
    this.refSeq = refSeq;
    this.tileToImage = {};
    this.zoomCache = {};
    this.baseUrl = (browserParams.baseUrl ? browserParams.baseUrl : "");
    this.load(this.baseUrl + url);

    this.imgErrorHandler = function(ev) {
        var img = ev.target || ev.srcElement;
        img.style.display = "none";
        dojo.stopEvent(ev);
    };
}

ImageTrack.prototype = new Track("");

ImageTrack.prototype.loadSuccess = function(o) {
    //tileWidth: width, in pixels, of the tiles
    this.tileWidth = o.tileWidth;
    //zoomLevels: array of {basesPerTile, scale, height, urlPrefix} hashes
    this.zoomLevels = o.zoomLevels;
    this.setLoaded();
};

ImageTrack.prototype.setViewInfo = function(numBlocks, trackDiv, labelDiv,
                                            widthPct, widthPx, scale) {
    Track.prototype.setViewInfo.apply(this, [numBlocks, trackDiv, labelDiv,
                                             widthPct, widthPx, scale]);
    this.setLabel(this.key);
};

ImageTrack.prototype.getZoom = function(scale) {
    var result = this.zoomCache[scale];
    if (result) return result;

    result = this.zoomLevels[0];
    var desiredBases = this.tileWidth / scale;
    for (i = 1; i < this.zoomLevels.length; i++) {
        if (Math.abs(this.zoomLevels[i].basesPerTile - desiredBases)
            < Math.abs(result.basesPerTile - desiredBases))
            result = this.zoomLevels[i];
    }

    this.zoomCache[scale] = result;
    return result;
};

ImageTrack.prototype.getImages = function(zoom, startBase, endBase) {
    //var startTile = ((startBase - this.refSeq.start) / zoom.basesPerTile) | 0;
    //var endTile = ((endBase - this.refSeq.start) / zoom.basesPerTile) | 0;
    var startTile = (startBase / zoom.basesPerTile) | 0;
    var endTile = (endBase / zoom.basesPerTile) | 0;
    startTile = Math.max(startTile, 0);
    var result = [];
    var im;
    for (var i = startTile; i <= endTile; i++) {
	im = this.tileToImage[i];
	if (!im) {
	    im = document.createElement("img");
            dojo.connect(im, "onerror", this.imgErrorHandler);
            //prepend this.baseUrl if zoom.urlPrefix is relative
            im.src = (zoom.urlPrefix.match(/^(([^/]+:)|\/)/) ? "" : this.baseUrl)
                     + zoom.urlPrefix + i + ".png";
            //TODO: need image coord systems that don't start at 0?
	    im.startBase = (i * zoom.basesPerTile); // + this.refSeq.start;
	    im.baseWidth = zoom.basesPerTile;
	    im.tileNum = i;
	    this.tileToImage[i] = im;
	}
	result.push(im);
    }
    return result;
};

ImageTrack.prototype.fillBlock = function(block, leftBlock, rightBlock, leftBase, rightBase, scale, stripeWidth) {
    var zoom = this.getZoom(scale);
    var blockWidth = rightBase - leftBase;
    var images = this.getImages(zoom, leftBase, rightBase);
    var im;

    for (var i = 0; i < images.length; i++) {
	im = images[i];
	if (!(im.parentNode && im.parentNode.parentNode)) {
            im.style.position = "absolute";
            im.style.left = (100 * ((im.startBase - leftBase) / blockWidth)) + "%";
            im.style.width = (100 * (im.baseWidth / blockWidth)) + "%";
            im.style.top = "0px";
            im.style.height = zoom.height + "px";
            block.appendChild(im);
	}
    }

    return zoom.height;
};

ImageTrack.prototype.startZoom = function(destScale, destStart, destEnd) {
    if (this.empty) return;
    this.tileToImage = {};
    this.getImages(this.getZoom(destScale), destStart, destEnd);
};

ImageTrack.prototype.endZoom = function(destScale, destBlockBases) {
    Track.prototype.clear.apply(this);
};

ImageTrack.prototype.clear = function() {
    Track.prototype.clear.apply(this);
    this.tileToImage = {};
};

ImageTrack.prototype.transfer = function(sourceBlock, destBlock) {
    if (!(sourceBlock && destBlock)) return;

    var children = sourceBlock.childNodes;
    var destLeft = destBlock.startBase;
    var destRight = destBlock.endBase;
    var im;
    for (var i = 0; i < children.length; i++) {
	im = children[i];
	if ("startBase" in im) {
	    //if sourceBlock contains an image that overlaps destBlock,
	    if ((im.startBase < destRight)
		&& ((im.startBase + im.baseWidth) > destLeft)) {
		//move image from sourceBlock to destBlock
		im.style.left = (100 * ((im.startBase - destLeft) / (destRight - destLeft))) + "%";
		destBlock.appendChild(im);
	    } else {
		delete this.tileToImage[im.tileNum];
	    }
	}
    }
};

/*

Copyright (c) 2007-2009 The Evolutionary Software Foundation

Created by Mitchell Skinner <mitch_skinner@berkeley.edu>

This package and its accompanying libraries are free software; you can
redistribute it and/or modify it under the terms of the LGPL (either
version 2.1, or at your option, any later version) or the Artistic
License 2.0.  Refer to LICENSE for the full license text.

*/
<|endoftext|>


<commit_before>/**
 * Library core: constructor, prototype
 * @param {string|object} selector
 * @param {string|object} context - optional
 * @returns {object} Grindstone
 */

	var Grindstone = function(selector, context) {
		var set = this;
		if (selector) {
			var ctx, elems;
			if (typeof selector === 'string') {
				if (context) {
					if (typeof context === 'string') {
						ctx = d.querySelectorAll(context);
					} else if(priv.isElementArray(context)) {
						ctx = context;
					} else {
						ctx = [context];
					}
					Array.prototype.forEach.call(ctx, function(item) {
						elems = item.querySelectorAll(selector);
						Array.prototype.forEach.call(elems, function(el) {
							if (set.indexOf(el) === -1) {
								set.push(el);
							}
						});
					});
				} else {
					set.push.apply(set, d.querySelectorAll(selector));
				}
			} else if (priv.isElementArray(selector)) {
				set.push.apply(set, selector);
			} else {
				set.push(selector);
			}
		}
		this.set = set; // Backwards compatibility.
		return this;
	};

	Grindstone.prototype = [];

	var $ = function(selector, context) {
		return new Grindstone(selector, context);
	};

	$.fn = Grindstone.prototype;

	// private functions
	var priv = {};

	priv.prop = function(obj, property) {
		return obj.hasOwnProperty(property);
	};

	priv.createInteraction = function(touchEvt, mouseEvt) {
		return 'ontouchend' in d ? touchEvt : mouseEvt;
	};

	priv.isWindow = function(obj) {
		if (typeof(window.constructor) === 'undefined') {
			return obj instanceof window.constructor;
		} else {
			return obj.window === obj;
		}
	};

	// This also returns true for Grindstone objects.
	priv.isElementArray = function(obj) {
		return !priv.isWindow(obj) && typeof obj.length === 'number';
	};

	priv.matchesFuncName = Element.prototype.matches ? 'matches' :
		Element.prototype.matchesSelector ? 'matchesSelector' :
		Element.prototype.webkitMatchesSelector ? 'webkitMatchesSelector' :
		Element.prototype.mozMatchesSelector ? 'mozMatchesSelector' :
		Element.prototype.msMatchesSelector ? 'msMatchesSelector' :
		undefined;
<commit_msg>Simpler array check<commit_after>/**
 * Library core: constructor, prototype
 * @param {string|object} selector
 * @param {string|object} context - optional
 * @returns {object} Grindstone
 */

	var Grindstone = function(selector, context) {
		var set = this;
		if (selector) {
			var ctx, elems;
			if (typeof selector === 'string') {
				if (context) {
					if (typeof context === 'string') {
						ctx = d.querySelectorAll(context);
					} else if(priv.isElementArray(context)) {
						ctx = context;
					} else {
						ctx = [context];
					}
					Array.prototype.forEach.call(ctx, function(item) {
						elems = item.querySelectorAll(selector);
						Array.prototype.forEach.call(elems, function(el) {
							if (set.indexOf(el) === -1) {
								set.push(el);
							}
						});
					});
				} else {
					set.push.apply(set, d.querySelectorAll(selector));
				}
			} else if (priv.isElementArray(selector)) {
				set.push.apply(set, selector);
			} else {
				set.push(selector);
			}
		}
		this.set = set; // Backwards compatibility.
		return this;
	};

	Grindstone.prototype = [];

	var $ = function(selector, context) {
		return new Grindstone(selector, context);
	};

	$.fn = Grindstone.prototype;

	// private functions
	var priv = {};

	priv.prop = function(obj, property) {
		return obj.hasOwnProperty(property);
	};

	priv.createInteraction = function(touchEvt, mouseEvt) {
		return 'ontouchend' in d ? touchEvt : mouseEvt;
	};

	// This also returns true for Grindstone objects.
	priv.isElementArray = function(obj) {
		return obj instanceof Array
	};

	priv.matchesFuncName = Element.prototype.matches ? 'matches' :
		Element.prototype.matchesSelector ? 'matchesSelector' :
		Element.prototype.webkitMatchesSelector ? 'webkitMatchesSelector' :
		Element.prototype.mozMatchesSelector ? 'mozMatchesSelector' :
		Element.prototype.msMatchesSelector ? 'msMatchesSelector' :
		undefined;
<|endoftext|>


<commit_before>// @flow

// Vendor
import React from 'react';
import { uniq, get, sortBy, groupBy, find } from 'lodash';
import { compose } from 'recompose';

import externalReferenceLinks from '@ncigdc/utils/externalReferenceLinks';

import { Row, Column } from '@ncigdc/uikit/Flex';
import { ExternalLink } from '@ncigdc/uikit/Links';
import { Tooltip } from '@ncigdc/uikit/Tooltip';

import GeneLink from '@ncigdc/components/Links/GeneLink';
import BubbleIcon from '@ncigdc/theme/icons/BubbleIcon';

import { tableToolTipHint } from '@ncigdc/theme/mixins';
import MinusIcon from '@ncigdc/theme/icons/Minus';
import PlusIcon from '@ncigdc/theme/icons/Plus';
import TableIcon from '@ncigdc/theme/icons/Table';
import BookIcon from '@ncigdc/theme/icons/Book';
import ChartIcon from '@ncigdc/theme/icons/BarChart';
import CollapsibleList from '@ncigdc/uikit/CollapsibleList';
import { withTheme } from '@ncigdc/theme';

import Lolliplot from '@ncigdc/containers/Lolliplot';

import EntityPageVerticalTable from './EntityPageVerticalTable';
import EntityPageHorizontalTable from './EntityPageHorizontalTable';
import CancerDistribution from './CancerDistribution';

const styles = {
  heading: {
    flexGrow: 1,
    fontSize: '2rem',
    marginBottom: 7,
    marginTop: 7,
  },
  summary: {
    marginBottom: '2rem',
    minWidth: '450px',
  },
  column: {
    width: '100%',
    minWidth: 450,
  },
  card: {
    backgroundColor: 'white',
  },
};

type TProps = {|
  viewer: Object,
  node: Object,
  allCasesAgg: Array<Object>,
|};

const Mutation = compose(
  withTheme
)(({
  viewer,
  node: mutation,
  allCasesAgg,
  theme,
}: TProps = {}) => {
  const allCasesAggByProject = allCasesAgg.reduce((acc, bucket) =>
    ({ ...acc, [bucket.key]: bucket.doc_count }), {}
  );
  const consequences = mutation.consequence.hits.edges;
  const dbSNP = consequences.reduce((acc, c) => (
    c.node.transcript.annotation.dbsnp_rs ? c.node.transcript.annotation.dbsnp_rs : acc
  ), '');
  const consequenceOfInterest = consequences
    .find(consequence => get(consequence, 'node.transcript.annotation.impact'), {});
  const functionalImpactTranscript = get(consequenceOfInterest, 'node.transcript', {});
  const functionalImpact = get(functionalImpactTranscript, 'annotation.impact');

  const cancerDistData = mutation.occurrence.hits.edges.reduce((acc, o) => {
    const cases = uniq([
      ...(acc[o.node.case.project.project_id] || { cases: [] }).cases,
      o.node.case.case_id,
    ]);
    return {
      ...acc,
      [o.node.case.project.project_id]: {
        disease_type: o.node.case.disease_type,
        cancer_type: o.node.case.project.cancer_type || 'tbd',
        site: o.node.case.primary_site,
        freq: cases.length / allCasesAggByProject[o.node.case.project.project_id],
        cases,
      },
    };
  }, {});

  const sortedCancerDistData = Object.keys(cancerDistData) // eslint-disable-line fp/no-mutating-methods
    .map(k => ({ project_id: k, ...cancerDistData[k] }))
    .sort((a, b) => b.freq - a.freq);

  const distinctCases = sortedCancerDistData.reduce((acc, d) => [...acc, ...d.cases], []);

  const strandIconMap = {
    '-1': <MinusIcon />,
    // $FlowIgnore
    1: <PlusIcon />,
  };

  const canonicalTranscriptId = (find(consequences, 'node.transcript.is_canonical') || { node: { transcript: { transcript_id: '' } } }).node.transcript.transcript_id;
  const consequenceDataGrouped = groupBy(consequences, c => {
    const { transcript } = c.node;
    return `${transcript.gene.symbol}${transcript.aa_change}${transcript.consequence_type}${transcript.annotation.hgvsc}${transcript.gene.gene_strand}`;
  });

  const canonicalGeneId = mutation.consequence.hits.edges.find(x =>
    x.node.transcript.is_canonical
  ).node.transcript.gene.gene_id;

  const consquenceDataMapped = Object.values(consequenceDataGrouped).map(d => {
    const first = d[0].node.transcript;
    const transcripts = d.map(n => n.node.transcript.transcript_id);
    return {
      symbol: <GeneLink id={first.gene.gene_id}>{first.gene.symbol}</GeneLink>,
      aa_change: first.aa_change,
      consequence: first.consequence_type,
      coding_dna_change: first.annotation.hgvsc,
      strand: first.gene.gene_strand ? strandIconMap[first.gene.gene_strand.toString(10)] : '--',
      transcripts: (
        <CollapsibleList
          style={{ width: '150px' }}
          data={sortBy(transcripts, t => t !== canonicalTranscriptId).map(t => (

              <ExternalLink
                key={t}
                style={{
                  paddingRight: '0.5em',
                  fontWeight: t === functionalImpactTranscript.transcript_id ? 'bold' : 'normal',
                }}
                href={externalReferenceLinks.ensembl(t)}
              >
                {t}
              </ExternalLink>
              {
                t === canonicalTranscriptId &&
                <BubbleIcon
                  text='C'
                  toolTipText='Canonical'
                  backgroundColor={theme.primary}
                />
              }

          ))}
        />
      ),
    };
  });

  return (

      <Row spacing="2rem" id="summary">
        <EntityPageVerticalTable
          id="Summary"
          title={<TableIcon style={{ marginRight: '1rem' }} />Summary}
          thToTd={[
            { th: 'ID', td: mutation.ssm_id },
            {
              th: 'DNA change',
              td:
                `${mutation.chromosome}:g.${mutation.start_position}`
              + `${mutation.reference_allele}>${mutation.tumor_allele}`,
            },
            { th: 'Type', td: mutation.mutation_subtype },
            { th: 'Reference genome assembly', td: mutation.ncbi_build || '' },
            { th: 'Allele in the reference assembly', td: mutation.reference_allele || '' },
            { th: 'Functional Impact (VEP)',
              td: (functionalImpact &&

                  <BubbleIcon
                    toolTipText={functionalImpact}
                    text={functionalImpact.slice(0, (functionalImpact === 'MODIFIER' ? 2 : 1))}
                    backgroundColor={theme.impacts[functionalImpact]}
                  />

                    {functionalImpact.toLowerCase()}

                  <ExternalLink
                    key={functionalImpactTranscript.transcript_id}
                    style={{ paddingRight: '0.5em' }}
                    href={externalReferenceLinks.ensembl(functionalImpactTranscript.transcript_id)}
                  >
                    {functionalImpactTranscript.transcript_id}
                  </ExternalLink>

              ),
              style: { textTransform: 'capitalize' } },
          ]}
          style={{
            ...styles.summary,
            ...styles.column,
            alignSelf: 'flex-start',
          }}
        />
        <EntityPageVerticalTable
          title={<BookIcon style={{ marginRight: '1rem' }} /> External References}
          thToTd={[
            {
              th: (dbSNP),
              td: (dbSNP && /rs(\d+)$/g.test(dbSNP)) ? dbSNP : '--',
            },
            { th: 'COSMIC',
              td: (mutation.cosmic_id.length ?

                {mutation.cosmic_id.map(
                (c) => (

                    <ExternalLink
                      href={externalReferenceLinks[c.substring(0, 4).toLowerCase()](c.match(/(\d+)$/g))}
                    >
                      {c}
                    </ExternalLink>

                ))
              }

 : '--') },
          ]}
          style={{ ...styles.summary, ...styles.column, alignSelf: 'flex-start' }}
        />
      </Row>
      <Column style={styles.card}>

          <TableIcon style={{ marginRight: '1rem' }} />
          Consequences

        <Row>
          <EntityPageHorizontalTable
            style={{ width: '100%', minWidth: '450px' }}
            headings={[
              { key: 'symbol', title: 'Gene' },
              { key: 'aa_change', title: 'AA Change' },
              { key: 'consequence',
                title: (
                  <Tooltip
                    Component={'SO Term: consequence type'}
                    style={tableToolTipHint()}
                  >
                    Consequence
                  </Tooltip>
                ),
              },
              { key: 'coding_dna_change', title: 'Coding DNA Change' },
              { key: 'strand', title: 'Strand' },
              { key: 'transcripts', title: 'Transcript(s)' },
            ]}
            data={consquenceDataMapped}
          />
        </Row>
      </Column>
      <Column style={{ ...styles.card, marginTop: '2rem' }} id="cancer-distribution">
        <Row>

            <ChartIcon style={{ marginRight: '1rem' }} />
            Cancer Distribution

        </Row>
        <CancerDistribution
          tableHeadings={[
            { key: 'project_id', title: 'Project ID' },
            { key: 'disease_type', title: 'Disease Type' },
            { key: 'site', title: 'Site' },
            {
              key: 'num_affected_cases',
              title: '# Affected Cases',
            },
          ]}
          tableData={sortedCancerDistData.map(d => ({
            ...d,
            project_id: <a href={`/projects/${d.project_id}`}>{d.project_id},
            num_affected_cases:
              `${d.cases.length} / ${allCasesAggByProject[d.project_id]} (${(d.freq * 100).toFixed(2)}%)`,
          }))}
          chartData={sortedCancerDistData.map(d => ({
            label: d.project_id,
            value: (d.freq * 100),
            tooltip: {d.project_id}
{(d.freq * 100).toFixed(2)}%,
          }))}
          tagline={`This mutation affects
          ${distinctCases.length} distinct case${distinctCases.length > 1 ? 's' : ''} across
          ${sortedCancerDistData.length} cancer projects`}
        />
      </Column>
      <Column style={{ ...styles.card, marginTop: '2rem' }}>
        <Lolliplot
          mutationId={mutation.ssm_id}
          geneId={canonicalGeneId}
          ssms={viewer.cohort.lolliplotFragment}
          viewer={viewer}
        />
      </Column>

  );
});

export default Mutation;
<commit_msg>🐛 guard against null ssm.cosmic_id<commit_after>// @flow

// Vendor
import React from 'react';
import { uniq, get, sortBy, groupBy, find } from 'lodash';
import { compose } from 'recompose';

import externalReferenceLinks from '@ncigdc/utils/externalReferenceLinks';

import { Row, Column } from '@ncigdc/uikit/Flex';
import { ExternalLink } from '@ncigdc/uikit/Links';
import { Tooltip } from '@ncigdc/uikit/Tooltip';

import GeneLink from '@ncigdc/components/Links/GeneLink';
import BubbleIcon from '@ncigdc/theme/icons/BubbleIcon';

import { tableToolTipHint } from '@ncigdc/theme/mixins';
import MinusIcon from '@ncigdc/theme/icons/Minus';
import PlusIcon from '@ncigdc/theme/icons/Plus';
import TableIcon from '@ncigdc/theme/icons/Table';
import BookIcon from '@ncigdc/theme/icons/Book';
import ChartIcon from '@ncigdc/theme/icons/BarChart';
import CollapsibleList from '@ncigdc/uikit/CollapsibleList';
import { withTheme } from '@ncigdc/theme';

import Lolliplot from '@ncigdc/containers/Lolliplot';

import EntityPageVerticalTable from './EntityPageVerticalTable';
import EntityPageHorizontalTable from './EntityPageHorizontalTable';
import CancerDistribution from './CancerDistribution';

const styles = {
  heading: {
    flexGrow: 1,
    fontSize: '2rem',
    marginBottom: 7,
    marginTop: 7,
  },
  summary: {
    marginBottom: '2rem',
    minWidth: '450px',
  },
  column: {
    width: '100%',
    minWidth: 450,
  },
  card: {
    backgroundColor: 'white',
  },
};

type TProps = {|
  viewer: Object,
  node: Object,
  allCasesAgg: Array<Object>,
|};

const Mutation = compose(
  withTheme
)(({
  viewer,
  node: mutation,
  allCasesAgg,
  theme,
}: TProps = {}) => {
  const allCasesAggByProject = allCasesAgg.reduce((acc, bucket) =>
    ({ ...acc, [bucket.key]: bucket.doc_count }), {}
  );
  const consequences = mutation.consequence.hits.edges;
  const dbSNP = consequences.reduce((acc, c) => (
    c.node.transcript.annotation.dbsnp_rs ? c.node.transcript.annotation.dbsnp_rs : acc
  ), '');
  const consequenceOfInterest = consequences
    .find(consequence => get(consequence, 'node.transcript.annotation.impact'), {});
  const functionalImpactTranscript = get(consequenceOfInterest, 'node.transcript', {});
  const functionalImpact = get(functionalImpactTranscript, 'annotation.impact');

  const cancerDistData = mutation.occurrence.hits.edges.reduce((acc, o) => {
    const cases = uniq([
      ...(acc[o.node.case.project.project_id] || { cases: [] }).cases,
      o.node.case.case_id,
    ]);
    return {
      ...acc,
      [o.node.case.project.project_id]: {
        disease_type: o.node.case.disease_type,
        cancer_type: o.node.case.project.cancer_type || 'tbd',
        site: o.node.case.primary_site,
        freq: cases.length / allCasesAggByProject[o.node.case.project.project_id],
        cases,
      },
    };
  }, {});

  const sortedCancerDistData = Object.keys(cancerDistData) // eslint-disable-line fp/no-mutating-methods
    .map(k => ({ project_id: k, ...cancerDistData[k] }))
    .sort((a, b) => b.freq - a.freq);

  const distinctCases = sortedCancerDistData.reduce((acc, d) => [...acc, ...d.cases], []);

  const strandIconMap = {
    '-1': <MinusIcon />,
    // $FlowIgnore
    1: <PlusIcon />,
  };

  const canonicalTranscriptId = (find(consequences, 'node.transcript.is_canonical') || { node: { transcript: { transcript_id: '' } } }).node.transcript.transcript_id;
  const consequenceDataGrouped = groupBy(consequences, c => {
    const { transcript } = c.node;
    return `${transcript.gene.symbol}${transcript.aa_change}${transcript.consequence_type}${transcript.annotation.hgvsc}${transcript.gene.gene_strand}`;
  });

  const canonicalGeneId = mutation.consequence.hits.edges.find(x =>
    x.node.transcript.is_canonical
  ).node.transcript.gene.gene_id;

  const consquenceDataMapped = Object.values(consequenceDataGrouped).map(d => {
    const first = d[0].node.transcript;
    const transcripts = d.map(n => n.node.transcript.transcript_id);
    return {
      symbol: <GeneLink id={first.gene.gene_id}>{first.gene.symbol}</GeneLink>,
      aa_change: first.aa_change,
      consequence: first.consequence_type,
      coding_dna_change: first.annotation.hgvsc,
      strand: first.gene.gene_strand ? strandIconMap[first.gene.gene_strand.toString(10)] : '--',
      transcripts: (
        <CollapsibleList
          style={{ width: '150px' }}
          data={sortBy(transcripts, t => t !== canonicalTranscriptId).map(t => (

              <ExternalLink
                key={t}
                style={{
                  paddingRight: '0.5em',
                  fontWeight: t === functionalImpactTranscript.transcript_id ? 'bold' : 'normal',
                }}
                href={externalReferenceLinks.ensembl(t)}
              >
                {t}
              </ExternalLink>
              {
                t === canonicalTranscriptId &&
                <BubbleIcon
                  text='C'
                  toolTipText='Canonical'
                  backgroundColor={theme.primary}
                />
              }

          ))}
        />
      ),
    };
  });

  return (

      <Row spacing="2rem" id="summary">
        <EntityPageVerticalTable
          id="Summary"
          title={<TableIcon style={{ marginRight: '1rem' }} />Summary}
          thToTd={[
            { th: 'ID', td: mutation.ssm_id },
            {
              th: 'DNA change',
              td:
                `${mutation.chromosome}:g.${mutation.start_position}`
              + `${mutation.reference_allele}>${mutation.tumor_allele}`,
            },
            { th: 'Type', td: mutation.mutation_subtype },
            { th: 'Reference genome assembly', td: mutation.ncbi_build || '' },
            { th: 'Allele in the reference assembly', td: mutation.reference_allele || '' },
            { th: 'Functional Impact (VEP)',
              td: (functionalImpact &&

                  <BubbleIcon
                    toolTipText={functionalImpact}
                    text={functionalImpact.slice(0, (functionalImpact === 'MODIFIER' ? 2 : 1))}
                    backgroundColor={theme.impacts[functionalImpact]}
                  />

                    {functionalImpact.toLowerCase()}

                  <ExternalLink
                    key={functionalImpactTranscript.transcript_id}
                    style={{ paddingRight: '0.5em' }}
                    href={externalReferenceLinks.ensembl(functionalImpactTranscript.transcript_id)}
                  >
                    {functionalImpactTranscript.transcript_id}
                  </ExternalLink>

              ),
              style: { textTransform: 'capitalize' } },
          ]}
          style={{
            ...styles.summary,
            ...styles.column,
            alignSelf: 'flex-start',
          }}
        />
        <EntityPageVerticalTable
          title={<BookIcon style={{ marginRight: '1rem' }} /> External References}
          thToTd={[
            {
              th: (dbSNP),
              td: (dbSNP && /rs(\d+)$/g.test(dbSNP)) ? dbSNP : '--',
            },
            { th: 'COSMIC',
              td: ((mutation.cosmic_id || []).length ?

                {(mutation.cosmic_id || []).map(
                (c) => (

                    <ExternalLink
                      href={externalReferenceLinks[c.substring(0, 4).toLowerCase()](c.match(/(\d+)$/g))}
                    >
                      {c}
                    </ExternalLink>

                ))
              }

 : '--') },
          ]}
          style={{ ...styles.summary, ...styles.column, alignSelf: 'flex-start' }}
        />
      </Row>
      <Column style={styles.card}>

          <TableIcon style={{ marginRight: '1rem' }} />
          Consequences

        <Row>
          <EntityPageHorizontalTable
            style={{ width: '100%', minWidth: '450px' }}
            headings={[
              { key: 'symbol', title: 'Gene' },
              { key: 'aa_change', title: 'AA Change' },
              { key: 'consequence',
                title: (
                  <Tooltip
                    Component={'SO Term: consequence type'}
                    style={tableToolTipHint()}
                  >
                    Consequence
                  </Tooltip>
                ),
              },
              { key: 'coding_dna_change', title: 'Coding DNA Change' },
              { key: 'strand', title: 'Strand' },
              { key: 'transcripts', title: 'Transcript(s)' },
            ]}
            data={consquenceDataMapped}
          />
        </Row>
      </Column>
      <Column style={{ ...styles.card, marginTop: '2rem' }} id="cancer-distribution">
        <Row>

            <ChartIcon style={{ marginRight: '1rem' }} />
            Cancer Distribution

        </Row>
        <CancerDistribution
          tableHeadings={[
            { key: 'project_id', title: 'Project ID' },
            { key: 'disease_type', title: 'Disease Type' },
            { key: 'site', title: 'Site' },
            {
              key: 'num_affected_cases',
              title: '# Affected Cases',
            },
          ]}
          tableData={sortedCancerDistData.map(d => ({
            ...d,
            project_id: <a href={`/projects/${d.project_id}`}>{d.project_id},
            num_affected_cases:
              `${d.cases.length} / ${allCasesAggByProject[d.project_id]} (${(d.freq * 100).toFixed(2)}%)`,
          }))}
          chartData={sortedCancerDistData.map(d => ({
            label: d.project_id,
            value: (d.freq * 100),
            tooltip: {d.project_id}
{(d.freq * 100).toFixed(2)}%,
          }))}
          tagline={`This mutation affects
          ${distinctCases.length} distinct case${distinctCases.length > 1 ? 's' : ''} across
          ${sortedCancerDistData.length} cancer projects`}
        />
      </Column>
      <Column style={{ ...styles.card, marginTop: '2rem' }}>
        <Lolliplot
          mutationId={mutation.ssm_id}
          geneId={canonicalGeneId}
          ssms={viewer.cohort.lolliplotFragment}
          viewer={viewer}
        />
      </Column>

  );
});

export default Mutation;
<|endoftext|>


<commit_before>// Copyright 2016, University of Colorado Boulder

/**
 * IO type for Property
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Andrew Adare (PhET Interactive Simulations)
 */
define( function( require ) {
  'use strict';

  // modules
  var axon = require( 'AXON/axon' );

  // ifphetio
  var assertInstanceOf = require( 'ifphetio!PHET_IO/assertInstanceOf' );
  var FunctionIO = require( 'ifphetio!PHET_IO/types/FunctionIO' );
  var ObjectIO = require( 'ifphetio!PHET_IO/types/ObjectIO' );
  var phetio = require( 'ifphetio!PHET_IO/phetio' );
  var phetioInherit = require( 'ifphetio!PHET_IO/phetioInherit' );
  var VoidIO = require( 'ifphetio!PHET_IO/types/VoidIO' );

  /**
   * An observable property that triggers notifications when the value changes.
   * @param {function} phetioValueType - If loaded by phet (not phet-io) it will be the function returned by the
   *                                     'ifphetio!' plugin.
   * @param options
   * @module PropertyIO
   * @constructor
   */
  function PropertyIO( phetioValueType, options ) {

    /**
     * @param {Property} property
     * @param {string} phetioID
     * @constructor
     */
    var PropertyIOImpl = function PropertyIOImpl( property, phetioID ) {
      assert && assert( !!phetioValueType, 'PropertyIO needs phetioValueType' );
      assert && assert( property, 'Property should exist' );
      assert && assert( _.endsWith( phetioID, 'Property' ), 'PropertyIO instances should end with the "Property" suffix, for ' + phetioID );

      assert && assertInstanceOf( property, phet.axon.Property );
      ObjectIO.call( this, property, phetioID );
    };

    return phetioInherit( ObjectIO, 'PropertyIO', PropertyIOImpl, {
      getValue: {
        returnType: phetioValueType,
        parameterTypes: [],
        implementation: function() {
          return this.instance.get();
        },
        documentation: 'Gets the current value.'
      },

      setValue: {
        returnType: VoidIO,
        parameterTypes: [ phetioValueType ],
        implementation: function( value ) {
          this.instance.set( value );
        },
        documentation: 'Sets the value of the property. If the value differs from the previous value, listeners are ' +
                       'notified with the new value.'
      },

      link: {
        returnType: VoidIO,
        parameterTypes: [ FunctionIO( VoidIO, [ phetioValueType, phetioValueType ] ) ],
        implementation: function( listener ) {
          this.instance.link( listener );
        },
        documentation: 'Adds a listener which will be called when the value changes. On registration, the listener is ' +
                       'also called with the current value. The listener takes two arguments, the new value and the ' +
                       'previous value.'
      },

      lazyLink: {
        returnType: VoidIO,
        parameterTypes: [ FunctionIO( VoidIO, [ phetioValueType, phetioValueType ] ) ],
        implementation: function( listener ) {
          this.instance.lazyLink( listener );
        },
        documentation: 'Adds a listener which will be called when the value changes. This method is like "link", but ' +
                       'without the current-value callback on registration. The listener takes two arguments, the new ' +
                       'value and the previous value.'
      },

      unlink: {
        returnType: VoidIO,
        parameterTypes: [ FunctionIO( VoidIO, [ phetioValueType ] ) ],
        implementation: function( listener ) {
          this.instance.unlink( listener );
        },
        documentation: 'Removes a listener.'
      }
    }, {
      documentation: 'Observable values that send out notifications when the value changes. This differs from the ' +
                     'traditional listener pattern in that added listeners also receive a callback with the current value ' +
                     'when the listeners are registered. This is a widely-used pattern in PhET-iO simulations.',
      methodOrder: [ 'link', 'lazyLink' ],
      elementType: phetioValueType,

      // Used to generate the unique parametric typename for each PropertyIO
      parameterTypes: [ phetioValueType ],

      events: [ 'changed' ],

      getAPI: function() {
        return {
          elementType: phetio.getAPIForType( phetioValueType )
        };
      },

      /**
       * Encodes a Property instance to a state.
       * @param {Object} property
       * @returns {Object} - a state object
       */
      toStateObject: function( property ) {
        assert && assertInstanceOf( property, phet.axon.Property );
        assert && assert( phetioValueType.toStateObject, 'toStateObject doesnt exist for ' + phetioValueType.typeName );
        var stateObject = {
          value: phetioValueType.toStateObject( property.value )
        };

        // Only include validValues if specified, so they only show up in studio when supplied.
        if ( property.validValues ) {
          stateObject.validValues = property.validValues.map( function( v ) {
            return phetioValueType.toStateObject( v );
          } );
        }

        // Only supply units if they were specified, to avoid seeing "units: null" in so many properties, see https://github.com/phetsims/phet-io/issues/1315
        if ( property.units ) {
          stateObject.units = property.units;
        }
        return stateObject;
      },

      /**
       * Decodes a state into a Property.
       * @param {Object} stateObject
       * @returns {Object}
       */
      fromStateObject: function( stateObject ) {
        return {
          units: stateObject.units,
          value: phetioValueType.fromStateObject( stateObject.value ),
          validValues: stateObject.validValues && stateObject.validValues.map( function( v ) {
            return phetioValueType.fromStateObject( v );
          } )
        };
      },

      /**
       * Used to set the value when loading a state
       * @param {Property} property
       * @param {Object} fromStateObject
       */
      setValue: function( property, fromStateObject ) {
        assert && assertInstanceOf( property, phet.axon.Property );
        property.units = fromStateObject.units;
        property.set( fromStateObject.value );
        property.validValues = fromStateObject.validValues;
      },

      options: options
    } );
  }

  axon.register( 'PropertyIO', PropertyIO );

  return PropertyIO;
} );<commit_msg>cleanup unused instance metadata in type signature, https://github.com/phetsims/phet-io/issues/1339<commit_after>// Copyright 2016, University of Colorado Boulder

/**
 * IO type for Property
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Andrew Adare (PhET Interactive Simulations)
 */
define( function( require ) {
  'use strict';

  // modules
  var axon = require( 'AXON/axon' );

  // ifphetio
  var assertInstanceOf = require( 'ifphetio!PHET_IO/assertInstanceOf' );
  var FunctionIO = require( 'ifphetio!PHET_IO/types/FunctionIO' );
  var ObjectIO = require( 'ifphetio!PHET_IO/types/ObjectIO' );
  var phetio = require( 'ifphetio!PHET_IO/phetio' );
  var phetioInherit = require( 'ifphetio!PHET_IO/phetioInherit' );
  var VoidIO = require( 'ifphetio!PHET_IO/types/VoidIO' );

  /**
   * An observable property that triggers notifications when the value changes.
   * @param {function} phetioValueType - If loaded by phet (not phet-io) it will be the function returned by the
   *                                     'ifphetio!' plugin.
   * @module PropertyIO
   * @constructor
   */
  function PropertyIO( phetioValueType ) {

    /**
     * @param {Property} property
     * @param {string} phetioID
     * @constructor
     */
    var PropertyIOImpl = function PropertyIOImpl( property, phetioID ) {
      assert && assert( !!phetioValueType, 'PropertyIO needs phetioValueType' );
      assert && assert( property, 'Property should exist' );
      assert && assert( _.endsWith( phetioID, 'Property' ), 'PropertyIO instances should end with the "Property" suffix, for ' + phetioID );

      assert && assertInstanceOf( property, phet.axon.Property );
      ObjectIO.call( this, property, phetioID );
    };

    return phetioInherit( ObjectIO, 'PropertyIO', PropertyIOImpl, {
      getValue: {
        returnType: phetioValueType,
        parameterTypes: [],
        implementation: function() {
          return this.instance.get();
        },
        documentation: 'Gets the current value.'
      },

      setValue: {
        returnType: VoidIO,
        parameterTypes: [ phetioValueType ],
        implementation: function( value ) {
          this.instance.set( value );
        },
        documentation: 'Sets the value of the property. If the value differs from the previous value, listeners are ' +
                       'notified with the new value.'
      },

      link: {
        returnType: VoidIO,
        parameterTypes: [ FunctionIO( VoidIO, [ phetioValueType, phetioValueType ] ) ],
        implementation: function( listener ) {
          this.instance.link( listener );
        },
        documentation: 'Adds a listener which will be called when the value changes. On registration, the listener is ' +
                       'also called with the current value. The listener takes two arguments, the new value and the ' +
                       'previous value.'
      },

      lazyLink: {
        returnType: VoidIO,
        parameterTypes: [ FunctionIO( VoidIO, [ phetioValueType, phetioValueType ] ) ],
        implementation: function( listener ) {
          this.instance.lazyLink( listener );
        },
        documentation: 'Adds a listener which will be called when the value changes. This method is like "link", but ' +
                       'without the current-value callback on registration. The listener takes two arguments, the new ' +
                       'value and the previous value.'
      },

      unlink: {
        returnType: VoidIO,
        parameterTypes: [ FunctionIO( VoidIO, [ phetioValueType ] ) ],
        implementation: function( listener ) {
          this.instance.unlink( listener );
        },
        documentation: 'Removes a listener.'
      }
    }, {
      documentation: 'Observable values that send out notifications when the value changes. This differs from the ' +
                     'traditional listener pattern in that added listeners also receive a callback with the current value ' +
                     'when the listeners are registered. This is a widely-used pattern in PhET-iO simulations.',
      methodOrder: [ 'link', 'lazyLink' ],
      elementType: phetioValueType,

      // Used to generate the unique parametric typename for each PropertyIO
      parameterTypes: [ phetioValueType ],

      events: [ 'changed' ],

      getAPI: function() {
        return {
          elementType: phetio.getAPIForType( phetioValueType )
        };
      },

      /**
       * Encodes a Property instance to a state.
       * @param {Object} property
       * @returns {Object} - a state object
       */
      toStateObject: function( property ) {
        assert && assertInstanceOf( property, phet.axon.Property );
        assert && assert( phetioValueType.toStateObject, 'toStateObject doesnt exist for ' + phetioValueType.typeName );
        var stateObject = {
          value: phetioValueType.toStateObject( property.value )
        };

        // Only include validValues if specified, so they only show up in studio when supplied.
        if ( property.validValues ) {
          stateObject.validValues = property.validValues.map( function( v ) {
            return phetioValueType.toStateObject( v );
          } );
        }

        // Only supply units if they were specified, to avoid seeing "units: null" in so many properties, see https://github.com/phetsims/phet-io/issues/1315
        if ( property.units ) {
          stateObject.units = property.units;
        }
        return stateObject;
      },

      /**
       * Decodes a state into a Property.
       * @param {Object} stateObject
       * @returns {Object}
       */
      fromStateObject: function( stateObject ) {
        return {
          units: stateObject.units,
          value: phetioValueType.fromStateObject( stateObject.value ),
          validValues: stateObject.validValues && stateObject.validValues.map( function( v ) {
            return phetioValueType.fromStateObject( v );
          } )
        };
      },

      /**
       * Used to set the value when loading a state
       * @param {Property} property
       * @param {Object} fromStateObject
       */
      setValue: function( property, fromStateObject ) {
        assert && assertInstanceOf( property, phet.axon.Property );
        property.units = fromStateObject.units;
        property.set( fromStateObject.value );
        property.validValues = fromStateObject.validValues;
      }
    } );
  }

  axon.register( 'PropertyIO', PropertyIO );

  return PropertyIO;
} );<|endoftext|>


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

/**
 * Reward node that shows many nodes animating continuously, for fun!  Shown when a perfect score is achieved in a game.
 * You can also test this by running vegas/vegas_en.html and clicking on the "Reward" screen.
 * Note that the number of images falling is constant, so if the screen is stretched out vertically (tall thin window)
 * they will be less dense.
 *
 * There are two ways to run the animation step function.  The client code can manually call step(dt), or the client
 * code can pass in an Events instance that triggers events on 'step'. In the latter case, the listener will
 * automatically be removed when the animation is complete.
 *
 * For details about the development of the RewardNode, please see https://github.com/phetsims/vegas/issues/4
 *
 * @author Sam Reid
 */

import Bounds2 from '../../dot/js/Bounds2.js';
import ScreenView from '../../joist/js/ScreenView.js';
import merge from '../../phet-core/js/merge.js';
import FaceNode from '../../scenery-phet/js/FaceNode.js';
import StarNode from '../../scenery-phet/js/StarNode.js';
import CanvasNode from '../../scenery/js/nodes/CanvasNode.js';
import Node from '../../scenery/js/nodes/Node.js';
import Tandem from '../../tandem/js/Tandem.js';
import vegas from './vegas.js';

// constants
const DEBUG = false; // shows a gray rectangle for the CanvasNode to help ensure that its bounds are accurate
const MAX_SPEED = 200; // The maximum speed an image can fall in screen pixels per second.

class RewardNode extends CanvasNode {

  /**
   * @param {Object} [options]
   */
  constructor( options ) {

    // @private
    options = merge( {

      // Scale things up for rasterization and back down for rendering so they have nice resolution on retina
      scaleForResolution: 2,

      // {Node[]} Nodes to appear in the reward node.  They will be cached as images to improve performance.
      // The simulation should override this array to provide images specific to the simulation.
      // If null, the default is initialized below.
      nodes: null,

      // If you pass in a stepEmitter {Emitter}, it will drive the animation
      stepEmitter: null
    }, options );

    if ( options.nodes === null ) {
      options.nodes = RewardNode.createRandomNodes( [
        new FaceNode( 40, { headStroke: 'black', headLineWidth: 1.5 } ),
        new StarNode()
      ], 150 );
    }

    super( options );

    // @private
    this.options = options;

    // Bounds in which to render the canvas, which represents the full window. See below for how this is computed based
    // on ScreenView bounds and relative transforms
    this.canvasDisplayBounds = new Bounds2( 0, 0, 0, 0 );

    // If you pass in a stepEmitter, it will drive the animation
    if ( options.stepEmitter ) {
      this.stepCallback = dt => this.step( dt );
      options.stepEmitter.addListener( this.stepCallback );
    }

    // @private {Node[]}
    // Cache each unique node as an image for faster rendering in canvas.  Use an intermediate imageWrapper since the
    // images will be returned later asynchronously. And we need a place to store them, and know when they have arrived.
    this.imageWrappers = [];

    // find the unique nodes in the array
    const uniqueNodes = _.uniq( this.options.nodes );
    uniqueNodes.forEach( ( node, i ) => {
      this.imageWrappers.push( {
        // The image to be rendered in the canvas, will be filled in by toImage callback
        image: null,

        // Record the width and height so the nodes can be positioned before the toImage call has completed
        width: node.width,
        height: node.height,

        // The node itself is recorded in the imageWrapper so the imageWrapper can be looked up based on the original node
        node: node
      } );

      const parent = new Node( { children: [ node ], scale: options.scaleForResolution } );
      parent.toImage( image => {
        this.imageWrappers[ i ].image = image;
        parent.dispose(); // not needed anymore, see https://github.com/phetsims/area-model-common/issues/128
      } );
    } );

    // @private {boolean} Some initialization must occur after this node is attached to the scene graph,
    // see documentation for initialize() method below.
    this.isInitialized = false;

    // @private these will be set by init
    this.scene = null; // {Node}
    this.screenView = null; // {ScreenView}
    this.updateBounds = null; // {function}

    // @private {function} For PhET-iO brand only: make sure this Node is initialized when state is being set for PhET-iO
    this.initializationVerifier = null;
    if ( Tandem.PHET_IO_ENABLED && phet.phetio.phetioEngine.phetioStateEngine ) {
      this.initializationVerifier = () => {
        if ( !this.isInitialized ) {
          this.initialize();
        }
      };
      Tandem.PHET_IO_ENABLED && phet.phetio.phetioEngine.phetioStateEngine.stateSetEmitter.addListener( this.initializationVerifier );
    }
  }

  /**
   * Convenience factory method to create an array of the specified nodes in an even distribution.
   * @param {Node[]} nodes array of nodes to use
   * @param {number} count
   * @returns {Node[]}
   * @public
   */
  static createRandomNodes( nodes, count ) {
    const array = [];
    for ( let i = 0; i < count; i++ ) {
      array.push( nodes[ i % nodes.length ] );
    }
    return array;
  }

  /**
   * Paint the rewards on the canvas
   * @param {CanvasRenderingContext2D} context
   * @protected
   */
  paintCanvas( context ) {

    // If the debugging flag is on, show the bounds of the canvas
    if ( DEBUG ) {
      const bounds = this.canvasDisplayBounds;

      // Fill the canvas with gray
      context.fillStyle = 'rgba(50,50,50,0.5)';
      context.fillRect( bounds.minX, bounds.minY, bounds.width, bounds.height );

      // Stroke the canvas border with blue
      context.strokeStyle = '#0000ff';
      context.lineWidth = 5;
      context.strokeRect( bounds.minX, bounds.minY, bounds.width, bounds.height );
    }
    context.scale( 1 / this.options.scaleForResolution, 1 / this.options.scaleForResolution );

    // Display the rewards, but check that they exist first.  They do not exist when attached to the timer with stepEmitter
    if ( this.rewards ) {
      for ( let i = 0; i < this.rewards.length; i++ ) {
        const reward = this.rewards[ i ];
        if ( reward.imageWrapper.image ) {
          context.drawImage( reward.imageWrapper.image, reward.x, reward.y );
        }
      }
    }
  }

  /**
   * Find the root of the scenegraph.
   * @returns {Node}
   * @private
   */
  getScene() {
    return this.getUniqueTrail().nodes[ 0 ];
  }

  /**
   * Finds the first parent that is a ScreenView so we can listen for its transform, see https://github.com/phetsims/vegas/issues/4
   * @returns {Node}
   * @private
   */
  getScreenView() {
    return this.getUniqueTrail( function( node ) { return node instanceof ScreenView; } ).rootNode();
  }

  /**
   * Only init after being attached to the scene graph, since we must ascertain the local bounds are such that they take
   * up the global screen.
   * 1. listen to the size of the scene/display
   * 2. record the trail between the scene and your CanvasNode, and
   * 3. apply the inverse of that transform to the CanvasNode (whenever an ancestor's transform changes, or when the scene/display size changes).
   *
   * @jonathanolson said: for implementing now, I'd watch the iso transform, compute the inverse, and set bounds on
   * changes to be precise (since you need them anyways to draw).
   * @public
   */
  initialize() {
    assert && assert( !this.isInitialized, 'already initialized' );

    this.scene = this.getScene();
    this.screenView = this.getScreenView();

    // Listen to the bounds of the scene, so the canvas can be resized if the window is reshaped
    this.updateBounds = () => {

      const local = this.globalToLocalBounds( phet.joist.sim.display.bounds );
      this.setCanvasBounds( local );

      // Also, store the bounds in the options so the debug flag can render the bounds
      this.canvasDisplayBounds = local;
    };

    // When the ScreenView transform changes, update the bounds.  This prevents a "behind by one" problem, see https://github.com/phetsims/vegas/issues/4
    this.screenView.transformEmitter.addListener( this.updateBounds );

    // Set the initial bounds
    this.updateBounds();

    /*
     * Store each reward, which has an imageWrapper (see above), x, y, speed. It is not an image, it is not a node,
     * but it is one of the things that animates as falling in the RewardNode and its associated data.
     * @private
     */
    // TODO should we create a separate class for this?  See https://github.com/phetsims/vegas/issues/79.
    this.rewards = [];
    const self = this;
    for ( let i = 0; i < this.options.nodes.length; i++ ) {

      const node = this.options.nodes[ i ];

      // IIFE
      ( node => {

        //find the image wrapper corresponding to the node
        const imageWrapper = _.find( self.imageWrappers, function( imageWrapper ) {return imageWrapper.node === node;} );
        self.rewards.push( {
          imageWrapper: imageWrapper,
          x: self.sampleImageXValue( imageWrapper ),
          y: self.sampleImageYValue( imageWrapper ),
          speed: ( phet.joist.random.nextDouble() + 1 ) * MAX_SPEED
        } );
      } )( node );
    }

    this.isInitialized = true;
  }

  /**
   * Stops the animation.  If there is a stepEmitter, remove its listener
   * @private
   */
  stop() {
    this.options.stepEmitter && this.options.stepEmitter.removeListener( this.stepCallback );
  }

  /**
   * Selects a random X value for the image when it is created.
   * @param {Node} imageWrapper
   * @returns {number}
   * @private
   */
  sampleImageXValue( imageWrapper ) {
    return ( phet.joist.random.nextDouble() * this.canvasDisplayBounds.width + this.canvasDisplayBounds.left ) *
           this.options.scaleForResolution - imageWrapper.width / 2;
  }

  /**
   * Selects a random Y value for the image when it is created, or when it goes back to the top of the screen
   * @param {Node} imageWrapper
   * @returns {number}
   * @public
   */
  sampleImageYValue( imageWrapper ) {
    // Start things about 1 second off the top of the screen
    return this.canvasDisplayBounds.top - phet.joist.random.nextDouble() * this.canvasDisplayBounds.height * 2 -
           MAX_SPEED - imageWrapper.height;
  }

  /**
   * Moves the rewards down according to their speed.
   * @param {number} dt
   * @public
   */
  step( dt ) {
    if ( !this.isInitialized && this.getScene() !== null ) {
      this.initialize();
    }

    // Update all of the rewards
    const maxY = this.canvasDisplayBounds.height * this.options.scaleForResolution;
    for ( let i = 0; i < this.rewards.length; i++ ) {
      const reward = this.rewards[ i ];

      // Move each node straight down at constant speed
      reward.y += reward.speed * dt;

      // Move back to the top after the node falls off the bottom
      if ( reward.y > maxY ) {
        reward.x = this.sampleImageXValue( reward.imageWrapper );
        reward.y = this.sampleImageYValue( reward.imageWrapper );
      }
    }
    this.invalidatePaint();
  }

  /**
   * @public
   * @override
   */
  dispose() {
    this.stop();
    this.screenView && this.screenView.transformEmitter.removeListener( this.updateBounds );
    this.initializationVerifier && phet.phetio.phetioEngine.phetioStateEngine.stateSetEmitter.removeListener( this.initializationVerifier );
    super.dispose();
  }
}

vegas.register( 'RewardNode', RewardNode );
export default RewardNode;<commit_msg>clean up RewardNode dispose, https://github.com/phetsims/vegas/issues/82<commit_after>// Copyright 2014-2020, University of Colorado Boulder

/**
 * Reward node that shows many nodes animating continuously, for fun!  Shown when a perfect score is achieved in a game.
 * You can also test this by running vegas/vegas_en.html and clicking on the "Reward" screen.
 * Note that the number of images falling is constant, so if the screen is stretched out vertically (tall thin window)
 * they will be less dense.
 *
 * There are two ways to run the animation step function.  The client code can manually call step(dt), or the client
 * code can pass in an Events instance that triggers events on 'step'. In the latter case, the listener will
 * automatically be removed when the animation is complete.
 *
 * For details about the development of the RewardNode, please see https://github.com/phetsims/vegas/issues/4
 *
 * @author Sam Reid
 */

import Bounds2 from '../../dot/js/Bounds2.js';
import ScreenView from '../../joist/js/ScreenView.js';
import merge from '../../phet-core/js/merge.js';
import FaceNode from '../../scenery-phet/js/FaceNode.js';
import StarNode from '../../scenery-phet/js/StarNode.js';
import CanvasNode from '../../scenery/js/nodes/CanvasNode.js';
import Node from '../../scenery/js/nodes/Node.js';
import Tandem from '../../tandem/js/Tandem.js';
import vegas from './vegas.js';

// constants
const DEBUG = false; // shows a gray rectangle for the CanvasNode to help ensure that its bounds are accurate
const MAX_SPEED = 200; // The maximum speed an image can fall in screen pixels per second.

class RewardNode extends CanvasNode {

  /**
   * @param {Object} [options]
   */
  constructor( options ) {

    // @private
    options = merge( {

      // Scale things up for rasterization and back down for rendering so they have nice resolution on retina
      scaleForResolution: 2,

      // {Node[]} Nodes to appear in the reward node.  They will be cached as images to improve performance.
      // The simulation should override this array to provide images specific to the simulation.
      // If null, the default is initialized below.
      nodes: null,

      // If you pass in a stepEmitter {Emitter}, it will drive the animation
      stepEmitter: null
    }, options );

    if ( options.nodes === null ) {
      options.nodes = RewardNode.createRandomNodes( [
        new FaceNode( 40, { headStroke: 'black', headLineWidth: 1.5 } ),
        new StarNode()
      ], 150 );
    }

    super( options );

    // @private
    this.options = options;

    // Bounds in which to render the canvas, which represents the full window. See below for how this is computed based
    // on ScreenView bounds and relative transforms
    this.canvasDisplayBounds = new Bounds2( 0, 0, 0, 0 );

    // @private If you pass in a stepEmitter, it will drive the animation
    this.stepCallback = null;
    if ( options.stepEmitter ) {
      this.stepCallback = dt => this.step( dt );
      options.stepEmitter.addListener( this.stepCallback );
    }

    // @private {Node[]}
    // Cache each unique node as an image for faster rendering in canvas.  Use an intermediate imageWrapper since the
    // images will be returned later asynchronously. And we need a place to store them, and know when they have arrived.
    this.imageWrappers = [];

    // find the unique nodes in the array
    const uniqueNodes = _.uniq( options.nodes );
    uniqueNodes.forEach( ( node, i ) => {
      this.imageWrappers.push( {
        // The image to be rendered in the canvas, will be filled in by toImage callback
        image: null,

        // Record the width and height so the nodes can be positioned before the toImage call has completed
        width: node.width,
        height: node.height,

        // The node itself is recorded in the imageWrapper so the imageWrapper can be looked up based on the original node
        node: node
      } );

      const parent = new Node( { children: [ node ], scale: options.scaleForResolution } );
      parent.toImage( image => {
        this.imageWrappers[ i ].image = image;
        parent.dispose(); // not needed anymore, see https://github.com/phetsims/area-model-common/issues/128
      } );
    } );

    // @private {boolean} Some initialization must occur after this node is attached to the scene graph,
    // see documentation for initialize() method below.
    this.isInitialized = false;

    // @private these will be set by init
    this.scene = null; // {Node}
    this.screenView = null; // {ScreenView}
    this.updateBounds = null; // {function}

    // @private {function} For PhET-iO brand only: make sure this Node is initialized when state is being set for PhET-iO
    this.initializationVerifier = null;
    if ( Tandem.PHET_IO_ENABLED && phet.phetio.phetioEngine.phetioStateEngine ) {
      this.initializationVerifier = () => {
        if ( !this.isInitialized ) {
          this.initialize();
        }
      };
      Tandem.PHET_IO_ENABLED && phet.phetio.phetioEngine.phetioStateEngine.stateSetEmitter.addListener( this.initializationVerifier );
    }

    // @private
    this.disposeRewardNode = () => {
      options.stepEmitter && options.stepEmitter.removeListener( this.stepCallback );
      this.screenView && this.screenView.transformEmitter.removeListener( this.updateBounds );
      this.initializationVerifier && phet.phetio.phetioEngine.phetioStateEngine.stateSetEmitter.removeListener( this.initializationVerifier );
    };
  }

  /**
   * @public
   * @override
   */
  dispose() {
    this.disposeRewardNode();
    super.dispose();
  }

  /**
   * Convenience factory method to create an array of the specified nodes in an even distribution.
   * @param {Node[]} nodes array of nodes to use
   * @param {number} count
   * @returns {Node[]}
   * @public
   */
  static createRandomNodes( nodes, count ) {
    const array = [];
    for ( let i = 0; i < count; i++ ) {
      array.push( nodes[ i % nodes.length ] );
    }
    return array;
  }

  /**
   * Paint the rewards on the canvas
   * @param {CanvasRenderingContext2D} context
   * @protected
   */
  paintCanvas( context ) {

    // If the debugging flag is on, show the bounds of the canvas
    if ( DEBUG ) {
      const bounds = this.canvasDisplayBounds;

      // Fill the canvas with gray
      context.fillStyle = 'rgba(50,50,50,0.5)';
      context.fillRect( bounds.minX, bounds.minY, bounds.width, bounds.height );

      // Stroke the canvas border with blue
      context.strokeStyle = '#0000ff';
      context.lineWidth = 5;
      context.strokeRect( bounds.minX, bounds.minY, bounds.width, bounds.height );
    }
    context.scale( 1 / this.options.scaleForResolution, 1 / this.options.scaleForResolution );

    // Display the rewards, but check that they exist first.  They do not exist when attached to the timer with stepEmitter
    if ( this.rewards ) {
      for ( let i = 0; i < this.rewards.length; i++ ) {
        const reward = this.rewards[ i ];
        if ( reward.imageWrapper.image ) {
          context.drawImage( reward.imageWrapper.image, reward.x, reward.y );
        }
      }
    }
  }

  /**
   * Find the root of the scenegraph.
   * @returns {Node}
   * @private
   */
  getScene() {
    return this.getUniqueTrail().nodes[ 0 ];
  }

  /**
   * Finds the first parent that is a ScreenView so we can listen for its transform, see https://github.com/phetsims/vegas/issues/4
   * @returns {Node}
   * @private
   */
  getScreenView() {
    return this.getUniqueTrail( function( node ) { return node instanceof ScreenView; } ).rootNode();
  }

  /**
   * Only init after being attached to the scene graph, since we must ascertain the local bounds are such that they take
   * up the global screen.
   * 1. listen to the size of the scene/display
   * 2. record the trail between the scene and your CanvasNode, and
   * 3. apply the inverse of that transform to the CanvasNode (whenever an ancestor's transform changes, or when the scene/display size changes).
   *
   * @jonathanolson said: for implementing now, I'd watch the iso transform, compute the inverse, and set bounds on
   * changes to be precise (since you need them anyways to draw).
   * @public
   */
  initialize() {
    assert && assert( !this.isInitialized, 'already initialized' );

    this.scene = this.getScene();
    this.screenView = this.getScreenView();

    // Listen to the bounds of the scene, so the canvas can be resized if the window is reshaped
    this.updateBounds = () => {

      const local = this.globalToLocalBounds( phet.joist.sim.display.bounds );
      this.setCanvasBounds( local );

      // Also, store the bounds in the options so the debug flag can render the bounds
      this.canvasDisplayBounds = local;
    };

    // When the ScreenView transform changes, update the bounds.  This prevents a "behind by one" problem, see https://github.com/phetsims/vegas/issues/4
    this.screenView.transformEmitter.addListener( this.updateBounds );

    // Set the initial bounds
    this.updateBounds();

    /*
     * Store each reward, which has an imageWrapper (see above), x, y, speed. It is not an image, it is not a node,
     * but it is one of the things that animates as falling in the RewardNode and its associated data.
     * @private
     */
    // TODO should we create a separate class for this?  See https://github.com/phetsims/vegas/issues/79.
    this.rewards = [];
    const self = this;
    for ( let i = 0; i < this.options.nodes.length; i++ ) {

      const node = this.options.nodes[ i ];

      // IIFE
      ( node => {

        //find the image wrapper corresponding to the node
        const imageWrapper = _.find( self.imageWrappers, function( imageWrapper ) {return imageWrapper.node === node;} );
        self.rewards.push( {
          imageWrapper: imageWrapper,
          x: self.sampleImageXValue( imageWrapper ),
          y: self.sampleImageYValue( imageWrapper ),
          speed: ( phet.joist.random.nextDouble() + 1 ) * MAX_SPEED
        } );
      } )( node );
    }

    this.isInitialized = true;
  }

  /**
   * Selects a random X value for the image when it is created.
   * @param {Node} imageWrapper
   * @returns {number}
   * @private
   */
  sampleImageXValue( imageWrapper ) {
    return ( phet.joist.random.nextDouble() * this.canvasDisplayBounds.width + this.canvasDisplayBounds.left ) *
           this.options.scaleForResolution - imageWrapper.width / 2;
  }

  /**
   * Selects a random Y value for the image when it is created, or when it goes back to the top of the screen
   * @param {Node} imageWrapper
   * @returns {number}
   * @public
   */
  sampleImageYValue( imageWrapper ) {
    // Start things about 1 second off the top of the screen
    return this.canvasDisplayBounds.top - phet.joist.random.nextDouble() * this.canvasDisplayBounds.height * 2 -
           MAX_SPEED - imageWrapper.height;
  }

  /**
   * Moves the rewards down according to their speed.
   * @param {number} dt
   * @public
   */
  step( dt ) {
    if ( !this.isInitialized && this.getScene() !== null ) {
      this.initialize();
    }

    // Update all of the rewards
    const maxY = this.canvasDisplayBounds.height * this.options.scaleForResolution;
    for ( let i = 0; i < this.rewards.length; i++ ) {
      const reward = this.rewards[ i ];

      // Move each node straight down at constant speed
      reward.y += reward.speed * dt;

      // Move back to the top after the node falls off the bottom
      if ( reward.y > maxY ) {
        reward.x = this.sampleImageXValue( reward.imageWrapper );
        reward.y = this.sampleImageYValue( reward.imageWrapper );
      }
    }
    this.invalidatePaint();
  }
}

vegas.register( 'RewardNode', RewardNode );
export default RewardNode;<|endoftext|>