<commit_before>/*
 * This file contains all of the Passport configuration that will
 * be used through the application for Facebook authentication.
 */

var FacebookStrategy = require('passport-facebook').Strategy;
var keys = require('./keys');
var model = require('../models/index');

module.exports = function(app, passport) {

  app.use(passport.initialize());
  app.use(passport.session());

  // Serializes the user for the session based on their ID
  passport.serializeUser(function(user, done) {
    done(null, user.id);
  });

  // Deserializes the user based on their ID
  passport.deserializeUser(function(id, done) {
    model.User.find({ where: { id: id }})
    .then(function(user) {
      done(null, user);
    })
    .catch(function(err) {
      done(err, null);
    });
  });

  // Setup for the Facebook authentication strategy
  passport.use(new FacebookStrategy({
      clientID        : keys.facebook.APP_ID,
      clientSecret    : keys.facebook.SECRET,
      callbackURL     : keys.facebook.CALLBACK
    },

    function(accessToken, refreshToken, profile, done) {

      // Performs verification asynchronously
      process.nextTick(function() {

        // Queries the database to see if the user is already stored there
        model.User.find({ where: { facebookId: profile.id }}, {})

        .then(function(user) {
          // If the user is not stored in the database, then store the user there
          if (user === null) {
            model.User.create({ facebookId: profile.id, v2AccessToken: accessToken })
            .then(function(user) {
              done(null, user);
            })
            .catch(function(err) {
              done(err, null);
            });
          }

          else {
            // If the user has a new access token, then update the token in the database
            if (user.v2AccessToken !== accessToken) {
              user.updateAttributes({ v2AccessToken: accessToken })
              .then(function() {
                done(null, user);
              });
            }
            else {
              done(null, user);
            }
          }
        })

        .catch(function(err) {
          done(err, null)
        });

      });
    }
  ));
}<commit_msg>Fix error in deserializeUser function<commit_after>/*
 * This file contains all of the Passport configuration that will
 * be used through the application for Facebook authentication.
 */

var FacebookStrategy = require('passport-facebook').Strategy;
var keys = require('./keys');
var model = require('../models/index');

module.exports = function(app, passport) {

  app.use(passport.initialize());
  app.use(passport.session());

  // Serializes the user for the session based on their ID
  passport.serializeUser(function(user, done) {
    done(null, user.id);
  });

  // Deserializes the user based on their ID
  passport.deserializeUser(function(id, done) {
    model.User.find({ where: { id: id }})
    .then(function(user) {
      done(null, user.dataValues);
    })
    .catch(function(err) {
      done(err, null);
    });
  });

  // Setup for the Facebook authentication strategy
  passport.use(new FacebookStrategy({
      clientID        : keys.facebook.APP_ID,
      clientSecret    : keys.facebook.SECRET,
      callbackURL     : keys.facebook.CALLBACK
    },

    function(accessToken, refreshToken, profile, done) {

      // Performs verification asynchronously
      process.nextTick(function() {

        // Queries the database to see if the user is already stored there
        model.User.find({ where: { facebookId: profile.id }}, {})

        .then(function(user) {
          // If the user is not stored in the database, then store the user there
          if (user === null) {
            model.User.create({ facebookId: profile.id, v2AccessToken: accessToken })
            .then(function(user) {
              done(null, user);
            })
            .catch(function(err) {
              done(err, null);
            });
          }

          else {
            // If the user has a new access token, then update the token in the database
            if (user.v2AccessToken !== accessToken) {
              user.updateAttributes({ v2AccessToken: accessToken })
              .then(function() {
                done(null, user);
              });
            }
            else {
              done(null, user);
            }
          }
        })

        .catch(function(err) {
          done(err, null);
        });

      });
    }
  ));
};<|endoftext|>


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

/**
 * Compare PhET-iO APIs for two versions of the same sim. This function treats the first API as the "ground truth"
 * and compares the second API to see if it has any breaking changes against the first API. This function returns a
 * list of "problems".
 *
 * This file runs in node (command line API comparison), in the diff wrapper (client-facing API comparison) and
 * in simulations in phetioEngine when ?ea&phetioCompareAPI is specified (for CT).
 *
 * Note that even though it is a preload, it uses a different global/namespacing pattern than phet-io-initialize-globals.js
 * in order to simplify usage in all these sites.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */

/**
 * @typedef API
 * @property {boolean} phetioFullAPI
 * @property {Object} phetioElements - phetioElements for version >=1.0 this will be a sparse, tree like structure with
 *                    metadata in key: `_metadata`. For version<1 this will be a flat list with phetioIDs as keys,
 *                    and values as metadata.
 * @property {Object} phetioTypes
 */

/**
 * See phetioEngine.js for where this is generated in master. Keep in mind that we support different versions, including
 * APIs that don't have a version attribute.
 * @typedef API_1_0
 * @extends API
 * @property {{major:number, minor:number}} version
 * @property {string} sim
 */
