bmi-implementation/case-study/hlclearCreek.js

import HydroLangBMI from "./../hydroLangBMI.js";
import { Hydro } from "./../globalHydro.js";

const h = Hydro.ins();

/**
 * Data module implementation for the Clear Creek HLM case study model
 * Overrides some of the methods implemented in the BMI HydroLang implementation.
 * Extends the HydroLang functional space by inheritance.
 * The instance calls either a data request from EPA precipitation model API's or
 * calls data from an preestablished model for the HLM-C implementation.
 * Provides data from EPA using grided models on an hourly scale.
 * Connects with the USGS gauging station for comparision purposes.
 * @class
 * @name HLclearCreek
 * @extends HydroLangBMI
 * @param {String} configfile
 */

class HLclearCreek extends HydroLangBMI {
  constructor(configfile = undefined) {
    super(configfile);
    this.links = [];
    this.stgGauge = [];
  }

  /**
   * Run if EPA data is to be queried.
   * @method additionalData
   * @memberof HLclearCreek
   */
  additionalData() {
    if (this._params.source === "epa") {
      this._startTime = new Date(this._args["dateTimeSpan"]["startDate"]);
      this._startTime = this._startTime.getTime() / 1000;
      this._endTime = new Date(this._args["dateTimeSpan"]["endDate"]);
      this._endTime = this._endTime.getTime() / 1000;
      (() => {
        fetch("./data/grid_coord.json")
          .then((res) => res.json())
          .then((data) => {
            this.grid = data;
          });
      })();
      (() => {
        fetch("./data/links_grid.json")
          .then((res) => res.json())
          .then((data) => {
            this.gridLinks = data;
            this.links = Object.keys(data);
          });
      })();
    }
  }

  /**
   * Update the model until a required time,
   * Depending on the module and function required.
   * @method update
   * @param {void}
   * @memberof HLclearCreek
   */
  update() {
    this.update_until();
  }

  /**
   * Updates a model and function call depending on the requirements
   * of the function and module called.
   * @method update_until
   * @memberof HLclearCreek
   * @param {Number} time - default time to be updated depending on the model
   * @returns {void} updates the current variable to the required timestep.
   */
  update_until(time = this._now + this._defaultStep) {
    time = Math.min(this._defaultStep, this._endTime);
    //setting current time to
    this._now === 0
      ? (this._now = this._startTime)
      : (this._now = this._now + time);
  }

  /**
   * Returns the current state of the model. For HLclearCreek is the default time step.
   * @method get_time_step
   * @memberof HLclearCreek
   * @returns {Number} default time step
   */

  get_time_step() {
    return this._defaultStep;
  }

  /**
   * Obtains the rainfall values at a specific index in the results array, when an index array is passed.
   * @method get_value_at_indices
   * @param {String} var_name - name of the variable used for the calculations
   * @param {Object[]} dest - array destiny required for the results
   * @param {Object[]} indices - array with the values of the links
   * @returns {Object[]} rainfall values perr order of link given.
   * @memberof HLclearCreek
   */

  get_value_at_indices(var_name, dest, indices) {
    var current = this.get_current_time(),
      timeIndex = super.value_index(current, this.results["dates"]);
    console.log(timeIndex);
    indices.forEach((link) => {
      this._params.source === "epa"
        ? dest.push(this.results[this.gridLinks[link]][timeIndex])
        : dest.push(this.results[link][timeIndex]);
    });

    return dest;
  }

  /**********************************/
  /*** Helper functions **/
  /**********************************/

