<commit_before>'use strict';

var Q = require('q');
var path = require('path');
var helper = rootRequire('helpers/releases');
var authenticationUtils = rootRequire('utils/authentication-utils');
var model = rootRequire('models/model');
var cloudstorage = rootRequire('libs/cdn/cloudstorage');
var rabbitmq = rootRequire('libs/message_broker/rabbitmq');
var imagesController = rootRequire('controllers/images');

module.exports.controller = function(app) {

  /**
   * Rollback a release back to the DropZone
   * 1) Restore DropZoneFiles entries for metadataFile, tracks and cover
   * 2) Remove tracks
   */
  function rollbackRelease(release, databaseRelease) {

    databaseRelease.status = model.ReleaseStatus.PROCESSING_FAILED;

    release.Tracks.forEach(function(track) {

      // Restore track dropzone files
      model.Track.find({
        where: {
          id: track.id
        }
      }).then(function(databaseTrack) {
        if (databaseTrack) {
          databaseTrack.path = track.path;
          databaseTrack.mp3Path = null;
          databaseTrack.snippetPath = null;
          databaseTrack.oggSnippetPath = null;
          databaseTrack.waveform = null;
          databaseTrack.lengthInSeconds = null;
          databaseTrack.bpm = null;
          databaseTrack.status = model.TrackStatus.PROCESSING_FAILED;
          databaseTrack.errorMessage = track.errorMessage;
          model.DropZoneFile.find({
            where: {
              path: track.path
            }
          }).then(function(file) {
            if (file) {
              file.status = 'UPLOADED';
              file.save();
              console.log(
                'Saving track ' + databaseTrack.id + ' Path ' + track.path);
              databaseTrack.save();
            }
          });
        }
      });
    });

    // Restore cover dropzone file
    model.DropZoneFile.find({
      where: {
        path: databaseRelease.cover
      }
    }).then(function(file) {
      if (file) {
        file.status = 'UPLOADED';
        file.save();
      }
    });

    // Restore metadata file
    model.DropZoneFile.find({
      where: {
        path: databaseRelease.metadataFile
      }
    }).then(function(file) {
      if (file) {
        file.status = 'UPLOADED';
        file.save();
      }
    });

    databaseRelease.save();
  }

  /**
   * Return a promise that updates a track given a track, a release Id and a
   * a new cover path
   */
  function updateTrackPromise(track, releaseId, newCoverPath) {
    // Updated track
    var deferredUpdate = Q.defer();
    model.Track.find({
      where: {
        id: track.id
      }
    }).then(function(databaseTrack) {
      if (!databaseTrack) {
        console.log('Track does not exist, update promise rejected');
        deferredUpdate.reject(new Error('Track does not exist for release'));
      } else {
        // Update track paths
        databaseTrack.waveform = track.waveform;
        databaseTrack.snippetPath = track.snippetPath;
        databaseTrack.oggSnippetPath = track.oggSnippetPath;
        databaseTrack.mp3Path = track.mp3Path;
        databaseTrack.lengthInSeconds = track.lengthInSeconds;
        databaseTrack.status = model.TrackStatus.PROCESSED;
        databaseTrack.cover = newCoverPath;
        databaseTrack.bpm = track.bpm;

        // Move lossless file
        var trackFilename = path.basename(databaseTrack.path);
        var newLosslessPath =
          helper.remoteReleasePath(releaseId, trackFilename);
        cloudstorage.copy(
          databaseTrack.path,
          newLosslessPath,
          function(err) {
            if (err) {
              deferredUpdate.reject(err);
            } else {
              databaseTrack.path = newLosslessPath;
              databaseTrack.save();
              deferredUpdate.resolve();
            }
          });
      }
    });

    // return update promise
    return deferredUpdate.promise;
  }

  /**
   * Returns a promise that removes a file from the DropZone's CDN
   */
  function deleteDropZoneFilePromise(dropZonePath) {
    var deferredDelete = Q.defer();
    model.DropZoneFile.find({
      where: {
        path: dropZonePath
      }
    }).then(function(file) {
      if (file) {
        file.destroy().then(function() {
          cloudstorage.remove(dropZonePath, function(err) {
            if (err) {
              deferredDelete.reject(err);
            } else {
              deferredDelete.resolve();
            }
          });
        });
      } else {
        console.log('DropZoneFile does not exist, delete promise rejected');
        deferredDelete.reject(new Error('DropZoneFile does not exist'));
      }
    });
    // Return delete promise
    return deferredDelete;
  }

  /**
   * Returns a promise that moves the cover of a release from the CDN to the
   * release directory
   */
  function moveCoverPromise(databaseRelease, newCoverPath) {
    var deferredMove = Q.defer();
    model.DropZoneFile.find({
      where: {
        path: databaseRelease.cover
      }
    }).then(function(file) {
      if (file) {
        file.destroy().then(function() {
          // Move lossless file
          var oldCoverPath = databaseRelease.cover;
          databaseRelease.cover = newCoverPath;
          cloudstorage.move(oldCoverPath, newCoverPath, function(err) {
            if (err) {
              deferredMove.reject(err);
            } else {
              deferredMove.resolve();
            }
          });
        });
      } else {
        console.log('DropZoneFile does not exist, delete promise rejected');
        deferredMove.reject(new Error('DropZoneFile does not exist'));
      }
    });

    // Return move promise
    return deferredMove.promise;
  }

  /**
   * Commits a release when its processing succeeded (A success message is
   * received from RabbitMq)
   * 1) Store tracks paths
   * 2) Move tracks lossless files
   * 3) Move release cover
   * 4) Remove DropZoneFiles entries
   * 5) Remove metadata file, if any
   */
  function commitRelease(release, databaseRelease) {

    databaseRelease.status = model.ReleaseStatus.PROCESSED;

    var updateTrackPromises = [];
    var deleteDropZoneFilePromises = [];

    var oldCoverPath = databaseRelease.cover;
    var coverFilename = path.basename(oldCoverPath);
    var newCoverPath =
      helper.remoteReleasePath(databaseRelease.id, coverFilename);

    for (var i = 0; i < release.Tracks.length; i++) {
      var track = release.Tracks[i];
      // Update track if it was marked for update
      if (track.status === model.TrackStatus.TO_BE_PROCESSED) {
        updateTrackPromises.push(
          updateTrackPromise(track, release.id, newCoverPath));
        deleteDropZoneFilePromises.push(deleteDropZoneFilePromise(track.path));
      }
    }

    Q.allSettled(updateTrackPromises).then(function(results) {

      var error = false;
      for (i = 0; i < results.length; i++) {
        if (results[i].state === 'rejected') {
          error = true;
          break;
        }
      }

      if (!error) {
        console.log('All tracks have been updated and files moved');
        Q.allSettled(deleteDropZoneFilePromises).then(function() {

          console.log('All track dropzone files deleted correctly');
          var morePromises = [];
          if (databaseRelease.metadataFile) {
            morePromises.push(
              deleteDropZoneFilePromise(databaseRelease.metadataFile));
          }

          if (databaseRelease.cover !== newCoverPath) {
            morePromises.push(
              moveCoverPromise(databaseRelease, newCoverPath));
          }

          Q.allSettled(morePromises).then(function() {
            databaseRelease.metadataFile = '';
            databaseRelease.save();
          });
        });
      } else {
        console.log('Track update failed');
        rollbackRelease(release, databaseRelease);
        return;
      }
    });
  }

  /**
   * Callback to the release result message on rabbitmq
   **/
  rabbitmq.onReleaseResult(
    function(message, headers, deliveryInfo) {

      if (deliveryInfo.contentType !== 'application/json') {
        console.log('Received status message is not in JSON format');
        return;
      }

      if (!message || !message.id) {
        console.log('Received status message is not a release');
        return;
      }

      var release = message;

      model.Release
        .find({
          where: {
            id: release.id
          }
        }).then(function(databaseRelease) {
          if (!databaseRelease) {
            return;
          }

          if (release.status === model.ReleaseStatus.PROCESSED) {
            console.log(
              'Release with id ' +
              release.id +
              ' has been correctly processed');
            commitRelease(release, databaseRelease);
          } else {
            console.log(
              'Release with id ' +
              release.id +
              ' processing failed');
            rollbackRelease(release, databaseRelease);
          }
        });
    });

  /**
   * GET /releases
   * Returns all releases
   * TODO pagination
   */
  app.get('/releases/', function(req, res) {

    model.Release.findAll({
      where: {
        isActive: true
      },
      order: 'position',
      include: [{
        model: model.Track,
        include: [{
          model: model.Artist,
          as: 'Remixer'
        }, {
          model: model.Artist,
          as: 'Producer'
        }]
      }, {
        model: model.Label
      }]
    }).then(function(releases) {
      res.send(releases);
    });
  });

  /**
   * GET /releases/:id
   * Return the release associated to the id
   */
  app.get('/releases/:id', function(req, res) {
    var releaseId = req.params.id;

    model.Release.find({
      where: {
        id: releaseId
      },
      order: 'position',
      include: [{
        model: model.Track,
        include: [{
          model: model.Artist,
          as: 'Remixer'
        }, {
          model: model.Artist,
          as: 'Producer'
        }, {
          model: model.Genre
        }]
      }, {
        model: model.Label
      }]
    }).then(function(release) {
      res.send(release);
    });
  });

  /**
   * POST /release/
   * Create a new release
   */
  app.post('/releases/',
    authenticationUtils.ensureAuthenticated,
    authenticationUtils.ensureAdmin,
    function(req, res) {

      var release = req.body.release;
      var idLabel = req.body.idLabel;
      console.log('____________' + idLabel);
      console.log('ADD RELEASE');
      console.log(release);
      model.Label.find({
        where: {
          id: idLabel
        }
      }).then(function(label) {
        model.Release.create(release).
        then(function(newRelease) {
          label.addReleases(newRelease).then(function() {
            model.Release.consolideJSON(newRelease.id);
            res.send(newRelease);
          });
        });
      });
    });

  /**
   * PUT /releases/:id
   * Update a release
   */
  app.put('/releases/:id',
    authenticationUtils.ensureAuthenticated, authenticationUtils.ensureAdmin,
    function(req, res) {
      var releaseId = req.params.id;
      var release = req.body.release;

      // Update the release
      model.Release
        .find({
          where: {
            id: releaseId
          }
        }).then(function(newRelease) {
          // newRelease is the current release
          release.status = model.ReleaseStatus.TO_BE_PROCESSED;
          newRelease.updateAttributes(release);
          var trackUpdatePromises = [];
          for (var i = release.Tracks.length - 1; i >= 0; i--) {
            var addOrUpdateTrack = function(i) {

              var currentTrack = release.Tracks[i];
              trackUpdatePromises.push(
                model.Track.find({
                  where: {
                    id: release.Tracks[i].id
                  }
                }).then(function(track) {
                  console.log('Update track to udpate release');
                  console.log(currentTrack);
                  var deferred = Q.defer();
                  if (track) {
                    // If a track exists we update its data
                    newRelease.addTrack(track, {
                      position: currentTrack.ReleaseTracks.position
                    });

                    track.updateAttributes(currentTrack).then(function() {
                      var i = 0;
                      var newProducers = [];
                      for (i = currentTrack.Producer.length - 1; i >= 0; i--) {
                        newProducers.push(currentTrack.Producer[i].id);
                      }
                      var newRemixers = [];
                      for (i = currentTrack.Remixer.length - 1; i >= 0; i--) {
                        newRemixers.push(currentTrack.Remixer[i].id);
                      }
                      var newGenres = [];
                      for (i = currentTrack.Genres.length - 1; i >= 0; i--) {
                        newGenres.push(currentTrack.Genres[i]);
                      }
                      // Accept an array of promises functions.
                      // Call the done when all are successful
                      track.status = model.TrackStatus.TO_BE_PROCESSED;
                      Q.all([
                        track.setRemixer(newRemixers),
                        track.setProducer(newProducers),
                        track.save()
                      ]).done(function() {
                        deferred.resolve();
                      });
                    });
                  } else {
                    console.log('Create the track');
                    model.Track.create({
                      title: currentTrack.title,
                      version: currentTrack.version,
                      path: currentTrack.path
                    }).then(function(track) {
                      newRelease.addTrack(track, {
                        position: currentTrack.ReleaseTracks.position
                      });
                      deferred.resolve();
                    });
                  }
                  // Return the promise
                  return deferred.promise;
                }));
            };
            addOrUpdateTrack(i);
          }

          Q.allSettled(trackUpdatePromises)
            .then(function(results) {
              results.forEach(function() {
                console.log('Update Track Request Done');
              });
              model.Release.consolideJSON(releaseId).then(
                function(jsonRelease) {
                  console.log('===LOGGING RELEASE IN JSON FORMAT====');
                  console.log(jsonRelease);
                  rabbitmq.sendReleaseToProcess(jsonRelease);
                });
              res.send(results);
            });
        });
    });

  /**
   * POST '/releases/:releaseId/cover/createFile'
   * Request a CDN policy to upload a new release cover, if an update was
   * already in progress the old request is deleted from the CDN
   */
  app.post('/releases/:releaseId/cover/createFile',
    authenticationUtils.ensureAuthenticated,
    imagesController.createImageFactory('cover', helper));

  /**
   * POST '/releases/:releaseId/cover/confirmFile'
   * Confirm the upload of the requested new cover, store it in the database
   * as release information
   */
  app.post('/releases/:releaseId/cover/confirmFile',
    authenticationUtils.ensureAuthenticated,
    imagesController.confirmImageFactory(
      'cover', ['small', 'medium', 'large'], helper));

  /**
   * GET '/releases/:releaseId/cover/:size(small|large|medium)'
   * Get the release cover in the desired size, if it does not exist download
   * the original avatar and resize it
   */
  app.get('/releases/:releaseId/cover/:size(small|medium|large)',
    imagesController.getImageFactory('cover', helper));
}; /* End of release controller */
<commit_msg>Send processRelease message to broker only if a track file has actually been updated<commit_after>'use strict';