( () => {

  const METADATA_KEY_NAME = '_metadata';
  const DATA_KEY_NAME = '_data';

  // Is not the reserved keys to store data/metadata on PhET-iO elements.
  const isChildKey = key => key !== METADATA_KEY_NAME && key !== DATA_KEY_NAME;

  /**
   * "up-convert" an API to be in the format of API version >=1.0. This generally is thought of as a "sparse, tree-like" API.
   * @param {API} api
   * @param _
   * @returns {API} - In this version, phetioElements will be structured as a tree, but will have a verbose and complete
   *                  set of all metadata keys for each element. There will not be `metadataDefaults` in each type.
   */
  const toStructuredTree = ( api, _ ) => {
    const sparseAPI = _.cloneDeep( api );

    // DUPLICATED with phetioEngine.js
    const sparseElements = {};
    Object.keys( api.phetioElements ).forEach( phetioID => {
      const entry = api.phetioElements[ phetioID ];

      // API versions < 1.0, use a tandem separator of '.'  If we ever change this separator in master (hopefully not!)
      // this value wouldn't change since it reflects the prior committed versions which do use '.'
      const chain = phetioID.split( '.' );

      // Fill in each level
      let level = sparseElements;
      chain.forEach( componentName => {
        level[ componentName ] = level[ componentName ] || {};
        level = level[ componentName ];
      } );

      level[ METADATA_KEY_NAME ] = {};

      Object.keys( entry ).forEach( key => {

          // write all values without trying to factor out defaults
          level[ METADATA_KEY_NAME ][ key ] = entry[ key ];
        }
      );
    } );

    sparseAPI.phetioElements = sparseElements;
    return sparseAPI;
  };

  /**
   * @param {Object} phetioElement
   * @param {API} api
   * @param {Object} _ - lodash
   * @param {function|undefined} assert - optional assert
   * @returns {Object}
   */
  const getMetadataValues = ( phetioElement, api, _, assert ) => {
    const typeName = phetioElement[ METADATA_KEY_NAME ] ? ( phetioElement[ METADATA_KEY_NAME ].phetioTypeName || 'ObjectIO' ) : 'ObjectIO';

    if ( api.version ) {
      const defaults = getMetadataDefaults( typeName, api, _, assert );
      return _.merge( defaults, phetioElement[ METADATA_KEY_NAME ] );
    }
    else {

      // Dense version supplies all metadata values
      return phetioElement[ METADATA_KEY_NAME ];
    }
  };

  /**
   * @param {string} typeName
   * @param {API} api
   * @param {Object} _ - lodash
   * @param {function|undefined} assert - optional assert
   * @returns {Object} - defensive copy, non-mutating
   */
  const getMetadataDefaults = ( typeName, api, _, assert ) => {
    const entry = api.phetioTypes[ typeName ];
    assert && assert( entry, `entry missing: ${typeName}` );
    if ( entry.supertype ) {
      return _.merge( getMetadataDefaults( entry.supertype, api, _ ), entry.metadataDefaults );
    }
    else {
      return _.merge( {}, entry.metadataDefaults );
    }
  };

  /**
   * @param {API} api
   * @returns {boolean} - whether or not the API is of type API_1_0
   */
  const isOldAPIVersion = api => {
    return !api.hasOwnProperty( 'version' );
  };

  /**
   * Compare two APIs for breaking or design changes.
   *
   * NOTE: Named with an underscore to avoid automatically defining `window.phetioCompareAPIs` as a global
   *
   * @param {API} referenceAPI - the "ground truth" or reference API
   * @param {API} proposedAPI - the proposed API for comparison with referenceAPI
   * @param _ - lodash, so this can be used from different contexts.
   * @param {function|undefined} assert - so this can be used from different contexts
   * @param {Object} [options]
   * @returns {{breakingProblems:string[], designedProblems:string[]}}
   */
  const _phetioCompareAPIs = ( referenceAPI, proposedAPI, _, assert, options ) => {

    // If the proposed version predates 1.0, then bring it forward to the structured tree with metadata under `_metadata`.
    if ( isOldAPIVersion( proposedAPI ) ) {
      proposedAPI = toStructuredTree( proposedAPI, _ );
    }

    if ( isOldAPIVersion( referenceAPI ) ) {
      referenceAPI = toStructuredTree( referenceAPI, _ );
    }

    options = _.merge( {
      compareDesignedAPIChanges: true,
      compareBreakingAPIChanges: true
    }, options );

    const breakingProblems = [];
    const designedProblems = [];

    const appendProblem = ( problemString, isDesignedProblem = false ) => {
      if ( isDesignedProblem && options.compareDesignedAPIChanges ) {
        designedProblems.push( problemString );
      }
      else if ( !isDesignedProblem && options.compareBreakingAPIChanges ) {
        breakingProblems.push( problemString );
      }
    };

    /**
     * Visit one element along the APIs.
     * @param {string[]} trail - the path of tandem componentNames
     * @param {Object} reference - current value in the referenceAPI
     * @param {Object} proposed - current value in the proposedAPI
     * @param {boolean} isDesigned
     */
    const visit = ( trail, reference, proposed, isDesigned ) => {
      const phetioID = trail.join( '.' );

      // Detect an instrumented instance
      if ( reference.hasOwnProperty( METADATA_KEY_NAME ) ) {

        // Override isDesigned, if specified
        isDesigned = isDesigned || reference[ METADATA_KEY_NAME ].phetioDesigned;

        const referenceCompleteMetadata = getMetadataValues( reference, referenceAPI, _, assert );
        const proposedCompleteMetadata = getMetadataValues( proposed, proposedAPI, _, assert );

        /**
         * Push any problems that may exist for the provided metadataKey.
         * @param {string} metadataKey - See PhetioObject.getMetadata()
         * @param {boolean} isDesignedChange - if the difference is from a design change, and not from a breaking change test
         * @param {*} [invalidProposedValue] - an optional new value that would signify a breaking change. Any other value would be acceptable.
         */
        const reportDifferences = ( metadataKey, isDesignedChange, invalidProposedValue ) => {
          const referenceValue = referenceCompleteMetadata[ metadataKey ];

          // Gracefully handle missing metadata from the <1.0 API format
          const proposedValue = proposedCompleteMetadata ? proposedCompleteMetadata[ metadataKey ] : {};

          if ( referenceValue !== proposedValue ) {

            // if proposed API is older (no version specified), ignore phetioArchetypePhetioID changed from null to undefined
            // because it used to be sparse, and in version 1.0 it became a default.
            const ignoreBrokenProposed = isOldAPIVersion( proposedAPI ) &&
                                         metadataKey === 'phetioArchetypePhetioID' &&
                                         referenceValue === null &&
                                         proposedValue === undefined;

            const ignoreBrokenReference = isOldAPIVersion( referenceAPI ) &&
                                          metadataKey === 'phetioArchetypePhetioID' &&
                                          proposedValue === null &&
                                          referenceValue === undefined;

            const ignore = ignoreBrokenProposed || ignoreBrokenReference;

            if ( !ignore ) {

              if ( invalidProposedValue === undefined || isDesignedChange ) {
                appendProblem( `${phetioID}.${metadataKey} changed from ${referenceValue} to ${proposedValue}`, isDesignedChange );
              }
              else if ( !isDesignedChange ) {
                if ( proposedValue === invalidProposedValue ) {
                  appendProblem( `${phetioID}.${metadataKey} changed from ${referenceValue} to ${proposedValue}` );
                }
                else {

                  // value changed, but it was a widening API (adding something to state, or making something read/write)
                }
              }
            }
          }
        };

        // Check for breaking changes
        reportDifferences( 'phetioTypeName', false );
        reportDifferences( 'phetioEventType', false );
        reportDifferences( 'phetioPlayback', false );
        reportDifferences( 'phetioDynamicElement', false );
        reportDifferences( 'phetioIsArchetype', false );
        reportDifferences( 'phetioArchetypePhetioID', false );
        reportDifferences( 'phetioState', false, false ); // Only report if something became non-stateful
        reportDifferences( 'phetioReadOnly', false, true ); // Only need to report if something became readOnly

        // The following metadata keys are non-breaking:
        // 'phetioDocumentation'
        // 'phetioFeatured'
        // 'phetioStudioControl'
        // 'phetioHighFrequency', non-breaking, assuming clients with data have the full data stream

        // Check for design changes
        if ( isDesigned ) {
          Object.keys( referenceCompleteMetadata ).forEach( metadataKey => {
            reportDifferences( metadataKey, true );
          } );
        }

        // If the reference file declares an initial state, check that it hasn't changed
        if ( reference._data && reference._data.initialState ) {

          // Detect missing expected state
          if ( !proposed._data || !proposed._data.initialState ) {
            const problemString = `${phetioID}._data.initialState is missing`;

            // Missing but expected state is a breaking problem
            appendProblem( problemString, false );

            // It is also a designed problem if we expected state in a designed subtree
            isDesigned && appendProblem( problemString, true );
          }
          else {

            const equals = _.isEqual( reference._data.initialState, proposed._data.initialState );
            if ( !equals ) {
              const problemString = `${phetioID}._data.initialState differs. Expected: ${JSON.stringify( reference._data.initialState )}, actual: ${JSON.stringify( proposed._data.initialState )}`;

              // A changed state value could break a client wrapper, so identify it with breaking changes.
              appendProblem( problemString, false );

              // It is also a designed problem if the proposed values deviate from the specified designed values
              isDesigned && appendProblem( problemString, true );
            }
          }
        }
      }

      // Recurse to children
      for ( const componentName in reference ) {
        if ( reference.hasOwnProperty( componentName ) && isChildKey( componentName ) ) {

          if ( !proposed.hasOwnProperty( componentName ) ) {
            const problemString = `PhET-iO Element missing: ${phetioID}.${componentName}`;
            appendProblem( problemString, false );

            if ( isDesigned ) {
              appendProblem( problemString, true );
            }
          }
          else {
            visit(
              trail.concat( componentName ),
              reference[ componentName ],
              proposed[ componentName ],
              isDesigned
            );
          }
        }
      }

      for ( const componentName in proposed ) {
        if ( isDesigned && proposed.hasOwnProperty( componentName ) && isChildKey( componentName ) && !reference.hasOwnProperty( componentName ) ) {
          appendProblem( `New PhET-iO Element not in reference: ${phetioID}.${componentName}`, true );
        }
      }
    };

    visit( [], referenceAPI.phetioElements, proposedAPI.phetioElements, false );

    // Check for: missing IO Types, missing methods, or differing parameter types or return types
    for ( const typeName in referenceAPI.phetioTypes ) {
      if ( referenceAPI.phetioTypes.hasOwnProperty( typeName ) ) {

        // make sure we have the desired type
        if ( !proposedAPI.phetioTypes.hasOwnProperty( typeName ) ) {
          appendProblem( `Type missing: ${typeName}` );
        }
        else {
          const referenceType = referenceAPI.phetioTypes[ typeName ];
          const proposedType = proposedAPI.phetioTypes[ typeName ];

          // make sure we have all of the methods
          const referenceMethods = referenceType.methods;
          const proposedMethods = proposedType.methods;
          for ( const referenceMethod in referenceMethods ) {
            if ( referenceMethods.hasOwnProperty( referenceMethod ) ) {
              if ( !proposedMethods.hasOwnProperty( referenceMethod ) ) {
                appendProblem( `Method missing, type=${typeName}, method=${referenceMethod}` );
              }
              else {

                // check parameter types (exact match)
                const referenceParams = referenceMethods[ referenceMethod ].parameterTypes;
                const proposedParams = proposedMethods[ referenceMethod ].parameterTypes;

                if ( referenceParams.join( ',' ) !== proposedParams.join( ',' ) ) {
                  appendProblem( `${typeName}.${referenceMethod} has different parameter types: [${referenceParams.join( ', ' )}] => [${proposedParams.join( ', ' )}]` );
                }

                const referenceReturnType = referenceMethods[ referenceMethod ].returnType;
                const proposedReturnType = proposedMethods[ referenceMethod ].returnType;
                if ( referenceReturnType !== proposedReturnType ) {
                  appendProblem( `${typeName}.${referenceMethod} has a different return type ${referenceReturnType} => ${proposedReturnType}` );
                }
              }
            }
          }

          // make sure we have all of the events (OK to add more)
          const referenceEvents = referenceType.events;
          const proposedEvents = proposedType.events;
          referenceEvents.forEach( event => {
            if ( !proposedEvents.includes( event ) ) {
              appendProblem( `${typeName} is missing event: ${event}` );
            }
          } );

          // make sure we have matching supertype names
          const referenceSupertypeName = referenceType.supertype;
          const proposedSupertypeName = proposedType.supertype;
          if ( referenceSupertypeName !== proposedSupertypeName ) {
            appendProblem( `${typeName} supertype changed from ${referenceSupertypeName} to ${proposedSupertypeName}. This may or may not
          be a breaking change, but we are reporting it just in case.` );
          }

          // make sure we have matching parameter types
          const referenceParameterTypes = referenceType.parameterTypes || [];
          const proposedParameterTypes = proposedType.parameterTypes;
          if ( !_.isEqual( referenceParameterTypes, proposedParameterTypes ) ) {
            appendProblem( `${typeName} parameter types changed from [${referenceParameterTypes.join( ', ' )}] to [${proposedParameterTypes.join( ', ' )}]. This may or may not
          be a breaking change, but we are reporting it just in case.` );
          }

          // This check assumes that each API will be of a version that has metadataDefaults
          if ( referenceAPI.version && proposedAPI.version ) {

            // Check whether the default values have changed. See https://github.com/phetsims/phet-io/issues/1753
            const referenceDefaults = referenceAPI.phetioTypes[ typeName ].metadataDefaults;
            const proposedDefaults = proposedAPI.phetioTypes[ typeName ].metadataDefaults;

            Object.keys( referenceDefaults ).forEach( key => {
              if ( referenceDefaults[ key ] !== proposedDefaults[ key ] ) {
                appendProblem( `${typeName} metadata value ${key} changed from ${referenceDefaults[ key ]} to ${proposedDefaults[ key ]}. This may or may not be a breaking change, but we are reporting it just in case.` );
              }
            } );
          }
        }
      }
    }

    return {
      breakingProblems: breakingProblems,
      designedProblems: designedProblems
    };
  };

// @public - used to "up-convert" an old versioned API to the new (version >=1), structured tree API.
  _phetioCompareAPIs.toStructuredTree = toStructuredTree;

  if ( typeof window === 'undefined' ) {

    // running in node
    module.exports = _phetioCompareAPIs;
  }
  else {

    window.phetio = window.phetio || {};
    window.phetio.phetioCompareAPIs = _phetioCompareAPIs;
  }
} )();
<commit_msg>add newlines to problem string, https://github.com/phetsims/natural-selection/issues/301<commit_after>// Copyright 2021, University of Colorado Boulder

/**
 * Compare PhET-iO APIs for two versions of the same sim. This function treats the first API as the "ground truth"
 * and compares the second API to see if it has any breaking changes against the first API. This function returns a
 * list of "problems".
 *
 * This file runs in node (command line API comparison), in the diff wrapper (client-facing API comparison) and
 * in simulations in phetioEngine when ?ea&phetioCompareAPI is specified (for CT).
 *
 * Note that even though it is a preload, it uses a different global/namespacing pattern than phet-io-initialize-globals.js
 * in order to simplify usage in all these sites.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */

/**
 * @typedef API
 * @property {boolean} phetioFullAPI
 * @property {Object} phetioElements - phetioElements for version >=1.0 this will be a sparse, tree like structure with
 *                    metadata in key: `_metadata`. For version<1 this will be a flat list with phetioIDs as keys,
 *                    and values as metadata.
 * @property {Object} phetioTypes
 */

/**
 * See phetioEngine.js for where this is generated in master. Keep in mind that we support different versions, including
 * APIs that don't have a version attribute.
 * @typedef API_1_0
 * @extends API
 * @property {{major:number, minor:number}} version
 * @property {string} sim
 */
