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

package ucar.nc2.grib.collection;

import com.beust.jcommander.*;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import thredds.featurecollection.FeatureCollectionConfig;
import thredds.featurecollection.FeatureCollectionConfigBuilder;
import thredds.featurecollection.FeatureCollectionType;
import thredds.filesystem.MFileOS;
import thredds.inventory.*;
import thredds.inventory.filter.StreamFilter;
import thredds.inventory.partition.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.dataset.DatasetUrl;
import ucar.nc2.grib.GribIndexCache;
import ucar.nc2.grib.grib1.Grib1RecordScanner;
import ucar.nc2.grib.grib2.Grib2RecordScanner;
import ucar.nc2.stream.NcStream;
import ucar.nc2.util.CancelTask;
import ucar.nc2.util.cache.FileCacheIF;
import ucar.nc2.util.cache.FileCacheable;
import ucar.nc2.util.cache.FileFactory;
import ucar.unidata.io.RandomAccessFile;
import ucar.unidata.util.StringUtil2;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Formatter;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Utilities for creating GRIB CDM index (ncx) files, both collections and partitions
 *
 * @author John
 * @since 12/5/13
 */
public class GribCdmIndex implements IndexReader {
  public enum GribCollectionType {GRIB1, GRIB2, Partition1, Partition2, none}
  public static final String NCX_SUFFIX = ".ncx4";  // LOOK this will have to be generalized, so different collections (GRIB, BUFR, FMRC can use different suffix)

  private static final Logger classLogger = LoggerFactory.getLogger(GribCdmIndex.class);

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

  // object cache for ncx files - these are opened only as GribCollection
  public static FileCacheIF gribCollectionCache;

  public static void initDefaultCollectionCache(int minElementsInMemory, int maxElementsInMemory, int period) {
    gribCollectionCache = new ucar.nc2.util.cache.FileCacheGuava("DefaultGribCollectionCache", maxElementsInMemory);
  }

  public static void disableGribCollectionCache() {
    if (null != gribCollectionCache) gribCollectionCache.disable();
    gribCollectionCache = null;
  }

  public static void setGribCollectionCache(FileCacheIF cache) {
    if (null != gribCollectionCache) gribCollectionCache.disable();
    gribCollectionCache = cache;
  }

  // open GribCollectionImmutable from an existing index file. return null on failure
  static GribCollectionImmutable acquireGribCollection(FileFactory factory, Object hashKey,
      String location, int buffer_size, CancelTask cancelTask, Object spiObject) throws IOException {
    FileCacheable result;
    DatasetUrl durl = new DatasetUrl(null, location);

    if (gribCollectionCache != null) {
      // FileFactory factory, Object hashKey, String location, int buffer_size, CancelTask cancelTask, Object spiObject
      result = GribCdmIndex.gribCollectionCache.acquire(factory, hashKey, durl, buffer_size, cancelTask, spiObject);

    } else {
      // String location, int buffer_size, ucar.nc2.util.CancelTask cancelTask, Object iospMessage
      result = factory.open(durl, buffer_size, cancelTask, spiObject);
    }

    return (GribCollectionImmutable) result;
  }

  public static void shutdown() {
    if (gribCollectionCache != null) gribCollectionCache.clearCache(true);
  }

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

  public static File getTopIndexFileFromConfig(FeatureCollectionConfig config) {
    File indexFile = makeTopIndexFileFromConfig(config);
    return GribIndexCache.getExistingFileOrCache(indexFile.getPath());
  }

  /**
   * This is only used for the top level GribCollection.
   *
   * @param config use this FeatureCollectionConfig
   * @return index File
   */
  private static File makeTopIndexFileFromConfig(FeatureCollectionConfig config) {
    Formatter errlog = new Formatter();
    CollectionSpecParser specp = config.getCollectionSpecParser(errlog);

    String name = StringUtil2.replace(config.collectionName, '\\', "/");
    // String cname = DirectoryCollection.makeCollectionName(name, Paths.get(specp.getRootDir()));

    return makeIndexFile(name, new File(specp.getRootDir()));
  }

  static File makeIndexFile(String collectionName, File directory) {
    String nameNoBlanks = StringUtil2.replace(collectionName, ' ', "_");
    return new File(directory, nameNoBlanks + NCX_SUFFIX);
  }

  private static String makeNameFromIndexFilename(String idxPathname) {
    idxPathname = StringUtil2.replace(idxPathname, '\\', "/");
    int pos = idxPathname.lastIndexOf('/');
    String idxFilename = (pos < 0) ? idxPathname : idxPathname.substring(pos + 1);
    assert idxFilename.endsWith(NCX_SUFFIX) : idxFilename;
    return idxFilename.substring(0, idxFilename.length() - NCX_SUFFIX.length());
  }

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

  /**
   * Find out what kind of index this is
   *
   * @param raf open RAF
   * @return GribCollectionType
   * @throws IOException on read error
   */
  public static GribCollectionType getType(RandomAccessFile raf) throws IOException {
    String magic;

    raf.seek(0);
    magic = raf.readString(Grib2CollectionWriter.MAGIC_START.getBytes(CDM.utf8Charset).length);

    switch (magic) {
      case Grib2CollectionWriter.MAGIC_START:
        return GribCollectionType.GRIB2;

      case Grib1CollectionWriter.MAGIC_START:
        return GribCollectionType.GRIB1;

      case Grib2PartitionBuilder.MAGIC_START:
        return GribCollectionType.Partition2;

      case Grib1PartitionBuilder.MAGIC_START:
        return GribCollectionType.Partition1;

    }
    return GribCollectionType.none;
  }

  /* open GribCollection from an existing index file. return null on failure
  public static GribCollectionImmutable openCdmIndex(String indexFilename, FeatureCollectionConfig config, Logger logger) {
    return openCdmIndex(indexFilename, config, true, logger);
  } */

  // open GribCollectionImmutable from an existing index file. return null on failure
  @Nullable
  public static GribCollectionImmutable openCdmIndex(String indexFilename, FeatureCollectionConfig config, boolean useCache, Logger logger) throws IOException {
    File indexFileInCache = useCache ? GribIndexCache.getExistingFileOrCache(indexFilename) : new File(indexFilename);
    if (indexFileInCache == null)
      return null;
    String indexFilenameInCache = indexFileInCache.getPath();
    String name = makeNameFromIndexFilename(indexFilename);
    GribCollectionImmutable result = null;

    try (RandomAccessFile raf = RandomAccessFile.acquire(indexFilenameInCache)) {
      GribCollectionType type = getType(raf);

      switch (type) {
        case GRIB2:
          result = Grib2CollectionBuilderFromIndex.readFromIndex(name, raf, config, logger);
          break;
        case Partition2:
          result = Grib2PartitionBuilderFromIndex.createTimePartitionFromIndex(name, raf, config, logger);
          break;
        case GRIB1:
          result = Grib1CollectionBuilderFromIndex.readFromIndex(name, raf, config, logger);
          break;
        case Partition1:
          result = Grib1PartitionBuilderFromIndex.createTimePartitionFromIndex(name, raf, config, logger);
          break;
        default:
          logger.warn("GribCdmIndex.openCdmIndex failed on {} type={}", indexFilenameInCache, type);
      }
    } catch (FileNotFoundException ioe) {
      throw ioe;

    } catch (Throwable t) {
      logger.warn("GribCdmIndex.openCdmIndex failed on " + indexFilenameInCache, t);
      RandomAccessFile.eject(indexFilenameInCache);
      if (!indexFileInCache.delete())
        logger.warn("failed to delete {}", indexFileInCache.getPath());
    }

    return result;
  }