  /**
   * Method for downloading data from the provided calls for the EPA API.
   * Calls the
   * @method retrieveData
   * @memberof HLclearCreek
   * @param {Object[]} arrayCont - array with all the lat-lon coordinates for each link.
   */
  retrieveEpaData(arrayCont) {
    //Downloads the data from EPA
    if (this._params.source === "epa") {
      //Removes first item from uploaded array
      // arrayCont.forEach((arr) => {
      //   arr.shift();
      // });
      // this.upload = arrayCont
      // var fnSt = step;
      const start = performance.now(),
        linkCalls = (st) => {
          var stgFunc = [];
          for (let k = 0; k < st[0].length; k++) {
            this._params.link = st[0][k];
            this.links.push(st[0][k]);
            const fs = () =>
              new Promise((resolve) =>
                resolve(
                  (this._args.geometry.point.latitude = st[1][k]),
                  (this._args.geometry.point.longitude = st[2][k])
                )
              );
            stgFunc.push(fs);
          }
          return stgFunc;
        };

      //Creating collection of unique
      // this.grid = this.hlIns()["analyze"]["stats"]["unique"]({data: arrayCont[1]});
      // this.links = this.hlIns()["analyze"]["stats"]["unique"]({data: arrayCont[0]});

      //Create stage index that contains first instances of each grid id's to get lat-lon
      // var stgInd = [];
      // var indLatLon = [[],[]]
      // for(var i = 0; i < this.grid.length; i++){
      //   stgInd.push(arrayCont[1].indexOf(this.grid[i]))
      // };
      // for (var j =0 ; j < stgInd.length; j++){
      //   indLatLon[0].push(arrayCont[3][stgInd[j]]);
      //   indLatLon[1].push(arrayCont[2][stgInd[j]])
      // }

      const gridCalls = (st) => {
        var stgFunc = [];
        for (let k = 0; k < st[0].length; k++) {
          const fs = () =>
            new Promise((resolve) =>
              resolve(
                (this._args.geometry.point.latitude = st[0][k]),
                (this._args.geometry.point.longitude = st[1][k])
              )
            );
          stgFunc.push(fs);
        }
        return stgFunc;
      };

      //Separate each call into bactches of equal step
      //To avoid overclocking the API.
      // for (; fnSt <= arrayCont[0].length; fnSt += step) {
      //   stgArr.push(arrayCont.map((arr) => arr.slice(iniSt, fnSt)));
      //   iniSt += step;
      // }

      //Loop through each of the requests in each batch, awaiting
      //a specific timespan defined by the user, compliant with what
      //the API require.
      // for (let i = 0; i < stgArr.length; i++) {
      //   var j = i * 240000;
      //   setTimeout(() => {
      //     var res = linkCalls(stgArr[i]);
      //     for (let f = 0; f < res.length; f++) {
      //       var intStart = performance.now();
      //       var k = f * 20000;
      //       setTimeout(() => {
      //         res[f]().then(() => {
      //           super.handleConfig();
      //           console.log(
      //             `Request time for bulk ${i} location ${
      //               this._args.geometry.point.latitude
      //             }, ${this._args.geometry.point.longitude} is: ${
      //               (performance.now() - intStart) / 1000
      //             } s, bulk time: ${(performance.now() - start) / 1000} s`
      //           );
      //         });
      //       }, k);
      //     }
      //   }, j);
      // }

      var res = gridCalls(this.grid.slice(1, 3));
      for (let f = 0; f < res.length; f++) {
        var intStart = performance.now(),
          k = f * 10000;
        setTimeout(() => {
          res[f]().then(() => {
            super.handleConfig();
            console.log(
              `Total bulk size: ${f} items, location ${
                this._args.geometry.point.latitude
              }, ${this._args.geometry.point.longitude}, time sent: ${
                (performance.now() - intStart) / 1000
              } s, bulk time: ${(performance.now() - start) / 1000} s`
            );
          });
        }, k);
      }
    } else {
      return console.log("Method implemented only for EPA data sources.");
    }
  }

  /**
   * Downloads data from the USGS site
   * @method retrieveGauge
   * @memberof HLclearCreek
   *
   */

