package thredds.server.wfs;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

/**
 * Writes the XML for a WFS DescribeFeatureType request
 *
 * @author Stanley Kaymen
 *
 */
public class WFSDescribeFeatureTypeWriter {

    private PrintWriter response;
    private String fileOutput;
    private final String server;
    private final String namespace;
    private List<WFSFeature> featureList;

    public WFSDescribeFeatureTypeWriter(PrintWriter response, String server, String namespace) {
        this.response = response;
        this.fileOutput = "";
        this.server = server;
        this.namespace = namespace;
        this.featureList = new ArrayList<WFSFeature>();
    }

    /**
     * Initiate the response with an XML file with an XML header
     */
    public void startXML() {
        fileOutput += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";

        fileOutput += "<schema " + "xmlns:" + WFSController.TDSNAMESPACE + "="  + WFSXMLHelper.encQuotes(namespace) + " " +
                "xmlns:ogc=\"http://www.opengis.net/ogc\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" " +
                "xmlns=\"http://www.w3.org/2001/XMLSchema\" xmlns:gml=\"http://www.opengis.net/gml\" " +
                "targetNamespace=\"" + server + "\" elementFormDefault=\"qualified\" " +
                "version=\"0.1\">";
        fileOutput += "<xsd:import namespace=\"http://www.opengis.net/gml\" " +
                "schemaLocation=\"http://schemas.opengis.net/gml/2.1.2/feature.xsd\"/>";
    }

    /**
     * Add a feature to the writer's feature list.
     *
     * @param feature to add
     */

    public void addFeature(WFSFeature feature) {

        featureList.add(feature);
    }

    /**
     * Write the features from the featureList. For each feature, write its attributes
     */
    public void writeFeatures() {

        for (WFSFeature feat : featureList) {
            fileOutput += "<xsd:complexType name=\"" + feat.getTitle() + "\">";
            fileOutput += "<xsd:complexContent>";
            fileOutput += "<xsd:extension base=\"gml:" + feat.getType() + "\">";
            fileOutput += "<xsd:sequence>";

            for (WFSFeatureAttribute attribute : feat.getAttributes()) {
                fileOutput += "<xsd:element name =\"" + attribute.getName() + "\" type=\"" + attribute.getType() + "\"/>";
            }

            fileOutput += "</xsd:sequence>";
            fileOutput += "</xsd:extension>";
            fileOutput += "</xsd:complexContent>";
            fileOutput += "</xsd:complexType>";
            fileOutput += "<xsd:element name =\"" + feat.getName() + "\" type=\"tds:" + feat.getTitle() + "\"/>";

        }
    }

    /**
     * Finish writing the XML file and append it all to the PrintWriter.
     *
     * Once a XML is finished, the WFSDataWriter is no longer usable.
     */
    public void finishXML() {
        fileOutput += "</schema>";
        this.response.append(fileOutput);
        response = null;
        fileOutput = null;
    }
}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.iosp.grid;

import ucar.ma2.*;

import ucar.nc2.*;
import ucar.nc2.constants.AxisType;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.units.SimpleUnit;

import java.util.*;

/**
 * A Coordinate System for a Grid variable.
 *
 * @author john
 */
public class GridCoordSys {

  /**
   * horizontal coordinate system
   */
  private GridHorizCoordSys hcs;

  /**
   * a grid record
   */
  private GridRecord record;

  /**
   * vertical name
   */
  private String verticalName;

  /**
   * lookup table
   */
  private GridTableLookup lookup;

  /**
   * list of levels
   */
  private List<Double> levels;

  /**
   * flag for not using the vertical
   */
  boolean dontUseVertical = false;

  /**
   * postive direction
   */
  String positive = "up";

  /**
   * units
   */
  String units;

  /**
   * Create a coordinate system
   *
   * @param hcs    horizontal coord sys
   * @param record grid record
   * @param name   name of the vertical dimension
   * @param lookup lookuptable
   */
  GridCoordSys(GridHorizCoordSys hcs, GridRecord record, String name,
               GridTableLookup lookup) {
    this.hcs = hcs;
    this.record = record;
    this.verticalName = name;
    this.lookup = lookup;
    this.levels = new ArrayList<Double>();

    dontUseVertical = !lookup.isVerticalCoordinate(record);
    positive = lookup.isPositiveUp(record)
            ? "up"
            : "down";
    units = lookup.getLevelUnit(record);

    if (GridServiceProvider.debugVert) {
      System.out.println("GridCoordSys: " + getVerticalDesc()
              + " useVertical= " + (!dontUseVertical)
              + " positive=" + positive + " units=" + units);
    }
  }

  /**
   * Get the coordinate system name
   *
   * @return the coordinate system name
   */
  String getCoordSysName() {
    return verticalName + "_CoordSys";
  }

  /**
   * Get the vertical name
   *
   * @return the vertical name
   */
  String getVerticalName() {
    return verticalName;
  }

  /**
   * Get the vertical description
   *
   * @return the vertical description
   */
  String getVerticalDesc() {
    return verticalName + "(" + record.getLevelType1() + ")";
  }

  /**
   * Get the number of levels
   *
   * @return the number of levels
   */
  int getNLevels() {
    return dontUseVertical
            ? 1
            : levels.size();
  }

  /**
   * Add levels from the GridRecords
   *
   * @param records the records
   */
  void addLevels(List<GridRecord> records) {
    for (GridRecord record : records) {
      Double d = new Double(record.getLevel1());
      if (!levels.contains(d)) {
        levels.add(d);
      }
      if (dontUseVertical && (levels.size() > 1)) {
        if (GridServiceProvider.debugVert) {
          System.out.println(
                  "GribCoordSys: unused level coordinate has > 1 levels = "
                          + verticalName + " " + record.getLevelType1() + " "
                          + levels.size());
        }
      }
    }
    Collections.sort(levels);
    if (positive.equals("down")) {
      Collections.reverse(levels);
      // TODO: delete
      /* for( int i = 0; i < (levels.size()/2); i++ ){
        Double tmp = (Double) levels.get( i );
        levels.set( i, levels.get(levels.size() -i -1));
        levels.set(levels.size() -i -1, tmp );
     } */
    }
  }

  /**
   * Match levels
   *
   * @param records list of GridRecords
   * @return true if they match
   */
  boolean matchLevels(List<GridRecord> records) {

    // first create a new list
    List<Double> levelList = new ArrayList<Double>(records.size());
    for (GridRecord record : records) {
      Double d = new Double(record.getLevel1());
      if (!levelList.contains(d)) {
        levelList.add(d);
      }
    }

    Collections.sort(levelList);
    if (positive.equals("down")) {
      Collections.reverse(levelList);
    }

    // gotta equal existing list
    return levelList.equals(levels);
  }

  /**
   * Add dimensions to the netcdf file
   *
   * @param ncfile the netCDF file
   * @param g      the group to add to
   */
  void addDimensionsToNetcdfFile(NetcdfFile ncfile, Group g) {
    if (dontUseVertical) {
      return;
    }
    int nlevs = levels.size();
    ncfile.addDimension(g, new Dimension(verticalName, nlevs, true));
  }

  /**
   * Add this coordinate system to the netCDF file
   *
   * @param ncfile the netCDF file
   * @param g      the group to add to
   */
  void addToNetcdfFile(NetcdfFile ncfile, Group g) {
    if (dontUseVertical) {
      return;
    }

    if (g == null) {
      g = ncfile.getRootGroup();
    }

    String dims = "time";
    if (!dontUseVertical) {
      dims = dims + " " + verticalName;
    }
    if (hcs.isLatLon()) {
      dims = dims + " lat lon";
    } else {
      dims = dims + " y x";
    }

    //Collections.sort( levels);
    int nlevs = levels.size();
    // ncfile.addDimension(g, new Dimension(verticalName, nlevs, true));

    // coordinate axis and coordinate system Variable
    Variable v = new Variable(ncfile, g, null, verticalName);
    v.setDataType(DataType.DOUBLE);

    v.addAttribute(new Attribute("long_name",
            lookup.getLevelDescription(record)));
    v.addAttribute(new Attribute("units", lookup.getLevelUnit(record)));

    // positive attribute needed for CF-1 Height and Pressure
    if (positive != null) {
      v.addAttribute(new Attribute("positive", positive));
    }

    if (units != null) {
      AxisType axisType;
      if (SimpleUnit.isCompatible("millibar", units)) {
        axisType = AxisType.Pressure;
      } else if (SimpleUnit.isCompatible("m", units)) {
        axisType = AxisType.Height;
      } else {
        axisType = AxisType.GeoZ;
      }

      v.addAttribute(
              new Attribute(
                      "grid_level_type",
                      Integer.toString(record.getLevelType1())));

      v.addAttribute(new Attribute(_Coordinate.AxisType,
              axisType.toString()));
      v.addAttribute(new Attribute(_Coordinate.Axes, dims));
      if (!hcs.isLatLon()) {
        v.addAttribute(new Attribute(_Coordinate.Transforms,
                hcs.getGridName()));
      }
    }

    double[] data = new double[nlevs];
    for (int i = 0; i < levels.size(); i++) {
      Double d = (Double) levels.get(i);
      data[i] = d.doubleValue();
    }
    Array dataArray = Array.factory(DataType.DOUBLE, new int[]{nlevs}, data);

    v.setDimensions(verticalName);
    v.setCachedData(dataArray, false);

    ncfile.addVariable(g, v);

    // look for vertical transforms
    if (record.getLevelType1() == 109) {
      findCoordinateTransform(g, "Pressure", record.getLevelType1());
    }
  }

  /**
   * Find the coordinate transform
   *
   * @param g              group to check
   * @param nameStartsWith beginning of name
   * @param levelType      type of level
   */
  void findCoordinateTransform(Group g, String nameStartsWith, int levelType) {
    // look for variable that uses this coordinate
    List<Variable> vars = g.getVariables();
    for (Variable v : vars) {
      if (v.getShortName().equals(nameStartsWith)) {
        Attribute att = v.findAttribute("grid_level_type");
        if ((att == null) || (att.getNumericValue().intValue() != levelType)) {
          continue;
        }

        v.addAttribute(new Attribute(_Coordinate.TransformType, "Vertical"));
        v.addAttribute(new Attribute("transform_name", "Existing3DField"));
      }
    }
  }

  /**
   * Get the index of a particular  GridRecord
   *
   * @param record record to find
   * @return the index or -1
   */
  int getIndex(GridRecord record) {
    Double d = new Double(record.getLevel1());
    return levels.indexOf(d);
  }
}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2.util;

import ucar.nc2.time.CalendarDate;
import ucar.unidata.util.StringUtil2;

import java.io.*;
import java.net.URLDecoder;
import java.nio.file.*;
import java.util.*;

/**
 * Manages a place on disk to persistently cache files, which are deleted when the last modified date exceeds a certain time.
 * This starts up a thread to periodically scour itself; be sure to call exit() to terminate the thread.
 *
 *
 Each DiskCache has a "root directory", which may be set as an absolute path, or reletive to the
 * DiskCache "home directory". The root directory must be writeable.
 *
 * The DiskCache home directory is set in the following order:

 *
  through the system property "nj22.cache" if it exists
 *
  through the system property "user.home" if it exists
 *
  through the system property "user.dir" if it exists
 *
  to the current working directory
 *

 * @author jcaron
 */