  // used by PartitionCollectionMutable.Partition
  // open GribCollectionImmutable from an existing index file. return null on failure
  @Nullable
  public static GribCollectionMutable openMutableGCFromIndex(String indexFilename, FeatureCollectionConfig config, boolean dataOnly, boolean useCache, Logger logger) {
    File indexFileInCache = useCache ? GribIndexCache.getExistingFileOrCache(indexFilename) : new File(indexFilename);
    if (indexFileInCache == null) {
      return null;
    }
    String indexFilenameInCache = indexFileInCache.getPath();
    String name = makeNameFromIndexFilename(indexFilename);
    GribCollectionMutable result = null;

    try (RandomAccessFile raf = RandomAccessFile.acquire(indexFilenameInCache)) {
      GribCollectionType type = getType(raf);

      switch (type) {
        case GRIB2:
          result = Grib2CollectionBuilderFromIndex.openMutableGCFromIndex(name, raf, config, logger);
          break;
        case Partition2:
          result = Grib2PartitionBuilderFromIndex.openMutablePCFromIndex(name, raf, config, logger);
          break;
        case GRIB1:
          result = Grib1CollectionBuilderFromIndex.openMutableGCFromIndex(name, raf, config, logger);
          break;
        case Partition1:
          result = Grib1PartitionBuilderFromIndex.openMutablePCFromIndex(name, raf, config, logger);
          break;
        default:
          logger.warn("GribCdmIndex.openMutableGCFromIndex failed on {} type={}", indexFilenameInCache, type);
      }

      if (result != null) {
        result.lastModified = raf.getLastModified();
        result.fileSize = raf.length();
      }

    } catch (Throwable t) {
      logger.warn("GribCdmIndex.openMutableGCFromIndex failed on " + indexFilenameInCache, t);
    }

    if (result == null) {
      RandomAccessFile.eject(indexFilenameInCache);
      if (!indexFileInCache.delete())
        logger.warn("failed to delete {}", indexFileInCache.getPath());
    }

    return result;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // used by PartitionCollection

  // used (only) by GribCollectionBuilder
  public static boolean updateGribCollectionFromPCollection(boolean isGrib1, PartitionManager dcm, CollectionUpdateType updateType,
                                                            Formatter errlog, org.slf4j.Logger logger) throws IOException {

    if (updateType == CollectionUpdateType.never || dcm instanceof CollectionSingleIndexFile) { // LOOK would isIndexFile() be better ?
      // then just open the existing index file
      return false;
    }

    boolean changed = updatePartition(isGrib1, dcm, updateType, logger, errlog);
    if (errlog != null) errlog.format("PartitionCollection %s was recreated %s%n", dcm.getCollectionName(), changed);
    return changed;
  }

  ////////////////////////////////////////////////////////////////////////////////////////////////
  // used by Tdm (indirectly by InvDatasetFcGrib)

  /**
   * Update Grib Collection if needed
   *
   * @return true if the collection was updated
   */
  public static boolean updateGribCollection(FeatureCollectionConfig config, CollectionUpdateType updateType, Logger logger) throws IOException {
    if (logger == null) logger = classLogger;

    long start = System.currentTimeMillis();

    Formatter errlog = new Formatter();
    CollectionSpecParser specp = config.getCollectionSpecParser(errlog);
    Path rootPath = Paths.get(specp.getRootDir());
    boolean isGrib1 = config.type == FeatureCollectionType.GRIB1;

    boolean changed;

    if (config.ptype == FeatureCollectionConfig.PartitionType.none || config.ptype == FeatureCollectionConfig.PartitionType.all) {

      try (CollectionAbstract dcm = new CollectionPathMatcher(config, specp, logger)) {
        changed = updateGribCollection(isGrib1, dcm, updateType, FeatureCollectionConfig.PartitionType.none, logger, errlog);
      }

    } else if (config.ptype == FeatureCollectionConfig.PartitionType.timePeriod) {

      try (TimePartition tp = new TimePartition(config, specp, logger)) {
        changed = updateTimePartition(isGrib1, tp, updateType, logger);
      }

    } else {

      // LOOK assume wantSubdirs makes it into a Partition. Isnt there something better ??
      if (specp.wantSubdirs()) {  // its a partition

        try (DirectoryPartition dpart = new DirectoryPartition(config, rootPath, true, new GribCdmIndex(logger), NCX_SUFFIX, logger)) {
          dpart.putAuxInfo(FeatureCollectionConfig.AUX_CONFIG, config);
          changed = updateDirectoryCollectionRecurse(isGrib1, dpart, config, updateType, logger);
        }

      } else { // otherwise its a leaf directory
        changed = updateLeafCollection(isGrib1, config, updateType, true, logger, rootPath);
      }
    }

    long took = System.currentTimeMillis() - start;
    logger.info("updateGribCollection {} changed {} took {} msecs", config.collectionName, changed, took);
    return changed;
  }

  // return true if changed, exception on failure
  public static boolean updateGribCollection(boolean isGrib1, MCollection dcm,
      CollectionUpdateType updateType, FeatureCollectionConfig.PartitionType ptype,
      Logger logger, Formatter errlog) throws IOException {

    logger.debug("GribCdmIndex.updateGribCollection %s %s%n", dcm.getCollectionName(), updateType);
    if (!isUpdateNeeded(dcm.getIndexFilename(NCX_SUFFIX), updateType, (isGrib1 ? GribCollectionType.GRIB1 : GribCollectionType.GRIB2), logger)) return false;

    boolean changed;
    if (isGrib1) {  // existing case handles correctly - make seperate index for each runtime (OR) partition == runtime
      Grib1CollectionBuilder builder = new Grib1CollectionBuilder(dcm.getCollectionName(), dcm, logger);
      changed = builder.updateNeeded(updateType) && builder.createIndex(ptype, errlog);
    } else {
      Grib2CollectionBuilder builder = new Grib2CollectionBuilder(dcm.getCollectionName(), dcm, logger);
      changed = builder.updateNeeded(updateType) && builder.createIndex(ptype, errlog);
    }
    return changed;
  }

  // return true if changed, exception on failure
  private static boolean updatePartition(boolean isGrib1, PartitionManager dcm, CollectionUpdateType updateType,
                                         Logger logger, Formatter errlog) throws IOException {
    boolean changed;
    if (isGrib1) {
      Grib1PartitionBuilder builder = new Grib1PartitionBuilder(dcm.getCollectionName(), new File(dcm.getRoot()), dcm, logger);
      changed = builder.updateNeeded(updateType) && builder.createPartitionedIndex(updateType, errlog);

    } else {
      Grib2PartitionBuilder builder = new Grib2PartitionBuilder(dcm.getCollectionName(), new File(dcm.getRoot()), dcm, logger);
      changed = builder.updateNeeded(updateType) && builder.createPartitionedIndex(updateType, errlog);
    }
    return changed;
  }

  private static boolean updateTimePartition(boolean isGrib1, TimePartition tp, CollectionUpdateType updateType, Logger logger) throws IOException {

    logger.debug("GribCdmIndex.updateTimePartition %s %s%n", tp.getRoot(), updateType);
    if (!isUpdateNeeded(tp.getIndexFilename(NCX_SUFFIX), updateType, (isGrib1 ? GribCollectionType.Partition1 : GribCollectionType.Partition2), logger)) return false;

    long start = System.currentTimeMillis();
    Formatter errlog = new Formatter();

    for (MCollection part : tp.makePartitions(updateType)) {
      try {
        updateGribCollection(isGrib1, part, updateType, FeatureCollectionConfig.PartitionType.timePeriod, logger, errlog);

      } catch (Throwable t) {
        logger.warn("Error making partition " + part.getRoot(), t);
        tp.removePartition(part); // keep on truckin; can happen if directory is empty
      }
    }   // loop over component grib collections

    try {
      boolean changed = updatePartition(isGrib1, tp, updateType, logger, errlog);

      long took = System.currentTimeMillis() - start;
      errlog.format(" INFO updateTimePartition %s took %d msecs%n", tp.getRoot(), took);
      logger.debug("GribCdmIndex.updateTimePartition complete (%s) on %s errlog=%s%n", changed, tp.getRoot(), errlog);
      return changed;

    } catch (IllegalStateException t) {
      logger.warn("Error making partition {} '{}'", tp.getRoot(), t.getMessage());
      return false;

    } catch (Throwable t) {
      logger.error("Error making partition " + tp.getRoot(), t);
      return false;
    }
  }

  private static boolean isUpdateNeeded(String idxFilenameOrg, CollectionUpdateType updateType, GribCollectionType wantType, Logger logger) {
    if (updateType == CollectionUpdateType.never) return false;

    // see if index already exists
    File collectionIndexFile = GribIndexCache.getExistingFileOrCache(idxFilenameOrg);
    if (collectionIndexFile != null) {   // it exists

      boolean bad;
      try (RandomAccessFile raf = RandomAccessFile.acquire(collectionIndexFile.getPath())) {  // read it to verify its good
        GribCollectionType type = getType(raf);
        bad = (type != wantType);
        if (!bad && updateType == CollectionUpdateType.nocheck) return false;  // use if index is ok

      } catch (IOException ioe) {
        bad = true;
      }

      if (bad) { // delete the file and remove from cache if its in there
        RandomAccessFile.eject(collectionIndexFile.getPath());
        if (!collectionIndexFile.delete())
          logger.warn("failed to delete {}", collectionIndexFile.getPath());
      }
    }

    return true;
  }

  private static boolean updateDirectoryCollectionRecurse(boolean isGrib1, DirectoryPartition dpart,
                                                          FeatureCollectionConfig config,
                                                          CollectionUpdateType updateType,
                                                          Logger logger) throws IOException {

    logger.debug("GribCdmIndex.updateDirectoryCollectionRecurse %s %s%n", dpart.getRoot(), updateType);
    if (!isUpdateNeeded(dpart.getIndexFilename(NCX_SUFFIX), updateType, (isGrib1 ? GribCollectionType.Partition1 : GribCollectionType.Partition2), logger)) return false;

    long start = System.currentTimeMillis();

    // check the children partitions first
    if (updateType != CollectionUpdateType.testIndexOnly) {   // skip children on testIndexOnly
      for (MCollection part : dpart.makePartitions(updateType)) {
        part.putAuxInfo(FeatureCollectionConfig.AUX_CONFIG, config);
        try {
          if (part instanceof DirectoryPartition) {   // LOOK if child partition fails, the parent partition doesnt know that - suckage
            updateDirectoryCollectionRecurse(isGrib1, (DirectoryPartition) part, config, updateType, logger);
          } else {
            Path partPath = Paths.get(part.getRoot());
            updateLeafCollection(isGrib1, config, updateType, false, logger, partPath); // LOOK why not using part ??
          }
        } catch (IllegalStateException t) {
          logger.warn("Error making partition {} '{}'", part.getRoot(), t.getMessage());
          dpart.removePartition(part); // keep on truckin; can happen if directory is empty

        } catch (Throwable t) {
          logger.error("Error making partition " + part.getRoot(), t);
          dpart.removePartition(part);
        }
      }   // loop over partitions
    }

    try {
      // update the partition
      Formatter errlog = new Formatter();
      boolean changed = updatePartition(isGrib1, dpart, updateType, logger, errlog);

      long took = System.currentTimeMillis() - start;
      errlog.format(" INFO updateDirectoryCollectionRecurse %s took %d msecs%n", dpart.getRoot(), took);
      logger.debug("GribCdmIndex.updateDirectoryCollectionRecurse complete (%s) on %s errlog=%s%n", changed, dpart.getRoot(), errlog);
      return changed;

    } catch (IllegalStateException t) {
      logger.warn("Error making partition {} '{}'", dpart.getRoot(), t.getMessage());
      return false;

    } catch (Throwable t) {
      logger.error("Error making partition " + dpart.getRoot(), t);
      return false;
    }
  }

  /**
   * Update all the gbx indices in one directory, and the ncx index for that directory
   *
   * @param config  FeatureCollectionConfig
   * @param dirPath directory path
   * @return true if collection was rewritten, exception on failure
   */
  private static boolean updateLeafCollection(boolean isGrib1, FeatureCollectionConfig config,
                                              CollectionUpdateType updateType, boolean isTop,
                                              Logger logger, Path dirPath) throws IOException {

    if (config.ptype == FeatureCollectionConfig.PartitionType.file) {
      return updateFilePartition(isGrib1, config, updateType, isTop, logger, dirPath);

    } else {
      Formatter errlog = new Formatter();
      CollectionSpecParser specp = config.getCollectionSpecParser(errlog);

      try (DirectoryCollection dcm = new DirectoryCollection(config.collectionName, dirPath, isTop, config.olderThan, logger)) {
        dcm.putAuxInfo(FeatureCollectionConfig.AUX_CONFIG, config);
        if (specp.getFilter() != null)
          dcm.setStreamFilter(new StreamFilter(specp.getFilter(), specp.getFilterOnName()));

        boolean changed = updateGribCollection(isGrib1, dcm, updateType, FeatureCollectionConfig.PartitionType.directory, logger, errlog);
        logger.debug("  GribCdmIndex.updateDirectoryPartition was updated=%s on %s%n", changed, dirPath);
        return changed;
      }
    }

  }

  /**
   * File Partition: each File is a collection of Grib records, and the collection of all files in the directory is a PartitionCollection.
   * Rewrite the PartitionCollection and optionally its children
   *
   * @param config     FeatureCollectionConfig
   * @param updateType always, test, nocheck, never
   * @return true if partition was rewritten, exception on failure
   */
  private static boolean updateFilePartition(final boolean isGrib1, final FeatureCollectionConfig config,
                                             final CollectionUpdateType updateType, boolean isTop,
                                             final Logger logger, Path dirPath) throws IOException {
    long start = System.currentTimeMillis();
    final Formatter errlog = new Formatter();
    CollectionSpecParser specp = config.getCollectionSpecParser(errlog);

    try (FilePartition partition = new FilePartition(config.collectionName, dirPath, isTop, config.olderThan, logger)) {
      partition.putAuxInfo(FeatureCollectionConfig.AUX_CONFIG, config);
      if (specp.getFilter() != null)
        partition.setStreamFilter(new StreamFilter(specp.getFilter(), specp.getFilterOnName()));

      logger.debug("GribCdmIndex.updateFilePartition %s %s%n", partition.getCollectionName(), updateType);
      if (!isUpdateNeeded(partition.getIndexFilename(NCX_SUFFIX), updateType, (isGrib1 ? GribCollectionType.Partition1 : GribCollectionType.Partition2), logger))
        return false;

      final AtomicBoolean anyChange = new AtomicBoolean(false); // just need a mutable boolean we can declare final

      // redo the children here
      if (updateType != CollectionUpdateType.testIndexOnly) {   // skip children on testIndexOnly
        partition.iterateOverMFileCollection(mfile -> {
          MCollection part = new CollectionSingleFile(mfile, logger);
          part.putAuxInfo(FeatureCollectionConfig.AUX_CONFIG, config);

          try {
            boolean changed = updateGribCollection(isGrib1, part, updateType, FeatureCollectionConfig.PartitionType.file, logger, errlog);
            if (changed) anyChange.set(true);

          } catch (IllegalStateException t) {
            logger.warn("Error making partition {} '{}'", part.getRoot(), t.getMessage());
            partition.removePartition(part); // keep on truckin; can happen if directory is empty

          } catch (Throwable t) {
            logger.error("Error making partition " + part.getRoot(), t);
            partition.removePartition(part);
          }
        });
      }

      // LOOK what if theres only one file?

      try {
        // redo partition index if needed, will detect if children have changed
        boolean recreated = updatePartition(isGrib1, partition, updateType, logger, errlog);

        long took = System.currentTimeMillis() - start;
        if (recreated) logger.info("RewriteFilePartition {} took {} msecs", partition.getCollectionName(), took);
        return recreated;

      } catch (IllegalStateException t) {
        logger.warn("Error making partition {} '{}'", partition.getRoot(), t.getMessage());
        return false;

      } catch (Throwable t) {
        logger.error("Error making partition " + partition.getRoot(), t);
        return false;
      }
    }
  }

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

  // DirectoryPartitionViewer
  public static boolean makeIndex(FeatureCollectionConfig config, Formatter errlog, Path topPath) throws IOException {
    return false;
    /* GribCdmIndex indexReader = new GribCdmIndex();
     MCollection dpart = DirectoryBuilder.factory(config, topPath, indexReader, logger);
     dpart.putAuxInfo(FeatureCollectionConfig.AUX_CONFIG, config);

     if (dpart.isLeaf()) {
       return Grib2TimePartitionBuilder.makePartitionIndex((PartitionManager) dpart, errlog, logger);

     } else {
       return Grib2CollectionBuilder.makeIndex(dpart, errlog, logger);
     }  */
  }

  // move index to be a directory partition
  public static boolean moveCdmIndex(String indexFilename, Logger logger) throws IOException {
    return false;
    /* RandomAccessFile raf = new RandomAccessFile(indexFilename, "r");
    RandomAccessFile newRaf = new RandomAccessFile(indexFilename + ".copy", "rw");

    try {
      raf.seek(0);
      byte[] b = new byte[Grib2CollectionBuilder.MAGIC_START.getBytes().length]; // they are all the same
      raf.read(b);
      String magic = new String(b, CDM.utf8Charset);

      GribCdmIndex gci = new GribCdmIndex();

      switch (magic) {
        case Grib1CollectionBuilder.MAGIC_START:
        case Grib2CollectionBuilder.MAGIC_START:
          return gci.moveIndex(raf, newRaf, logger);

        /* case Grib1CollectionBuilder.MAGIC_START:
          gc = Grib1CollectionBuilder.createFromIndex(indexFile, null, raf, null, logger);
          break;
        case Grib2TimePartitionBuilder.MAGIC_START:
          gc = Grib2TimePartitionBuilder.createFromIndex(indexFile, null, raf, logger);
          break;
        case Grib1TimePartitionBuilder.MAGIC_START:
          gc = Grib1TimePartitionBuilder.createFromIndex(indexFile, null, raf, logger);
          break; //
      }
    } catch (Throwable t) {
      raf.close();
      throw t;

    } finally {
      if (raf != null) raf.close();
      if (newRaf != null) newRaf.close();
    }

    return false;   */
  }

  /*
   * Directory Collection: the collection of all files in the directory is a DirectoryCollection.
   * Rewrite the DirectoryCollection in one Directory
   *
   * @param config       FeatureCollectionConfig
   * @param forceCollection       always, test, nocheck, never
   * @param forceChildren         always, test, nocheck, never
   * @throws IOException
   */
  /* public static boolean rewriteDirectoryCollection(final FeatureCollectionConfig config,
                                              final CollectionUpdateType forceCollection,
                                              final CollectionUpdateType forceChildren,
                                              final Logger logger) throws IOException {
    long start = System.currentTimeMillis();
    final Formatter errlog = new Formatter();

    int pos = config.spec.lastIndexOf("/");
    Path dirPath = Paths.get(config.spec.substring(0,pos));

    DirectoryCollection dirCollection = new DirectoryCollection(config.name, dirPath, logger);
    String collectionName = DirectoryCollection.makeCollectionName(config.name, dirPath);

    if (forceChildren != CollectionUpdateType.never) {
      dirCollection.iterateOverMFileCollection(new DirectoryCollection.Visitor() {
        public void consume(MFile mfile) {
          try (GribCollection gcNested =
                       Grib2CollectionBuilder.readOrCreateIndexFromSingleFile(mfile, forceChildren, config.gribConfig, errlog, logger)) {
          } catch (IOException e) {
            logger.error("rewriteIndexesFilesAndCollection", e);
          }
        }
      });
    }

    // redo partition index
    dirCollection.putAuxInfo(FeatureCollectionConfig.AUX_GRIB_CONFIG, config.gribConfig);
    try (GribCollection gcNew = makeGribCollectionFromMCollection(false, dirCollection, forceCollection, errlog, logger)) {
    }

    long took = System.currentTimeMillis() - start;
    logger.info("RewriteDirectoryPartition {} took {} msecs", collectionName, took);
    return true;
  }  */

  ///////////////////////////////////////////////////////
  // used by InvDatasetFcGrib, Grib2CollectionPanel

  /**
   * Open GribCollection from config.
   * CollectionUpdater calls InvDatasetFc.update() calls InvDatasetFcGrib.updateCollection()
   */
  public static GribCollectionImmutable openGribCollection(FeatureCollectionConfig config, CollectionUpdateType updateType, Logger logger) throws IOException {

    // update if needed
    boolean changed = updateGribCollection(config, updateType, logger);

    File idxFile = makeTopIndexFileFromConfig(config);

    // If call to updateGribCollection shows a change happened, then collection changed.
    // If updateType is never (tds is in charge of updating, not TDM or some other external application),
    // then this is being called after receiving an outside trigger. Assume collection changed.
    //
    // At this point, there isn't a good way of invalidating the gribColectionCache entries associated with the
    // particular collection being updated, so we have to clear the whole cache. Will revisit this in
    // 5.0 if performance is an issue
    if ((updateType == CollectionUpdateType.never) || changed) {
      gribCollectionCache.clearCache(true);
    }

    return openCdmIndex(idxFile.getPath(), config, true, logger);
  }

  ////////////////////////////////////////////////////////////////////////////////////
  // Used by IOSPs

  public static GribCollectionImmutable openGribCollectionFromRaf(RandomAccessFile raf, FeatureCollectionConfig config,
                CollectionUpdateType updateType, org.slf4j.Logger logger) throws IOException {

    GribCollectionImmutable result;

    // check if its a plain ole GRIB1/2 data file
    boolean isGrib1 = false;
    boolean isGrib2 = Grib2RecordScanner.isValidFile(raf);
    if (!isGrib2) isGrib1 = Grib1RecordScanner.isValidFile(raf);

    if (isGrib1 || isGrib2) {
      result = openGribCollectionFromDataFile(isGrib1, raf, config, updateType, null, logger);
      // close the data file, the ncx raf file is managed by gribCollection
      raf.close();

    } else  {  // check its an ncx file
      result = openGribCollectionFromIndexFile(raf, config, logger);
    }

    return result;
  }

  /**
   * Open a grib collection from a single grib1 or grib2 file.
   * Create the gbx9 and ncx2 files if needed.
   *
   * @param isGrib1    true if grib1
   * @param dataRaf    the data file already open
   * @param config     special configuration
   * @param updateType force writing index
   * @return the resulting GribCollection
   * @throws IOException on io error
   */
  private static GribCollectionImmutable openGribCollectionFromDataFile(boolean isGrib1, RandomAccessFile dataRaf, FeatureCollectionConfig config,
                                                                        CollectionUpdateType updateType, Formatter errlog, org.slf4j.Logger logger) throws IOException {

    String filename = dataRaf.getLocation();
    File dataFile = new File(filename);

    MFile mfile = new MFileOS(dataFile);
    return openGribCollectionFromDataFile(isGrib1, mfile, updateType, config, errlog, logger);
  }

  // for InvDatasetFeatureCollection.getNetcdfDataset() and getGridDataset()

  // from a single file, read in the index, create if it doesnt exist; return null on failure
  @Nullable
  public static GribCollectionImmutable openGribCollectionFromDataFile(boolean isGrib1, MFile mfile, CollectionUpdateType updateType,
                                                                       FeatureCollectionConfig config, Formatter errlog, org.slf4j.Logger logger) throws IOException {

    MCollection dcm = new CollectionSingleFile(mfile, logger);
    dcm.putAuxInfo(FeatureCollectionConfig.AUX_CONFIG, config);
    if (isGrib1) {
      Grib1CollectionBuilder builder = new Grib1CollectionBuilder(dcm.getCollectionName(), dcm, logger);       // LOOK ignoring partition type
      boolean changed = (builder.updateNeeded(updateType) && builder.createIndex(FeatureCollectionConfig.PartitionType.file, errlog));
    } else {
      Grib2CollectionBuilder builder = new Grib2CollectionBuilder(dcm.getCollectionName(), dcm, logger);
      boolean changed = (builder.updateNeeded(updateType) && builder.createIndex(FeatureCollectionConfig.PartitionType.file, errlog));
    }

    // the index file should now exist, open it
    GribCollectionImmutable result = openCdmIndex(dcm.getIndexFilename(NCX_SUFFIX), config, true, logger);
    if (result != null) return result;

    // if open fails, force recreate the index
    if (updateType == CollectionUpdateType.never) return null; // not allowed to write
    if (updateType == CollectionUpdateType.always) return null;// already tried to force write, give up
    return openGribCollectionFromDataFile(isGrib1, mfile, CollectionUpdateType.always, config, errlog, logger);
  }

  /**
   * Create a grib collection / partition collection from an existing ncx2 file.
   * PartionCollection.partition.getGribCollection().
   *
   * @param indexRaf the ncx2 file already open
   * @param config   special configuration
   * @return the resulting GribCollection, or null on failure
   * @throws IOException on io error
   */
  @Nullable
  public static GribCollectionImmutable openGribCollectionFromIndexFile(RandomAccessFile indexRaf, FeatureCollectionConfig config,
                                                                        org.slf4j.Logger logger) throws IOException {

    GribCollectionType type = getType(indexRaf);

    String location = indexRaf.getLocation();
    File f = new File(location);
    int pos = f.getName().lastIndexOf(".");
    String name = (pos > 0) ? f.getName().substring(0, pos) : f.getName(); // remove ".ncx2"

    switch (type) {
      case Partition1:
        return Grib1PartitionBuilderFromIndex.createTimePartitionFromIndex(name, indexRaf, config, logger);
      case GRIB1:
        return Grib1CollectionBuilderFromIndex.readFromIndex(name, indexRaf, config, logger);
      case Partition2:
        return Grib2PartitionBuilderFromIndex.createTimePartitionFromIndex(name, indexRaf, config, logger);
      case GRIB2:
        return Grib2CollectionBuilderFromIndex.readFromIndex(name, indexRaf, config, logger);
    }

    return null;
  }

  /////////////////////////////////////////////////////////////////////////////////////
  // manipulate the ncx without building a gc
  private static final boolean debug = false;
  private GribCollectionProto.GribCollection gribCollectionIndex;
  private final Logger logger;

  public GribCdmIndex(Logger logger) {
    this.logger = logger;
  }

  /// IndexReader interface
  @Override
  public boolean readChildren(Path indexFile, AddChildCallback callback) throws IOException {
    logger.debug("GribCdmIndex.readChildren %s%n", indexFile);
    try (RandomAccessFile raf = RandomAccessFile.acquire(indexFile.toString())) {
      GribCollectionType type = getType(raf);
      if (type == GribCollectionType.Partition1 || type == GribCollectionType.Partition2) {
        if (openIndex(raf, logger)) {
          String topDir = gribCollectionIndex.getTopDir();
          int n = gribCollectionIndex.getMfilesCount(); // partition index files stored in MFiles
          for (int i = 0; i < n; i++) {
            GribCollectionProto.MFile mfilep = gribCollectionIndex.getMfiles(i);
            callback.addChild(topDir, mfilep.getFilename(), mfilep.getLastModified());
          }
          return true;
        }
      }
      return false;
    }
  }

  @Override
  public boolean isPartition(Path indexFile) throws IOException {
    logger.debug("GribCdmIndex.isPartition %s%n", indexFile);
    try (RandomAccessFile raf = RandomAccessFile.acquire(indexFile.toString())) {
      GribCollectionType type = getType(raf);
      return (type == GribCollectionType.Partition1) || (type == GribCollectionType.Partition2);
    }
  }

  @Override
  public boolean readMFiles(Path indexFile, List<MFile> result) throws IOException {
    logger.debug("GribCdmIndex.readMFiles %s%n", indexFile);
    try (RandomAccessFile raf = RandomAccessFile.acquire(indexFile.toString())) {
      // GribCollectionType type = getType(raf);
      // if (type == GribCollectionType.GRIB1 || type == GribCollectionType.GRIB2) {
      if (openIndex(raf, logger)) {
        File protoDir = new File(gribCollectionIndex.getTopDir());
        int n = gribCollectionIndex.getMfilesCount();
        for (int i = 0; i < n; i++) {
          GribCollectionProto.MFile mfilep = gribCollectionIndex.getMfiles(i);
          result.add(new GcMFile(protoDir, mfilep.getFilename(), mfilep.getLastModified(), mfilep.getLength(), mfilep.getIndex()));
        }
      }
      return true;
      //}
    }
    //return false;
  }

  private boolean openIndex(RandomAccessFile indexRaf, Logger logger) {
    try {
      indexRaf.order(RandomAccessFile.BIG_ENDIAN);
      indexRaf.seek(0);

      //// header message
      byte[] magic = new byte[Grib2CollectionWriter.MAGIC_START
          .getBytes(CDM.utf8Charset).length];   // they are all the same
      indexRaf.readFully(magic);

      int version = indexRaf.readInt();

      long recordLength = indexRaf.readLong();
      if (recordLength > Integer.MAX_VALUE) {
        logger.error("Grib2Collection {}: invalid recordLength size {}", indexRaf.getLocation(), recordLength);
        return false;
      }
      indexRaf.skipBytes(recordLength);

      int size = NcStream.readVInt(indexRaf);
      if ((size < 0) || (size > 100 * 1000 * 1000)) {
        logger.warn("GribCdmIndex {}: invalid index size {}", indexRaf.getLocation(), size);
        return false;
      }

      byte[] m = new byte[size];
      indexRaf.readFully(m);
      gribCollectionIndex = GribCollectionProto.GribCollection.parseFrom(m);
      return true;

    } catch (Throwable t) {
      logger.error("Error reading index " + indexRaf.getLocation(), t);
      return false;
    }
  }

  public static void main2(String[] args) throws IOException {
    org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger("test");
    PartitionManager partition = new PartitionManagerFromIndexDirectory("NCDC-gfs4_all", new FeatureCollectionConfig(), new File("B:/ncdc/gfs4_all/"), NCX_SUFFIX, logger);
    Grib1PartitionBuilder builder = new Grib1PartitionBuilder("NCDC-gfs4_all", new File(partition.getRoot()), partition, logger);
    builder.createPartitionedIndex(CollectionUpdateType.nocheck, new Formatter());
  }

  public static void main3(String[] args) throws IOException {
    org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger("test");

    /*
       <featureCollection name="DGEX-CONUS_12km" featureType="GRIB" harvest="true" path="grib/NCEP/DGEX/CONUS_12km">
                  <collection spec="F:/data/grib/idd/dgex/  /.*grib2$"
                    dateFormatMark="#DGEX_CONUS_12km_#yyyyMMdd_HHmm"
                    timePartition="directory"
                    olderThan="5 min"/>
     */

    // String name, String path, FeatureCollectionType fcType, String spec, String dateFormatMark, String olderThan, String timePartition, String useIndexOnlyS, Element innerNcml
    //FeatureCollectionConfig config = new FeatureCollectionConfig("DGEX-test", "grib/NCEP/DGEX/CONUS_12km", FeatureCollectionType.GRIB2,
    //        "Q:/cdmUnitTest/gribCollections/dgex/**/.*grib2$", "#DGEX_CONUS_12km_#yyyyMMdd_HHmm", null, "directory", null, null);

    //  FeatureCollectionConfig config = new FeatureCollectionConfig("GFS_CONUS_80km", "grib/NCEP/GFS/CONUS_80km", FeatureCollectionType.GRIB1,
    //          "Q:/cdmUnitTest/ncss/GFS/CONUS_80km/GFS_CONUS_80km_#yyyyMMdd_HHmm#.grib1", null, null, "file", null, null);

    //FeatureCollectionConfig config = new FeatureCollectionConfig("ds083.2_Aggregation", "ds083.2/Aggregation", FeatureCollectionType.GRIB1,
    //         "Q:/cdmUnitTest/gribCollections/rdavm/ds083.2/grib1/**/.*grib1", "#fnl_#yyyyMMdd_HH_mm", null, "directory", null, null);
    /*
    <pdsHash>
       <useTableVersion>false</useTableVersion>
     </pdsHash>
     */

    FeatureCollectionConfig config = new FeatureCollectionConfig("RFC", "grib/NPVU/RFC", FeatureCollectionType.GRIB1,
            "B:/motherlode/rfc/**/.*grib1$", null, "yyyyMMdd#.grib1#", null, "directory", null);

    // config.gribConfig.pdsHash.put("useTableVersion", false);

    // boolean isGrib1, MCollection dcm, CollectionUpdateType updateType, Formatter errlog, org.slf4j.Logger logger
    boolean changed = GribCdmIndex.updateGribCollection(config, CollectionUpdateType.test, logger);
  }

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

  private static class CommandLine {
    @Parameter(names = {"-fc", "--featureCollection"}, description = "Input XML file containing <featureCollection> root element", required = true)
    File inputFile;

    @Parameter(names = {"-update", "--CollectionUpdateType"}, description = "Collection Update Type")
    final
    CollectionUpdateType updateType = CollectionUpdateType.always;

    @Parameter(names = {"-h", "--help"}, description = "Display this help and exit", help = true)
    public final boolean help = false;

    public class CollectionUpdateTypeConverter implements IStringConverter<CollectionUpdateType> {
      @Override
      public CollectionUpdateType convert(String value) {
        return CollectionUpdateType.valueOf(value);
      }
    }

    private static class ParameterDescriptionComparator implements Comparator<ParameterDescription> {
      // Display parameters in this order in the usage information.
      private final List<String> orderedParamNames = Arrays.asList("--featureCollection", "--CollectionUpdateType", "--help");

      @Override
      public int compare(ParameterDescription p0, ParameterDescription p1) {
        int index0 = orderedParamNames.indexOf(p0.getLongestName());
        int index1 = orderedParamNames.indexOf(p1.getLongestName());
        assert index0 >= 0 : "Unexpected parameter name: " + p0.getLongestName();
        assert index1 >= 0 : "Unexpected parameter name: " + p1.getLongestName();

        return Integer.compare(index0, index1);
      }
    }

    private final JCommander jc;

    public CommandLine(String progName, String[] args) throws ParameterException {
      this.jc = new JCommander(this, args);  // Parses args and uses them to initialize *this*.
      jc.setProgramName(progName);           // Displayed in the usage information.

      // Set the ordering of parameters in the usage information.
      jc.setParameterDescriptionComparator(new ParameterDescriptionComparator());
    }

    public void printUsage() {
      jc.usage();
    }
  }

  public static void main(String[] args) throws Exception {
    String progName = GribCdmIndex.class.getName();

    try {
      CommandLine cmdLine = new CommandLine(progName, args);

      if (cmdLine.help) {
        cmdLine.printUsage();
        return;
      }

      Formatter errlog = new Formatter();
      try {
        FeatureCollectionConfigBuilder reader = new FeatureCollectionConfigBuilder(errlog);
        String location = cmdLine.inputFile.getAbsolutePath();
        FeatureCollectionConfig config = reader.readConfigFromFile(location);
        boolean changed = GribCdmIndex.updateGribCollection(config, cmdLine.updateType, classLogger);
        System.out.printf("changed = %s%n", changed);

      } catch (Exception e) {
        System.out.printf("%s = %s %n", e.getClass().getName(), e.getMessage());
        String err = errlog.toString();
        if (err.length() > 0)
          System.out.printf(" errlog=%s%n", err);
        e.printStackTrace();
      }

    } catch (ParameterException e) {
      System.err.println(e.getMessage());
      System.err.printf("Try \"%s --help\" for more information.%n", progName);
    }
  }

}


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

package ucar.nc2.util.rc;

import ucar.nc2.constants.CDM;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;

public class RC {
  static boolean showlog = false; /* do not do any logging */
  static public org.slf4j.Logger log
          = org.slf4j.LoggerFactory.getLogger(RC.class);

//////////////////////////////////////////////////
// Predefined flags
// To add a new flag:
// 1. choose a name for the flag
// 2. Define the protected static field with default value
// 3. Define a get function
// 4. Add an arm to the set function
// 5. Add any usefull utilities like booleanize()

