baja/obj/UnitDatabase.js

/**
 * @copyright 2015 Tridium, Inc. All Rights Reserved.
 * @author Gareth Johnson
 */

/**
 * Defines {@link baja.UnitDatabase}.
 * @module baja/obj/UnitDatabase
 */
define([ "bajaScript/sys",
        "bajaScript/baja/comm/Callback",
        "bajaScript/baja/obj/Dimension",
        "bajaScript/baja/obj/Unit" ], function (
         baja,
         Callback,
         Dimension,
         Unit) {
  
  'use strict';
  
  var callbackify = baja.callbackify,
    
      STORAGE_KEY = 'bsUnitDatabase',

      retrievePromise;

////////////////////////////////////////////////////////////////
// Utility functions
////////////////////////////////////////////////////////////////  

  function decodeDimension(str) {
    return Dimension.DEFAULT.decodeFromString(str);
  }

  /**
   * Clear the saved unit database JSON from local storage.
   * 
   * @inner
   */
  function clearJsonFromStorage() {
    try {
      return baja.storage.removeItem(STORAGE_KEY);
    } catch (ignore) {
      //what to do?
    }
  }

  /**
   * Retrieve the saved unit database JSON from local storage.
   * 
   * @inner
   * @returns {Object} retrieved and parsed object, or null if not found
   */
  function getJsonFromStorage() {
    try {
      var str = baja.storage.getItem(STORAGE_KEY);
      return str && JSON.parse(str);
    } catch (ignore) {
      clearJsonFromStorage();
    }
  }

  /**
   * Persist the unit database JSON retrieved from the station into local 
   * storage.
   * 
   * @param {Object} json
   */
  function saveJsonToStorage(json) {
    try {
      return baja.storage.setItem(STORAGE_KEY, JSON.stringify(json));
    } catch (ignore) {
      //what to do?
    }
  }

  /**
   * Retrieve the unit database from the station.
   * 
   * It will make a network call to the `UnitChannel` on the BOX service, 
   * passing along the last time the unit database was retrieved. If the unit
   * database has been updated, it will be returned from the BOX service and
   * stored in local storage. If the database has not been updated since the
   * last time it was retrieved, the `UnitChannel` will not send it down
   * and the copy in local storage will be used instead.
   * 
   * @inner
   * @param {baja.comm.Batch} [batch] optional batch to use to retrieve the
   * unit database
   * @returns {Promise}
   */
  function retrieveJson(batch) {
    var cb = new Callback(baja.ok, baja.fail, batch),
        fromStorage = getJsonFromStorage(),
        lastKnownBuildTime = (fromStorage && fromStorage.buildTime) || 0;

    cb.addOk(function (ok, fail, resp) {
      baja.runAsync(function () {
        var json;

        if (fromStorage && !resp.db) {
          //we had the database saved locally, and it hasn't been updated since
          //we last retrieved it. use the existing db.
          json = fromStorage;
        } else {
          //new unit database from the station.
          json = resp;
          saveJsonToStorage(json);
        }

        ok(json);
      });
    });

    cb.addReq('unit', 'getUnitDatabase', {
      ifModifiedSince: lastKnownBuildTime
    });

    cb.autoCommit();

    return cb.promise();
  }

////////////////////////////////////////////////////////////////
// Quantities
////////////////////////////////////////////////////////////////  
  
  /**
   * Denotes one particular quantity from the unit database, and all the units
   * it contains.
   * 
   * @private
   * @memberOf baja.UnitDatabase
   * @class
   * @param {String} name quantity display name
   * @param {baja.Dimension} dimension quantity dimension
   * @param {Array.<baja.Unit>} units the units this quantity contains
   */
  var Quantity = function (name, dimension, units) {
    this.$dimension = dimension;
    this.$name = name;
    this.$units = units;
  };

  /**
   * Returns the display name of this quantity.
   * @returns {String}
   */
  Quantity.prototype.getName = function () {
    return this.$name;
  };

  /**
   * Returns the units this quantity contains.
   * 
   * @returns {Array.<baja.Unit>}
   */
  Quantity.prototype.getUnits = function () {
    return this.$units.slice();
  };

  /**
   * Returns the dimension of this quantity
   * @returns {baja.Dimension}
   * @since Niagara 4.13
   */
  Quantity.prototype.getDimension = function () {
    return this.$dimension;
  };

  /**
   * Returns the quantity name and dimension
   * @returns {String}
   */
  Quantity.prototype.toString = function () {
      return this.getName() + ' (' + this.getDimension().toString() + ')';
  };

  /**
   * JSON structure:
   * 
   *     {
   *       "buildTime": {long} db last build time,
   *       "db": [
   *         {
   *           "n": {string} quantity name,
   *           "d": {baja.dimension} dimension,
   *           "u": [
   *             {
   *               "n": {string} unit name,
   *               "s": {string} unit symbol,
   *               "d": {string} unit dimension (BDimension#decodeFromString),
   *               "sc": {double} unit scale,
   *               "o": {double} unit offset,
   *               "p": {boolean} unit is prefix
   *             }
   *           ]
   *         }
   *       ],
   *       "v": {long} version number
   *     }
   * 
   * @inner
   * @param json
   * @returns {Array}
   */
  function toQuantityArray(json) {
    var quantities = [],
        db = json.db;
   
    for (var i = 0; i < db.length; i++) {
      var quantity = db[i],
          u = quantity.u,
          units = [];
      
      for (var j = 0; j < u.length; j++) {
        var unitObj = u[j];
        units.push(Unit.make(unitObj.n, unitObj.s, 
          decodeDimension(unitObj.d), unitObj.sc, unitObj.o, unitObj.p));
      }
      quantities.push(new Quantity(quantity.n, decodeDimension(quantity.d), units));
    }
    
    return quantities;
  }



////////////////////////////////////////////////////////////////
// UnitDatabase implementation
////////////////////////////////////////////////////////////////
  
  /**
   * Queries the unit database from the station.
   * 
   * There is no reason to call this constructor directly; rather use the 
   * static accessor functions.
   *
   * @class
   * @alias baja.UnitDatabase
   */
  var UnitDatabase = function UnitDatabase(json) {
    this.$quantities = toQuantityArray(json);
    this.$byName = {};

    var conversions = json.cdb || [],
        byMetric = {},
        byEnglish = {};

    for (var i = 0; i < conversions.length; i++) {
      var conv = conversions[i];
      byMetric[conv.metric] = this.getUnit(conv.english);
      byEnglish[conv.english] = this.getUnit(conv.metric);
    }

    this.$conversions = conversions;
    this.$byMetric = byMetric;
    this.$byEnglish = byEnglish;
  };
  
  UnitDatabase.Quantity = Quantity;

  /**
   * Asynchronously retrieve the unit database from the station. The network 
   * call to the station will only happen once: the same database instance will
   * be resolved no matter how many times this function is called.
   * 
   * @param {Object} [callbacks]
   * @param {Function} [callbacks.ok] (Deprecated: use Promise) ok callback,
   * will receive a `UnitDatabase` instance populated with data retrieved from
   * the station
   * @param {Function} [callbacks.fail] (Deprecated: use Promise) fail callback
   * @param {baja.comm.Batch} [callbacks.batch] batch to use for the network
   * request
   * @returns {Promise.<baja.UnitDatabase>}
   */
  UnitDatabase.get = function (callbacks) {
    callbacks = callbackify(callbacks);
    
    if (!retrievePromise) {
      retrievePromise = retrieveJson(callbacks.batch)
        .then(function (json) {
          return new UnitDatabase(json);
        });
    }

    retrievePromise.then(callbacks.ok, callbacks.fail);
    return retrievePromise;
  };

  /**
   * Get all quantities contained in the unit database.
   * 
   * @returns {Array.<baja.UnitDatabase.Quantity>}
   */
  UnitDatabase.prototype.getQuantities = function () {
    return this.$quantities.slice();
  };

  /**
   * Get an array of all raw entries from `unitConversion.xml`.
   * @private
   * @returns {Array.<Object>} array where each object has `english` and
   * `metric` properties - each are unit names
   */
  UnitDatabase.prototype.$getUnitConversions = function () {
    return this.$conversions.slice();
  };

  /**
   * Retrieve the unit instance with the given name.
   *
   * @param {String|baja.Unit} name the desired unit name. If a baja.Unit is
   * passed, it will just be returned back directly.
   * @returns {baja.Unit|null} the unit, or `null` if not found
   */
  UnitDatabase.prototype.getUnit = function (name) {
    if (name instanceof baja.Unit) { return name; }

    var byName = this.$byName, quantities = this.$quantities, units, i, j;

    if (byName[name] !== undefined) {
      return byName[name];
    }

    for (i = 0; i < quantities.length; i++) {
      units = quantities[i].getUnits();
      for (j = 0; j < units.length; j++) {
        if (units[j].getUnitName() === name) {
          return (byName[name] = units[j]);
        }
      }
    }

    return (byName[name] = null);
  };

  /**
   * Convert a unit from metric to English or vice versa.
   *
   * @param {String|number|baja.FrozenEnum} unitConversion `metric` to convert
   * from English to metric; `english` to convert from metric to English. Also
   * accepts baja:UnitConversion instances or ordinals.
   * @param {baja.Unit} unit the unit to convert
   * @returns {baja.Unit|null} the converted unit. If the unit cannot be
   * converted, the same unit will be returned directly. If no unit given,
   * returns null.
   */
  UnitDatabase.prototype.convertUnit = function (unitConversion, unit) {
    if (!unit || !unitConversion) {
      return unit || null;
    }

    var name = unit.getUnitName();

    switch (unitConversion.valueOf()) {
      case 1: case 'metric': return this.$byEnglish[name] || unit;
      case 2: case 'english': return this.$byMetric[name] || unit;
      default: return unit;
    }
  };

  /**
   * Get the Quantity which contains the specified unit or return undefined if the unit is not
   * contained by the database.
   * @since Niagara 4.14
   * @param {baja.Unit} unit the unit we want the quantity for
   * @returns {baja.UnitDatabase.Quantity|undefined}
   */
  UnitDatabase.prototype.getQuantity = function (unit) {
    const quantities = this.getQuantities();

    for (let index1 = 0; index1 < quantities.length; index1++) {
      const quantity = quantities[index1];
      const units = quantity.getUnits();
      for (let index2 = 0; index2 < units.length; index2++) {
        if (units[index2].equivalent(unit)) {
          return quantity;
        }
      }
    }

    return undefined;
  };

  return UnitDatabase;
});