public class DiskCache2 {
  private static org.slf4j.Logger cacheLog = org.slf4j.LoggerFactory.getLogger("cacheLogger");
  static private Timer timer;

  /** Be sure to call this when your application exits, otherwise your process may not exit without being killed. */
  static public void exit() {
    if (timer != null) {
      timer.cancel();
      System.out.printf("DiskCache2.exit()%n");
    }
    timer = null;
  }

  static private synchronized void startTimer() {
    if (timer == null)
      timer = new Timer("DiskCache2");
  }

  /////////////////////////////////////////////////////////////

  public enum CachePathPolicy {
    OneDirectory,
    NestedDirectory,
    NestedTruncate
  }

  private CachePathPolicy cachePathPolicy = CachePathPolicy.NestedDirectory;
  private boolean alwaysUseCache = false;
  private boolean neverUseCache = false;
  private String cachePathPolicyParam = null;

  private String root;
  private int persistMinutes, scourEveryMinutes;
  private boolean fail = false;

  /**
   * Default DiskCache2 strategy: use $user_home/.unidata/cache/, no scouring, alwaysUseCache = false
   * Mimics default DiskCache static class
   */
  static public DiskCache2 getDefault() {
    String root = System.getProperty("nj22.cache");

    if (root == null) {
      String home = System.getProperty("user.home");

      if (home == null)
        home = System.getProperty("user.dir");

      if (home == null)
        home = ".";

      root = home + "/.unidata/cache/";
    }

    DiskCache2 result = new DiskCache2();
    result.setRootDirectory(root);
    result.alwaysUseCache = false;
    return result;
  }

  // NOOP
  static public DiskCache2 getNoop() {
    DiskCache2 noop = new DiskCache2();
    noop.neverUseCache = true;
    return noop;
  }

  private DiskCache2() {}

  /**
   * Create a cache on disk. Use default policy (CachePathPolicy.NestedDirectory)
   * @param root the root directory of the cache. Must be writeable.
   * @param reletiveToHome if the root directory is relative to the cache home directory.
   * @param persistMinutes  a file is deleted if its last modified time is greater than persistMinutes
   * @param scourEveryMinutes how often to run the scour process. If <= 0, don't scour.
   */
  public DiskCache2(String root, boolean reletiveToHome, int persistMinutes, int scourEveryMinutes) {
    this.persistMinutes = persistMinutes;
    this.scourEveryMinutes = scourEveryMinutes;

    if (reletiveToHome) {
      String home = System.getProperty("nj22.cachePersistRoot");

      if (home == null)
        home = System.getProperty("user.home");

      if (home == null)
        home = System.getProperty("user.dir");

      if (home == null)
        home = ".";

      if (!home.endsWith("/"))
        home = home + "/";

      root = home + root;
    }
    setRootDirectory(root);

    if (!fail && scourEveryMinutes > 0) {
      startTimer();
      Calendar c = Calendar.getInstance(); // contains current startup time
      c.add(Calendar.MINUTE, scourEveryMinutes);
      timer.scheduleAtFixedRate(new CacheScourTask(), c.getTime(), (long) 1000 * 60 * scourEveryMinutes);
      cacheLog.info("Started a DiskCache2 scour task on {} every {} minutes for files older than {} minutes", root, scourEveryMinutes, persistMinutes);
    }

  }

  /*
   * Set the cache root directory. Create it if it doesnt exist.
   * Normally this is set in the Constructor.
   * @param cacheDir the cache directory
   */
  public void setRootDirectory(String cacheDir) {
    if (!cacheDir.endsWith("/"))
      cacheDir = cacheDir + "/";
    root = StringUtil2.replace(cacheDir, '\\', "/"); // no nasty backslash

    File dir = new File(root);
    if (!dir.mkdirs()) {
      // ok
    }
    if (!dir.exists()) {
      fail = true;
      cacheLog.error("DiskCache2 failed to create directory "+root);
    } else {
      cacheLog.debug("DiskCache2 create directory "+root);
    }
  }

  /**
   * Get the cache root directory.
   * @return the cache root directory.
   */
   public String getRootDirectory() { return root; }

  /**
   * Get a File in the cache, corresponding to the fileLocation.
   * File may or may not exist.
   * If fileLocation has "/" in it, and cachePathPolicy == NestedDirectory, the
   * nested directories will be created.
   *
   * @param fileLocation logical file location
   * @return physical File in the cache.
   */
  public File getCacheFile(String fileLocation) {
    if (neverUseCache)
      return null;

    if (!alwaysUseCache) {
      File f = new File(fileLocation);
      if (canWrite(f)) return f;
    }

    File f = new File(makeCachePath(fileLocation));
    //if (f.exists())
    // f.setLastModified( System.currentTimeMillis());

    if (cachePathPolicy == CachePathPolicy.NestedDirectory) {
      File dir = f.getParentFile();
      if (!dir.exists()) {
        boolean ret = dir.mkdirs();
        if (!ret) cacheLog.warn("Error creating dir: " + dir);
      }
    }

    return f;
  }

  /**
   * Get the named File.
   * If exists or isWritable, return it.
   * Otherwise get corresponding file in the cache directory.
   *
   * If fileLocation has "/" in it, and cachePathPolicy == NestedDirectory, the
   * nested directories will be created.
   *
   * @param fileLocation logical file location
   * @return physical File as named, or in the cache.
   */
  public File getFile(String fileLocation) {
    if (!alwaysUseCache) {
      File f = new File(fileLocation);
      if (f.exists()) return f;
      if (canWrite(f)) return f;
    }

    if (neverUseCache) {
      throw new IllegalStateException("neverUseCache=true, but file does not exist and directory is not writeable ="+fileLocation);
    }

    File f = new File(makeCachePath(fileLocation));
    if (cachePathPolicy == CachePathPolicy.NestedDirectory) {
      File dir = f.getParentFile();
      if (!dir.exists() && !dir.mkdirs())
        cacheLog.warn("Cant create directories for file "+dir.getPath());
    }

    return f;
  }

  /**
   * Returns {@code true} if we can write to the file.
   *
   * @param f a file. It may be a regular file or a directory. It may even be non-existent, in which case the
   *          writability of the file's parent dir is tested.
   * @return {@code true} if we can write to the file.
   */
  public static boolean canWrite(File f) {
    Path path = f.toPath().toAbsolutePath();

    try {
      if (Files.isDirectory(path)) {
        // Try to create a file within the directory to determine if it's writable.
        Files.delete(Files.createTempFile(path, "check", null));
      } else if (Files.isRegularFile(path)) {
        // Try to open the file for writing in append mode.
        Files.newOutputStream(path, StandardOpenOption.APPEND).close();
      } else {
        // File does not exist. See if it's parent directory exists and is writeable.
        Files.delete(Files.createTempFile(path.getParent(), "check", null));
      }
    } catch (IOException | SecurityException e) {
      return false;
    }

    return true;
  }

  /**
   * Looking for an existing file, in cache or no
   * @param fileLocation the original name
   * @return existing file if you can find it
   */
  public File getExistingFileOrCache(String fileLocation) {
    File f = new File(fileLocation);
    if (f.exists()) return f;

    if (neverUseCache) return null;

    File fc = new File(makeCachePath(fileLocation));
    if (fc.exists()) return fc;

    return null;
  }

  /**
   * Create a new, uniquely named file in the root directory.
   * Mimics File.createTempFile()
   *
   * @param prefix The prefix string to be used in generating the file's
   *               name; must be at least three characters long
   * @param suffix The suffix string to be used in generating the file's
   *               name; may be null, in which case the
   *               suffix ".tmp" will be used
   * @return uniquely named file
   */
  public synchronized File createUniqueFile(String prefix, String suffix) {
    if (suffix == null) suffix = ".tmp";
    Random random = new Random(System.currentTimeMillis());
    File result = new File(getRootDirectory(), prefix + Integer.toString(random.nextInt()) + suffix);
    while (result.exists())
      result = new File(getRootDirectory(), prefix + Integer.toString(random.nextInt()) + suffix);
    return result;
  }

  /**
   * Set the cache path policy
   * @param cachePathPolicy one of:
   *   OneDirectory (default) : replace "/" with "-", so all files are in one directory.
   *   NestedDirectory: cache files are in nested directories under the root.
   *   NestedTruncate: eliminate leading directories
   */
  public void setPolicy(CachePathPolicy cachePathPolicy) {
    this.cachePathPolicy = cachePathPolicy;
  }

  /**
   * Set the cache path policy
   * @param cachePathPolicy one of:
   *   OneDirectory (default) : replace "/" with "-", so all files are in one directory.
   *   NestedDirectory: cache files are in nested directories under the root.
   *   NestedTruncate: eliminate leading directories
   *
   * @param cachePathPolicyParam for NestedTruncate, eliminate this string
   */
  public void setCachePathPolicy(CachePathPolicy cachePathPolicy, String cachePathPolicyParam) {
    this.cachePathPolicy = cachePathPolicy;
    this.cachePathPolicyParam = cachePathPolicyParam;
  }

  public void setPolicy(String policy) {
    if (policy == null) return;
    if (policy.equalsIgnoreCase("oneDirectory")) setCachePathPolicy(CachePathPolicy.OneDirectory, null);
    else if (policy.equalsIgnoreCase("nestedDirectory")) setCachePathPolicy(CachePathPolicy.NestedDirectory, null);
  }

  /**
   * If true, always put the file in the cache. default false.
   * @param alwaysUseCache  If true, always put the file in the cache
   */
  public void setAlwaysUseCache(boolean alwaysUseCache) {
    this.alwaysUseCache = alwaysUseCache;
  }

  /**
   * If true, never put the file in the cache. default false.
   * @param neverUseCache  If true, never put the file in the cache
   */
  public void setNeverUseCache(boolean neverUseCache) {
    this.neverUseCache = neverUseCache;
  }

  /**
   * Make the cache filename
   * @param fileLocation normal file location
   * @return cache filename
   */
  private String makeCachePath(String fileLocation) {

    // remove ':', '?', '=', replace '\' with '/', leading or trailing '/'
    String cachePath = fileLocation;
    cachePath = StringUtil2.remove(cachePath, '?');
    cachePath = StringUtil2.remove(cachePath, '=');
    cachePath = StringUtil2.replace(cachePath, '\\', "/");
    if (cachePath.startsWith("/"))
      cachePath = cachePath.substring(1);
    if (cachePath.endsWith("/"))
      cachePath = cachePath.substring(0, cachePath.length()-1);
    cachePath = StringUtil2.remove(cachePath, ':');

    // remove directories
    if (cachePathPolicy == CachePathPolicy.OneDirectory) {
      cachePath = StringUtil2.replace(cachePath, '/', "-");
    }

    // eliminate leading directories
    else if (cachePathPolicy == CachePathPolicy.NestedTruncate) {
      int pos = cachePath.indexOf(cachePathPolicyParam);
      if (pos >= 0)
        cachePath = cachePath.substring(pos+cachePathPolicyParam.length());
      if (cachePath.startsWith("/"))
        cachePath = cachePath.substring(1);
    }

    // make sure the parent directory exists
    if (cachePathPolicy != CachePathPolicy.OneDirectory) {
      File file = new File(root + cachePath);
      File parent = file.getParentFile();
      if (!parent.exists()) {
        if (root == null) { // LOOK shouldnt happen, remove soon
          System.out.printf("mkdir4 %s%n", parent.getPath());
          new Throwable().printStackTrace();
        }
        boolean ret = parent.mkdirs();
        if (!ret) cacheLog.warn("Error creating parent: " + parent);
      }
    }

    return root + cachePath;
  }

