<commit_before>'use strict';

angular.module('offlineApp').service('offlineService', function($http) {

    var view_model = this;

    /* --------------- Configuration --------------- */

    // API Parameters
    view_model.readAPI = "http://188.166.147.80/get?after=";
    view_model.updateAPI = "http://188.166.147.80/post";
    view_model.createAPI = "http://188.166.147.80/post";
    view_model.primaryKeyProperty = "id";
    view_model.timestampProperty = "timestamp";

    // Default Config:
    view_model.autoSync = 10000; /* Set to zero for no auto synchronisation */
    view_model.pushSync = false;
    view_model.initialSync = true;
    view_model.allowIndexedDB = true; /* Switching to false disables IndexedDB */
    view_model.allowRemote = true;

    // IndexedDB Config:
    view_model.indexedDBDatabaseName = "localDB";
    view_model.indexedDBVersionNumber = 158; /* Increment this to wipe and reset IndexedDB */
    view_model.objectStoreName = "testObjectStore";

    /* --------------- Offlinify Internals --------------- */

    // Service Variables
    view_model.idb = null;
    //view_model.serviceDB = []; /* Local image of the data */
    view_model.localData = [];
    view_model.observerCallbacks = [];
    view_model.lastChecked = new Date("1970-01-01T00:00:00.000Z").toISOString(); /* Initially the epoch */

    // Public Functions
    view_model.registerController = registerController;
    view_model._generateTimestamp = _generateTimestamp;
    view_model.sync = sync;
    view_model.objectUpdate = objectUpdate;
    view_model.wipeIndexedDB = wipeIndexedDB;

    // Determine IndexedDB Support
    view_model.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB || window.shimIndexedDB;
    if(!view_model.allowIndexedDB) view_model.indexedDB = null;

    /* --------------- Create & Update --------------- */

    // Filters create or update ops by queue state:
    function objectUpdate(obj) {
      _.set(obj, view_model.timestampProperty, _generateTimestamp());
      obj = _stripAngularHashKeys(obj);
      if(obj.hasOwnProperty("syncState")) {
        if(obj.syncState > 0) { obj.syncState = 2; }
      } else {
        obj = _.cloneDeep(obj);
        obj.syncState = 0;
        _.set(obj, view_model.primaryKeyProperty, _generateUUID());
      }
      _patchLocal(_stripAngularHashKeys([obj]), function(response) {
        if(view_model.pushSync) sync(_notifyObservers);
      });
     };

    /* --------------- Observer Pattern --------------- */

    function registerObjectStores(array, callback) {

       // Add a new localData entry.

       // Add to indexedDB.

       _.forEach(array, function(item) {

         var exists = _.filter(view_model.localData, {"storeName" : item.name});
         if(exists == -1) {callback(); return; }

         item.data = [];
         view_model.localData.push(item);

       });

    };

    function addIDBObjectStore(name, primaryKey, timestamp, callback) {

      // Here check if IDB is null or not, like registerController did.
      if(view_model.idb == null) {
        _establishIndexedDB(function() {

        });
      } else {

       }

    };

     // Called by the controller to receive updates (observer pattern)
    function registerController(ctrlCallback) {
      if(view_model.idb == null) {
        _establishIndexedDB(function() {
          view_model.observerCallbacks.push(ctrlCallback);
          if(!view_model.initialSync) return;
          view_model.sync(function(response) {
            ctrlCallback(response);
          });
        });
      } else {
        view_model.observerCallbacks.push(ctrlCallback);
        if(!view_model.initialSync) return;
        view_model.sync(function(response) {
          ctrlCallback(response);
        });
       }
     };

    /* --------------- Synchronisation --------------- */

    // Restores local state on first sync, or patches local and remote changes:
    function sync(callback) {
      var startClock = _generateTimestamp();
      var newLocalRecords = _getLocalRecords(view_model.lastChecked);
      if( newLocalRecords.length == 0 && view_model.serviceDB.length == 0 ) {
        _restoreLocalState( function(localResponse) {
          _patchRemoteChanges(function(remoteResponse) {
            _reduceQueue(function(queueResponse) {
              callback( {dataSource: remoteResponse, currentQueue: queueResponse} );
              console.log("Initial load took: " + (new Date(_generateTimestamp()) - new Date(startClock))/1000 + " sec." );
            });
          });
        });
      } else {
        _patchRemoteChanges(function(remoteResponse) {
          _reduceQueue(function(queueResponse) {
            callback( { dataSource: remoteResponse, currentQueue: queueResponse } );
          });
        });
      }
    };

    // Patches remote edits to serviceDB + IndexedDB:
    function _patchRemoteChanges(callback) {
      if(!view_model.allowRemote) {
          if(_hasIndexedDB()) callback(-1);
          else callback(-2);
          return;
      }
      _getRemoteRecords(function(response) {
        if(response.status == 200) {
          _patchLocal(response.data, function(localResponse) {
            callback(localResponse);
          });
        } else { callback("Could not connect to remote server: (" + response.status + ") error."); }
      });
    };

    // Patches the local storages with a dataset.
    function _patchLocal(data, callback) {
      _patchServiceDB(data);
      view_model.lastChecked = _generateTimestamp();
      if( _hasIndexedDB() ) {
        _putArrayToIndexedDB(data, function() {
          callback(1); // Patched to IDB + ServiceDB
        });
      } else {
        callback(0); // Patched to ServiceDB only.
      }
    };

    function _notifyObservers(status) {
      angular.forEach(view_model.observerCallbacks, function(callback){
        callback(status);
      });
    };

    /* --------------- Queue + State --------------- */

    // Puts IndexedDB store into scope:
    function _restoreLocalState(callback) {
      if(!_hasIndexedDB()) { callback(-1); return; }
      _getIndexedDB(function(idbRecords) {

        var sortedElements = _.reverse(_.sortBy(idbRecords, function(o) {
          return new Date(_.get(o, view_model.timestampProperty)).toISOString();
        }));
        var nonQueueElements = _.filter(sortedElements, {syncState: 1});
        var queueElements = _.filter(sortedElements, function(o) { return o.syncState != 1; });

        if(nonQueueElements.length > 0) {
          view_model.lastChecked = _.get(sortedElements[0], view_model.timestampProperty);
        } else {
          if(queueElements.length > 0)
            view_model.lastChecked = _.get(queueElements[queueElements.length - 1], view_model.timestampProperty);
        }

        _patchServiceDB(idbRecords);
        callback(idbRecords.length);
      });
    };

    // Synchronises elements to remote when connection is available:
    function _reduceQueue(callback) {
      if(!view_model.allowRemote) { callback(-1); return; }

      var createQueue = _.filter(view_model.serviceDB, { "syncState" : 0 });
      var updateQueue = _.filter(view_model.serviceDB, { "syncState" : 2 });

      // Get rid of the create queue:
      _safeArrayPost(createQueue, view_model.createAPI, function(successfulCreates) {
        _safeArrayPost(updateQueue, view_model.updateAPI, function(successfulUpdates) {

          var totalQueue = successfulCreates.concat(successfulUpdates);
          _.forEach(totalQueue, function(value) {
            _.set(value, view_model.timestampProperty, _generateTimestamp());
          });

          _patchLocal(_resetSyncState(totalQueue), function(response) {

            var queueLength = updateQueue.length + createQueue.length;
            var popLength = successfulCreates.length + successfulUpdates.length;

            // Check here for integrity:
            if( queueLength == popLength ) {
              callback(1); // Success: All queue elements synchronised.
            } else {
              callback(0); // Partial success: Some or none were synchronised.
            }

          });
        });
      });

    };

    /* --------------- ServiceDB Interface --------------- */

    function _patchServiceDB(data) {
      var operations = _filterOperations(data);
      _updatesToServiceDB(operations.updateOperations);
      _pushToServiceDB(operations.createOperations);
    };

    function _pushToServiceDB(array) {
      for(var i=0; i<array.length; i++) view_model.serviceDB.push(array[i]);
    };

    function _updatesToServiceDB(array) {
      for(var i=0; i<array.length; i++) {
        var indexJSON = {};
        _.set(indexJSON, view_model.primaryKeyProperty, _.get(array[i], view_model.primaryKeyProperty));
        var matchID = _.findIndex(view_model.serviceDB, indexJSON);
        if(matchID > -1) view_model.serviceDB[matchID] = array[i];
      }
    };

    function _getLocalRecords(sinceTime) {
      return _.filter(view_model.serviceDB, function(o) {
        return new Date(_.get(o, view_model.timestampProperty)).toISOString() > sinceTime;
      });
    };

    /* --------------- Data Handling --------------- */

    /* Filter remote data into create or update operations */
    function _filterOperations(data) {
      var updateOps = [];
      var createOps = [];
      for(var i=0; i<data.length; i++) {
        var queryJSON = {};
        _.set(queryJSON, view_model.primaryKeyProperty, _.get(data[i], view_model.primaryKeyProperty));
        var query = _.findIndex(view_model.serviceDB, queryJSON);
        if( query > -1 ) updateOps.push(data[i]);
        else createOps.push(data[i]);
      }
      return { updateOperations: updateOps, createOperations: createOps };
    }

    function _resetSyncState(records) {
      for(var i=0; i<records.length; i++) {
        records[i].syncState = 1;
      }
      return records;
    };

    /* --------------- Remote --------------- */

    function _postRemote(data, url, callback) {
      // Data should be a single record.
      $http({
          url: url,
          method: "POST",
          data: [data],
          headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }
      })
      .then(
        function successCallback(response) {
          callback(response.status); // return response code.
        }, function errorCallback(response) {
          callback(response.status);
        });
    };

    function _getRemoteRecords(callback) {
      $http({
          method: 'GET',
          url: view_model.readAPI + view_model.lastChecked
        })
        .then(
          function successCallback(response) {
            if(response.data.length > 0)
              callback({data: _resetSyncState(response.data), status: 200});
            else
              callback({data: [], status: 200});

        }, function errorCallback(response) {
            callback({data: [], status: response.status});
        });
    };

    // Tries to post an array one-by-one; returns successful elements.
    function _safeArrayPost(array, url, callback) {
      var x = 0;
      var successfulElements = [];
      if(array.length == 0) { callback([]); return; }
      function loopArray(array) {
        _postRemote(array[x],url,function(response) {
          if(response == 200) successfulElements.push(array[x]);
          x++;
          if(x < array.length) { loopArray(array); }
          else { callback(successfulElements); }
        });
      };
      loopArray(array);
    };

    /* --------------- IndexedDB --------------- */

    function _establishIndexedDB(callback) {
      if(!_hasIndexedDB()) { callback(); /* No browser support for IDB */ return; }
      var request = view_model.indexedDB.open(view_model.indexedDBDatabaseName, view_model.indexedDBVersionNumber);
      request.onupgradeneeded = function(e) {
        var db = e.target.result;
        e.target.transaction.onerror = function() { console.error(this.error); };
        if(db.objectStoreNames.contains(view_model.objectStoreName)) {
          db.deleteObjectStore(view_model.objectStoreName);
        }
        var offlineItems = db.createObjectStore(view_model.objectStoreName, { keyPath: view_model.primaryKeyProperty, autoIncrement: false } );
        var dateIndex = offlineItems.createIndex("byDate", view_model.timestampProperty, {unique: false});
        view_model.idb = db;
      };
      request.onsuccess = function(e) {
        view_model.idb = e.target.result;
        callback();
      };
      request.onerror = function() { console.error(this.error); };
    };

    function _hasIndexedDB() {
      return !(view_model.indexedDB === undefined || view_model.indexedDB === null );
    };

    // Get from IndexedDB. This function returns appropriate records.
    function _getIndexedDB(callback) {
      var transaction = _newIDBTransaction();
      var objStore = transaction.objectStore(view_model.objectStoreName);
      var keyRange = IDBKeyRange.lowerBound(0);
      var cursorRequest = objStore.index('byDate').openCursor(keyRange);
      var returnableItems = [];
      transaction.oncomplete = function(e) { callback(_stripAngularHashKeys(returnableItems)); };
      cursorRequest.onsuccess = function(e) {
        var result = e.target.result;
        if (!!result == false) { return; }
        returnableItems.push(result.value);
        result.continue();
      };
      cursorRequest.onerror = function() { console.error("error"); };
    };

    // Apply array of edited objects to IndexedDB.
    function _putArrayToIndexedDB(array, callback) {
      var x = 0;
      if(array.length == 0) {
        callback();
        return;
      }

      var objStore = _newIDBTransaction().objectStore(view_model.objectStoreName);

      function putNext() {
        if(x < array.length) {
          objStore.put(array[x]).onsuccess = function() {
            x++;
            putNext();
          };
        } else {
          callback();
        }
      }
      putNext();

    };

    function _newIDBTransaction() {
      return view_model.idb.transaction([view_model.objectStoreName], 'readwrite');
    };

    // Only used for diagnostics:
    function wipeIndexedDB(callback) {
      var req = view_model.indexedDB.deleteDatabase(view_model.indexedDBDatabaseName);
      req.onsuccess = function(event) { callback(); }
    };

    /* --------------- Utilities --------------- */

    function _generateTimestamp() {
      var d = new Date();
      return d.toISOString();
    };

    // (v4) With thanks to http://stackoverflow.com/a/8809472/3381433
    function _generateUUID() {
      var d = new Date().getTime();
      if(window.performance && typeof window.performance.now === "function"){
          d += performance.now(); // use high-precision timer if available
      }
      var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
          var r = (d + Math.random()*16)%16 | 0;
          d = Math.floor(d/16);
          return (c=='x' ? r : (r&0x3|0x8)).toString(16);
      });
      return uuid;
    };

    function _stripAngularHashKeys(array) {
      for(var i=0; i<array.length; i++) delete array[i].$$hashKey;
      return array;
    };

    /* --------------- Sync Loop -------------- */

    if(view_model.autoSync > 0 && parseInt(view_model.autoSync) === view_model.autoSync) {
      (function syncLoop() {
        setTimeout(function() {
          sync(function(response) {
            _notifyObservers(response);
          });
          syncLoop();
        }, view_model.autoSync);
      })();
    }

  });