  static final public String USEGROUPSKEY = "ucar.nc2.cdm.usegroups";
  static final public String VERIFYSERVERKEY = "ucar.nc2.net.verifyserver";
  static final public String ALLOWSELFSIGNEDKEY = "ucar.nc2.net.allowselfsigned";

  static protected boolean useGroups = true;
  static protected boolean verifyServer = false;
  static protected boolean allowSelfSigned = true;

  static public boolean getUseGroups() {
    if (!initialized) RC.initialize();
    return useGroups;
  }

  static public boolean getVerifyServer() {
    if (!initialized) RC.initialize();
    return verifyServer;
  }

  static public boolean getAllowSelfSigned() {
    if (!initialized) RC.initialize();
    return allowSelfSigned;
  }

  static public void set(String key, String value) {
    // TODO: think about the rc properties naming hierarchy
    assert (key != null);
    if (USEGROUPSKEY.equals(key)) {
      useGroups = booleanize(value);
    } else if (VERIFYSERVERKEY.equals(key)) {
      verifyServer = booleanize(value);
    } else if (ALLOWSELFSIGNEDKEY.equals(key)) {
      allowSelfSigned = booleanize(value);
    }
  }

  static boolean
  booleanize(String value) {
    // canonical boolean values
    if (value == null || "0".equals(value) || "false".equalsIgnoreCase(value))
      return false;
    if (value.length() == 0 || "1".equals(value) || "true".equalsIgnoreCase(value))
      return true;
    return value != null; // any non-null value?
  }

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