  /**
   * Show cache contents, for debugging.
   * @param pw write to this PrintStream.
   */
  public void showCache(PrintStream pw) {
    pw.println("Cache files");
    pw.println("Size   LastModified       Filename");
    File dir = new File(root);
    File[] files = dir.listFiles();
    if (files != null)
      for (File file : files) {
        String org = null;
        try {
          org = URLDecoder.decode(file.getName(), "UTF8");
        } catch (UnsupportedEncodingException e) {
          e.printStackTrace();
        }

        pw.println(" " + file.length() + " " + new Date(file.lastModified()) + " " + org);
      }
  }

  /**
   * Remove any files or directories whose last modified time greater than persistMinutes
   * @param dir clean starting here
   * @param sbuff status messages here, may be null
   * @param isRoot delete empty directories, bit not root directory
   */
  public void cleanCache(File dir, Formatter sbuff, boolean isRoot) {
    long now = System.currentTimeMillis();
    File[] files = dir.listFiles();
    if (files == null) {
      throw new IllegalStateException( "DiskCache2: not a directory or I/O error on dir="+dir.getAbsolutePath());
    }

    // check for empty directory
    if (!isRoot && (files.length == 0)) {
      long duration = now - dir.lastModified();
      duration /= 1000 * 60; // minutes
      if (duration > persistMinutes) {
        boolean ok = dir.delete();
        if (!ok)
          cacheLog.error("Unable to delete file " + dir.getAbsolutePath());
        if (sbuff != null)
          sbuff.format(" deleted %s %s lastModified= %s%n", ok, dir.getPath(), CalendarDate.of(dir.lastModified()));
      }
      return;
    }

    // check for expired files
    for (File file : files) {
      if (file.isDirectory()) {
        cleanCache(file, sbuff, false);
      } else {
        long duration = now - file.lastModified();
        duration /= 1000 * 60; // minutes
        if (duration > persistMinutes) {
          boolean ok = file.delete();
          if (!ok)
            cacheLog.error("Unable to delete file " + file.getAbsolutePath());
          if (sbuff != null)
            sbuff.format(" deleted %s %s lastModified= %s%n", ok, file.getPath(), CalendarDate.of(file.lastModified()));
        }
      }
    }
  }

  private class CacheScourTask extends TimerTask {
    @Override
    public void run() {
      Formatter sbuff = new Formatter();
      sbuff.format("DiskCache2 scour on directory= %s%n", root);
      cleanCache(new File(root), sbuff, true);
      if (cacheLog.isDebugEnabled()) cacheLog.debug(sbuff.toString());
    }
  }

  @Override
  public String toString() {
    final StringBuilder sb = new StringBuilder();
    sb.append("DiskCache2");
    sb.append("{cachePathPolicy=").append(cachePathPolicy);
    sb.append(", alwaysUseCache=").append(alwaysUseCache);
    sb.append(", cachePathPolicyParam='").append(cachePathPolicyParam).append('\'');
    sb.append(", root='").append(root).append('\'');
    sb.append(", scourEveryMinutes=").append(scourEveryMinutes);
    sb.append(", persistMinutes=").append(persistMinutes);
    sb.append(", fail=").append(fail);
    sb.append('}');
    return sb.toString();
  }

}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 4.1 */
/* JavaCCOptions:KEEP_LINE_COL=null */
package ucar.units;

/**
 * This exception is thrown when parse errors are encountered.
 * You can explicitly create objects of this exception type by
 * calling the method generateParseException in the generated
 * parser.
 *
 * You can modify this class to customize your error reporting
 * mechanisms so long as you retain the public fields.
 */
public class ParseException extends Exception {

  /**
   * This constructor is used by the method "generateParseException"
   * in the generated parser.  Calling this constructor generates
   * a new object of this type with the fields "currentToken",
   * "expectedTokenSequences", and "tokenImage" set.  The boolean
   * flag "specialConstructor" is also set to true to indicate that
   * this constructor was used to create this object.
   * This constructor calls its super class with the empty string
   * to force the "toString" method of parent class "Throwable" to
   * print the error message in the form:
   *     ParseException: <result of getMessage>
   */
  public ParseException(Token currentTokenVal,
                        int[][] expectedTokenSequencesVal,
                        String[] tokenImageVal
                       )
  {
    super("");
    specialConstructor = true;
    currentToken = currentTokenVal;
    expectedTokenSequences = expectedTokenSequencesVal;
    tokenImage = tokenImageVal;
  }

  /**
   * The following constructors are for use by you for whatever
   * purpose you can think of.  Constructing the exception in this
   * manner makes the exception behave in the normal way - i.e., as
   * documented in the class "Throwable".  The fields "errorToken",
   * "expectedTokenSequences", and "tokenImage" do not contain
   * relevant information.  The JavaCC generated code does not use
   * these constructors.
   */

  public ParseException() {
    super();
    specialConstructor = false;
  }

  /** Constructor with message. */
  public ParseException(String message) {
    super(message);
    specialConstructor = false;
  }

  /**
   * This variable determines which constructor was used to create
   * this object and thereby affects the semantics of the
   * "getMessage" method (see below).
   */
  protected boolean specialConstructor;

  /**
   * This is the last token that has been consumed successfully.  If
   * this object has been created due to a parse error, the token
   * followng this token will (therefore) be the first error token.
   */
  public Token currentToken;

  /**
   * Each entry in this array is an array of integers.  Each array
   * of integers represents a sequence of tokens (by their ordinal
   * values) that is expected at this point of the parse.
   */
  public int[][] expectedTokenSequences;

  /**
   * This is a reference to the "tokenImage" array of the generated
   * parser within which the parse error occurred.  This array is
   * defined in the generated ...Constants interface.
   */
  public String[] tokenImage;

  /**
   * This method has the standard behavior when this object has been
   * created using the standard constructors.  Otherwise, it uses
   * "currentToken" and "expectedTokenSequences" to generate a parse
   * error message and returns it.  If this object has been created
   * due to a parse error, and you do not catch it (it gets thrown
   * from the parser), then this method is called during the printing
   * of the final stack trace, and hence the correct error message
   * gets displayed.
   */
  public String getMessage() {
    if (!specialConstructor) {
      return super.getMessage();
    }
    StringBuilder expected = new StringBuilder();
    int maxSize = 0;
    for (int[] expectedTokenSequence : expectedTokenSequences) {
      if (maxSize < expectedTokenSequence.length) {
        maxSize = expectedTokenSequence.length;
      }
      for (int anExpectedTokenSequence : expectedTokenSequence) {
        expected.append(tokenImage[anExpectedTokenSequence]).append(' ');
      }
      if (expectedTokenSequence[expectedTokenSequence.length - 1] != 0) {
        expected.append("...");
      }
      expected.append(eol).append("    ");
    }

    StringBuilder b = new StringBuilder("Encountered \"");
    Token tok = currentToken.next;
    for (int i = 0; i < maxSize; i++) {
      if (i != 0) b.append(" ");
      if (tok.kind == 0) {
        b.append(tokenImage[0]);
        break;
      }
      b.append(" ").append(tokenImage[tok.kind]).append(" \"");
      b.append(add_escapes(tok.image));
      b.append(" \"");
      tok = tok.next;
    }
    b.append("\" at line ").append(currentToken.next.beginLine).append(", column ").append(currentToken.next.beginColumn);
    b.append(".").append(eol);
    if (expectedTokenSequences.length == 1) {
      b.append("Was expecting:").append(eol).append("    ");
    } else {
      b.append("Was expecting one of:").append(eol).append("    ");
    }
    expected.append(expected.toString());
    return b.toString();
  }

  /**
   * The end of line string for this machine.
   */
  protected String eol = System.getProperty("line.separator", "\n");

  /**
   * Used to convert raw characters to their escaped version
   * when these raw version cannot be used as part of an ASCII
   * string literal.
   */
  protected String add_escapes(String str) {
      StringBuilder retval = new StringBuilder();
      char ch;
      for (int i = 0; i < str.length(); i++) {
        switch (str.charAt(i))
        {
           case 0 :
              continue;
           case '\b':
              retval.append("\\b");
              continue;
           case '\t':
              retval.append("\\t");
              continue;
           case '\n':
              retval.append("\\n");
              continue;
           case '\f':
              retval.append("\\f");
              continue;
           case '\r':
              retval.append("\\r");
              continue;
           case '\"':
              retval.append("\\\"");
              continue;
           case '\'':
              retval.append("\\\'");
              continue;
           case '\\':
              retval.append("\\\\");
              continue;
           default:
              if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
                 String s = "0000" + Integer.toString(ch, 16);
                 retval.append("\\u").append(s.substring(s.length() - 4, s.length()));
              } else {
                 retval.append(ch);
              }
        }
      }
      return retval.toString();
   }

}
/* JavaCC - OriginalChecksum=c12c8e2ab928ca7687b02f2580b4469a (do not edit this line) */


package ucar.nc2.ui.table;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import java.awt.*;

/**
 * A listener that sets the preferred widths of a {@link JTable}'s columns such that they're just big enough to display
 * all of their contents without truncation. Expected usage:
 *

 * JTable table = new JTable(...);
 * ColumnWidthsResizer listener = new ColumnWidthsResizer(table);
 * table.getModel().addTableModelListener(listener);  // Respond to row and data changes.
 * table.getColumnModel().addColumnModelListener(listener);  // Respond to column changes.
 *

 */
public class ColumnWidthsResizer implements TableModelListener, TableColumnModelListener {
    private final JTable table;
    private final int fullScanCutoff;

    /**
     * Creates a listener that resizes {@code table}'s column widths when its data and/or structure changes.
     *

     * If {@code table.getRowCount() <= }{@link #DEFAULT_FULL_SCAN_CUTOFF}, a full scan will be performed. That is,
     * every row will be examined to determine the optimal column widths. Otherwise, a partial scan will be
     * performed. That is, only the header and the first, middle, and last rows will be examined to determine the
     * approximate column widths.
     *
     * @param table  a table.
     */
    public ColumnWidthsResizer(JTable table) {
        this(table, DEFAULT_FULL_SCAN_CUTOFF);
    }

    /**
     * Creates a listener that resizes {@code table}'s column widths when its data and/or structure changes.
     *
     * @param table  a table.
     * @param fullScanCutoff  {@code true} if a full scan should be performed; {@code false} if a partial scan should
     *                        be done instead.
     */
    public ColumnWidthsResizer(JTable table, int fullScanCutoff) {
        this.table = table;
        this.fullScanCutoff = fullScanCutoff;
        ColumnWidthsResizer.resize(table);  // Perform initial resize.
    }

    //////////////////////////////////////////////// TableModelListener ////////////////////////////////////////////////

    @Override public void tableChanged(final TableModelEvent e) {
        if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
            return;  // Do not respond to changes in the number of columns here, only row and data changes.
        }

