/**
   * Retrieves velocities as a vector [u,v,w] based on given positions
   *
   * @param time - time coordinate in milliseconds
   * @param z - depth coordinate
   * @param lon - longitude (decimal degrees)
   * @param lat - latitude (decimal degrees)
   */
  @Override
  public synchronized double[] getVelocities(long time, double z, double lon, double lat) {

    if (Double.isNaN(lon) || Double.isNaN(lat)) {
      throw new IllegalArgumentException("Latitude or Longitude value is NaN");
    }

    // Completely outside the time bounds

    if (time < bounds[0][0] || time > bounds[0][1]) {
      this.notifyAll();
      return null;
    }

    // Completely outside the vertical bounds

    if (z < bounds[1][0] || z > bounds[1][1]) {
      this.notifyAll();
      return null;
    }

    // Completely outside the north-south bounds - return null as opposed
    // to NODATA

    if (lat < bounds[2][0] || lat > bounds[2][1]) {
      this.notifyAll();
      return null;
    }

    // Completely outside the east-west bounds

    if (lon < bounds[3][0] || lon > bounds[3][1]) {
      this.notifyAll();
      return null;
    }

    checkTime(time);

    try {
      int js, is, ks, ts;

      // Searching for the cell indices nearest to the given location

      is = yloc.lookup(lat);
      js = xloc.lookup(lon);
      ks = zloc.lookup(z);
      ts = tloc.lookup(TimeConvert.millisToHYCOM(time));

      // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
      // ATTENTION!!!! THE ORDER IS VERY IMPORTANT HERE!!!! Latitude (i/y)
      // and then Longitude (j/x). That's the way it is set up in the
      // NetCDF File. The best way would be to have automatic order
      // detection
      // somehow....
      // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

      // Handling data edges

      int i_lhs = halfKernel;
      int i_rhs = halfKernel;
      int j_lhs = halfKernel;
      int j_rhs = halfKernel;
      int k_lhs = zHalfKernel;
      int k_rhs = zHalfKernel;

      if (is < halfKernel) {
        i_lhs = is;
      }
      if (is + halfKernel >= yloc.arraySize()) {
        i_rhs = yloc.arraySize() - is - 1;
      }
      if (js < halfKernel) {
        j_lhs = js;
      }
      if (js + halfKernel >= xloc.arraySize()) {
        j_rhs = xloc.arraySize() - js - 1;
      }
      if (ks < zHalfKernel) {
        k_lhs = ks;
      }
      if (ks + zHalfKernel >= zloc.arraySize()) {
        k_rhs = zloc.arraySize() - ks - 1;
      }

      int istart = is - i_lhs;
      int jstart = js - j_lhs;
      int kstart = ks - k_lhs;

      // LHS + RHS + middle

      int idim = i_lhs + i_rhs + 1;
      int jdim = j_lhs + j_rhs + 1;
      int kdim = k_lhs + k_rhs + 1;

      // Splines cannot be used with only 2 points. If we are using a
      // kernel size of 3 and it is reduced due to edge effects, then
      // slide the window.

      if (kdim == 2) {
        if (kstart != 0) {
          kstart -= (k_rhs + 1);
        }
        kdim = zKernelSize;
      }

      int blocksize = idim * jdim * kdim;
      // int semiblock = (idim * (jdim + 1) * kdim) / 3;

      int[] origin = new int[] {ts, kstart, istart, jstart};
      int[] shape = new int[] {1, kdim, idim, jdim};

      try {
        uArr = uVar.read(origin, shape);
        uArr = uArr.reduce();
      } catch (InvalidRangeException e) {
        // Should not occur. Checking done above.
        e.printStackTrace();
      }

      try {
        vArr = vVar.read(origin, shape);
        vArr = vArr.reduce();
      } catch (InvalidRangeException e) {
        // Should not occur. Checking done above.
        e.printStackTrace();
      }

      if (zloc.isIn_Bounds() >= 0) {

        try {
          wArr = wVar.read(origin, shape);
          wArr = wArr.reduce();

        } catch (InvalidRangeException e) {
          // Should not occur. Checking done above.
          e.printStackTrace();
        }
      }

      float[][][] autmp = (float[][][]) uArr.copyToNDJavaArray();
      float[][][] avtmp = (float[][][]) vArr.copyToNDJavaArray();
      float[][][] awtmp = (float[][][]) wArr.copyToNDJavaArray();

      // Mitigate NODATA values

      int uct = 0;
      double usum = 0;
      double uavg = 0;
      double ussq = 0;
      double uvar = 0;

      int vct = 0;
      double vsum = 0;
      double vavg = 0;
      double vssq = 0;
      double vvar = 0;

      int wct = 0;
      double wsum = 0;
      double wavg = 0;
      double wssq = 0;
      double wvar = 0;

      for (int i = 0; i < idim; i++) {
        for (int j = 0; j < jdim; j++) {
          for (int k = 0; k < kdim; k++) {

            if (autmp[k][i][j] < cutoff && !Double.isNaN(autmp[k][i][j])) {
              uct++;
              usum += autmp[k][i][j];
              uavg = usum / uct;
              ussq += autmp[k][i][j] * autmp[k][i][j];
              uvar = ussq / uct - uavg * uavg;
              // } else {
              // if ((k > 0) && Double.isNaN(autmp[k - 1][i][j]))
              // {
              // autmp[k][i][j] = 0;
              // }
            }
          }
        }
      }

      for (int i = 0; i < idim; i++) {
        for (int j = 0; j < jdim; j++) {
          for (int k = 0; k < kdim; k++) {
            if (avtmp[k][i][j] < cutoff && !Double.isNaN(avtmp[k][i][j])) {
              vct++;
              vsum += avtmp[k][i][j];
              vavg = vsum / vct;
              vssq += avtmp[k][i][j] * avtmp[k][i][j];
              vvar = vssq / vct - vavg * vavg;
              // } else {
              // if ((k > 0)
              // && Double.isNaN(avtmp[k - 1][i][j])) {
              // avtmp[k][i][j] = 0;
              // }
            }
          }
        }
      }

      if (zloc.isIn_Bounds() >= 0) {

        for (int i = 0; i < idim; i++) {
          for (int j = 0; j < jdim; j++) {
            for (int k = 0; k < kdim; k++) {

              if (awtmp[k][i][j] < cutoff && !Double.isNaN(awtmp[k][i][j])) {
                wct++;
                wsum += awtmp[k][i][j];
                wavg = wsum / wct;
                wssq += awtmp[k][i][j] * awtmp[k][i][j];
                wvar = wssq / wct - wavg * wavg;
                // } else {
                // if ((k > 0)
                // && Double.isNaN(awtmp[k - 1][i][j])) {
                // awtmp[k][i][j] = 0;
                // }
              }
            }
          }
        }
      }

      nearNoData = false;

      // If there are null values...

      if (uct < blocksize) {

        nearNoData = true;

        // If there are more than (io-1)^2, return null

        /*
         * if (uct < semiblock) { velocities = NODATA; averages =
         * NODATA; variances = NODATA; return NODATA; }
         */

        // Otherwise mitigate by replacing using the average value.

        for (int i = 0; i < idim; i++) {
          for (int j = 0; j < jdim; j++) {
            for (int k = 0; k < kdim; k++) {

              if (autmp[k][i][j] > cutoff || Double.isNaN(autmp[k][i][j])) {

                if (ks == 0) {
                  autmp[k][i][j] = 0;
                }

                autmp[k][i][j] = (float) uavg;
              }
            }
          }
        }
      }

      if (vct < blocksize) {

        nearNoData = true;

        // If there are more than halfKernel/2, return null

        /*
         * if (vct < semiblock) { this.notifyAll(); velocities = NODATA;
         * averages = NODATA; variances = NODATA; return NODATA; }
         */

        // Otherwise mitigate by replacing using the average value.

        for (int i = 0; i < idim; i++) {
          for (int j = 0; j < jdim; j++) {
            for (int k = 0; k < kdim; k++) {

              if (avtmp[k][i][j] > cutoff || Double.isNaN(avtmp[k][i][j])) {

                avtmp[k][i][j] = (float) vavg;
              }
            }
          }
        }
      }
      if (zloc.isIn_Bounds() >= 0) {
        if (wct < blocksize) {

          nearNoData = true;

          // If there are more than (halfKernel-1)^2, return null

          /*
           * if (wct < semiblock) { this.notifyAll(); velocities =
           * NODATA; averages = NODATA; variances = NODATA; return
           * NODATA; }
           */

          // Otherwise mitigate by replacing using the average value.

          for (int i = 0; i < idim; i++) {
            for (int j = 0; j < jdim; j++) {
              for (int k = 0; k < kdim; k++) {

                if (awtmp[k][i][j] > cutoff || Double.isNaN(awtmp[k][i][j])) {

                  awtmp[k][i][j] = (float) wavg;
                }
              }
            }
          }
        }
      }

      /*
       * Convert the Arrays into Java arrays - because latitude and z
       * should be consistent among files, we subset from a constant
       * array.
       */

      double[] latja = Arrays.copyOfRange(yloc.getJavaArray(), istart, istart + idim);
      double[] lonja = Arrays.copyOfRange(xloc.getJavaArray(), jstart, jstart + jdim);
      double[] zja = Arrays.copyOfRange(zloc.getJavaArray(), kstart, kstart + kdim);

      // Obtain the interpolated values

      TricubicSplineInterpolator tci = new TricubicSplineInterpolator();
      TricubicSplineInterpolatingFunction tsf = tci.interpolate(zja, latja, lonja, autmp);

      // u = tcs.interpolate(z, lat, lon);
      u = tsf.value(z, lat, lon);
      // tcs.setValues(avtmp);
      // v = tcs.interpolate(z, lat, lon);
      tsf = tci.interpolate(zja, latja, lonja, avtmp);
      v = tsf.value(z, lat, lon);

      if (zloc.isIn_Bounds() >= 0) {
        // tcs.setValues(awtmp);
        // w = tcs.interpolate(z, lat, lon);
        tsf = tci.interpolate(zja, latja, lonja, awtmp);
        w = tsf.value(z, lat, lon);
      } else {
        w = 0;
      }

      // If there is something strange with the values, return NODATA.

      if (Math.abs(u) > cutoff) {
        u = 0.0f;
        v = 0.0f;
        w = 0.0f;
        this.notifyAll();
        velocities = NODATA;
        averages = NODATA;
        variances = NODATA;
        return NODATA;
      } else if (Math.abs(v) > cutoff) {
        u = 0.0f;
        v = 0.0f;
        w = 0.0f;
        this.notifyAll();
        velocities = NODATA;
        averages = NODATA;
        variances = NODATA;
        return NODATA;
      } else if (Math.abs(w) > cutoff) {
        u = 0.0f;
        v = 0.0f;
        w = 0.0f;
        this.notifyAll();
        velocities = NODATA;
        averages = NODATA;
        variances = NODATA;
        return NODATA;
      }

      if (Double.isNaN(u) || Double.isNaN(v) || Double.isNaN(w)) {
        u = 0.0f;
        v = 0.0f;
        w = 0.0f;
        this.notifyAll();
        velocities = NODATA;
        averages = NODATA;
        variances = NODATA;
        return NODATA;
      }

      // Otherwise return the interpolated values.

      this.notifyAll();
      velocities = new double[] {u, v, w};
      averages = new double[] {uavg, vavg, wavg};

      // Correct running population variance to be sample variance.
      double ucorr = (double) uct / (double) (uct - 1);
      double vcorr = (double) vct / (double) (vct - 1);
      double wcorr = (double) wct / (double) (wct - 1);
      variances = new double[] {uvar * ucorr, vvar * vcorr, wvar * wcorr};

      return velocities;

      // If for some reason there was an error reading from the file,
      // return null.

    } catch (IOException e) {
      System.out.println("WARNING:  Error reading from velocity files.\n\n");
      e.printStackTrace();
    }
    this.notifyAll();
    return null;
  }
  public void initialize(String dir) throws IOException {
    // Set Time Zone to UTC
    formatUTC.setTimeZone(TimeZone.getTimeZone("UTC"));

    this.dir = dir;
    File f = new File(dir);

    // Ensure the path is a directory
    if (!f.isDirectory()) {
      throw new IllegalArgumentException(f.getName() + " is not a directory.");
    }

    // Filter the list of files

    File[] fa = f.listFiles(new FilenamePatternFilter(".*_[uvw].*\\.nc"));

    if (fa == null) {
      throw new IOException("File list is empty.");
    }

    for (File fil : fa) {

      String name = fil.getName();

      NetcdfFile ncf = NetcdfFile.open(fil.getPath());
      tVar = ncf.findVariable(tName);

      if (tVar == null) {
        System.out.println(
            "WARNING: Time variable "
                + tVar
                + " was not found in "
                + fil.getPath()
                + " when initializing the VelocityReader.  This file will be skipped.");
        continue;
      }

      Array arr = tVar.read();

      // Convert into a java array

      double[] ja = (double[]) arr.copyTo1DJavaArray();

      // Determine the minimum and maximum times

      long[] minmax = new long[2];

      minmax[0] = TimeConvert.HYCOMToMillis((long) ja[0]);
      minmax[1] = TimeConvert.HYCOMToMillis((long) ja[ja.length - 1]);

      // Put into an index linking start time with the associated file

      if (name.lastIndexOf("_u") > 0) {
        if (uFiles.containsKey(minmax[0])) {
          System.out.print(
              "WARNING:  Velocity files have duplicate time keys. "
                  + name
                  + "/"
                  + uFiles.get(minmax[0])
                  + " at "
                  + new Date(minmax[0]));
          System.out.println(" Skipping latter file.");
        } else {
          uFiles.put(minmax[0], ncf);
        }
      }

      if (name.lastIndexOf("_v") > 0) {
        if (vFiles.containsKey(minmax[0])) {
          System.out.print(
              "WARNING:  Velocity files have duplicate time keys. "
                  + name
                  + "/"
                  + vFiles.get(minmax[0])
                  + " at "
                  + new Date(minmax[0]));
          System.out.println(" Skipping latter file.");
        } else {
          vFiles.put(minmax[0], ncf);
        }
      }

      if (name.lastIndexOf("_w") > 0) {
        if (wFiles.containsKey(minmax[0])) {
          System.out.print(
              "WARNING:  Velocity files have duplicate time keys. "
                  + name
                  + "/"
                  + wFiles.get(minmax[0])
                  + " at "
                  + new Date(minmax[0]));
          System.out.println(" Skipping latter file.");
        } else {
          wFiles.put(minmax[0], ncf);
        }
      }
    }

    // If there are no files in one of the index collections, then exit.

    if (uFiles.size() == 0 || vFiles.size() == 0 || wFiles.size() == 0) {
      System.out.println(
          "Velocity directory is empty, or files/variables are not named properly."
              + "Files  must be named as *_u*, *_v*, and *_w*.");

      System.exit(0);
    }

    uKeys = new ArrayList<Long>(uFiles.keySet());
    vKeys = new ArrayList<Long>(vFiles.keySet());
    wKeys = new ArrayList<Long>(wFiles.keySet());

    // Populate uFile, vFile and wFile with the first entry so that they
    // are not null

    uFile = uFiles.get(uKeys.get(0));
    vFile = vFiles.get(vKeys.get(0));
    wFile = wFiles.get(wKeys.get(0));

    uVar = uFile.findVariable(uName);
    vVar = vFile.findVariable(vName);
    wVar = wFile.findVariable(wName);

    // Latitude and depth are read here because they should not change
    // and therefore can be input once only.

    latVar = uFile.findVariable(latName);
    lonVar = uFile.findVariable(lonName);
    zVar = uFile.findVariable(zName);

    setXLookup(lonName);
    setYLookup(latName);
    setZLookup(zName);

    if (positiveDown) {
      zloc.setNegate(true);
    }
    bounds[0][0] = uKeys.get(0);
    NetcdfFile lastfile = uFiles.get(uKeys.get(uKeys.size() - 1));
    Variable t = lastfile.findVariable(tName);
    double last;
    try {
      last = t.read(new int[] {t.getShape(0) - 1}, new int[] {1}).getDouble(0);
      bounds[0][1] = TimeConvert.HYCOMToMillis((long) last);
    } catch (InvalidRangeException e) {
      e.printStackTrace();
    }
  }