  static final String DFALTRCFILE = ".threddsrc";

  static final char LTAG = '[';
  static final char RTAG = ']';

  static final String[] rcfilelist = new String[]{".dodsrc", ".tdsrc"};

  static int urlCompare(URL u1, URL u2) {
    int relation;
    if (u1 == null && u2 == null) return 0;
    if (u1 == null) return -1;
    if (u2 == null) return +1;
    // 1. host test
    String host1 = (new StringBuilder(u1.getHost())).reverse().toString();
    String host2 = (new StringBuilder(u2.getHost())).reverse().toString();
    // Use lexical order on the reversed host names
    relation = host1.compareTo(host2);
    if (relation != 0) return relation;
    // 2. path test
    relation = (u1.getPath().compareTo(u2.getPath()));
    if (relation != 0) return relation;
    // 3. port number
    relation = (u1.getPort() - u2.getPort());
    if (relation != 0) return relation;
    // 4. note: all other fields are ignored
    return 0;
  }

  // Match has different semantics than urlCompare
  static boolean
  urlMatch(URL pattern, URL url) {
    int relation;

    if (pattern == null)
      return (url == null);

    if (!(url.getHost().endsWith(pattern.getHost())))
      return false; // e.g. pattern=x.y.org url=y.org

    if (!(url.getPath().startsWith(pattern.getPath())))
      return false; // e.g. pattern=y.org/a/b url=y.org/a

    if (pattern.getPort() > 0 && pattern.getPort() != url.getPort())
      return false;

    // note: all other fields are ignored

    return true;
  }

