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

/**
 * Creates a composite report of all of the 3rd party images, code, audio and other media used by all of the published
 * PhET Simulations. The reports is published at: https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
 *
 * Usage:
 * grunt report-third-party
 * // then push the updated license file to sherpa
 *
 * Third party entries are parsed from the HTML files for all simulations published on the PhET website.
 * See getLicenseEntry.js for documentation of the fields in the entries.
 *
 * @author Sam Reid
 */

// Load shared constants
var ChipperConstants = require( '../../../chipper/js/common/ChipperConstants' );
var ChipperStringUtils = require( '../../../chipper/js/common/ChipperStringUtils' );
var assert = require( 'assert' );

// constants
var SHERPA = '../sherpa';  // The relative path to sherpa, from the chipper path

// node modules
var fs = require( 'fs' );
var http = require( 'http' );

var _ = require( '../../' + SHERPA + '/lib/lodash-2.4.1.min' ); // eslint-disable-line require-statement-match

/**
 * @param grunt - the grunt instance
 */
module.exports = function( grunt ) {
  'use strict';

  // The file where the report will be written
  var outputFilename = SHERPA + '/third-party-licenses.md';

  // Since we download the HTML files, this task must wait until it is complete
  var gruntDone = grunt.task.current.async();

  // Prepare a directory where the HTML files will be downloaded.
  // This directory should have been deleted at the end of the last task, but this could be useful if the task was
  // interrupted before cleanup.
  if ( grunt.file.exists( 'downloaded-sims' ) ) {
    grunt.file.delete( 'downloaded-sims' );
  }
  grunt.file.mkdir( 'downloaded-sims' );

  // Download a URL to a local file
  // http://stackoverflow.com/questions/11944932/how-to-download-a-file-with-node-js-without-using-third-party-libraries
  var download = function( url, dest, cb ) {
    var file = fs.createWriteStream( dest );
    http.get( url, function( response ) {
      response.pipe( file );
      file.on( 'finish', function() {
        file.close( cb );  // close() is async, call cb after close completes.
      } );
    } ).on( 'error', function( err ) { // Handle errors
      fs.unlink( dest ); // Delete the file async. (But we don't check the result)
      if ( cb ) {
        cb( err.message );
      }
    } );
  };

  // After the downloads are complete, write the report based on the metadata in the files.
  var writeReport = function() {

    // Aggregate results for each of the license types
    var compositeCode = {};
    var compositeMedia = {};

    // List of all of the repository names, so that we can detect which libraries are used by all-sims
    var simTitles = [];

    /**
     * Given an HTML text, find the title attribute by parsing for <title>
     * @param html
     * @returns {string}
     */
    var parseTitle = function( html ) {
      var startKey = '<title>';
      var endKey = '</title>';

      var startIndex = html.indexOf( startKey );
      var endIndex = html.indexOf( endKey );

      return html.substring( startIndex + startKey.length, endIndex ).trim();
    };

    /**
     * Add the source (images/audio/media or code) entries to the destination object, keyed by name.
     * @param {string} repositoryName - the name of the repository, such as 'energy-skate-park-basics'
     * @param {Object} source - the object from which to read the entry
     * @param {Object} destination - the object to which to append the entry
     */
    var augment = function( repositoryName, source, destination ) {
      for ( var entry in source ) {
        if ( source.hasOwnProperty( entry ) ) {
          if ( !destination.hasOwnProperty( entry ) ) {
            destination[ entry ] = source[ entry ];//overwrites
            destination[ entry ].usedBy = [];
          }
          destination[ entry ].usedBy.push( repositoryName );
        }
      }
    };

    grunt.file.recurse( 'downloaded-sims', function( abspath, rootdir, subdir, filename ) {
      if ( ChipperStringUtils.endsWith( filename.toLowerCase(), '.html' ) ) {

        // load the file into a string
        var html = grunt.file.read( abspath ).trim();

        // If you request a page that doesn't exist, Apache returns HTML that says the page "was not found on this server"
        // Since we are looping through all active-sims, there are likely to be many simulations for which published
        // HTML is not available.  This is not an error, it is simply how we can differentiate between active-sims and
        // published sims.
        if ( html.indexOf( 'was not found on this server' ) < 0 || html.indexOf( 'was not found on this server' ) > 400 ) {

          var startIndex = html.indexOf( ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES );
          var endIndex = html.indexOf( ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES );
          var substring = html.substring( startIndex, endIndex );

          var firstCurlyBrace = substring.indexOf( '{' );
          var lastCurlyBrace = substring.lastIndexOf( '}' );
          var jsonString = substring.substring( firstCurlyBrace, lastCurlyBrace + 1 );

          var json = JSON.parse( jsonString );

          var title = parseTitle( html );
          if ( !title || title.indexOf( 'undefined' ) === 0 || title.indexOf( 'TITLE' ) >= 0 ) {
            grunt.log.writeln( 'title not found for ' + abspath );
            title = filename;
          }
          augment( title, json.lib, compositeCode );
          augment( title, json.audio, compositeMedia );
          augment( title, json.images, compositeMedia );

          simTitles.push( title );
        }
      }
    } );

    // Sort to easily compare lists of repositoryNames with usedBy columns, to see which resources are used by everything.
    simTitles.sort();

    // If anything is used by every sim indicate that here
    for ( var entry in compositeCode ) {
      if ( compositeCode.hasOwnProperty( entry ) ) {
        compositeCode[ entry ].usedBy.sort();
        if ( _.isEqual( simTitles, compositeCode[ entry ].usedBy ) ) {
          compositeCode[ entry ].usedBy = 'all-sims';// Confusing that this matches a repo name, but since that repo isn't actually a sim, perhaps this will be ok
        }
      }
    }

    var licenseJSON = grunt.file.readJSON( SHERPA + '/lib/license.json' );

    var codeOutput = [];
    var codeLicensesUsed = [];
    var mediaLicensesUsed = [];

    // Get a list of the library names
    var libraryNames = [];
    for ( var lib in licenseJSON ) {
      if ( licenseJSON.hasOwnProperty( lib ) ) {
        libraryNames.push( lib );
      }
    }

    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    libraryNames.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Add info for each library to the MD report
    for ( var i = 0; i < libraryNames.length; i++ ) {
      var library = libraryNames[ i ];

      var lineElementsForLibrary = [
        '**' + library + '**',
        licenseJSON[ library ].text.join( '
' ),
        licenseJSON[ library ].projectURL,
        'License: [' + licenseJSON[ library ].license + '](licenses/' + library + '.txt)',
        'Notes: ' + licenseJSON[ library ].notes
      ];

      if ( licenseJSON[ library ].dependencies ) {
        lineElementsForLibrary.push( 'Dependencies: **' + licenseJSON[ library ].dependencies + '**' );
      }

      if ( compositeCode.hasOwnProperty( library ) && compositeCode[ library ].usedBy instanceof Array ) {
        lineElementsForLibrary.push( 'Used by: ' + compositeCode[ library ].usedBy.join( ', ' ) );
      }

      // \n worked well when viewing GitHub markdown as an issue comment, but for unknown reasons
 is necessary when
      // viewing from https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
      codeOutput.push( lineElementsForLibrary.join( '
' ) );

      if ( codeLicensesUsed.indexOf( licenseJSON[ library ].license ) < 0 ) {
        codeLicensesUsed.push( licenseJSON[ library ].license );
      }
    }

    var mediaOutput = [];
    var mediaKeys = [];
    for ( var imageAudioEntry in compositeMedia ) {
      if ( compositeMedia.hasOwnProperty( imageAudioEntry ) ) {
        mediaKeys.push( imageAudioEntry );
      }
    }
    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    mediaKeys.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Create the text for the image and audio, and keep track of which licenses were used by them.
    for ( i = 0; i < mediaKeys.length; i++ ) {
      var mediaKey = mediaKeys[ i ];

      var text = compositeMedia[ mediaKey ].text.join( '
' ).trim();
      var projectURL = compositeMedia[ mediaKey ].projectURL.trim();

      if ( text.length === 0 ) {
        text = '(no text)';
      }

      if ( projectURL.length === 0 ) {
        projectURL = '(no project url)';
      }

      var notes = compositeMedia[ mediaKey ].notes.trim();
      if ( notes.length === 0 ) {
        notes = '(no notes)';
      }

      var license = compositeMedia[ mediaKey ].license.trim();
      assert && assert( license.length > 0, 'All media entries must have a license' );

      var mediaEntryLines = [
        '**' + mediaKey + '**',
        text,
        projectURL,
        'License: ' + license,
        'Notes: ' + notes
      ];

      // PhET has temporarily chosen to publish John Travoltage with incompatible licenses, so the reasons for
      // making the exceptions are noted here.  The new artwork is being developed now and the simulation
      // will be republished without exception cases soon.
      // This code will remain in case we have other exception cases in the future.
      if ( compositeMedia[ mediaKey ].exception ) {
        mediaEntryLines.push( 'Exception: ' + compositeMedia[ mediaKey ].exception );
      }

      // TODO: is this a real license?  Why is there logic here but nowhere else?  Perhaps files annotated with
      // contact phethelp@colorado.edu are leaking into the HTML files unnecessarily
      if ( license !== 'contact phethelp@colorado.edu' ) {
        mediaOutput.push( mediaEntryLines.join( '
' ) );

        if ( mediaLicensesUsed.indexOf( license ) < 0 ) {
          mediaLicensesUsed.push( license );
        }
      }
    }

    // Summarize licenses used
    var fileList = simTitles.join( '\n* ' );

    var outputString =
      'This report enumerates the third-party resources (code, images, audio, etc) used in a set of simulations.\n' +
      '* [Third-party Code](#third-party-code)\n' +
      '* [Third-party Code License Summary](#third-party-code-license-summary)\n' +
      '* [Third-party Media](#third-party-media)\n' +
      '* [Third-party Media License Summary](#third-party-media-license-summary)\n' +
      '\n' +
      'This report is for the following simulations: \n\n* ' + fileList + '\n\nTo see the third party resources used in a particular published ' +
      'simulation, inspect the HTML file between the `' + ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES + '` and `' + ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES + '` ' +
      '(only exists in sim publications after Aug 7, 2015).\n' +
      '# Third-party Code:
\n' +
      codeOutput.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Code License Summary:
\n' +
      codeLicensesUsed.join( '
' ) + '\n\n' +

      '---\n' +

      '# Third-party Media:
\n' +
      mediaOutput.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Media License Summary:
\n' +
      mediaLicensesUsed.join( '
' ) + '\n\n';

    // Compare the file output to the existing file, and write & git commit only if different
    if ( !grunt.file.exists( outputFilename ) || grunt.file.read( outputFilename ) !== outputString ) {
      grunt.log.writeln( 'File output changed, writing file ' + outputFilename );
      grunt.file.write( outputFilename, outputString );
    }
    else {
      grunt.log.writeln( outputFilename + ' contents are the same.  No need to save.' );
    }

    // Delete the temporarily downloaded files when task complete
    if ( grunt.file.exists( 'downloaded-sims' ) ) {
      grunt.file.delete( 'downloaded-sims' );
    }

    gruntDone();
  };

  // Download all of the simulations.  For simulations that are in "active-sims" but not published,
  // download them anyways but skip them in the report.
  var activeSimsString = fs.readFileSync( '../chipper/data/active-sims', 'utf-8' ).trim();
  var activeSimsArray = activeSimsString.split( '\n' );

  // Download files one at a time so we can make sure we get everything.
  var downloadNext = function( index ) {
    var sim = activeSimsArray[ index ];

    var url = 'http://phet.colorado.edu/sims/html/' + sim + '/latest/' + sim + '_en.html';
    console.log( 'downloading ' + (index + 1) + '/' + activeSimsArray.length + ': ' + url );

    download( url, 'downloaded-sims/' + sim + '_en.html', function( err ) {
      assert && assert( !err, 'Error during download: ' + err );

      var next = index + 1;

      if ( next < activeSimsArray.length ) {
        downloadNext( next );
      }
      else {
        writeReport();
      }
    } );
  };

  downloadNext( 0 );
};<commit_msg>3rd party script now uploads data to website phetsims/sherpa#67<commit_after>// Copyright 2015-2016, University of Colorado Boulder

/**
 * Creates a composite report of all of the 3rd party images, code, audio and other media used by all of the published
 * PhET Simulations. The reports is published at: https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
 *
 * Usage:
 * grunt report-third-party
 * // then push the updated license file to sherpa
 *
 * Third party entries are parsed from the HTML files for all simulations published on the PhET website.
 * See getLicenseEntry.js for documentation of the fields in the entries.
 *
 * @author Sam Reid
 */

// Load shared constants
var ChipperConstants = require( '../../../chipper/js/common/ChipperConstants' );
var ChipperStringUtils = require( '../../../chipper/js/common/ChipperStringUtils' );
var assert = require( 'assert' );

// constants
var SHERPA = '../sherpa';  // The relative path to sherpa, from the chipper path

// node modules
var fs = require( 'fs' );
var https = require( 'https' );

var _ = require( '../../' + SHERPA + '/lib/lodash-2.4.1.min' ); // eslint-disable-line require-statement-match

// read configuration file - required to write to website database
var BUILD_LOCAL_FILENAME = process.env.HOME + '/.phet/build-local.json';
var buildLocalJSON = JSON.parse( fs.readFileSync( BUILD_LOCAL_FILENAME, { encoding: 'utf-8' } ) );

/**
 * @param grunt - the grunt instance
 */
module.exports = function( grunt ) {
  'use strict';

  // The file where the report will be written
  var outputFilename = SHERPA + '/third-party-licenses.md';

  // Since we download the HTML files, this task must wait until it is complete
  var gruntDone = grunt.task.current.async();

  // Prepare a directory where the HTML files will be downloaded.
  // This directory should have been deleted at the end of the last task, but this could be useful if the task was
  // interrupted before cleanup.
  if ( grunt.file.exists( 'downloaded-sims' ) ) {
    grunt.file.delete( 'downloaded-sims' );
  }
  grunt.file.mkdir( 'downloaded-sims' );

  // Download a URL to a local file
  // http://stackoverflow.com/questions/11944932/how-to-download-a-file-with-node-js-without-using-third-party-libraries
  var download = function( url, dest, cb ) {
    var file = fs.createWriteStream( dest );
    https.get( url, function( response ) {
      response.pipe( file );
      file.on( 'finish', function() {
        file.close( cb );  // close() is async, call cb after close completes.
      } );
    } ).on( 'error', function( err ) { // Handle errors
      fs.unlink( dest ); // Delete the file async. (But we don't check the result)
      if ( cb ) {
        cb( err.message );
      }
    } );
  };

  // After the downloads are complete, write the report based on the metadata in the files.
  var writeReport = function() {

    // Aggregate results for each of the license types
    var compositeCode = {};
    var compositeMedia = {};

    // List of all of the repository names, so that we can detect which libraries are used by all-sims
    var simTitles = [];

    // List of libraries for each sim
    // Type: string in JSON format
    var simLibraries = '{';

    /**
     * Given an HTML text, find the title attribute by parsing for <title>
     * @param html
     * @returns {string}
     */
    var parseTitle = function( html ) {
      var startKey = '<title>';
      var endKey = '</title>';

      var startIndex = html.indexOf( startKey );
      var endIndex = html.indexOf( endKey );

      return html.substring( startIndex + startKey.length, endIndex ).trim();
    };

    /**
     * Add the source (images/audio/media or code) entries to the destination object, keyed by name.
     * @param {string} repositoryName - the name of the repository, such as 'energy-skate-park-basics'
     * @param {Object} source - the object from which to read the entry
     * @param {Object} destination - the object to which to append the entry
     */
    var augment = function( repositoryName, source, destination ) {
      for ( var entry in source ) {
        if ( source.hasOwnProperty( entry ) ) {
          if ( !destination.hasOwnProperty( entry ) ) {
            destination[ entry ] = source[ entry ];//overwrites
            destination[ entry ].usedBy = [];
          }
          destination[ entry ].usedBy.push( repositoryName );
        }
      }
    };

    grunt.file.recurse( 'downloaded-sims', function( abspath, rootdir, subdir, filename ) {
      if ( ChipperStringUtils.endsWith( filename.toLowerCase(), '.html' ) ) {

        // load the file into a string
        var html = grunt.file.read( abspath ).trim();

        // If you request a page that doesn't exist, Apache returns HTML that says the page "was not found on this server"
        // Since we are looping through all active-sims, there are likely to be many simulations for which published
        // HTML is not available.  This is not an error, it is simply how we can differentiate between active-sims and
        // published sims.
        // The request may be redirected to the PhET Website 404 page instead of the default Apache page in some
        // circumstances, which contains the text: "The requested page could not be found."
        if ( (html.indexOf( 'was not found on this server' ) < 0 || html.indexOf( 'was not found on this server' ) > 400)
              && html.indexOf( 'The requested page could not be found.' ) === -1 ) {

          var startIndex = html.indexOf( ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES );
          var endIndex = html.indexOf( ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES );
          var substring = html.substring( startIndex, endIndex );

          var firstCurlyBrace = substring.indexOf( '{' );
          var lastCurlyBrace = substring.lastIndexOf( '}' );
          var jsonString = substring.substring( firstCurlyBrace, lastCurlyBrace + 1 );

          var json = JSON.parse( jsonString );

          var title = parseTitle( html );
          if ( !title || title.indexOf( 'undefined' ) === 0 || title.indexOf( 'TITLE' ) >= 0 ) {
            grunt.log.writeln( 'title not found for ' + abspath );
            title = filename;
          }
          augment( title, json.lib, compositeCode );
          augment( title, json.audio, compositeMedia );
          augment( title, json.images, compositeMedia );

          simTitles.push( title );

          // Get the name of the sim as referenced in the website database out of the filename
          var hyphenatedTitle = filename.substring( 0, filename.indexOf( '_' ) );

          // Concatenate all the libraries for this sim with html newline.
          var libString = '';
          for ( var entry in json.lib ) {
            libString += entry + '
';
          }

          //  Update the object to be pushed to the website database
          simLibraries += '"' + hyphenatedTitle + '":"' + libString + '",';
        }
      }
    } );

    simLibraries = simLibraries.slice( 0, simLibraries.length - 1 ) + '}';

    // POST the library data to the website database
    if ( buildLocalJSON && buildLocalJSON.websiteAuthorizationCode ) {

      // Semaphore for receipt of http response.  If we call gruntDone() before it exists the database query will fail silently.
      var databaseResponseSemaphore;

      var options = {
        host: 'phet.colorado.edu',
        path: '/services/add-simulation-libraries',
        method: 'POST',
        auth: 'token:' + buildLocalJSON.websiteAuthorizationCode,
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Content-Length': Buffer.byteLength( simLibraries )
        }
      };
      var databaseRequest = https.request( options, function( res ) {
        databaseResponseSemaphore = res;
      } );
      databaseRequest.on( 'error', function( e ) {
        grunt.log.writeln( 'There was a problem uploading the data to the website: ' + e.message );
        databaseResponseSemaphore = e.message;
      } );

      // write data to request body
      databaseRequest.write( simLibraries );

      databaseRequest.end();
    }
    else {
      grunt.log.writeln( 'build-local.json is missing the value "websiteAuthorizationCode". ' +
        'The third party information was not uploaded to the website.  See a web developer for details.'
      );
    }

    // Sort to easily compare lists of repositoryNames with usedBy columns, to see which resources are used by everything.
    simTitles.sort();

    // If anything is used by every sim indicate that here
    for ( var entry in compositeCode ) {
      if ( compositeCode.hasOwnProperty( entry ) ) {
        compositeCode[ entry ].usedBy.sort();
        if ( _.isEqual( simTitles, compositeCode[ entry ].usedBy ) ) {
          compositeCode[ entry ].usedBy = 'all-sims';// Confusing that this matches a repo name, but since that repo isn't actually a sim, perhaps this will be ok
        }
      }
    }

    var licenseJSON = grunt.file.readJSON( SHERPA + '/lib/license.json' );

    var codeOutput = [];
    var codeLicensesUsed = [];
    var mediaLicensesUsed = [];

    // Get a list of the library names
    var libraryNames = [];
    for ( var lib in licenseJSON ) {
      if ( licenseJSON.hasOwnProperty( lib ) ) {
        libraryNames.push( lib );
      }
    }

    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    libraryNames.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Add info for each library to the MD report
    for ( var i = 0; i < libraryNames.length; i++ ) {
      var library = libraryNames[ i ];

      var lineElementsForLibrary = [
        '**' + library + '**',
        licenseJSON[ library ].text.join( '
' ),
        licenseJSON[ library ].projectURL,
        'License: [' + licenseJSON[ library ].license + '](licenses/' + library + '.txt)',
        'Notes: ' + licenseJSON[ library ].notes
      ];

      if ( licenseJSON[ library ].dependencies ) {
        lineElementsForLibrary.push( 'Dependencies: **' + licenseJSON[ library ].dependencies + '**' );
      }

      if ( compositeCode.hasOwnProperty( library ) && compositeCode[ library ].usedBy instanceof Array ) {
        lineElementsForLibrary.push( 'Used by: ' + compositeCode[ library ].usedBy.join( ', ' ) );
      }

      // \n worked well when viewing GitHub markdown as an issue comment, but for unknown reasons
 is necessary when
      // viewing from https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
      codeOutput.push( lineElementsForLibrary.join( '
' ) );

      if ( codeLicensesUsed.indexOf( licenseJSON[ library ].license ) < 0 ) {
        codeLicensesUsed.push( licenseJSON[ library ].license );
      }
    }

    var mediaOutput = [];
    var mediaKeys = [];
    for ( var imageAudioEntry in compositeMedia ) {
      if ( compositeMedia.hasOwnProperty( imageAudioEntry ) ) {
        mediaKeys.push( imageAudioEntry );
      }
    }
    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    mediaKeys.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Create the text for the image and audio, and keep track of which licenses were used by them.
    for ( i = 0; i < mediaKeys.length; i++ ) {
      var mediaKey = mediaKeys[ i ];

      var text = compositeMedia[ mediaKey ].text.join( '
' ).trim();
      var projectURL = compositeMedia[ mediaKey ].projectURL.trim();

      if ( text.length === 0 ) {
        text = '(no text)';
      }

      if ( projectURL.length === 0 ) {
        projectURL = '(no project url)';
      }

      var notes = compositeMedia[ mediaKey ].notes.trim();
      if ( notes.length === 0 ) {
        notes = '(no notes)';
      }

      var license = compositeMedia[ mediaKey ].license.trim();
      assert && assert( license.length > 0, 'All media entries must have a license' );

      var mediaEntryLines = [
        '**' + mediaKey + '**',
        text,
        projectURL,
        'License: ' + license,
        'Notes: ' + notes
      ];

      // PhET has temporarily chosen to publish John Travoltage with incompatible licenses, so the reasons for
      // making the exceptions are noted here.  The new artwork is being developed now and the simulation
      // will be republished without exception cases soon.
      // This code will remain in case we have other exception cases in the future.
      if ( compositeMedia[ mediaKey ].exception ) {
        mediaEntryLines.push( 'Exception: ' + compositeMedia[ mediaKey ].exception );
      }

      // TODO: is this a real license?  Why is there logic here but nowhere else?  Perhaps files annotated with
      // contact phethelp@colorado.edu are leaking into the HTML files unnecessarily
      if ( license !== 'contact phethelp@colorado.edu' ) {
        mediaOutput.push( mediaEntryLines.join( '
' ) );

        if ( mediaLicensesUsed.indexOf( license ) < 0 ) {
          mediaLicensesUsed.push( license );
        }
      }
    }

    // Summarize licenses used
    var fileList = simTitles.join( '\n* ' );

    var outputString =
      'This report enumerates the third-party resources (code, images, audio, etc) used in a set of simulations.\n' +
      '* [Third-party Code](#third-party-code)\n' +
      '* [Third-party Code License Summary](#third-party-code-license-summary)\n' +
      '* [Third-party Media](#third-party-media)\n' +
      '* [Third-party Media License Summary](#third-party-media-license-summary)\n' +
      '\n' +
      'This report is for the following simulations: \n\n* ' + fileList + '\n\nTo see the third party resources used in a particular published ' +
      'simulation, inspect the HTML file between the `' + ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES + '` and `' + ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES + '` ' +
      '(only exists in sim publications after Aug 7, 2015).\n' +
      '# Third-party Code:
\n' +
      codeOutput.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Code License Summary:
\n' +
      codeLicensesUsed.join( '
' ) + '\n\n' +

      '---\n' +

      '# Third-party Media:
\n' +
      mediaOutput.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Media License Summary:
\n' +
      mediaLicensesUsed.join( '
' ) + '\n\n';

    // Compare the file output to the existing file, and write & git commit only if different
    if ( !grunt.file.exists( outputFilename ) || grunt.file.read( outputFilename ) !== outputString ) {
      grunt.log.writeln( 'File output changed, writing file ' + outputFilename );
      grunt.file.write( outputFilename, outputString );
    }
    else {
      grunt.log.writeln( outputFilename + ' contents are the same.  No need to save.' );
    }

    // Delete the temporarily downloaded files when task complete
    if ( grunt.file.exists( 'downloaded-sims' ) ) {
      grunt.file.delete( 'downloaded-sims', { force: true } );
    }

    if ( databaseRequest ) {
      // If we sent a request to the website, wait for the file upload to finish or fail.
      grunt.log.writeln( 'Uploading library information to wesite database ...' );
      var interval = setInterval( function () {
        if ( databaseResponseSemaphore ) {
          clearInterval( interval );
          grunt.log.writeln( 'Upload finished.' );
          gruntDone();
        }
      }, 1000 );
    }
    else {
      gruntDone();
    }
  };

  // Download all of the simulations.  For simulations that are in "active-sims" but not published,
  // download them anyways but skip them in the report.
  var activeSimsString = fs.readFileSync( '../chipper/data/active-sims', 'utf-8' ).trim();
  var activeSimsArray = activeSimsString.split( '\n' );

  // Download files one at a time so we can make sure we get everything.
  var downloadNext = function( index ) {
    var sim = activeSimsArray[ index ].trim();
    var url = 'https://phet.colorado.edu/sims/html/' + sim + '/latest/' + sim + '_en.html';
    console.log( 'downloading ' + (index + 1) + '/' + activeSimsArray.length + ': ' + url );
    download( url, 'downloaded-sims/' + sim + '_en.html', function( err ) {
      assert && assert( !err, 'Error during download: ' + err );

      var next = index + 1;

      if ( next < activeSimsArray.length ) {
        downloadNext( next );
      }
      else {
        writeReport();
      }
    } );
  };

  downloadNext( 0 );
};<|endoftext|>


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

/**
 * Creates a composite report of all of the 3rd party images, code, audio and other media used by all of the published
 * PhET Simulations. The reports is published at: https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
 *
 * Usage:
 * cd chipper
 * grunt report-third-party
 * // then push the updated license file to sherpa
 *
 * Third party entries are parsed from the HTML files for all simulations published on the PhET website, see
 * getLicenseEntry.js
 *
 * @author Sam Reid
 */

// Load shared constants
var ChipperConstants = require( '../../../chipper/js/common/ChipperConstants' );
var ChipperStringUtils = require( '../../../chipper/js/common/ChipperStringUtils' );
var assert = require( 'assert' );

// constants
var SHERPA = '../sherpa';  // The relative path to sherpa, from the chipper path

// node modules
var fs = require( 'fs' );
var http = require( 'http' );

var _ = require( '../../' + SHERPA + '/lib/lodash-2.4.1.min' ); // eslint-disable-line require-statement-match

/**
 * @param grunt - the grunt instance
 */
module.exports = function( grunt ) {
  'use strict';

  // The file where the report will be written
  var output = SHERPA + '/third-party-licenses.md';

  // Since we download the HTML files, this task must wait until it is complete
  var gruntDone = grunt.task.current.async();

  // Prepare a directory where the HTML files will be downloaded
  if ( grunt.file.exists( 'downloaded-sims' ) ) {
    grunt.file.delete( 'downloaded-sims' );
  }
  grunt.file.mkdir( 'downloaded-sims' );

  // Download a URL to a local file
  // http://stackoverflow.com/questions/11944932/how-to-download-a-file-with-node-js-without-using-third-party-libraries
  var download = function( url, dest, cb ) {
    var file = fs.createWriteStream( dest );
    http.get( url, function( response ) {
      response.pipe( file );
      file.on( 'finish', function() {
        file.close( cb );  // close() is async, call cb after close completes.
      } );
    } ).on( 'error', function( err ) { // Handle errors
      fs.unlink( dest ); // Delete the file async. (But we don't check the result)
      if ( cb ) {
        cb( err.message );
      }
    } );
  };

  // After the downloads are complete, write the report based on the metadata in the files.
  var writeReport = function() {

    // Aggregate results for each of the license types
    var compositeCode = {};
    var compositeMedia = {};

    // List of all of the repository names, so that we can detect which libraries are used by all-sims
    var simTitles = [];

    /**
     * Given an HTML text, find the title attribute by parsing for <title>
     * @param html
     * @returns {string}
     */
    var parseTitle = function( html ) {
      var startKey = '<title>';
      var endKey = '</title>';

      var startIndex = html.indexOf( startKey );
      var endIndex = html.indexOf( endKey );

      return html.substring( startIndex + startKey.length, endIndex ).trim();
    };

    /**
     * Add the source (images/audio/media or code) entries to the destination object, keyed by name.
     * @param {string} repositoryName - the name of the repository, such as 'energy-skate-park-basics'
     * @param {Object} source - the object from which to read the entry
     * @param {Object} destination - the object to which to append the entry
     */
    var augment = function( repositoryName, source, destination ) {
      for ( var entry in source ) {
        if ( source.hasOwnProperty( entry ) ) {
          if ( !destination.hasOwnProperty( entry ) ) {
            destination[ entry ] = source[ entry ];//overwrites
            destination[ entry ].usedBy = [];
          }
          destination[ entry ].usedBy.push( repositoryName );
        }
      }
    };

    grunt.file.recurse( 'downloaded-sims', function( abspath, rootdir, subdir, filename ) {
      if ( ChipperStringUtils.endsWith( filename.toLowerCase(), '.html' ) ) {

        // load the file into a string
        var html = grunt.file.read( abspath ).trim();

        if ( html.indexOf( 'was not found on this server' ) < 0 || html.indexOf( 'was not found on this server' ) > 400 ) {

          var startIndex = html.indexOf( ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES );
          var endIndex = html.indexOf( ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES );
          var substring = html.substring( startIndex, endIndex );

          var firstCurlyBrace = substring.indexOf( '{' );
          var lastCurlyBrace = substring.lastIndexOf( '}' );
          var jsonString = substring.substring( firstCurlyBrace, lastCurlyBrace + 1 );

          var json = JSON.parse( jsonString );

          var title = parseTitle( html );
          if ( !title || title.indexOf( 'undefined' ) === 0 || title.indexOf( 'TITLE' ) >= 0 ) {
            grunt.log.writeln( 'title not found for ' + abspath );
            title = filename;
          }
          augment( title, json.lib, compositeCode );
          augment( title, json.audio, compositeMedia );
          augment( title, json.images, compositeMedia );

          simTitles.push( title );
        }
      }
    } );

    // Sort to easily compare lists of repositoryNames with usedBy columns, to see which resources are used by everything.
    simTitles.sort();

    // If anything is used by every sim indicate that here
    for ( var entry in compositeCode ) {
      if ( compositeCode.hasOwnProperty( entry ) ) {
        compositeCode[ entry ].usedBy.sort();
        if ( _.isEqual( simTitles, compositeCode[ entry ].usedBy ) ) {
          compositeCode[ entry ].usedBy = 'all-sims';// Confusing that this matches a repo name, but since that repo isn't actually a sim, perhaps this will be ok
        }
      }
    }

    var json = grunt.file.readJSON( SHERPA + '/lib/license.json' );

    var entries = [];
    var codeLicensesUsed = [];
    var mediaLicensesUsed = [];

    // Get a list of the library names
    var libraries = [];
    for ( var lib in json ) {
      if ( json.hasOwnProperty( lib ) ) {
        libraries.push( lib );
      }
    }

    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    libraries.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Add info for each library to the MD report
    for ( var i = 0; i < libraries.length; i++ ) {
      var library = libraries[ i ];

      var lines = [
        '**' + library + '**',
        json[ library ].text.join( '
' ),
        json[ library ].projectURL,
        'License: [' + json[ library ].license + '](licenses/' + library + '.txt)',
        'Notes: ' + json[ library ].notes
      ];

      if ( json[ library ].dependencies ) {
        lines.push( 'Dependencies: **' + json[ library ].dependencies + '**' );
      }

      if ( compositeCode.hasOwnProperty( library ) && compositeCode[ library ].usedBy instanceof Array ) {
        lines.push( 'Used by: ' + compositeCode[ library ].usedBy.join( ', ' ) );
      }

      // \n worked well when viewing GitHub markdown as an issue comment, but for unknown reasons
 is necessary when
      // viewing from https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
      entries.push( lines.join( '
' ) );

      if ( codeLicensesUsed.indexOf( json[ library ].license ) < 0 ) {
        codeLicensesUsed.push( json[ library ].license );
      }
    }

    var mediaOutput = [];
    var mediaKeys = [];
    for ( var imageAudioEntry in compositeMedia ) {
      if ( compositeMedia.hasOwnProperty( imageAudioEntry ) ) {
        mediaKeys.push( imageAudioEntry );
      }
    }
    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    mediaKeys.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Create the text for the image and audio, and keep track of which licenses were used by them.
    for ( i = 0; i < mediaKeys.length; i++ ) {
      var mediaKey = mediaKeys[ i ];

      var copyrightStatement = compositeMedia[ mediaKey ].text.join( '
' ).trim();
      var projectURL = compositeMedia[ mediaKey ].projectURL.trim();

      if ( copyrightStatement.length === 0 ) {
        copyrightStatement = '(no text)';
      }

      if ( projectURL.length === 0 ) {
        projectURL = '(no project url)';
      }

      var notes = compositeMedia[ mediaKey ].notes.trim();
      if ( notes.length === 0 ) {
        notes = '(no notes)';
      }

      var license = compositeMedia[ mediaKey ].license.trim();
      if ( license.length === 0 ) {
        license = '(no license)';
      }

      var mediaEntryLines = [
        '**' + mediaKey + '**',
        copyrightStatement,
        projectURL,
        'License: ' + license,
        'Notes: ' + notes
      ];
      if ( compositeMedia[ mediaKey ].exception ) {
        mediaEntryLines.push( 'Exception: ' + compositeMedia[ mediaKey ].exception );
      }

      // TODO: is this a real license?  Why is there logic here but nowhere else?  Perhaps files annotated with
      // contact phethelp@colorado.edu are leaking into the HTML files unnecessarily
      if ( license !== 'contact phethelp@colorado.edu' ) {
        mediaOutput.push( mediaEntryLines.join( '
' ) );

        if ( mediaLicensesUsed.indexOf( license ) < 0 ) {
          mediaLicensesUsed.push( license );
        }
      }
    }

    // Summarize licenses used
    var fileList = simTitles.join( '\n* ' );

    var outputString =
      'This report enumerates the third-party resources (code, images, audio, etc) used in a set of simulations.\n' +
      '* [Third-party Code](#third-party-code)\n' +
      '* [Third-party Code License Summary](#third-party-code-license-summary)\n' +
      '* [Third-party Media](#third-party-media)\n' +
      '* [Third-party Media License Summary](#third-party-media-license-summary)\n' +
      '\n' +
      'This report is for the following simulations: \n\n* ' + fileList + '\n\nTo see the third party resources used in a particular published ' +
      'simulation, inspect the HTML file between the `' + ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES + '` and `' + ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES + '` ' +
      '(only exists in sim publications after Aug 7, 2015).\n' +
      '# Third-party Code:
\n' +
      entries.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Code License Summary:
\n' +
      codeLicensesUsed.join( '
' ) + '\n\n' +

      '---\n' +

      '# Third-party Media:
\n' +
      mediaOutput.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Media License Summary:
\n' +
      mediaLicensesUsed.join( '
' ) + '\n\n';

    // Compare the file output to the existing file, and write & git commit only if different
    if ( !grunt.file.exists( output ) || grunt.file.read( output ) !== outputString ) {
      grunt.log.writeln( 'File output changed, writing file ' + output );
      grunt.file.write( output, outputString );
    }
    else {
      grunt.log.writeln( output + ' contents are the same.  No need to save.' );
    }

    gruntDone();
  };

  // Download all of the simulations.  For simulations that are in "active-sims" but not published,
  // download them anyways but skip them in the report.
  var activeSimsString = fs.readFileSync( '../chipper/data/active-sims', 'utf-8' ).trim();
  var activeSimsArray = activeSimsString.split( '\n' );

  // Download files one at a time so we can make sure we get everything.
  var downloadNext = function( index ) {
    var sim = activeSimsArray[ index ];

    var url = 'http://phet.colorado.edu/sims/html/' + sim + '/latest/' + sim + '_en.html';
    console.log( 'downloading ' + (index + 1) + '/' + activeSimsArray.length + ': ' + url );

    download( url, 'downloaded-sims/' + sim + '_en.html', function( err ) {
      assert && assert( !err, 'Error during download: ' + err );

      var next = index + 1;

      if ( next < activeSimsArray.length ) {
        downloadNext( next );
      }
      else {
        writeReport();
      }
    } );
  };

  downloadNext( 0 );
};<commit_msg>Remove downloaded files when task complete, see https://github.com/phetsims/sherpa/issues/64<commit_after>// Copyright 2015, University of Colorado Boulder

/**
 * Creates a composite report of all of the 3rd party images, code, audio and other media used by all of the published
 * PhET Simulations. The reports is published at: https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
 *
 * Usage:
 * cd chipper
 * grunt report-third-party
 * // then push the updated license file to sherpa
 *
 * Third party entries are parsed from the HTML files for all simulations published on the PhET website, see
 * getLicenseEntry.js
 *
 * @author Sam Reid
 */

// Load shared constants
var ChipperConstants = require( '../../../chipper/js/common/ChipperConstants' );
var ChipperStringUtils = require( '../../../chipper/js/common/ChipperStringUtils' );
var assert = require( 'assert' );

// constants
var SHERPA = '../sherpa';  // The relative path to sherpa, from the chipper path

// node modules
var fs = require( 'fs' );
var http = require( 'http' );

var _ = require( '../../' + SHERPA + '/lib/lodash-2.4.1.min' ); // eslint-disable-line require-statement-match

/**
 * @param grunt - the grunt instance
 */
module.exports = function( grunt ) {
  'use strict';

  // The file where the report will be written
  var output = SHERPA + '/third-party-licenses.md';

  // Since we download the HTML files, this task must wait until it is complete
  var gruntDone = grunt.task.current.async();

  // Prepare a directory where the HTML files will be downloaded.
  // This directory should have been deleted at the end of the last task, but this could be useful if the task was
  // interrupted before cleanup.
  if ( grunt.file.exists( 'downloaded-sims' ) ) {
    grunt.file.delete( 'downloaded-sims' );
  }
  grunt.file.mkdir( 'downloaded-sims' );

  // Download a URL to a local file
  // http://stackoverflow.com/questions/11944932/how-to-download-a-file-with-node-js-without-using-third-party-libraries
  var download = function( url, dest, cb ) {
    var file = fs.createWriteStream( dest );
    http.get( url, function( response ) {
      response.pipe( file );
      file.on( 'finish', function() {
        file.close( cb );  // close() is async, call cb after close completes.
      } );
    } ).on( 'error', function( err ) { // Handle errors
      fs.unlink( dest ); // Delete the file async. (But we don't check the result)
      if ( cb ) {
        cb( err.message );
      }
    } );
  };

  // After the downloads are complete, write the report based on the metadata in the files.
  var writeReport = function() {

    // Aggregate results for each of the license types
    var compositeCode = {};
    var compositeMedia = {};

    // List of all of the repository names, so that we can detect which libraries are used by all-sims
    var simTitles = [];

    /**
     * Given an HTML text, find the title attribute by parsing for <title>
     * @param html
     * @returns {string}
     */
    var parseTitle = function( html ) {
      var startKey = '<title>';
      var endKey = '</title>';

      var startIndex = html.indexOf( startKey );
      var endIndex = html.indexOf( endKey );

      return html.substring( startIndex + startKey.length, endIndex ).trim();
    };

    /**
     * Add the source (images/audio/media or code) entries to the destination object, keyed by name.
     * @param {string} repositoryName - the name of the repository, such as 'energy-skate-park-basics'
     * @param {Object} source - the object from which to read the entry
     * @param {Object} destination - the object to which to append the entry
     */
    var augment = function( repositoryName, source, destination ) {
      for ( var entry in source ) {
        if ( source.hasOwnProperty( entry ) ) {
          if ( !destination.hasOwnProperty( entry ) ) {
            destination[ entry ] = source[ entry ];//overwrites
            destination[ entry ].usedBy = [];
          }
          destination[ entry ].usedBy.push( repositoryName );
        }
      }
    };

    grunt.file.recurse( 'downloaded-sims', function( abspath, rootdir, subdir, filename ) {
      if ( ChipperStringUtils.endsWith( filename.toLowerCase(), '.html' ) ) {

        // load the file into a string
        var html = grunt.file.read( abspath ).trim();

        if ( html.indexOf( 'was not found on this server' ) < 0 || html.indexOf( 'was not found on this server' ) > 400 ) {

          var startIndex = html.indexOf( ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES );
          var endIndex = html.indexOf( ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES );
          var substring = html.substring( startIndex, endIndex );

          var firstCurlyBrace = substring.indexOf( '{' );
          var lastCurlyBrace = substring.lastIndexOf( '}' );
          var jsonString = substring.substring( firstCurlyBrace, lastCurlyBrace + 1 );

          var json = JSON.parse( jsonString );

          var title = parseTitle( html );
          if ( !title || title.indexOf( 'undefined' ) === 0 || title.indexOf( 'TITLE' ) >= 0 ) {
            grunt.log.writeln( 'title not found for ' + abspath );
            title = filename;
          }
          augment( title, json.lib, compositeCode );
          augment( title, json.audio, compositeMedia );
          augment( title, json.images, compositeMedia );

          simTitles.push( title );
        }
      }
    } );

    // Sort to easily compare lists of repositoryNames with usedBy columns, to see which resources are used by everything.
    simTitles.sort();

    // If anything is used by every sim indicate that here
    for ( var entry in compositeCode ) {
      if ( compositeCode.hasOwnProperty( entry ) ) {
        compositeCode[ entry ].usedBy.sort();
        if ( _.isEqual( simTitles, compositeCode[ entry ].usedBy ) ) {
          compositeCode[ entry ].usedBy = 'all-sims';// Confusing that this matches a repo name, but since that repo isn't actually a sim, perhaps this will be ok
        }
      }
    }

    var json = grunt.file.readJSON( SHERPA + '/lib/license.json' );

    var entries = [];
    var codeLicensesUsed = [];
    var mediaLicensesUsed = [];

    // Get a list of the library names
    var libraries = [];
    for ( var lib in json ) {
      if ( json.hasOwnProperty( lib ) ) {
        libraries.push( lib );
      }
    }

    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    libraries.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Add info for each library to the MD report
    for ( var i = 0; i < libraries.length; i++ ) {
      var library = libraries[ i ];

      var lines = [
        '**' + library + '**',
        json[ library ].text.join( '
' ),
        json[ library ].projectURL,
        'License: [' + json[ library ].license + '](licenses/' + library + '.txt)',
        'Notes: ' + json[ library ].notes
      ];

      if ( json[ library ].dependencies ) {
        lines.push( 'Dependencies: **' + json[ library ].dependencies + '**' );
      }

      if ( compositeCode.hasOwnProperty( library ) && compositeCode[ library ].usedBy instanceof Array ) {
        lines.push( 'Used by: ' + compositeCode[ library ].usedBy.join( ', ' ) );
      }

      // \n worked well when viewing GitHub markdown as an issue comment, but for unknown reasons
 is necessary when
      // viewing from https://github.com/phetsims/sherpa/blob/master/third-party-licenses.md
      entries.push( lines.join( '
' ) );

      if ( codeLicensesUsed.indexOf( json[ library ].license ) < 0 ) {
        codeLicensesUsed.push( json[ library ].license );
      }
    }

    var mediaOutput = [];
    var mediaKeys = [];
    for ( var imageAudioEntry in compositeMedia ) {
      if ( compositeMedia.hasOwnProperty( imageAudioEntry ) ) {
        mediaKeys.push( imageAudioEntry );
      }
    }
    // Use a case insensitive sort, see http://stackoverflow.com/questions/8996963/how-to-perform-case-insensitive-sorting-in-javascript
    mediaKeys.sort( function( a, b ) {
      return a.toLowerCase().localeCompare( b.toLowerCase() );
    } );

    // Create the text for the image and audio, and keep track of which licenses were used by them.
    for ( i = 0; i < mediaKeys.length; i++ ) {
      var mediaKey = mediaKeys[ i ];

      var copyrightStatement = compositeMedia[ mediaKey ].text.join( '
' ).trim();
      var projectURL = compositeMedia[ mediaKey ].projectURL.trim();

      if ( copyrightStatement.length === 0 ) {
        copyrightStatement = '(no text)';
      }

      if ( projectURL.length === 0 ) {
        projectURL = '(no project url)';
      }

      var notes = compositeMedia[ mediaKey ].notes.trim();
      if ( notes.length === 0 ) {
        notes = '(no notes)';
      }

      var license = compositeMedia[ mediaKey ].license.trim();
      if ( license.length === 0 ) {
        license = '(no license)';
      }

      var mediaEntryLines = [
        '**' + mediaKey + '**',
        copyrightStatement,
        projectURL,
        'License: ' + license,
        'Notes: ' + notes
      ];
      if ( compositeMedia[ mediaKey ].exception ) {
        mediaEntryLines.push( 'Exception: ' + compositeMedia[ mediaKey ].exception );
      }

      // TODO: is this a real license?  Why is there logic here but nowhere else?  Perhaps files annotated with
      // contact phethelp@colorado.edu are leaking into the HTML files unnecessarily
      if ( license !== 'contact phethelp@colorado.edu' ) {
        mediaOutput.push( mediaEntryLines.join( '
' ) );

        if ( mediaLicensesUsed.indexOf( license ) < 0 ) {
          mediaLicensesUsed.push( license );
        }
      }
    }

    // Summarize licenses used
    var fileList = simTitles.join( '\n* ' );

    var outputString =
      'This report enumerates the third-party resources (code, images, audio, etc) used in a set of simulations.\n' +
      '* [Third-party Code](#third-party-code)\n' +
      '* [Third-party Code License Summary](#third-party-code-license-summary)\n' +
      '* [Third-party Media](#third-party-media)\n' +
      '* [Third-party Media License Summary](#third-party-media-license-summary)\n' +
      '\n' +
      'This report is for the following simulations: \n\n* ' + fileList + '\n\nTo see the third party resources used in a particular published ' +
      'simulation, inspect the HTML file between the `' + ChipperConstants.START_THIRD_PARTY_LICENSE_ENTRIES + '` and `' + ChipperConstants.END_THIRD_PARTY_LICENSE_ENTRIES + '` ' +
      '(only exists in sim publications after Aug 7, 2015).\n' +
      '# Third-party Code:
\n' +
      entries.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Code License Summary:
\n' +
      codeLicensesUsed.join( '
' ) + '\n\n' +

      '---\n' +

      '# Third-party Media:
\n' +
      mediaOutput.join( '\n\n' ) + '\n\n' +

      '---\n' +

      '# Third-party Media License Summary:
\n' +
      mediaLicensesUsed.join( '
' ) + '\n\n';

    // Compare the file output to the existing file, and write & git commit only if different
    if ( !grunt.file.exists( output ) || grunt.file.read( output ) !== outputString ) {
      grunt.log.writeln( 'File output changed, writing file ' + output );
      grunt.file.write( output, outputString );
    }
    else {
      grunt.log.writeln( output + ' contents are the same.  No need to save.' );
    }

    // Delete the temporarily downloaded files when task complete
    if ( grunt.file.exists( 'downloaded-sims' ) ) {
      grunt.file.delete( 'downloaded-sims' );
    }

    gruntDone();
  };

  // Download all of the simulations.  For simulations that are in "active-sims" but not published,
  // download them anyways but skip them in the report.
  var activeSimsString = fs.readFileSync( '../chipper/data/active-sims', 'utf-8' ).trim();
  var activeSimsArray = activeSimsString.split( '\n' );

  // Download files one at a time so we can make sure we get everything.
  var downloadNext = function( index ) {
    var sim = activeSimsArray[ index ];

    var url = 'http://phet.colorado.edu/sims/html/' + sim + '/latest/' + sim + '_en.html';
    console.log( 'downloading ' + (index + 1) + '/' + activeSimsArray.length + ': ' + url );

    download( url, 'downloaded-sims/' + sim + '_en.html', function( err ) {
      assert && assert( !err, 'Error during download: ' + err );

      var next = index + 1;

      if ( next < activeSimsArray.length ) {
        downloadNext( next );
      }
      else {
        writeReport();
      }
    } );
  };

  downloadNext( 0 );
};<|endoftext|>


<commit_before>/*
 *An audio spectrum visualizer built with HTML5 Audio API
 *Author:Wayou
 *License:feel free to use but keep this info please!
 *Feb 15 2014
 *Need support you can :
 *view the project page:https://github.com/Wayou/HTML5_Audio_Visualizer/
 *view online demo:http://wayouliu.duapp.com/mess/audio_visualizer.html
 view the blog on how this is done:http://www.cnblogs.com/Wayou/p/html5_audio_api_visualizer.html
 *or contact me:liuwayong@gmail.com
 */
window.onload = function() {
    new Visualizer().ini();
};
var Visualizer = function() {
    this.file = null, //the current file
    this.fileName = null, //the current file name
    this.audioContext = null,
    this.source = null, //the audio source
    this.info = document.getElementById('info').innerHTML, //this used to upgrade the UI information
    this.infoUpdateId = null, //to sotore the setTimeout ID and clear the interval
    this.animationId = null,
    this.status = 0, //flag for sound is playing 1 or stopped 0
    this.forceStop = false
};
Visualizer.prototype = {
    ini: function() {
        this._prepareAPI();
        this._addEventListner();
    },
    _prepareAPI: function() {
        //fix browser vender for AudioContext and requestAnimationFrame
        window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext;
        window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame;
        window.cancelAnimationFrame = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.msCancelAnimationFrame;
        try {
            this.audioContext = new AudioContext();
        } catch (e) {
            that._updateInfo('!Your browser does not support AudioContext', false);
            console.log(e);
        }
    },
    _addEventListner: function() {
        var that = this,
            audioInput = document.getElementById('uploadedFile'),
            dropContainer = document.getElementsByTagName("canvas")[0];
        //listen the file upload
        audioInput.onchange = function() {
            //the if statement fixes the file selction cancle, because the onchange will trigger even the file selection been canceled
            if (audioInput.files.length !== 0) {
                //only process the first file
                that.file = audioInput.files[0];
                that.fileName = that.file.name;
                if (that.status === 1) {
                    //the sound is still playing but we upload another file, so set the forceStop flag to true
                    that.forceStop = true;
                };
                document.getElementById('fileWrapper').style.opacity = 1;
                that._updateInfo('Uploading', true);
                //once the file is ready,start the visualizer
                that._start();
            };
        };
        //listen the drag & drop
        dropContainer.addEventListener("dragenter", function() {
            that._updateInfo('Drop it on the page', true);
        }, false);
        dropContainer.addEventListener("dragover", function(e) {
            e.stopPropagation();
            e.preventDefault();
            //set the drop mode
            e.dataTransfer.dropEffect = 'copy';
        }, false);
        dropContainer.addEventListener("dragleave", function() {
            that._updateInfo(that.info, false);
        }, false);
        dropContainer.addEventListener("drop", function(e) {
            e.stopPropagation();
            e.preventDefault();
            document.getElementById('fileWrapper').style.opacity = 1;
            that._updateInfo('Uploading', true);
            //get the dropped file
            that.file = e.dataTransfer.files[0];
            if (that.status === 1) {
                document.getElementById('fileWrapper').style.opacity = 1;
                that.forceStop = true;
            };
            that.fileName = that.file.name;
            //once the file is ready,start the visualizer
            that._start();
        }, false);
    },
    _start: function() {
        //read and decode the file into audio array buffer
        var that = this,
            file = this.file,
            fr = new FileReader();
        fr.onload = function(e) {
            var fileResult = e.target.result;
            var audioContext = that.audioContext;
            if (audioContext === null) {
                return;
            };
            that._updateInfo('Decoding the audio', true);
            audioContext.decodeAudioData(fileResult, function(buffer) {
                that._updateInfo('Decode succussfully,start the visualizer', true);
                that._visualize(audioContext, buffer);
            }, function(e) {
                that._updateInfo('!Fail to decode the file', false);
                console.log(e);
            });
        };
        fr.onerror = function(e) {
            that._updateInfo('!Fail to read the file', false);
            console.log(e);
        };
        //assign the file to the reader
        this._updateInfo('Starting read the file', true);
        fr.readAsArrayBuffer(file);
    },
    _visualize: function(audioContext, buffer) {
        var audioBufferSouceNode = audioContext.createBufferSource(),
            analyser = audioContext.createAnalyser(),
            that = this;
        //connect the source to the analyser
        audioBufferSouceNode.connect(analyser);
        //connect the analyser to the destination(the speaker), or we won't hear the sound
        analyser.connect(audioContext.destination);
        //then assign the buffer to the buffer source node
        audioBufferSouceNode.buffer = buffer;
        //play the source
        if (!audioBufferSouceNode.start) {
            audioBufferSouceNode.start = audioBufferSouceNode.noteOn //in old browsers use noteOn method
            audioBufferSouceNode.stop = audioBufferSouceNode.noteOff //in old browsers use noteOn method
        };
        //stop the previous sound if any
        if (this.source !== null) {
            cancelAnimationFrame(this.animationId);
            this.source.stop(0);
        }
        audioBufferSouceNode.start(0);
        this.status = 1;
        this.source = audioBufferSouceNode;
        audioBufferSouceNode.onended = function() {
            that._audioEnd(that);
        };
        this._updateInfo('Playing ' + this.fileName, false);
        this.info = 'Playing ' + this.fileName;
        document.getElementById('fileWrapper').style.opacity = 0.2;
        this._drawSpectrum(analyser);
    },
    _drawSpectrum: function(analyser) {
        var that = this,
            canvas = document.getElementById('canvas'),
            cwidth = canvas.width,
            cheight = canvas.height - 2,
            /*2 is the gap between meter and cap*/
            meterWidth = 10, //width of the meters in the spectrum
            gap = 2, //gap between meters
            capHeight = 2,
            capStyle = '#fff',
            meterNum = 800 / (10 + 2), //count of the meters
            capYPositionArray = []; ////store the vertical position of hte caps for the preivous frame
        ctx = canvas.getContext('2d'),
        gradient = ctx.createLinearGradient(0, 0, 0, 300);
        gradient.addColorStop(1, '#0f0');
        gradient.addColorStop(0.5, '#ff0');
        gradient.addColorStop(0, '#f00');
        var drawMeter = function() {
            var array = new Uint8Array(analyser.frequencyBinCount);
            analyser.getByteFrequencyData(array);
            var step = Math.round(array.length / meterNum); //sample limited data from the total array
            ctx.clearRect(0, 0, cwidth, cheight);
            for (var i = 0; i < meterNum; i++) {
                var value = array[i * step];
                if (that.status === 0) {
                    value = 0; //fix when some sounds end the value still not back to zero
                };
                if (capYPositionArray.length < Math.round(meterNum)) {
                    capYPositionArray.push(value);
                };
                ctx.fillStyle = capStyle;
                //draw the cap, with transition effect
                if (value < capYPositionArray[i]) {
                    ctx.fillRect(i * 12, cheight - (--capYPositionArray[i]), meterWidth, capHeight);
                } else {
                    ctx.fillRect(i * 12, cheight - value, meterWidth, capHeight);
                    capYPositionArray[i] = value;
                };
                ctx.fillStyle = gradient; //set the filllStyle to gradient for a better look
                ctx.fillRect(i * 12 /*meterWidth+gap*/ , cheight - value + capHeight, meterWidth, cheight); //the meter
            }
            this.animationId = requestAnimationFrame(drawMeter);
        }
        this.animationId = requestAnimationFrame(drawMeter);
    },
    _audioEnd: function(instance) {
        if (this.forceStop) {
            this.forceStop = false;
            this.status = 1;
            return;
        };
        this.status = 0;
        console.log('audio ended');
        var text = 'HTML5 Audio API showcase | An Audio Viusalizer';
        document.getElementById('fileWrapper').style.opacity = 1;
        document.getElementById('info').innerHTML = text;
        instance.info = text;
        document.getElementById('uploadedFile').value = '';
    },
    _updateInfo: function(text, processing) {
        var infoBar = document.getElementById('info'),
            dots = '...',
            i = 0,
            that = this;
        infoBar.innerHTML = text + dots.substring(0, i++);
        if (this.infoUpdateId !== null) {
            clearTimeout(this.infoUpdateId);
        };
        if (processing) {
            //animate dots at the end of the info text
            var animateDot = function() {
                if (i > 3) {
                    i = 0
                };
                infoBar.innerHTML = text + dots.substring(0, i++);
                that.infoUpdateId = setTimeout(animateDot, 250);
            }
            this.infoUpdateId = setTimeout(animateDot, 250);
        };
    }
}<commit_msg>enhance and optmize<commit_after>/*
 *An audio spectrum visualizer built with HTML5 Audio API
 *Author:Wayou
 *License:feel free to use but keep this info please!
 *Feb 15 2014
 *Need support you can :
 *view the project page:https://github.com/Wayou/HTML5_Audio_Visualizer/
 *view online demo:http://wayouliu.duapp.com/mess/audio_visualizer.html
 view the blog on how this is done:http://www.cnblogs.com/Wayou/p/html5_audio_api_visualizer.html
 *or contact me:liuwayong@gmail.com
 */
window.onload = function() {
    new Visualizer().ini();
};
var Visualizer = function() {
    this.file = null, //the current file
    this.fileName = null, //the current file name
    this.audioContext = null,
    this.source = null, //the audio source
    this.info = document.getElementById('info').innerHTML, //this used to upgrade the UI information
    this.infoUpdateId = null, //to sotore the setTimeout ID and clear the interval
    this.animationId = null,
    this.status = 0, //flag for sound is playing 1 or stopped 0
    this.forceStop = false,
    this.allCapsReachBottom = false
};
Visualizer.prototype = {
    ini: function() {
        this._prepareAPI();
        this._addEventListner();
    },
    _prepareAPI: function() {
        //fix browser vender for AudioContext and requestAnimationFrame
        window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext;
        window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame;
        window.cancelAnimationFrame = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.msCancelAnimationFrame;
        try {
            this.audioContext = new AudioContext();
        } catch (e) {
            that._updateInfo('!Your browser does not support AudioContext', false);
            console.log(e);
        }
    },
    _addEventListner: function() {
        var that = this,
            audioInput = document.getElementById('uploadedFile'),
            dropContainer = document.getElementsByTagName("canvas")[0];
        //listen the file upload
        audioInput.onchange = function() {
            //the if statement fixes the file selction cancle, because the onchange will trigger even the file selection been canceled
            if (audioInput.files.length !== 0) {
                //only process the first file
                that.file = audioInput.files[0];
                that.fileName = that.file.name;
                if (that.status === 1) {
                    //the sound is still playing but we upload another file, so set the forceStop flag to true
                    that.forceStop = true;
                };
                document.getElementById('fileWrapper').style.opacity = 1;
                that._updateInfo('Uploading', true);
                //once the file is ready,start the visualizer
                that._start();
            };
        };
        //listen the drag & drop
        dropContainer.addEventListener("dragenter", function() {
            that._updateInfo('Drop it on the page', true);
        }, false);
        dropContainer.addEventListener("dragover", function(e) {
            e.stopPropagation();
            e.preventDefault();
            //set the drop mode
            e.dataTransfer.dropEffect = 'copy';
        }, false);
        dropContainer.addEventListener("dragleave", function() {
            that._updateInfo(that.info, false);
        }, false);
        dropContainer.addEventListener("drop", function(e) {
            e.stopPropagation();
            e.preventDefault();
            document.getElementById('fileWrapper').style.opacity = 1;
            that._updateInfo('Uploading', true);
            //get the dropped file
            that.file = e.dataTransfer.files[0];
            if (that.status === 1) {
                document.getElementById('fileWrapper').style.opacity = 1;
                that.forceStop = true;
            };
            that.fileName = that.file.name;
            //once the file is ready,start the visualizer
            that._start();
        }, false);
    },
    _start: function() {
        //read and decode the file into audio array buffer
        var that = this,
            file = this.file,
            fr = new FileReader();
        fr.onload = function(e) {
            var fileResult = e.target.result;
            var audioContext = that.audioContext;
            if (audioContext === null) {
                return;
            };
            that._updateInfo('Decoding the audio', true);
            audioContext.decodeAudioData(fileResult, function(buffer) {
                that._updateInfo('Decode succussfully,start the visualizer', true);
                that._visualize(audioContext, buffer);
            }, function(e) {
                that._updateInfo('!Fail to decode the file', false);
                console.log(e);
            });
        };
        fr.onerror = function(e) {
            that._updateInfo('!Fail to read the file', false);
            console.log(e);
        };
        //assign the file to the reader
        this._updateInfo('Starting read the file', true);
        fr.readAsArrayBuffer(file);
    },
    _visualize: function(audioContext, buffer) {
        var audioBufferSouceNode = audioContext.createBufferSource(),
            analyser = audioContext.createAnalyser(),
            that = this;
        //connect the source to the analyser
        audioBufferSouceNode.connect(analyser);
        //connect the analyser to the destination(the speaker), or we won't hear the sound
        analyser.connect(audioContext.destination);
        //then assign the buffer to the buffer source node
        audioBufferSouceNode.buffer = buffer;
        //play the source
        if (!audioBufferSouceNode.start) {
            audioBufferSouceNode.start = audioBufferSouceNode.noteOn //in old browsers use noteOn method
            audioBufferSouceNode.stop = audioBufferSouceNode.noteOff //in old browsers use noteOn method
        };
        //stop the previous sound if any
        if (this.animationId !== null) {
            cancelAnimationFrame(this.animationId);
        }
        if (this.source !== null) {
            this.source.stop(0);
        }
        audioBufferSouceNode.start(0);
        this.status = 1;
        this.source = audioBufferSouceNode;
        audioBufferSouceNode.onended = function() {
            that._audioEnd(that);
        };
        this._updateInfo('Playing ' + this.fileName, false);
        this.info = 'Playing ' + this.fileName;
        document.getElementById('fileWrapper').style.opacity = 0.2;
        this._drawSpectrum(analyser);
    },
    _drawSpectrum: function(analyser) {
        var that = this,
            canvas = document.getElementById('canvas'),
            cwidth = canvas.width,
            cheight = canvas.height - 2,
            /*2 is the gap between meter and cap*/
            meterWidth = 10, //width of the meters in the spectrum
            gap = 2, //gap between meters
            capHeight = 2,
            capStyle = '#fff',
            meterNum = 800 / (10 + 2), //count of the meters
            capYPositionArray = []; ////store the vertical position of hte caps for the preivous frame
        ctx = canvas.getContext('2d'),
        gradient = ctx.createLinearGradient(0, 0, 0, 300);
        gradient.addColorStop(1, '#0f0');
        gradient.addColorStop(0.5, '#ff0');
        gradient.addColorStop(0, '#f00');
        var drawMeter = function() {
            var array = new Uint8Array(analyser.frequencyBinCount);
            analyser.getByteFrequencyData(array);
            if (that.status === 0) {
                //fix when some sounds end the value still not back to zero
                for (var i = array.length - 1; i >= 0; i--) {
                    array[i] = 0;
                };
                allCapsReachBottom = true;
                for (var i = capYPositionArray.length - 1; i >= 0; i--) {
                    allCapsReachBottom = allCapsReachBottom && (capYPositionArray[i] === 0);
                };
                if (allCapsReachBottom) {
                    cancelAnimationFrame(that.animationId);//since the sound is top and animation finished, stop the requestAnimation to prevent potential memory leak,THIS IS VERY IMPORTANT!
                    return;
                };
            };
            var step = Math.round(array.length / meterNum); //sample limited data from the total array
            ctx.clearRect(0, 0, cwidth, cheight);
            for (var i = 0; i < meterNum; i++) {
                var value = array[i * step];
                if (capYPositionArray.length < Math.round(meterNum)) {
                    capYPositionArray.push(value);
                };
                ctx.fillStyle = capStyle;
                //draw the cap, with transition effect
                if (value < capYPositionArray[i]) {
                    ctx.fillRect(i * 12, cheight - (--capYPositionArray[i]), meterWidth, capHeight);
                } else {
                    ctx.fillRect(i * 12, cheight - value, meterWidth, capHeight);
                    capYPositionArray[i] = value;
                };
                ctx.fillStyle = gradient; //set the filllStyle to gradient for a better look
                ctx.fillRect(i * 12 /*meterWidth+gap*/ , cheight - value + capHeight, meterWidth, cheight); //the meter
            }
            that.animationId = requestAnimationFrame(drawMeter);
        }
        this.animationId = requestAnimationFrame(drawMeter);
    },
    _audioEnd: function(instance) {
        if (this.forceStop) {
            this.forceStop = false;
            this.status = 1;
            return;
        };
        this.status = 0;
        var text = 'HTML5 Audio API showcase | An Audio Viusalizer';
        document.getElementById('fileWrapper').style.opacity = 1;
        document.getElementById('info').innerHTML = text;
        instance.info = text;
        document.getElementById('uploadedFile').value = '';
    },
    _updateInfo: function(text, processing) {
        var infoBar = document.getElementById('info'),
            dots = '...',
            i = 0,
            that = this;
        infoBar.innerHTML = text + dots.substring(0, i++);
        if (this.infoUpdateId !== null) {
            clearTimeout(this.infoUpdateId);
        };
        if (processing) {
            //animate dots at the end of the info text
            var animateDot = function() {
                if (i > 3) {
                    i = 0
                };
                infoBar.innerHTML = text + dots.substring(0, i++);
                that.infoUpdateId = setTimeout(animateDot, 250);
            }
            this.infoUpdateId = setTimeout(animateDot, 250);
        };
    }
}<|endoftext|>


<commit_before>/*! Copyright (c) 2012 Mieszko Domagała (http://miecho.pl)
 * Licensed under the MIT License (LICENSE.txt).
 *
 * Version: 1.0.0
 *
 * Requires: 1.8.1+
 */

(function($){
	$.fn.extend({
		customSelectbox: function() {
			return this.each(function() {
				var $select = $(this);
				var $id = $(this).attr('id');
				var $new_id = $id+'_customSelectbox';
				var $name = $(this).attr('name');
				var $new_select = $('<div/>', {id: $new_id, 'class': 'customSelectbox'});
				var $selected_display = $('<div/>', {'class': 'customSelectbox_display_container'});
				var $selected_div = $('<div/>', {'class': 'customSelectbox_selected'});
				var $select_icon = $('<div/>', {'class': 'customSelectbox_icon'});
				var $select_iconbg = $('<div/>', {'class': 'customSelectbox_icon_bg'});
				var $list_container = $('<ul/>', {'class': 'customSelectbox_container'});
				$select.find('option').each(function(index){
					var $new_option = $('<li/>', {'data-option_id': index});
					$new_option.html($(this).text());
					$new_option.appendTo($list_container);
				});
				$selected_div.appendTo($selected_display);
				$select_icon.appendTo($selected_display);
				$select_iconbg.appendTo($selected_display);
				$selected_display.appendTo($new_select);
				$list_container.appendTo($new_select);
				$selected_display.click(function() {
					if ($new_select.hasClass('customSelectbox_opened')) {
						$list_container.slideUp();
						$new_select.removeClass('customSelectbox_opened');
					} else {
						$list_container.slideDown();
						$new_select.addClass('customSelectbox_opened');
					}
				});
				$select.after($new_select);
				$list_container.find('li').click(function() {
					var $selected_index = $(this).attr('data-option_id');
					var $selected_html = $(this).html();
					$select.prop('selectedIndex', $selected_index);
					$select.change();
					$selected_div.html($selected_html);
					$list_container.slideUp();
					$new_select.removeClass('customSelectbox_opened');
					$list_container.find('li').removeClass('customSelectbox_this_is_selected');
					$(this).addClass('customSelectbox_this_is_selected');
				});
				$list_container.hide();
				$select.hide();
				$new_select.width($new_select.find('ul').width()+30);
				$new_select.find('ul').width($new_select.width());
				var $selected_index = $select.prop('selectedIndex');
				$('li[data-option_id='+$selected_index+']').click();
				$(document).mousedown(function(e) {
					if($(e.target).parents('#'+$new_id).length==0 && !$(e.target).is('#'+$new_id)) {
						$list_container.slideUp();
						$new_select.removeClass('customSelectbox_opened');
					}
				});
			});
        }
    });
})(jQuery);<commit_msg>some bugs fixed<commit_after>/*! Copyright (c) 2012 Mieszko Domagała (http://miecho.pl)
 * Licensed under the MIT License (LICENSE.txt).
 *
 * Version: 1.0.0
 *
 * Requires: 1.8.1+
 */

(function($){
	$.fn.extend({
		customSelectbox: function() {
			return this.each(function() {
				var $select = $(this);
				var $id = $(this).attr('id');
				var $new_id = $id+'_customSelectbox';
				var $name = $(this).attr('name');
				var $new_select = $('<div/>', {id: $new_id, 'class': 'customSelectbox'});
				var $selected_display = $('<div/>', {'class': 'customSelectbox_display_container'});
				var $selected_div = $('<div/>', {'class': 'customSelectbox_selected'});
				var $select_icon = $('<div/>', {'class': 'customSelectbox_icon'});
				var $select_iconbg = $('<div/>', {'class': 'customSelectbox_icon_bg'});
				var $list_container = $('<ul/>', {'class': 'customSelectbox_container'});
				$select.find('option').each(function(index){
					var $new_option = $('<li/>', {'data-option_id': index});
					$new_option.html($(this).text());
					$new_option.appendTo($list_container);
				});
				$selected_div.appendTo($selected_display);
				$select_icon.appendTo($selected_display);
				$select_iconbg.appendTo($selected_display);
				$selected_display.appendTo($new_select);
				$list_container.appendTo($new_select);
				$selected_display.click(function() {
					if ($new_select.hasClass('customSelectbox_opened')) {
						$list_container.slideUp();
						$new_select.removeClass('customSelectbox_opened');
					} else {
						$list_container.slideDown();
						$new_select.addClass('customSelectbox_opened');
					}
				});
				$select.after($new_select);
				$list_container.find('li').click(function() {
					var $selected_index = $(this).attr('data-option_id');
					var $selected_html = $(this).html();
					$select.prop('selectedIndex', $selected_index);
					$select.trigger('custom_change');
					$selected_div.html($selected_html);
					$list_container.slideUp();
					$new_select.removeClass('customSelectbox_opened');
					$list_container.find('li').removeClass('customSelectbox_this_is_selected');
					$(this).addClass('customSelectbox_this_is_selected');
				});
				$list_container.hide();
				$select.hide();
				$new_select.width($new_select.find('ul').width()+30);
				$new_select.find('ul').width($new_select.width());
				var $selected_index = $select.prop('selectedIndex');
				$('li[data-option_id='+$selected_index+']').click();
				$(document).mousedown(function(e) {
					if($(e.target).parents('#'+$new_id).length==0 && !$(e.target).is('#'+$new_id)) {
						$list_container.slideUp();
						$new_select.removeClass('customSelectbox_opened');
					}
				});
				$select.bind('custom_change', function() {
					$(this).trigger('change');
				});
			});
        }
    });
})(jQuery);<|endoftext|>


<commit_before>(function($,Panorama){

/**
 * Region()
 *
 * Region constructor
 *
 * Instantiate rectangular selection rectangle on panorama_mousedown,
 * dispatch a region_ready event on panorama_mouseup
 *
 */
function Region(options) {
  if (!(this instanceof Region)) {
    return new Region(options);
  }
  $.extend(true,this,this.defaults,options);
  this.init();

} // Region

$.extend(true,Region.prototype,{

  // default region options
  defaults: {
    selrect: {
      css: {
        // selection rectangle color
        borderColor: 'red'
      }
    }
  }, // region_defaults

  /**
   * Region.init()
   *
   * Initialize region instance
   *
   */
  init: function region_init() {

    var region=this;
    var panorama=region.panorama;

    // instantiate the selection rectangle
    $(panorama.container).selrect({

      region: region,

      css: region.selrect.css,

      // selrect event handler
      callback: function selrect_callback(e) {

        var selrect=this;

        if (typeof(e)=="string") {
          e={type: e};
        }

        // forward selrect events to region instance
        if (region['on_selrect_'+e.type]) {
          return region['on_selrect_'+e.type].apply(selrect,[e]);
        }

      } // selrect_callback

    }); // selrect

  }, // region_init

  /**
   * Region.on_selrect_init()
   *
   * selrect_init event handler
   *
   */
  on_selrect_init: function region_on_selrect_init(e) {

    var selrect=this;

    // forward future panorama (mouse) events to this selrect instance
    Panorama.prototype.dispatchEventsTo(selrect,{

      // intercept mouse events before Controls.prototype handlers
      prepend: true

    });

  }, // region_on_selrect_init

  /**
   * Region.on_selrect_mousemove()
   *
   * selrect_mousemove event handler
   *
   */
  on_selrect_mousemove: function region_on_selrect_mousemove(e) {
    // prevent panorama_mousemove event from reaching Controls or Panorama handlers
    e.preventDefault();
    e.stopPropagation();
    return false;
  }, // region_on_select_mousemove

  /**
   * Region.on_selrect_mouseup()
   *
   * selrect_mouseup event handler
   *
   */
  on_selrect_mouseup: function region_on_selrect_mouseup(e) {

    var selrect=this;
    var region=selrect.region;

    // prevent panorama_mouseup event from reaching Controls or Panorama handlers
    e.preventDefault();
    e.stopPropagation();

    // region has been selected
    region.rect=selrect.rect;
    region.dispatch({
      type: 'ready',
      rect: selrect.rect
    });

    // selrect instance does not need to receive Panorama events anymore
    Panorama.prototype.dispatchEventsTo(selrect,{dispose: true});

    // selrect div is not needed anymore
    $(selrect.div).remove();

    return false;

  }, // region_on_selrect_mouseup

});

// add Region to Panorama.prototype
Panorama.prototype.Region=Region;

})(jQuery,Panorama);

// Region need to receive Panorama mouse events

setupEventDispatcher(Panorama.prototype.Region.prototype);

<commit_msg>update license<commit_after>/*
 * freepano - WebGL panorama viewer
 *
 * Copyright (c) 2014-2015 FOXEL SA - http://foxel.ch
 * Please read <http://foxel.ch/license> for more information.
 *
 *
 * Author(s):
 *
 *      Luc Deschenaux <l.deschenaux@foxel.ch>
 *
 * This file is part of the FOXEL project <http://foxel.ch>.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * Additional Terms:
 *
 *      You are required to preserve legal notices and author attributions in
 *      that material or in the Appropriate Legal Notices displayed by works
 *      containing it.
 *
 *      You are required to attribute the work as explained in the "Usage and
 *      Attribution" section of <http://foxel.ch/license>.
 */

(function($,Panorama){

/**
 * Region()
 *
 * Region constructor
 *
 * Instantiate rectangular selection rectangle on panorama_mousedown,
 * dispatch a region_ready event on panorama_mouseup
 *
 */
function Region(options) {
  if (!(this instanceof Region)) {
    return new Region(options);
  }
  $.extend(true,this,this.defaults,options);
  this.init();

} // Region

$.extend(true,Region.prototype,{

  // default region options
  defaults: {
    selrect: {
      css: {
        // selection rectangle color
        borderColor: 'red'
      }
    }
  }, // region_defaults

  /**
   * Region.init()
   *
   * Initialize region instance
   *
   */
  init: function region_init() {

    var region=this;
    var panorama=region.panorama;

    // instantiate the selection rectangle
    $(panorama.container).selrect({

      region: region,

      css: region.selrect.css,

      // selrect event handler
      callback: function selrect_callback(e) {

        var selrect=this;

        if (typeof(e)=="string") {
          e={type: e};
        }

        // forward selrect events to region instance
        if (region['on_selrect_'+e.type]) {
          return region['on_selrect_'+e.type].apply(selrect,[e]);
        }

      } // selrect_callback

    }); // selrect

  }, // region_init

  /**
   * Region.on_selrect_init()
   *
   * selrect_init event handler
   *
   */
  on_selrect_init: function region_on_selrect_init(e) {

    var selrect=this;

    // forward future panorama (mouse) events to this selrect instance
    Panorama.prototype.dispatchEventsTo(selrect,{

      // intercept mouse events before Controls.prototype handlers
      prepend: true

    });

  }, // region_on_selrect_init

  /**
   * Region.on_selrect_mousemove()
   *
   * selrect_mousemove event handler
   *
   */
  on_selrect_mousemove: function region_on_selrect_mousemove(e) {
    // prevent panorama_mousemove event from reaching Controls or Panorama handlers
    e.preventDefault();
    e.stopPropagation();
    return false;
  }, // region_on_select_mousemove

  /**
   * Region.on_selrect_mouseup()
   *
   * selrect_mouseup event handler
   *
   */
  on_selrect_mouseup: function region_on_selrect_mouseup(e) {

    var selrect=this;
    var region=selrect.region;

    // prevent panorama_mouseup event from reaching Controls or Panorama handlers
    e.preventDefault();
    e.stopPropagation();

    // region has been selected
    region.rect=selrect.rect;
    region.dispatch({
      type: 'ready',
      rect: selrect.rect
    });

    // selrect instance does not need to receive Panorama events anymore
    Panorama.prototype.dispatchEventsTo(selrect,{dispose: true});

    // selrect div is not needed anymore
    $(selrect.div).remove();

    return false;

  }, // region_on_selrect_mouseup

});

// add Region to Panorama.prototype
Panorama.prototype.Region=Region;

})(jQuery,Panorama);

// Region need to receive Panorama mouse events

setupEventDispatcher(Panorama.prototype.Region.prototype);

<|endoftext|>