        // ColumnWidthsResizer requires that the internal Swing TableModelListeners that update the JTable view
        // run their updates BEFORE it does its thing. Unfortunately, the order in which listeners are notified is
        // undefined (see https://weblogs.java.net/blog/alexfromsun/archive/2011/06/15/swing-better-world-listeners).
        // As a work-around, we're going to place the resize operation at the end of the event queue. That way, it'll
        // be executed after all TableModelListeners have been notified.
        EventQueue.invokeLater(new Runnable() {
            @Override public void run() {
                // Do not cache the value of doFullScan; we need to reevaluate each time because the number of rows in
                // the table could have changed.
                boolean doFullScan = table.getRowCount() <= fullScanCutoff;

                if (e.getColumn() == TableModelEvent.ALL_COLUMNS) {
                    resize(table, doFullScan);  // Resize all columns.
                } else {
                    resize(table, e.getColumn(), doFullScan);  // Resize only the affected column.
                }
            }
        });
    }

    ///////////////////////////////////////////// TableColumnModelListener /////////////////////////////////////////////

    @Override public void columnAdded(TableColumnModelEvent e) {
        boolean doFullScan = table.getRowCount() <= fullScanCutoff;
        resize(table, e.getToIndex(), doFullScan);  // Only resize added column.
    }

    @Override public void columnRemoved(TableColumnModelEvent e) { }

    @Override public void columnMoved(TableColumnModelEvent e) { }

    @Override public void columnMarginChanged(ChangeEvent e) { }

    @Override public void columnSelectionChanged(ListSelectionEvent e) { }

    ////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////

    /**
     * The default maximum number of table rows for which a full scan will be performed. If a table has more rows
     * than this, only a partial scan will be done.
     */
    public static final int DEFAULT_FULL_SCAN_CUTOFF = 10000;

    public static void resize(JTable table) {
        resize(table, doFullScanDefault(table));
    }

    public static void resize(JTable table, boolean doFullScan) {
        for (int col = 0; col < table.getColumnCount(); ++col) {
            resize(table, col, doFullScan);
        }
    }

    public static void resize(JTable table, int colViewIndex) {
        resize(table, colViewIndex, doFullScanDefault(table));
    }

    public static void resize(JTable table, int colViewIndex, boolean doFullScan) {
        int maxWidth = 0;

        // Get header width.
        TableColumn column = table.getColumnModel().getColumn(colViewIndex);
        TableCellRenderer headerRenderer = column.getHeaderRenderer();

        if (headerRenderer == null) {
            headerRenderer = table.getTableHeader().getDefaultRenderer();
        }

        Object headerValue = column.getHeaderValue();
        Component headerRendererComp =
                headerRenderer.getTableCellRendererComponent(table, headerValue, false, false, 0, colViewIndex);

        maxWidth = Math.max(maxWidth, headerRendererComp.getPreferredSize().width);

        // Get cell widths.
        if (doFullScan) {
            for (int row = 0; row < table.getRowCount(); ++row) {
                maxWidth = Math.max(maxWidth, getCellWidth(table, row, colViewIndex));
            }
        } else {
            maxWidth = Math.max(maxWidth, getCellWidth(table, 0, colViewIndex));
            maxWidth = Math.max(maxWidth, getCellWidth(table, table.getRowCount() / 2, colViewIndex));
            maxWidth = Math.max(maxWidth, getCellWidth(table, table.getRowCount() - 1, colViewIndex));
        }

        // For some reason, the calculation above gives a value that is 1 pixel too small.
        // Maybe that's because of the cell divider line?
        ++maxWidth;

        column.setPreferredWidth(maxWidth);
    }

    public static int getCellWidth(JTable table, int rowViewIndex, int colViewIndex) {
        TableCellRenderer cellRenderer = table.getCellRenderer(rowViewIndex, colViewIndex);
        Object value = table.getValueAt(rowViewIndex, colViewIndex);

        Component cellRendererComp =
                cellRenderer.getTableCellRendererComponent(table, value, false, false, rowViewIndex, colViewIndex);
        return cellRendererComp.getPreferredSize().width;
    }

    private static boolean doFullScanDefault(JTable table) {
        return table.getRowCount() <= DEFAULT_FULL_SCAN_CUTOFF;
    }
}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.unidata.geoloc;

import java.lang.Math;

/**
 * Computes the distance, azimuth, and back azimuth between
 * two lat-lon positions on the Earth's surface. Reference ellipsoid is the WGS-84.
 *
 * You may use a default Earth (equator radius = 6378137.0 meters,
 * flattening =  1.0 / 298.257223563) or you may define your own using
 * a ucar.unidata.geoloc.Earth object.
 *
 * @author Unidata Development Team
 */
public class Bearing {

    /**
     * Default Earth.  Major radius and flattening;
     *
     */
    private static final Earth defaultEarth = new Earth(6378137.0, 0., 298.257223563);

    /**
     * Earth radius
     */
    private static double A;
    //private static double A = 6378137.0;  // in meters (for reference)

    /**
     * The Earth flattening value
     */
    private static double F;
    //private static double F = 1.0 / 298.257223563;  (for reference)

    /**
     * epsilon
     */
    private static final double EPS = 0.5E-13;

    /**
     * constant R
     */
    //private static final double R = 1.0 - F;  (for reference)
    private static double R;

    /**
     * conversion for degrees to radians
     */
    private static final double rad = Math.toRadians(1.0);

    /**
     * conversion for radians to degrees
     */
    private static final double deg = Math.toDegrees(1.0);

    /**
     * Calculate the bearing between the 2 points.
     * See calculateBearing below.
     *
     * @param e      Earth object (defines radius & flattening)
     * @param pt1    Point 1
     * @param pt2    Point 2
     * @param result Object to use if non-null
     * @return The bearing
     */
    public static Bearing calculateBearing(Earth e, LatLonPoint pt1,
                                           LatLonPoint pt2, Bearing result) {

        return calculateBearing(e, pt1.getLatitude(), pt1.getLongitude(),
                                pt2.getLatitude(), pt2.getLongitude(),
                                result);
    }

    /**
     * Calculate the bearing between the 2 points.
     * See calculateBearing below.  Uses default Earth object.
     *
     * @param pt1    Point 1
     * @param pt2    Point 2
     * @param result Object to use if non-null
     * @return The bearing
     */
    public static Bearing calculateBearing(LatLonPoint pt1, LatLonPoint pt2,
                                           Bearing result) {

        return calculateBearing(defaultEarth, pt1.getLatitude(),
                                pt1.getLongitude(), pt2.getLatitude(),
                                pt2.getLongitude(), result);
    }

    /** _more_          */
    private static int maxLoopCnt = 0;

    /**
     * Computes distance (in km), azimuth (degrees clockwise positive
     * from North, 0 to 360), and back azimuth (degrees clockwise positive
     * from North, 0 to 360), from latitude-longituide point pt1 to
     * latitude-longituide pt2. Uses default Earth object.
     *
     * @param lat1   Lat of point 1
     * @param lon1   Lon of point 1
     * @param lat2   Lat of point 2
     * @param lon2   Lon of point 2
     * @param result put result here, or null to allocate
     * @return a Bearing object with distance (in km), azimuth from
     *         pt1 to pt2 (degrees, 0 = north, clockwise positive)
     */
    public static Bearing calculateBearing(double lat1, double lon1,
                                           double lat2, double lon2,
                                           Bearing result) {

        return calculateBearing(defaultEarth, lat1, lon1, lat2, lon2, result);
    }

    /**
     * Computes distance (in km), azimuth (degrees clockwise positive
     * from North, 0 to 360), and back azimuth (degrees clockwise positive
     * from North, 0 to 360), from latitude-longituide point pt1 to
     * latitude-longituide pt2.

     * Algorithm from U.S. National Geodetic Survey, FORTRAN program "inverse,"
     * subroutine "INVER1," by L. PFEIFER and JOHN G. GERGEN.
     * See http://www.ngs.noaa.gov/TOOLS/Inv_Fwd/Inv_Fwd.html
     *
Original documentation:
     *
SOLUTION OF THE GEODETIC INVERSE PROBLEM AFTER T.VINCENTY
     *
MODIFIED RAINSFORD'S METHOD WITH HELMERT'S ELLIPTICAL TERMS
     *
EFFECTIVE IN ANY AZIMUTH AND AT ANY DISTANCE SHORT OF ANTIPODAL
     *
STANDPOINT/FOREPOINT MUST NOT BE THE GEOGRAPHIC POLE
     *

     * Reference ellipsoid is the WGS-84 ellipsoid.
     *
See http://www.colorado.edu/geography/gcraft/notes/datum/elist.html
     *

     * Requires close to 1.4 E-5 seconds wall clock time per call
     * on a 550 MHz Pentium with Linux 7.2.
     *
     * @param e      Earth object (defines radius and flattening)
     * @param lat1   Lat of point 1
     * @param lon1   Lon of point 1
     * @param lat2   Lat of point 2
     * @param lon2   Lon of point 2
     * @param result put result here, or null to allocate
     * @return a Bearing object with distance (in km), azimuth from
     *         pt1 to pt2 (degrees, 0 = north, clockwise positive)
     */
    public static Bearing calculateBearing(Earth e, double lat1, double lon1,
                                           double lat2, double lon2,
                                           Bearing result) {

        if (result == null) {
            result = new Bearing();
        }

        if ((lat1 == lat2) && (lon1 == lon2)) {
            result.distance    = 0;
            result.azimuth     = 0;
            result.backazimuth = 0;
            return result;
        }

        A = e.getMajor();
        F = e.getFlattening();
        R = 1.0 - F;

        // Algorithm from National Geodetic Survey, FORTRAN program "inverse,"
        // subroutine "INVER1," by L. PFEIFER and JOHN G. GERGEN.
        // http://www.ngs.noaa.gov/TOOLS/Inv_Fwd/Inv_Fwd.html
        // Conversion to JAVA from FORTRAN was made with as few changes as possible
        // to avoid errors made while recasting form, and to facilitate any future
        // comparisons between the original code and the altered version in Java.
        // Original documentation:
        // SOLUTION OF THE GEODETIC INVERSE PROBLEM AFTER T.VINCENTY
        // MODIFIED RAINSFORD'S METHOD WITH HELMERT'S ELLIPTICAL TERMS
        // EFFECTIVE IN ANY AZIMUTH AND AT ANY DISTANCE SHORT OF ANTIPODAL
        // STANDPOINT/FOREPOINT MUST NOT BE THE GEOGRAPHIC POLE
        // A IS THE SEMI-MAJOR AXIS OF THE REFERENCE ELLIPSOID
        // F IS THE FLATTENING (NOT RECIPROCAL) OF THE REFERNECE ELLIPSOID
        // LATITUDES GLAT1 AND GLAT2
        // AND LONGITUDES GLON1 AND GLON2 ARE IN RADIANS POSITIVE NORTH AND EAST
        // FORWARD AZIMUTHS AT BOTH POINTS RETURNED IN RADIANS FROM NORTH
        //
        // Reference ellipsoid is the WGS-84 ellipsoid.
        // See http://www.colorado.edu/geography/gcraft/notes/datum/elist.html
        // FAZ is forward azimuth in radians from pt1 to pt2;
        // BAZ is backward azimuth from point 2 to 1;
        // S is distance in meters.
        //
        // Conversion to JAVA from FORTRAN was made with as few changes as possible
        // to avoid errors made while recasting form, and to facilitate any future
        // comparisons between the original code and the altered version in Java.
        //
        //IMPLICIT REAL*8 (A-H,O-Z)
        //  COMMON/CONST/PI,RAD
        //  COMMON/ELIPSOID/A,F
        double GLAT1 = rad * lat1;
        double GLAT2 = rad * lat2;
        double TU1   = R * Math.sin(GLAT1) / Math.cos(GLAT1);
        double TU2   = R * Math.sin(GLAT2) / Math.cos(GLAT2);
        double CU1   = 1. / Math.sqrt(TU1 * TU1 + 1.);
        double SU1   = CU1 * TU1;
        double CU2   = 1. / Math.sqrt(TU2 * TU2 + 1.);
        double S     = CU1 * CU2;
        double BAZ   = S * TU2;
        double FAZ   = BAZ * TU1;
        double GLON1 = rad * lon1;
        double GLON2 = rad * lon2;
        double X     = GLON2 - GLON1;
        double D, SX, CX, SY, CY, Y, SA, C2A, CZ, E, C;
        int    loopCnt = 0;
        do {
            loopCnt++;
            //Check for an infinite loop
            if (loopCnt > 1000) {
                throw new IllegalArgumentException(
                    "Too many iterations calculating bearing:" + lat1 + " "
                    + lon1 + " " + lat2 + " " + lon2);
            }
            SX  = Math.sin(X);
            CX  = Math.cos(X);
            TU1 = CU2 * SX;
            TU2 = BAZ - SU1 * CU2 * CX;
            SY  = Math.sqrt(TU1 * TU1 + TU2 * TU2);
            CY  = S * CX + FAZ;
            Y   = Math.atan2(SY, CY);
            SA  = S * SX / SY;
            C2A = -SA * SA + 1.;
            CZ  = FAZ + FAZ;
            if (C2A > 0.) {
                CZ = -CZ / C2A + CY;
            }
            E = CZ * CZ * 2. - 1.;
            C = ((-3. * C2A + 4.) * F + 4.) * C2A * F / 16.;
            D = X;
            X = ((E * CY * C + CZ) * SY * C + Y) * SA;
            X = (1. - C) * X * F + GLON2 - GLON1;
            //IF(DABS(D-X).GT.EPS) GO TO 100
        } while (Math.abs(D - X) > EPS);

        if (loopCnt > maxLoopCnt) {
            maxLoopCnt = loopCnt;
            //        System.err.println("loopCnt:" + loopCnt);
        }

        FAZ = Math.atan2(TU1, TU2);
        BAZ = Math.atan2(CU1 * SX, BAZ * CX - SU1 * CU2) + Math.PI;
        X   = Math.sqrt((1. / R / R - 1.) * C2A + 1.) + 1.;
        X   = (X - 2.) / X;
        C   = 1. - X;
        C   = (X * X / 4. + 1.) / C;
        D   = (0.375 * X * X - 1.) * X;
        X   = E * CY;
        S   = 1. - E - E;
        S = ((((SY * SY * 4. - 3.) * S * CZ * D / 6. - X) * D / 4. + CZ) * SY
             * D + Y) * C * A * R;

        result.distance = S / 1000.0;  // meters to km
        result.azimuth  = FAZ * deg;   // radians to degrees

        if (result.azimuth < 0.0) {
            result.azimuth += 360.0;  // reset azs from -180 to 180 to 0 to 360
        }

        result.backazimuth = BAZ * deg;  // radians to degrees; already in 0 to 360 range

        return result;
    }