var Q = require('q');
var path = require('path');
var helper = rootRequire('helpers/releases');
var authenticationUtils = rootRequire('utils/authentication-utils');
var model = rootRequire('models/model');
var cloudstorage = rootRequire('libs/cdn/cloudstorage');
var rabbitmq = rootRequire('libs/message_broker/rabbitmq');
var imagesController = rootRequire('controllers/images');

module.exports.controller = function(app) {

  /**
   * Rollback a release back to the DropZone
   * 1) Restore DropZoneFiles entries for metadataFile, tracks and cover
   * 2) Remove tracks
   */
  function rollbackRelease(release, databaseRelease) {

    databaseRelease.status = model.ReleaseStatus.PROCESSING_FAILED;

    release.Tracks.forEach(function(track) {

      // Restore track dropzone files
      model.Track.find({
        where: {
          id: track.id
        }
      }).then(function(databaseTrack) {
        if (databaseTrack) {
          databaseTrack.path = track.path;
          databaseTrack.mp3Path = null;
          databaseTrack.snippetPath = null;
          databaseTrack.oggSnippetPath = null;
          databaseTrack.waveform = null;
          databaseTrack.lengthInSeconds = null;
          databaseTrack.bpm = null;
          databaseTrack.status = model.TrackStatus.PROCESSING_FAILED;
          databaseTrack.errorMessage = track.errorMessage;
          model.DropZoneFile.find({
            where: {
              path: track.path
            }
          }).then(function(file) {
            if (file) {
              file.status = 'UPLOADED';
              file.save();
              console.log(
                'Saving track ' + databaseTrack.id + ' Path ' + track.path);
              databaseTrack.save();
            }
          });
        }
      });
    });

    // Restore cover dropzone file
    model.DropZoneFile.find({
      where: {
        path: databaseRelease.cover
      }
    }).then(function(file) {
      if (file) {
        file.status = 'UPLOADED';
        file.save();
      }
    });

    // Restore metadata file
    model.DropZoneFile.find({
      where: {
        path: databaseRelease.metadataFile
      }
    }).then(function(file) {
      if (file) {
        file.status = 'UPLOADED';
        file.save();
      }
    });

    databaseRelease.save();
  }

  /**
   * Return a promise that updates a track given a track, a release Id and a
   * a new cover path
   */
  function updateTrackPromise(track, releaseId, newCoverPath) {
    // Updated track
    var deferredUpdate = Q.defer();
    model.Track.find({
      where: {
        id: track.id
      }
    }).then(function(databaseTrack) {
      if (!databaseTrack) {
        console.log('Track does not exist, update promise rejected');
        deferredUpdate.reject(new Error('Track does not exist for release'));
      } else {
        // Update track paths
        databaseTrack.waveform = track.waveform;
        databaseTrack.snippetPath = track.snippetPath;
        databaseTrack.oggSnippetPath = track.oggSnippetPath;
        databaseTrack.mp3Path = track.mp3Path;
        databaseTrack.lengthInSeconds = track.lengthInSeconds;
        databaseTrack.status = model.TrackStatus.PROCESSED;
        databaseTrack.cover = newCoverPath;
        databaseTrack.bpm = track.bpm;

        // Move lossless file
        var trackFilename = path.basename(databaseTrack.path);
        var newLosslessPath =
          helper.remoteReleasePath(releaseId, trackFilename);
        cloudstorage.copy(
          databaseTrack.path,
          newLosslessPath,
          function(err) {
            if (err) {
              deferredUpdate.reject(err);
            } else {
              databaseTrack.path = newLosslessPath;
              databaseTrack.save();
              deferredUpdate.resolve();
            }
          });
      }
    });

    // return update promise
    return deferredUpdate.promise;
  }

  /**
   * Returns a promise that removes a file from the DropZone's CDN
   */
  function deleteDropZoneFilePromise(dropZonePath) {
    var deferredDelete = Q.defer();
    model.DropZoneFile.find({
      where: {
        path: dropZonePath
      }
    }).then(function(file) {
      if (file) {
        file.destroy().then(function() {
          cloudstorage.remove(dropZonePath, function(err) {
            if (err) {
              deferredDelete.reject(err);
            } else {
              deferredDelete.resolve();
            }
          });
        });
      } else {
        console.log('DropZoneFile does not exist, delete promise rejected');
        deferredDelete.reject(new Error('DropZoneFile does not exist'));
      }
    });
    // Return delete promise
    return deferredDelete;
  }

  /**
   * Returns a promise that moves the cover of a release from the CDN to the
   * release directory
   */
  function moveCoverPromise(databaseRelease, newCoverPath) {
    var deferredMove = Q.defer();
    model.DropZoneFile.find({
      where: {
        path: databaseRelease.cover
      }
    }).then(function(file) {
      if (file) {
        file.destroy().then(function() {
          // Move lossless file
          var oldCoverPath = databaseRelease.cover;
          databaseRelease.cover = newCoverPath;
          cloudstorage.move(oldCoverPath, newCoverPath, function(err) {
            if (err) {
              deferredMove.reject(err);
            } else {
              deferredMove.resolve();
            }
          });
        });
      } else {
        console.log('DropZoneFile does not exist, delete promise rejected');
        deferredMove.reject(new Error('DropZoneFile does not exist'));
      }
    });

    // Return move promise
    return deferredMove.promise;
  }

  /**
   * Commits a release when its processing succeeded (A success message is
   * received from RabbitMq)
   * 1) Store tracks paths
   * 2) Move tracks lossless files
   * 3) Move release cover
   * 4) Remove DropZoneFiles entries
   * 5) Remove metadata file, if any
   */
  function commitRelease(release, databaseRelease) {

    databaseRelease.status = model.ReleaseStatus.PROCESSED;

    var updateTrackPromises = [];
    var deleteDropZoneFilePromises = [];

    var oldCoverPath = databaseRelease.cover;
    var coverFilename = path.basename(oldCoverPath);
    var newCoverPath =
      helper.remoteReleasePath(databaseRelease.id, coverFilename);

    for (var i = 0; i < release.Tracks.length; i++) {
      var track = release.Tracks[i];
      // Update track if it was marked for update
      if (track.status === model.TrackStatus.TO_BE_PROCESSED) {
        updateTrackPromises.push(
          updateTrackPromise(track, release.id, newCoverPath));
        deleteDropZoneFilePromises.push(deleteDropZoneFilePromise(track.path));
      }
    }

    Q.allSettled(updateTrackPromises).then(function(results) {

      var error = false;
      for (i = 0; i < results.length; i++) {
        if (results[i].state === 'rejected') {
          error = true;
          break;
        }
      }

      if (!error) {
        console.log('All tracks have been updated and files moved');
        Q.allSettled(deleteDropZoneFilePromises).then(function() {

          console.log('All track dropzone files deleted correctly');
          var morePromises = [];
          if (databaseRelease.metadataFile) {
            morePromises.push(
              deleteDropZoneFilePromise(databaseRelease.metadataFile));
          }

          if (databaseRelease.cover !== newCoverPath) {
            morePromises.push(
              moveCoverPromise(databaseRelease, newCoverPath));
          }

          Q.allSettled(morePromises).then(function() {
            databaseRelease.metadataFile = '';
            databaseRelease.save();
          });
        });
      } else {
        console.log('Track update failed');
        rollbackRelease(release, databaseRelease);
        return;
      }
    });
  }

  /**
   * Callback to the release result message on rabbitmq
   **/
  rabbitmq.onReleaseResult(
    function(message, headers, deliveryInfo) {

      if (deliveryInfo.contentType !== 'application/json') {
        console.log('Received status message is not in JSON format');
        return;
      }

      if (!message || !message.id) {
        console.log('Received status message is not a release');
        return;
      }

      var release = message;

      model.Release
        .find({
          where: {
            id: release.id
          }
        }).then(function(databaseRelease) {
          if (!databaseRelease) {
            return;
          }

          if (release.status === model.ReleaseStatus.PROCESSED) {
            console.log(
              'Release with id ' +
              release.id +
              ' has been correctly processed');
            commitRelease(release, databaseRelease);
          } else {
            console.log(
              'Release with id ' +
              release.id +
              ' processing failed');
            rollbackRelease(release, databaseRelease);
          }
        });
    });

  /**
   * GET /releases
   * Returns all releases
   * TODO pagination
   */
  app.get('/releases/', function(req, res) {

    model.Release.findAll({
      where: {
        isActive: true
      },
      order: 'position',
      include: [{
        model: model.Track,
        include: [{
          model: model.Artist,
          as: 'Remixer'
        }, {
          model: model.Artist,
          as: 'Producer'
        }]
      }, {
        model: model.Label
      }]
    }).then(function(releases) {
      res.send(releases);
    });
  });

  /**
   * GET /releases/:id
   * Return the release associated to the id
   */
  app.get('/releases/:id', function(req, res) {
    var releaseId = req.params.id;

    model.Release.find({
      where: {
        id: releaseId
      },
      order: 'position',
      include: [{
        model: model.Track,
        include: [{
          model: model.Artist,
          as: 'Remixer'
        }, {
          model: model.Artist,
          as: 'Producer'
        }, {
          model: model.Genre
        }]
      }, {
        model: model.Label
      }]
    }).then(function(release) {
      res.send(release);
    });
  });

  /**
   * POST /release/
   * Create a new release
   */
  app.post('/releases/',
    authenticationUtils.ensureAuthenticated,
    authenticationUtils.ensureAdmin,
    function(req, res) {

      var release = req.body.release;
      var idLabel = req.body.idLabel;
      console.log('____________' + idLabel);
      console.log('ADD RELEASE');
      console.log(release);
      model.Label.find({
        where: {
          id: idLabel
        }
      }).then(function(label) {
        model.Release.create(release).
        then(function(newRelease) {
          label.addReleases(newRelease).then(function() {
            model.Release.consolideJSON(newRelease.id);
            res.send(newRelease);
          });
        });
      });
    });

  /**
   * PUT /releases/:id
   * Update a release
   */
  app.put('/releases/:id',
    authenticationUtils.ensureAuthenticated, authenticationUtils.ensureAdmin,
    function(req, res) {
      var releaseId = req.params.id;
      var release = req.body.release;
      var trackUpdate = false;

      // Update the release
      model.Release
        .find({
          where: {
            id: releaseId
          }
        }).then(function(newRelease) {
          // newRelease is the current release
          release.status = model.ReleaseStatus.TO_BE_PROCESSED;
          newRelease.updateAttributes(release);
          var trackUpdatePromises = [];
          for (var i = release.Tracks.length - 1; i >= 0; i--) {
            var addOrUpdateTrack = function(i) {

              var currentTrack = release.Tracks[i];
              trackUpdatePromises.push(
                model.Track.find({
                  where: {
                    id: release.Tracks[i].id
                  }
                }).then(function(track) {
                  console.log('Update track to udpate release');
                  console.log(currentTrack);
                  var deferred = Q.defer();
                  if (track) {
                    // If a track exists we update its data
                    newRelease.addTrack(track, {
                      position: currentTrack.ReleaseTracks.position
                    });

                    track.updateAttributes(currentTrack).then(function() {
                      var i = 0;
                      var newProducers = [];
                      for (i = currentTrack.Producer.length - 1; i >= 0; i--) {
                        newProducers.push(currentTrack.Producer[i].id);
                      }
                      var newRemixers = [];
                      for (i = currentTrack.Remixer.length - 1; i >= 0; i--) {
                        newRemixers.push(currentTrack.Remixer[i].id);
                      }
                      var newGenres = [];
                      for (i = currentTrack.Genres.length - 1; i >= 0; i--) {
                        newGenres.push(currentTrack.Genres[i]);
                      }
                      // Accept an array of promises functions.
                      // Call the done when all are successful
                      // Set to be processed only if track file changed
                      if (track.path !== currentTrack.path) {
                        trackUpdate = true;
                        track.status = model.TrackStatus.TO_BE_PROCESSED;
                      }
                      Q.all([
                        track.setRemixer(newRemixers),
                        track.setProducer(newProducers),
                        track.save()
                      ]).done(function() {
                        deferred.resolve();
                      });
                    });
                  } else {
                    console.log('Create the track');
                    model.Track.create({
                      title: currentTrack.title,
                      version: currentTrack.version,
                      path: currentTrack.path
                    }).then(function(track) {
                      newRelease.addTrack(track, {
                        position: currentTrack.ReleaseTracks.position
                      });
                      deferred.resolve();
                    });
                  }
                  // Return the promise
                  return deferred.promise;
                }));
            };
            addOrUpdateTrack(i);
          }

          Q.allSettled(trackUpdatePromises)
            .then(function(results) {
              results.forEach(function() {
                console.log('Update Track Request Done');
              });
              model.Release.consolideJSON(releaseId, function(jsonRelease) {
                console.log('===LOGGING RELEASE IN JSON FORMAT====');
                console.log(jsonRelease);
                if (trackUpdate) {
                  rabbitmq.sendReleaseToProcess(jsonRelease);
                }
              });
              res.send(results);
            });
        });
    });

  /**
   * POST '/releases/:releaseId/cover/createFile'
   * Request a CDN policy to upload a new release cover, if an update was
   * already in progress the old request is deleted from the CDN
   */
  app.post('/releases/:releaseId/cover/createFile',
    authenticationUtils.ensureAuthenticated,
    imagesController.createImageFactory('cover', helper));

  /**
   * POST '/releases/:releaseId/cover/confirmFile'
   * Confirm the upload of the requested new cover, store it in the database
   * as release information
   */
  app.post('/releases/:releaseId/cover/confirmFile',
    authenticationUtils.ensureAuthenticated,
    imagesController.confirmImageFactory(
      'cover', ['small', 'medium', 'large'], helper));

  /**
   * GET '/releases/:releaseId/cover/:size(small|large|medium)'
   * Get the release cover in the desired size, if it does not exist download
   * the original avatar and resize it
   */
  app.get('/releases/:releaseId/cover/:size(small|medium|large)',
    imagesController.getImageFactory('cover', helper));
}; /* End of release controller */
<|endoftext|>