( () => {

  const METADATA_KEY_NAME = '_metadata';
  const DATA_KEY_NAME = '_data';

  // Is not the reserved keys to store data/metadata on PhET-iO elements.
  const isChildKey = key => key !== METADATA_KEY_NAME && key !== DATA_KEY_NAME;

  /**
   * "up-convert" an API to be in the format of API version >=1.0. This generally is thought of as a "sparse, tree-like" API.
   * @param {API} api
   * @param _
   * @returns {API} - In this version, phetioElements will be structured as a tree, but will have a verbose and complete
   *                  set of all metadata keys for each element. There will not be `metadataDefaults` in each type.
   */
  const toStructuredTree = ( api, _ ) => {
    const sparseAPI = _.cloneDeep( api );

    // DUPLICATED with phetioEngine.js
    const sparseElements = {};
    Object.keys( api.phetioElements ).forEach( phetioID => {
      const entry = api.phetioElements[ phetioID ];

      // API versions < 1.0, use a tandem separator of '.'  If we ever change this separator in master (hopefully not!)
      // this value wouldn't change since it reflects the prior committed versions which do use '.'
      const chain = phetioID.split( '.' );

      // Fill in each level
      let level = sparseElements;
      chain.forEach( componentName => {
        level[ componentName ] = level[ componentName ] || {};
        level = level[ componentName ];
      } );

      level[ METADATA_KEY_NAME ] = {};

      Object.keys( entry ).forEach( key => {

          // write all values without trying to factor out defaults
          level[ METADATA_KEY_NAME ][ key ] = entry[ key ];
        }
      );
    } );

    sparseAPI.phetioElements = sparseElements;
    return sparseAPI;
  };

  /**
   * @param {Object} phetioElement
   * @param {API} api
   * @param {Object} _ - lodash
   * @param {function|undefined} assert - optional assert
   * @returns {Object}
   */
  const getMetadataValues = ( phetioElement, api, _, assert ) => {
    const typeName = phetioElement[ METADATA_KEY_NAME ] ? ( phetioElement[ METADATA_KEY_NAME ].phetioTypeName || 'ObjectIO' ) : 'ObjectIO';

    if ( api.version ) {
      const defaults = getMetadataDefaults( typeName, api, _, assert );
      return _.merge( defaults, phetioElement[ METADATA_KEY_NAME ] );
    }
    else {

      // Dense version supplies all metadata values
      return phetioElement[ METADATA_KEY_NAME ];
    }
  };

  /**
   * @param {string} typeName
   * @param {API} api
   * @param {Object} _ - lodash
   * @param {function|undefined} assert - optional assert
   * @returns {Object} - defensive copy, non-mutating
   */
  const getMetadataDefaults = ( typeName, api, _, assert ) => {
    const entry = api.phetioTypes[ typeName ];
    assert && assert( entry, `entry missing: ${typeName}` );
    if ( entry.supertype ) {
      return _.merge( getMetadataDefaults( entry.supertype, api, _ ), entry.metadataDefaults );
    }
    else {
      return _.merge( {}, entry.metadataDefaults );
    }
  };

  /**
   * @param {API} api
   * @returns {boolean} - whether or not the API is of type API_1_0
   */
  const isOldAPIVersion = api => {
    return !api.hasOwnProperty( 'version' );
  };

  /**
   * Compare two APIs for breaking or design changes.
   *
   * NOTE: Named with an underscore to avoid automatically defining `window.phetioCompareAPIs` as a global
   *
   * @param {API} referenceAPI - the "ground truth" or reference API
   * @param {API} proposedAPI - the proposed API for comparison with referenceAPI
   * @param _ - lodash, so this can be used from different contexts.
   * @param {function|undefined} assert - so this can be used from different contexts
   * @param {Object} [options]
   * @returns {{breakingProblems:string[], designedProblems:string[]}}
   */
  const _phetioCompareAPIs = ( referenceAPI, proposedAPI, _, assert, options ) => {

    // If the proposed version predates 1.0, then bring it forward to the structured tree with metadata under `_metadata`.
    if ( isOldAPIVersion( proposedAPI ) ) {
      proposedAPI = toStructuredTree( proposedAPI, _ );
    }

    if ( isOldAPIVersion( referenceAPI ) ) {
      referenceAPI = toStructuredTree( referenceAPI, _ );
    }

    options = _.merge( {
      compareDesignedAPIChanges: true,
      compareBreakingAPIChanges: true
    }, options );

    const breakingProblems = [];
    const designedProblems = [];

    const appendProblem = ( problemString, isDesignedProblem = false ) => {
      if ( isDesignedProblem && options.compareDesignedAPIChanges ) {
        designedProblems.push( problemString );
      }
      else if ( !isDesignedProblem && options.compareBreakingAPIChanges ) {
        breakingProblems.push( problemString );
      }
    };

    /**
     * Visit one element along the APIs.
     * @param {string[]} trail - the path of tandem componentNames
     * @param {Object} reference - current value in the referenceAPI
     * @param {Object} proposed - current value in the proposedAPI
     * @param {boolean} isDesigned
     */
    const visit = ( trail, reference, proposed, isDesigned ) => {
      const phetioID = trail.join( '.' );

      // Detect an instrumented instance
      if ( reference.hasOwnProperty( METADATA_KEY_NAME ) ) {

        // Override isDesigned, if specified
        isDesigned = isDesigned || reference[ METADATA_KEY_NAME ].phetioDesigned;

        const referenceCompleteMetadata = getMetadataValues( reference, referenceAPI, _, assert );
        const proposedCompleteMetadata = getMetadataValues( proposed, proposedAPI, _, assert );

        /**
         * Push any problems that may exist for the provided metadataKey.
         * @param {string} metadataKey - See PhetioObject.getMetadata()
         * @param {boolean} isDesignedChange - if the difference is from a design change, and not from a breaking change test
         * @param {*} [invalidProposedValue] - an optional new value that would signify a breaking change. Any other value would be acceptable.
         */
        const reportDifferences = ( metadataKey, isDesignedChange, invalidProposedValue ) => {
          const referenceValue = referenceCompleteMetadata[ metadataKey ];

          // Gracefully handle missing metadata from the <1.0 API format
          const proposedValue = proposedCompleteMetadata ? proposedCompleteMetadata[ metadataKey ] : {};

          if ( referenceValue !== proposedValue ) {

            // if proposed API is older (no version specified), ignore phetioArchetypePhetioID changed from null to undefined
            // because it used to be sparse, and in version 1.0 it became a default.
            const ignoreBrokenProposed = isOldAPIVersion( proposedAPI ) &&
                                         metadataKey === 'phetioArchetypePhetioID' &&
                                         referenceValue === null &&
                                         proposedValue === undefined;

            const ignoreBrokenReference = isOldAPIVersion( referenceAPI ) &&
                                          metadataKey === 'phetioArchetypePhetioID' &&
                                          proposedValue === null &&
                                          referenceValue === undefined;

            const ignore = ignoreBrokenProposed || ignoreBrokenReference;

            if ( !ignore ) {

              if ( invalidProposedValue === undefined || isDesignedChange ) {
                appendProblem( `${phetioID}.${metadataKey} changed from ${referenceValue} to ${proposedValue}`, isDesignedChange );
              }
              else if ( !isDesignedChange ) {
                if ( proposedValue === invalidProposedValue ) {
                  appendProblem( `${phetioID}.${metadataKey} changed from ${referenceValue} to ${proposedValue}` );
                }
                else {

                  // value changed, but it was a widening API (adding something to state, or making something read/write)
                }
              }
            }
          }
        };

        // Check for breaking changes
        reportDifferences( 'phetioTypeName', false );
        reportDifferences( 'phetioEventType', false );
        reportDifferences( 'phetioPlayback', false );
        reportDifferences( 'phetioDynamicElement', false );
        reportDifferences( 'phetioIsArchetype', false );
        reportDifferences( 'phetioArchetypePhetioID', false );
        reportDifferences( 'phetioState', false, false ); // Only report if something became non-stateful
        reportDifferences( 'phetioReadOnly', false, true ); // Only need to report if something became readOnly

        // The following metadata keys are non-breaking:
        // 'phetioDocumentation'
        // 'phetioFeatured'
        // 'phetioStudioControl'
        // 'phetioHighFrequency', non-breaking, assuming clients with data have the full data stream

        // Check for design changes
        if ( isDesigned ) {
          Object.keys( referenceCompleteMetadata ).forEach( metadataKey => {
            reportDifferences( metadataKey, true );
          } );
        }

        // If the reference file declares an initial state, check that it hasn't changed
        if ( reference._data && reference._data.initialState ) {

          // Detect missing expected state
          if ( !proposed._data || !proposed._data.initialState ) {
            const problemString = `${phetioID}._data.initialState is missing`;

            // Missing but expected state is a breaking problem
            appendProblem( problemString, false );

            // It is also a designed problem if we expected state in a designed subtree
            isDesigned && appendProblem( problemString, true );
          }
          else {

            const equals = _.isEqual( reference._data.initialState, proposed._data.initialState );
            if ( !equals ) {
              const problemString = `${phetioID}._data.initialState differs. \nExpected:\n${JSON.stringify( reference._data.initialState )}\n actual:\n${JSON.stringify( proposed._data.initialState )}\n`;

              // A changed state value could break a client wrapper, so identify it with breaking changes.
              appendProblem( problemString, false );

              // It is also a designed problem if the proposed values deviate from the specified designed values
              isDesigned && appendProblem( problemString, true );
            }
          }
        }
      }

      // Recurse to children
      for ( const componentName in reference ) {
        if ( reference.hasOwnProperty( componentName ) && isChildKey( componentName ) ) {

          if ( !proposed.hasOwnProperty( componentName ) ) {
            const problemString = `PhET-iO Element missing: ${phetioID}.${componentName}`;
            appendProblem( problemString, false );

            if ( isDesigned ) {
              appendProblem( problemString, true );
            }
          }
          else {
            visit(
              trail.concat( componentName ),
              reference[ componentName ],
              proposed[ componentName ],
              isDesigned
            );
          }
        }
      }

      for ( const componentName in proposed ) {
        if ( isDesigned && proposed.hasOwnProperty( componentName ) && isChildKey( componentName ) && !reference.hasOwnProperty( componentName ) ) {
          appendProblem( `New PhET-iO Element not in reference: ${phetioID}.${componentName}`, true );
        }
      }
    };

    visit( [], referenceAPI.phetioElements, proposedAPI.phetioElements, false );

    // Check for: missing IO Types, missing methods, or differing parameter types or return types
    for ( const typeName in referenceAPI.phetioTypes ) {
      if ( referenceAPI.phetioTypes.hasOwnProperty( typeName ) ) {

        // make sure we have the desired type
        if ( !proposedAPI.phetioTypes.hasOwnProperty( typeName ) ) {
          appendProblem( `Type missing: ${typeName}` );
        }
        else {
          const referenceType = referenceAPI.phetioTypes[ typeName ];
          const proposedType = proposedAPI.phetioTypes[ typeName ];

          // make sure we have all of the methods
          const referenceMethods = referenceType.methods;
          const proposedMethods = proposedType.methods;
          for ( const referenceMethod in referenceMethods ) {
            if ( referenceMethods.hasOwnProperty( referenceMethod ) ) {
              if ( !proposedMethods.hasOwnProperty( referenceMethod ) ) {
                appendProblem( `Method missing, type=${typeName}, method=${referenceMethod}` );
              }
              else {

                // check parameter types (exact match)
                const referenceParams = referenceMethods[ referenceMethod ].parameterTypes;
                const proposedParams = proposedMethods[ referenceMethod ].parameterTypes;

                if ( referenceParams.join( ',' ) !== proposedParams.join( ',' ) ) {
                  appendProblem( `${typeName}.${referenceMethod} has different parameter types: [${referenceParams.join( ', ' )}] => [${proposedParams.join( ', ' )}]` );
                }

                const referenceReturnType = referenceMethods[ referenceMethod ].returnType;
                const proposedReturnType = proposedMethods[ referenceMethod ].returnType;
                if ( referenceReturnType !== proposedReturnType ) {
                  appendProblem( `${typeName}.${referenceMethod} has a different return type ${referenceReturnType} => ${proposedReturnType}` );
                }
              }
            }
          }

          // make sure we have all of the events (OK to add more)
          const referenceEvents = referenceType.events;
          const proposedEvents = proposedType.events;
          referenceEvents.forEach( event => {
            if ( !proposedEvents.includes( event ) ) {
              appendProblem( `${typeName} is missing event: ${event}` );
            }
          } );

          // make sure we have matching supertype names
          const referenceSupertypeName = referenceType.supertype;
          const proposedSupertypeName = proposedType.supertype;
          if ( referenceSupertypeName !== proposedSupertypeName ) {
            appendProblem( `${typeName} supertype changed from ${referenceSupertypeName} to ${proposedSupertypeName}. This may or may not
          be a breaking change, but we are reporting it just in case.` );
          }

          // make sure we have matching parameter types
          const referenceParameterTypes = referenceType.parameterTypes || [];
          const proposedParameterTypes = proposedType.parameterTypes;
          if ( !_.isEqual( referenceParameterTypes, proposedParameterTypes ) ) {
            appendProblem( `${typeName} parameter types changed from [${referenceParameterTypes.join( ', ' )}] to [${proposedParameterTypes.join( ', ' )}]. This may or may not
          be a breaking change, but we are reporting it just in case.` );
          }

          // This check assumes that each API will be of a version that has metadataDefaults
          if ( referenceAPI.version && proposedAPI.version ) {

            // Check whether the default values have changed. See https://github.com/phetsims/phet-io/issues/1753
            const referenceDefaults = referenceAPI.phetioTypes[ typeName ].metadataDefaults;
            const proposedDefaults = proposedAPI.phetioTypes[ typeName ].metadataDefaults;

            Object.keys( referenceDefaults ).forEach( key => {
              if ( referenceDefaults[ key ] !== proposedDefaults[ key ] ) {
                appendProblem( `${typeName} metadata value ${key} changed from ${referenceDefaults[ key ]} to ${proposedDefaults[ key ]}. This may or may not be a breaking change, but we are reporting it just in case.` );
              }
            } );
          }
        }
      }
    }

    return {
      breakingProblems: breakingProblems,
      designedProblems: designedProblems
    };
  };

// @public - used to "up-convert" an old versioned API to the new (version >=1), structured tree API.
  _phetioCompareAPIs.toStructuredTree = toStructuredTree;

  if ( typeof window === 'undefined' ) {

    // running in node
    module.exports = _phetioCompareAPIs;
  }
  else {

    window.phetio = window.phetio || {};
    window.phetio.phetioCompareAPIs = _phetioCompareAPIs;
  }
} )();
<|endoftext|>