<commit_msg>Added refactoring<commit_after>'use strict';

angular.module('offlineApp').service('offlineService', function($http) {

    var view_model = this;

    /* --------------- Configuration --------------- */

    // API Parameters
    view_model.readAPI = "http://188.166.147.80/get?after=";
    view_model.updateAPI = "http://188.166.147.80/post";
    view_model.createAPI = "http://188.166.147.80/post";
    view_model.primaryKeyProperty = "id";
    view_model.timestampProperty = "timestamp";

    // Default Config:
    view_model.autoSync = 10000; /* Set to zero for no auto synchronisation */
    view_model.pushSync = false;
    view_model.initialSync = true;
    view_model.allowIndexedDB = true; /* Switching to false disables IndexedDB */
    view_model.allowRemote = true;

    // IndexedDB Config:
    view_model.indexedDBDatabaseName = "localDB";
    view_model.indexedDBVersionNumber = 158; /* Increment this to wipe and reset IndexedDB */
    view_model.objectStoreName = "testObjectStore";

    /* --------------- Offlinify Internals --------------- */

    // Service Variables
    view_model.idb = null;
    //view_model.serviceDB = []; /* Local image of the data */
    view_model.localData = [];
    view_model.observerCallbacks = [];
    view_model.lastChecked = new Date("1970-01-01T00:00:00.000Z").toISOString(); /* Initially the epoch */

    // Public Functions
    view_model.registerController = registerController;
    view_model._generateTimestamp = _generateTimestamp;
    view_model.sync = sync;
    view_model.objectUpdate = objectUpdate;
    view_model.wipeIndexedDB = wipeIndexedDB;

    // Determine IndexedDB Support
    view_model.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB || window.shimIndexedDB;
    if(!view_model.allowIndexedDB) view_model.indexedDB = null;

    /* --------------- Create & Update --------------- */

    // Filters create or update ops by queue state:
    function objectUpdate(obj) {
      _.set(obj, view_model.timestampProperty, _generateTimestamp());
      obj = _stripAngularHashKeys(obj);
      if(obj.hasOwnProperty("syncState")) {
        if(obj.syncState > 0) { obj.syncState = 2; }
      } else {
        obj = _.cloneDeep(obj);
        obj.syncState = 0;
        _.set(obj, view_model.primaryKeyProperty, _generateUUID());
      }
      _patchLocal(_stripAngularHashKeys([obj]), function(response) {
        if(view_model.pushSync) sync(_notifyObservers);
      });
     };

    /* --------------- Observer Pattern --------------- */

    function registerObjectStores(array, callback) {

       // Add a new localData entry.

       // Add to indexedDB.

       _.forEach(array, function(item) {

         var exists = _.filter(view_model.localData, {"storeName" : item.name});
         if(exists == -1) {callback(); return; }

         item.data = [];
         view_model.localData.push(item);

       });

    };

    function addIDBObjectStore(name, primaryKey, timestamp, callback) {

      // Here check if IDB is null or not, like registerController did.
      if(view_model.idb == null) {
        _establishIndexedDB(function() {

        });
      } else {

       }

    };

     // Called by the controller to receive updates (observer pattern)
    function registerController(ctrlCallback) {
       ensureIDBReady(function() {
         view_model.observerCallbacks.push(ctrlCallback);
         if(!view_model.initialSync) return;
         view_model.sync(function(response) {
           ctrlCallback(response);
         });
       });
     };

     function ensureIDBReady(callback) {
        if(view_model.idb == null)
          _establishIndexedDB(function() { callback(); });
        else
          callback();
     };

    /* --------------- Synchronisation --------------- */

    // Restores local state on first sync, or patches local and remote changes:
    function sync(callback) {
      var startClock = _generateTimestamp();
      var newLocalRecords = _getLocalRecords(view_model.lastChecked);
      if( newLocalRecords.length == 0 && view_model.serviceDB.length == 0 ) {
        _restoreLocalState( function(localResponse) {
          _patchRemoteChanges(function(remoteResponse) {
            _reduceQueue(function(queueResponse) {
              callback( {dataSource: remoteResponse, currentQueue: queueResponse} );
              console.log("Initial load took: " + (new Date(_generateTimestamp()) - new Date(startClock))/1000 + " sec." );
            });
          });
        });
      } else {
        _patchRemoteChanges(function(remoteResponse) {
          _reduceQueue(function(queueResponse) {
            callback( { dataSource: remoteResponse, currentQueue: queueResponse } );
          });
        });
      }
    };

    // Patches remote edits to serviceDB + IndexedDB:
    function _patchRemoteChanges(callback) {
      if(!view_model.allowRemote) {
          if(_hasIndexedDB()) callback(-1);
          else callback(-2);
          return;
      }
      _getRemoteRecords(function(response) {
        if(response.status == 200) {
          _patchLocal(response.data, function(localResponse) {
            callback(localResponse);
          });
        } else { callback("Could not connect to remote server: (" + response.status + ") error."); }
      });
    };

    // Patches the local storages with a dataset.
    function _patchLocal(data, callback) {
      _patchServiceDB(data);
      view_model.lastChecked = _generateTimestamp();
      if( _hasIndexedDB() ) {
        _putArrayToIndexedDB(data, function() {
          callback(1); // Patched to IDB + ServiceDB
        });
      } else {
        callback(0); // Patched to ServiceDB only.
      }
    };

    function _notifyObservers(status) {
      angular.forEach(view_model.observerCallbacks, function(callback){
        callback(status);
      });
    };

    /* --------------- Queue + State --------------- */

    // Puts IndexedDB store into scope:
    function _restoreLocalState(callback) {
      if(!_hasIndexedDB()) { callback(-1); return; }
      _getIndexedDB(function(idbRecords) {

        var sortedElements = _.reverse(_.sortBy(idbRecords, function(o) {
          return new Date(_.get(o, view_model.timestampProperty)).toISOString();
        }));
        var nonQueueElements = _.filter(sortedElements, {syncState: 1});
        var queueElements = _.filter(sortedElements, function(o) { return o.syncState != 1; });

        if(nonQueueElements.length > 0) {
          view_model.lastChecked = _.get(sortedElements[0], view_model.timestampProperty);
        } else {
          if(queueElements.length > 0)
            view_model.lastChecked = _.get(queueElements[queueElements.length - 1], view_model.timestampProperty);
        }

        _patchServiceDB(idbRecords);
        callback(idbRecords.length);
      });
    };

    // Synchronises elements to remote when connection is available:
    function _reduceQueue(callback) {
      if(!view_model.allowRemote) { callback(-1); return; }

      var createQueue = _.filter(view_model.serviceDB, { "syncState" : 0 });
      var updateQueue = _.filter(view_model.serviceDB, { "syncState" : 2 });

      // Get rid of the create queue:
      _safeArrayPost(createQueue, view_model.createAPI, function(successfulCreates) {
        _safeArrayPost(updateQueue, view_model.updateAPI, function(successfulUpdates) {

          var totalQueue = successfulCreates.concat(successfulUpdates);
          _.forEach(totalQueue, function(value) {
            _.set(value, view_model.timestampProperty, _generateTimestamp());
          });

          _patchLocal(_resetSyncState(totalQueue), function(response) {

            var queueLength = updateQueue.length + createQueue.length;
            var popLength = successfulCreates.length + successfulUpdates.length;

            // Check here for integrity:
            if( queueLength == popLength ) {
              callback(1); // Success: All queue elements synchronised.
            } else {
              callback(0); // Partial success: Some or none were synchronised.
            }

          });
        });
      });

    };

    /* --------------- ServiceDB Interface --------------- */

    function _patchServiceDB(data) {
      var operations = _filterOperations(data);
      _updatesToServiceDB(operations.updateOperations);
      _pushToServiceDB(operations.createOperations);
    };

    function _pushToServiceDB(array) {
      for(var i=0; i<array.length; i++) view_model.serviceDB.push(array[i]);
    };

    function _updatesToServiceDB(array) {
      for(var i=0; i<array.length; i++) {
        var indexJSON = {};
        _.set(indexJSON, view_model.primaryKeyProperty, _.get(array[i], view_model.primaryKeyProperty));
        var matchID = _.findIndex(view_model.serviceDB, indexJSON);
        if(matchID > -1) view_model.serviceDB[matchID] = array[i];
      }
    };

    function _getLocalRecords(sinceTime) {
      return _.filter(view_model.serviceDB, function(o) {
        return new Date(_.get(o, view_model.timestampProperty)).toISOString() > sinceTime;
      });
    };

    /* --------------- Data Handling --------------- */

    /* Filter remote data into create or update operations */
    function _filterOperations(data) {
      var updateOps = [];
      var createOps = [];
      for(var i=0; i<data.length; i++) {
        var queryJSON = {};
        _.set(queryJSON, view_model.primaryKeyProperty, _.get(data[i], view_model.primaryKeyProperty));
        var query = _.findIndex(view_model.serviceDB, queryJSON);
        if( query > -1 ) updateOps.push(data[i]);
        else createOps.push(data[i]);
      }
      return { updateOperations: updateOps, createOperations: createOps };
    }

    function _resetSyncState(records) {
      for(var i=0; i<records.length; i++) {
        records[i].syncState = 1;
      }
      return records;
    };

    /* --------------- Remote --------------- */

    function _postRemote(data, url, callback) {
      // Data should be a single record.
      $http({
          url: url,
          method: "POST",
          data: [data],
          headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }
      })
      .then(
        function successCallback(response) {
          callback(response.status); // return response code.
        }, function errorCallback(response) {
          callback(response.status);
        });
    };

    function _getRemoteRecords(callback) {
      $http({
          method: 'GET',
          url: view_model.readAPI + view_model.lastChecked
        })
        .then(
          function successCallback(response) {
            if(response.data.length > 0)
              callback({data: _resetSyncState(response.data), status: 200});
            else
              callback({data: [], status: 200});

        }, function errorCallback(response) {
            callback({data: [], status: response.status});
        });
    };

    // Tries to post an array one-by-one; returns successful elements.
    function _safeArrayPost(array, url, callback) {
      var x = 0;
      var successfulElements = [];
      if(array.length == 0) { callback([]); return; }
      function loopArray(array) {
        _postRemote(array[x],url,function(response) {
          if(response == 200) successfulElements.push(array[x]);
          x++;
          if(x < array.length) { loopArray(array); }
          else { callback(successfulElements); }
        });
      };
      loopArray(array);
    };

    /* --------------- IndexedDB --------------- */

    function _establishIndexedDB(callback) {
      if(!_hasIndexedDB()) { callback(); /* No browser support for IDB */ return; }
      var request = view_model.indexedDB.open(view_model.indexedDBDatabaseName, view_model.indexedDBVersionNumber);
      request.onupgradeneeded = function(e) {
        var db = e.target.result;
        e.target.transaction.onerror = function() { console.error(this.error); };
        if(db.objectStoreNames.contains(view_model.objectStoreName)) {
          db.deleteObjectStore(view_model.objectStoreName);
        }
        var offlineItems = db.createObjectStore(view_model.objectStoreName, { keyPath: view_model.primaryKeyProperty, autoIncrement: false } );
        var dateIndex = offlineItems.createIndex("byDate", view_model.timestampProperty, {unique: false});
        view_model.idb = db;
      };
      request.onsuccess = function(e) {
        view_model.idb = e.target.result;
        callback();
      };
      request.onerror = function() { console.error(this.error); };
    };

    function _hasIndexedDB() {
      return !(view_model.indexedDB === undefined || view_model.indexedDB === null );
    };

    // Get from IndexedDB. This function returns appropriate records.
    function _getIndexedDB(callback) {
      var transaction = _newIDBTransaction();
      var objStore = transaction.objectStore(view_model.objectStoreName);
      var keyRange = IDBKeyRange.lowerBound(0);
      var cursorRequest = objStore.index('byDate').openCursor(keyRange);
      var returnableItems = [];
      transaction.oncomplete = function(e) { callback(_stripAngularHashKeys(returnableItems)); };
      cursorRequest.onsuccess = function(e) {
        var result = e.target.result;
        if (!!result == false) { return; }
        returnableItems.push(result.value);
        result.continue();
      };
      cursorRequest.onerror = function() { console.error("error"); };
    };

    // Apply array of edited objects to IndexedDB.
    function _putArrayToIndexedDB(array, callback) {
      var x = 0;
      if(array.length == 0) {
        callback();
        return;
      }

      var objStore = _newIDBTransaction().objectStore(view_model.objectStoreName);

      function putNext() {
        if(x < array.length) {
          objStore.put(array[x]).onsuccess = function() {
            x++;
            putNext();
          };
        } else {
          callback();
        }
      }
      putNext();

    };

    function _newIDBTransaction() {
      return view_model.idb.transaction([view_model.objectStoreName], 'readwrite');
    };

    // Only used for diagnostics:
    function wipeIndexedDB(callback) {
      var req = view_model.indexedDB.deleteDatabase(view_model.indexedDBDatabaseName);
      req.onsuccess = function(event) { callback(); }
    };

    /* --------------- Utilities --------------- */

    function _generateTimestamp() {
      var d = new Date();
      return d.toISOString();
    };

    // (v4) With thanks to http://stackoverflow.com/a/8809472/3381433
    function _generateUUID() {
      var d = new Date().getTime();
      if(window.performance && typeof window.performance.now === "function"){
          d += performance.now(); // use high-precision timer if available
      }
      var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
          var r = (d + Math.random()*16)%16 | 0;
          d = Math.floor(d/16);
          return (c=='x' ? r : (r&0x3|0x8)).toString(16);
      });
      return uuid;
    };

    function _stripAngularHashKeys(array) {
      for(var i=0; i<array.length; i++) delete array[i].$$hashKey;
      return array;
    };

    /* --------------- Sync Loop -------------- */

    if(view_model.autoSync > 0 && parseInt(view_model.autoSync) === view_model.autoSync) {
      (function syncLoop() {
        setTimeout(function() {
          sync(function(response) {
            _notifyObservers(response);
          });
          syncLoop();
        }, view_model.autoSync);
      })();
    }

  });