<commit_before>// ==UserScript==
// @name          Ask Ubuntu Chat Status
// @author        Nathan Osman
// @version       1.1
// @namespace     https://quickmediasolutions.com
// @description   A UserScript for displaying the status of other users in chat.
// @include       http://chat.stackexchange.com/rooms/201/*
// ==/UserScript==

function withjQuery(fn) {
    var code = document.createElement('script');
    code.type = 'text/javascript';
    code.textContent = '(' + fn.toString() + ')(jQuery);';
    document.body.appendChild(code);
}

withjQuery(function($) {

    // Constants.
    var SERVER = 'auchat.quickmediasolutions.com';

    // Retrieve the current UTC time as a Unix timestamp.
    function now() {
        return parseInt(new Date().getTime() / 1000);
    }

    // Given an email hash from chat, determine the user's avatar.
    function avatar(u) {
        if(u.email_hash[0] == '!') {
            return u.email_hash.slice(1);
        } else {
            return 'http://www.gravatar.com/avatar/' + u.email_hash +
                '?s=16&d=identicon&r=PG';
        }
    }

    // Display a drop-down notification in the top of the chat window.
    function notify(msg) {
        window.Notifier().notify(msg);
    }

    // Check for updates to the chat script once every hour.
    function updateCheck() {
        $.ajax({
            complete: function() {
                window.setTimeout(updateCheck, 60 * 60 * 1000);
            },
            dataType: 'json',
            error: function() {
                notify('Unable to communicate with the chat status API.');
            },
            success: function(data) {
                if('version' in data) {
                    if(data.version != '1.1') {
                        var msg = "
" +
                                "New version of the chat status script available.
Please update as soon as possible.
";
                        if('changes' in data) {
                            msg += '
';
                            $.each(data.changes, function(i, v) {
                                // v is HTML-escaped on the server
                                msg += '
' + v + '
';
                            });
                            msg += '
';
                        }
                        notify(msg);
                    }
                } else {
                    notify('Received corrupt version number from the chat status API.');
                }
            },
            url: 'https://' + SERVER + '/api/version'
        });
    }
    updateCheck();

    // Add the element that will display typing status.
    $('#chat').css('paddingBottom', '0');
    var typingStatus = $('
')
            .css({
                color: 'rgba(0, 0, 0, 0.5)',
                height: '20px',
                paddingBottom: '90px',
                paddingLeft: '70px',
                paddingTop: '10px'
            })
            .insertAfter('#chat'),
        typingIndicator = $('
')
            .hide()
            .text('is typing...')
            .appendTo(typingStatus);

    // Establish a websocket connection.
    function connect() {
        var socket = new WebSocket('wss://' + SERVER + '/api/connect');

        // JavaScript doesn't let us send ping/pong control messages, so
        // instead, just send an empty message and the server will reply with
        // an empty message.
        var pingTimeout;
        function ping() {
            socket.send('');
            pingTimeout = window.setTimeout(ping, 30000);
        }

        // The status API will send a message when a user types a character. To
        // avoid a bunch of unnecessary messages, this message is only sent a
        // maximum of once per two seconds. Therefore the "xyz is typing..."
        // indicator must assume the user is typing for up to four seconds
        // after having received the last such update.
        var usersTyping = {};

        // A user has stopped typing (the timeout expired). The indicator
        // should be hidden if no more users are typing - and there's a small
        // hack to figure that out.
        function userTypingStopped(id) {
            usersTyping[id].remove();
            delete usersTyping[id];
            var anyUsersTyping = false;
            $.each(usersTyping, function() {
                anyUsersTyping = true;
                return false;
            });
            if(!anyUsersTyping) {
                typingIndicator.hide();
            }
        }

        // A user has started typing. Create the element for the indicator if
        // one does not already exist. If one does, reset the timeout.
        function userTypingStarted(id) {
            var elem;
            if(id in usersTyping) {
                elem = usersTyping[id];
                window.clearTimeout(elem.data('timeout'));
            } else {
                elem = $('')
                    .addClass('acs-typing-avatar')
                    .css({
                        float: 'left',
                        height: '16px',
                        paddingRight: '4px',
                        width: '16px'
                    });
                usersTyping[id] = elem;
                CHAT.RoomUsers.get(id).then(function(u) {
                    elem.attr('src', avatar(u));
                });
            }
            elem.detach().prependTo(typingStatus).show();
            typingIndicator.show();
            elem.data('timeout', window.setTimeout(userTypingStopped, 4000, id));
        }

        // The user typed a message.
        var lastCharEntered = 0;
        function userTyped() {
            var n = now();
            if(lastCharEntered < (n - 2)) {
                socket.send(JSON.stringify({
                    id: CHAT.CURRENT_USER_ID,
                    data: {
                        last_char_entered: n.toString()
                    }
                }));
                lastCharEntered = n;
            }
        }

        // Monitor the input field for key presses. If this is the first
        // keypress during the last two seconds, immediately notify everyone.
        // This prevents a message being sent for every keypress.
        socket.onopen = function() {
            console.log("Connection to server established.")
            ping();
            $('#input').on('keypress', userTyped);
        }

        // Process new messages received on the socket - note that all packets are
        // assumed valid - invalid ones will generate an error but won't prevent
        // more from being processed in the future.
        socket.onmessage = function(e) {
            if(e.data.length) {
                var s = JSON.parse(e.data);
                if('last_char_entered' in s.data &&
                        s.data.last_char_entered > (now() - 5)) {
                    userTypingStarted(s.id);
                }
            }
        };

        // TODO: better error handling (no idea when this gets called)
        socket.onerror = function(e) {
            console.log(e);
        };

        // Assume the server is restarting or something and try to reconnect in
        // one minute intervals. Cancel some pending timeouts.
        socket.onclose = function() {
            console.log("Server closed connection.");
            $('#input').off('keypress', userTyped);
            window.clearTimeout(pingTimeout);
            window.setTimeout(connect, 60000);
        }
    }
    connect();
});
<commit_msg>Removed old UserScript.<commit_after><|endoftext|>


<commit_before>const passport = require('passport');
const Request = require('../models/Request');

/**
 * GET /request/new
 * Request a pickup page.
 */
exports.getRequest = (req, res) => {
  if (!req.user) {
    return res.redirect('/login');
  }
  res.render('requests/new', {
    title: 'Request Pickup'
  });
};

/**
 * POST /request/new
 * Create a new request.
 */
exports.postRequest = (req, res) => {
  // req.assert('date', 'Date must be in the past').len(4);
  req.sanitize('email').normalizeEmail({ remove_dots: false });

  const errors = req.validationErrors();

  if (errors) {
    req.flash('errors', errors);
    return res.redirect('requests/new');
  }
  let requestData = {
    user: req.user.id,
    address: req.body.address,
    date: req.body.date,
    time: req.body.time,
    // type: req.body.type,
    // amount: req.body.amount,
    note: req.body.note
    // recurring: req.body.recurring
  };

  const request = new Request(requestData);

  request.save((err) => {
    if (err) { return next(err); }
  res.redirect('/');
  });
};

/**
 * GET /request/show
 * Show pickups for this user.
 */
exports.getRequests = (req, res) => {
  if (!req.user) {
    return res.redirect('/login');
  }

  // find the requests for this person

  // if (req.user.admin) {
  //   Request
  //     .find()
  //     .populate('user')
  //     .sort({ _id: -1 })
  //     .limit(30)
  //     .exec((err, requests) => {
  //       res.title = 'Show Requests'
  //       res.render('requests/show', {requests});
  //     });
  // } else {
  // }
    Request
      .find({ user: req.user.id })
      .populate('user')
      .sort({ _id: -1 })
      .limit(10)
      .exec((err, requests) => {
        res.title = 'Show Requests'
        res.render('requests/show', {requests});
      });
};
<commit_msg>fixed request/new loop was directing to / which was going to request/new.<commit_after>const passport = require('passport');
const Request = require('../models/Request');

/**
 * GET /request/new
 * Request a pickup page.
 */
exports.getRequest = (req, res) => {
  if (!req.user) {
    return res.redirect('/login');
  }
  res.render('requests/new', {
    title: 'Request Pickup'
  });
};

/**
 * POST /request/new
 * Create a new request.
 */
exports.postRequest = (req, res) => {
  // req.assert('date', 'Date must be in the past').len(4);
  req.sanitize('email').normalizeEmail({ remove_dots: false });

  const errors = req.validationErrors();

  if (errors) {
    req.flash('errors', errors);
    return res.redirect('requests/new');
  }
  let requestData = {
    user: req.user.id,
    address: req.body.address,
    date: req.body.date,
    time: req.body.time,
    // type: req.body.type,
    // amount: req.body.amount,
    note: req.body.note
    // recurring: req.body.recurring
  };

  const request = new Request(requestData);

  request.save((err) => {
    if (err) { return next(err); }
  res.redirect('/requests');
  });
};

/**
 * GET /request/show
 * Show pickups for this user.
 */
exports.getRequests = (req, res) => {
  if (!req.user) {
    return res.redirect('/login');
  }

  // find the requests for this person

  // if (req.user.admin) {
  //   Request
  //     .find()
  //     .populate('user')
  //     .sort({ _id: -1 })
  //     .limit(30)
  //     .exec((err, requests) => {
  //       res.title = 'Show Requests'
  //       res.render('requests/show', {requests});
  //     });
  // } else {
  // }
    Request
      .find({ user: req.user.id })
      .populate('user')
      .sort({ _id: -1 })
      .limit(10)
      .exec((err, requests) => {
        res.title = 'Show Requests'
        res.render('requests/show', {requests});
      });
};
<|endoftext|>


<commit_before>/*
Input Mask plugin for jquery
http://github.com/RobinHerbots/jquery.inputmask
Copyright (c) 2010 Robin Herbots
Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php)
Version: 0.0.5

This plugin is based on the masked input plugin written by Josh Bush (digitalbush.com)
*/

(function($) {

    $.fn.inputmask = function(fn, options) {
        //options default
        var defaults = {
            placeholder: "_",
            mask: null,
            onComplete: null,
            repeat: 0, //repetitions of the mask
            definitions: {
                '9': {
                    "validator": "[0-9]",
                    "cardinality": 1,
                    'prevalidator': null
                },
                'a': {
                    "validator": "[A-Za-z]",
                    "cardinality": 1,
                    "prevalidator": null
                },
                '*': {
                    "validator": "[A-Za-z0-9]",
                    "cardinality": 1,
                    "prevalidator": null
                },
                'd': { //day
                    "validator": "0[1-9]|[12][0-9]|3[01]",
                    "cardinality": 2,
                    "prevalidator": [{ "validator": "[0-3]", "cardinality": 1}]
                },
                'm': { //month
                    "validator": "0[1-9]|1[012]",
                    "cardinality": 2,
                    "prevalidator": [{ "validator": "[01]", "cardinality": 1}]
                },
                'y': { //year
                    "validator": "(19|20)\\d\\d",
                    "cardinality": 4,
                    "prevalidator": [
                        { "validator": "[12]", "cardinality": 1 },
                        { "validator": "(19|20)", "cardinality": 2 },
                        { "validator": "(19|20)\\d", "cardinality": 3 }
                        ]
                }
            },
            keyCode: { ALT: 18, BACKSPACE: 8, CAPS_LOCK: 20, COMMA: 188, COMMAND: 91, COMMAND_LEFT: 91, COMMAND_RIGHT: 93, CONTROL: 17, DELETE: 46, DOWN: 40, END: 35, ENTER: 13, ESCAPE: 27, HOME: 36, INSERT: 45, LEFT: 37, MENU: 93, NUMPAD_ADD: 107, NUMPAD_DECIMAL: 110, NUMPAD_DIVIDE: 111, NUMPAD_ENTER: 108,
                NUMPAD_MULTIPLY: 106, NUMPAD_SUBTRACT: 109, PAGE_DOWN: 34, PAGE_UP: 33, PERIOD: 190, RIGHT: 39, SHIFT: 16, SPACE: 32, TAB: 9, UP: 38, WINDOWS: 91
            }
        };

        var opts = $.extend({}, defaults, options);

        if (typeof fn == "string") {
            if (fn == 'mask') {
                //init buffer
                var _buffer = getMaskTemplate();
                var pasteEventName = $.browser.msie ? 'paste' : 'input';
                var iPhone = (window.orientation != undefined);
                var tests = getTestingChain();

                return this.each(function() {
                    mask($(this));
                });
            } else if (fn == 'unmaskedvalue') {
                return unmaskedvalue($(this));
            } else { //maybe fn is a mask so we try
                //set mask
                opts.mask = fn;

                //init buffer
                var _buffer = getMaskTemplate();
                var pasteEventName = $.browser.msie ? 'paste' : 'input';
                var iPhone = (window.orientation != undefined);
                var tests = getTestingChain();

                return this.each(function() {
                    mask($(this));
                });
            }
        } if (typeof fn == "object") {
            opts = $.extend({}, defaults, fn);

            //init buffer
            var _buffer = getMaskTemplate();
            var pasteEventName = $.browser.msie ? 'paste' : 'input';
            var iPhone = (window.orientation != undefined);
            var tests = getTestingChain();

            return this.each(function() {
                mask($(this));
            });
        }

        //functions
        function getMaskTemplate() {
            var singleMask = $.map(opts.mask.split(""), function(element, index) {
                var outElem = [];
                var maskdef = opts.definitions[element];
                if (maskdef) {
                    for (i = 0; i < maskdef.cardinality; i++) {
                        outElem.push(opts.placeholder);
                    }
                } else outElem.push(element);

                return outElem;
            });

            var repeatedMask = singleMask.slice();
            for (var i = 0; i < opts.repeat; i++) {
                repeatedMask = repeatedMask.concat(singleMask.slice());
            }
            return repeatedMask;
        };

        function getTestingChain() {
            return $.map(opts.mask.split(""), function(element, index) {
                var outElem = [];
                var maskdef = opts.definitions[element];
                if (maskdef) {
                    for (i = 1; i < maskdef.cardinality; i++) {
                        var prevalidator = maskdef.prevalidator[i - 1];
                        outElem.push([new RegExp(prevalidator.validator), prevalidator.cardinality]);
                    }
                    outElem.push([new RegExp(maskdef.validator), maskdef.cardinality]);
                } else outElem.push(null);

                return outElem;
            });
        };

        function unmaskedvalue(el) {
            var tests = el.data('tests');
            if (tests) {
                return $.map(el.val().split(""), function(element, index) {
                    return tests[index] ? element : null;
                }).join('');
            }
            else {
                return el.val();
            }
        }

        function mask(el) {
            var input = $(el);
            //store tests in the input element - used to get the unmasked value
            input.data('tests', tests);

            //init buffer
            var buffer = _buffer.slice();
            var undoBuffer = input.val();
            var ignore = false;              //Variable for ignoring control keys
            var len = _buffer.length;

            //bind events
            if (!input.attr("readonly")) {
                input.bind("focus", function() {
                    input.addClass('focus');
                    undoBuffer = input.val();
                    var pos = checkVal();
                    writeBuffer();
                    setTimeout(function() {
                        if (pos >= _buffer.length)
                            caret(input, 0, pos);
                        else
                            caret(input, pos);
                    }, 0);
                }).bind("mouseenter", function() {
                    if (!input.hasClass('focus') && input.val().length == 0)
                        writeBuffer();
                }).bind("blur", function() {
                    input.removeClass('focus');
                    if (input.val() == _buffer.join('')) {
                        input.val('');
                    } else {
                        checkVal();
                        if (input.val() != undoBuffer)
                            input.change();
                    }
                }).bind("mouseleave", function() {
                    if (!input.hasClass('focus') && input.val() == _buffer.join(''))
                        input.val('');
                }).bind("keydown", keydownEvent
                ).bind("keypress", keypressEvent
                ).bind(pasteEventName, function() {
                    setTimeout(function() { caret(input, checkVal(true)); }, 0);
                });

            }

            checkVal();

            //private functions
            function writeBuffer() { return input.val(buffer.join('')).val(); };
            function clearBuffer(start, end) {
                for (var i = start; i < end && i < len; i++) {
                    buffer[i] = _buffer[i];
                }
            };

            function seekNext(pos) {
                while (++pos <= len && !isMask(pos));
                return pos;
            };

            function shiftL(pos) {
                while (!isMask(pos) && --pos >= 0);
                for (var i = pos; i < len; i++) {
                    if (isMask(i)) {
                        buffer[i] = opts.placeholder;
                        var j = seekNext(i);
                        if (j < len && isValid(i, buffer[j])) {
                            buffer[i] = buffer[j];
                        } else
                            break;
                    }
                }
                writeBuffer();
                caret(input, pos);
            };

            function shiftR(pos) {
                for (var i = pos, c = opts.placeholder; i < len; i++) {
                    if (isMask(i)) {
                        var j = seekNext(i);
                        var t = buffer[i];
                        buffer[i] = c;
                        if (j < len && isValid(j, t))
                            c = t;
                        else
                            break;
                    }
                }
            };

            function caret(input, begin, end) {
                if (input.length == 0) return;
                if (typeof begin == 'number') {
                    end = (typeof end == 'number') ? end : begin;
                    return input.each(function() {
                        if (this.setSelectionRange) {
                            this.focus();
                            this.setSelectionRange(begin, end);
                        } else if (this.createTextRange) {
                            var range = this.createTextRange();
                            range.collapse(true);
                            range.moveEnd('character', end);
                            range.moveStart('character', begin);
                            range.select();
                        }
                    });
                } else {
                    if (input[0].setSelectionRange) {
                        begin = input[0].selectionStart;
                        end = input[0].selectionEnd;
                    } else if (document.selection && document.selection.createRange) {
                        var range = document.selection.createRange();
                        begin = 0 - range.duplicate().moveStart('character', -100000);
                        end = begin + range.text.length;
                    }
                    return { begin: begin, end: end };
                }
            };

            function keydownEvent(e) {
                var pos = caret($(this));
                var k = e.keyCode;
                ignore = (k < 16 || (k > 16 && k < 32) || (k > 32 && k < 41));

                //delete selection before proceeding
                if ((pos.begin - pos.end) != 0 && (!ignore || k == opts.keyCode.BACKSPACE || k == opts.keyCode.DELETE))
                    clearBuffer(pos.begin, pos.end);

                //backspace, delete, and escape get special treatment
                if (k == opts.keyCode.BACKSPACE || k == opts.keyCode.DELETE || (iPhone && k == 127)) {//backspace/delete
                    shiftL(pos.begin + (k == opts.keyCode.DELETE ? 0 : -1));
                    return false;
                } else if (k == opts.keyCode.ESCAPE) {//escape
                    input.val(undoBuffer);
                    caret(input, 0, checkVal());
                    return false;
                }
            }

            function keypressEvent(e) {
                if (ignore) {
                    ignore = false;
                    //Fixes Mac FF bug on backspace
                    return (e.keyCode == 8) ? false : null;
                }
                e = e || window.event;
                var k = e.charCode || e.keyCode || e.which;
                var pos = caret($(this));

                if (e.ctrlKey || e.altKey || e.metaKey) {//Ignore
                    return true;
                } else if ((k >= 32 && k <= 125) || k > 186) {//typeable characters
                    var p = seekNext(pos.begin - 1);
                    if (p < len) {
                        var c = String.fromCharCode(k);
                        if (isValid(p, c)) {
                            shiftR(p);
                            buffer[p] = c;
                            writeBuffer();
                            var next = seekNext(p);
                            caret($(this), next);
                            if (opts.onComplete && next == len)
                                opts.onComplete.call(input);
                        }
                    }
                }
                return false;
            }

            function isValid(pos, c) {
                var testPos = determineTestPosition(pos);

                var loopend = 0;
                if (c) { loopend = 1; ; }

                var chrs = '';
                for (var i = tests[testPos][1]; i > loopend; i--) {
                    chrs += buffer[testPos - (i - 1)];
                }

                if (c) { chrs += c; }

                return tests[testPos][0].test(chrs);
            }

            function isMask(pos) {
                return tests[determineTestPosition(pos)];
            }

            function determineTestPosition(pos) {
                return pos % tests.length;
            }

            function checkVal(clearInvalid) {
                var inputValue = input.val();
                buffer = _buffer.slice(); //clear buffer
                var lastMatch = -1;
                for (var i = 0; i < inputValue.length; i++) {
                    for (var pos = lastMatch + 1; pos < len; pos++) {
                        if (isMask(pos)) {
                            buffer[pos] = opts.placeholder;
                            if (isValid(pos, inputValue.charAt(i))) {
                                buffer[pos] = inputValue.charAt(i);
                                lastMatch = pos;
                            }
                            break;
                        } else {   //nonmask
                            lastMatch++;
                        }
                    }
                }
                if (clearInvalid) {
                    writeBuffer();
                }
                return seekNext(lastMatch);
            };
        }
    };

})(jQuery);<commit_msg>bugfix for mouseenter event<commit_after>/*
Input Mask plugin for jquery
http://github.com/RobinHerbots/jquery.inputmask
Copyright (c) 2010 Robin Herbots
Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php)
Version: 0.0.5

This plugin is based on the masked input plugin written by Josh Bush (digitalbush.com)
*/

(function($) {

    $.fn.inputmask = function(fn, options) {
        //options default
        var defaults = {
            placeholder: "_",
            mask: null,
            onComplete: null,
            repeat: 0, //repetitions of the mask
            definitions: {
                '9': {
                    "validator": "[0-9]",
                    "cardinality": 1,
                    'prevalidator': null
                },
                'a': {
                    "validator": "[A-Za-z]",
                    "cardinality": 1,
                    "prevalidator": null
                },
                '*': {
                    "validator": "[A-Za-z0-9]",
                    "cardinality": 1,
                    "prevalidator": null
                },
                'd': { //day
                    "validator": "0[1-9]|[12][0-9]|3[01]",
                    "cardinality": 2,
                    "prevalidator": [{ "validator": "[0-3]", "cardinality": 1}]
                },
                'm': { //month
                    "validator": "0[1-9]|1[012]",
                    "cardinality": 2,
                    "prevalidator": [{ "validator": "[01]", "cardinality": 1}]
                },
                'y': { //year
                    "validator": "(19|20)\\d\\d",
                    "cardinality": 4,
                    "prevalidator": [
                        { "validator": "[12]", "cardinality": 1 },
                        { "validator": "(19|20)", "cardinality": 2 },
                        { "validator": "(19|20)\\d", "cardinality": 3 }
                        ]
                }
            },
            keyCode: { ALT: 18, BACKSPACE: 8, CAPS_LOCK: 20, COMMA: 188, COMMAND: 91, COMMAND_LEFT: 91, COMMAND_RIGHT: 93, CONTROL: 17, DELETE: 46, DOWN: 40, END: 35, ENTER: 13, ESCAPE: 27, HOME: 36, INSERT: 45, LEFT: 37, MENU: 93, NUMPAD_ADD: 107, NUMPAD_DECIMAL: 110, NUMPAD_DIVIDE: 111, NUMPAD_ENTER: 108,
                NUMPAD_MULTIPLY: 106, NUMPAD_SUBTRACT: 109, PAGE_DOWN: 34, PAGE_UP: 33, PERIOD: 190, RIGHT: 39, SHIFT: 16, SPACE: 32, TAB: 9, UP: 38, WINDOWS: 91
            }
        };

        var opts = $.extend({}, defaults, options);

        if (typeof fn == "string") {
            if (fn == 'mask') {
                //init buffer
                var _buffer = getMaskTemplate();
                var pasteEventName = $.browser.msie ? 'paste' : 'input';
                var iPhone = (window.orientation != undefined);
                var tests = getTestingChain();

                return this.each(function() {
                    mask($(this));
                });
            } else if (fn == 'unmaskedvalue') {
                return unmaskedvalue($(this));
            } else { //maybe fn is a mask so we try
                //set mask
                opts.mask = fn;

                //init buffer
                var _buffer = getMaskTemplate();
                var pasteEventName = $.browser.msie ? 'paste' : 'input';
                var iPhone = (window.orientation != undefined);
                var tests = getTestingChain();

                return this.each(function() {
                    mask($(this));
                });
            }
        } if (typeof fn == "object") {
            opts = $.extend({}, defaults, fn);

            //init buffer
            var _buffer = getMaskTemplate();
            var pasteEventName = $.browser.msie ? 'paste' : 'input';
            var iPhone = (window.orientation != undefined);
            var tests = getTestingChain();

            return this.each(function() {
                mask($(this));
            });
        }

        //functions
        function getMaskTemplate() {
            var singleMask = $.map(opts.mask.split(""), function(element, index) {
                var outElem = [];
                var maskdef = opts.definitions[element];
                if (maskdef) {
                    for (i = 0; i < maskdef.cardinality; i++) {
                        outElem.push(opts.placeholder);
                    }
                } else outElem.push(element);

                return outElem;
            });

            var repeatedMask = singleMask.slice();
            for (var i = 0; i < opts.repeat; i++) {
                repeatedMask = repeatedMask.concat(singleMask.slice());
            }
            return repeatedMask;
        };

        function getTestingChain() {
            return $.map(opts.mask.split(""), function(element, index) {
                var outElem = [];
                var maskdef = opts.definitions[element];
                if (maskdef) {
                    for (i = 1; i < maskdef.cardinality; i++) {
                        var prevalidator = maskdef.prevalidator[i - 1];
                        outElem.push([new RegExp(prevalidator.validator), prevalidator.cardinality]);
                    }
                    outElem.push([new RegExp(maskdef.validator), maskdef.cardinality]);
                } else outElem.push(null);

                return outElem;
            });
        };

        function unmaskedvalue(el) {
            var tests = el.data('tests');
            if (tests) {
                return $.map(el.val().split(""), function(element, index) {
                    return tests[index] ? element : null;
                }).join('');
            }
            else {
                return el.val();
            }
        }

        function mask(el) {
            var input = $(el);
            //store tests in the input element - used to get the unmasked value
            input.data('tests', tests);

            //init buffer
            var buffer = _buffer.slice();
            var undoBuffer = input.val();
            var ignore = false;              //Variable for ignoring control keys
            var len = _buffer.length;

            //bind events
            if (!input.attr("readonly")) {
                input.bind("focus", function() {
                    input.addClass('focus');
                    undoBuffer = input.val();
                    var pos = checkVal();
                    writeBuffer();
                    setTimeout(function() {
                        if (pos >= _buffer.length)
                            caret(input, 0, pos);
                        else
                            caret(input, pos);
                    }, 0);
                }).bind("mouseenter", function() {
                    if (!input.hasClass('focus') && input.val().length == 0) {
                        buffer = _buffer.slice();
                        writeBuffer();
                    }
                }).bind("blur", function() {
                    input.removeClass('focus');
                    if (input.val() == _buffer.join('')) {
                        input.val('');
                    } else {
                        checkVal();
                        if (input.val() != undoBuffer)
                            input.change();
                    }
                }).bind("mouseleave", function() {
                    if (!input.hasClass('focus') && input.val() == _buffer.join(''))
                        input.val('');
                }).bind("keydown", keydownEvent
                ).bind("keypress", keypressEvent
                ).bind(pasteEventName, function() {
                    setTimeout(function() { caret(input, checkVal(true)); }, 0);
                });

            }

            checkVal();

            //private functions
            function writeBuffer() { return input.val(buffer.join('')).val(); };
            function clearBuffer(start, end) {
                for (var i = start; i < end && i < len; i++) {
                    buffer[i] = _buffer[i];
                }
            };

            function seekNext(pos) {
                while (++pos <= len && !isMask(pos));
                return pos;
            };

            function shiftL(pos) {
                while (!isMask(pos) && --pos >= 0);
                for (var i = pos; i < len; i++) {
                    if (isMask(i)) {
                        buffer[i] = opts.placeholder;
                        var j = seekNext(i);
                        if (j < len && isValid(i, buffer[j])) {
                            buffer[i] = buffer[j];
                        } else
                            break;
                    }
                }
                writeBuffer();
                caret(input, pos);
            };

            function shiftR(pos) {
                for (var i = pos, c = opts.placeholder; i < len; i++) {
                    if (isMask(i)) {
                        var j = seekNext(i);
                        var t = buffer[i];
                        buffer[i] = c;
                        if (j < len && isValid(j, t))
                            c = t;
                        else
                            break;
                    }
                }
            };

            function caret(input, begin, end) {
                if (input.length == 0) return;
                if (typeof begin == 'number') {
                    end = (typeof end == 'number') ? end : begin;
                    return input.each(function() {
                        if (this.setSelectionRange) {
                            this.focus();
                            this.setSelectionRange(begin, end);
                        } else if (this.createTextRange) {
                            var range = this.createTextRange();
                            range.collapse(true);
                            range.moveEnd('character', end);
                            range.moveStart('character', begin);
                            range.select();
                        }
                    });
                } else {
                    if (input[0].setSelectionRange) {
                        begin = input[0].selectionStart;
                        end = input[0].selectionEnd;
                    } else if (document.selection && document.selection.createRange) {
                        var range = document.selection.createRange();
                        begin = 0 - range.duplicate().moveStart('character', -100000);
                        end = begin + range.text.length;
                    }
                    return { begin: begin, end: end };
                }
            };

            function keydownEvent(e) {
                var pos = caret($(this));
                var k = e.keyCode;
                ignore = (k < 16 || (k > 16 && k < 32) || (k > 32 && k < 41));

                //delete selection before proceeding
                if ((pos.begin - pos.end) != 0 && (!ignore || k == opts.keyCode.BACKSPACE || k == opts.keyCode.DELETE))
                    clearBuffer(pos.begin, pos.end);

                //backspace, delete, and escape get special treatment
                if (k == opts.keyCode.BACKSPACE || k == opts.keyCode.DELETE || (iPhone && k == 127)) {//backspace/delete
                    shiftL(pos.begin + (k == opts.keyCode.DELETE ? 0 : -1));
                    return false;
                } else if (k == opts.keyCode.ESCAPE) {//escape
                    input.val(undoBuffer);
                    caret(input, 0, checkVal());
                    return false;
                }
            }

            function keypressEvent(e) {
                if (ignore) {
                    ignore = false;
                    //Fixes Mac FF bug on backspace
                    return (e.keyCode == 8) ? false : null;
                }
                e = e || window.event;
                var k = e.charCode || e.keyCode || e.which;
                var pos = caret($(this));

                if (e.ctrlKey || e.altKey || e.metaKey) {//Ignore
                    return true;
                } else if ((k >= 32 && k <= 125) || k > 186) {//typeable characters
                    var p = seekNext(pos.begin - 1);
                    if (p < len) {
                        var c = String.fromCharCode(k);
                        if (isValid(p, c)) {
                            shiftR(p);
                            buffer[p] = c;
                            writeBuffer();
                            var next = seekNext(p);
                            caret($(this), next);
                            if (opts.onComplete && next == len)
                                opts.onComplete.call(input);
                        }
                    }
                }
                return false;
            }

            function isValid(pos, c) {
                var testPos = determineTestPosition(pos);

                var loopend = 0;
                if (c) { loopend = 1; ; }

                var chrs = '';
                for (var i = tests[testPos][1]; i > loopend; i--) {
                    chrs += buffer[testPos - (i - 1)];
                }

                if (c) { chrs += c; }

                return tests[testPos][0].test(chrs);
            }

            function isMask(pos) {
                return tests[determineTestPosition(pos)];
            }

            function determineTestPosition(pos) {
                return pos % tests.length;
            }

            function checkVal(clearInvalid) {
                var inputValue = input.val();
                buffer = _buffer.slice(); //clear buffer
                var lastMatch = -1;
                for (var i = 0; i < inputValue.length; i++) {
                    for (var pos = lastMatch + 1; pos < len; pos++) {
                        if (isMask(pos)) {
                            buffer[pos] = opts.placeholder;
                            if (isValid(pos, inputValue.charAt(i))) {
                                buffer[pos] = inputValue.charAt(i);
                                lastMatch = pos;
                            }
                            break;
                        } else {   //nonmask
                            lastMatch++;
                        }
                    }
                }
                if (clearInvalid) {
                    writeBuffer();
                }
                return seekNext(lastMatch);
            };
        }
    };

})(jQuery);<|endoftext|>


<commit_before>/**
 * Created by tim on 10/7/15.
 */
var svgNS = "http://www.w3.org/2000/svg";

var trafficManager;
var trafficModel;
var roadView;

var Light = function () {
    this.color = "green";
    this.phase = 2;
    this.yellowDwell = 3;
    this.location = 400;
    this.size = 30;
    this.SVGRect = null;

    //  make the svg, put the rect inside
    //  attach an event handler to the larger svg
    //  somehow give it an ID so we know which light gets clicked.

    this.SVGRect = document.createElementNS(svgNS, "rect");
    this.SVGRect.setAttribute("fill", this.color);
    this.SVGRect.setAttribute("width", this.size);
    this.SVGRect.setAttribute("height", roadView.roadSVG.getAttribute("height"));

    roadView.roadSVG.appendChild(this.SVGRect);         //  here we put the new object into the DOM.

};

Light.prototype.setColor = function (time, period) {
    var theta = time % period;
    if (this.phase > period / 2 && theta < period / 2) {
        theta += period;
    }
    if (theta > this.phase && theta < (this.phase + period / 2 - this.yellowDwell)) {
        this.color = "green";
    } else if (theta > this.phase + period / 2 - this.yellowDwell && theta < this.phase + period / 2) {
        this.color = "yellow";
    } else {
        this.color = "red";
    }
};

Light.prototype.draw = function ( ) {
    this.SVGRect.setAttribute("fill", this.color);
    this.SVGRect.setAttribute("x", this.location - this.size/2);
    this.SVGRect.setAttribute("y", 0);

};

roadView = {

    roadSVG: null,

    initialize: function () {
        this.roadSVG = document.getElementById("road");

    },

    draw: function () {
        this.drawRoad();
        var i;
        for (i = 0; i < trafficModel.lightSystem.lights.length; i++) {
            trafficModel.lightSystem.lights[i].draw( );
        }
        for (i = 0; i < trafficModel.cars.length; i++) {
            trafficModel.cars[i].draw( );
        }

    },

    drawRoad: function () {

    },

    clickOnRoad: function () {

    }
};

trafficModel = {
    time: 0,
    cars: [],
    lightSystem: {lights: [], period: 16},
    streetLength: null,

    update: function (dt) {
        this.time += dt;
        var i;

        //  for every car, find its environment (situation) with cars in the current positions
        for ( i = 0; i < this.cars.length; i++) {
            this.cars[i].decision = this.cars[i].decide(this.findCarEnvironment(i));
        }

        //  now have each car update its position
        for ( i = 0; i < this.cars.length; i++) {
            this.cars[i].update(dt);
        }

        // now update the lights
        for (i = 0; i < trafficModel.lightSystem.lights.length; i++) {
            this.lightSystem.lights[i].setColor(this.time, this.lightSystem.period);
        }
    },

    newGame: function () {
        this.cars = [];
        this.time = 0;
        this.streetLength = Number(roadView.roadSVG.getAttribute("width"));
        console.log("The street is this length: " + this.streetLength);
    },

    /**
     * Make an object that describes the car's environment
     * @param thisCar   the INDEX of the car in the this.cars array
     * @returns {{lightDistance: number, lightColor: (string)}}
     */
    findCarEnvironment: function(thisCar) {

        // find the distance to the next light
        var tMinLightDistance = Number.MAX_VALUE;
        var tMinLightIndex = 0;
        var thisLight;
        var tMe = this.cars[thisCar];
        var tDistance;

        for (thisLight = 0; thisLight < this.lightSystem.lights.length; thisLight++) {
            tDistance = (tMe.direction == "east")
                ? this.lightSystem.lights[thisLight].location - tMe.location
                : tMe.location - this.lightSystem.lights[thisLight].location;
            // todo: account for "wrapping" here
            if (tDistance > 0 && tDistance < tMinLightDistance) {
                tMinLightDistance = tDistance;
                tMinLightIndex = thisLight;
            }
        }

        //  find the distance and speed of the next car

        var tMinCarDistance = Number.MAX_VALUE;
        var tMinCarIndex = -1;
        var tNextCarSpeed = Number.MAX_VALUE;
        var tNextCarLength = 0;
        var thatCar;

        for (thatCar = 0; thatCar < this.cars.length; thatCar++) {
            if (thatCar != thisCar) {
                var tYou = this.cars[thatCar];

                if (tMe.direction == "east") {
                    tDistance = (tYou.direction == "east")
                        ? (tYou.location - tMe.location)
                        : (2 * this.streetLength - tYou.location - tMe.location);
                } else {    //   I am headed west
                    tDistance = (tYou.direction == "west")
                        ? (tMe.location - tYou.location)
                        : (tYou.location + tMe.location);
                }
                if (tDistance < 0) tDistance += 2 * this.streetLength;
                if (tDistance >= 0 && tDistance < tMinCarDistance && tMe.lane == tYou.lane) {
                    tMinCarDistance = tDistance;
                    tMinCarIndex = thatCar;
                    tNextCarSpeed = tYou.speed;
                    tNextCarLength = tYou.carLength;
                }
            }
        }

        return {
            lightDistance: tMinLightDistance,
            lightColor: this.lightSystem.lights[tMinLightIndex].SVGRect.getAttribute("fill") ,
            nextCarDistance: tMinCarDistance,
            nextCarSpeed: tNextCarSpeed,
            nextCarLength: tNextCarLength
        };
    }
};

trafficManager = {
    gameNumber: 0,
    gameCaseID: 0,
    gameInProgress: Boolean(false),
    running: Boolean(false),
    previous: 0,
    latestCarID: 0,

    gameButtonPressed: function () {
        if (this.gameInProgress) {  //  we're ending a game
            this.running = Boolean(false);
            this.endGame("abort");
        } else {    //  we're starting a new game
            trafficModel.newGame();
            window.requestAnimationFrame(this.animate);
            this.running = Boolean(true);
            this.startGame();
        }
        this.gameInProgress = !(this.gameInProgress);
        this.updateScreen();
    },

    addCar: function () {
        this.latestCarID++;

        codapHelper.createCase(
            'cars',
            [this.latestCarID, null],
            trafficManager.gameCaseID,
            trafficManager.setUpNewCarData
        );
    },

    update: function (dt) {
        trafficModel.update(dt);    //  also updates trafficModel.time
        this.updateScreen();
    },

    updateScreen: function () {
        roadView.draw();
        this.updateUIStuff();
    },

    updateUIStuff: function() {
        var timeText = document.getElementById("time");
        timeText.innerHTML = parseFloat(trafficModel.time.toFixed(2));

        var startStopButton = document.getElementById("startStop");
        startStopButton.innerHTML = (this.running) ? "pause" : "go";

        var gameButton = document.getElementById("game");
        gameButton.innerHTML = (this.gameInProgress) ? "abort game" : "new game";
    },

    click: function () {

    },

    startStop: function () {
        this.running = !(this.running);

        if (this.running) { //  START UP
            this.previous = 0;
            window.requestAnimationFrame(this.animate);
        } else {    // PAUSE
            this.previous = 0;  //  so next animate will have a short (zero) dt
        };
        this.updateScreen();
    },

    initializeComponent: function () {
        roadView.initialize();
        trafficModel.streetLength = roadView.roadSVG.getAttribute("width");
        trafficModel.lightSystem.lights.push( new Light() ); // default location
        this.updateScreen();
    },

    animate: function (timestamp) {
        if (!trafficManager.previous)  trafficManager.previous = timestamp;
        var tDt = (timestamp - trafficManager.previous) / 1000.0;
        trafficManager.previous = timestamp;
        trafficManager.update(tDt);
        if (trafficManager.running) window.requestAnimationFrame(trafficManager.animate);
    },

    startGame: function() {
        this.gameNumber++;
        trafficModel.time = 0;      //  restart time for each game
        codapHelper.openCase(
            'games',
            [this.gameNumber, null],
            trafficManager.setUpNewGameData
        );
    },

    setUpNewGameData: function(iResult) {
        trafficManager.gameCaseID = iResult.caseID;
        console.log("got game case ID " + (trafficManager.gameCaseID));
    },

    setUpNewCarData: function(iResult) {
        console.log("got CAR case ID " + (iResult.caseID) + " res: " +  iResult);

        var c = new Car();
        c.carCaseID = iResult.caseID;

        console.log(c.toString());

        codapHelper.updateCase("cars",[c.carCaseID, null], c.carCaseID);

        if (Math.random() > 0.4) c.lane = 2;

        var newCarSVG = document.createElementNS(svgNS, "svg");
        newCarSVG.setAttribute("x","0");  //  note: attribute names are strings!!
        newCarSVG.setAttribute("y","50");
        newCarSVG.setAttribute("width", c.carLength);
        newCarSVG.setAttribute("height", c.width);

        var carRect = document.createElementNS(svgNS, "rect");
        carRect.setAttribute("x","0");  //  note: attribute names are strings!!
        carRect.setAttribute("y","0");
        carRect.setAttribute("fill","black");
        carRect.setAttribute("width", c.carLength);
        carRect.setAttribute("height", c.width);

        var brakeRect = document.createElementNS(svgNS, "rect");
        brakeRect.setAttribute("x","0");  //  note: attribute names are strings!!
        brakeRect.setAttribute("y","0");
        brakeRect.setAttribute("fill","black");
        brakeRect.setAttribute("width", 4);
        brakeRect.setAttribute("height", c.width);
        brakeRect.setAttribute("id", "brake");

        var carText = document.createElementNS(svgNS, "text");
        carText.setAttribute("fill", "white");
        carText.setAttribute("y", -2);
        carText.setAttribute("textContent", "foo");

        newCarSVG.appendChild(carRect);
        newCarSVG.appendChild(brakeRect);
        newCarSVG.appendChild(carText);

        roadView.roadSVG.appendChild(newCarSVG);         //  here we put the new object into the DOM.

        c.SVG = newCarSVG;
        trafficModel.cars.push(c);
    },

    endGame: function(reason) {
        codapHelper.closeCase(
            'games',
            [
                this.gameNumber,
                reason
            ],
            this.gameCaseID
        );
        this.gameCaseID = 0;     //  so we know there is no open case

    }
};

/**
 * Required call to initialize the sim, connect it to CODAP.
 */
codapHelper.initSim({
    name: 'Traffic1d',
    dimensions: {width: 820, height: 200},
    collections: [  // There are > two collections: games, cars, moments
        {
            name: 'games',
            // The parent collection spec:
            attrs: [
                {name: "gameNumber", type: 'categorical'},
                {name: "result", type: 'categorical'}
            ],
            childAttrName: "car"
        },
        {
            name: 'cars',
            labels: {
                pluralCase: "cars",
                setOfCasesWithArticle: "a game"
            },
            // The child collection specification:
            attrs: [
                {name: "ID", type: 'categorical'},
                {name: "flips", type: 'numeric', precision: 0}
            ],
            childAttrName: "moment"
        },
        {
            name: 'moments',
            labels: {
                pluralCase: "moments",
                setOfCasesWithArticle: "a car"
            },
            // The child collection specification:
            attrs: [
                {name: "time", type: 'numeric', precision: 2},
                {name: "location", type: 'numeric', precision: 0},
                {name: "speed", type: 'numeric', precision: 1},
                {name: "action", type: 'categorical'}
            ]
        }
    ]
});

<commit_msg>refactor new car so that the SVG graphics are in the view.<commit_after>/**
 * Created by tim on 10/7/15.
 */
var svgNS = "http://www.w3.org/2000/svg";

var trafficManager;
var trafficModel;
var roadView;

var Light = function () {
    this.color = "green";
    this.phase = 2;
    this.yellowDwell = 3;
    this.location = 400;
    this.size = 30;
    this.SVGRect = null;

    //  make the svg, put the rect inside
    //  attach an event handler to the larger svg
    //  somehow give it an ID so we know which light gets clicked.

    this.SVGRect = document.createElementNS(svgNS, "rect");
    this.SVGRect.setAttribute("fill", this.color);
    this.SVGRect.setAttribute("width", this.size);
    this.SVGRect.setAttribute("height", roadView.roadSVG.getAttribute("height"));

    roadView.roadSVG.appendChild(this.SVGRect);         //  here we put the new object into the DOM.

};

Light.prototype.setColor = function (time, period) {
    var theta = time % period;
    if (this.phase > period / 2 && theta < period / 2) {
        theta += period;
    }
    if (theta > this.phase && theta < (this.phase + period / 2 - this.yellowDwell)) {
        this.color = "green";
    } else if (theta > this.phase + period / 2 - this.yellowDwell && theta < this.phase + period / 2) {
        this.color = "yellow";
    } else {
        this.color = "red";
    }
};

Light.prototype.draw = function ( ) {
    this.SVGRect.setAttribute("fill", this.color);
    this.SVGRect.setAttribute("x", this.location - this.size/2);
    this.SVGRect.setAttribute("y", 0);

};

roadView = {

    roadSVG: null,

    initialize: function () {
        this.roadSVG = document.getElementById("road");

    },

    draw: function () {
        this.drawRoad();
        var i;
        for (i = 0; i < trafficModel.lightSystem.lights.length; i++) {
            trafficModel.lightSystem.lights[i].draw( );
        }
        for (i = 0; i < trafficModel.cars.length; i++) {
            trafficModel.cars[i].draw( );
        }

    },

    drawRoad: function () {

    },

    clickOnRoad: function () {

    },

    addCarSVG: function (c ) {
        var newCarSVG = document.createElementNS(svgNS, "svg");
        newCarSVG.setAttribute("x","0");  //  note: attribute names are strings!!
        newCarSVG.setAttribute("y","50");
        newCarSVG.setAttribute("width", c.carLength);
        newCarSVG.setAttribute("height", c.width);

        var carRect = document.createElementNS(svgNS, "rect");
        carRect.setAttribute("x","0");  //  note: attribute names are strings!!
        carRect.setAttribute("y","0");
        carRect.setAttribute("fill","black");
        carRect.setAttribute("width", c.carLength);
        carRect.setAttribute("height", c.width);

        var brakeRect = document.createElementNS(svgNS, "rect");
        brakeRect.setAttribute("x","0");  //  note: attribute names are strings!!
        brakeRect.setAttribute("y","0");
        brakeRect.setAttribute("fill","black");
        brakeRect.setAttribute("width", 4);
        brakeRect.setAttribute("height", c.width);
        brakeRect.setAttribute("id", "brake");

        var carText = document.createElementNS(svgNS, "text");
        carText.setAttribute("fill", "white");
        carText.setAttribute("y", -2);
        carText.setAttribute("textContent", "foo");

        newCarSVG.appendChild(carRect);
        newCarSVG.appendChild(brakeRect);
        newCarSVG.appendChild(carText);

        this.roadSVG.appendChild(newCarSVG);         //  here we put the new object into the DOM.
        c.SVG = newCarSVG;

    }

};

trafficModel = {
    time: 0,
    cars: [],
    lightSystem: {lights: [], period: 16},
    streetLength: null,

    update: function (dt) {
        this.time += dt;
        var i;

        //  for every car, find its environment (situation) with cars in the current positions
        for ( i = 0; i < this.cars.length; i++) {
            this.cars[i].decision = this.cars[i].decide(this.findCarEnvironment(i));
        }

        //  now have each car update its position
        for ( i = 0; i < this.cars.length; i++) {
            this.cars[i].update(dt);
        }

        // now update the lights
        for (i = 0; i < trafficModel.lightSystem.lights.length; i++) {
            this.lightSystem.lights[i].setColor(this.time, this.lightSystem.period);
        }
    },

    newGame: function () {
        this.cars = [];
        this.time = 0;
        this.streetLength = Number(roadView.roadSVG.getAttribute("width"));
        console.log("The street is this length: " + this.streetLength);
    },

    /**
     * Make an object that describes the car's environment
     * @param thisCar   the INDEX of the car in the this.cars array
     * @returns {{lightDistance: number, lightColor: (string)}}
     */
    findCarEnvironment: function(thisCar) {

        // find the distance to the next light
        var tMinLightDistance = Number.MAX_VALUE;
        var tMinLightIndex = 0;
        var thisLight;
        var tMe = this.cars[thisCar];
        var tDistance;

        for (thisLight = 0; thisLight < this.lightSystem.lights.length; thisLight++) {
            tDistance = (tMe.direction == "east")
                ? this.lightSystem.lights[thisLight].location - tMe.location
                : tMe.location - this.lightSystem.lights[thisLight].location;
            // todo: account for "wrapping" here
            if (tDistance > 0 && tDistance < tMinLightDistance) {
                tMinLightDistance = tDistance;
                tMinLightIndex = thisLight;
            }
        }

        //  find the distance and speed of the next car

        var tMinCarDistance = Number.MAX_VALUE;
        var tMinCarIndex = -1;
        var tNextCarSpeed = Number.MAX_VALUE;
        var tNextCarLength = 0;
        var thatCar;

        for (thatCar = 0; thatCar < this.cars.length; thatCar++) {
            if (thatCar != thisCar) {
                var tYou = this.cars[thatCar];

                if (tMe.direction == "east") {
                    tDistance = (tYou.direction == "east")
                        ? (tYou.location - tMe.location)
                        : (2 * this.streetLength - tYou.location - tMe.location);
                } else {    //   I am headed west
                    tDistance = (tYou.direction == "west")
                        ? (tMe.location - tYou.location)
                        : (tYou.location + tMe.location);
                }
                if (tDistance < 0) tDistance += 2 * this.streetLength;
                if (tDistance >= 0 && tDistance < tMinCarDistance && tMe.lane == tYou.lane) {
                    tMinCarDistance = tDistance;
                    tMinCarIndex = thatCar;
                    tNextCarSpeed = tYou.speed;
                    tNextCarLength = tYou.carLength;
                }
            }
        }

        return {
            lightDistance: tMinLightDistance,
            lightColor: this.lightSystem.lights[tMinLightIndex].SVGRect.getAttribute("fill") ,
            nextCarDistance: tMinCarDistance,
            nextCarSpeed: tNextCarSpeed,
            nextCarLength: tNextCarLength
        };
    }
};

trafficManager = {
    gameNumber: 0,
    gameCaseID: 0,
    gameInProgress: Boolean(false),
    running: Boolean(false),
    previous: 0,

    gameButtonPressed: function () {
        if (this.gameInProgress) {  //  we're ending a game
            this.running = Boolean(false);
            this.endGame("abort");
        } else {    //  we're starting a new game
            trafficModel.newGame();
            window.requestAnimationFrame(this.animate);
            this.running = Boolean(true);
            this.startGame();
        }
        this.gameInProgress = !(this.gameInProgress);
        this.updateScreen();
    },

    addCar: function () {

        codapHelper.createCase(
            'cars',
            [null, null],
            trafficManager.gameCaseID,
            trafficManager.setUpNewCarData
        );
    },

    setUpNewCarData: function(iResult) {
        var c = new Car();
        c.carCaseID = iResult.caseID;
        if (Math.random() > 0.4) c.lane = 2;
        roadView.addCarSVG( c );
        trafficModel.cars.push(c);
        codapHelper.updateCase("cars",[c.carCaseID, null], c.carCaseID);
    },

    update: function (dt) {
        trafficModel.update(dt);    //  also updates trafficModel.time
        this.updateScreen();
    },

    updateScreen: function () {
        roadView.draw();
        this.updateUIStuff();
    },

    updateUIStuff: function() {
        var timeText = document.getElementById("time");
        timeText.innerHTML = parseFloat(trafficModel.time.toFixed(2));

        var startStopButton = document.getElementById("startStop");
        startStopButton.innerHTML = (this.running) ? "pause" : "go";

        var gameButton = document.getElementById("game");
        gameButton.innerHTML = (this.gameInProgress) ? "abort game" : "new game";
    },

    click: function () {

    },

    startStop: function () {
        this.running = !(this.running);

        if (this.running) { //  START UP
            this.previous = 0;
            window.requestAnimationFrame(this.animate);
        } else {    // PAUSE
            this.previous = 0;  //  so next animate will have a short (zero) dt
        };
        this.updateScreen();
    },

    initializeComponent: function () {
        roadView.initialize();
        trafficModel.streetLength = roadView.roadSVG.getAttribute("width");
        trafficModel.lightSystem.lights.push( new Light() ); // default location
        this.updateScreen();
    },

    animate: function (timestamp) {
        if (!trafficManager.previous)  trafficManager.previous = timestamp;
        var tDt = (timestamp - trafficManager.previous) / 1000.0;
        trafficManager.previous = timestamp;
        trafficManager.update(tDt);
        if (trafficManager.running) window.requestAnimationFrame(trafficManager.animate);
    },

    startGame: function() {
        this.gameNumber++;
        codapHelper.openCase(
            'games',
            [this.gameNumber, null],
            trafficManager.setUpNewGameData
        );
    },

    setUpNewGameData: function(iResult) {
        trafficManager.gameCaseID = iResult.caseID;
        console.log("got game case ID " + (trafficManager.gameCaseID));
    },

    endGame: function(reason) {
        codapHelper.closeCase(
            'games',
            [
                this.gameNumber,
                reason
            ],
            this.gameCaseID
        );
        this.gameCaseID = 0;     //  so we know there is no open case

    }
};

/**
 * Required call to initialize the sim, connect it to CODAP.
 */
codapHelper.initSim({
    name: 'Traffic1d',
    dimensions: {width: 820, height: 200},
    collections: [  // There are > two collections: games, cars, moments
        {
            name: 'games',
            // The parent collection spec:
            attrs: [
                {name: "gameNumber", type: 'categorical'},
                {name: "result", type: 'categorical'}
            ],
            childAttrName: "car"
        },
        {
            name: 'cars',
            labels: {
                pluralCase: "cars",
                setOfCasesWithArticle: "a game"
            },
            // The child collection specification:
            attrs: [
                {name: "ID", type: 'categorical'},
                {name: "flips", type: 'numeric', precision: 0}
            ],
            childAttrName: "moment"
        },
        {
            name: 'moments',
            labels: {
                pluralCase: "moments",
                setOfCasesWithArticle: "a car"
            },
            // The child collection specification:
            attrs: [
                {name: "time", type: 'numeric', precision: 2},
                {name: "location", type: 'numeric', precision: 0},
                {name: "speed", type: 'numeric', precision: 1},
                {name: "action", type: 'categorical'}
            ]
        }
    ]
});

<|endoftext|>