    /*
    * This method is for same use as calculateBearing, but has much simpler calculations
    * by assuming a spherical earth. It is actually slower than
    * "calculateBearing" code, probably due to having more trig function calls.
    * It is less accurate, too.
    * Errors are on the order of 1/300 or less. This code
    * saved here only as a warning to future programmers thinking of this approach.
    *
    * Requires close to 2.0 E-5 seconds wall clock time per call
    * on a 550 MHz Pentium with Linux 7.2.
    *
    * public static Bearing calculateBearingAlternate
    *   (LatLonPoint pt1, LatLonPoint pt2, Bearing result) {
    *
    * // to convert degrees to radians, multiply by:
    * final double rad = Math.toRadians(1.0);
    * // to convert radians to degrees:
    * final double deg = Math.toDegrees(1.0);
    *
    * if (result == null)
    *   result = new Bearing();
    *
    * double R = 6371008.7;  // mean earth radius in meters; WGS 84 definition
    * double GLAT1 = rad*(pt1.getLatitude());
    * double GLAT2 = rad*(pt2.getLatitude());
    * double GLON1 = rad*(pt1.getLongitude());
    * double GLON2 = rad*(pt2.getLongitude());
    *
    * // great circle angular separation in radians
    * double alpha = Math.acos( Math.sin(GLAT1)*Math.sin(GLAT2)
    *                          +Math.cos(GLAT1)*Math.cos(GLAT2)*Math.cos(GLON1-GLON2) );
    * // great circle distance in meters
    * double gcd = R * alpha;
    *
    * result.distance = gcd / 1000.0;      // meters to km
    *
    * // forward azimuth from point 1 to 2 in radians
    * double s2 = rad*(90.0-pt2.getLatitude());
    * double FAZ = Math.asin(Math.sin(s2)*Math.sin(GLON2-GLON1) / Math.sin(alpha));
    *
    * result.azimuth = FAZ * deg;        // radians to degrees
    * if (result.azimuth < 0.0)
    * result.azimuth += 360.0;       // reset az from -180 to 180 to 0 to 360
    *
    * // back azimuth from point 2 to 1 in radians
    * double s1 = rad*(90.0-pt1.getLatitude());
    * double BAZ = Math.asin(Math.sin(s1)*Math.sin(GLON1-GLON2) / Math.sin(alpha));
    *
    * result.backazimuth = BAZ * deg;
    * if (result.backazimuth < 0.0)
    *     result.backazimuth += 360.0;   // reset backaz from -180 to 180 to 0 to 360
    *
    * return result;
    * }
    */

    /**
     * the azimuth, degrees, 0 = north, clockwise positive
     */
    private double azimuth;

    /**
     * the back azimuth, degrees, 0 = north, clockwise positive
     */
    private double backazimuth;

    /**
     * separation in kilometers
     */
    private double distance;

    /**
     * Get the azimuth in degrees, 0 = north, clockwise positive
     *
     * @return azimuth in degrees
     */
    public double getAngle() {
        return azimuth;
    }

    /**
     * Get the back azimuth in degrees, 0 = north, clockwise positive
     *
     * @return back azimuth in degrees
     */
    public double getBackAzimuth() {
        return backazimuth;
    }

    /**
     * Get the distance in kilometers
     *
     * @return distance in km
     */
    public double getDistance() {
        return distance;
    }

    /**
     * Nice format.
     *
     * @return return a nice format of this Bearing
     */
    public String toString() {
        StringBuilder buf = new StringBuilder();
        buf.append("Azimuth: ");
        buf.append(azimuth);
        buf.append(" Back azimuth: ");
        buf.append(backazimuth);
        buf.append(" Distance: ");
        buf.append(distance);
        return buf.toString();
    }

    /**
     * Test the calculations - forward and back
     *
     * @param args non used
     */
    public static void main(String[] args) {
        //Bearing         workBearing = new Bearing();
        LatLonPointImpl pt1 = new LatLonPointImpl(40, -105);
        LatLonPointImpl pt2 = new LatLonPointImpl(37.4, -118.4);
        Bearing         b   = calculateBearing(pt1, pt2, null);
        System.out.println("Bearing from " + pt1 + " to " + pt2 + " = \n\t"
                           + b);
        LatLonPointImpl pt3 = new LatLonPointImpl();
        pt3 = findPoint(pt1, b.getAngle(), b.getDistance(), pt3);
        System.out.println(
            "using first point, angle and distance, found second point at "
            + pt3);
        pt3 = findPoint(pt2, b.getBackAzimuth(), b.getDistance(), pt3);
        System.out.println(
            "using second point, backazimuth and distance, found first point at "
            + pt3);
        /*  uncomment for timing tests
        for(int j=0;j<10;j++) {
            long t1 = System.currentTimeMillis();
            for(int i=0;i<30000;i++) {
                workBearing = Bearing.calculateBearing(42.5,-93.0,
                                                       48.9,-117.09,workBearing);
            }
            long t2 = System.currentTimeMillis();
            System.err.println ("time:" + (t2-t1));
        }
        */
    }

    /**
     * Calculate a position given an azimuth and distance from
     * another point.
     *
     * @param e      Earth object (defines radius and flattening)
     * @param pt1    Point 1
     * @param az     azimuth (degrees)
     * @param dist   distance from the point (km)
     * @param result Object to use if non-null
     * @return The LatLonPoint
     * @see #findPoint(double,double,double,double,LatLonPointImpl)
     */
    public static LatLonPointImpl findPoint(Earth e, LatLonPoint pt1,
                                            double az, double dist,
                                            LatLonPointImpl result) {
        return findPoint(e, pt1.getLatitude(), pt1.getLongitude(), az, dist,
                         result);
    }

    /**
     * Calculate a position given an azimuth and distance from
     * another point.  Uses default Earth.
     *
     * @param pt1    Point 1
     * @param az     azimuth (degrees)
     * @param dist   distance from the point (km)
     * @param result Object to use if non-null
     * @return The LatLonPoint
     * @see #findPoint(double,double,double,double,LatLonPointImpl)
     */
    public static LatLonPointImpl findPoint(LatLonPoint pt1, double az,
                                            double dist,
                                            LatLonPointImpl result) {
        return findPoint(defaultEarth, pt1.getLatitude(), pt1.getLongitude(),
                         az, dist, result);
    }

    /**
     * Calculate a position given an azimuth and distance from
     * another point.  See details, below.  Uses default Earth.
     *
     * @param lat1   latitude of starting point
     * @param lon1   longitude of starting point
     * @param az     forward azimuth (degrees)
     * @param dist   distance from the point (km)
     * @param result Object to use if non-null
     * @return the position as a LatLonPointImpl
     */
    public static LatLonPointImpl findPoint(double lat1, double lon1,
                                            double az, double dist,
                                            LatLonPointImpl result) {
        return findPoint(defaultEarth, lat1, lon1, az, dist, result);
    }