  static public class Triple implements Comparable {
    public String key; // also sort key
    public String value;
    public URL url;

    public Triple(String key, String value, String url) {
      URL u = null;
      if (url != null && url.length() > 0)
        try {
          u = new URL(url);
        } catch (MalformedURLException e) {
          u = null;
        }
      set(key, value, u);
    }

    public Triple(String key, String value, URL url) {
      set(key, value, url);
    }

    void set(String key, String value, URL url) {
      this.key = key.trim().toLowerCase();
      this.url = url;
      this.value = value;
      if (this.value == null) this.value = "";
    }

    public boolean equals(Object o) {
      if (o == null || !(o instanceof Triple)) return false;
      return (compareTo((Triple) o) == 0);
    }

    public int compareTo(Object o) {
      if (o == null) throw new NullPointerException();
      Triple t = (Triple) o;
      int relation = key.compareTo(t.key);
      if (relation != 0) return relation;
      relation = urlCompare(this.url, t.url);
      return relation;
    }

    // toString produces an rc line
    public String toString() {
      StringBuilder line = new StringBuilder();
      if (url != null) {
        line.append("[");
        line.append(url.toString());
        line.append("]");
      }
      line.append(key);
      line.append("=");
      line.append(value);
      return line.toString();
    }
  }

  // Define a singlton RC instance for general global use
  static RC dfaltRC = null;

  static private boolean initialized = false;

  static {
    RC.initialize();
  }

  static synchronized public void initialize() {
    if (!initialized) {
      initialized = true;
      RC.loadDefaults();
      RC.setWellKnown();
      RC.loadFromJava();
    }
  }

  /**
   * Allow users to add to the default rc
   *
   * @param key
   * @param value
   * @param url   null => not url specific
   */
  static synchronized public void
  add(String key, String value, String url) {
    if (key == null) return;
    if (!initialized) RC.initialize();
    Triple t = new Triple(key, value, url);
    dfaltRC.insert(t);
    // recompute well-knowns
    setWellKnown();
  }

  /**
   * Allow users to search the default rc
   *
   * @param key
   * @param url null => not url specific
   * @return value corresponding to key+url, or null if does not exist
   */
  static synchronized public String
  find(String key, String url) {
    if (key == null) return null;
    if (!initialized) RC.initialize();
    Triple t = dfaltRC.lookup(key, url);
    return (t == null ? null : t.value);
  }

  /**
   * Record some well known parameters
   */
  static void
  setWellKnown() {
    if (dfaltRC.triplestore.size() == 0) return;
    // Walk the set of triples looking for those that have no url
    for (String key : dfaltRC.keySet()) {
      Triple triple = dfaltRC.lookup(key);
      if (triple.url == null) {
        RC.set(key, triple.value); // let set sort it out
      }
    }
  }

  static void
  loadDefaults() {
    RC rc0 = new RC();
    String[] locations = new String[]{
            System.getProperty("user.home"),
            System.getProperty("user.dir"),
    };

    boolean found1 = false;
    for (String loc : locations) {
      if (loc == null) continue;
      String dir = loc.replace('\\', '/');
      if (dir.endsWith("/")) dir = dir.substring(0, dir.length() - 1);
      for (String rcpath : rcfilelist) {
        String filepath = loc + "/" + rcpath;
        if (rc0.load(filepath)) found1 = true;
      }
    }
    if (!found1)
      if (showlog) log.debug("No .rc file found");
    dfaltRC = rc0;
  }