<|endoftext|>


<commit_before>/**
 * @fileoverview Externs for backbone-0.9.2.js
 *
 * built with http://www.dotnetwise.com/Code/Externs/index.html
 * see also: http://blog.dotnetwise.com/2009/11/closure-compiler-externs-extractor.html
 * via: http://code.google.com/p/closure-compiler/wiki/FAQ#How_do_I_write_an_externs_file?
 *
 * Note: when building via that page, you first need to load in underscrore.js, as that's a dependency.
 *       also, after running the extern for Backbone, you need to manually run it for:
 *       Backbone.Model.prototype, Backbone.Collection.prototype, Backbone.Router.prototype,
 *       Backbone.History.prototype, and Backbone.View.prototype
 *       because these objects are modified using _.extend(Backbone.Model.prototype ...)
 * @see http://documentcloud.github.com/backbone/
 * @externs
 */

var Backbone = {
    "VERSION": {},
    "setDomLibrary": function () {},
    "noConflict": function () {},
    "emulateHTTP": {},
    "emulateJSON": {}
};

/**
 * @typedef {Object}
 */
Backbone.Events;

/**
 * @param {string} event
 * @param {Function} callback
 * @param {Object=} context
 */
Backbone.Events.on = function(event, callback, context) {};
Backbone.Events.bind = Backbone.Events.on;

/**
 * @param {string=} event
 * @param {Function=} callback
 * @param {Object=} context
 */
Backbone.Events.off = function(event, callback, context) {};
Backbone.Events.unbind = Backbone.Events.off;

/**
 * @param {string} event
 * @param {...*} args
 */
Backbone.Events.trigger = function(event, args) {};

/**
 * @param {Function} onError
 * @param {Backbone.Model} originalModel
 * @param {Object} options
 */
Backbone.wrapError = function(onError, originalModel, options){};

/**
 * @param {string=} method
 * @param {Backbone.Model=} model
 * @param {Object=} options
 */
Backbone.sync = function(method, model, options){};

/**
 * @extends {Backbone.Events}
 * @constructor
 * @param {Object=} config
 */
Backbone.Model = function(config) {};

Backbone.Model.prototype.on = Backbone.Events.on;
Backbone.Model.prototype.off = Backbone.Events.off;

/**
 * @param {Object} properties
 * @param {Object=} classProperties
 */
Backbone.Model.prototype.extend = function(properties, classProperties) {};

/**
 * @param {...*} config
 */
Backbone.Model.prototype.initialize = function(config) {};

/**
 * @param {string} attribute
 * @return {*}
 */
Backbone.Model.prototype.get = function(attribute) {};

/**
 * @param {string|Object} key
 * @param {*=} value
 * @param {Object=} options
 * @return {Backbone.Model}
 */
Backbone.Model.prototype.set = function(key, value, options) {};

/**
 * @param {string} attribute
 * @return {string}
 */
Backbone.Model.prototype.escape = function(attribute) {};

/**
 * @param {string} attribute
 * @return {boolean}
 */
Backbone.Model.prototype.has = function(attribute) {};

/**
 * @param {string} attribute
 * @param {Object=} options
 * @return {Backbone.Model}
 */
Backbone.Model.prototype.unset = function(attribute, options) {};

/**
 * @param {Object=} options
 */
Backbone.Model.prototype.clear = function(options) {};

/** @type {number} */
Backbone.Model.id;

/** @type {string} */
Backbone.Model.prototype.idAttribute;

/** @type {number} */
Backbone.Model.cid;

/** @type {Object} */
Backbone.Model.attributes;

/** @type {Object} */
Backbone.Model.changed;

/** @type {Object|Function} */
Backbone.Model.defaults;

/**
 * @return {Object}
 */
Backbone.Model.toJSON();

/**
 * @param {Object} options
 * @return {Object} returns jQuery xhr
 */
Backbone.Model.prototype.fetch = function(options) {};

/**
 * @param {string|Object} key
 * @param {*} value
 * @param {Object=} options
 * @return {boolean|Object}
 */
Backbone.Model.prototype.save = function(key, value, options) {};

/**
 * @param {Object=} options
 * @return {boolean|Object}
 */
Backbone.Model.prototype.destroy = function(options) {};

/**
 * @param {Object} attributes
 */
Backbone.Model.prototype.validate = function(attributes) {};

/**
 * @return {boolean}
 */
Backbone.Model.prototype.isValid = function() {};

/**
 * @return {string}
 */
Backbone.Model.prototype.url = function() {};

/** @type {string|Function} */
Backbone.Model.urlRoot;

/**
 * @param {Object} resp
 * @param {Object} xhr
 * @return {Object}
 */
Backbone.Model.prototype.parse = function(resp, xhr) {};

/**
 * @return {Backbone.Model}
 */
Backbone.Model.prototype.clone = function() {};

/**
 * @return {boolean}
 */
Backbone.Model.prototype.isNew = function() {};

/**
 * @param {Object=} options
 * @return {Backbone.Model}
 */
Backbone.Model.prototype.change = function(options) {};

/**
 * @param {string|number=} attr
 * @return {boolean}
 */
Backbone.Model.prototype.hasChanged = function(attr) {};

/**
 * @param {Object=} attributes
 * @return {Object}
 */
Backbone.Model.prototype.changedAttributes = function(attributes) {};

/**
 * @param {string|number} attribute
 * @return {*}
 */
Backbone.Model.prototype.previous = function(attribute) {};

/**
 * @return {Object}
 */
Backbone.Model.prototype.previousAttributes = function() {};

/**
 * @constructor
 */
Backbone.Collection = function() {};

Backbone.Collection.prototype = {
    "trigger": function () {},
    "initialize": function () {},
    "toJSON": function () {},
    "getByCid": function () {},
    "sort": function () {},
    "pluck": function () {},
    "reset": function () {},
    "fetch": function () {},
    "create": function () {},
    "parse": function () {},
    "chain": function () {},
    "_reset": function () {},
    "_prepareModel": function () {},
    "_removeReference": function () {},
    "_onModelEvent": function () {},
    "forEach": function () {},
    "each": function () {},
    "map": function () {},
    "reduce": function () {},
    "reduceRight": function () {},
    "find": function () {},
    "detect": function () {},
    "filter": function () {},
    "select": function () {},
    "reject": function () {},
    "every": function () {},
    "all": function () {},
    "some": function () {},
    "any": function () {},
    "include": function () {},
    "contains": function () {},
    "invoke": function () {},
    "max": function () {},
    "min": function () {},
    "sortBy": function () {},
    "sortedIndex": function () {},
    "toArray": function () {},
    "size": function () {},
    "first": function () {},
    "initial": function () {},
    "rest": function () {},
    "last": function () {},
    "without": function () {},
    "indexOf": function () {},
    "shuffle": function () {},
    "lastIndexOf": function () {},
    "isEmpty": function () {},
    "groupBy": function () {}
};

/** @type {number} */
Backbone.Collection.length;

/** @type {Array} */
Backbone.Collection.prototype.models;