    /**
     * Calculate a position given an azimuth and distance from
     * another point.
     *

     *

     * Algorithm from National Geodetic Survey, FORTRAN program "forward,"
     * subroutine "DIRCT1," by stephen j. frakes.
     * http://www.ngs.noaa.gov/TOOLS/Inv_Fwd/Inv_Fwd.html
     *
Original documentation:
     *

     *    SOLUTION OF THE GEODETIC DIRECT PROBLEM AFTER T.VINCENTY
     *    MODIFIED RAINSFORD'S METHOD WITH HELMERT'S ELLIPTICAL TERMS
     *    EFFECTIVE IN ANY AZIMUTH AND AT ANY DISTANCE SHORT OF ANTIPODAL
     *

     *
     * @param e      Earth object (defines radius and flattening)
     * @param lat1   latitude of starting point
     * @param lon1   longitude of starting point
     * @param az     forward azimuth (degrees)
     * @param dist   distance from the point (km)
     * @param result Object to use if non-null
     * @return the position as a LatLonPointImpl
     */
    public static LatLonPointImpl findPoint(Earth e, double lat1,
                                            double lon1, double az,
                                            double dist,
                                            LatLonPointImpl result) {
        if (result == null) {
            result = new LatLonPointImpl();
        }

        if ((dist == 0)) {
            result.setLatitude(lat1);
            result.setLongitude(lon1);
            return result;
        }

        A = e.getMajor();
        F = e.getFlattening();
        R = 1.0 - F;

        // Algorithm from National Geodetic Survey, FORTRAN program "forward,"
        // subroutine "DIRCT1," by stephen j. frakes.
        // http://www.ngs.noaa.gov/TOOLS/Inv_Fwd/Inv_Fwd.html
        // Conversion to JAVA from FORTRAN was made with as few changes as
        // possible to avoid errors made while recasting form, and
        // to facilitate any future comparisons between the original
        // code and the altered version in Java.
        // Original documentation:
        //   SUBROUTINE DIRCT1(GLAT1,GLON1,GLAT2,GLON2,FAZ,BAZ,S)
        //
        //   SOLUTION OF THE GEODETIC DIRECT PROBLEM AFTER T.VINCENTY
        //   MODIFIED RAINSFORD'S METHOD WITH HELMERT'S ELLIPTICAL TERMS
        //   EFFECTIVE IN ANY AZIMUTH AND AT ANY DISTANCE SHORT OF ANTIPODAL
        //
        //   A IS THE SEMI-MAJOR AXIS OF THE REFERENCE ELLIPSOID
        //   F IS THE FLATTENING OF THE REFERENCE ELLIPSOID
        //   LATITUDES AND LONGITUDES IN RADIANS POSITIVE NORTH AND EAST
        //   AZIMUTHS IN RADIANS CLOCKWISE FROM NORTH
        //   GEODESIC DISTANCE S ASSUMED IN UNITS OF SEMI-MAJOR AXIS A
        //
        //   PROGRAMMED FOR CDC-6600 BY LCDR L.PFEIFER NGS ROCKVILLE MD 20FEB75
        //   MODIFIED FOR SYSTEM 360 BY JOHN G GERGEN NGS ROCKVILLE MD 750608
        //

        if (az < 0.0) {
            az += 360.0;  // reset azs from -180 to 180 to 0 to 360
        }
        double FAZ   = az * rad;
        double GLAT1 = lat1 * rad;
        double GLON1 = lon1 * rad;
        double S     = dist * 1000.;  // convert to meters
        double TU    = R * Math.sin(GLAT1) / Math.cos(GLAT1);
        double SF    = Math.sin(FAZ);
        double CF    = Math.cos(FAZ);
        double BAZ   = 0.;
        if (CF != 0) {
            BAZ = Math.atan2(TU, CF) * 2;
        }
        double CU  = 1. / Math.sqrt(TU * TU + 1.);
        double SU  = TU * CU;
        double SA  = CU * SF;
        double C2A = -SA * SA + 1.;
        double X   = Math.sqrt((1. / R / R - 1.) * C2A + 1.) + 1.;
        X = (X - 2.) / X;
        double C = 1. - X;
        C = (X * X / 4. + 1) / C;
        double D = (0.375 * X * X - 1.) * X;
        TU = S / R / A / C;
        double Y = TU;
        double SY, CY, CZ, E, GLAT2, GLON2;
        do {
            SY = Math.sin(Y);
            CY = Math.cos(Y);
            CZ = Math.cos(BAZ + Y);
            E  = CZ * CZ * 2. - 1.;
            C  = Y;
            X  = E * CY;
            Y  = E + E - 1.;
            Y = (((SY * SY * 4. - 3.) * Y * CZ * D / 6. + X) * D / 4. - CZ)
                * SY * D + TU;
        } while (Math.abs(Y - C) > EPS);
        BAZ   = CU * CY * CF - SU * SY;
        C     = R * Math.sqrt(SA * SA + BAZ * BAZ);
        D     = SU * CY + CU * SY * CF;
        GLAT2 = Math.atan2(D, C);
        C     = CU * CY - SU * SY * CF;
        X     = Math.atan2(SY * SF, C);
        C     = ((-3. * C2A + 4.) * F + 4.) * C2A * F / 16.;
        D     = ((E * CY * C + CZ) * SY * C + Y) * SA;
        GLON2 = GLON1 + X - (1. - C) * D * F;
        BAZ   = (Math.atan2(SA, BAZ) + Math.PI) * deg;
        result.setLatitude(GLAT2 * deg);
        result.setLongitude(GLON2 * deg);
        return result;
    }

}


/*
Copyright (C) 2002  Richard Eigenmann.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or any later version. This program is distributed
in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
more details. You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
The license is in gpl.txt.
See http://www.gnu.org/copyleft/gpl.html for the details.
*/
package ucar.nc2.ui.image;

import java.util.*;
import java.io.*;
import java.net.*;
import java.awt.image.*;
import java.awt.Dimension;
import java.util.Vector;
import java.util.Enumeration;
import javax.imageio.*;
import javax.imageio.stream.*;
import javax.imageio.event.*;
import java.text.*;
import javax.swing.*;
import java.awt.geom.*;

/*
SourcePicture.java:  class that can load a picture from a URL

Copyright (C) 2002  Richard Eigenmann.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or any later version. This program is distributed
in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
more details. You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
The license is in gpl.txt.
See http://www.gnu.org/copyleft/gpl.html for the details.
*/

/**
 *  a class to load and scale a picture either immediately or in a
 *  seperate thread from a URL
 */

public class SourcePicture implements Cloneable {

 /**
	 *  status code used to signal that the picture is not loaded
	 */
	public static final int UNINITIALISED = 0;

	/**
	 * status code used to signal that the thread is loading the image
	 */
	public static final int LOADING = UNINITIALISED + 1;

	/**
	 * status code used to signal that the thread is rotating the image
	 */
	public static final int ROTATING = LOADING + 1;

	/**
	 *  status code used to signal that the rotated image is available.
	 */
	public static final int READY = ROTATING + 1;

	/**
	 *  status code used to signal that there was an error
	 */
	public static final int ERROR = READY + 1;

	/**
	 *  status code used to tell that we have started loading an
	 *  image but only used on notifySourceLoadProgressListeners
	 */
	public static final int LOADING_STARTED = ERROR + 1;

	/**
	 *  status code used to tell that we have a progress update
	 *  but only used on notifySourceLoadProgressListeners
	 */
	public static final int LOADING_PROGRESS = LOADING_STARTED + 1;

	/**
	 *  status code used to tell that we have a finished loading
	 *  but only used on notifySourceLoadProgressListeners
	 */
	public static final int LOADING_COMPLETED = LOADING_PROGRESS + 1;

  ////////////////////////////////////////////////////////////////////////////

	/**
	 *  the Buffered Image that this class protects and provides features for.
	 */
	public BufferedImage sourcePictureBufferedImage = null;

	/**
	 *  the URL of the picture
	 */
	private URL imageUrl = null;

	/**
	 *  variable to track the status of the picture
	 */
	private int pictureStatusCode;

	/**
	 *  variable to compose te status message
	 */
	private String pictureStatusMessage;

	/**
	 *  A vector that holds all the listeners that want to be notified about
	 *  changes to this SourcePicture.
	 */
	private Vector sourcePictureListeners = new Vector();

	/**
	 *  variable that records how much has been loaded
	 */
	private int percentLoaded = 0;

	/**
	 *  reference to the inner class that listens to the image loading progress
	 */
	protected ImageProgressListener imageProgressListener = new ImageProgressListener();

	/**
	 * the reader object that will read the image
	 */
	private ImageReader reader;

	/**
	 *   Indicator to tell us if the loading was aborted.
	 */
	private boolean abortFlag = false;

	/**
	 *  Rotation 0-360 that the image is subjected to after loading
	 */
	private double rotation = 0;

	/**
	 *   Constructor
	 */
	SourcePicture() {
		setStatus(UNINITIALISED, "Uninitialised SourcePicture object.");
	}

	/**
	 *  method to invoke with a filename or URL of a picture that is to be loaded
	 *  a new thread. This is handy to update the screen while the loading chuggs along in the background.
	 *
	 *  @param	imageUrl	The URL of the image to be loaded
	 *  @param	priority	The Thread priority for this thread.
	 *  @param	rotation	The rotation 0-360 to be used on this picture
	 */
	public void loadPictureInThread( URL imageUrl, int priority, double rotation ) {
		if ( pictureStatusCode == LOADING ) {
			stopLoadingExcept( imageUrl );
		}

		this.imageUrl = imageUrl;
		this.rotation = rotation;
		LoadThread t = new LoadThread( this );
		t.setPriority( priority );
		t.start();
	}

	/**
	 *  method to invoke with a filename or URL of a picture that is to be loaded in
	 *  the main thread.
	 */
	public void loadPicture( URL imageUrl, double rotation ) {
		if ( pictureStatusCode == LOADING ) {
			stopLoadingExcept( imageUrl );
		}
		this.imageUrl = imageUrl;
		this.rotation = rotation;
		loadPicture();

	}

	/**
	 *  loads a picture from the URL in the imageUrl object into the sourcePictureBufferedImage
	 *  object and updates the status when done or failed.
	 */
	public void loadPicture() {
		Tools.log("SourcePicture.loadPicture: " + imageUrl.toString() + " loaded into SourcePicture object: " + Integer.toString(this.hashCode()) );
		//Tools.freeMem();

		setStatus( LOADING, "Loading: " + imageUrl.toString() );
		abortFlag = false;

		try {

			// Java 1.4 way with a Listener
			ImageInputStream iis = ImageIO.createImageInputStream( imageUrl.openStream() );
			Iterator i = ImageIO.getImageReaders( iis );
			if ( ! i.hasNext() ) { throw new IOException ("No Readers Available!"); }
			reader = (ImageReader) i.next();  // grab the first one

			reader.addIIOReadProgressListener( imageProgressListener );
			reader.setInput( iis );
			sourcePictureBufferedImage = null;
			// try {
				sourcePictureBufferedImage = reader.read( 0 ); // just get the first image
			/* } catch ( OutOfMemoryError e ) {
				Tools.log("SourcePicture caught an OutOfMemoryError while loading an image." );

				iis.close();
				reader.removeIIOReadProgressListener( imageProgressListener );
				reader.dispose();

				setStatus(ERROR, "Out of Memory Error while reading " + imageUrl.toString());
				sourcePictureBufferedImage = null;
				// PictureCache.clear();

				JOptionPane.showMessageDialog( null,  //deliberately null or it swaps the window
					"outOfMemoryError",
					"genericError",
					JOptionPane.ERROR_MESSAGE);

				System.gc();
				System.runFinalization();

				Tools.log("JPO has now run a garbage collection and finalization.");
				return;
			} */

			iis.close();
			reader.removeIIOReadProgressListener( imageProgressListener );
			//Tools.log("!!dispose being called!!");
			reader.dispose();

			if ( ! abortFlag ) {

				if ( rotation != 0 ) {
					setStatus( ROTATING, "Rotating: " + imageUrl.toString() );
					int xRot = sourcePictureBufferedImage.getWidth() / 2;
					int yRot = sourcePictureBufferedImage.getHeight() / 2;
					AffineTransform rotateAf = AffineTransform.getRotateInstance( Math.toRadians( rotation ), xRot, yRot );
					AffineTransformOp op = new AffineTransformOp( rotateAf, AffineTransformOp.TYPE_BILINEAR );
					Rectangle2D newBounds = op.getBounds2D( sourcePictureBufferedImage );
					// a simple AffineTransform would give negative top left coordinates -->
					// do another transform to get 0,0 as top coordinates again.
					double minX = newBounds.getMinX();
					double minY = newBounds.getMinY();

					AffineTransform translateAf = AffineTransform.getTranslateInstance( minX * (-1), minY * (-1) );
					rotateAf.preConcatenate( translateAf );
					op = new AffineTransformOp( rotateAf, AffineTransformOp.TYPE_BILINEAR );
					newBounds = op.getBounds2D( sourcePictureBufferedImage );

					// this piece of code is so essential!!! Otherwise the internal image format
					// is totally altered and either the AffineTransformOp decides it doesn't
					// want to rotate the image or web browsers can't read the resulting image.
					BufferedImage targetImage = new BufferedImage(
						(int) newBounds.getWidth(),
						(int) newBounds.getHeight(),
						BufferedImage.TYPE_3BYTE_BGR );

					sourcePictureBufferedImage = op.filter( sourcePictureBufferedImage, targetImage );
				}

				setStatus( READY, "Loaded: " + imageUrl.toString() );
				PictureCache.add( imageUrl, (SourcePicture) this.clone() );
			} else {
				setStatus( ERROR, "Aborted: " + imageUrl.toString() );
				sourcePictureBufferedImage = null;
			}
		} catch ( IOException e ) {
			setStatus(ERROR, "Error while reading " + imageUrl.toString());
			sourcePictureBufferedImage = null;
		}

	}