  static void
  loadFromJava() {
    String[] flags = new String[]{
            USEGROUPSKEY,
            VERIFYSERVERKEY,
            ALLOWSELFSIGNEDKEY
    };
    for (String flag : flags) {
      String value = System.getProperty(flag);
      if (value != null) {
        set(flag, value);
      }
    }
  }

  static RC getDefault() {
    return dfaltRC;
  }

//////////////////////////////////////////////////
// Instance Data

  Map<String, List<Triple>> triplestore;

//////////////////////////////////////////////////
// constructors

  public RC() {
    triplestore = new HashMap<String, List<Triple>>();
  }

//////////////////////////////////////////////////
// Loaders

// Load this triple store from an rc file
// overwrite existing entries

  public boolean
  load(String abspath) {
    abspath = abspath.replace('\\', '/');
    File rcFile = new File(abspath);
    if (!rcFile.exists() || !rcFile.canRead()) {
      return false;
    }
    if (showlog) log.debug("Loading rc file: " + abspath);
    try (BufferedReader rdr = new BufferedReader(new InputStreamReader(new FileInputStream(rcFile), CDM.UTF8))) {
        for (int lineno = 1; ; lineno++) {
          URL url = null;
          String line = rdr.readLine();
          if (line == null) break;
          // trim leading blanks
          line = line.trim();
          if (line.length() == 0) continue; // empty line
          if (line.charAt(0) == '#') continue; // check for comment
          // parse the line
          if (line.charAt(0) == LTAG) {
            int rindex = line.indexOf(RTAG);
            if (rindex < 0) return false;
            if (showlog) log.error("Malformed [url] at " + abspath + "." + lineno);
            String surl = line.substring(1, rindex);
            try {
              url = new URL(surl);
            } catch (MalformedURLException mue) {
              if (showlog) log.error("Malformed [url] at " + abspath + "." + lineno);
            }
            line = line.substring(rindex + 1);
            // trim again
            line = line.trim();
          }
          // Get the key,value part
          String[] pieces = line.split("\\s*=\\s*");
          assert (pieces.length == 1 || pieces.length == 2);
          // Create the triple
          String value = "1";
          if (pieces.length == 2) value = pieces[1].trim();
          Triple triple = new Triple(pieces[0].trim(), value, url);
          List<Triple> list = triplestore.get(triple.key);
          if (list == null) list = new ArrayList<Triple>();
          Triple prev = addtriple(list, triple);
          triplestore.put(triple.key, list);
        }

      } catch (FileNotFoundException fe) {
        if (showlog) log.debug("Loading rc file: " + abspath);
        return false;

      } catch (IOException ioe) {
        if (showlog) log.error("File " + abspath + ": IO exception: " + ioe.getMessage());
        return false;
      }
      return true;
    }

    public Set<String> keySet () {
      return triplestore.keySet();
    }

    public List<Triple> getTriples (String key)
    {
      List<Triple> list = triplestore.get(key);
      if (list == null) list = new ArrayList<Triple>();
      return list;
    }

  public Triple lookup(String key) {
    return lookup(key, (URL) null);
  }

  public Triple lookup(String key, String url) {
    if (url == null || url.length() == 0)
      return lookup(key);
    try {
      URL u = new URL(url);
      return lookup(key, u);
    } catch (MalformedURLException m) {
    }
    return null;
  }

  public Triple lookup(String key, URL url) {
    List<Triple> list = triplestore.get(key);
    if (list == null) return null;
    if (url == null) {
      if (list.size() == 0) return null;
      return list.get(0);
    } else for (Triple t : list) {
      if (urlMatch(t.url, url)) return t;
    }
    return null;
  }

  Triple
  addtriple(List<Triple> list, Triple triple) {
    Triple prev = null;
    assert (list != null);
    // Look for duplicates
    int i = list.indexOf(triple);
    if (i >= 0) {
      prev = list.remove(i);
    }
    list.add(triple);
    Collections.sort(list);
    return prev;
  }

  // Allow for external loading
  public Triple
  insert(Triple t) {
    if (t.key == null) return null;
    List<Triple> list = triplestore.get(t.key);
    if (list == null) list = new ArrayList<Triple>();
    Triple prev = addtriple(list, t);
    triplestore.put(t.key, list);
    return prev;
  }

  // Output in .rc form
  public String
  toString() {
    StringBuilder rc = new StringBuilder();
    for (String key : keySet()) {
      List<Triple> list = getTriples(key);
      for (Triple triple : list) {
        String line = triple.toString();
        rc.append(line);
        rc.append("\n");
      }
    }
    return rc.toString();
  }

} // class RC


/////////////////////////////////////////////////////////////////////////////
// This file is part of the "Java-DAP" project, a Java implementation
// of the OPeNDAP Data Access Protocol.
//
// Copyright (c) 2010, OPeNDAP, Inc.
// Copyright (c) 2002,2003 OPeNDAP, Inc.
//
// Author: James Gallagher <jgallagher@opendap.org>
//
// All rights reserved.
//
// Redistribution and use in source and binary forms,
// with or without modification, are permitted provided
// that the following conditions are met:
//
// - Redistributions of source code must retain the above copyright
//   notice, this list of conditions and the following disclaimer.
//
// - Redistributions in binary form must reproduce the above copyright
//   notice, this list of conditions and the following disclaimer in the
//   documentation and/or other materials provided with the distribution.
//
// - Neither the name of the OPeNDAP nor the names of its contributors may
//   be used to endorse or promote products derived from this software
//   without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/////////////////////////////////////////////////////////////////////////////

package opendap.servers;

import opendap.servers.parsers.ExprParserConstants;

import java.util.*;
import java.io.*;

/**
 * Represents a clause which compares subclauses, using one of the
 * relative operators supported by the Operator class.
 *
 * @author joew
 * @see Operator
 * @see ClauseFactory
 */
public class RelOpClause
        extends AbstractClause
        implements TopLevelClause {
    /**
     * Creates a new RelOpClause. If the lhs and all the elements of the rhs
     * are constant, the RelOpClause will be flagged as constant, and
     * evaluated immediately.
     *
     * @param operator The operator invoked by the clause
     * @param lhs      The left-hand side of the comparison.
     * @param rhs      A list of SubClauses representing the right-hand side of the
     *                 comparison.
     * @throws DAP2ServerSideException Thrown if the clause is constant, but
     *                        the attempt to evaluate it fails.
     */
    protected RelOpClause(int operator, SubClause lhs, List rhs) throws DAP2ServerSideException {

        this.operator = operator;
        this.lhs = lhs;
        this.rhs = rhs;
        this.children = new ArrayList();
        children.add(lhs);
        children.addAll(rhs);
        this.constant = true;
        Iterator it = children.iterator();
        while (it.hasNext()) {
            SubClause current = (SubClause) it.next();
            current.setParent(this);
            if (!current.isConstant()) {
                constant = false;
            }
        }
        if (constant) {
            evaluate();
        }
    }

    public boolean getValue() {
        return value;
    }

    public boolean evaluate()
            throws DAP2ServerSideException {

        if (constant && defined) {
            return value;
        }

        if (rhs.size() == 1) {
            value = Operator.op(operator,
                    lhs.evaluate(),
                    ((SubClause) rhs.get(0)).evaluate());
        } else {
            value = false;
            Iterator it = rhs.iterator();
            while (it.hasNext() && !value) {
                if (Operator.op(operator, lhs.evaluate(), ((SubClause) it.next()).evaluate())) {
                    value = true;
                }
            }
        }
        defined = true;
        return value;
    }

    /**
     * Returns a SubClause representing the right-hand side of the
     * comparison.
     */
    public SubClause getLHS() {
        return lhs;
    }

    /**
     * Returns a list of SubClauses representing the right-hand side of the
     * comparison.
     */
    public List getRHS() {
        return rhs;
    }

    /**
     * Returns the type of comparison
     *
     * @see opendap.servers.parsers.ExprParserConstants
     */
    public int getOperator() {
        return operator;
    }

    /**
     * Prints the original string representation of this clause.
     * For use in debugging.
     */
    public void printConstraint(PrintWriter os)
    {
        lhs.printConstraint(os);
        String op = ExprParserConstants.tokenImage[operator];
        op = op.substring(1, op.length() - 1);
        os.print(op);
        //os.print(ExprParserConstants.tokenImage[operator].substring(2, 3));
        if (rhs.size() == 1) {
            ((ValueClause)rhs.get(0)).printConstraint(os);
        } else {
            os.print("{");
            Iterator it = rhs.iterator();
	    boolean first = true;
            while (it.hasNext()) {
                if(!first) os.print(",");
	        ((ValueClause)it.next()).printConstraint(os);
		first = false;
            }
            os.print("}");
        }
    }

    protected boolean value;

    protected int operator;

    protected SubClause lhs;

    protected List rhs;
}


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

import thredds.client.catalog.builder.DatasetBuilder;

import javax.annotation.concurrent.Immutable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * node in a tree of datasets, superclass of Catalog and Dataset
 *
 * @author caron
 * @since 1/8/2015
 */
@Immutable
public class DatasetNode {
  protected final DatasetNode parent;
  protected final String name;
  protected final Map<String, Object> flds;     // keep memory small. dont store reference objects for nulls

  protected DatasetNode(DatasetNode parent, String name, Map<String, Object> flds, List<DatasetBuilder> datasetBuilders) {
    this.parent = parent;
    this.name = name;
    this.flds = Collections.unmodifiableMap(flds);
    if (datasetBuilders != null && datasetBuilders.size() > 0) {
      List<Dataset> datasets = new ArrayList<>(datasetBuilders.size());
      for (DatasetBuilder dsb : datasetBuilders)
        datasets.add(dsb.makeDataset(this));
      flds.put(Dataset.Datasets, Collections.unmodifiableList(datasets));
    }
  }

  public Iterable<Map.Entry<String, Object>> getFldIterator() {
    return flds.entrySet();
  }

  public Object get(String key) {
    return flds.get(key);
  }

  public DatasetNode getParent() {
    return parent;
  }

  public String getName() {
    return name;
  }