Backbone.Collection.prototype.on = Backbone.Events.on;
Backbone.Collection.prototype.off = Backbone.Events.on;
Backbone.Collection.prototype.bind = Backbone.Events.on;
Backbone.Collection.prototype.unbind = Backbone.Events.on;

/** @type {Backbone.Model} */
Backbone.Collection.model;

/**
 * @param {string|number} index
 * @return {Backbone.Model|undefined}
 */
Backbone.Collection.prototype.at = function(index) {};

/**
 * @param {string|number|Backbone.Model} id
 * @return {Backbone.Model|undefined}
 */
Backbone.Collection.prototype.get = function(id) {};

/**
 * @param {Object|Backbone.Model|Array.<Object>} models
 * @param {Object=} options
 */
Backbone.Collection.prototype.add = function(models, options){};

/**
 * @param {Object|Backbone.Model|Array.<Object>} models
 * @param {Object=} options
 */
Backbone.Collection.prototype.remove = function(models, options){};

Backbone.Router.prototype = {
    "on": function () {},
    "off": function () {},
    "trigger": function () {},
    "bind": function () {},
    "unbind": function () {},
    "initialize": function () {},
    "route": function () {},
    "navigate": function () {},
    "_bindRoutes": function () {},
    "_routeToRegExp": function () {},
    "_extractParameters": function () {}
};
Backbone.History.prototype = {
    "on": function () {},
    "off": function () {},
    "trigger": function () {},
    "bind": function () {},
    "unbind": function () {},
    "interval": {},
    "getFragment": function () {},
    "start": function () {},
    "stop": function () {},
    "route": function () {},
    "checkUrl": function () {},
    "loadUrl": function () {},
    "navigate": function () {},
    "_updateHash": function () {}
};
/**
 * @param {...*} args
 * @constructor
 */
Backbone.View = function(args){};
/** @type {string} */
Backbone.View.prototype.tagName;
/**
 * @param {...*} args
 */
Backbone.View.prototype.initialize = function(args) {};
/**
 * @return {Backbone.View}
 */
Backbone.View.prototype.render = function(){};
/** @type {Element} */
Backbone.View.prototype.el;
/** @type {jQuery} */
Backbone.View.prototype.$el;
Backbone.View.prototype.remove = function(){};
/**
 * @param {string} tagName
 * @param {Object=} attributes
 * @param {string=} content
 */
Backbone.View.prototype.make = function(tagName, attributes, content){};
Backbone.View.prototype.remove = function(){};
/**
 * @param {Object=} events
 */
Backbone.View.prototype.delegateEvents = function(events){};
/**
 * @param {Object=} events
 */
Backbone.View.prototype.undelegateEvents = function(events){};

/**
 * @param {Element|jQuery} element
 * @param {boolean=} delegate
 */
Backbone.View.prototype.setElement = function(element, delegate){};

/**
 * @param {string} selector
 */
Backbone.View.prototype.$ = function(selector){};
<commit_msg>making collection.length a prototype property<commit_after>/**
 * @fileoverview Externs for backbone-0.9.2.js
 *
 * built with http://www.dotnetwise.com/Code/Externs/index.html
 * see also: http://blog.dotnetwise.com/2009/11/closure-compiler-externs-extractor.html
 * via: http://code.google.com/p/closure-compiler/wiki/FAQ#How_do_I_write_an_externs_file?
 *
 * Note: when building via that page, you first need to load in underscrore.js, as that's a dependency.
 *       also, after running the extern for Backbone, you need to manually run it for:
 *       Backbone.Model.prototype, Backbone.Collection.prototype, Backbone.Router.prototype,
 *       Backbone.History.prototype, and Backbone.View.prototype
 *       because these objects are modified using _.extend(Backbone.Model.prototype ...)
 * @see http://documentcloud.github.com/backbone/
 * @externs
 */

var Backbone = {
    "VERSION": {},
    "setDomLibrary": function () {},
    "noConflict": function () {},
    "emulateHTTP": {},
    "emulateJSON": {}
};

/**
 * @typedef {Object}
 */
Backbone.Events;

/**
 * @param {string} event
 * @param {Function} callback
 * @param {Object=} context
 */
Backbone.Events.on = function(event, callback, context) {};
Backbone.Events.bind = Backbone.Events.on;

/**
 * @param {string=} event
 * @param {Function=} callback
 * @param {Object=} context
 */
Backbone.Events.off = function(event, callback, context) {};
Backbone.Events.unbind = Backbone.Events.off;

/**
 * @param {string} event
 * @param {...*} args
 */
Backbone.Events.trigger = function(event, args) {};

/**
 * @param {Function} onError
 * @param {Backbone.Model} originalModel
 * @param {Object} options
 */
Backbone.wrapError = function(onError, originalModel, options){};

/**
 * @param {string=} method
 * @param {Backbone.Model=} model
 * @param {Object=} options
 */
Backbone.sync = function(method, model, options){};

/**
 * @extends {Backbone.Events}
 * @constructor
 * @param {Object=} config
 */
Backbone.Model = function(config) {};

Backbone.Model.prototype.on = Backbone.Events.on;
Backbone.Model.prototype.off = Backbone.Events.off;

/**
 * @param {Object} properties
 * @param {Object=} classProperties
 */
Backbone.Model.prototype.extend = function(properties, classProperties) {};

/**
 * @param {...*} config
 */
Backbone.Model.prototype.initialize = function(config) {};

/**
 * @param {string} attribute
 * @return {*}
 */
Backbone.Model.prototype.get = function(attribute) {};

/**
 * @param {string|Object} key
 * @param {*=} value
 * @param {Object=} options
 * @return {Backbone.Model}
 */
Backbone.Model.prototype.set = function(key, value, options) {};

/**
 * @param {string} attribute
 * @return {string}
 */
Backbone.Model.prototype.escape = function(attribute) {};

/**
 * @param {string} attribute
 * @return {boolean}
 */
Backbone.Model.prototype.has = function(attribute) {};

/**
 * @param {string} attribute
 * @param {Object=} options
 * @return {Backbone.Model}
 */
Backbone.Model.prototype.unset = function(attribute, options) {};

/**
 * @param {Object=} options
 */
Backbone.Model.prototype.clear = function(options) {};

/** @type {number} */
Backbone.Model.id;

/** @type {string} */
Backbone.Model.prototype.idAttribute;

/** @type {number} */
Backbone.Model.cid;

/** @type {Object} */
Backbone.Model.attributes;

/** @type {Object} */
Backbone.Model.changed;

/** @type {Object|Function} */
Backbone.Model.defaults;

/**
 * @return {Object}
 */
Backbone.Model.toJSON();

/**
 * @param {Object} options
 * @return {Object} returns jQuery xhr
 */
Backbone.Model.prototype.fetch = function(options) {};

/**
 * @param {string|Object} key
 * @param {*} value
 * @param {Object=} options
 * @return {boolean|Object}
 */
Backbone.Model.prototype.save = function(key, value, options) {};

/**
 * @param {Object=} options
 * @return {boolean|Object}
 */
Backbone.Model.prototype.destroy = function(options) {};

/**
 * @param {Object} attributes
 */
Backbone.Model.prototype.validate = function(attributes) {};

/**
 * @return {boolean}
 */
Backbone.Model.prototype.isValid = function() {};

/**
 * @return {string}
 */
Backbone.Model.prototype.url = function() {};

/** @type {string|Function} */
Backbone.Model.urlRoot;

/**
 * @param {Object} resp
 * @param {Object} xhr
 * @return {Object}
 */
Backbone.Model.prototype.parse = function(resp, xhr) {};

/**
 * @return {Backbone.Model}
 */
Backbone.Model.prototype.clone = function() {};

/**
 * @return {boolean}
 */
Backbone.Model.prototype.isNew = function() {};

/**
 * @param {Object=} options
 * @return {Backbone.Model}
 */
Backbone.Model.prototype.change = function(options) {};

/**
 * @param {string|number=} attr
 * @return {boolean}
 */
Backbone.Model.prototype.hasChanged = function(attr) {};

/**
 * @param {Object=} attributes
 * @return {Object}
 */
Backbone.Model.prototype.changedAttributes = function(attributes) {};

/**
 * @param {string|number} attribute
 * @return {*}
 */
Backbone.Model.prototype.previous = function(attribute) {};

/**
 * @return {Object}
 */
Backbone.Model.prototype.previousAttributes = function() {};

/**
 * @constructor
 */
Backbone.Collection = function() {};

Backbone.Collection.prototype = {
    "trigger": function () {},
    "initialize": function () {},
    "toJSON": function () {},
    "getByCid": function () {},
    "sort": function () {},
    "pluck": function () {},
    "reset": function () {},
    "fetch": function () {},
    "create": function () {},
    "parse": function () {},
    "chain": function () {},
    "_reset": function () {},
    "_prepareModel": function () {},
    "_removeReference": function () {},
    "_onModelEvent": function () {},
    "forEach": function () {},
    "each": function () {},
    "map": function () {},
    "reduce": function () {},
    "reduceRight": function () {},
    "find": function () {},
    "detect": function () {},
    "filter": function () {},
    "select": function () {},
    "reject": function () {},
    "every": function () {},
    "all": function () {},
    "some": function () {},
    "any": function () {},
    "include": function () {},
    "contains": function () {},
    "invoke": function () {},
    "max": function () {},
    "min": function () {},
    "sortBy": function () {},
    "sortedIndex": function () {},
    "toArray": function () {},
    "size": function () {},
    "first": function () {},
    "initial": function () {},
    "rest": function () {},
    "last": function () {},
    "without": function () {},
    "indexOf": function () {},
    "shuffle": function () {},
    "lastIndexOf": function () {},
    "isEmpty": function () {},
    "groupBy": function () {}
};

/** @type {number} */
Backbone.Collection.prototype.length;

/** @type {Array} */
Backbone.Collection.prototype.models;

Backbone.Collection.prototype.on = Backbone.Events.on;
Backbone.Collection.prototype.off = Backbone.Events.on;
Backbone.Collection.prototype.bind = Backbone.Events.on;
Backbone.Collection.prototype.unbind = Backbone.Events.on;

/** @type {Backbone.Model} */
Backbone.Collection.model;

/**
 * @param {string|number} index
 * @return {Backbone.Model|undefined}
 */
Backbone.Collection.prototype.at = function(index) {};

/**
 * @param {string|number|Backbone.Model} id
 * @return {Backbone.Model|undefined}
 */
Backbone.Collection.prototype.get = function(id) {};

/**
 * @param {Object|Backbone.Model|Array.<Object>} models
 * @param {Object=} options
 */
Backbone.Collection.prototype.add = function(models, options){};

/**
 * @param {Object|Backbone.Model|Array.<Object>} models
 * @param {Object=} options
 */
Backbone.Collection.prototype.remove = function(models, options){};

Backbone.Router.prototype = {
    "on": function () {},
    "off": function () {},
    "trigger": function () {},
    "bind": function () {},
    "unbind": function () {},
    "initialize": function () {},
    "route": function () {},
    "navigate": function () {},
    "_bindRoutes": function () {},
    "_routeToRegExp": function () {},
    "_extractParameters": function () {}
};
Backbone.History.prototype = {
    "on": function () {},
    "off": function () {},
    "trigger": function () {},
    "bind": function () {},
    "unbind": function () {},
    "interval": {},
    "getFragment": function () {},
    "start": function () {},
    "stop": function () {},
    "route": function () {},
    "checkUrl": function () {},
    "loadUrl": function () {},
    "navigate": function () {},
    "_updateHash": function () {}
};
/**
 * @param {...*} args
 * @constructor
 */
Backbone.View = function(args){};
/** @type {string} */
Backbone.View.prototype.tagName;
/**
 * @param {...*} args
 */