	/**
	 *  this method can be invoked to stop the current reader
	 */
	public void stopLoading() {
		if ( imageUrl == null )
			return; // SourcePicture has never been used yet

		Tools.log("SourcePicture.stopLoading: called on " + imageUrl );
		if ( pictureStatusCode == LOADING ) {
			reader.abort();
			abortFlag = true;
			//reader.dispose();
			//setStatus( ERROR, "Cache Loading was stopped " + imageUrl.toString() );
			//sourcePictureBufferedImage = null;
			// actually the thread reading the image continues
		}
	}

	/**
	 *  this method can be invoked to stop the current reader except if it
	 *  is reading the desired file. It returns true is the desired file
	 *  is being loaded. Otherwise it returns false.
	 */
	public boolean stopLoadingExcept( URL exemptionURL ) {
		if ( imageUrl == null )
			return false; // has never been used yet

		if ( pictureStatusCode != LOADING ) {
			Tools.log( "SourcePicture.stopLoadingExcept: called but pointless since image is not LOADING: " + imageUrl.toString());
			return false;
		}

		if ( ! exemptionURL.toString().equals( imageUrl.toString() ) ) {
			Tools.log ("SourcePicture.stopLoadingExcept: called with Url " + exemptionURL.toString() + " --> stopping loading of " + imageUrl.toString() );
			stopLoading();
			return true;
		} else
			return false;
	}

	/**
	 *   return the size of the image or Zero if there is none
	 */
	public Dimension getSize() {
		if ( sourcePictureBufferedImage != null )
			return new Dimension( sourcePictureBufferedImage.getWidth(), sourcePictureBufferedImage.getHeight());
		else
			return new Dimension(0,0);

	}

	/**
	 *   return the height of the image or Zero if there is none
	 */
	public int getHeight() {
		if ( sourcePictureBufferedImage != null )
			return sourcePictureBufferedImage.getHeight();
		else
			return 0;
	}

	/**
	 *   return the width of the image or Zero if there is none
	 */
	public int getWidth() {
		if ( sourcePictureBufferedImage != null )
			return sourcePictureBufferedImage.getWidth();
		else
			return 0;
	}

	/**
	 *   return the URL of the original image as a string
	 */
	public String getUrlString() {
		return imageUrl.toString();
	}

	/**
	 *  return the URL of the original image
	 */
	public URL getUrl() {
		return imageUrl;
	}

	/**
	 *  return the rotation of the image
	 */
	public double getRotation() {
		return rotation;
	}

	/**
	 *  method to register the listening object of the status events
	 */
	public void addListener ( SourcePictureListener listener ) {
		Tools.log("SourcePicture.addListener: listener added on SourcePicture " + Integer.toString(this.hashCode()) + " of class: " + listener.getClass().toString() );
		sourcePictureListeners.add( listener );
		//showListeners();
	}

	/**
	 *  method to register the listening object of the status events
	 */
	public void removeListener ( SourcePictureListener listener ) {
		Tools.log("SourcePicture.removeListener: listener removed from SourcePicture " + Integer.toString(this.hashCode()) + " of class: " + listener.getClass().toString() );
		sourcePictureListeners.remove( listener );
		//showListeners();
	}

	public void showListeners() {
		Tools.log("SourcePicture.showListeners: SoucePicture " + Integer.toString(this.hashCode()));
		Enumeration en = sourcePictureListeners.elements();
		while ( en.hasMoreElements() ) {
			Object obj = en.nextElement();
			Tools.log("    reports to Listener: " + Integer.toString(obj.hashCode()) + " of class " + obj.getClass().toString() );
		}
		Tools.log("    --------");
	}

	/**
	 *  method that says whether there are any listeners attached to this object
	 */
	public boolean hasNoListeners() {
		return ( sourcePictureListeners.isEmpty() );
	}

	/**
	 * Method that sets the status of the ScalablePicture object and notifies
	 * intereasted objects of a change in status (not built yet).
	 */
	private void setStatus(int statusCode, String statusMessage) {
		Tools.log("\nSourcePicture.setStatus: sending status: " + statusMessage );
		pictureStatusCode = statusCode;
		pictureStatusMessage = statusMessage;

		Vector nonmodifiedVector = (Vector) sourcePictureListeners.clone();

		Enumeration e = nonmodifiedVector.elements();
		while ( e.hasMoreElements() ) {
			//((SourcePictureListener) e.nextElement())
			//	.sourceStatusChange(pictureStatusCode, pictureStatusMessage, this );
			SourcePictureListener spl = ((SourcePictureListener) e.nextElement());
				spl.sourceStatusChange(pictureStatusCode, pictureStatusMessage, this );

			Tools.log("\nSourcePicture.setStatus: sending status: " + statusMessage + " to " + spl.getClass().toString() );
    }
	}

	/**
	 * Method that returns the status code of the picture loading.
	 */
	public int getStatusCode() {
		return pictureStatusCode;
	}

	/**
	 * Method that returns the status code of the picture loading.
	 */
	public String getStatusMessage() {
		return pictureStatusMessage;
	}

	/**
	 *  Returns how much of the image has been loaded
	 */
	public int getPercentLoaded() {
		return percentLoaded;
	}

	/**
	 *  returns the buffered image that was loaded or null if there is no image.
	 *
	 *  @return	the BufferedImage that was loaded or null if there is no image.
	 */
	public BufferedImage getSourceBufferedImage() {
		return sourcePictureBufferedImage;
	}

	/**
	 *  sets the buffered image. Unusual method use with care.
	 *
	 */
	public void setSourceBufferedImage( BufferedImage img, String statusMessage ) {
		sourcePictureBufferedImage = img;
		setStatus( READY, statusMessage );
	}

	/**
	 *  returns a reference to this SourcePicture object
	 *
	 *  @return	the reference to this SourcePicture object
	 */
	public SourcePicture getSourcePicture() {
		return this;
	}

	/**
	 *  creates a copy of the SourcePicture
	 */
	public Object clone() {
		Object obj = null;
		try {
			obj = super.clone();
		} catch ( CloneNotSupportedException e ) {
			Tools.log("Cloning not supported on SourcePicture!");
		}
		return obj;
	}

	/**
	 *  Special class that allows to catch notifications about how the image
	 *  reading is getting along
	 */
	class ImageProgressListener implements IIOReadProgressListener {
		private DecimalFormat percentageFormatter = new DecimalFormat("##0");

		private void notifySourceLoadProgressListeners( int statusCode, int percentage ) {
			percentLoaded = percentage;
			Enumeration e = sourcePictureListeners.elements();
			while ( e.hasMoreElements() ) {
				((SourcePictureListener) e.nextElement())
					.sourceLoadProgressNotification( statusCode, percentage );
			}
		}

		public void imageComplete( ImageReader source ) {
			notifySourceLoadProgressListeners ( LOADING_COMPLETED, 100 );
			Tools.log( "imageComplete" );
		}
		public void imageProgress( ImageReader source, float percentageDone ) {
			notifySourceLoadProgressListeners ( LOADING_PROGRESS,
                    (int) percentageDone);
			Tools.log( "imageProgress: " + percentageFormatter.format( percentageDone ) + "%");
		}
		public void imageStarted( ImageReader source, int imageIndex ) {
			notifySourceLoadProgressListeners ( LOADING_STARTED, 0 );
			Tools.log( "imageStarted" );
		}
		public void readAborted( ImageReader source ) {
		}
		public void sequenceComplete( ImageReader source ) {
		}
		public void sequenceStarted( ImageReader source, int minIndex ) {
		}
		public void thumbnailComplete( ImageReader source ) {
		}
		public void thumbnailProgress( ImageReader source, float percentageDone ) {
		}
		public void thumbnailStarted( ImageReader source, int imageIndex, int thumbnailIndex ) {
		}

	}

}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package thredds.client.catalog;

import thredds.client.catalog.tools.DataFactory;
import ucar.nc2.constants.DataFormatType;
import ucar.nc2.ft.remote.CdmrFeatureDataset;
import ucar.nc2.stream.CdmRemote;

import javax.annotation.concurrent.Immutable;
import java.net.URI;

/**
 *  A Dataset Access element
 *
 * @author caron
 * @since 1/7/2015
 */
@Immutable
public class Access {                 // (5)
  private final Dataset dataset;
  private final String urlPath;
  private final Service service;
  private final String dataFormatS;
  private final long dataSize;

  public Access(Dataset dataset, String urlPath, Service service, String dataFormatS, long dataSize) {
    this.dataset = dataset;
    this.urlPath = urlPath; // urlPath.startsWith("/") ? urlPath.substring(1) : urlPath;
    this.service = service;
    this.dataFormatS = dataFormatS;
    this.dataSize = dataSize;
  }

  public Dataset getDataset() {
    return dataset;
  }

  public String getUrlPath() {
    return urlPath;
  }

  public Service getService() {
    return service;
  }

  public DataFormatType getDataFormatType() {
    if (dataFormatS == null) return null;
    try {
      return DataFormatType.getType(dataFormatS);
    } catch (Exception e) {
      return null;
    }
  }

  public String getDataFormatName() {
    return dataFormatS;
  }

  public long getDataSize() {
    return dataSize;
  }                   // optional

  /**
   * Get the standard URL, with resolution if the URL is reletive.
   * catalog.resolveURI( getUnresolvedUrlName())
   *
   * @return URL string, or null if error.
   */
  public String getStandardUrlName() {
    URI uri = getStandardUri();
    if (uri == null) return null;
    return uri.toString();
  }

  /**
   * Construct the standard THREDDS access URI for this dataset access method,
   * resolved agaisnt the parent catalog if the URI is relative.
   *
   * @return the standard fully resolved THREDDS access URI for this dataset access method, or null if error.
   */
  public URI getStandardUri() {
    try {
      Catalog cat = dataset.getParentCatalog();
      if (cat == null)
        return new URI(getUnresolvedUrlName());
      return cat.resolveUri(getUnresolvedUrlName());

    } catch (java.net.URISyntaxException e) {
      throw new RuntimeException("Error parsing URL= " + getUnresolvedUrlName());
    }
  }

  /**
   * Construct "unresolved" URL: service.getBase() + getUrlPath() + service.getSuffix().
   * It is not resolved, so it may be a reletive URL.
   * @return unresolved Url as a String
   */
  public String getUnresolvedUrlName() {
    return service.getBase() + getUrlPath() + service.getSuffix();
  }