  /**
    Get top level datasets contained directly in this catalog.
    Do not dereference catRefs.
   */
  public List<Dataset> getDatasetsLocal() {
    List<Dataset> datasets = (List<Dataset>) flds.get(Dataset.Datasets);
    return datasets == null ? new ArrayList<>(0) : datasets;
  }

  /**
    Get top level datasets contained in this catalog, or if its a catref,
    get the datasets in the referenced catalog only if already read in.
 */
  public List<Dataset> getDatasets() {
    return getDatasetsLocal();
  }

  /**
    Get top level datasets logically contained in this catalog.
    If this is a catalogRef, read it in.
 */
  public List<Dataset> getDatasetsLogical() {
    return getDatasets();
  }

  // Look though all datasets here or under here. do not go into catrefs
  public Dataset findDatasetByName(String name) {
    for (Dataset ds : getDatasets()) {
      if (ds.getName().equals(name)) return ds;
      Dataset result = ds.findDatasetByName(name);
      if (result != null) return result;
    }
    return null;
  }

  public boolean hasNestedDatasets() {
    List<Dataset> datasets = getDatasets();
    return !datasets.isEmpty();
  }

  public Catalog getParentCatalog() {
    if (parent == null) return null;
    if (parent instanceof Catalog) return (Catalog) parent;
    return parent.getParentCatalog();
  }

  public Dataset getParentDataset() {
    if (parent == null) return null;
    return (parent instanceof Dataset) ? (Dataset) parent : null;
  }

  //////////////////////////////////////////////
  // Utilities

  public List getLocalFieldAsList(String fldName) {
    Object value = flds.get(fldName);
    if (value != null) {
      if (value instanceof List) return (List) value;
      List result = new ArrayList(1);
      result.add(value);
      return result;
    }
    return new ArrayList(0);
  }

}


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

import ucar.unidata.geoloc.*;
import ucar.unidata.geoloc.projection.*;

import ucar.util.prefs.ui.Debug;

import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.List;

/**
 * Superclass for rendering collections of GisFeatures.
 *
 * @author John Caron
 */
public abstract class GisFeatureRendererMulti extends GisFeatureRenderer {
  private static boolean useDiscretization = false;
  private static double pixelMatch = 2.0;
  public static void setDiscretization( boolean b) { useDiscretization = b; }
  public static void setPixelMatch( double d) { pixelMatch = d; }

  private ArrayList featSetList = null;    // list of fetaureSets for progressive disclosure

  ////// this is what the subclasses have to implement (besides the constructor)
  public abstract LatLonRect getPreferredArea();
  protected abstract java.util.List getFeatures();      // collection of AbstractGisFeature
  protected abstract ProjectionImpl getDataProjection();    // what projection is the data in?

    /**
     * Sets new projection for subsequent drawing.
     *
     * @param project the new projection
     */
  public void setProjection(ProjectionImpl project) {
    displayProject = project;

    if (featSetList == null)
      return;
    Iterator iter = featSetList.iterator();
    while (iter.hasNext()) {
      FeatureSet fs = (FeatureSet) iter.next();
      fs.newProjection = true;
    }
  }

  public void createFeatureSet(double minDist) {
    // make a FeatureSet out of this, defer actually creating the points
    FeatureSet fs = new FeatureSet(null, minDist);

    // add to the list of featureSets
    if (featSetList == null)
      initFeatSetList();
    featSetList.add(fs);
  }

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

  // get the set of shapes to draw.
  // we have to deal with both projections and resolution-dependence
  protected Iterator getShapes(java.awt.Graphics2D g, AffineTransform normal2device) {
    long startTime = System.currentTimeMillis();

    if (featSetList == null) {
      initFeatSetList();
      assert !featSetList.isEmpty();
    }

    // which featureSet should we ue?
    FeatureSet fs = (FeatureSet) featSetList.get(0);
    if (featSetList.size() > 1) {
        // compute scale
      double scale = 1.0;
      try {
        AffineTransform world2device = g.getTransform();
        AffineTransform world2normal = normal2device.createInverse();
        world2normal.concatenate( world2device);
        scale = Math.max(Math.abs(world2normal.getScaleX()), Math.abs(world2normal.getShearX()));   // drawing or printing
        if (Debug.isSet("GisFeature/showTransform")) {
          System.out.println("GisFeature/showTransform: "+world2normal+ "\n scale = "+ scale);
        }
      } catch ( java.awt.geom.NoninvertibleTransformException e) {
        System.out.println( " GisRenderFeature: NoninvertibleTransformException on " + normal2device);
      }
      if (!displayProject.isLatLon())
        scale *= 111.0;  // km/deg
      double minD = Double.MAX_VALUE;
      for (Object aFeatSetList : featSetList) {
        FeatureSet tryfs = (FeatureSet) aFeatSetList;
        double d = Math.abs(scale * tryfs.minDist - pixelMatch);  // we want min features ~ 2 pixels
        if (d < minD) {
          minD = d;
          fs = tryfs;
        }
      }
      if (Debug.isSet("GisFeature/MapResolution")) {
        System.out.println("GisFeature/MapResolution: scale = "+scale+" minDist = "+fs.minDist);
      }
    }

    // we may have deferred the actual creation of the points
    if (fs.featureList == null)
      fs.createFeatures();

    // ok, now see if we need to project
    if (!displayProject.equals(fs.project)) {
      fs.setProjection( displayProject);
    } else {    // deal with LatLon
      if (fs.newProjection && displayProject.isLatLon()) {
        fs.setProjection( displayProject);
      }
    }
    fs.newProjection = false;

    if (Debug.isSet("GisFeature/timing/getShapes")) {
      long tookTime = System.currentTimeMillis() - startTime;
      System.out.println("timing.getShapes: " + tookTime*.001 + " seconds");
    }

      // so return it, already
    return fs.getShapes();
  }

    // make an ArrayList of Shapes from the given featureList and current display Projection
  private ArrayList makeShapes(Iterator featList) {
    Shape shape;
    ArrayList shapeList = new ArrayList();
    ProjectionImpl dataProject = getDataProjection();

    if (Debug.isSet("GisFeature/MapDraw")) {
      System.out.println("GisFeature/MapDraw: makeShapes with "+displayProject);
    }

/*    if (Debug.isSet("bug.drawShapes")) {
      int count =0;
      // make each GisPart a seperate shape for debugging
feats:while (featList.hasNext()) {
        AbstractGisFeature feature = (AbstractGisFeature) featList.next();
        java.util.Iterator pi = feature.getGisParts();
        while (pi.hasNext()) {
          GisPart gp = (GisPart) pi.next();
          int np = gp.getNumPoints();
          GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD, np);
          double[] xx = gp.getX();
          double[] yy = gp.getY();
          path.moveTo((float) xx[0], (float) yy[0]);
          if (count == 63)
                System.out.println("moveTo x ="+xx[0]+" y= "+yy[0]);
          for(int i = 1; i < np; i++) {
            path.lineTo((float) xx[i], (float) yy[i]);
            if (count == 63)
                System.out.println("lineTo x ="+xx[i]+" y= "+yy[i]);
          }
          shapeList.add(path);
          if (count == 63)
            break feats;
          count++;
        }
      }
      System.out.println("bug.drawShapes: #shapes =" +shapeList.size());
      return shapeList;
    }  */

    while (featList.hasNext()) {
      AbstractGisFeature feature = (AbstractGisFeature) featList.next();
      if (dataProject.isLatLon())   // always got to run it through if its lat/lon
        shape = feature.getProjectedShape(displayProject);
      else if (dataProject == displayProject)
        shape = feature.getShape();
      else
        shape = feature.getProjectedShape(dataProject, displayProject);

      shapeList.add(shape);
    }

    return shapeList;
  }

  private void initFeatSetList() {
    featSetList = new ArrayList();
    featSetList.add( new FeatureSet( getFeatures(), 0.0)); // full resolution set
  }

  private class FeatureSet {
    List featureList = null;
    double minDist;
    ProjectionImpl project = null;
    ArrayList shapeList = null;
    boolean newProjection = true;

    FeatureSet(List featureList, double minDist) {
      this.featureList = featureList;
      this.minDist = minDist;
    }

    void setProjection(ProjectionImpl project) {
      this.project = project;
      shapeList = makeShapes( featureList.iterator());

      /* if (project.isLatLon()) {   // why?
        LatLonProjection llproj = (LatLonProjection) project;
      } */
    }

    Iterator getShapes() { return shapeList.iterator(); }

    void createFeatures() {
      featureList = new ArrayList();

      Iterator iter = GisFeatureRendererMulti.this.getFeatures().iterator();   // this is the original, full resolution set
      while (iter.hasNext()) {
        AbstractGisFeature feature = (AbstractGisFeature) iter.next();
        FeatureMD featMD = new FeatureMD(minDist);

        Iterator pi = feature.getGisParts();
        while (pi.hasNext()) {
          GisPart gp = (GisPart) pi.next();
          FeatureMD.Part part = featMD.newPart(gp.getNumPoints());

          int np = gp.getNumPoints();
          double[] xx = gp.getX();
          double[] yy = gp.getY();

          part.set( xx[0], yy[0]);
          for(int i=1; i < np-1; i++)
            part.setIfDistant( xx[i], yy[i]);

          if (part.getNumPoints() > 1) {
            part.set( xx[np-1], yy[np-1]); // close polygons
            part.truncateArray();
            featMD.add(part);
          }
        } // loop over parts

        if (featSetList == null)
          initFeatSetList();
        if (featMD.getNumParts() > 0)
          featureList.add( featMD);
      } // loop over featuures

      getStats(featureList.iterator());
    }  // createFeatures()

    private void discretizeArray(double[] d, int n) {
      if (minDist == 0.0)
        return;
      for (int i = 0; i < n; i++) {
        d[i] = (Math.rint(d[i]/minDist) * minDist) + minDist/2;
      }
    }

  } // FeatureSet inner class

    // these are derived Features based on a mimimum distance between points
  private static class FeatureMD extends AbstractGisFeature {
    private ArrayList parts = new ArrayList();
    private int total_pts = 0;
    private double minDist;
    private double minDist2;

    FeatureMD(double minDist) {
      this.minDist = minDist;
      minDist2 = minDist * minDist;
    }