Backbone.View.prototype.initialize = function(args) {};
/**
 * @return {Backbone.View}
 */
Backbone.View.prototype.render = function(){};
/** @type {Element} */
Backbone.View.prototype.el;
/** @type {jQuery} */
Backbone.View.prototype.$el;
Backbone.View.prototype.remove = function(){};
/**
 * @param {string} tagName
 * @param {Object=} attributes
 * @param {string=} content
 */
Backbone.View.prototype.make = function(tagName, attributes, content){};
Backbone.View.prototype.remove = function(){};
/**
 * @param {Object=} events
 */
Backbone.View.prototype.delegateEvents = function(events){};
/**
 * @param {Object=} events
 */
Backbone.View.prototype.undelegateEvents = function(events){};

/**
 * @param {Element|jQuery} element
 * @param {boolean=} delegate
 */
Backbone.View.prototype.setElement = function(element, delegate){};

/**
 * @param {string} selector
 */
Backbone.View.prototype.$ = function(selector){};
<|endoftext|>


<commit_before><commit_msg>add datasets pca select menu...<commit_after><|endoftext|>


<commit_before>/*
 * Copyright 2015 Open Networking Laboratory
 *
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 ONOS GUI -- App View Module
 */

(function () {
    'use strict';

    // constants
    var INSTALLED = 'INSTALLED',
        ACTIVE = 'ACTIVE',
        APP_MGMENT_REQ = 'appManagementRequest',
        FILE_UPLOAD_URL = 'applications/upload';

    angular.module('ovApp', [])
    .controller('OvAppCtrl',
        ['$log', '$scope', '$http',
        'FnService', 'TableBuilderService', 'WebSocketService', 'UrlFnService',

    function ($log, $scope, $http, fs, tbs, wss, ufs) {
        $scope.ctrlBtnState = {};

        function selCb($event, row) {
            // selId comes from tableBuilder
            $scope.ctrlBtnState.selection = !!$scope.selId;
            $log.debug('Got a click on:', row);

            refreshCtrls();
        }

        function refreshCtrls() {
            var row, rowIdx;
            if ($scope.ctrlBtnState.selection) {
                rowIdx = fs.find($scope.selId, $scope.tableData);
                row = rowIdx >= 0 ? $scope.tableData[rowIdx] : null;

                $scope.ctrlBtnState.installed = row && row.state === INSTALLED;
                $scope.ctrlBtnState.active = row && row.state === ACTIVE;
            } else {
                $scope.ctrlBtnState.installed = false;
                $scope.ctrlBtnState.active = false;
            }
        }

        tbs.buildTable({
            scope: $scope,
            tag: 'app',
            selCb: selCb,
            respCb: refreshCtrls
        });

        $scope.appAction = function (action) {
            if ($scope.ctrlBtnState.selection) {
                $log.debug('Initiating ' + action + ' of ' + $scope.selId);
                wss.sendEvent(APP_MGMENT_REQ, {
                    action: action,
                    name: $scope.selId
                });
            }
        };

        $scope.$on('FileChanged', function () {
            var formData = new FormData();
            if ($scope.appFile) {
                formData.append('file', $scope.appFile);
                $http.post(ufs.rsUrl(FILE_UPLOAD_URL), formData, {
                    transformRequest: angular.identity,
                    headers: {
                        'Content-Type': undefined
                    }
                })
                    .finally(function () {
                        $scope.sortCallback($scope.sortParams);
                        document.getElementById('inputFileForm').reset();
                    });
            }
        });

        $log.log('OvAppCtrl has been created');
    }])

    // triggers the input form to appear when button is clicked
    .directive('triggerForm', function () {
        return {
            restrict: 'A',
            link: function (scope, elem) {
                elem.bind('click', function () {
                    document.getElementById('uploadFile')
                        .dispatchEvent(new Event('click'));
                });
            }
        };
    })

    // binds the model file to the scope in scope.appFile
    // sends upload request to the server
    .directive('fileModel', ['$parse',
            function ($parse) {
        return {
            restrict: 'A',
            link: function (scope, elem, attrs) {
                var model = $parse(attrs.fileModel),
                    modelSetter = model.assign;

                elem.bind('change', function () {
                    scope.$apply(function () {
                        modelSetter(scope, elem[0].files[0]);
                    });
                    scope.$emit('FileChanged');
                });
            }
        };
    }]);
}());
<commit_msg>GUI -- App View no longer auto refreshes by default.<commit_after>/*
 * Copyright 2015 Open Networking Laboratory
 *
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 ONOS GUI -- App View Module
 */

(function () {
    'use strict';

    // constants
    var INSTALLED = 'INSTALLED',
        ACTIVE = 'ACTIVE',
        APP_MGMENT_REQ = 'appManagementRequest',
        FILE_UPLOAD_URL = 'applications/upload';

    angular.module('ovApp', [])
    .controller('OvAppCtrl',
        ['$log', '$scope', '$http',
        'FnService', 'TableBuilderService', 'WebSocketService', 'UrlFnService',

    function ($log, $scope, $http, fs, tbs, wss, ufs) {
        $scope.ctrlBtnState = {};

        function selCb($event, row) {
            // selId comes from tableBuilder
            $scope.ctrlBtnState.selection = !!$scope.selId;
            $log.debug('Got a click on:', row);

            refreshCtrls();
        }

        function refreshCtrls() {
            var row, rowIdx;
            if ($scope.ctrlBtnState.selection) {
                rowIdx = fs.find($scope.selId, $scope.tableData);
                row = rowIdx >= 0 ? $scope.tableData[rowIdx] : null;

                $scope.ctrlBtnState.installed = row && row.state === INSTALLED;
                $scope.ctrlBtnState.active = row && row.state === ACTIVE;
            } else {
                $scope.ctrlBtnState.installed = false;
                $scope.ctrlBtnState.active = false;
            }
        }

        tbs.buildTable({
            scope: $scope,
            tag: 'app',
            selCb: selCb,
            respCb: refreshCtrls
        });
        $scope.toggleRefresh();

        $scope.appAction = function (action) {
            if ($scope.ctrlBtnState.selection) {
                $log.debug('Initiating ' + action + ' of ' + $scope.selId);
                wss.sendEvent(APP_MGMENT_REQ, {
                    action: action,
                    name: $scope.selId
                });
            }
        };

        $scope.$on('FileChanged', function () {
            var formData = new FormData();
            if ($scope.appFile) {
                formData.append('file', $scope.appFile);
                $http.post(ufs.rsUrl(FILE_UPLOAD_URL), formData, {
                    transformRequest: angular.identity,
                    headers: {
                        'Content-Type': undefined
                    }
                })
                    .finally(function () {
                        $scope.sortCallback($scope.sortParams);
                        document.getElementById('inputFileForm').reset();
                    });
            }
        });

        $log.log('OvAppCtrl has been created');
    }])

    // triggers the input form to appear when button is clicked
    .directive('triggerForm', function () {
        return {
            restrict: 'A',
            link: function (scope, elem) {
                elem.bind('click', function () {
                    document.getElementById('uploadFile')
                        .dispatchEvent(new Event('click'));
                });
            }
        };
    })

    // binds the model file to the scope in scope.appFile
    // sends upload request to the server
    .directive('fileModel', ['$parse',
            function ($parse) {
        return {
            restrict: 'A',
            link: function (scope, elem, attrs) {
                var model = $parse(attrs.fileModel),
                    modelSetter = model.assign;

                elem.bind('change', function () {
                    scope.$apply(function () {
                        modelSetter(scope, elem[0].files[0]);
                    });
                    scope.$emit('FileChanged');
                });
            }
        };
    }]);
}());
<|endoftext|>


<commit_before>// pathNodes store the costs during the route calculation, with parents
function pathNode(position, costModifier, passable) {
    this.position = position;
    this.costModifier = costModifier;
    this.passable = passable;
    this.Gcost = 0;
    this.Hcost = 0;
    this.pathparent = null;

    this.totalCost = function() {
        return this.Gcost + this.Hcost;
    }
}

function pathNodeScore(node) {
    return node.totalCost();
}

function pathNodeEqual(n1, n2) {
    return (n1.position.x == n2.position.x &&
            n1.position.y == n2.position.y &&
            n1.Gcost == n2.Gcost && n1.Hcost == n2.Hcost);
}

function PathFinder(world, start) {
    this.GCost = function(current, neighbor) {
        // I don't really want to do the square root
        var xsq = (current.position.x - neighbor.position.x) *
                  (current.position.x - neighbor.position.x);
        var ysq = (current.position.y - neighbor.position.y) *
                  (current.position.y - neighbor.position.y);
        return Math.sqrt(xsq + ysq) * neighbor.costModifier;
    }

    // current and goal are pathNodes
    this.ManhattanDistance = function(current, goal) {
        return (Math.abs(goal.position.x - current.position.x) +
                Math.abs(goal.position.y - current.position.y));
    }

    this.findPath = function(goal) {
        if (!goal.passable) {
            return null;
        }
        // the openlist is pathNodes
        var openlist = new BinaryHeap(pathNodeScore, pathNodeEqual);
        // The closedlist does not need to be a heap, but I'm lazy
        var closedlist = new BinaryHeap(pathNodeScore, pathNodeEqual);
        var current = this.start;

        while (!this.atGoal(current, goal)) {
            this.updateOpenList(current, goal, openlist, closedlist);
            closedlist.push(current);
            if (openlist.size() == 0) {
                return null;
            }
            current = openlist.pop();
        }

        return this.reconstructPath(current, this.start);
    }

    this.reconstructPath = function(goal, start) {
        var current = goal;
        var path = [];
        while (!this.atGoal(current, start)) {
            path.push(current);
            current = current.pathparent;
        }
        path.push(current);

        return path.reverse();
    }

    this.updateOpenList = function(current, goal, open, closed) {
        var neighbors = this.getNeighbors(current);
        for (var i in neighbors) {
            this.processNeighbor(current, neighbors[i], goal, open, closed);
        }
    }

    this.processNeighbor = function(current, neighbor, goal, open, closed) {
        if (open.contains(neighbor)) {
        // Recalculate G cost and update parentage if necessary
            var g = this.GCost(current, neighbor) + current.Gcost;
            if (g < neighbor.Gcost) {
                neighbor.Gcost = g;
                neighbor.pathparent = current;
            }
        } else {
            if (neighbor.passable && !closed.contains(neighbor)) {
                // calculate the F = G + H cost of the neighbor
                var g = this.movementCostFunc(current, neighbor) + current.Gcost;
                var h = this.heuristicCostFunc(current, goal);
                neighbor.pathparent = current;
                neighbor.Gcost = g;
                neighbor.Hcost = h;
                // add the neighbor to the open list
                open.push(neighbor);
            }
        }
    }

    this.getNeighbors = function(current) {
        return getMatrixNeighbors(this.world, current.position);
    }

    this.defaultGoal = function(current, goal) {
        return (current.position.x == goal.position.x &&
                current.position.y == goal.position.y);
    }

    // world is a 2d array of pathNodes
    this.world = world;
    this.start = start;
    this.movementCostFunc = this.GCost;
    this.heuristicCostFunc = this.ManhattanDistance;
    this.atGoal = this.defaultGoal;
}