<commit_before>  const FACEBOOK_accessToken = 'FACEBOOK_accessToken';
  const FACEBOOK_expiresIn = 'FACEBOOK_expiresIn';
  const FACEBOOK_signedRequest = 'FACEBOOK_signedRequest';
  const FACEBOOK_userID = 'FACEBOOK_userID';

  window.fbAsyncInit = function() {
      FB.init({
          appId: '275315012923774',
          cookie: true, // enable cookies to allow the server to access
          // the session
          xfbml: true, // parse social plugins on this page
          version: 'v2.8' // use graph api version 2.8
      });

      FB.getLoginStatus(function(response) {
          if (response.status === "connected") {
              // user is logged in with facebook
              localStorage.setItem(FACEBOOK_accessToken, response.authResponse.accessToken);
              localStorage.setItem(FACEBOOK_expiresIn, response.authResponse.expiresIn);
              localStorage.setItem(FACEBOOK_signedRequest, response.authResponse.signedRequest);
              localStorage.setItem(FACEBOOK_userID, response.authResponse.userID);
          } else if (response.status === "not_authorized" || response.status === "unknown ") {
              // promt the user to loggin
              $(".facebook-login").on('click', function() {
                  FB.login(function(response) {
                      console.log("Log in");
                      console.log(response)
                  })
              })
          }
          console.log("Initial state");
          console.log(response);
      });

  };

  (function(d, s, id) {
      var js, fjs = d.getElementsByTagName(s)[0];
      if (d.getElementById(id)) return;
      js = d.createElement(s);
      js.id = id;
      js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.8&appId=275315012923774";
      fjs.parentNode.insertBefore(js, fjs);
  }(document, 'script', 'facebook-jssdk'));<commit_msg>facebook user data fetch<commit_after>  const FACEBOOK_accessToken = 'FACEBOOK_accessToken';
  const FACEBOOK_expiresIn = 'FACEBOOK_expiresIn';
  const FACEBOOK_signedRequest = 'FACEBOOK_signedRequest';
  const FACEBOOK_userID = 'FACEBOOK_userID';

  window.fbAsyncInit = function() {
      FB.init({
          appId: '275315012923774',
          cookie: true, // enable cookies to allow the server to access
          // the session
          xfbml: true, // parse social plugins on this page
          version: 'v2.8' // use graph api version 2.8
      });

      FB.getLoginStatus(function(response) {
          if (response.status === "connected") {
              // user is logged in with facebook
              localStorage.setItem(FACEBOOK_accessToken, response.authResponse.accessToken);
              localStorage.setItem(FACEBOOK_expiresIn, response.authResponse.expiresIn);
              localStorage.setItem(FACEBOOK_signedRequest, response.authResponse.signedRequest);
              localStorage.setItem(FACEBOOK_userID, response.authResponse.userID);
          } else if (response.status === "not_authorized" || response.status === "unknown ") {
              // promt the user to loggin
              $(".facebook-login").on('click', function() {
                  FB.login(function(response) {
                      console.log("Log in");
                      console.log(response)
                  })
              })
          }
          console.log("Initial state");
          console.log(response);
          FB.api(FACEBOOK_userID, function(response) {
              console.log(response);
          });
      });

  };

  (function(d, s, id) {
      var js, fjs = d.getElementsByTagName(s)[0];
      if (d.getElementById(id)) return;
      js = d.createElement(s);
      js.id = id;
      js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.8&appId=275315012923774";
      fjs.parentNode.insertBefore(js, fjs);
  }(document, 'script', 'facebook-jssdk'));<|endoftext|>


<commit_before>import { extend } from 'flarum/extension-utils';
import ComposerBody from 'flarum/components/composer-body';
import ReplyComposer from 'flarum/components/reply-composer';
import EditComposer from 'flarum/components/edit-composer';
import avatar from 'flarum/helpers/avatar';
import username from 'flarum/helpers/username';

import AutocompleteDropdown from 'mentions/components/autocomplete-dropdown';