  public String getWrappedUrlName() {
    URI uri = getStandardUri();
    if (uri == null) return null;

    if (service.getType() == ServiceType.THREDDS)
      return DataFactory.SCHEME + uri;
    if (service.getType() == ServiceType.CdmRemote)
      return CdmRemote.SCHEME + uri;
    if (service.getType() == ServiceType.CdmrFeature)
      return CdmrFeatureDataset.SCHEME + uri;
    return uri.toString();
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    Access access = (Access) o;

    if (dataSize != access.dataSize) return false;
    if (dataFormatS != null ? !dataFormatS.equals(access.dataFormatS) : access.dataFormatS != null) return false;
    if (service != null ? !service.equals(access.service) : access.service != null) return false;
    if (urlPath != null ? !urlPath.equals(access.urlPath) : access.urlPath != null) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result = urlPath != null ? urlPath.hashCode() : 0;
    result = 31 * result + (service != null ? service.hashCode() : 0);
    result = 31 * result + (dataFormatS != null ? dataFormatS.hashCode() : 0);
    result = 31 * result + (int) (dataSize ^ (dataSize >>> 32));
    return result;
  }

  @Override
  public String toString() {
    return "Access{" +
            "service=" + service +
            ", urlPath='" + urlPath + '\'' +
            ", dataFormatS='" + dataFormatS + '\'' +
            ", dataSize=" + dataSize +
            '}';
  }
}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2.iosp.bufr;

import ucar.nc2.iosp.bufr.tables.TableC;
import ucar.nc2.iosp.bufr.tables.TableB;

import java.util.List;

/**
 * Essentially a TableB entry, modified by any relevent TableC operators.
 * TableD has been expanded.
 * Replication gets made into nested DataDescriptors, which we map to Structures (fixed replication) or
 * Sequences (deferred replication).
 * Most of the processing is done by DataDescriptorTreeConstructor.convert().
 * Here we encapsulate the final result, ready to map to the CDM.
 *
 * @author caron
 * @since Apr 5, 2008
 */
public class DataDescriptor {
  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DataDescriptor.class);

  ////////////////////////////////

  // from the TableB.Descriptor
  short fxy;
  int f, x, y;
  String name, units, desc, source;
  boolean bad;  // no descriptor found
  boolean localOverride;

  // may get modified by TableC operators
  int scale;
  int refVal;
  int bitWidth;
  int type; // 0 = isNumeric, 1 = isString, 2 = isEnum, 3 = compound;

  // replication info
  List<DataDescriptor> subKeys;
  int replication = 1; // number of replications, essentially dk.y when sk.f == 1
  int replicationCountSize; // for delayed replication : size of count in bits
  int repetitionCountSize; // for delayed repetition

  AssociatedField assField; // associated field == 02 04 Y, Y number of extra bits
  Object refersTo; // temporary place to put a sequence object
  DataDescriptorTreeConstructor.DataPresentIndicator dpi;

  DataDescriptor() {
  }

  public DataDescriptor(short fxy, BufrTableLookup lookup) {
    this.fxy = fxy;
    this.f = (fxy & 0xC000) >> 14;
    this.x = (fxy & 0x3F00) >> 8;
    this.y = fxy & 0xFF;

    TableB.Descriptor db = null;
    if (f == 0) {
      db = lookup.getDescriptorTableB(fxy);
      if (db != null)
        setDescriptor(db);
      else {
        bad = true;
        if (f != 1) this.name = "*NOT FOUND";
      }
    }

    else if (f == 1) // replication
      this.type = 3; // compound

    else if (f == 2) {
      this.name = TableC.getOperatorName(x);
    }
  }

  private void setDescriptor(TableB.Descriptor d) {
    this.name = d.getName().trim();
    this.units = d.getUnits().trim();
    this.desc = d.getDesc();
    this.refVal = d.getRefVal();
    this.scale = d.getScale();
    this.bitWidth = d.getDataWidth();
    this.localOverride = d.getLocalOverride();
    this.source = d.getSource();

    if (units.equalsIgnoreCase("CCITT IA5") || units.equalsIgnoreCase("CCITT_IA5")) {
      this.type = 1; // String
      //this.bitWidth *= 8;
    }

    // LOOK what about flag table ??
    if (units.equalsIgnoreCase("Code Table") || units.equalsIgnoreCase("Code_Table")) {
      this.type = 2; // enum
    }
  }

  /* for dpi fields
  DataDescriptor makeStatField(String statType) {
    DataDescriptor statDD = new DataDescriptor();
    statDD.name = name + "_" + statType;
    statDD.units = units;
    statDD.refVal = 0;

    return statDD;
  } */

  // for associated fields
  DataDescriptor makeAssociatedField(int bitWidth) {
    DataDescriptor assDD = new DataDescriptor();
    assDD.name = name + "_associated_field";
    assDD.units = "";
    assDD.refVal = 0;
    assDD.scale = 0;
    assDD.bitWidth = bitWidth;
    assDD.type = 0;

    assDD.f = 0;
    assDD.x = 31;
    assDD.y = 22;
    assDD.fxy =  (short) ((f << 14) + (x << 8) + (y));

    return assDD;
  }

  static class AssociatedField {
    int nbits;
    int nfields;
    String dataFldName;

    AssociatedField(int nbits) {
      this.nbits = nbits;
    }
  }

  public List<DataDescriptor> getSubKeys() {
    return subKeys;
  }

  public boolean isOkForVariable() {
    return (f == 0) || (f == 1) || ((f == 2) && (x == 5) || ((f == 2) && (x == 24) && (y == 255)));
  }

  public boolean isLocal() {
    if ((f == 0) || (f == 3)) {
      if ((x >= 48) || (y >= 192))
        return true;
    }
    return false;
  }

  public boolean isLocalOverride() {
    return localOverride;
  }

  public String getFxyName() {
    return Descriptor.makeString(f,x,y);
  }

  public short getFxy() {
    return fxy;
  }

  public String getName() {
    return name;
  }

  public String getSource() {
    return source;
  }

  public int getType() {
    return type;
  }

  public int getScale() {
    return scale;
  }

  public int getRefVal() {
    return refVal;
  }

  public String getUnits() {
    return units;
  }

  public String getDesc() {
    return desc;
  }

  public float convert( long raw) {
    if ( BufrNumbers.isMissing(raw, bitWidth)) return Float.NaN;

    // bpacked = (value * 10^scale - refVal)
    // value = (bpacked + refVal) / 10^scale
    float fscale = (float) Math.pow(10.0, -scale); // LOOK precompute ??
    float fval = (raw + refVal);
    return fscale * fval;
  }

  public static float convert( long raw, int scale, int refVal, int bitWidth) {
    if ( BufrNumbers.isMissing(raw, bitWidth)) return Float.NaN;

    // bpacked = (value * 10^scale - refVal)
    // value = (bpacked + refVal) / 10^scale
    float fscale = (float) Math.pow(10.0, -scale); // LOOK precompute ??
    float fval = (raw + refVal);
    return fscale * fval;
  }

  /**
   * Transfer info from the "proto message" to another message with the exact same structure.
   * @param fromList transfer from here
   * @param toList to here
   */
  static public void transferInfo(List<DataDescriptor> fromList, List<DataDescriptor> toList) { // get info from proto message
    if (fromList.size() != toList.size())
      throw new IllegalArgumentException("list sizes dont match "+fromList.size()+" != "+toList.size());

    for (int i=0; i<fromList.size(); i++) {
      DataDescriptor from = fromList.get(i);
      DataDescriptor to = toList.get(i);
      to.refersTo = from.refersTo;
      to.name = from.name;

      if (from.getSubKeys() != null)
        transferInfo(from.getSubKeys(), to.getSubKeys());
    }
  }

  ///////////////////////////////////////////////////////////////////////////////////////////////
  private int total_nbytesCDM = 0;

  /**
   * count the bits used by the data in this dd and its children
   * only accurate for not compressed, and not variable length
   *
   * @return bits used by the data in the file
   */
  int countBits() {
    int total_nbits = 0;
    total_nbytesCDM = 0;

    for (DataDescriptor dd : subKeys) {
      if (dd.subKeys != null) {
        total_nbits += dd.countBits();
        total_nbytesCDM += dd.total_nbytesCDM;

      } else if (dd.f == 0) {
        total_nbits += dd.bitWidth;
        total_nbytesCDM += dd.getByteWidthCDM();
      }
    }

    // replication
    if (replication > 1) {
      total_nbits *= replication;
      total_nbytesCDM *= replication;
    }

    return total_nbits;
  }

  public int getBitWidth() {
    return bitWidth;
  }

  /**
   * Get the number of bytes the CDM datatype will take
   *
   * @return the number of bytes the CDM datatype will take
   */
  public int getByteWidthCDM() {
    if (type == 1) // string
      return bitWidth / 8;

    if (type == 3) // compound
      return total_nbytesCDM;

    // numeric or enum
    if (bitWidth < 9) return 1;
    if (bitWidth < 17) return 2;
    if (bitWidth < 33) return 4;
    return 8;
  }

  public String toString() {
    String id = getFxyName();
    StringBuilder sbuff = new StringBuilder();
    if (f == 0) {
      sbuff.append(getFxyName()).append(": ");
      sbuff.append(name).append(" units=").append(units);
      if (type == 0) {
        sbuff.append(" scale=").append(scale).append(" refVal=").append(refVal);
        sbuff.append(" nbits=").append(bitWidth);
      } else if (type == 1) {
        sbuff.append(" nchars=").append(bitWidth / 8);
      } else {
        sbuff.append(" enum nbits=").append(bitWidth);
      }

    } else if (f == 1) {
      sbuff.append(id).append(": ").append( "Replication");
      if (replication != 1)
        sbuff.append(" count=").append(replication);
      if (replicationCountSize != 0)
        sbuff.append(" replicationCountSize=").append(replicationCountSize);
      if (repetitionCountSize != 0)
        sbuff.append(" repetitionCountSize=").append(repetitionCountSize);
      if (name != null)
        sbuff.append(": " + name);

    } else if (f == 2) {
      String desc = TableC.getOperatorName(x);
      if (desc == null) desc = "Operator";
      sbuff.append(id).append(": ").append(desc);

    } else
      sbuff.append(id).append(": ").append( name);

    return sbuff.toString();
  }

  /////////////////////////////////
  // stuff for the root
  boolean isVarLength;
  boolean isBad;
  int total_nbits;

  public int getTotalBits() { return total_nbits; }
  public boolean isVarLength() { return isVarLength; }

  ////////////////////////////////////////////////////////////////////////////////////////////////
  // LOOK need different hashCode, reader assumes using object id
  public boolean equals2(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    DataDescriptor that = (DataDescriptor) o;

    if (fxy != that.fxy) return false;
    if (replication != that.replication) return false;
    if (type != that.type) return false;
    if (subKeys != null ? !subKeys.equals(that.subKeys) : that.subKeys != null) return false;

    return true;
  }

  public int hashCode2() {
    int result = (int) fxy;
    result = 31 * result + type;
    result = 31 * result + getListHash();
    result = 31 * result + replication;
    return result;
  }

  // has to use hashCode2, so cant use list.hashCode()
  private int getListHash() {
    if (subKeys == null) return 0;
    int result = 1;
    for (DataDescriptor e : subKeys)
      result = 31*result + (e==null ? 0 : e.hashCode2());
    return result;
  }

}