function getMatrixNeighbors(matrix, curPos) {
    function withinMatrix(position) {
        return position.x >= 0 && position.x < matrix.length &&
               position.y >= 0 && position.y < matrix[0].length;
    }

    var neighbors = [];
    var leftpos = {x: curPos.x - 1, y: curPos.y};
    if (withinMatrix(leftpos)) {
        neighbors.push(matrix[leftpos.x][leftpos.y]);
    }

    var rightpos = {x: curPos.x + 1, y: curPos.y};
    if (withinMatrix(rightpos)) {
        neighbors.push(matrix[rightpos.x][rightpos.y]);
    }

    var uppos = {x: curPos.x, y: curPos.y - 1};
    if (withinMatrix(uppos)) {
        neighbors.push(matrix[uppos.x][uppos.y]);
    }

    var downpos = {x: curPos.x, y: curPos.y + 1};
    if (withinMatrix(downpos)) {
        neighbors.push(matrix[downpos.x][downpos.y]);
    }

    return neighbors;
}
<commit_msg>Move manhattan distance calc for generalization<commit_after>// pathNodes store the costs during the route calculation, with parents
function pathNode(position, costModifier, passable) {
    this.position = position;
    this.costModifier = costModifier;
    this.passable = passable;
    this.Gcost = 0;
    this.Hcost = 0;
    this.pathparent = null;

    this.totalCost = function() {
        return this.Gcost + this.Hcost;
    }
}

function pathNodeScore(node) {
    return node.totalCost();
}

function pathNodeEqual(n1, n2) {
    return (n1.position.x == n2.position.x &&
            n1.position.y == n2.position.y &&
            n1.Gcost == n2.Gcost && n1.Hcost == n2.Hcost);
}

function PathFinder(world, start) {
    this.GCost = function(current, neighbor) {
        // I don't really want to do the square root
        var xsq = (current.position.x - neighbor.position.x) *
                  (current.position.x - neighbor.position.x);
        var ysq = (current.position.y - neighbor.position.y) *
                  (current.position.y - neighbor.position.y);
        return Math.sqrt(xsq + ysq) * neighbor.costModifier;
    }

    this.findPath = function(goal) {
        if (!goal.passable) {
            return null;
        }
        // the openlist is pathNodes
        var openlist = new BinaryHeap(pathNodeScore, pathNodeEqual);
        // The closedlist does not need to be a heap, but I'm lazy
        var closedlist = new BinaryHeap(pathNodeScore, pathNodeEqual);
        var current = this.start;

        while (!this.atGoal(current, goal)) {
            this.updateOpenList(current, goal, openlist, closedlist);
            closedlist.push(current);
            if (openlist.size() == 0) {
                return null;
            }
            current = openlist.pop();
        }

        return this.reconstructPath(current, this.start);
    }

    this.reconstructPath = function(goal, start) {
        var current = goal;
        var path = [];
        while (!this.atGoal(current, start)) {
            path.push(current);
            current = current.pathparent;
        }
        path.push(current);

        return path.reverse();
    }

    this.updateOpenList = function(current, goal, open, closed) {
        var neighbors = this.getNeighbors(current);
        for (var i in neighbors) {
            this.processNeighbor(current, neighbors[i], goal, open, closed);
        }
    }

    this.processNeighbor = function(current, neighbor, goal, open, closed) {
        if (open.contains(neighbor)) {
        // Recalculate G cost and update parentage if necessary
            var g = this.GCost(current, neighbor) + current.Gcost;
            if (g < neighbor.Gcost) {
                neighbor.Gcost = g;
                neighbor.pathparent = current;
            }
        } else {
            if (neighbor.passable && !closed.contains(neighbor)) {
                // calculate the F = G + H cost of the neighbor
                var g = this.movementCostFunc(current, neighbor) + current.Gcost;
                var h = this.heuristicCostFunc(current.position,
                                               goal.position);
                neighbor.pathparent = current;
                neighbor.Gcost = g;
                neighbor.Hcost = h;
                // add the neighbor to the open list
                open.push(neighbor);
            }
        }
    }

    this.getNeighbors = function(current) {
        return getMatrixNeighbors(this.world, current.position);
    }

    this.defaultGoal = function(current, goal) {
        return (current.position.x == goal.position.x &&
                current.position.y == goal.position.y);
    }

    // world is a 2d array of pathNodes
    this.world = world;
    this.start = start;
    this.movementCostFunc = this.GCost;
    this.heuristicCostFunc = ManhattanDistance;
    this.atGoal = this.defaultGoal;
}

function ManhattanDistance(current, goal) {
    return (Math.abs(current.x - goal.x) +
            Math.abs(current.y - goal.y));
}

function getMatrixNeighbors(matrix, curPos) {
    function withinMatrix(position) {
        return position.x >= 0 && position.x < matrix.length &&
               position.y >= 0 && position.y < matrix[0].length;
    }

    var neighbors = [];
    var leftpos = {x: curPos.x - 1, y: curPos.y};
    if (withinMatrix(leftpos)) {
        neighbors.push(matrix[leftpos.x][leftpos.y]);
    }

    var rightpos = {x: curPos.x + 1, y: curPos.y};
    if (withinMatrix(rightpos)) {
        neighbors.push(matrix[rightpos.x][rightpos.y]);
    }

    var uppos = {x: curPos.x, y: curPos.y - 1};
    if (withinMatrix(uppos)) {
        neighbors.push(matrix[uppos.x][uppos.y]);
    }

    var downpos = {x: curPos.x, y: curPos.y + 1};
    if (withinMatrix(downpos)) {
        neighbors.push(matrix[downpos.x][downpos.y]);
    }

    return neighbors;
}
<|endoftext|>


<commit_before>/**
 * Change news status
 */
(function($) {

    var widgetOptions = {

        /**
         * List of default options
         */
        options: {
        },

        /**
         * Constructor
         */
        _create: function() {

            var self = this;

            // Publish news
            $('.btn', self.element).on('click', function() {
                var $selfElement = $(this);
                $selfElement.attr('disabled', true);
                $.ajax({
                    url: app.baseUrl + '/staff/news/publish',
                    data: {
                        id:     $(self.element).data('news-id'),
                        status: $selfElement.data('status')
                    },
                    success: function() {
                        $('.btn', self.element).attr('disabled', false).removeClass('hide');
                        $selfElement.addClass('hide');
                    }
                });
            });

        },

        /**
         * Option setter
         */
        _setOption: function(key, value) {

        },

        /**
         * Destructor
         */
        _destroy: function() {

        }

    };

    $.widget('app.staffNewsStatusSwitcher', widgetOptions);

}(jQuery));<commit_msg>#90 Added error on news publishing<commit_after>/**
 * Change news status
 */
(function($) {

    var widgetOptions = {

        /**
         * List of default options
         */
        options: {
        },

        /**
         * Constructor
         */
        _create: function() {

            var self = this;

            // Publish news
            $('.btn', self.element).on('click', function() {
                var $selfElement = $(this);
                $selfElement.attr('disabled', true);
                $.ajax({
                    url: app.baseUrl + '/staff/news/publish',
                    data: {
                        id:     $(self.element).data('news-id'),
                        status: $selfElement.data('status')
                    },
                    success: function(response) {
                        if (response.errors) {
                            for (attrName in response.errors) {break;}
                            $('.btn:visible', self.element).tooltip({
                                title: response.errors[attrName][0]
                            }).tooltip('show');
                        } else {
                            $('.btn', self.element).attr('disabled', false).removeClass('hide');
                            $selfElement.addClass('hide');
                        }
                    }
                });
            });

        },

        /**
         * Option setter
         */
        _setOption: function(key, value) {

        },

        /**
         * Destructor
         */
        _destroy: function() {

        }

    };

    $.widget('app.staffNewsStatusSwitcher', widgetOptions);

}(jQuery));<|endoftext|>


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

/**
 * base class for light bulbs in the model
 *
 * @author John Blanco
 * @author Andrew Adare
 */