export default function() {
  extend(ComposerBody.prototype, 'onload', function(original, element, isInitialized, context) {
    if (isInitialized) return;

    var composer = this;
    var $container = $('

');
    var dropdown = new AutocompleteDropdown({items: []});
    var typed;
    var mentionStart;
    var $textarea = this.$('textarea');
    var searched = [];
    var searchTimeout;

    var applySuggestion = function(replacement) {
      replacement += ' ';

      var content = composer.content();
      composer.editor.setContent(content.substring(0, mentionStart - 1)+replacement+content.substr($textarea[0].selectionStart));

      var index = mentionStart - 1 + replacement.length;
      composer.editor.setSelectionRange(index, index);

      dropdown.hide();
    };

    $textarea
      .after($container)
      .on('keydown', dropdown.navigate.bind(dropdown))
      .on('click keyup', function(e) {
        // Up, down, enter, tab, escape, left, right.
        if ([9, 13, 27, 40, 38, 37, 39].indexOf(e.which) !== -1) return;

        var cursor = this.selectionStart;

        if (this.selectionEnd - cursor > 0) return;

        // Search backwards from the cursor for an '@' symbol, without any
        // intervening whitespace. If we find one, we will want to show the
        // autocomplete dropdown!
        var value = this.value;
        mentionStart = 0;
        for (var i = cursor - 1; i >= 0; i--) {
          var character = value.substr(i, 1);
          if (/\s/.test(character)) break;
          if (character == '@') {
            mentionStart = i + 1;
            break;
          }
        }

        dropdown.hide();
        dropdown.active(false);

        if (mentionStart) {
          typed = value.substring(mentionStart, cursor).toLowerCase();

          var makeSuggestion = function(user, replacement, content, className) {
            return m('a[href=javascript:;].post-preview', {
              className,
              onclick: () => applySuggestion(replacement),
              onmouseenter: function() { dropdown.setIndex($(this).parent().index()); }
            }, m('div.post-preview-content', [
              avatar(user),
              (function() {
                var vdom = username(user);
                if (typed) {
                  var regexp = new RegExp(typed, 'gi');
                  vdom.children[0] = m.trust(
                    $('
').text(vdom.children[0]).html().replace(regexp, '$&')
                  );
                }
                return vdom;
              })(), ' ',
              content
            ]));
          };

          var buildSuggestions = () => {
            var suggestions = [];

            // If the user is replying to a discussion, or if they are editing a
            // post, then we can suggest other posts in the discussion to mention.
            // We will add the 5 most recent comments in the discussion which
            // match any username characters that have been typed.
            var composerPost = composer.props.post;
            var discussion = (composerPost && composerPost.discussion()) || composer.props.discussion;
            if (discussion) {
              discussion.posts()
                .filter(post => post && post.contentType() === 'comment' && (!composerPost || post.number() < composerPost.number()))
                .sort((a, b) => b.time() - a.time())
                .filter(post => {
                  var user = post.user();
                  return user && user.username().toLowerCase().substr(0, typed.length) === typed;
                })
                .splice(0, 5)
                .forEach(post => {
                  var user = post.user();
                  suggestions.push(
                    makeSuggestion(user, '@'+user.username()+'#'+post.number(), [
                      'Reply to #', post.number(), ' — ',
                      post.excerpt()
                    ], 'suggestion-post')
                  );
                });
            }

            // If the user has started to type a username, then suggest users
            // matching that username.
            if (typed) {
              app.store.all('users').forEach(user => {
                if (user.username().toLowerCase().substr(0, typed.length) !== typed) return;

                suggestions.push(
                  makeSuggestion(user, '@'+user.username(), '', 'suggestion-user')
                );
              });
            }

            if (suggestions.length) {
              dropdown.props.items = suggestions;
              m.render($container[0], dropdown.view());

              dropdown.show();
              var coordinates = getCaretCoordinates(this, mentionStart);
              var left = coordinates.left;
              var top = coordinates.top + 15;
              var width = dropdown.$().outerWidth();
              var height = dropdown.$().outerHeight();
              var parent = dropdown.$().offsetParent();
              if (top + height > parent.height()) {
                top = coordinates.top - height - 15;
              }
              if (left + width > parent.width()) {
                left = parent.width() - width;
              }
              dropdown.show(left, top);
            }
          };

          buildSuggestions();

          dropdown.setIndex(0);
          dropdown.$().scrollTop(0);
          dropdown.active(true);

          clearTimeout(searchTimeout);
          searchTimeout = setTimeout(function() {
            var typedLower = typed.toLowerCase();
            if (searched.indexOf(typedLower) === -1) {
              app.store.find('users', {q: typed, page: {limit: 5}}).then(users => {
                if (dropdown.active()) buildSuggestions();
              });
              searched.push(typedLower);
            }
          }, 250);
        }
      });
  });
}
<commit_msg>Use core's new highlight helper<commit_after>import { extend } from 'flarum/extension-utils';
import ComposerBody from 'flarum/components/composer-body';
import ReplyComposer from 'flarum/components/reply-composer';
import EditComposer from 'flarum/components/edit-composer';
import avatar from 'flarum/helpers/avatar';
import username from 'flarum/helpers/username';
import highlight from 'flarum/helpers/highlight';

import AutocompleteDropdown from 'mentions/components/autocomplete-dropdown';

export default function() {
  extend(ComposerBody.prototype, 'onload', function(original, element, isInitialized, context) {
    if (isInitialized) return;

    var composer = this;
    var $container = $('

');
    var dropdown = new AutocompleteDropdown({items: []});
    var typed;
    var mentionStart;
    var $textarea = this.$('textarea');
    var searched = [];
    var searchTimeout;

    var applySuggestion = function(replacement) {
      replacement += ' ';

      var content = composer.content();
      composer.editor.setContent(content.substring(0, mentionStart - 1)+replacement+content.substr($textarea[0].selectionStart));

      var index = mentionStart - 1 + replacement.length;
      composer.editor.setSelectionRange(index, index);

      dropdown.hide();
    };

    $textarea
      .after($container)
      .on('keydown', dropdown.navigate.bind(dropdown))
      .on('click keyup', function(e) {
        // Up, down, enter, tab, escape, left, right.
        if ([9, 13, 27, 40, 38, 37, 39].indexOf(e.which) !== -1) return;

        var cursor = this.selectionStart;

        if (this.selectionEnd - cursor > 0) return;

        // Search backwards from the cursor for an '@' symbol, without any
        // intervening whitespace. If we find one, we will want to show the
        // autocomplete dropdown!
        var value = this.value;
        mentionStart = 0;
        for (var i = cursor - 1; i >= 0; i--) {
          var character = value.substr(i, 1);
          if (/\s/.test(character)) break;
          if (character == '@') {
            mentionStart = i + 1;
            break;
          }
        }

        dropdown.hide();
        dropdown.active(false);

        if (mentionStart) {
          typed = value.substring(mentionStart, cursor).toLowerCase();

          var makeSuggestion = function(user, replacement, content, className) {
            return m('a[href=javascript:;].post-preview', {
              className,
              onclick: () => applySuggestion(replacement),
              onmouseenter: function() { dropdown.setIndex($(this).parent().index()); }
            }, m('div.post-preview-content', [
              avatar(user),
              (function() {
                var vdom = username(user);
                if (typed) {
                  vdom.children[0] = highlight(vdom.children[0], typed);
                }
                return vdom;
              })(), ' ',
              content
            ]));
          };

          var buildSuggestions = () => {
            var suggestions = [];

            // If the user is replying to a discussion, or if they are editing a
            // post, then we can suggest other posts in the discussion to mention.
            // We will add the 5 most recent comments in the discussion which
            // match any username characters that have been typed.
            var composerPost = composer.props.post;
            var discussion = (composerPost && composerPost.discussion()) || composer.props.discussion;
            if (discussion) {
              discussion.posts()
                .filter(post => post && post.contentType() === 'comment' && (!composerPost || post.number() < composerPost.number()))
                .sort((a, b) => b.time() - a.time())
                .filter(post => {
                  var user = post.user();
                  return user && user.username().toLowerCase().substr(0, typed.length) === typed;
                })
                .splice(0, 5)
                .forEach(post => {
                  var user = post.user();
                  suggestions.push(
                    makeSuggestion(user, '@'+user.username()+'#'+post.number(), [
                      'Reply to #', post.number(), ' — ',
                      post.excerpt()
                    ], 'suggestion-post')
                  );
                });
            }

            // If the user has started to type a username, then suggest users
            // matching that username.
            if (typed) {
              app.store.all('users').forEach(user => {
                if (user.username().toLowerCase().substr(0, typed.length) !== typed) return;

                suggestions.push(
                  makeSuggestion(user, '@'+user.username(), '', 'suggestion-user')
                );
              });
            }

            if (suggestions.length) {
              dropdown.props.items = suggestions;
              m.render($container[0], dropdown.view());

              dropdown.show();
              var coordinates = getCaretCoordinates(this, mentionStart);
              var left = coordinates.left;
              var top = coordinates.top + 15;
              var width = dropdown.$().outerWidth();
              var height = dropdown.$().outerHeight();
              var parent = dropdown.$().offsetParent();
              if (top + height > parent.height()) {
                top = coordinates.top - height - 15;
              }
              if (left + width > parent.width()) {
                left = parent.width() - width;
              }
              dropdown.show(left, top);
            }
          };

          buildSuggestions();

          dropdown.setIndex(0);
          dropdown.$().scrollTop(0);
          dropdown.active(true);

          clearTimeout(searchTimeout);
          searchTimeout = setTimeout(function() {
            var typedLower = typed.toLowerCase();
            if (searched.indexOf(typedLower) === -1) {
              app.store.find('users', {q: typed, page: {limit: 5}}).then(users => {
                if (dropdown.active()) buildSuggestions();
              });
              searched.push(typedLower);
            }
          }, 250);
        }
      });
  });
}
<|endoftext|>


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

/**
 * A Scenery Node that represents a ray of light in the view.  Rays of light can have shapes that reduce or block the
 * amount of light passing through.
 *
 * @author John Blanco
 * @author Andrew Adare
 */
define( function( require ) {
  'use strict';

  // modules
  var EFACConstants = require( 'ENERGY_FORMS_AND_CHANGES/common/EFACConstants' );
  var energyFormsAndChanges = require( 'ENERGY_FORMS_AND_CHANGES/energyFormsAndChanges' );
  var inherit = require( 'PHET_CORE/inherit' );
  var KiteLine = require( 'KITE/segments/Line' ); // eslint-disable-line require-statement-match
  var Line = require( 'SCENERY/nodes/Line' );
  var LinearGradient = require( 'SCENERY/util/LinearGradient' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Shape = require( 'KITE/Shape' );
  var Util = require( 'DOT/Util' );
  var Vector2 = require( 'DOT/Vector2' );

  // constants
  var STROKE_THICKNESS = 2;
  var SEARCH_ITERATIONS = 10;

  /**
   * @param {Vector2} origin
   * @param {Vector2} endpoint
   * @param {Color} color
   * @constructor
   */
  function LightRayNode( origin, endpoint, color ) {

    // @private - data that defines this ray
    this.lightAbsorbingShapes = [];
    this.pointAndFadeValues = [];
    this.origin = origin;
    this.endpoint = endpoint;
    this.color = color;

    Node.call( this );

    this.updateRay();
  }

  /**
   * helper type that consolidates a point and a fade value
   * @param {Vector2} point - position
   * @param {number} fadeValue - Fade coefficient
   * @constructor
   * @private
   */
  function PointAndFadeValue( point, fadeValue ) {
    this.point = point;
    this.fadeValue = fadeValue;
  }

  energyFormsAndChanges.register( 'LightRayNode', LightRayNode );

  return inherit( Node, LightRayNode, {

    /**
     * add a shape that will potentially interact with this light ray
     * @param {LightAbsorbingShape} lightAbsorbingShape
     * @public
     */
    addLightAbsorbingShape: function( lightAbsorbingShape ) {
      var self = this;
      this.lightAbsorbingShapes.push( lightAbsorbingShape );
      lightAbsorbingShape.absorptionCoefficientProperty.link( function() {
        self.updateRay();
      } );
    },

    /**
     * @param  {LightAbsorbingShape} lightAbsorbingShape
     * @public
     */
    removeLightAbsorbingShape: function( lightAbsorbingShape ) {
      // TODO: This probably works, but is not quite correct, since it should really be unlinking the added listener, not everything.
      lightAbsorbingShape.absorptionCoefficientProperty.unlinkAll();
      _.pull( this.lightAbsorbingShapes, lightAbsorbingShape );
      this.updateRay();
    },

    /**
     * @private
     */
    updateRay: function() {
      this.removeAllChildren();
      this.pointAndFadeValues.length = 0;

      this.pointAndFadeValues.push( new PointAndFadeValue( this.origin, EFACConstants.FADE_COEFFICIENT_IN_AIR ) );
      this.pointAndFadeValues.push( new PointAndFadeValue( this.endpoint, 0 ) );

      var self = this;
      this.lightAbsorbingShapes.forEach( function( absorbingShape ) {

        var entryPoint = self.getShapeEntryPoint( self.origin, self.endpoint, absorbingShape.shape );

        if ( entryPoint !== null ) {
          var fade = absorbingShape.absorptionCoefficientProperty.get();
          self.pointAndFadeValues.push( new PointAndFadeValue( entryPoint, fade ) );
          var exitPoint = self.getShapeExitPoint( self.origin, self.endpoint, absorbingShape.shape );
          if ( exitPoint !== null ) {
            self.pointAndFadeValues.push( new PointAndFadeValue( exitPoint, EFACConstants.FADE_COEFFICIENT_IN_AIR ) );
          }
        }
      } );

      // sort the list of PointAndFadeValues by their distance from the origin, closest first
      var sortedPointAndFadeValues = _.sortBy( this.pointAndFadeValues, function( p ) {
        return p.point.distance( self.origin );
      } );

      var rayLength = this.origin.distance( this.endpoint );

      var rayGradient = new LinearGradient( this.origin.x, this.origin.y, this.endpoint.x, this.endpoint.y )
        .addColorStop( 0, this.color );

      var prevIntensity = this.color.alpha;
      for ( var i = 0; i < sortedPointAndFadeValues.length - 1; i++ ) {
        var distanceFromOrigin = this.origin.distance( sortedPointAndFadeValues[ i + 1 ].point );
        var distanceFromPreviousPoint = sortedPointAndFadeValues[ i ].point.distance( sortedPointAndFadeValues[ i + 1 ].point );

        var intensityAtEndPoint = prevIntensity * Math.pow( Math.E, -sortedPointAndFadeValues[ i ].fadeValue * distanceFromPreviousPoint );
        intensityAtEndPoint = Util.roundSymmetric( intensityAtEndPoint * 100 ) / 100; // round to nearest tenth

        var endPointColor = this.color.copy().setAlpha( intensityAtEndPoint );
        rayGradient.addColorStop( distanceFromOrigin / rayLength, endPointColor );

        prevIntensity = intensityAtEndPoint;
      }
      rayGradient.addColorStop( 1, this.color.copy().setAlpha( 0 ) );

      var fadingRay = new Line( this.origin, this.endpoint, {
        stroke: rayGradient,
        lineWidth: STROKE_THICKNESS
      } );
      this.addChild( fadingRay );
    },

    /**
     * @param {KiteLine} line1
     * @param {KiteLine} line2
     * @returns {Vector2}
     * @private
     */
    getLineIntersection: function( line1, line2 ) {

      var start1 = line1.start;
      var start2 = line2.start;
      var end1 = line1.end;
      var end2 = line2.end;

      var denominator = ( ( end1.x - start1.x ) * ( end2.y - start2.y ) ) -
                        ( ( end1.y - start1.y ) * ( end2.x - start2.x ) );

      // Check if the lines are parallel, and thus don't intersect.
      if ( denominator === 0 ) {
        return null;
      }

      var numerator = ( ( start1.y - start2.y ) * ( end2.x - start2.x ) ) -
                      ( ( start1.x - start2.x ) * ( end2.y - start2.y ) );
      var r = numerator / denominator;

      var numerator2 = ( ( start1.y - start2.y ) * ( end1.x - start1.x ) ) -
                       ( ( start1.x - start2.x ) * ( end1.y - start1.y ) );
      var s = numerator2 / denominator;

      if ( ( r < 0 || r > 1 ) || ( s < 0 || s > 1 ) ) {
        return null;
      }

      // find intersection point
      return new Vector2(
        start1.x + ( r * ( end1.x - start1.x ) ),
        start1.y + ( r * ( end1.y - start1.y ) )
      );
    },

    /**
     * @param  {Vector2} origin
     * @param  {Vector2} endpoint
     * @param  {Shape} shape
     * @returns {Vector2|null}
     * @private
     */
    getShapeEntryPoint: function( origin, endpoint, shape ) {
      var b = shape.bounds;
      var shapeRect = Shape.rect( b.minX, b.minY, b.getWidth(), b.getHeight() );
      var entryPoint = null;

      if ( shape.interiorIntersectsLineSegment( origin, endpoint ) ) {
        var boundsEntryPoint = this.getRectangleEntryPoint( origin, endpoint, shapeRect );
        if ( boundsEntryPoint === null ) {

          // DEBUG
          var l = new Line( origin, endpoint, {
            stroke: 'lime',
            lineWidth: 3
          } );
          this.addChild( l );

          var p = new Path( shape, {
            stroke: 'red',
            lineWidth: 3
          } );
          this.addChild( p );
          return null;
        }
        var boundsExitPoint = this.getRectangleExitPoint( origin, endpoint, shapeRect );
        var searchEndPoint = boundsExitPoint === null ? endpoint : boundsExitPoint;

        // Search linearly for edge of the shape.  BIG HAIRY NOTE - This will not work in all cases.  It worked for the
        // coarse shapes and rough bounds needed for this simulation.  Don't reuse if you need good general edge
        // finding.
        var angle = endpoint.minus( origin ).angle();
        var incrementalDistance = boundsEntryPoint.distance( searchEndPoint ) / SEARCH_ITERATIONS;
        for ( var i = 0; i < SEARCH_ITERATIONS; i++ ) {
          var testPoint = boundsEntryPoint.plus( new Vector2( incrementalDistance * i, 0 ).rotated( angle ) );
          if ( shape.bounds.containsPoint( testPoint ) ) {
            entryPoint = testPoint;
            break;
          }
        }
      }
      return entryPoint;
    },

    /**
     * @param  {Vector2} origin
     * @param  {Vector2} endpoint
     * @param  {Shape} shape
     * @returns {Vector2}
     * @private
     */
    getShapeExitPoint: function( origin, endpoint, shape ) {

      var exitPoint = null;

      if ( shape.bounds.containsPoint( endpoint ) ) {

        // line ends inside shape, return null
        return null;
      }

      if ( !shape.bounds.containsPoint( endpoint ) && shape.interiorIntersectsLineSegment( origin, endpoint ) ) {

        // phase I - Do a binary search to locate the edge of the rectangle that encloses the shape
        var angle = endpoint.minus( origin ).angle();
        var length = origin.distance( endpoint );
        var lengthChange = length / 2;
        for ( var i = 0; i < SEARCH_ITERATIONS; i++ ) {
          var start = origin.plus( new Vector2( length, 0 ).rotated( angle ) );
          length += lengthChange * ( shape.interiorIntersectsLineSegment( start, endpoint ) ? 1 : -1 );
          lengthChange = lengthChange / 2;
        }
        exitPoint = origin.plus( new Vector2( length, 0 ).rotated( angle ) );
      }
      return exitPoint;
    },

    /**
     * @param  {Vector2} origin
     * @param  {Vector2} endpoint
     * @param  {Rectangle} rect
     * @returns {Vector2}
     * @private
     */
    getRectangleEntryPoint: function( origin, endpoint, rect ) {
      var intersectingPoints = this.getRectangleLineIntersectionPoints( rect, new KiteLine( origin, endpoint ) );
      var closestIntersectionPoint = null;
      intersectingPoints.forEach( function( point ) {
        if ( closestIntersectionPoint === null ||
             closestIntersectionPoint.distance( origin ) > point.distance( origin ) ) {
          closestIntersectionPoint = point;
        }
      } );

      return closestIntersectionPoint;
    },

    /**
     * @param  {Vector2} origin
     * @param  {Vector2} endpoint
     * @param  {Rectangle} rect
     * @returns {Vector2}
     * @private
     */
    getRectangleExitPoint: function( origin, endpoint, rect ) {
      var intersectingPoints = this.getRectangleLineIntersectionPoints( rect, new KiteLine( origin, endpoint ) );

      if ( intersectingPoints.length < 2 ) {

        // line either doesn't intersect or ends inside the rectangle
        return null;
      }

      var furthestIntersectionPoint = null;
      intersectingPoints.forEach( function( point ) {
        if ( furthestIntersectionPoint === null ||
             furthestIntersectionPoint.distance( origin ) < point.distance( origin ) ) {
          furthestIntersectionPoint = point;
        }
      } );

      return furthestIntersectionPoint;
    },

    /**
     * @param  {Rectangle} rect
     * @param  {Line} line
     * @returns {Vector2[]}
     */
    getRectangleLineIntersectionPoints: function( rect, line ) {

      // corners of rect
      var p = [
        new Vector2( rect.bounds.minX, rect.bounds.minY ),
        new Vector2( rect.bounds.minX, rect.bounds.maxY ),
        new Vector2( rect.bounds.maxX, rect.bounds.maxY ),
        new Vector2( rect.bounds.maxX, rect.bounds.minY )
      ];

      // perimeter lines of rect
      var lines = [];
      lines.push( new KiteLine( p[ 0 ], p[ 1 ] ) );
      lines.push( new KiteLine( p[ 1 ], p[ 2 ] ) );
      lines.push( new KiteLine( p[ 2 ], p[ 3 ] ) );
      lines.push( new KiteLine( p[ 3 ], p[ 0 ] ) );

      var intersectingPoints = [];
      var self = this;
      lines.forEach( function( rectLine ) {
        var intersectingPoint = self.getLineIntersection( rectLine, line );
        if ( intersectingPoint !== null ) {
          intersectingPoints.push( intersectingPoint );
        }
      } );

      return intersectingPoints;
    }

  } );
} );

<commit_msg>Convert to ES6, see https://github.com/phetsims/energy-forms-and-changes/issues/211<commit_after>// Copyright 2016-2019, University of Colorado Boulder

/**
 * A Scenery Node that represents a ray of light in the view.  Rays of light can have shapes that reduce or block the
 * amount of light passing through.
 *
 * @author John Blanco
 * @author Andrew Adare
 */
define( require => {
  'use strict';

  // modules
  const EFACConstants = require( 'ENERGY_FORMS_AND_CHANGES/common/EFACConstants' );
  const energyFormsAndChanges = require( 'ENERGY_FORMS_AND_CHANGES/energyFormsAndChanges' );
  const KiteLine = require( 'KITE/segments/Line' ); // eslint-disable-line require-statement-match
  const Line = require( 'SCENERY/nodes/Line' );
  const LinearGradient = require( 'SCENERY/util/LinearGradient' );
  const Node = require( 'SCENERY/nodes/Node' );
  const Path = require( 'SCENERY/nodes/Path' );
  const Shape = require( 'KITE/Shape' );
  const Util = require( 'DOT/Util' );
  const Vector2 = require( 'DOT/Vector2' );

  // constants
  const STROKE_THICKNESS = 2;
  const SEARCH_ITERATIONS = 10;

  class LightRayNode extends Node {

    /**
     * @param {Vector2} origin
     * @param {Vector2} endpoint
     * @param {Color} color
     */
    constructor( origin, endpoint, color ) {
      super();

      // @private - data that defines this ray
      this.lightAbsorbingShapes = [];
      this.pointAndFadeValues = [];
      this.origin = origin;
      this.endpoint = endpoint;
      this.color = color;

      this.updateRay();
    }

    /**
     * add a shape that will potentially interact with this light ray
     * @param {LightAbsorbingShape} lightAbsorbingShape
     * @public
     */
    addLightAbsorbingShape( lightAbsorbingShape ) {
      this.lightAbsorbingShapes.push( lightAbsorbingShape );
      lightAbsorbingShape.absorptionCoefficientProperty.link( () => {
        this.updateRay();
      } );
    }

    /**
     * @param  {LightAbsorbingShape} lightAbsorbingShape
     * @public
     */
    removeLightAbsorbingShape( lightAbsorbingShape ) {
      // TODO: This probably works, but is not quite correct, since it should really be unlinking the added listener, not everything.
      lightAbsorbingShape.absorptionCoefficientProperty.unlinkAll();
      _.pull( this.lightAbsorbingShapes, lightAbsorbingShape );
      this.updateRay();
    }

    /**
     * @private
     */
    updateRay() {
      this.removeAllChildren();
      this.pointAndFadeValues.length = 0;

      this.pointAndFadeValues.push( new PointAndFadeValue( this.origin, EFACConstants.FADE_COEFFICIENT_IN_AIR ) );
      this.pointAndFadeValues.push( new PointAndFadeValue( this.endpoint, 0 ) );

      this.lightAbsorbingShapes.forEach( absorbingShape => {

        const entryPoint = this.getShapeEntryPoint( this.origin, this.endpoint, absorbingShape.shape );

        if ( entryPoint !== null ) {
          const fade = absorbingShape.absorptionCoefficientProperty.get();
          this.pointAndFadeValues.push( new PointAndFadeValue( entryPoint, fade ) );
          const exitPoint = this.getShapeExitPoint( this.origin, this.endpoint, absorbingShape.shape );
          if ( exitPoint !== null ) {
            this.pointAndFadeValues.push( new PointAndFadeValue( exitPoint, EFACConstants.FADE_COEFFICIENT_IN_AIR ) );
          }
        }
      } );

      // sort the list of PointAndFadeValues by their distance from the origin, closest first
      const sortedPointAndFadeValues = _.sortBy( this.pointAndFadeValues, p => {
        return p.point.distance( this.origin );
      } );

      const rayLength = this.origin.distance( this.endpoint );

      const rayGradient = new LinearGradient( this.origin.x, this.origin.y, this.endpoint.x, this.endpoint.y )
        .addColorStop( 0, this.color );

      let prevIntensity = this.color.alpha;
      for ( let i = 0; i < sortedPointAndFadeValues.length - 1; i++ ) {
        const distanceFromOrigin = this.origin.distance( sortedPointAndFadeValues[ i + 1 ].point );
        const distanceFromPreviousPoint = sortedPointAndFadeValues[ i ].point.distance( sortedPointAndFadeValues[ i + 1 ].point );

        let intensityAtEndPoint = prevIntensity * Math.pow( Math.E, -sortedPointAndFadeValues[ i ].fadeValue * distanceFromPreviousPoint );
        intensityAtEndPoint = Util.roundSymmetric( intensityAtEndPoint * 100 ) / 100; // round to nearest tenth

        const endPointColor = this.color.copy().setAlpha( intensityAtEndPoint );
        rayGradient.addColorStop( distanceFromOrigin / rayLength, endPointColor );

        prevIntensity = intensityAtEndPoint;
      }
      rayGradient.addColorStop( 1, this.color.copy().setAlpha( 0 ) );

      const fadingRay = new Line( this.origin, this.endpoint, {
        stroke: rayGradient,
        lineWidth: STROKE_THICKNESS
      } );
      this.addChild( fadingRay );
    }

    /**
     * @param {KiteLine} line1
     * @param {KiteLine} line2
     * @returns {Vector2}
     * @private
     */
    getLineIntersection( line1, line2 ) {

      const start1 = line1.start;
      const start2 = line2.start;
      const end1 = line1.end;
      const end2 = line2.end;

      const denominator = ( ( end1.x - start1.x ) * ( end2.y - start2.y ) ) -
                        ( ( end1.y - start1.y ) * ( end2.x - start2.x ) );

      // Check if the lines are parallel, and thus don't intersect.
      if ( denominator === 0 ) {
        return null;
      }

      const numerator = ( ( start1.y - start2.y ) * ( end2.x - start2.x ) ) -
                        ( ( start1.x - start2.x ) * ( end2.y - start2.y ) );
      const r = numerator / denominator;

      const numerator2 = ( ( start1.y - start2.y ) * ( end1.x - start1.x ) ) -
                       ( ( start1.x - start2.x ) * ( end1.y - start1.y ) );
      const s = numerator2 / denominator;

      if ( ( r < 0 || r > 1 ) || ( s < 0 || s > 1 ) ) {
        return null;
      }

      // find intersection point
      return new Vector2(
        start1.x + ( r * ( end1.x - start1.x ) ),
        start1.y + ( r * ( end1.y - start1.y ) )
      );
    }

    /**
     * @param  {Vector2} origin
     * @param  {Vector2} endpoint
     * @param  {Shape} shape
     * @returns {Vector2|null}
     * @private
     */
    getShapeEntryPoint( origin, endpoint, shape ) {
      const b = shape.bounds;
      const shapeRect = Shape.rect( b.minX, b.minY, b.getWidth(), b.getHeight() );
      let entryPoint = null;

      if ( shape.interiorIntersectsLineSegment( origin, endpoint ) ) {
        const boundsEntryPoint = this.getRectangleEntryPoint( origin, endpoint, shapeRect );
        if ( boundsEntryPoint === null ) {

          // DEBUG
          const l = new Line( origin, endpoint, {
            stroke: 'lime',
            lineWidth: 3
          } );
          this.addChild( l );

          const p = new Path( shape, {
            stroke: 'red',
            lineWidth: 3
          } );
          this.addChild( p );
          return null;
        }
        const boundsExitPoint = this.getRectangleExitPoint( origin, endpoint, shapeRect );
        const searchEndPoint = boundsExitPoint === null ? endpoint : boundsExitPoint;

        // Search linearly for edge of the shape.  BIG HAIRY NOTE - This will not work in all cases.  It worked for the
        // coarse shapes and rough bounds needed for this simulation.  Don't reuse if you need good general edge
        // finding.
        const angle = endpoint.minus( origin ).angle();
        const incrementalDistance = boundsEntryPoint.distance( searchEndPoint ) / SEARCH_ITERATIONS;
        for ( let i = 0; i < SEARCH_ITERATIONS; i++ ) {
          const testPoint = boundsEntryPoint.plus( new Vector2( incrementalDistance * i, 0 ).rotated( angle ) );
          if ( shape.bounds.containsPoint( testPoint ) ) {
            entryPoint = testPoint;
            break;
          }
        }
      }
      return entryPoint;
    }

    /**
     * @param  {Vector2} origin
     * @param  {Vector2} endpoint
     * @param  {Shape} shape
     * @returns {Vector2}
     * @private
     */
    getShapeExitPoint( origin, endpoint, shape ) {
      let exitPoint = null;

      if ( shape.bounds.containsPoint( endpoint ) ) {

        // line ends inside shape, return null
        return null;
      }

      if ( !shape.bounds.containsPoint( endpoint ) && shape.interiorIntersectsLineSegment( origin, endpoint ) ) {

        // phase I - Do a binary search to locate the edge of the rectangle that encloses the shape
        const angle = endpoint.minus( origin ).angle();
        let length = origin.distance( endpoint );
        let lengthChange = length / 2;
        for ( let i = 0; i < SEARCH_ITERATIONS; i++ ) {
          const start = origin.plus( new Vector2( length, 0 ).rotated( angle ) );
          length += lengthChange * ( shape.interiorIntersectsLineSegment( start, endpoint ) ? 1 : -1 );
          lengthChange = lengthChange / 2;
        }
        exitPoint = origin.plus( new Vector2( length, 0 ).rotated( angle ) );
      }
      return exitPoint;
    }

    /**
     * @param  {Vector2} origin
     * @param  {Vector2} endpoint
     * @param  {Rectangle} rect
     * @returns {Vector2}
     * @private
     */
    getRectangleEntryPoint( origin, endpoint, rect ) {
      const intersectingPoints = this.getRectangleLineIntersectionPoints( rect, new KiteLine( origin, endpoint ) );
      let closestIntersectionPoint = null;
      intersectingPoints.forEach( point => {
        if ( closestIntersectionPoint === null ||
             closestIntersectionPoint.distance( origin ) > point.distance( origin ) ) {
          closestIntersectionPoint = point;
        }
      } );

      return closestIntersectionPoint;
    }

    /**
     * @param  {Vector2} origin
     * @param  {Vector2} endpoint
     * @param  {Rectangle} rect
     * @returns {Vector2}
     * @private
     */
    getRectangleExitPoint( origin, endpoint, rect ) {
      const intersectingPoints = this.getRectangleLineIntersectionPoints( rect, new KiteLine( origin, endpoint ) );

      if ( intersectingPoints.length < 2 ) {

        // line either doesn't intersect or ends inside the rectangle
        return null;
      }

      let furthestIntersectionPoint = null;
      intersectingPoints.forEach( point => {
        if ( furthestIntersectionPoint === null ||
             furthestIntersectionPoint.distance( origin ) < point.distance( origin ) ) {
          furthestIntersectionPoint = point;
        }
      } );

      return furthestIntersectionPoint;
    }

    /**
     * @param  {Rectangle} rect
     * @param  {Line} line
     * @returns {Vector2[]}
     */
    getRectangleLineIntersectionPoints( rect, line ) {

      // corners of rect
      const p = [
        new Vector2( rect.bounds.minX, rect.bounds.minY ),
        new Vector2( rect.bounds.minX, rect.bounds.maxY ),
        new Vector2( rect.bounds.maxX, rect.bounds.maxY ),
        new Vector2( rect.bounds.maxX, rect.bounds.minY )
      ];

      // perimeter lines of rect
      const lines = [];
      lines.push( new KiteLine( p[ 0 ], p[ 1 ] ) );
      lines.push( new KiteLine( p[ 1 ], p[ 2 ] ) );
      lines.push( new KiteLine( p[ 2 ], p[ 3 ] ) );
      lines.push( new KiteLine( p[ 3 ], p[ 0 ] ) );

      const intersectingPoints = [];
      lines.forEach( rectLine => {
        const intersectingPoint = this.getLineIntersection( rectLine, line );
        if ( intersectingPoint !== null ) {
          intersectingPoints.push( intersectingPoint );
        }
      } );

      return intersectingPoints;
    }
  }

  class PointAndFadeValue {

    /**
     * helper type that consolidates a point and a fade value
     * @param {Vector2} point - position
     * @param {number} fadeValue - Fade coefficient
     * @private
     */
    constructor( point, fadeValue ) {
      this.point = point;
      this.fadeValue = fadeValue;
    }
  }

  return energyFormsAndChanges.register( 'LightRayNode', LightRayNode );
} );

<|endoftext|>


<commit_before>/*global define*/
define([
        './Color',
        './ComponentDatatype',
        './defaultValue',
        './defined',
        './defineProperties',
        './DeveloperError'
    ], function(
        Color,
        ComponentDatatype,
        defaultValue,
        defined,
        defineProperties,
        DeveloperError) {
    "use strict";

    /**
     * Value and type information for per-instance geometry color.
     *
     * @alias ColorGeometryInstanceAttribute
     * @constructor
     *
     * @param {Number} [red=1.0] The red component.
     * @param {Number} [green=1.0] The green component.
     * @param {Number} [blue=1.0] The blue component.
     * @param {Number} [alpha=1.0] The alpha component.
     *
     *
     * @example
     * var instance = new Cesium.GeometryInstance({
     *   geometry : Cesium.BoxGeometry.fromDimensions({
     *     dimensions : new Cesium.Cartesian3(1000000.0, 1000000.0, 500000.0)
     *   }),
     *   modelMatrix : Cesium.Matrix4.multiplyByTranslation(Cesium.Transforms.eastNorthUpToFixedFrame(
     *     Cesium.Cartesian3.fromDegrees(0.0, 0.0)), new Cesium.Cartesian3(0.0, 0.0, 1000000.0), new Cesium.Matrix4()),
     *   id : 'box',
     *   attributes : {
     *     color : new Cesium.ColorGeometryInstanceAttribute(red, green, blue, alpha)
     *   }
     * });
     * @see GeometryInstance
     * @see GeometryInstanceAttribute
     */
    function ColorGeometryInstanceAttribute(red, green, blue, alpha) {
        red = defaultValue(red, 1.0);
        green = defaultValue(green, 1.0);
        blue = defaultValue(blue, 1.0);
        alpha = defaultValue(alpha, 1.0);

        /**
         * The values for the attributes stored in a typed array.
         *
         * @type Uint8Array
         *
         * @default [255, 255, 255, 255]
         */
        this.value = new Uint8Array([
            Color.floatToByte(red),
            Color.floatToByte(green),
            Color.floatToByte(blue),
            Color.floatToByte(alpha)
        ]);
    }

    defineProperties(ColorGeometryInstanceAttribute.prototype, {
        /**
         * The datatype of each component in the attribute, e.g., individual elements in
         * {@link ColorGeometryInstanceAttribute#value}.
         *
         * @memberof ColorGeometryInstanceAttribute.prototype
         *
         * @type {ComponentDatatype}
         * @readonly
         *
         * @default {@link ComponentDatatype.UNSIGNED_BYTE}
         */
        componentDatatype : {
            get : function() {
                return ComponentDatatype.UNSIGNED_BYTE;
            }
        },

        /**
         * The number of components in the attributes, i.e., {@link ColorGeometryInstanceAttribute#value}.
         *
         * @memberof ColorGeometryInstanceAttribute.prototype
         *
         * @type {Number}
         * @readonly
         *
         * @default 4
         */
        componentsPerAttribute : {
            get : function() {
                return 4;
            }
        },

        /**
         * When true and componentDatatype is an integer format,
         * indicate that the components should be mapped to the range [0, 1] (unsigned)
         * or [-1, 1] (signed) when they are accessed as floating-point for rendering.
         *
         * @memberof ColorGeometryInstanceAttribute.prototype
         *
         * @type {Boolean}
         * @readonly
         *
         * @default true
         */
        normalize : {
            get : function() {
                return true;
            }
        }
    });

    /**
     * Creates a new {@link ColorGeometryInstanceAttribute} instance given the provided {@link Color}.
     *
     * @param {Color} color The color.
     * @returns {ColorGeometryInstanceAttribute} The new {@link ColorGeometryInstanceAttribute} instance.
     *
     * @example
     * var instance = new Cesium.GeometryInstance({
     *   geometry : geometry,
     *   attributes : {
     *     color : Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.CORNFLOWERBLUE),
     *   }
     * });
     */
    ColorGeometryInstanceAttribute.fromColor = function(color) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(color)) {
            throw new DeveloperError('color is required.');
        }
        //>>includeEnd('debug');

        return new ColorGeometryInstanceAttribute(color.red, color.green, color.blue, color.alpha);
    };

    /**
     * Converts a color to a typed array that can be used to assign a color attribute.
     *
     * @param {Color} color The color.
     * @param {Uint8Array} [result] The array to store the result in, if undefined a new instance will be created.
     *
     * @returns {Uint8Array} The modified result parameter or a new instance if result was undefined.
     *
     * @example
     * var attributes = primitive.getGeometryInstanceAttributes('an id');
     * attributes.color = Cesium.ColorGeometryInstanceAttribute.toValue(Cesium.Color.AQUA, attributes.color);
     */
    ColorGeometryInstanceAttribute.toValue = function(color, result) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(color)) {
            throw new DeveloperError('color is required.');
        }
        //>>includeEnd('debug');

        if (!defined(result)) {
            return new Uint8Array(color.toBytes());
        }
        return color.toBytes(result);
    };

    return ColorGeometryInstanceAttribute;
});
<commit_msg>Update ColorGeometryInstanceAttribute.js<commit_after>/*global define*/
define([
        './Color',
        './ComponentDatatype',
        './defaultValue',
        './defined',
        './defineProperties',
        './DeveloperError'
    ], function(
        Color,
        ComponentDatatype,
        defaultValue,
        defined,
        defineProperties,
        DeveloperError) {
    "use strict";

    /**
     * Value and type information for per-instance geometry color.
     *
     * @alias ColorGeometryInstanceAttribute
     * @constructor
     *
     * @param {Number} [red=1.0] The red component.
     * @param {Number} [green=1.0] The green component.
     * @param {Number} [blue=1.0] The blue component.
     * @param {Number} [alpha=1.0] The alpha component.
     *
     *
     * @example
     * var instance = new Cesium.GeometryInstance({
     *   geometry : Cesium.BoxGeometry.fromDimensions({
     *     dimensions : new Cesium.Cartesian3(1000000.0, 1000000.0, 500000.0)
     *   }),
     *   modelMatrix : Cesium.Matrix4.multiplyByTranslation(Cesium.Transforms.eastNorthUpToFixedFrame(
     *     Cesium.Cartesian3.fromDegrees(0.0, 0.0)), new Cesium.Cartesian3(0.0, 0.0, 1000000.0), new Cesium.Matrix4()),
     *   id : 'box',
     *   attributes : {
     *     color : new Cesium.ColorGeometryInstanceAttribute(red, green, blue, alpha)
     *   }
     * });
     *
     * @see GeometryInstance
     * @see GeometryInstanceAttribute
     */
    function ColorGeometryInstanceAttribute(red, green, blue, alpha) {
        red = defaultValue(red, 1.0);
        green = defaultValue(green, 1.0);
        blue = defaultValue(blue, 1.0);
        alpha = defaultValue(alpha, 1.0);

        /**
         * The values for the attributes stored in a typed array.
         *
         * @type Uint8Array
         *
         * @default [255, 255, 255, 255]
         */
        this.value = new Uint8Array([
            Color.floatToByte(red),
            Color.floatToByte(green),
            Color.floatToByte(blue),
            Color.floatToByte(alpha)
        ]);
    }

    defineProperties(ColorGeometryInstanceAttribute.prototype, {
        /**
         * The datatype of each component in the attribute, e.g., individual elements in
         * {@link ColorGeometryInstanceAttribute#value}.
         *
         * @memberof ColorGeometryInstanceAttribute.prototype
         *
         * @type {ComponentDatatype}
         * @readonly
         *
         * @default {@link ComponentDatatype.UNSIGNED_BYTE}
         */
        componentDatatype : {
            get : function() {
                return ComponentDatatype.UNSIGNED_BYTE;
            }
        },

        /**
         * The number of components in the attributes, i.e., {@link ColorGeometryInstanceAttribute#value}.
         *
         * @memberof ColorGeometryInstanceAttribute.prototype
         *
         * @type {Number}
         * @readonly
         *
         * @default 4
         */
        componentsPerAttribute : {
            get : function() {
                return 4;
            }
        },

        /**
         * When true and componentDatatype is an integer format,
         * indicate that the components should be mapped to the range [0, 1] (unsigned)
         * or [-1, 1] (signed) when they are accessed as floating-point for rendering.
         *
         * @memberof ColorGeometryInstanceAttribute.prototype
         *
         * @type {Boolean}
         * @readonly
         *
         * @default true
         */
        normalize : {
            get : function() {
                return true;
            }
        }
    });

    /**
     * Creates a new {@link ColorGeometryInstanceAttribute} instance given the provided {@link Color}.
     *
     * @param {Color} color The color.
     * @returns {ColorGeometryInstanceAttribute} The new {@link ColorGeometryInstanceAttribute} instance.
     *
     * @example
     * var instance = new Cesium.GeometryInstance({
     *   geometry : geometry,
     *   attributes : {
     *     color : Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.CORNFLOWERBLUE),
     *   }
     * });
     */
    ColorGeometryInstanceAttribute.fromColor = function(color) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(color)) {
            throw new DeveloperError('color is required.');
        }
        //>>includeEnd('debug');

        return new ColorGeometryInstanceAttribute(color.red, color.green, color.blue, color.alpha);
    };

    /**
     * Converts a color to a typed array that can be used to assign a color attribute.
     *
     * @param {Color} color The color.
     * @param {Uint8Array} [result] The array to store the result in, if undefined a new instance will be created.
     *
     * @returns {Uint8Array} The modified result parameter or a new instance if result was undefined.
     *
     * @example
     * var attributes = primitive.getGeometryInstanceAttributes('an id');
     * attributes.color = Cesium.ColorGeometryInstanceAttribute.toValue(Cesium.Color.AQUA, attributes.color);
     */
    ColorGeometryInstanceAttribute.toValue = function(color, result) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(color)) {
            throw new DeveloperError('color is required.');
        }
        //>>includeEnd('debug');

        if (!defined(result)) {
            return new Uint8Array(color.toBytes());
        }
        return color.toBytes(result);
    };

    return ColorGeometryInstanceAttribute;
});
<|endoftext|>