  async retrieveGauge(startDate, endDate) {
    //Downloads the data for the gauging station.

    var gaugeData = {
      params: {
        source: "usgs",
        datatype: "instant-values",
        proxyurl:
          this._params.proxyServer === undefined
            ? "https://cors-anywhere.herokuapp.com/"
            : this._params.proxyServer,
      },
      args: {
        site: "USGS:05454300",
        format: "json",
        startDt:
          this._params.source === "epa"
            ? this._args.dateTimeSpan.startDate.slice(0, -9)
            : this._args.startDate.slice(0, -6),
        endDt:
          this._params.source === "epa"
            ? this._args.dateTimeSpan.endDate.slice(0, -9)
            : this._args.endDate.slice(0, -6),
      },
    };

    this.stgGauge.push(
      await h[this._moduleName][this._functionName]({
        args: gaugeData.args,
        params: gaugeData.params,
        data: this._inputVars,
      })
    );
  }

  /**
   * Grabs the downloaded object to create the links object and parse the dates
   * for each link so they can be in unixtime.
   * Overrides the values of the time used depending on the type of unit used
   * @method spreadResults
   * @memberof HLclearCreek
   * @returns {Void} sets the results to be access later by HLM
   */

  spreadResults() {
    //For the clear creek and EPA data
    var stgDate = [],
      stgRes = {};
    //Manipulating the results coming from the Clear Creek API
    if (this._params.source === "clearcreek") {
      (() => (this.results = this.results[0]))();
      //Just using the first element to get the dates in unix epoch
      this.links.slice(0, 1).forEach((link) => {
        this.results[link]["dates"].forEach((date) => {
          var parsedDate = new Date(date),
            stgUnix = parsedDate.getTime() / 1000;
          this._timeUnit == "hr" ? stgUnix / 3600 : stgUnix;
          stgDate.push(stgUnix);
        });
        this.results["dates"] = stgDate;
      });
      this.links.forEach((link) => {
        stgRes[link] = this.results[link]["values"];
      });
      stgRes["dates"] = [...stgDate];
      this.results = stgRes;

      //Gets missing index of
      const indLoc = (arr, step) => {
        //Adding the 0s and corresponding times to the indexes
        var indexes = [[], []];
        for (i = 0; i + 1 < arr.length; i++) {
          var div = (arr[i + 1] - arr[i]) / step;
          if (div !== 1) {
            indexes[0].push(arr.indexOf(arr[i]));
            indexes[1].push(div - 1);
          }
        }
        return indexes;
      };

      var indexes = indLoc(stgDate, 3600),
        stgDateVals = [],
        stgArrays = [];
      for (var k = 0; k < indexes[0].length; k++) {
        var l = indexes[1][k],
          h = stgDate[indexes[0][k]],
          sg = Array(l).fill(0);
        stgArrays.push(sg);
        sg = sg.map((val, c) => {
          return (val = h + 3600 * (c + 1));
        });
        stgDateVals.push(sg);
      }

      var stgStg = [...stgDate];
      for (var n = 0; n < stgDateVals.length; n++) {
        var accum = indexes[1].slice(0, n).reduce((a, b) => a + b, 0);
        stgStg.splice(
          n === 0
            ? indexes[0][n] + 1
            : // : indexes[0][n] + (stgStg.length - this.results['dates'].length),
              indexes[0][n] + accum + 1,
          0,
          ...stgDateVals[n]
        );
        this.links.forEach((link) => {
          this.results[link].splice(
            n === 0
              ? indexes[0][n] + 1
              : // : indexes[0][n] + (stgStg.length - this.results['dates'].length),
                indexes[0][n] + accum + 1,
            0,
            ...stgArrays[n]
          );
        });
      }

      this.results["dates"] = stgStg;

      //Memory cleanup
      stgStg = [];
      stgDateVals = [];
      stgArrays = [];
      stgRes = [];
      stgDate = [];
      //Manipulating the results coming from the EPA API's
    } else {
      stgDate = Object.keys(this.results[0].data).map((date) => {
        var stg = new Date(date);
        return stg.getTime() / 1000;
      });
      for (var i = 0; i < this.results.length; i++) {
        var stgVal = [];
        this.results[i] = this.results[i].data;

        stgVal = Object.values(this.results[i].data).map((val) => {
          return JSON.parse(val);
        });

        stgRes[this.grid[0][i]] = stgVal;
      }
      stgRes["dates"] = stgDate;
      this.results = stgRes;
      stgRes = [];
      stgDate = [];

      //Reassigning the values of each of the grids to the corresponding links
      // var position = {}; this.upload[1].forEach((val, pos) => {position[val] = position[val] || []; position[val].push(pos)});
      // var links = {}; Object.keys(position).forEach(key => {var stgLinks = position[key].map(val => this.upload[0][val]); links[key] = stgLinks});
      // var finalLinks = {}; Object.keys(links).forEach(key => {links[key].forEach(val => {(val in finalLinks) ? null : finalLinks[val] = this.results[key]})});
      // finalLinks["dates"] = this.results.dates
      // this.results = finalLinks;

      // //Data cleanup
      // position = {}
      // this.upload = [];
      // links = {}
      // finalLinks = {}
    }
  }