define( require => {
  'use strict';

  // modules
  const EFACConstants = require( 'ENERGY_FORMS_AND_CHANGES/common/EFACConstants' );
  const EnergyChunk = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyChunk' );
  const EnergyChunkPathMover = require( 'ENERGY_FORMS_AND_CHANGES/systems/model/EnergyChunkPathMover' );
  const energyFormsAndChanges = require( 'ENERGY_FORMS_AND_CHANGES/energyFormsAndChanges' );
  const EnergyType = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyType' );
  const EnergyUser = require( 'ENERGY_FORMS_AND_CHANGES/systems/model/EnergyUser' );
  const NumberProperty = require( 'AXON/NumberProperty' );
  const Range = require( 'DOT/Range' );
  const Utils = require( 'DOT/Utils' );
  const Vector2 = require( 'DOT/Vector2' );

  // constants
  const THERMAL_ENERGY_CHUNK_TIME_ON_FILAMENT = new Range( 2, 2.5 );
  const ENERGY_TO_FULLY_LIGHT = EFACConstants.MAX_ENERGY_PRODUCTION_RATE;
  const LIGHT_CHUNK_LIT_BULB_RADIUS = 0.1; // In meters.
  const LIGHT_CHANGE_RATE = 0.5; // In proportion per second.

  // energy chunk path offsets
  const LEFT_SIDE_OF_WIRE_OFFSET = new Vector2( -0.04, -0.041 );
  const WIRE_CURVE_POINT_1_OFFSET = new Vector2( -0.02, -0.041 );
  const WIRE_CURVE_POINT_2_OFFSET = new Vector2( -0.015, -0.04 );
  const WIRE_CURVE_POINT_3_OFFSET = new Vector2( -0.006, -0.034 );
  const WIRE_CURVE_POINT_4_OFFSET = new Vector2( -0.001, -0.026 );
  const WIRE_CURVE_POINT_5_OFFSET = new Vector2( -0.0003, -0.02 );
  const BOTTOM_OF_CONNECTOR_OFFSET = new Vector2( 0.0002, -0.01 );
  const RADIATE_POINT_OFFSET = new Vector2( 0.0002, 0.066 );
  const ELECTRICAL_ENERGY_CHUNK_OFFSETS = [
    WIRE_CURVE_POINT_1_OFFSET,
    WIRE_CURVE_POINT_2_OFFSET,
    WIRE_CURVE_POINT_3_OFFSET,
    WIRE_CURVE_POINT_4_OFFSET,
    WIRE_CURVE_POINT_5_OFFSET,
    BOTTOM_OF_CONNECTOR_OFFSET,
    RADIATE_POINT_OFFSET
  ];

  class LightBulb extends EnergyUser {

    /**
     * @param {Image} iconImage
     * @param {boolean} hasFilament
     * @param {Property.<boolean>} energyChunksVisibleProperty
     * @param {Tandem} tandem
     */
    constructor( iconImage, hasFilament, energyChunksVisibleProperty, tandem ) {
      super( iconImage, tandem );

      // @public (read-only) {NumberProperty}
      this.litProportionProperty = new NumberProperty( 0, {
        range: new Range( 0, 1 ),
        tandem: tandem.createTandem( 'litProportionProperty' ),
        phetioReadOnly: true,
        phetioHighFrequency: true,
        phetioDocumentation: 'proportion of brightness from the bulb'
      } );

      // @private
      this.hasFilament = hasFilament;
      this.energyChunksVisibleProperty = energyChunksVisibleProperty;

      // @private {number} - fewer thermal energy chunks are radiated for bulbs without a filament
      this.proportionOfThermalChunksRadiated = hasFilament ? 0.35 : 0.2;

      // @private - movers and flags that control how the energy chunks move through the light bulb
      this.electricalEnergyChunkMovers = [];
      this.filamentEnergyChunkMovers = [];
      this.radiatedEnergyChunkMovers = [];
      this.goRightNextTime = true;
    }

    /**
     * @param  {number} dt - time step, in seconds
     * @param  {Energy} incomingEnergy
     * @public
     */
    step( dt, incomingEnergy ) {
      if ( this.activeProperty.value ) {

        // handle any incoming energy chunks
        if ( this.incomingEnergyChunks.length > 0 ) {

          const incomingChunks = _.clone( this.incomingEnergyChunks );

          incomingChunks.forEach( incomingChunk => {

            if ( incomingChunk.energyTypeProperty.get() === EnergyType.ELECTRICAL ) {

              // add the energy chunk to the list of those under management
              this.energyChunkList.push( incomingChunk );

              // add a "mover" that will move this energy chunk through the wire to the bulb
              this.electricalEnergyChunkMovers.push(
                new EnergyChunkPathMover(
                  incomingChunk,
                  EnergyChunkPathMover.createPathFromOffsets( this.positionProperty.value, ELECTRICAL_ENERGY_CHUNK_OFFSETS ),
                  EFACConstants.ENERGY_CHUNK_VELOCITY )
              );
            }

            // yy design, this shouldn't happen, so warn if it does
            else {
              assert && assert(
                false,
                `Encountered energy chunk with unexpected type: ${this.incomingEnergyChunk.energyTypeProperty.get()}`
              );
            }
          } );

          this.incomingEnergyChunks.length = 0;
        }

        // move all of the energy chunks
        this.moveElectricalEnergyChunks( dt );
        this.moveFilamentEnergyChunks( dt );
        this.moveRadiatedEnergyChunks( dt );

        // set how lit the bulb is
        if ( this.energyChunksVisibleProperty.get() ) {

          // energy chunks are visible, so the lit proportion is dependent upon whether light energy chunks are present
          let lightChunksInLitRadius = 0;

          this.radiatedEnergyChunkMovers.forEach( mover => {
            const distance = mover.energyChunk.positionProperty.value.distance( this.positionProperty.value.plus( RADIATE_POINT_OFFSET ) );
            if ( distance < LIGHT_CHUNK_LIT_BULB_RADIUS ) {
              lightChunksInLitRadius++;
            }
          } );

          if ( lightChunksInLitRadius > 0 ) {

            // light is on
            this.litProportionProperty.set( Math.min( 1, this.litProportionProperty.get() + LIGHT_CHANGE_RATE * dt ) );
          }
          else {

            // light is off
            this.litProportionProperty.set( Math.max( 0, this.litProportionProperty.get() - LIGHT_CHANGE_RATE * dt ) );
          }
        }

        // energy chunks not currently visible
        else {
          if ( this.activeProperty.value && incomingEnergy.type === EnergyType.ELECTRICAL ) {
            this.litProportionProperty.set( Utils.clamp( incomingEnergy.amount / ( ENERGY_TO_FULLY_LIGHT * dt ), 0, 1 ) );
          }
          else {
            this.litProportionProperty.set( 0.0 );
          }
        }
      }
    }

    /**
     * @param  {number} dt - time step, in seconds
     * @private
     */
    moveRadiatedEnergyChunks( dt ) {

      // iterate over a copy to mutate original without problems
      const movers = _.clone( this.radiatedEnergyChunkMovers );

      movers.forEach( mover => {
        mover.moveAlongPath( dt );

        // remove the chunk and its mover
        if ( mover.pathFullyTraversed ) {
          this.energyChunkList.remove( mover.energyChunk );
          _.pull( this.radiatedEnergyChunkMovers, mover );
        }
      } );
    }

    /**
     * @param  {number} dt - time step, in seconds
     * @private
     */
    moveFilamentEnergyChunks( dt ) {

      // iterate over a copy to mutate original without problems
      const movers = _.clone( this.filamentEnergyChunkMovers );

      movers.forEach( mover => {
        mover.moveAlongPath( dt );

        // cause this energy chunk to be radiated from the bulb
        if ( mover.pathFullyTraversed ) {
          _.pull( this.filamentEnergyChunkMovers, mover );
          this.radiateEnergyChunk( mover.energyChunk );
        }
      } );
    }

    /**
     * @param  {number} dt - time step, in seconds
     * @private
     */
    moveElectricalEnergyChunks( dt ) {

      // iterate over a copy to mutate original without problems
      const movers = _.clone( this.electricalEnergyChunkMovers );

      movers.forEach( mover => {
        mover.moveAlongPath( dt );

        if ( mover.pathFullyTraversed ) {
          _.pull( this.electricalEnergyChunkMovers, mover );

          // turn this energy chunk into thermal energy on the filament
          if ( this.hasFilament ) {
            mover.energyChunk.energyTypeProperty.set( EnergyType.THERMAL );
            const path = this.createPathOnFilament( mover.energyChunk.positionProperty.value );
            const speed = getTotalPathLength( mover.energyChunk.positionProperty.value, path ) /
                          generateThermalChunkTimeOnFilament();
            this.filamentEnergyChunkMovers.push( new EnergyChunkPathMover( mover.energyChunk, path, speed ) );
          }
          else {

            // there is no filament, so just radiate the chunk
            this.radiateEnergyChunk( mover.energyChunk );
          }
        }
      } );
    }

    /**
     * @param  {Energy} incomingEnergy
     * @public
     * @override
     */
    preloadEnergyChunks( incomingEnergy ) {

      this.clearEnergyChunks();

      if ( incomingEnergy.amount < EFACConstants.MAX_ENERGY_PRODUCTION_RATE / 10 ||
           incomingEnergy.type !== EnergyType.ELECTRICAL ) {

        // no energy chunk pre-loading needed
        return;
      }

      const dt = 1 / EFACConstants.FRAMES_PER_SECOND;
      let energySinceLastChunk = EFACConstants.ENERGY_PER_CHUNK * 0.99; // prime the pump

      // simulate energy chunks moving through the system
      let preloadComplete = false;
      while ( !preloadComplete ) {
        energySinceLastChunk += incomingEnergy.amount * dt;

        // determine if time to add a new chunk
        if ( energySinceLastChunk >= EFACConstants.ENERGY_PER_CHUNK ) {
          const newEnergyChunk = new EnergyChunk(
            EnergyType.ELECTRICAL,
            this.positionProperty.value.plus( LEFT_SIDE_OF_WIRE_OFFSET ),
            Vector2.ZERO,
            this.energyChunksVisibleProperty
          );

          this.energyChunkList.push( newEnergyChunk );

          // add a "mover" that will move this energy chunk through the wire to the heating element
          this.electricalEnergyChunkMovers.push( new EnergyChunkPathMover(
            newEnergyChunk,
            EnergyChunkPathMover.createPathFromOffsets( this.positionProperty.value, ELECTRICAL_ENERGY_CHUNK_OFFSETS ),
            EFACConstants.ENERGY_CHUNK_VELOCITY
          ) );

          // update energy since last chunk
          energySinceLastChunk = energySinceLastChunk - EFACConstants.ENERGY_PER_CHUNK;
        }

        this.moveElectricalEnergyChunks( dt );
        this.moveFilamentEnergyChunks( dt );

        if ( this.radiatedEnergyChunkMovers.length > 1 ) {

          // a couple of chunks are radiating, which completes the pre-load
          preloadComplete = true;
        }
      }
    }

    /**
     * @param  {EnergyChunk} energyChunk
     * @private
     */
    radiateEnergyChunk( energyChunk ) {
      if ( phet.joist.random.nextDouble() > this.proportionOfThermalChunksRadiated ) {
        energyChunk.energyTypeProperty.set( EnergyType.LIGHT );
      }
      else {
        energyChunk.energyTypeProperty.set( EnergyType.THERMAL );
      }

      this.radiatedEnergyChunkMovers.push( new EnergyChunkPathMover(
        energyChunk,
        EnergyChunkPathMover.createRandomStraightPath(
          this.positionProperty.value,
          new Range( Math.PI / 3, Math.PI / 3 * 2 ) ),
        EFACConstants.ENERGY_CHUNK_VELOCITY )
      );
    }

    /**
     * @param  {Vector2} startingPoint
     * @returns {Vector2[]}
     * @private
     */
    createPathOnFilament( startingPoint ) {
      const path = [];
      const filamentWidth = 0.03;
      const x = ( 0.5 + phet.joist.random.nextDouble() / 2 ) * filamentWidth / 2 * ( this.goRightNextTime ? 1 : -1 );

      path.push( startingPoint.plus( new Vector2( x, 0 ) ) );
      this.goRightNextTime = !this.goRightNextTime;

      return path;
    }

    /**
     * deactivate the light bulb
     * @public
     * @override
     */
    deactivate() {
      super.deactivate();
      this.litProportionProperty.set( 0 );
    }

    /**
     * @public
     * @override
     */
    clearEnergyChunks() {
      super.clearEnergyChunks();
      this.electricalEnergyChunkMovers.length = 0;
      this.filamentEnergyChunkMovers.length = 0;
      this.radiatedEnergyChunkMovers.length = 0;
      this.incomingEnergyChunks.length = 0;
    }
  }

  /**
   * @returns {number} time
   * @private
   */
  const generateThermalChunkTimeOnFilament = () => {
    return THERMAL_ENERGY_CHUNK_TIME_ON_FILAMENT.min +
           phet.joist.random.nextDouble() * THERMAL_ENERGY_CHUNK_TIME_ON_FILAMENT.getLength();
  };

  /**
   * @param {Vector2} startingLocation
   * @param {Vector2[]} pathPoints
   * @returns {number}
   * @private
   */
  const getTotalPathLength = ( startingLocation, pathPoints ) => {
    if ( pathPoints.length === 0 ) {
      return 0;
    }

    let pathLength = startingLocation.distance( pathPoints[ 0 ] );
    for ( let i = 0; i < pathPoints.length - 1; i++ ) {
      pathLength += pathPoints[ i ].distance( pathPoints[ i + 1 ] );
    }

    return pathLength;
  };

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

<commit_msg>Clamp light bulb chunks-on proportion at 0.7, see https://github.com/phetsims/energy-forms-and-changes/issues/307<commit_after>// Copyright 2016-2020, University of Colorado Boulder

/**
 * base class for light bulbs in the model
 *
 * @author John Blanco
 * @author Andrew Adare
 */
define( require => {
  'use strict';

  // modules
  const EFACConstants = require( 'ENERGY_FORMS_AND_CHANGES/common/EFACConstants' );
  const EnergyChunk = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyChunk' );
  const EnergyChunkPathMover = require( 'ENERGY_FORMS_AND_CHANGES/systems/model/EnergyChunkPathMover' );
  const energyFormsAndChanges = require( 'ENERGY_FORMS_AND_CHANGES/energyFormsAndChanges' );
  const EnergyType = require( 'ENERGY_FORMS_AND_CHANGES/common/model/EnergyType' );
  const EnergyUser = require( 'ENERGY_FORMS_AND_CHANGES/systems/model/EnergyUser' );
  const NumberProperty = require( 'AXON/NumberProperty' );
  const Range = require( 'DOT/Range' );
  const Utils = require( 'DOT/Utils' );
  const Vector2 = require( 'DOT/Vector2' );

  // constants
  const THERMAL_ENERGY_CHUNK_TIME_ON_FILAMENT = new Range( 2, 2.5 );
  const ENERGY_TO_FULLY_LIGHT = EFACConstants.MAX_ENERGY_PRODUCTION_RATE;
  const LIGHT_CHUNK_LIT_BULB_RADIUS = 0.1; // In meters.
  const LIGHT_CHANGE_RATE = 0.5; // In proportion per second.

  // energy chunk path offsets
  const LEFT_SIDE_OF_WIRE_OFFSET = new Vector2( -0.04, -0.041 );
  const WIRE_CURVE_POINT_1_OFFSET = new Vector2( -0.02, -0.041 );
  const WIRE_CURVE_POINT_2_OFFSET = new Vector2( -0.015, -0.04 );
  const WIRE_CURVE_POINT_3_OFFSET = new Vector2( -0.006, -0.034 );
  const WIRE_CURVE_POINT_4_OFFSET = new Vector2( -0.001, -0.026 );
  const WIRE_CURVE_POINT_5_OFFSET = new Vector2( -0.0003, -0.02 );
  const BOTTOM_OF_CONNECTOR_OFFSET = new Vector2( 0.0002, -0.01 );
  const RADIATE_POINT_OFFSET = new Vector2( 0.0002, 0.066 );
  const ELECTRICAL_ENERGY_CHUNK_OFFSETS = [
    WIRE_CURVE_POINT_1_OFFSET,
    WIRE_CURVE_POINT_2_OFFSET,
    WIRE_CURVE_POINT_3_OFFSET,
    WIRE_CURVE_POINT_4_OFFSET,
    WIRE_CURVE_POINT_5_OFFSET,
    BOTTOM_OF_CONNECTOR_OFFSET,
    RADIATE_POINT_OFFSET
  ];

  class LightBulb extends EnergyUser {

    /**
     * @param {Image} iconImage
     * @param {boolean} hasFilament
     * @param {Property.<boolean>} energyChunksVisibleProperty
     * @param {Tandem} tandem
     */
    constructor( iconImage, hasFilament, energyChunksVisibleProperty, tandem ) {
      super( iconImage, tandem );

      // @public (read-only) {NumberProperty}
      this.litProportionProperty = new NumberProperty( 0, {
        range: new Range( 0, 1 ),
        tandem: tandem.createTandem( 'litProportionProperty' ),
        phetioReadOnly: true,
        phetioHighFrequency: true,
        phetioDocumentation: 'proportion of brightness from the bulb'
      } );

      // @private
      this.hasFilament = hasFilament;
      this.energyChunksVisibleProperty = energyChunksVisibleProperty;

      // @private {number} - fewer thermal energy chunks are radiated for bulbs without a filament
      this.proportionOfThermalChunksRadiated = hasFilament ? 0.35 : 0.2;

      // @private - movers and flags that control how the energy chunks move through the light bulb
      this.electricalEnergyChunkMovers = [];
      this.filamentEnergyChunkMovers = [];
      this.radiatedEnergyChunkMovers = [];
      this.goRightNextTime = true;
    }

    /**
     * @param  {number} dt - time step, in seconds
     * @param  {Energy} incomingEnergy
     * @public
     */
    step( dt, incomingEnergy ) {
      if ( this.activeProperty.value ) {

        // handle any incoming energy chunks
        if ( this.incomingEnergyChunks.length > 0 ) {

          const incomingChunks = _.clone( this.incomingEnergyChunks );

          incomingChunks.forEach( incomingChunk => {

            if ( incomingChunk.energyTypeProperty.get() === EnergyType.ELECTRICAL ) {

              // add the energy chunk to the list of those under management
              this.energyChunkList.push( incomingChunk );

              // add a "mover" that will move this energy chunk through the wire to the bulb
              this.electricalEnergyChunkMovers.push(
                new EnergyChunkPathMover(
                  incomingChunk,
                  EnergyChunkPathMover.createPathFromOffsets( this.positionProperty.value, ELECTRICAL_ENERGY_CHUNK_OFFSETS ),
                  EFACConstants.ENERGY_CHUNK_VELOCITY )
              );
            }

            // yy design, this shouldn't happen, so warn if it does
            else {
              assert && assert(
                false,
                `Encountered energy chunk with unexpected type: ${this.incomingEnergyChunk.energyTypeProperty.get()}`
              );
            }
          } );

          this.incomingEnergyChunks.length = 0;
        }

        // move all of the energy chunks
        this.moveElectricalEnergyChunks( dt );
        this.moveFilamentEnergyChunks( dt );
        this.moveRadiatedEnergyChunks( dt );

        // set how lit the bulb is
        if ( this.energyChunksVisibleProperty.get() ) {

          // energy chunks are visible, so the lit proportion is dependent upon whether light energy chunks are present
          let lightChunksInLitRadius = 0;

          this.radiatedEnergyChunkMovers.forEach( mover => {
            const distance = mover.energyChunk.positionProperty.value.distance( this.positionProperty.value.plus( RADIATE_POINT_OFFSET ) );
            if ( distance < LIGHT_CHUNK_LIT_BULB_RADIUS ) {
              lightChunksInLitRadius++;
            }
          } );

          if ( lightChunksInLitRadius > 0 ) {

            // light is on - empirically determined max to match the
            this.litProportionProperty.set( Math.min( 0.7, this.litProportionProperty.get() + LIGHT_CHANGE_RATE * dt ) );
          }
          else {

            // light is off
            this.litProportionProperty.set( Math.max( 0, this.litProportionProperty.get() - LIGHT_CHANGE_RATE * dt ) );
          }
        }

        // energy chunks not currently visible
        else {
          if ( this.activeProperty.value && incomingEnergy.type === EnergyType.ELECTRICAL ) {
            this.litProportionProperty.set( Utils.clamp( incomingEnergy.amount / ( ENERGY_TO_FULLY_LIGHT * dt ), 0, 1 ) );
          }
          else {
            this.litProportionProperty.set( 0.0 );
          }
        }
      }
    }

    /**
     * @param  {number} dt - time step, in seconds
     * @private
     */
    moveRadiatedEnergyChunks( dt ) {

      // iterate over a copy to mutate original without problems
      const movers = _.clone( this.radiatedEnergyChunkMovers );

      movers.forEach( mover => {
        mover.moveAlongPath( dt );

        // remove the chunk and its mover
        if ( mover.pathFullyTraversed ) {
          this.energyChunkList.remove( mover.energyChunk );
          _.pull( this.radiatedEnergyChunkMovers, mover );
        }
      } );
    }

    /**
     * @param  {number} dt - time step, in seconds
     * @private
     */
    moveFilamentEnergyChunks( dt ) {

      // iterate over a copy to mutate original without problems
      const movers = _.clone( this.filamentEnergyChunkMovers );

      movers.forEach( mover => {
        mover.moveAlongPath( dt );

        // cause this energy chunk to be radiated from the bulb
        if ( mover.pathFullyTraversed ) {
          _.pull( this.filamentEnergyChunkMovers, mover );
          this.radiateEnergyChunk( mover.energyChunk );
        }
      } );
    }

    /**
     * @param  {number} dt - time step, in seconds
     * @private
     */
    moveElectricalEnergyChunks( dt ) {

      // iterate over a copy to mutate original without problems
      const movers = _.clone( this.electricalEnergyChunkMovers );

      movers.forEach( mover => {
        mover.moveAlongPath( dt );

        if ( mover.pathFullyTraversed ) {
          _.pull( this.electricalEnergyChunkMovers, mover );

          // turn this energy chunk into thermal energy on the filament
          if ( this.hasFilament ) {
            mover.energyChunk.energyTypeProperty.set( EnergyType.THERMAL );
            const path = this.createPathOnFilament( mover.energyChunk.positionProperty.value );
            const speed = getTotalPathLength( mover.energyChunk.positionProperty.value, path ) /
                          generateThermalChunkTimeOnFilament();
            this.filamentEnergyChunkMovers.push( new EnergyChunkPathMover( mover.energyChunk, path, speed ) );
          }
          else {

            // there is no filament, so just radiate the chunk
            this.radiateEnergyChunk( mover.energyChunk );
          }
        }
      } );
    }

    /**
     * @param  {Energy} incomingEnergy
     * @public
     * @override
     */
    preloadEnergyChunks( incomingEnergy ) {

      this.clearEnergyChunks();

      if ( incomingEnergy.amount < EFACConstants.MAX_ENERGY_PRODUCTION_RATE / 10 ||
           incomingEnergy.type !== EnergyType.ELECTRICAL ) {

        // no energy chunk pre-loading needed
        return;
      }

      const dt = 1 / EFACConstants.FRAMES_PER_SECOND;
      let energySinceLastChunk = EFACConstants.ENERGY_PER_CHUNK * 0.99; // prime the pump

      // simulate energy chunks moving through the system
      let preloadComplete = false;
      while ( !preloadComplete ) {
        energySinceLastChunk += incomingEnergy.amount * dt;

        // determine if time to add a new chunk
        if ( energySinceLastChunk >= EFACConstants.ENERGY_PER_CHUNK ) {
          const newEnergyChunk = new EnergyChunk(
            EnergyType.ELECTRICAL,
            this.positionProperty.value.plus( LEFT_SIDE_OF_WIRE_OFFSET ),
            Vector2.ZERO,
            this.energyChunksVisibleProperty
          );

          this.energyChunkList.push( newEnergyChunk );

          // add a "mover" that will move this energy chunk through the wire to the heating element
          this.electricalEnergyChunkMovers.push( new EnergyChunkPathMover(
            newEnergyChunk,
            EnergyChunkPathMover.createPathFromOffsets( this.positionProperty.value, ELECTRICAL_ENERGY_CHUNK_OFFSETS ),
            EFACConstants.ENERGY_CHUNK_VELOCITY
          ) );

          // update energy since last chunk
          energySinceLastChunk = energySinceLastChunk - EFACConstants.ENERGY_PER_CHUNK;
        }

        this.moveElectricalEnergyChunks( dt );
        this.moveFilamentEnergyChunks( dt );

        if ( this.radiatedEnergyChunkMovers.length > 1 ) {

          // a couple of chunks are radiating, which completes the pre-load
          preloadComplete = true;
        }
      }
    }

    /**
     * @param  {EnergyChunk} energyChunk
     * @private
     */
    radiateEnergyChunk( energyChunk ) {
      if ( phet.joist.random.nextDouble() > this.proportionOfThermalChunksRadiated ) {
        energyChunk.energyTypeProperty.set( EnergyType.LIGHT );
      }
      else {
        energyChunk.energyTypeProperty.set( EnergyType.THERMAL );
      }

      this.radiatedEnergyChunkMovers.push( new EnergyChunkPathMover(
        energyChunk,
        EnergyChunkPathMover.createRandomStraightPath(
          this.positionProperty.value,
          new Range( Math.PI / 3, Math.PI / 3 * 2 ) ),
        EFACConstants.ENERGY_CHUNK_VELOCITY )
      );
    }

    /**
     * @param  {Vector2} startingPoint
     * @returns {Vector2[]}
     * @private
     */
    createPathOnFilament( startingPoint ) {
      const path = [];
      const filamentWidth = 0.03;
      const x = ( 0.5 + phet.joist.random.nextDouble() / 2 ) * filamentWidth / 2 * ( this.goRightNextTime ? 1 : -1 );

      path.push( startingPoint.plus( new Vector2( x, 0 ) ) );
      this.goRightNextTime = !this.goRightNextTime;

      return path;
    }

    /**
     * deactivate the light bulb
     * @public
     * @override
     */
    deactivate() {
      super.deactivate();
      this.litProportionProperty.set( 0 );
    }

    /**
     * @public
     * @override
     */
    clearEnergyChunks() {
      super.clearEnergyChunks();
      this.electricalEnergyChunkMovers.length = 0;
      this.filamentEnergyChunkMovers.length = 0;
      this.radiatedEnergyChunkMovers.length = 0;
      this.incomingEnergyChunks.length = 0;
    }
  }

  /**
   * @returns {number} time
   * @private
   */
  const generateThermalChunkTimeOnFilament = () => {
    return THERMAL_ENERGY_CHUNK_TIME_ON_FILAMENT.min +
           phet.joist.random.nextDouble() * THERMAL_ENERGY_CHUNK_TIME_ON_FILAMENT.getLength();
  };

  /**
   * @param {Vector2} startingLocation
   * @param {Vector2[]} pathPoints
   * @returns {number}
   * @private
   */
  const getTotalPathLength = ( startingLocation, pathPoints ) => {
    if ( pathPoints.length === 0 ) {
      return 0;
    }

    let pathLength = startingLocation.distance( pathPoints[ 0 ] );
    for ( let i = 0; i < pathPoints.length - 1; i++ ) {
      pathLength += pathPoints[ i ].distance( pathPoints[ i + 1 ] );
    }

    return pathLength;
  };

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

<|endoftext|>