    void add( FeatureMD.Part part) {
      total_pts += part.getNumPoints();
      parts.add(part);
    }

    FeatureMD.Part newPart(int maxPts) {
      return new FeatureMD.Part(maxPts);
    }

    private double discretize(double d) {
      if (!useDiscretization || (minDist == 0.0))
        return d;
      return (Math.rint(d/minDist) * minDist) + minDist/2;
    }

      // implement GisFeature
    public java.awt.geom.Rectangle2D getBounds2D() { return null; }
    public int getNumPoints() { return total_pts; }
    public int getNumParts() { return parts.size(); }
    public java.util.Iterator getGisParts() { return parts.iterator(); }

    class Part implements GisPart {
      private int size;
      private double [] wx;  // lat/lon coords
      private double [] wy;

      // constructor
      Part ( int maxPts) {
        wx = new double[maxPts];
        wy = new double[maxPts];
        size = 0;
        minDist2 = minDist * minDist;
      }

      void set(double x, double y) {
        wx[size] = discretize(x);
        wy[size] = discretize(y);
        size++;
      }

      private void setNoD(double x, double y) {
        wx[size] = x;
        wy[size] = y;
        size++;
      }

      void setIfDistant(double x, double y) {
        x = discretize(x);
        y = discretize(y);
        double dx = x - wx[size-1];
        double dy = y - wy[size-1];
        double dist2 = dx*dx + dy*dy;
        if (dist2 >= minDist2)
     //   if ((x != wx[size-1]) || (y != wy[size-1]))
          setNoD(x, y);
      }

      void truncateArray() {
        double [] x = new double[size];
        double [] y = new double[size];

        for (int i=0; i<size; i++) {
          x[i] = wx[i];                 // arraycopy better?
          y[i] = wy[i];                 // arraycopy better?
        }
        wx = x;
        wy = y;
      }

        // implement GisPart
      public int getNumPoints() { return size; }
      public double[] getX() { return wx; }
      public double[] getY() { return wy; }
    }
  }

  protected double getStats(Iterator featList) {
    int total_pts = 0;
    int total_parts = 0;
    int total_feats = 0;
    int cross_pts = 0;
    double avgD = 0;
    double minD = Double.MAX_VALUE;
    double maxD = -Double.MAX_VALUE;

    ProjectionImpl dataProject = getDataProjection();
    ProjectionPointImpl thisW = new ProjectionPointImpl();
    ProjectionPointImpl lastW = new ProjectionPointImpl();

    while (featList.hasNext()) {
      AbstractGisFeature feature = (AbstractGisFeature) featList.next();
      total_feats++;

      Iterator pi = feature.getGisParts();
      while (pi.hasNext()) {
        GisPart gp = (GisPart) pi.next();
        total_parts++;

        double[] xx = gp.getX();
        double[] yy = gp.getY();
        int np = gp.getNumPoints();

        lastW.setLocation( xx[0], yy[0]);

        for(int i = 1; i < np; i++) {
          thisW.setLocation( xx[i], yy[i]);
          if (!dataProject.crossSeam(thisW, lastW)) {
            double dx = (xx[i] - xx[i-1]);
            double dy = (yy[i] - yy[i-1]);
            double dist = Math.sqrt(dx*dx + dy*dy);

            total_pts++;
            avgD += dist;
            minD = Math.min(minD, dist);
            maxD = Math.max(maxD, dist);
          } else
            cross_pts++;

          lastW.setLocation( xx[i], yy[i]);
        }
      }
    }

    avgD = total_pts == 0 ? 0 : (avgD/total_pts);
    if (Debug.isSet("GisFeature/MapResolution")) {
      System.out.println("Map.resolution: total_feats = "+ total_feats);
      System.out.println(" total_parts = "+ total_parts);
      System.out.println(" total_pts = "+ total_pts);
      System.out.println(" cross_pts = "+ cross_pts);
      System.out.println(" avg distance = "+ avgD);
      System.out.println(" min distance = "+ minD);
      System.out.println(" max distance = "+ maxD);
    }

    return avgD;
  }
}


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

import javax.annotation.concurrent.Immutable;

/**
 * Provides support for prefix names and numeric values.
 *
 * Instances of this class are immutable.
 *
 * @author Steven R. Emmerson
 */
@Immutable

public final class
PrefixName
    extends	Prefix
{
    /**
     * Constructs from a name and a numeric value.
     * @param name		The name for the prefix.
     * @param value		The numeric value for the prefix.
     */
    public
    PrefixName(String name, double value)
    {
	super(name, value);
    }

    /**
     * Compares this PrefixName with another PrefixName.
     * @param obj		The other PrefixName.
     * @return			A negative value, zero, or a positive value
     *				depending on whether this PrefixName is less
     *				than, equal to, or greater than
     *				obj, respectively.
     */
    public final int
    compareTo(Object obj)
    {
	return getID().compareToIgnoreCase(((PrefixName)obj).getID());
    }

    /**
     * Compares this PrefixName with a string.
     * @param string		The string to compare this PrefixName against.
     * @return			A negative value, zero, or a positive value
     *				depending on whether this PrefixName is less
     *				than, equal to, or greater than
     *				string, respectively.
     */
    public final int
    compareTo(String string)
    {
	return getID().length() >= string.length()
		? getID().compareToIgnoreCase(string)
		: getID().compareToIgnoreCase(
		    string.substring(0, getID().length()));
    }
}


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

package ucar.nc2.ui.widget;

import javax.swing.*;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.event.EventListenerList;

import ucar.nc2.util.CancelTask;

/**
 * A UI Component for running background tasks and letting user cancel them.
 * Also can be used as a CancelTask implementation.
 * @see ucar.nc2.ui.widget.ProgressMonitorTask
 *
 * @author caron
 */

public class StopButton extends JButton implements CancelTask {
  static private ImageIcon[] icon = new ImageIcon[2];
  static {
    icon[0] = BAMutil.getIcon("Stop24", true);
    icon[1] = BAMutil.getIcon("Stop16", true);
  }
  static private boolean debug = false;

  private ProgressMonitorTask task;
  private javax.swing.Timer myTimer;
  private boolean busy, isCancelled;
  private int count;

  // event handling
  private EventListenerList listenerList = new EventListenerList();

  // bean for JForm Designer - let it set sizes
  public StopButton() {
   setIcon(icon[0]);
    //setMaximumSize(new java.awt.Dimension(28,28));       // kludge; consistent with BAMutil
    //setPreferredSize(new java.awt.Dimension(28,28));
    setFocusPainted(false);

    super.addActionListener(ne -> {
        if (debug) System.out.println(" StopButton.EVENT");
        isCancelled = true;
    });
  }

  public StopButton(String tooltip) {
    setIcon(icon[0]);
    setMaximumSize(new java.awt.Dimension(28,28));       // kludge; consistent with BAMutil
    setPreferredSize(new java.awt.Dimension(28,28));
    setToolTipText(tooltip);
    setFocusPainted(false);

    super.addActionListener(e -> {
        if (debug) System.out.println(" StopButton.EVENT");
        isCancelled = true;
    });
  }

  /** Add listener: action event sent when task is done. event.getActionCommand() =
   *  <ul><li> "success"
   *  <li> "error"
   *  <li> "cancel"
   *  <li> "done" if done, but success/error/cancel not set
   *  </ul>
   */
  public void addActionListener(ActionListener l) {
    listenerList.add(ActionListener.class, l);
  }
  /** Remove listener */
  public void removeActionListener(ActionListener l) {
    listenerList.remove(ActionListener.class, l);
  }
  private void fireEvent(ActionEvent event) {
    // Guaranteed to return a non-null array
    Object[] listeners = listenerList.getListenerList();
    // Process the listeners last to first
    for (int i = listeners.length-2; i>=0; i-=2) {
      ((java.awt.event.ActionListener)listeners[i+1]).actionPerformed(event);
    }
  }

  @Override
  public boolean isCancel() {
    return isCancelled;
  }

  public void setCancel(boolean isCancelled) {
    this.isCancelled = isCancelled;
  }

  @Override
  public void setError(String msg) {
    System.out.println("Got Error= "+msg);
  }

  @Override
  public void setProgress(String msg, int progress) {
    // NOP
  }

  /**
   * The given task is run in a background thread.
   * Progress is indicated once a second.
   * You cannot call this method again till the task is completed.
   * @param pmt heres where the work is done.
   * @return true task was started, false if still busy with previous task.
   */
  public boolean startProgressMonitorTask( ProgressMonitorTask pmt) {
    if (busy) return false;
    busy = true;

    this.task = pmt;
    isCancelled = false;
    count = 0;
    setIcon(icon[0]);

    // create timer, whose events happen on the awt event Thread
    ActionListener watcher = new ActionListener() {
      public void actionPerformed(ActionEvent evt) {
        //System.out.println("timer event"+evt);

        if (isCancelled && !task.isCancel()) {
          task.cancel();
          if (debug) System.out.println(" task.cancel");
          return; // give it a chance to finish up
        } else {
          // indicate progress
          count++;
          setIcon( icon[count % 2]);
          if (debug) System.out.println(" stop count="+count);
        }

        // need to make sure task acknowledges the cancel; so dont shut down
        // until the task is done
        if (task.isDone()) {
          if (myTimer != null)
            myTimer.stop();
          myTimer = null;

          if (task.isError())
            javax.swing.JOptionPane.showMessageDialog(null, task.getErrorMessage());

          if (task.isSuccess())
            fireEvent( new ActionEvent(this, 0, "success"));
          else if (task.isError())
            fireEvent( new ActionEvent(this, 0, "error"));
          else if (task.isCancel())
            fireEvent( new ActionEvent(this, 0, "cancel"));
          else
            fireEvent( new ActionEvent(this, 0, "done"));

          busy = false;
        }
      }
    };
    myTimer = new javax.swing.Timer(1000, watcher); // every second
    myTimer.start();

    // do task in a seperate, non-event, thread
    Thread taskThread = new Thread(task);
    taskThread.start();

    return true;
  }

  public void clear() {
    if (myTimer != null)
      myTimer.stop();
    myTimer = null;
    busy = false;
  }

}