  /**
   * Spreads the results downloaded from the USGS service to a global variable
   * called gauge
   * @method spreadGauge
   * @memberof HLclearCreek
   */
  spreadGauge() {
    //For the gauging statation data
    //Stremflow data
    var r = h[this._moduleName]["transform"]({
        params: { save: "value" },
        args: { type: "ARR", keep: '["datetime", "value"]' },
        data: this.stgGauge[0],
      }),
      //Gauge height
      r1 = h[this._moduleName]["transform"]({
        params: { save: "value" },
        args: { type: "ARR", keep: '["value"]' },
        data: this.stgGauge[0]["value"]["timeseries"][1],
      });

    //Having all the data into a single array
    r.push(r1[0]);
    //Converting the data types
    r.forEach((arr) => arr.shift());

    //Converting to unixtime
    r[0] = r[0].map((val) => {
      var stgDate = new Date(val);
      return stgDate.getTime() / 1000;
    });
    //Converting to numbers
    r[1] = r[1].map(Number);
    r[2] = r[2].map(Number);

    //Defining the gauging variable
    this.gauge = r;
    r = [];
  }

  /**
   * @method getLinks
   * @memberof HLclearCreek
   * Searches and seeks for the name of the links for the analysis.
   * @returns {Object[]} array with names of the links
   */

  getLinks() {
    this.links = Object.keys(this.results[0][0]);
  }

  /**
   * Checks for storm events given dates.
   * @memberof HLclearCreek
   * @todo finish implementation.
   * @returns {Number} identification of storm event
   */
  rainID() {
    var ev = this.results.dates,
      events = {};
    for (var i = 0; i < ev.length; i++) {
      var stgDates = [];
      if (this.dataStep(ev[i], ev[i + 1]) === super.get_time_step()) {
        stgDates.push(ev[i]);
      }
      Object.assign(events, {
        [`Start ${stgDates[0]}`]: [stgDates[0], stgDates[stgDates.length]],
      });
      stgDates = [];
    }
    return events;
  }

  /**
   * Steps through an event given step size.
   * @method dataStep
   * @memberof HLclearCreek
   * @param {Number} i - initial value
   * @param {Number} j - i + 1 value
   * @returns {Number} difference between the two
   */

  dataStep(i, j) {
    try {
      j - i !== super.get_time_step()
        ? (() => {
            return j - i;
          })()
        : null;
    } catch (undefined) {
      return {};
    }
  }

  /**********************************/
  /*** End of Helper functions **/
  /**********************************/
}

typeof window !== "undefined" ? (window.HLclearCreek = HLclearCreek) : null;
export default HLclearCreek;