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

import ucar.nc2.*;
import ucar.nc2.constants.AxisType;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.dataset.*;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.util.NamedObject;
import ucar.nc2.units.*;

import ucar.unidata.geoloc.*;
import ucar.unidata.geoloc.projection.LatLonProjection;
import ucar.unidata.geoloc.projection.VerticalPerspectiveView;
import ucar.unidata.geoloc.projection.RotatedPole;
import ucar.unidata.geoloc.projection.RotatedLatLon;
import ucar.unidata.geoloc.projection.sat.MSGnavigation;
import ucar.unidata.geoloc.projection.sat.Geostationary;
import ucar.unidata.geoloc.vertical.*;
import ucar.ma2.*;

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

import ucar.nc2.units.DateRange;

/**
 * A georeferencing "gridded" CoordinateSystem. This describes a "grid" of coordinates, which
 * implies a connected topology such that values next to each other in index space are next to
 * each other in coordinate space.
 *

 * This currently assumes that the CoordinateSystem
 *

 *
 is georeferencing (has Lat, Lon or GeoX, GeoY axes)
 *
 x, y are 1 or 2-dimensional axes.
 *
 rt, z, e are 1-dimensional axes.
 *
 t is 1 or 2 dimensional. if 2d, then rt exists
 *

 *

 * This is the common case for georeferencing coordinate systems. Mathematically it is a product set:
 * {X,Y} x {Z} x {T}. The x and y axes may be 1 or 2 dimensional.
 *

 *

 * A CoordinateSystem may have multiple horizontal and vertical axes. GridCoordSys chooses one
 * axis corresponding to X, Y, Z, and T. It gives preference to one dimensional axes (CoordinateAxis1D).
 *
 * @author caron
 */

public class GridCoordSys extends CoordinateSystem implements ucar.nc2.dt.GridCoordSystem {
  static private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GridCoordSys.class);
  static private final boolean warnUnits = false;

  /**
   * Determine if this CoordinateSystem can be made into a GridCoordSys. Optionally for a given variable.
   * This currently assumes that the CoordinateSystem:
   *

   *
 is georeferencing (cs.isGeoReferencing())
   *
 x, y are 1 or 2-dimensional axes.
   *
 z, t, if they exist, are 1-dimensional axes.
   *
 domain rank > 1
   *

   *
   * @param sbuff place information messages here, may be null
   * @param cs    the CoordinateSystem to test
   * @param v     can it be used for this variable; v may be null
   * @return true if it can be made into a GridCoordSys.
   * @see CoordinateSystem#isGeoReferencing
   */
  public static boolean isGridCoordSys(Formatter sbuff, CoordinateSystem cs, VariableEnhanced v) {
    // must be at least 2 axes
    if (cs.getRankDomain() < 2) {
      if (sbuff != null) {
        sbuff.format("%s: domain rank < 2%n", cs.getName());
      }
      return false;
    }

    // must be lat/lon or have x,y and projecction
    if (!cs.isLatLon()) {
      // do check for GeoXY ourself
      if ((cs.getXaxis() == null) || (cs.getYaxis() == null)) {
        if (sbuff != null) {
          sbuff.format("%s: NO Lat,Lon or X,Y axis%n", cs.getName());
        }
        return false;
      }
      if (null == cs.getProjection()) {
        if (sbuff != null) {
          sbuff.format("%s: NO projection found%n", cs.getName());
        }
        return false;
      }
    }

    // obtain the x,y or lat/lon axes. x,y normally must be convertible to km
    CoordinateAxis xaxis, yaxis;
    if (cs.isGeoXY()) {
      xaxis = cs.getXaxis();
      yaxis = cs.getYaxis();

      // change to warning
      ProjectionImpl p = cs.getProjection();
      if (!(p instanceof RotatedPole)) {
        if (!SimpleUnit.kmUnit.isCompatible(xaxis.getUnitsString())) {
          if (sbuff != null) {
            sbuff.format("%s: X axis units are not convertible to km%n", cs.getName());
          }
          //return false;
        }
        if (!SimpleUnit.kmUnit.isCompatible(yaxis.getUnitsString())) {
          if (sbuff != null) {
            sbuff.format("%s: Y axis units are not convertible to km%n", cs.getName());
          }
          //return false;
        }
      }
    } else {
      xaxis = cs.getLonAxis();
      yaxis = cs.getLatAxis();
    }

    // check x,y rank <= 2
    if ((xaxis.getRank() > 2) || (yaxis.getRank() > 2)) {
      if (sbuff != null)
        sbuff.format("%s: X or Y axis rank must be <= 2%n", cs.getName());
      return false;
    }

    // check that the x,y have at least 2 dimensions between them ( this eliminates point data)
    int xyDomainSize = CoordinateSystem.countDomain(new CoordinateAxis[]{xaxis, yaxis});
    if (xyDomainSize < 2) {
      if (sbuff != null)
        sbuff.format("%s: X and Y axis must have 2 or more dimensions%n", cs.getName());
      return false;
    }

    List<CoordinateAxis> testAxis = new ArrayList<>();
    testAxis.add(xaxis);
    testAxis.add(yaxis);

    //int countRangeRank = 2;

    CoordinateAxis z = cs.getHeightAxis();
    if ((z == null) || !(z instanceof CoordinateAxis1D)) z = cs.getPressureAxis();
    if ((z == null) || !(z instanceof CoordinateAxis1D)) z = cs.getZaxis();
    if ((z != null) && !(z instanceof CoordinateAxis1D)) {
      if (sbuff != null) {
        sbuff.format("%s: Z axis must be 1D%n", cs.getName());
      }
      return false;
    }
    if (z != null)
      testAxis.add(z);

    // tom margolis 3/2/2010
    // allow runtime independent of time
    CoordinateAxis t = cs.getTaxis();
    CoordinateAxis rt = cs.findAxis(AxisType.RunTime);

    // A runtime axis must be scalar or one-dimensional
    if (rt != null) {
      if (!rt.isScalar() && !(rt instanceof CoordinateAxis1D)) {
        if (sbuff != null) sbuff.format("%s: RunTime axis must be 1D%n", cs.getName());
          return false;
      }
    }

    // If time axis is two-dimensional...
    if ((t != null) && !(t instanceof CoordinateAxis1D) && (t.getRank() != 0)) {

      if (rt != null) {
        if (rt.getRank() != 1) {
          if (sbuff != null) sbuff.format("%s: Runtime axis must be 1D%n", cs.getName());
          return false;
        }

        // time first dimension must agree with runtime
        if (!rt.getDimension(0).equals(t.getDimension(0))) {
          if (sbuff != null) sbuff.format("%s: 2D Time axis first dimension must be runtime%n", cs.getName());
          return false;
        }
      }
    }

    if (t != null)
      testAxis.add(t);
    if (rt != null)
      testAxis.add(rt);

    CoordinateAxis ens = cs.getEnsembleAxis();
    if (ens != null)
      testAxis.add(ens);

    if (v != null) { // test to see that v doesnt have extra dimensions. LOOK RELAX THIS
      List<Dimension> testDomain = new ArrayList<>();
      for (CoordinateAxis axis : testAxis) {
        for (Dimension dim : axis.getDimensions()) {
          if (!testDomain.contains(dim))
            testDomain.add(dim);
        }
      }
      if (!CoordinateSystem.isSubset(v.getDimensionsAll(), testDomain)) {
        if (sbuff != null) sbuff.format(" NOT complete%n");
        return false;
      }
    }

    return true;
  }

  /**
   * Determine if the CoordinateSystem cs can be made into a GridCoordSys for the Variable v.
   *
   * @param sbuff put debug information into this StringBuffer; may be null.
   * @param cs    CoordinateSystem to check.
   * @param v     Variable to check.
   * @return the GridCoordSys made from cs, else null.
   */
  public static GridCoordSys makeGridCoordSys(Formatter sbuff, CoordinateSystem cs, VariableEnhanced v) {
    if (sbuff != null) {
      sbuff.format(" ");
      v.getNameAndDimensions(sbuff, false, true);
      sbuff.format(" check CS %s: ", cs.getName());
    }

    if (isGridCoordSys(sbuff, cs, v)) {
      GridCoordSys gcs = new GridCoordSys(cs, sbuff);
      if (sbuff != null) sbuff.format(" OK%n");
      return gcs;
    }

    return null;
  }

  /////////////////////////////////////////////////////////////////////////////
  private ProjectionImpl proj;
  private GridCoordinate2D g2d;
  private CoordinateAxis horizXaxis, horizYaxis;
  private CoordinateAxis1D vertZaxis, ensembleAxis;
  private CoordinateAxis1DTime timeTaxis, runTimeAxis;
  private VerticalCT vCT;
  private VerticalTransform vt;
  private Dimension timeDim;

  private boolean isLatLon = false;

  /**
   * Create a GridCoordSys from an existing Coordinate System.
   * This will choose which axes are the XHoriz, YHoriz, Vertical, Time, RunTIme, Ensemble.
   * If theres a Projection, it will set its map area
   *
   * @param cs    create from this Coordinate System
   * @param sbuff place information messages here, may be null
   */
  public GridCoordSys(CoordinateSystem cs, Formatter sbuff) {
    super();
    this.ds = cs.getNetcdfDataset();

    if (cs.isGeoXY()) {
      horizXaxis = xAxis = cs.getXaxis();
      horizYaxis = yAxis = cs.getYaxis();

      ProjectionImpl p = cs.getProjection();
      if (!(p instanceof RotatedPole) && !(p instanceof RotatedLatLon)) {
        // make a copy of the axes if they need to change
        horizXaxis = convertUnits(horizXaxis);
        horizYaxis = convertUnits(horizYaxis);
      }
    } else if (cs.isLatLon()) {
      horizXaxis = lonAxis = cs.getLonAxis();
      horizYaxis = latAxis = cs.getLatAxis();
      isLatLon = true;

      if (lonAxis instanceof CoordinateAxis1D) {
        ((CoordinateAxis1D) lonAxis).correctLongitudeWrap();
      }

    } else
      throw new IllegalArgumentException("CoordinateSystem is not geoReferencing");

    coordAxes.add(horizXaxis);
    coordAxes.add(horizYaxis);

    // set canonical area
    ProjectionImpl projOrig = cs.getProjection();
    if (projOrig != null) {
      proj = projOrig.constructCopy();
      proj.setDefaultMapArea(getBoundingBox());  // LOOK too expensive for 2D
    }

   // LOOK: require 1D vertical - need to generalize to nD vertical.
    CoordinateAxis z_oneD = hAxis = cs.getHeightAxis();
    if ((z_oneD == null) || !(z_oneD instanceof CoordinateAxis1D)) z_oneD = pAxis = cs.getPressureAxis();
    if ((z_oneD == null) || !(z_oneD instanceof CoordinateAxis1D)) z_oneD = zAxis = cs.getZaxis();
    if ((z_oneD != null) && !(z_oneD instanceof CoordinateAxis1D))
      z_oneD = null;

    CoordinateAxis z_best = hAxis;
    if (pAxis != null) {
      if ((z_best == null) || !(z_best.getRank() > pAxis.getRank())) z_best = pAxis;
    }
    if (zAxis != null) {
      if ((z_best == null) || !(z_best.getRank() > zAxis.getRank())) z_best = zAxis;
    }

    if ((z_oneD == null) && (z_best != null)) { // cant find one-d z but have nD z
      if (sbuff != null) sbuff.format("GridCoordSys needs a 1D Coordinate, instead has %s%n", z_best.getNameAndDimensions());
    }

    if (z_oneD != null) {
      vertZaxis = (CoordinateAxis1D) z_oneD;
      coordAxes.add(vertZaxis);
    } else {
      hAxis = pAxis = zAxis = null;
    }

    // timeTaxis must be CoordinateAxis1DTime
    CoordinateAxis t = cs.getTaxis();
    if (t != null) {

      if (t instanceof CoordinateAxis1D) {

        try {
          if (t instanceof CoordinateAxis1DTime)
            timeTaxis = (CoordinateAxis1DTime) t;
          else {
            timeTaxis = CoordinateAxis1DTime.factory(ds, t, sbuff);
          }

          tAxis = timeTaxis;
          coordAxes.add(timeTaxis);
          timeDim = t.getDimension(0);

        } catch (Exception e) {
          if (sbuff != null)
            sbuff.format("%s: Error reading time coord= %s err= %s%n", t.getDatasetLocation(), t.getFullName(), e.getMessage());
          log.error(t.getDatasetLocation()+": Error reading time coord= "+t.getFullName(), e);
          throw new IllegalStateException("CoordinateSystem does not have a usable time axis");
        }

      } else { // 2d

        tAxis = t;
        timeTaxis = null;
        coordAxes.add(t); // LOOK ??
      }
    }

    // look for special axes
    ensembleAxis = (CoordinateAxis1D) cs.findAxis(AxisType.Ensemble);
    if (null != ensembleAxis) coordAxes.add(ensembleAxis);

    CoordinateAxis rtAxis = cs.findAxis(AxisType.RunTime);
    if (null != rtAxis) {
      try {
        if (rtAxis instanceof CoordinateAxis1DTime)
          runTimeAxis = (CoordinateAxis1DTime) rtAxis;
        else
          runTimeAxis = CoordinateAxis1DTime.factory(ds, rtAxis, sbuff);

        coordAxes.add(runTimeAxis);

      } catch (IOException e) {
        if (sbuff != null) {
          sbuff.format("Error reading runtime coord= %s err= %s%n", rtAxis.getFullName(), e.getMessage());
        }
      }
    }

    // look for VerticalCT
    List<CoordinateTransform> list = cs.getCoordinateTransforms();
    for (CoordinateTransform ct : list) {
      if (ct instanceof VerticalCT) {
        vCT = (VerticalCT) ct;
        break;
      }
    }

    // make name based on coordinate
    Collections.sort(coordAxes, new CoordinateAxis.AxisComparator()); // canonical ordering of axes
    this.name = makeName(coordAxes);

    // copy all coordinate transforms into here
    this.coordTrans = new ArrayList<>(cs.getCoordinateTransforms());

    // collect dimensions
    for (CoordinateAxis axis : coordAxes) {
      List<Dimension> dims = axis.getDimensionsAll();
      for (Dimension dim : dims) {
        if (!domain.contains(dim))
          domain.add(dim);
      }
    }

    // WRF NMM
    Attribute att = getXHorizAxis().findAttribute(_Coordinate.Stagger);
    if (att != null)
      setHorizStaggerType(att.getStringValue());
  }

  /**
   * Create a GridCoordSys as a section of an existing GridCoordSys.
   * This will create sections of the corresponding CoordinateAxes.
   *
   * @param from    copy this GridCoordSys
   * @param t_range subset the time dimension, or null if you want all of it
   * @param z_range subset the vertical dimension, or null if you want all of it
   * @param y_range subset the y dimension, or null if you want all of it
   * @param x_range subset the x dimension, or null if you want all of it
   * @throws InvalidRangeException if any of the ranges are illegal
   */
  public GridCoordSys(GridCoordSys from, Range t_range, Range z_range, Range y_range, Range x_range) throws InvalidRangeException {
    this(from, null, null, t_range, z_range, y_range, x_range);
  }

  /**
   * Create a GridCoordSys as a section of an existing GridCoordSys.
   * This will create sections of the corresponding CoordinateAxes.
   *
   * @param from     copy this GridCoordSys
   * @param rt_range subset the runtime dimension, or null if you want all of it
   * @param e_range  subset the ensemble dimension, or null if you want all of it
   * @param t_range  subset the time dimension, or null if you want all of it
   * @param z_range  subset the vertical dimension, or null if you want all of it
   * @param y_range  subset the y dimension, or null if you want all of it
   * @param x_range  subset the x dimension, or null if you want all of it
   * @throws InvalidRangeException if any of the ranges are illegal
   */
  public GridCoordSys(GridCoordSys from, Range rt_range, Range e_range, Range t_range, Range z_range, Range y_range, Range x_range) throws InvalidRangeException {
    super();

    CoordinateAxis xaxis = from.getXHorizAxis();
    CoordinateAxis yaxis = from.getYHorizAxis();

    if ((xaxis instanceof CoordinateAxis1D) && (yaxis instanceof CoordinateAxis1D)) {
      CoordinateAxis1D xaxis1 = (CoordinateAxis1D) xaxis;
      CoordinateAxis1D yaxis1 = (CoordinateAxis1D) yaxis;

      horizXaxis = (x_range == null) ? xaxis1 : xaxis1.section(x_range);
      horizYaxis = (y_range == null) ? yaxis : yaxis1.section(y_range);

    } else if ((xaxis instanceof CoordinateAxis2D) && (yaxis instanceof CoordinateAxis2D) && from.isLatLon()) {
      CoordinateAxis2D lon_axis = (CoordinateAxis2D) xaxis;
      CoordinateAxis2D lat_axis = (CoordinateAxis2D) yaxis;

      horizXaxis = lon_axis.section(y_range, x_range);
      horizYaxis = lat_axis.section(y_range, x_range);

    } else
      throw new IllegalArgumentException("must be 1D or 2D/LatLon ");

    if (from.isGeoXY()) {
      xAxis = horizXaxis;
      yAxis = horizYaxis;
    } else {
      lonAxis = horizXaxis;
      latAxis = horizYaxis;
      isLatLon = true;
    }

    coordAxes.add(horizXaxis);
    coordAxes.add(horizYaxis);

    // set canonical area
    ProjectionImpl projOrig = from.getProjection();
    if (projOrig != null) {
      proj = projOrig.constructCopy();
      proj.setDefaultMapArea(getBoundingBox()); // LOOK expensive for 2D
    }

    CoordinateAxis1D zaxis = from.getVerticalAxis();
    if (zaxis != null) {
      vertZaxis = (z_range == null) ? zaxis : zaxis.section(z_range);
      coordAxes.add(vertZaxis);
      // LOOK assign hAxis, pAxis or zAxis ??
    }

    if (from.getVerticalCT() != null) {
      VerticalTransform vtFrom = from.getVerticalTransform(); // LOOK may need to make sure this exists?
      if (vtFrom != null)
        vt = vtFrom.subset(t_range, z_range, y_range, x_range);

      vCT = from.getVerticalCT();
    }

    CoordinateAxis1D eaxis = from.getEnsembleAxis();
    if (eaxis != null) {
      ensembleAxis = (e_range == null) ? eaxis : eaxis.section(e_range);
      coordAxes.add(ensembleAxis);
    }

    CoordinateAxis taxis = from.getTimeAxis();
    CoordinateAxis1DTime taxis1D = null;
    if (taxis != null) {
      if (taxis instanceof CoordinateAxis1DTime) {
        taxis1D = (CoordinateAxis1DTime) taxis;
        tAxis = timeTaxis = (t_range == null) ? taxis1D : taxis1D.section(t_range);
        coordAxes.add(timeTaxis);
        timeDim = timeTaxis.getDimension(0);
      } else {
        if ((rt_range == null) && (t_range == null))
          tAxis = taxis;
        else {
          Section timeSection = new Section().appendRange(rt_range).appendRange(t_range);
          tAxis = (CoordinateAxis) taxis.section(timeSection);
        }
        coordAxes.add(tAxis);
      }
    }

    CoordinateAxis1DTime rtaxis = from.getRunTimeAxis();
    if (rtaxis != null) {
      if (taxis1D != null) {
        Dimension tDim = taxis1D.getDimension(0);
        Dimension rtDim = rtaxis.getDimension(0);
        if (rtDim != null && tDim.getShortName().equals(rtDim.getShortName())) { // must use time subset range if dims match - FMRC 1D has this a lot
          runTimeAxis = (t_range == null) ? rtaxis : rtaxis.section(t_range);
        }
      }
      if (runTimeAxis == null) { // regular case of a run time axis
        runTimeAxis = (rt_range == null) ? rtaxis : rtaxis.section(rt_range);
      }

      coordAxes.add(runTimeAxis);
    }

    // make name based on coordinate
    Collections.sort(coordAxes, new CoordinateAxis.AxisComparator()); // canonical ordering of axes
    this.name = makeName(coordAxes);

    this.coordTrans = new ArrayList<>(from.getCoordinateTransforms());

    // collect dimensions
    for (CoordinateAxis axis : coordAxes) {
      List<Dimension> dims = axis.getDimensionsAll();
      for (Dimension dim : dims) {
        dim.setShared(true); // make them shared (section will make them unshared)
        if (!domain.contains(dim))
          domain.add(dim);
      }
    }

    setHorizStaggerType(from.getHorizStaggerType());
  }

  private CoordinateAxis convertUnits(CoordinateAxis axis) {
    String units = axis.getUnitsString();
    SimpleUnit axisUnit = SimpleUnit.factory(units);
    if (axisUnit == null) {
      if (warnUnits) log.warn("cant parse unit= {}", units);
      return axis;
    }

    double factor;
    try {
      factor = axisUnit.convertTo(1.0, SimpleUnit.kmUnit);
    } catch (IllegalArgumentException e) {
      if (warnUnits) log.warn("convertUnits failed", e);
      return axis;
    }
    if (factor == 1.0) return axis;

    Array data;
    try {
      data = axis.read();
    } catch (IOException e) {
      log.warn("convertUnits read failed", e);
      return axis;
    }

    DataType dtype = axis.getDataType();
    if (dtype.isFloatingPoint()) {
      IndexIterator ii = data.getIndexIterator();
      while (ii.hasNext())
        ii.setDoubleCurrent(factor * ii.getDoubleNext());

      CoordinateAxis newAxis = axis.copyNoCache();
      newAxis.setCachedData(data, false);
      newAxis.setUnitsString("km");
      return newAxis;

    } else {  // convert to DOUBLE
      Array newData = Array.factory(DataType.DOUBLE, axis.getShape());
      IndexIterator newi = newData.getIndexIterator();
      IndexIterator ii = data.getIndexIterator();
      while (ii.hasNext() && newi.hasNext())
        newi.setDoubleNext(factor * ii.getDoubleNext());

      CoordinateAxis newAxis = axis.copyNoCache();
      newAxis.setDataType(DataType.DOUBLE);
      newAxis.setCachedData(newData, false);
      newAxis.setUnitsString("km");
      return newAxis;
    }
  }

  /**
   * Get the vertical transform function, or null if none
   *
   * @return the vertical transform function, or null if none
   */
  @Override
  public VerticalTransform getVerticalTransform() {
    return vt;
  }

  /**
   * Get the Coordinate Transform description.
   *
   * @return Coordinate Transform description, or null if none
   */
  @Override
  public VerticalCT getVerticalCT() {
    return vCT;
  }

  // we have to delay making these, since we dont identify the dimensions specifically until now
  void makeVerticalTransform(GridDataset gds, Formatter parseInfo) {
    if (vt != null) return; // already done
    if (vCT == null) return;  // no vt

    vt = vCT.makeVerticalTransform(gds.getNetcdfDataset(), timeDim);

    if (vt == null) {
      if (parseInfo != null)
        parseInfo.format("  - ERR can't make VerticalTransform = %s%n", vCT.getVerticalTransformType());
    } else {
      if (parseInfo != null) parseInfo.format("  - VerticalTransform = %s%n", vCT.getVerticalTransformType());
    }
  }

  /**
   * get the X Horizontal axis (either GeoX or Lon)
   */
  @Override
  public CoordinateAxis getXHorizAxis() {
    return horizXaxis;
  }

  /**
   * get the Y Horizontal axis (either GeoY or Lat)
   */
  @Override
  public CoordinateAxis getYHorizAxis() {
    return horizYaxis;
  }

  /**
   * get the Vertical axis (either Geoz, Height, or Pressure)
   */
  @Override
  public CoordinateAxis1D getVerticalAxis() {
    return vertZaxis;
  }

  /**
   * get the Time axis
   */
  @Override
  public CoordinateAxis getTimeAxis() {
    return tAxis;
  }

  /**
   * get the Time axis, if its 1-dimensional
   */
  @Override
  public CoordinateAxis1DTime getTimeAxis1D() {
    return timeTaxis;
  }

  /**
   * get the RunTime axis, else null
   */
  @Override
  public CoordinateAxis1DTime getRunTimeAxis() {
    return runTimeAxis;
  }

  /**
   * get the Ensemble axis, else null
   */
  @Override
  public CoordinateAxis1D getEnsembleAxis() {
    return ensembleAxis;
  }

  /**
   * get the projection
   */
  @Override
  public ProjectionImpl getProjection() {
    return proj;
  }

  @Override
  public void setProjectionBoundingBox() {
    // set canonical area
    if (proj != null) {
      proj.setDefaultMapArea(getBoundingBox());  // LOOK too expensive for 2D
    }
  }

  /**
   * is this a Lat/Lon coordinate system?
   */
  @Override
  public boolean isLatLon() {
    return isLatLon;
  }

  /**
   * Is this a global coverage over longitude ?
   * @return true if isLatLon and longitude wraps
   */
  @Override
  public boolean isGlobalLon() {
    if (!isLatLon) return false;
    if (!(horizXaxis instanceof CoordinateAxis1D)) return false;
    CoordinateAxis1D lon = (CoordinateAxis1D) horizXaxis;
    double first = lon.getCoordEdge(0);
    double last = lon.getCoordEdge((int) lon.getSize());
    double min = Math.min(first, last);
    double max =  Math.max(first, last);
    return (max - min) >= 360;
  }

  /**
   * true if increasing z coordinate values means "up" in altitude
   */
  @Override
  public boolean isZPositive() {
    if (vertZaxis == null) return false;
    if (vertZaxis.getPositive() != null) {
      return vertZaxis.getPositive().equalsIgnoreCase(ucar.nc2.constants.CF.POSITIVE_UP);
    }
    if (vertZaxis.getAxisType() == AxisType.Height) return true;
    return vertZaxis.getAxisType() != AxisType.Pressure;
  }

  /**
   * true if x and y axes are CoordinateAxis1D and are regular
   */
  @Override
  public boolean isRegularSpatial() {
    if (!isRegularSpatial(getXHorizAxis())) return false;
    if (!isRegularSpatial(getYHorizAxis())) return false;
    //if (!isRegularSpatial(getVerticalAxis())) return false; LOOK removed July 30, 2006 for WCS
    return true;
  }

  private boolean isRegularSpatial(CoordinateAxis axis) {
    if (axis == null) return true;
    if (!(axis instanceof CoordinateAxis1D)) return false;
    return ((CoordinateAxis1D) axis).isRegular();
  }

  private String horizStaggerType;

  @Override
  public String getHorizStaggerType() {
    return horizStaggerType;
  }

  public void setHorizStaggerType(String horizStaggerType) {
    this.horizStaggerType = horizStaggerType;
  }

  /**
   * Given a point in x,y coordinate space, find the x,y index in the coordinate system.
   *
   * @param x_coord position in x coordinate space.
   * @param y_coord position in y coordinate space.
   * @param result  put result (x,y) index in here, may be null
   * @return int[2], 0=x,1=y indices in the coordinate system of the point. These will be -1 if out of range.
   */
  @Override
  public int[] findXYindexFromCoord(double x_coord, double y_coord, int[] result) {
    if (result == null)
      result = new int[2];

    if ((horizXaxis instanceof CoordinateAxis1D) && (horizYaxis instanceof CoordinateAxis1D)) {
      result[0] = ((CoordinateAxis1D) horizXaxis).findCoordElement(x_coord);
      result[1] = ((CoordinateAxis1D) horizYaxis).findCoordElement(y_coord);
      return result;

    } else if ((horizXaxis instanceof CoordinateAxis2D) && (horizYaxis instanceof CoordinateAxis2D)) {
      if (g2d == null)
        g2d = new GridCoordinate2D((CoordinateAxis2D) horizYaxis, (CoordinateAxis2D) horizXaxis);
      int[] result2 = new int[2];
      boolean found = g2d.findCoordElement(y_coord, x_coord, result2);
      if (found) {
        result[0] = result2[1];
        result[1] = result2[0];
      } else {
        result[0] = -1;
        result[1] = -1;
      }
      return result;
    }

    // cant happen
    throw new IllegalStateException("GridCoordSystem.findXYindexFromCoord");
  }

  /**
   * Given a point in x,y coordinate space, find the x,y index in the coordinate system.
   * If outside the range, the closest point is returned, eg, 0 or n-1 depending on if the coordinate is too small or too large.
   *
   * @param x_coord position in x coordinate space.
   * @param y_coord position in y coordinate space.
   * @param result  put result in here, may be null
   * @return int[2], 0=x,1=y indices in the coordinate system of the point.
   */
  @Override
  public int[] findXYindexFromCoordBounded(double x_coord, double y_coord, int[] result) {
    if (result == null)
      result = new int[2];

    if ((horizXaxis instanceof CoordinateAxis1D) && (horizYaxis instanceof CoordinateAxis1D)) {
      result[0] = ((CoordinateAxis1D) horizXaxis).findCoordElementBounded(x_coord);
      result[1] = ((CoordinateAxis1D) horizYaxis).findCoordElementBounded(y_coord);
      return result;

    } else if ((horizXaxis instanceof CoordinateAxis2D) && (horizYaxis instanceof CoordinateAxis2D)) {
      if (g2d == null)
        g2d = new GridCoordinate2D((CoordinateAxis2D) horizYaxis, (CoordinateAxis2D) horizXaxis);

      int[] result2 = new int[2];
      g2d.findCoordElement(y_coord, x_coord, result2); // returns best guess
      result[0] = result2[1];
      result[1] = result2[0];
      return result;
    }

    // cant happen
    throw new IllegalStateException("GridCoordSystem.findXYindexFromCoord");
  }

  /**
   * Given a lat,lon point, find the x,y index in the coordinate system.
   *
   * @param lat    latitude position.
   * @param lon    longitude position.
   * @param result put result in here, may be null
   * @return int[2], 0=x,1=y indices in the coordinate system of the point. These will be -1 if out of range.
   */
  @Override
  public int[] findXYindexFromLatLon(double lat, double lon, int[] result) {
    Projection dataProjection = getProjection();
    ProjectionPoint pp = dataProjection.latLonToProj(new LatLonPointImpl(lat, lon), new ProjectionPointImpl());

    return findXYindexFromCoord(pp.getX(), pp.getY(), result);
  }

  /**
   * Given a lat,lon point, find the x,y index in the coordinate system.
   * If outside the range, the closest point is returned
   *
   * @param lat    latitude position.
   * @param lon    longitude position.
   * @param result put result in here, may be null
   * @return int[2], 0=x,1=y indices in the coordinate system of the point.
   */
  @Override
  public int[] findXYindexFromLatLonBounded(double lat, double lon, int[] result) {
    Projection dataProjection = getProjection();
    ProjectionPoint pp = dataProjection.latLonToProj(new LatLonPointImpl(lat, lon), new ProjectionPointImpl());

    return findXYindexFromCoordBounded(pp.getX(), pp.getY(), result);
  }

  /**
   * True if there is a Time Axis.
   */
  @Override
  public boolean hasTimeAxis() {
    return tAxis != null;
  }

  /**
   * True if there is a Time Axis and it is 1D.
   */
  @Override
  public boolean hasTimeAxis1D() {
    return timeTaxis != null;
  }

  /**
   * @deprecated doesnt work correctly for intervals
   */
  @Override
  public CoordinateAxis1DTime getTimeAxisForRun(int run_index) {
    if (!hasTimeAxis() || hasTimeAxis1D() || runTimeAxis == null) return null;
    int nruns = (int) runTimeAxis.getSize();
    if ((run_index < 0) || (run_index >= nruns))
      throw new IllegalArgumentException("getTimeAxisForRun index out of bounds= " + run_index);

    if (timeAxisForRun == null)
      timeAxisForRun = new CoordinateAxis1DTime[nruns];

    if (timeAxisForRun[run_index] == null)
      timeAxisForRun[run_index] = makeTimeAxisForRun(run_index);

    return timeAxisForRun[run_index];
  }

  private CoordinateAxis1DTime[] timeAxisForRun;

  private CoordinateAxis1DTime makeTimeAxisForRun(int run_index) {
    VariableDS section;
    try {
      section = (VariableDS) tAxis.slice(0, run_index);
      return CoordinateAxis1DTime.factory(ds, section, null);
    } catch (InvalidRangeException | IOException e) {
      e.printStackTrace();
    }

    return null;
  }

  private ProjectionRect mapArea = null;

  /**
   * Get the x,y bounding box in projection coordinates.
   */
  @Override
  public ProjectionRect getBoundingBox() {
    if (mapArea == null) {

      if ((horizXaxis == null) || !horizXaxis.isNumeric() || (horizYaxis == null) || !horizYaxis.isNumeric())
        return null; // impossible

      // x,y may be 2D
      if (!(horizXaxis instanceof CoordinateAxis1D) || !(horizYaxis instanceof CoordinateAxis1D)) {
        /*  could try to optimize this - just get cord=ners or something
        CoordinateAxis2D xaxis2 = (CoordinateAxis2D) horizXaxis;
        CoordinateAxis2D yaxis2 = (CoordinateAxis2D) horizYaxis;
        MAMath.MinMax
        */

        mapArea = new ProjectionRect(horizXaxis.getMinValue(), horizYaxis.getMinValue(),
                horizXaxis.getMaxValue(), horizYaxis.getMaxValue());

      } else {

        CoordinateAxis1D xaxis1 = (CoordinateAxis1D) horizXaxis;
        CoordinateAxis1D yaxis1 = (CoordinateAxis1D) horizYaxis;

        /* add one percent on each side if its a projection. WHY?
        double dx = 0.0, dy = 0.0;
        if (!isLatLon()) {
          dx = .01 * (xaxis1.getCoordEdge((int) xaxis1.getSize()) - xaxis1.getCoordEdge(0));
          dy = .01 * (yaxis1.getCoordEdge((int) yaxis1.getSize()) - yaxis1.getCoordEdge(0));
        }

        mapArea = new ProjectionRect(xaxis1.getCoordEdge(0) - dx, yaxis1.getCoordEdge(0) - dy,
            xaxis1.getCoordEdge((int) xaxis1.getSize()) + dx,
            yaxis1.getCoordEdge((int) yaxis1.getSize()) + dy); */

        mapArea = new ProjectionRect(xaxis1.getCoordEdge(0), yaxis1.getCoordEdge(0),
                xaxis1.getCoordEdge((int) xaxis1.getSize()),
                yaxis1.getCoordEdge((int) yaxis1.getSize()));
      }
    }

    return mapArea;
  }

  /**
   * Get the Lat/Lon coordinates of the midpoint of a grid cell, using the x,y indices
   *
   * @param xindex x index
   * @param yindex y index
   * @return lat/lon coordinate of the midpoint of the cell
   */
  @Override
  public LatLonPoint getLatLon(int xindex, int yindex) {
    double x, y;

    if (horizXaxis instanceof CoordinateAxis1D) {
      CoordinateAxis1D horiz1D = (CoordinateAxis1D) horizXaxis;
      x = horiz1D.getCoordValue(xindex);
    } else {
      CoordinateAxis2D horiz2D = (CoordinateAxis2D) horizXaxis;
      x = horiz2D.getCoordValue( yindex, xindex);
    }

    if (horizYaxis instanceof CoordinateAxis1D) {
      CoordinateAxis1D horiz1D = (CoordinateAxis1D) horizYaxis;
      y = horiz1D.getCoordValue(yindex);
    } else {
      CoordinateAxis2D horiz2D = (CoordinateAxis2D) horizYaxis;
      y = horiz2D.getCoordValue( yindex, xindex);
    }

    return isLatLon() ? new LatLonPointImpl(y, x) : getLatLon(x, y);
  }

  public LatLonPoint getLatLon(double xcoord, double ycoord) {
    Projection dataProjection = getProjection();
    return dataProjection.projToLatLon(new ProjectionPointImpl(xcoord, ycoord), new LatLonPointImpl());
  }

  private LatLonRect llbb = null;

  /**
   * Get horizontal bounding box in lat, lon coordinates.
   *
   * @return lat, lon bounding box.
   */
  @Override
  public LatLonRect getLatLonBoundingBox() {

    if (llbb == null) {

      if (isLatLon()) {
        double startLat = horizYaxis.getMinValue();
        double startLon = horizXaxis.getMinValue();

        double deltaLat = horizYaxis.getMaxValue() - startLat;
        double deltaLon = horizXaxis.getMaxValue() - startLon;

        LatLonPoint llpt = new LatLonPointImpl(startLat, startLon);
        llbb = new LatLonRect(llpt, deltaLat, deltaLon);

      } else {
        ProjectionImpl dataProjection = getProjection();
        ProjectionRect bb = getBoundingBox();
        llbb = dataProjection.projToLatLonBB(bb);
      }
    }

    return llbb;

      /*  // look at all 4 corners of the bounding box
        LatLonPointImpl llpt = (LatLonPointImpl) dataProjection.projToLatLon(bb.getLowerLeftPoint(), new LatLonPointImpl());
        LatLonPointImpl lrpt = (LatLonPointImpl) dataProjection.projToLatLon(bb.getLowerRightPoint(), new LatLonPointImpl());
        LatLonPointImpl urpt = (LatLonPointImpl) dataProjection.projToLatLon(bb.getUpperRightPoint(), new LatLonPointImpl());
        LatLonPointImpl ulpt = (LatLonPointImpl) dataProjection.projToLatLon(bb.getUpperLeftPoint(), new LatLonPointImpl());

        // Check if grid contains poles.
        boolean includesNorthPole = false;
        int[] resultNP;
        resultNP = findXYindexFromLatLon(90.0, 0, null);
        if (resultNP[0] != -1 && resultNP[1] != -1)
          includesNorthPole = true;
        boolean includesSouthPole = false;
        int[] resultSP;
        resultSP = findXYindexFromLatLon(-90.0, 0, null);
        if (resultSP[0] != -1 && resultSP[1] != -1)
          includesSouthPole = true;

        if (includesNorthPole && !includesSouthPole) {
          llbb = new LatLonRect(llpt, new LatLonPointImpl(90.0, 0.0)); // ??? lon=???
          llbb.extend(lrpt);
          llbb.extend(urpt);
          llbb.extend(ulpt);
          // OR
          //llbb.extend( new LatLonRect( llpt, lrpt ));
          //llbb.extend( new LatLonRect( lrpt, urpt ) );
          //llbb.extend( new LatLonRect( urpt, ulpt ) );
          //llbb.extend( new LatLonRect( ulpt, llpt ) );
        } else if (includesSouthPole && !includesNorthPole) {
          llbb = new LatLonRect(llpt, new LatLonPointImpl(-90.0, -180.0)); // ??? lon=???
          llbb.extend(lrpt);
          llbb.extend(urpt);
          llbb.extend(ulpt);
        } else {
          double latMin = Math.min(llpt.getLatitude(), lrpt.getLatitude());
          double latMax = Math.max(ulpt.getLatitude(), urpt.getLatitude());

          // longitude is a bit tricky as usual
          double lonMin = getMinOrMaxLon(llpt.getLongitude(), ulpt.getLongitude(), true);
          double lonMax = getMinOrMaxLon(lrpt.getLongitude(), urpt.getLongitude(), false);

          llpt.set(latMin, lonMin);
          urpt.set(latMax, lonMax);

          llbb = new LatLonRect(llpt, urpt);
        }
      }
    }  */

  }

  /**
   * Get Index Ranges for the given lat, lon bounding box.
   *
   * @deprecated use getRangesFromLatLonRect.
   */
  public List<Range> getLatLonBoundingBox(LatLonRect rect) throws InvalidRangeException {
    return getRangesFromLatLonRect(rect);
  }

  /**
   * Get Index Ranges for the given lat, lon bounding box.
   * For projection, only an approximation based on latlon corners.
   * Must have CoordinateAxis1D or 2D for x and y axis.
   *
   * @param rect the requested lat/lon bounding box
   * @return list of 2 Range objects, first y then x.
   */
  @Override
  public List<Range> getRangesFromLatLonRect(LatLonRect rect) throws InvalidRangeException {
    double minx, maxx, miny, maxy;

    ProjectionImpl proj = getProjection();
    if (proj != null && !(proj instanceof VerticalPerspectiveView) && !(proj instanceof MSGnavigation)
         && !(proj instanceof Geostationary)) { // LOOK kludge - how to do this generrally ??
      // first clip the request rectangle to the bounding box of the grid
      LatLonRect bb = getLatLonBoundingBox();
      LatLonRect rect2 = bb.intersect(rect);
      if (null == rect2)
        throw new InvalidRangeException("Request Bounding box does not intersect Grid ");
      rect = rect2;
    }

    CoordinateAxis xaxis = getXHorizAxis();
    CoordinateAxis yaxis = getYHorizAxis();

    if (isLatLon()) {

      LatLonPointImpl llpt = rect.getLowerLeftPoint();
      LatLonPointImpl urpt = rect.getUpperRightPoint();
      LatLonPointImpl lrpt = rect.getLowerRightPoint();
      LatLonPointImpl ulpt = rect.getUpperLeftPoint();

      minx = getMinOrMaxLon(llpt.getLongitude(), ulpt.getLongitude(), true);
      miny = Math.min(llpt.getLatitude(), lrpt.getLatitude());
      maxx = getMinOrMaxLon(urpt.getLongitude(), lrpt.getLongitude(), false);
      maxy = Math.min(ulpt.getLatitude(), urpt.getLatitude());

      // normalize to [minLon,minLon+360]
      double minLon = xaxis.getMinValue();
      minx = LatLonPointImpl.lonNormalFrom( minx, minLon);
      maxx = LatLonPointImpl.lonNormalFrom( maxx, minLon);

    } else {
      ProjectionRect prect = getProjection().latLonToProjBB(rect); // allow projection to override
      minx = prect.getMinPoint().getX();
      miny = prect.getMinPoint().getY();
      maxx = prect.getMaxPoint().getX();
      maxy = prect.getMaxPoint().getY();

      /*
      see ProjectionImpl.latLonToProjBB2()
      Projection dataProjection = getProjection();
      ProjectionPoint ll = dataProjection.latLonToProj(llpt, new ProjectionPointImpl());
      ProjectionPoint ur = dataProjection.latLonToProj(urpt, new ProjectionPointImpl());
      ProjectionPoint lr = dataProjection.latLonToProj(lrpt, new ProjectionPointImpl());
      ProjectionPoint ul = dataProjection.latLonToProj(ulpt, new ProjectionPointImpl());

      minx = Math.min(ll.getX(), ul.getX());
      miny = Math.min(ll.getY(), lr.getY());
      maxx = Math.max(ur.getX(), lr.getX());
      maxy = Math.max(ul.getY(), ur.getY()); */
    }

    if ((xaxis instanceof CoordinateAxis1D) && (yaxis instanceof CoordinateAxis1D)) {
      CoordinateAxis1D xaxis1 = (CoordinateAxis1D) xaxis;
      CoordinateAxis1D yaxis1 = (CoordinateAxis1D) yaxis;

      int minxIndex = xaxis1.findCoordElementBounded(minx);
      int minyIndex = yaxis1.findCoordElementBounded(miny);

      int maxxIndex = xaxis1.findCoordElementBounded(maxx);
      int maxyIndex = yaxis1.findCoordElementBounded(maxy);

      List<Range> list = new ArrayList<>();
      list.add(new Range(Math.min(minyIndex, maxyIndex), Math.max(minyIndex, maxyIndex)));
      list.add(new Range(Math.min(minxIndex, maxxIndex), Math.max(minxIndex, maxxIndex)));
      return list;

    } else if ((xaxis instanceof CoordinateAxis2D) && (yaxis instanceof CoordinateAxis2D) && isLatLon()) {
      CoordinateAxis2D lon_axis = (CoordinateAxis2D) xaxis;
      CoordinateAxis2D lat_axis = (CoordinateAxis2D) yaxis;
      int shape[] = lon_axis.getShape();
      int nj = shape[0];
      int ni = shape[1];

      int mini = Integer.MAX_VALUE, minj = Integer.MAX_VALUE;
      int maxi = -1, maxj = -1;

      // margolis 2/18/2010
      //minx = LatLonPointImpl.lonNormal( minx ); // <-- THIS IS NEW
      //maxx = LatLonPointImpl.lonNormal( maxx ); // <-- THIS IS NEW

      // brute force, examine every point LOOK BAD
      for (int j = 0; j < nj; j++) {
        for (int i = 0; i < ni; i++) {
          double lat = lat_axis.getCoordValue(j, i);
          double lon = lon_axis.getCoordValue(j, i);
          //lon = LatLonPointImpl.lonNormal( lon ); // <-- THIS IS NEW

          if ((lat >= miny) && (lat <= maxy) && (lon >= minx) && (lon <= maxx)) {
            if (i > maxi) maxi = i;
            if (i < mini) mini = i;
            if (j > maxj) maxj = j;
            if (j < minj) minj = j;
            //System.out.println(j+" "+i+" lat="+lat+" lon="+lon);
          }
        }
      }

      // this is the case where no points are included
      if ((mini > maxi) || (minj > maxj)) {
        mini = 0;
        minj = 0;
        maxi = -1;
        maxj = -1;
      }

      ArrayList<Range> list = new ArrayList<>();
      list.add(new Range(minj, maxj));
      list.add(new Range(mini, maxi));
      return list;

    } else {
      throw new IllegalArgumentException("must be 1D or 2D/LatLon ");
    }

  }

  /* private int getCrossing(CoordinateAxis2D axis) {
    for (int i=0; i<n; i++)
      if (axis.getCoordValue(i,j) > min) return i;
  } */

  /* private GeneralPath bbShape = null;
 public Shape getLatLonBoundingShape() {
   if (isLatLon())
     return getBoundingBox();

   if (bbShape == null) {
     ProjectionRect bb = getBoundingBox();
     Projection displayProjection = displayMap.getProjection();
     Projection dataProjection = getProjection();

     bbShape = new GeneralPath();

     LatLonPoint llpt = dataProjection.projToLatLon( bb.getX(), bb.getY());
     ProjectionPoint pt = displayProjection.latLonToProj( llpt);
     bbShape.lineTo(pt.getX(), pt.getY());

     llpt = dataProjection.projToLatLon( bb.getX(), bb.getY()+bb.getHeight());
     pt = displayProjection.latLonToProj( llpt);
     bbShape.lineTo(pt.getX(), pt.getY());

     llpt = dataProjection.projToLatLon( bb.getX()+bb.getWidth(), bb.getY()+bb.getHeight());
     pt = displayProjection.latLonToProj( llpt);
     bbShape.lineTo(pt.getX(), pt.getY());

     llpt = dataProjection.projToLatLon( bb.getX()+bb.getWidth(), bb.getY());
     pt = displayProjection.latLonToProj( llpt);
     bbShape.lineTo(pt.getX(), pt.getY());

     bbShape.closePath();
   }

   return bbShape;
 } */

  /**
   * String representation.
   */
  @Override
  public String toString() {
    Formatter buff = new Formatter();
    show(buff, false);
    return buff.toString();
  }

  @Override
  public void show(Formatter f, boolean showCoords) {
    f.format("Coordinate System (%s)%n", getName());

    if (getRunTimeAxis() != null) {
      f.format(" rt=%s (%s)", runTimeAxis.getNameAndDimensions(), runTimeAxis.getClass().getName());
      if (showCoords) showCoords(runTimeAxis, f);
      f.format("%n");
    }
    if (getEnsembleAxis() != null) {
      f.format(" ens=%s (%s)", ensembleAxis.getNameAndDimensions(), ensembleAxis.getClass().getName());
      if (showCoords) showCoords(ensembleAxis, f);
      f.format("%n");
    }
    if (getTimeAxis() != null) {
      f.format(" t=%s (%s)", tAxis.getNameAndDimensions(), tAxis.getClass().getName());
      if (showCoords) showCoords(tAxis, f);
      f.format("%n");
    }
    if (getVerticalAxis() != null) {
      f.format(" z=%s (%s)", vertZaxis.getNameAndDimensions(), vertZaxis.getClass().getName());
      if (showCoords) showCoords(vertZaxis, f);
      f.format("%n");
    }
    if (getYHorizAxis() != null) {
      f.format(" y=%s (%s)", horizYaxis.getNameAndDimensions(), horizYaxis.getClass().getName());
      if (showCoords) showCoords(horizYaxis, f);
      f.format("%n");
    }
    if (getXHorizAxis() != null) {
      f.format(" x=%s (%s)", horizXaxis.getNameAndDimensions(), horizXaxis.getClass().getName());
      if (showCoords) showCoords(horizXaxis, f);
      f.format("%n");
    }

    if (proj != null)
      f.format(" Projection: %s %s%n", proj.getName(), proj.paramsToString());
  }

  private void showCoords(CoordinateAxis axis, Formatter f) {
    try {
      if (axis instanceof CoordinateAxis1D && axis.isNumeric()) {
        CoordinateAxis1D axis1D = (CoordinateAxis1D) axis;
        if (!axis1D.isInterval()) {
          double[] e = axis1D.getCoordEdges();
          for (double anE : e) {
            f.format("%f,", anE);
          }
        } else {
          double[] b1 = axis1D.getBound1();
          double[] b2 = axis1D.getBound2();
          for (int i=0; i<b1.length; i++) {
            f.format("(%f,%f) = %f%n", b1[i], b2[i], b2[i] - b1[i]);
          }
        }
      } else {
        f.format("%s", NCdumpW.printVariableData(axis, null));
      }
    } catch (IOException ioe) {
      f.format(ioe.getMessage());
    }
    f.format(" %s%n", axis.getUnitsString());
  }

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

  @Override
  public List<CalendarDate> getCalendarDates() {
    if (timeTaxis != null)
      return timeTaxis.getCalendarDates();

    else if (getRunTimeAxis() != null)
      return makeCalendarDates2D();

    else
      return new ArrayList<>();
  }

  @Override
  public CalendarDateRange getCalendarDateRange() {
    if (timeTaxis != null)
      return timeTaxis.getCalendarDateRange();

    else if (getRunTimeAxis() != null) {
      List<CalendarDate>  cd = makeCalendarDates2D();
      int last = cd.size();
      return (last > 0) ? CalendarDateRange.of(cd.get(0), cd.get(last-1)) : null;

    } else
      return null;
  }

  private List<CalendarDate> makeCalendarDates2D() {
    Set<CalendarDate> dates = new HashSet<>();

    CoordinateAxis1DTime rtaxis = getRunTimeAxis();
    List<CalendarDate> runtimes = rtaxis.getCalendarDates();
    for (int i = 0; i < runtimes.size(); i++) {
      CoordinateAxis1DTime taxis = getTimeAxisForRun(i);
      if (taxis == null) throw new IllegalStateException();
      List<CalendarDate> times = taxis.getCalendarDates();
      for (CalendarDate time : times) dates.add(time);
    }

    // sorted list
    int n = dates.size();
    CalendarDate[] dd = dates.toArray(new CalendarDate[n]);
    List<CalendarDate> dateList = Arrays.asList(dd);
    Collections.sort(dateList);

    return dateList;
  }

  //////////////////////////////////////////////////////////////////////////////////////
  // cruft

  /**
   * Get the list of level names, to be used for user selection.
   * The ith one refers to the ith level coordinate.
   *
   * @return List of ucar.nc2.util.NamedObject, or empty list.
   */
  public List<NamedObject> getLevels() {
    if (vertZaxis == null)
      return new ArrayList<>(0);

    int n = (int) vertZaxis.getSize();
    List<NamedObject> levels = new ArrayList<>(n);
    for (int i = 0; i < n; i++)
      levels.add(new ucar.nc2.util.NamedAnything(vertZaxis.getCoordName(i), vertZaxis.getUnitsString()));

    return levels;
  }

 /**
   * Get the String name for the ith level(z) coordinate.
   *
   * @param index which level coordinate
   * @return level name
   */
  public String getLevelName(int index) {
    if ((vertZaxis == null) || (index < 0) || (index >= vertZaxis.getSize()))
      throw new IllegalArgumentException("getLevelName = " + index);
    return vertZaxis.getCoordName(index).trim();
  }

  /**
   * Get the index corresponding to the level name.
   *
   * @param name level name
   * @return level index, or -1 if not found
   */
  public int getLevelIndex(String name) {
    if ((vertZaxis == null) || (name == null)) return -1;

    for (int i = 0; i < vertZaxis.getSize(); i++) {
      if (vertZaxis.getCoordName(i).trim().equals(name))
        return i;
    }
    return -1;
  }

  /**
   * Get the list of time names, to be used for user selection.
   * The ith one refers to the ith time coordinate.
   *
   * @return List of ucar.nc2.util.NamedObject, or empty list.
   */
  public List<NamedObject> getTimes() {
    List<CalendarDate> cdates = getCalendarDates();
    List<NamedObject> times = new ArrayList<>( cdates.size());
    for (CalendarDate cd: cdates) {
      times.add(new ucar.nc2.util.NamedAnything(cd.toString(), "calendar date"));
    }
    return times;
  }

   ///////////////////////////////////////////////////////////////////////////
  // deprecated

  /**
   * Given a point in x,y coordinate space, find the x,y index in the coordinate system.
   *
   * @deprecated use findXYindexFromCoord
   */
  public int[] findXYCoordElement(double x_coord, double y_coord, int[] result) {
    return findXYindexFromCoord(x_coord, y_coord, result);
  }

  /**
   * Get the date range
   * @return date range
   * @deprecated  use getCalendarDateRange
   */
  public DateRange getDateRange() {
    Date[] dates = getTimeDates();
    if (dates.length > 0)
      return new DateRange(dates[0], dates[dates.length - 1]);
    return null;
  }

  /**
   * Get the list of times as Dates.
   * If 2D, return list of unique dates.
   *
   * @return array of java.util.Date, or Date[0].
   * @deprecated  use getCalendarDates
   */
  public java.util.Date[] getTimeDates() {
    if ((timeTaxis != null) && (timeTaxis.getSize() > 0)) {
      return timeTaxis.getTimeDates();

    } else if ((tAxis != null) && (tAxis.getSize() > 0))  {
      return makeTimes2D();
    }

    return new Date[0];
  }

  private Date[] makeTimes2D() {
    Set<Date> dates = new HashSet<>();

    try {
      // common case: see if it has a valid udunits unit
      String units = tAxis.getUnitsString();
      if (units != null && SimpleUnit.isDateUnit(units) && tAxis.getDataType().isNumeric()) {
        DateUnit du = new DateUnit(units);
        Array data = tAxis.read();
        data.resetLocalIterator();
        while (data.hasNext()) {
          Date d = du.makeDate(data.nextDouble());
          dates.add(d);
        }

      } else if (tAxis.getDataType() == DataType.STRING) {
        // otherwise, see if its a String or CHAR, and if we can parse the values as an ISO date
        DateFormatter formatter = new DateFormatter();
        Array data = tAxis.read();
        data.resetLocalIterator();
        while (data.hasNext()) {
          Date d = formatter.getISODate((String) data.next());
          dates.add(d);
        }

      } else if (tAxis.getDataType() == DataType.CHAR) {
        DateFormatter formatter = new DateFormatter();
        ArrayChar data = (ArrayChar) tAxis.read();
        ArrayChar.StringIterator iter = data.getStringIterator();
        while (iter.hasNext()) {
          Date d = formatter.getISODate(iter.next());
          dates.add(d);
        }

      } else {
        return new Date[0];
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }

    // sorted list
    int n = dates.size();
    Date[] dd = dates.toArray(new Date[n]);
    List<Date> dateList = Arrays.asList(dd);
    Collections.sort(dateList);
    Date[] timeDates = new Date[n];
    int count=0;
    for (Date d : dateList)
      timeDates[count++] = d;

    return timeDates;
  }

  /* old way
  private boolean makeTimes1D() {
    int n = (int) timeTaxis.getSize();
    timeDates = new Date[n];

    // common case: see if it has a valid udunits unit
    try {
      DateUnit du = null;
      String units = timeTaxis.getUnitsString();
      if (units != null)
        du = new DateUnit(units);
      for (int i = 0; i < n; i++) {
        Date d = du.makeDate(timeTaxis.getCoordValue(i));
        timeDates[i] = d;
      }
      isDate = true;
      return true;
    } catch (Exception e) {
      // ok to fall through
    }

    // otherwise, see if its a String, and if we can parse the values as an ISO date
    if ((timeTaxis.getDataType() == DataType.STRING) || (timeTaxis.getDataType() == DataType.CHAR)) {
      DateFormatter formatter = new DateFormatter();
      for (int i = 0; i < n; i++) {
        String coordValue = timeTaxis.getCoordName(i);
        Date d = formatter.getISODate(coordValue);
        if (d == null) {
          isDate = false;
          return false;
        } else {
          timeDates[i] = d;
        }
      }
      isDate = true;
      return true;
    }

    return false;
  }  */

  /**
   * Get the string name for the ith time coordinate.
   *
   * @param index which time coordinate
   * @return time name.
   * @deprecated
   */
  public String getTimeName(int index) {
    List<CalendarDate> cdates = getCalendarDates();

    if ((index < 0) || (index >= cdates.size()))
      throw new IllegalArgumentException("getTimeName illegal index = " + index);

    return cdates.get(index).toString();
  }

  /**
   * Get the index corresponding to the time name.
   *
   * @param name time name
   * @return time index, or -1 if not found
   * @deprecated
   */
  public int getTimeIndex(String name) {
    List<CalendarDate> cdates = getCalendarDates();
    for (int i=0; i < cdates.size(); i++) {
      if( cdates.get(i).toString().equals(name)) return i;
    }

    return -1;
  }

  /**
   * Only works if coordsys has 1d time axis
   * @deprecated use CoordinateAxis1DTime.findTimeIndexFromDate
   */
  public int findTimeIndexFromDate(java.util.Date d) {
    if (timeTaxis == null) return -1;
    return timeTaxis.findTimeIndexFromDate(d);
  }

  ///////////////////////////////////////////////////////////////////////
  // experimental

  static private double getMinOrMaxLon(double lon1, double lon2, boolean wantMin) {
    double midpoint = (lon1 + lon2) / 2;
    lon1 = LatLonPointImpl.lonNormal(lon1, midpoint);
    lon2 = LatLonPointImpl.lonNormal(lon2, midpoint);

    return wantMin ? Math.min(lon1, lon2) : Math.max(lon1, lon2);
  }

  static public LatLonRect getLatLonBoundingBox(Projection proj, double startx, double starty, double endx, double endy) {

    if (proj instanceof LatLonProjection) {
      double deltaLat = endy - starty;
      double deltaLon = endx - startx;

      LatLonPoint llpt = new LatLonPointImpl(starty, startx);
      return new LatLonRect(llpt, deltaLat, deltaLon);

    }

    ProjectionRect bb = new ProjectionRect(startx, starty, endx, endy);

    // look at all 4 corners of the bounding box
    LatLonPointImpl llpt = (LatLonPointImpl) proj.projToLatLon(bb.getLowerLeftPoint(), new LatLonPointImpl());
    LatLonPointImpl lrpt = (LatLonPointImpl) proj.projToLatLon(bb.getLowerRightPoint(), new LatLonPointImpl());
    LatLonPointImpl urpt = (LatLonPointImpl) proj.projToLatLon(bb.getUpperRightPoint(), new LatLonPointImpl());
    LatLonPointImpl ulpt = (LatLonPointImpl) proj.projToLatLon(bb.getUpperLeftPoint(), new LatLonPointImpl());

    // Check if grid contains poles. LOOK disabled
    boolean includesNorthPole = false;
    /* int[] resultNP = new int[2];
    resultNP = findXYindexFromLatLon(90.0, 0, null);
    if (resultNP[0] != -1 && resultNP[1] != -1)
      includesNorthPole = true;  */

    boolean includesSouthPole = false;
    /* int[] resultSP = new int[2];
    resultSP = findXYindexFromLatLon(-90.0, 0, null);
    if (resultSP[0] != -1 && resultSP[1] != -1)
      includesSouthPole = true;  */

    LatLonRect llbb;

    if (includesNorthPole && !includesSouthPole) {
      llbb = new LatLonRect(llpt, new LatLonPointImpl(90.0, 0.0)); // ??? lon=???
      llbb.extend(lrpt);
      llbb.extend(urpt);
      llbb.extend(ulpt);

    } else if (includesSouthPole && !includesNorthPole) {
      llbb = new LatLonRect(llpt, new LatLonPointImpl(-90.0, -180.0)); // ??? lon=???
      llbb.extend(lrpt);
      llbb.extend(urpt);
      llbb.extend(ulpt);

    } else {
      double latMin = Math.min(llpt.getLatitude(), lrpt.getLatitude());
      double latMax = Math.max(ulpt.getLatitude(), urpt.getLatitude());

      // longitude is a bit tricky as usual
      double lonMin = getMinOrMaxLon(llpt.getLongitude(), ulpt.getLongitude(), true);
      double lonMax = getMinOrMaxLon(lrpt.getLongitude(), urpt.getLongitude(), false);

      llpt.set(latMin, lonMin);
      urpt.set(latMax, lonMax);

      llbb = new LatLonRect(llpt, urpt);
    }
    return llbb;
  }
}


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

import ucar.nc2.dt.grid.GeoGrid;
import ucar.nc2.ui.event.ActionSourceListener;
import ucar.nc2.ui.event.ActionValueEvent;
import ucar.nc2.ui.event.ActionValueListener;
import ucar.nc2.ui.table.JTableSorted;
import ucar.nc2.ui.table.TableRow;
import ucar.nc2.ui.table.TableRowAbstract;
import ucar.nc2.util.NamedObject;
import ucar.util.prefs.ui.Field;
import ucar.util.prefs.ui.PrefPanel;

import javax.swing.*;
import javax.swing.border.EtchedBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.Iterator;

/**
 * SuperComboBox is a complete rewrite of JComboBox;
 * it does not extend JComboBox (!)
 *
 * Items added may implement NamedObject, in which case getName() is used as the row name,
 * and getDescription() is used as the tooltip. Otherwise, o.toString() is used as the row name,
 * and there is no tooltip. The row names must be unique.
 *
 * An ActionValueEvent is thrown when the selection is changed through manipulation of the widget.
 * The selection can be obtained from actionEvent.getValue(), which returns the the selection row name.
 *
 * The caller can change the selection by calling setSelectionByName(). In this case,
 * no event is thrown.
 *
 * @author John Caron
 */
public class SuperComboBox extends JPanel {

  // data
  private ArrayList list = new ArrayList(); // list of TableRow objects

  // UI stuff
  private String name;
  private MyTextField text; // this is what shows all the time
  private JButton next, prev, down; // arrow button
  private JWindow pulldown; // pulldown menu
  //private Popup popup = null;
  private JTableSorted table; // inside the pulldown
  private IndependentWindow loopControl = null;  // loop control

  private ActionSourceListener actionSource;

  private boolean isNamedObject = false;
  private boolean eventOK = true;               // disallow events to prevent infinite loop
  private boolean sendExternalEvent = true;     // disallow events to prevent infinite loop
  private boolean immediateMode = false;        // used for looping

  private boolean debug = false, debugEvent = false;

  private int height = 222;
  private int width = 100;

   /** default is one column, with an iterator of NamedObjects
    * @param parent parent container
    * @param name column name
    * @param sortOK  true allow sorting, column adding and removing
    * @param iter Iterator of objects inside the combobox.
    */
  public SuperComboBox(RootPaneContainer parent, String name, boolean sortOK, Iterator iter) {
    this.name = name;

      // create JLabel
    text = new MyTextField(" ");
    text.setToolTipText(name);

      // create arrow buttons
    JPanel seqPanel = new JPanel(new GridLayout(1,2));
    prev = makeButton(SpinIcon.TypeLeft);
    prev.setToolTipText("previous");
    seqPanel.add( prev);

    next = makeButton(SpinIcon.TypeRight);
    next.setToolTipText("next");
    seqPanel.add( next);

    //JPanel bPanel = new JPanel(new GridLayout(2,1));
    JPanel bPanel = new JPanel(new BorderLayout());
    down = makeButton(SpinIcon.TypeDown);
    down.setToolTipText("show menu");
    bPanel.add( seqPanel, BorderLayout.NORTH);
    bPanel.add( down, BorderLayout.SOUTH);

      // the jtable
    String [] colNames = new String [1];
    colNames[0] = name;

    table = new JTableSorted(colNames, list);
    table.setSortOK( sortOK);
    if (iter != null)
      setCollection( iter);

      // the pulldown menu list
    JFrame parentComponent = (parent != null) && (parent instanceof JFrame) ? (JFrame) parent : null;
    pulldown = new JWindow(parentComponent);
    pulldown.getContentPane().add( table);
    pulldown.pack();

      // put it together
    setBorder(new EtchedBorder(EtchedBorder.RAISED));
    setLayout(new BorderLayout());
    //add(seqPanel, BorderLayout.WEST);
    add(text, BorderLayout.CENTER);
    add(bPanel, BorderLayout.EAST);

    // size
    //text.setColumns(numTextCols);

    // add the listeners
    text.addMouseListener( new MyMouseAdapter() {
      public void click(MouseEvent e) {
        if (SwingUtilities.isLeftMouseButton(e))
          showPulldownMenu();
      }
    });

    // popup menu
    LoopControl popup = new LoopControl();
    text.addMouseListener( popup);
    loopControl = popup.getLoopControl();

    prev.addMouseListener( new MouseAdapter() {
      public void mouseClicked(MouseEvent e) {
        table.incrSelected(false);
      }
    });
    next.addMouseListener( new MouseAdapter() {
      public void mouseClicked(MouseEvent e) {
        table.incrSelected(true);
      }
    });
    down.addMouseListener( new MouseAdapter() {
      public void mouseClicked(MouseEvent e) {
        showPulldownMenu();
      }
    });

    table.addListSelectionListener( new ListSelectionListener() {
      public void valueChanged(ListSelectionEvent e) {
        if (debugEvent) System.out.println(" JTable event ");
        if (eventOK && !e.getValueIsAdjusting()) {
          setSelection();
          hidePulldownMenu();
        }
      }
    });
    table.getTable().addMouseListener( new MyMouseAdapter() {
      public void click(MouseEvent e) {
        //System.out.println("table.table click");
        hidePulldownMenu();
      }
    });

      // event management
    actionSource = new ActionSourceListener(name) {
      public void actionPerformed( ActionValueEvent e) {
        if (debugEvent) System.out.println(" actionSource event "+e);
        setSelectedByName( e.getValue().toString());
      }
    };
  }

    /** add ActionValueListener listener */
  public void addActionValueListener( ActionValueListener l) { actionSource.addActionValueListener(l); }
    /** remove ActionValueListener listener */
  public void removeActionValueListener( ActionValueListener l) { actionSource.removeActionValueListener(l); }

    /** better way to do event management */
  public ActionSourceListener getActionSourceListener() { return actionSource; }

       /** get the LoopControl Window associated with this list */
  public IndependentWindow getLoopControl() { return loopControl; }
       /** get the name associated with this list */
  public String getName() { return name; }

  /**
   * Set the list of things to be selected.
   * Iterator may return objects of type NamedObjects,
   * otherwise will use object.toString()
   */
  public void setCollection( Iterator iter) {
    setCollection(iter, false);
  }

  public void setCollection( Iterator iter, boolean keepIndex) {
    eventOK = false;
    list = new ArrayList();

    if (iter != null) {
      FontMetrics fm = text.getFontMetrics( text.getFont());

      int width = 0;
      while (iter.hasNext()) {
        Object o = iter.next();
        TableRow row;
        if (o instanceof GeoGrid) // LOOK hackawack
          row = new GeoGridRow((NamedObject) o);
        else if (o instanceof NamedObject)
          row = new NamedObjectRow((NamedObject) o);
        else
          row = new SimpleRow(o);

        String s = row.getValueAt(0).toString().trim();
        int slen = fm.stringWidth(s);
        width = Math.max(width, slen);

        list.add(row);
      }
      // resize
      Dimension d = text.getPreferredSize();
      d.width = width+10;
      text.setPreferredSize(d);
      text.revalidate();
    }

    if (debugEvent) System.out.println(" New collection set = ");
    table.setList(list);
    if (list.size() == 0)
      setLabel("none");
    else {
      int currIndex = getSelectedIndex();
      if (currIndex >= list.size() || currIndex < 0 || !keepIndex)
        setSelectedByIndex(0);
    }
    eventOK = true;
  }

  /** Set the displayed text. Typically its only used when the list is empty.
   */
  public void setLabel(String s) {
    text.setText( s);
  }

  /** Get the currently selected object. May be null.
   */
  public Object getSelectedObject() {
    TableRow selected = getSelectedRow();
    return (selected == null) ? null : selected.getUserObject();
  }

  /** Get the index of the currently selected object in the list.
   * If sortOK, then this may not be the original index.
   * @return index of selected object, or -1 if none selected.
   */
  public int getSelectedIndex() {
    return table.getSelectedRowIndex();
  }

  /** Set the currently selected object using its choice name.
   * Note that no event is sent due to this call.
   * @param choiceName name of object to match.
   * @return index of selection, or -1 if not found;
   */
  public int setSelectedByName( String choiceName) {
    for (int i=0; i<list.size(); i++) {
      TableRow row = (TableRow) list.get(i);
      String value = row.getValueAt(0).toString();
      if (choiceName.equals(value)) {
        sendExternalEvent = false;
        setSelectedByIndex(i);
        sendExternalEvent = true;
        return i;
      }
    }
    // setSelectedByIndex(-1);  // force it to 0 or none
    return -1;
  }

  /** Set the currently selected object using its index.
   * If sortOK, then this may not be the original index.
   * @param index of selected object.
   */
   public void setSelectedByIndex( int index) {
    if ((index >= 0) && (index < list.size())) {
      table.setSelected(index);
      setSelection();
    } else if ( list.size() > 0) {
      table.setSelected(0);
      setSelection();
    } else
      setLabel("none");
  }

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

  private TableRow getSelectedRow() {
    return table.getSelected();
  }

  private void setSelection() {
    TableRow selected = getSelectedRow();
    if (debug) System.out.println(" setSelection = "+ selected);
    if (selected != null) {
      Object selectedObject = selected.getValueAt(0);
      String selectedName = selectedObject.toString();
      text.setText( selectedName.trim());
      if ((selected instanceof NamedObjectRow) || (selected instanceof GeoGridRow)) {
        text.setToolTipText( ((NamedObject)selected.getUserObject()).getDescription());
      }
      repaint();

      if (selectedObject instanceof GeoGridRow) {
        selectedObject = ((GeoGridRow) selectedObject).getUserObject();
      }

      if (sendExternalEvent) {
        if (debugEvent) System.out.println("--->SuperCombo send event "+selectedName);
        if (immediateMode)
          actionSource.fireActionValueEvent( "redrawImmediate", selectedObject);
        else
          actionSource.fireActionValueEvent( ActionSourceListener.SELECTED, selectedObject);
      }
    }
  }

  private void showPulldownMenu() {
    if (pulldown.isShowing())
      pulldown.setVisible(false);
    else {
      Dimension d = new Dimension(getWidth(), height);
      pulldown.setSize(d);
      Point p = text.getLocationOnScreen();
      p.y += text.getHeight();
      pulldown.setLocation(p);
      pulldown.setVisible(true);
    }
  }

  private void hidePulldownMenu() {
    if (pulldown.isShowing()) {
      pulldown.setVisible(false);
      //System.out.println("hidePulldownMenu");
    }
  }

  /* private void showPulldownMenu() {
    hidePulldownMenu();
    Point p = text.getLocationOnScreen();
    p.y += text.getHeight();
    PopupFactory factory = PopupFactory.getSharedInstance();
    popup = factory.getPopup(this, table, p.x, p.y);
    popup.show();
  }

  private void hidePulldownMenu() {
    if (popup != null) {
      popup.hide();
      popup = null;
    }
  } */

  private JButton makeButton(SpinIcon.Type type) {
      SpinIcon icon = new SpinIcon(type);
      JButton butt  = new JButton( icon);
      Insets i = new Insets(0,0,0,0);
      butt.setMargin( i);
      butt.setBorderPainted(false);
      butt.setFocusPainted(false);
      butt.setPreferredSize(new Dimension(icon.getIconWidth()+2, icon.getIconHeight()+2));
      return butt;
  }

  private static class SimpleRow extends TableRowAbstract {
    Object o;
    SimpleRow( Object o){ this.o = o; }
    public Object getValueAt( int col) { return o; }
    public Object getUserObject() { return o; }
  }

  private static class NamedObjectRow extends TableRowAbstract implements NamedObject {
    NamedObject o;
    NamedObjectRow( NamedObject o){ this.o = o; }
    public Object getValueAt( int col) { return this; }
    public Object getUserObject() { return o; }
    public Object getValue() { return o; }

    public String getName() { return o.getName(); }
    public String getDescription() { return o.getDescription(); }
    public String toString() { return getName(); }
  }

  private static class GeoGridRow extends TableRowAbstract implements NamedObject {
    NamedObject o;
    GeoGridRow(NamedObject o){ this.o = o; }
    public Object getValueAt( int col) { return this; }
    public Object getUserObject() { return o; }
    public Object getValue() { return o; }

    public String getName() { return o.getName(); }
    public String getDescription() { return o.getDescription(); }
    public String toString() {
      String desc = o.getDescription();
      if (desc == null || desc.isEmpty() || desc.trim().equals("-"))
        return o.getName();
      else
        return o.getName() +" == "+ o.getDescription();
    }
  }

  private class MyTextField extends JLabel {
    private int arrow_size = 4;
    private boolean wasDragged = false;
    private Rectangle b;
    private int nitems = 0;
    private int currentItem = 0;

    MyTextField( String name) {
      super(name);
      setOpaque(true);
      setBackground(Color.white);
      setForeground(Color.black);

      addMouseListener( new MyMouseListener());
      addMouseMotionListener( new MyMouseMotionListener());
    }

    public void paintComponent(Graphics g) {
      super.paintComponent(g);

      g.setColor( Color.black);
//      g.setColor( component.isEnabled() ? MetalLookAndFeel.getControlInfo() :
 //                                           MetalLookAndFeel.getControlShadow() );
      b = getBounds();
      nitems = list.size();  // number of items

      int posx = getItemPos();
      int line = b.height-1;
      for (int w2=arrow_size; w2>=0; w2--) {
        g.drawLine( posx-w2, line, posx+w2, line );
        line--;
      }
    }

        // return slider indicator position for currently selected item
    protected int getItemPos() {
      if (nitems < 1)
        return -arrow_size;   // dont show indicator
      else if (nitems == 1)
        return b.width/2;   // indicator in center

      int item = table.getSelectedRowIndex();  // selected item
      int eff_width = b.width - 2*arrow_size;   // effective width
      int pixel =  (item * eff_width)/(nitems-1);  // divided into n-1 intervals
      return pixel+arrow_size;
    }

    // return item selected by this pixel position
    protected int getItem( int pixel) {
      if (nitems < 2)
        return 0;

      int eff_width = b.width - 2*arrow_size;   // effective width
      double fitem = ((double) (pixel-arrow_size)*(nitems-1)) / eff_width;
      int item =  (int)(fitem+.5);
      item = Math.max( Math.min(item, nitems-1), 0);
      return item;
    }

    private class MyMouseListener extends MouseAdapter {
      public void mousePressed(MouseEvent anEvent) {
        sendExternalEvent = false;
        wasDragged = false;
        currentItem = table.getSelectedRowIndex();
      }

      public void mouseReleased(MouseEvent anEvent) {
        sendExternalEvent = true;
        if (wasDragged) {
          int item = getItem( anEvent.getX());
          if (item != currentItem) {
            setSelection();
            //if (debug) System.out.println("release select "+item);
          }
        }
        wasDragged = false;
      }
    }

    private class MyMouseMotionListener extends MouseMotionAdapter {

      public void mouseDragged(MouseEvent anEvent) {
        int item = getItem( anEvent.getX());
        table.setSelected(item);
        MyTextField.this.repaint();
        wasDragged = true;
      }
    }

  } // inner class MyTextField

  private class LoopControl extends ucar.nc2.ui.widget.PopupMenu.PopupTriggerListener {
    private IndependentWindow iw;

    private JPanel loopPanel;
    private JButton moreOrLess;
    private JSpinner stepSpinner;
    private AbstractButton loopButt, helpButt;
    private PrefPanel ifPanel;
    private Field.Int stepIF;
    private Field.Text startIF;

    private AbstractAction play, fastforward, stop, rewind, back, next, prev;
    private AbstractAction loopAct, helpAct;
    private boolean stopped, forward, first = true, continuous = true, less = true;
    private int step = 1, start = -1;

    LoopControl () {
      loopPanel = new JPanel();

      // create VCR buttons
      play = new AbstractAction() {
        public void actionPerformed(ActionEvent e) {
          start(true);
        }
      };
      BAMutil.setActionProperties( play, "VCRPlay", "play", false, 'S', KeyEvent.VK_NUMPAD6);
      AbstractButton b = BAMutil.addActionToContainer( loopPanel, play);

      fastforward = new AbstractAction() {
        public void actionPerformed(ActionEvent e) {
          setSelectedByIndex(list.size()-1);
        }
      };
      BAMutil.setActionProperties( fastforward, "VCRFastForward", "go to end", false, 'F', KeyEvent.VK_NUMPAD1);
      b = BAMutil.addActionToContainer( loopPanel, fastforward);

      next = new AbstractAction() {
        public void actionPerformed(ActionEvent e) {
          int current = incr(true, false);
          if (current >= 0) {
            setSelectedByIndex(current);
            text.paintImmediately(text.getBounds());
          }
        }
      };
      BAMutil.setActionProperties( next, "VCRNextFrame", "Next frame", false, 'N', KeyEvent.VK_PAGE_DOWN);
      b = BAMutil.addActionToContainer( loopPanel, next);

      stop = new AbstractAction() {
        public void actionPerformed(ActionEvent e) {
          stopped = true;
        }
      };
      BAMutil.setActionProperties( stop, "VCRStop", "stop", false, 'S', KeyEvent.VK_ESCAPE);
      b = BAMutil.addActionToContainer( loopPanel, stop);

      prev = new AbstractAction() {
        public void actionPerformed(ActionEvent e) {
          int current = incr(false, false);
          if (current >= 0) {
            setSelectedByIndex(current);
            text.paintImmediately(text.getBounds());
          }
        }
      };
      BAMutil.setActionProperties( prev, "VCRPrevFrame", "Previous frame", false, 'P', KeyEvent.VK_PAGE_UP);
      b = BAMutil.addActionToContainer( loopPanel, prev);

      rewind = new AbstractAction() {
        public void actionPerformed(ActionEvent e) {
          setSelectedByIndex(0);
        }
      };
      BAMutil.setActionProperties( rewind, "VCRRewind", "rewind", false, 'R', KeyEvent.VK_NUMPAD7);
      b = BAMutil.addActionToContainer( loopPanel, rewind);

      back = new AbstractAction() {
        public void actionPerformed(ActionEvent e) {
          start(false);
        }
      };
      BAMutil.setActionProperties( back, "VCRBack", "play backwards", false, 'B', KeyEvent.VK_NUMPAD4);
      b = BAMutil.addActionToContainer( loopPanel, back);

      moreOrLess = new JButton( new SpinIcon( SpinIcon.TypeRight));
      moreOrLess.setBorder(BorderFactory.createEmptyBorder());
      moreOrLess.setMargin(new Insets(0,0,0,0));
      moreOrLess.addActionListener( new AbstractAction() {
        public void actionPerformed(ActionEvent e) {
          if (less) makeMore();
          else makeLess();
        }
      });
      loopPanel.add(moreOrLess);

      iw = new IndependentWindow(name+" loop", null, loopPanel);
      iw.setResizable( false);

        // these arent added to the panel right away
      loopAct = new AbstractAction() {
        public void actionPerformed(ActionEvent e) {
          Boolean state = (Boolean) getValue(BAMutil.STATE);
          continuous = state.booleanValue();
        }
      };
      BAMutil.setActionProperties( loopAct, "MovieLoop", "continuous loop", true, 'L', 0);

      helpAct = new AbstractAction() {
        public void actionPerformed(ActionEvent evt) {
          // faqo.apputil.Help.getDefaultHelp().gotoTarget("movieLoop");
        }
      };
      BAMutil.setActionProperties( helpAct, "Help", "online Help...", false, 'H', KeyEvent.VK_H);

      stepSpinner = new JSpinner();
      stepSpinner.setToolTipText("step");

      ifPanel = new PrefPanel("loopControl", null);
      stepIF = ifPanel.addIntField("step", "step", 1);
      startIF = ifPanel.addTextField("start", "start", "    0");
      ifPanel.finish(false);
    }

    private void makeMore() {
      loopButt = BAMutil.addActionToContainer( loopPanel, loopAct);
      loopAct.putValue(BAMutil.STATE, new Boolean(continuous));
      helpButt = BAMutil.addActionToContainer( loopPanel, helpAct);
      loopPanel.add( ifPanel);

      moreOrLess.setIcon( new SpinIcon( SpinIcon.TypeLeft));
      moreOrLess.setToolTipText("less");

      loopPanel.revalidate();
      iw.pack();
      less = false;
    }

    private void makeLess() {
      loopPanel.remove(loopButt);
      loopPanel.remove(helpButt);
      loopPanel.remove(ifPanel);

      moreOrLess.setIcon( new SpinIcon( SpinIcon.TypeRight));
      moreOrLess.setToolTipText("more");

      loopPanel.revalidate();
      iw.pack();
      less = true;
    }

    IndependentWindow getLoopControl() { return iw; }
    public void showPopup(MouseEvent e) {
      if (first) {
        Point pt = new Point( 0, 0);
        SwingUtilities.convertPointToScreen( pt, SuperComboBox.this);
        iw.setLocation( pt);
        first = false;
      }
      iw.show();
    }

    private void start(boolean forward) {
      this.forward = forward;
      eventOK = false;
      immediateMode = true;
      stopped = false;

      ifPanel.accept();
      step = Math.min(stepIF.getInt(), 1);

      start = -1;
      String startName = startIF.getText();
      if ((startName != null) && (startName.length() > 0)) {
        start = setSelectedByName(startName);
      }
      if (debug) System.out.println(" start = "+start+" step = "+step);

      SwingUtilities.invokeLater( new RunLoop());  // execute on eventTread
    }

    private int incr( boolean forward, boolean continuous) {
      int current = getSelectedIndex();
      if (forward) current += step;
      else current -= step;
      if (!continuous && ((current < 0) || (current >= list.size()))) {
        return -1;
      }

      if (current >= list.size())
        current = (start >= 0) ? start : 0;
      if (current < 0)
        current = (start >= 0) ? start : list.size() - 1;
      return current;
    }

    class RunLoop implements Runnable {
      public void run() {
        loopPanel.repaint();
        if (stopped) { stop(); return; }

        int current = incr(forward, continuous);
        if (current < 0) {
          stop();
          return;
        }

          // goto next
        setSelectedByIndex(current);
        text.paintImmediately(text.getBounds());
          // set another event
        SwingUtilities.invokeLater( new RunLoop());
      }

      private void stop() {
        immediateMode = false;
        eventOK = true;
        /* if (Debug.isSet("timing/loop")) {
          long tookTime = System.currentTimeMillis() - startTime;
          System.out.println("timing/loop: "+list.size()+" " + tookTime*.001 + " seconds");
        } */
      }

    }

  }  // inner class LoopControl

  public static void main(String args[]) {

    JFrame frame = new JFrame("Test Combo Box");
    frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {System.exit(0);}
    });

    ArrayList a = new ArrayList(30);
    for (int i=0; i<30; i++)
      a.add("hifdsjflkjslfk "+i);
    SuperComboBox scb = new SuperComboBox(frame, "myTestdjdslkfjslkj", true, a.iterator());
    JComboBox cb = new JComboBox();
    for (int i=0; i<a.size(); i++)
      cb.addItem(a.get(i));

    JPanel main = new JPanel(new FlowLayout());
    frame.getContentPane().add(main);
    main.setPreferredSize(new Dimension(200, 200));
    main.add( scb);
    main.add( cb);

    frame.pack();
    frame.setLocation(300, 300);
    frame.setVisible(true);
  }

}


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

package thredds.inventory.partition;

import thredds.filesystem.MFileOS7;
import thredds.inventory.CollectionAbstract;
import thredds.inventory.MFile;
import ucar.nc2.util.CloseableIterator;

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.*;

/**
 * Manage MFiles from one directory.
 * Doesnt know about parents or children.
 * Use getFileIterator() for best performance on large directories
 *
 * @author caron
 * @since 11/16/13
 */
public class DirectoryCollection extends CollectionAbstract {

  /**
   * Create standard name = topCollectionName + last directory
   * @param topCollectionName from config, name of the collection
   * @param dir directory for this
   * @return  standard collection name, to name the index file
   */
  public static String makeCollectionName(String topCollectionName, Path dir) {
    int last = dir.getNameCount()-1;
    Path lastDir = dir.getName(last);
    String lastDirName = lastDir.toString();
    return topCollectionName +"-" + lastDirName;
  }

  /**
   * Create standard name = topCollectionName + last directory
   * @param topCollectionName from config, name of the collection
   * @param dir directory for this
   * @return  standard collection name, to name the index file
   */
  public static Path makeCollectionIndexPath(String topCollectionName, Path dir, String suffix) {
    String collectionName = makeCollectionName(topCollectionName, dir);
    return Paths.get(dir.toString(), collectionName + suffix);
  }

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

  final String topCollection;
  final Path collectionDir;              //  directory for this collection
  final long olderThanMillis;
  final boolean isTop;

  public DirectoryCollection(String topCollectionName, String topDirS, boolean isTop, String olderThan, org.slf4j.Logger logger) {
    this(topCollectionName, Paths.get(topDirS), isTop, olderThan, logger);
  }

  public DirectoryCollection(String topCollectionName, Path collectionDir, boolean isTop, String olderThan, org.slf4j.Logger logger) {
    super(null, logger);
    this.topCollection = cleanName(topCollectionName);
    this.collectionDir = collectionDir;
    this.collectionName = isTop ? this.topCollection : makeCollectionName(topCollection, collectionDir);
    this.isTop = isTop;

    this.olderThanMillis = parseOlderThanString(olderThan);
    if (debug) System.out.printf("Open DirectoryCollection %s%n", collectionName);
  }

  @Override
  public String getRoot() {
    return collectionDir.toString();
  }

  @Override
  public String getIndexFilename(String suffix) {
    if (isTop) return super.getIndexFilename(suffix);
    Path indexPath = DirectoryCollection.makeCollectionIndexPath(topCollection, collectionDir, suffix);
    return indexPath.toString();
  }

  @Override
  public Iterable<MFile> getFilesSorted() throws IOException {
    return makeFileListSorted();
  }

  @Override
  public CloseableIterator<MFile> getFileIterator() throws IOException {
    return new MyFileIterator(collectionDir);
  }

  @Override
  public void close() {
    if (debug) System.out.printf("Close DirectoryCollection %s%n", collectionName);
  }

  // returns everything in the current directory, subject to sfilter
  private class MyFileIterator implements CloseableIterator<MFile> {
    int debugNum;
    DirectoryStream<Path> dirStream;
    Iterator<Path> dirStreamIterator;
    MFile nextMFile;
    int count = 0;

    MyFileIterator(Path dir) throws IOException {
      if (debug) {
        debugNum = debugCount++;
        System.out.printf(" MyFileIterator %s (%d)", dir, debugNum);
      }
      try {
        dirStream = Files.newDirectoryStream(dir, new MyStreamFilter());
        dirStreamIterator = dirStream.iterator();
      } catch (IOException ioe) {
        logger.error("Files.newDirectoryStream failed to open directory "+dir.getFileName(), ioe);
        throw ioe;
      }
    }

    public boolean hasNext() {
      while (true) {
        // if (debug && count % 100 == 0) System.out.printf("%d ", count);
        count++;
        if (!dirStreamIterator.hasNext()) {
          nextMFile = null;
          return false;
        }

        long now = System.currentTimeMillis();
        try {
          Path nextPath = dirStreamIterator.next();
          BasicFileAttributes attr =  Files.readAttributes(nextPath, BasicFileAttributes.class);
          if (attr.isDirectory()) continue;
          FileTime last = attr.lastModifiedTime();
          //System.out.printf("%s%n", last);
          long millisSinceModified = now - last.toMillis();
          if (millisSinceModified < olderThanMillis)
            continue;
          nextMFile = new MFileOS7(nextPath, attr);
          return true;

       } catch (IOException e) {
         throw new RuntimeException(e);
       }
      }
    }

    public MFile next() {
      if (nextMFile == null) throw new NoSuchElementException();
      return nextMFile;
    }

    public void remove() {
      throw new UnsupportedOperationException();
    }

    // better alternative is for caller to send in callback (Visitor pattern)
    // then we could use the try-with-resource
    public void close() throws IOException {
      if (debug) System.out.printf(" closed %d (%d)%n", count, debugNum);
      dirStream.close();
    }
  }

  ////////////////////////////////////////////////////////////////////////////////////////////
  private static final boolean debug = false;
  private static int debugCount = 0;
  // this idiom keeps the iterator from escaping, so that we can use try-with-resource, and ensure DirectoryStream closes. like++
  public void iterateOverMFileCollection(Visitor visit) throws IOException {
    if (debug) System.out.printf(" iterateOverMFileCollection %s ", collectionDir);
    int count = 0;
    try (DirectoryStream<Path> ds = Files.newDirectoryStream(collectionDir, new MyStreamFilter())) {
      for (Path p : ds) {
        try {
          BasicFileAttributes attr = Files.readAttributes(p, BasicFileAttributes.class);
          if (!attr.isDirectory())
            visit.consume(new MFileOS7(p));
          if (debug) System.out.printf("%d ", count++);
        } catch (IOException ioe) {
          // catch error and skip file
          logger.error("Failed to read attributes from file found in Files.newDirectoryStream ", ioe);
        }
      }
    }
    if (debug) System.out.printf("%d%n", count);
  }

  public interface Visitor {
     void consume(MFile mfile);
  }

}


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

package thredds.ui.monitor;

import ucar.httpservices.*;
import ucar.nc2.util.CancelTask;
import ucar.nc2.util.net.HttpClientManager;

import java.io.*;
import javax.swing.*;

/**
 * Download files from TDS, must have remote management turned on.
 * <pre>
 *   1) log files
 *   2) roots
 * </pre>
 * @author caron
 * @since Apr 24, 2009
 */
public class TdsDownloader {
  static private final String latestServletLog = "threddsServlet.log";

  public enum Type {access, thredds}

  private ManageForm.Data config;
  private Type type;

  private File localDir;
  private JTextArea ta;
  private CancelTask cancel;
  private HTTPSession session;

  TdsDownloader(JTextArea ta, ManageForm.Data config, Type type) throws IOException {
    this.ta = ta;
    this.config = config;
    this.type = type;

    session = HTTPFactory.newSession(config.getServerPrefix());

    localDir = LogLocalManager.getDirectory(config.server, type.toString());
    if (!localDir.exists() && !localDir.mkdirs()) {
      ta.setText(String.format("Failed to create local directory in = %s%n%n", localDir));
      return;
    }
  }

  // copy remote files to localDir
  public void getRemoteFiles(final CancelTask _cancel) {
    this.cancel = _cancel;

    String urls = config.getServerPrefix() + "/thredds/admin/log/"+type+"/";
    ta.append(String.format("Download URL = %s%n", urls));

    String contents = null;
    try (HTTPMethod method = HTTPFactory.Get(session, urls)) {
        int statusCode = method.execute();
        if (statusCode == 200)
          contents = method.getResponseAsString();

      if ((contents == null) || (contents.length() == 0)) {
        ta.append(String.format("Failed to get logs at URL = %s%n%n", urls));
        return;
      } else {
        ta.append(String.format("Logs at URL = %s%n%s%n", urls, contents));
      }
    } catch (Throwable t) {
      ta.append(String.format("Failed to get logs at URL = %s error = %s%n%n", urls, t.getMessage()));
      t.printStackTrace();
      return;
    }

    // update text area in background  http://technobuz.com/2009/05/update-jtextarea-dynamically/
    final String list = contents;
    SwingWorker worker = new SwingWorker<String, Void>() {

      @Override
      protected String doInBackground() throws Exception {
        try {
          ta.append(String.format("Local log files stored in = %s%n%n", localDir));
          String[] lines = list.split("\n");
          for (String line : lines) {
            new RemoteLog(line.trim());
            if (cancel.isCancel()) {
              break;
            }
          }
        } catch (Throwable t) {
          t.printStackTrace();
        }

        return null;
      }

      public void done() {
        if (cancel.isCancel())
          ta.append(String.format("Download was cancelled for %s%n", type));
        else
          ta.append(String.format("Download complete for %s%n", type));
      }
    };

    // do in background
    worker.execute();
  }

  private class RemoteLog {
    String name;
    long size;
    File localFile;

    RemoteLog(String line) throws IOException {
      String[] tokes = line.split(" ");
      name = tokes[0];
      size = Long.parseLong(tokes[1]);

      localFile = new File(localDir, name);
      if (!localFile.exists() || (localFile.length() > size) || name.equals(latestServletLog)) {
        ta.append(String.format("Read RemoteLog length=%6d Kb for %s%n", size/1000, name));
        read();
      } else if (localFile.length() < size) {
        ta.append(String.format("Append RemoteLog length=%6d Kb to local=%6d Kb for %s%n", size/1000, localFile.length()/1000, name));
        append();
      } else {
        ta.append(String.format("Ok RemoteLog length=%6d local=%6d (kb) for %s%n", size/1000, localFile.length()/1000, name));
      }
      ta.setCaretPosition(ta.getText().length());   // needed to get the text to update ?

    }

    void read() throws IOException {
      String urls = config.getServerPrefix() + "/thredds/admin/log/"+type+"/" + name;
      ta.append(String.format(" reading %s to %s%n", urls, localFile.getPath()));
      HttpClientManager.copyUrlContentsToFile(session, urls, localFile);
    }

    void append() throws IOException {
      String urls = config.getServerPrefix() + "/thredds/admin/log/"+type+"/" + name;
      long start = localFile.length();
      long want = size - start;
      long got = HttpClientManager.appendUrlContentsToFile(session, urls, localFile, start, size);
      if (want == got)
        ta.append(String.format(" append %d bytes to %s %n", got, localFile.getPath()));
      else
        ta.append(String.format(" *** append got=%d want=%d bytes to %s %n", got, want, localFile.getPath()));
    }

    @Override
    public String toString() {
      return "RemoteLog{" +
              "name='" + name + '\'' +
              ", size=" + size +
              '}';
    }
  }

}


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

import java.util.NoSuchElementException;
import com.google.common.base.Preconditions;
import ucar.nc2.ft.PointFeature;
import ucar.nc2.ft.PointFeatureIterator;
import ucar.nc2.time.CalendarDateRange;
import ucar.unidata.geoloc.LatLonRect;

/**
 * Decorate a PointFeatureIterator with filtering.
 * @author caron
 * @since Mar 20, 2008
 */
public class PointIteratorFiltered extends PointIteratorAbstract {
    private final PointFeatureIterator origIter;
    private final PointFeatureIterator.Filter filter;
    private PointFeature pointFeature;

    // Originally, this was the only constructor for this class.
    public PointIteratorFiltered(PointFeatureIterator orgIter, LatLonRect filter_bb, CalendarDateRange filter_date) {
        this(orgIter, new SpaceAndTimeFilter(filter_bb, filter_date));
    }

    public PointIteratorFiltered(PointFeatureIterator origIter, PointFeatureIterator.Filter filter) {
        this.origIter = Preconditions.checkNotNull(origIter);
        this.filter = Preconditions.checkNotNull(filter);
    }

    /**
     * Returns {@code true} if the iteration has more elements. (In other words, returns {@code true} if {@link #next}
     * would return an element rather than throwing an exception.)
     *

     * This method is idempotent, meaning that when it is called repeatedly without an intervening
     * {@link #next}, calls after the first will have no effect.
     *
     * @return {@code true} if the iteration has more elements
     */
    // PointFeatureIterator.hasNext() doesn't guarantee idempotency, but we do.
    @Override
    public boolean hasNext() {
        if (pointFeature != null) {
            return true;  // pointFeature hasn't yet been consumed.
        }

        pointFeature = nextFilteredDataPoint();
        if (pointFeature == null) {
            close();
            return false;
        } else {
            return true;
        }
    }

  /**
   * Returns the next element in the iteration.
   *
   * @return the next element in the iteration
   * @throws java.util.NoSuchElementException if the iteration has no more elements.
   */
  // PointFeatureIterator.next() doesn't actually specify the behavior of next() when there are no more elements,
  // but we can define a stronger contract.
  @Override
  public PointFeature next() throws NoSuchElementException {
    if (!hasNext()) {
      throw new NoSuchElementException("This iterator has no more elements.");
    }

    assert pointFeature != null;
    PointFeature ret = pointFeature;
    calcBounds(ret);

    pointFeature = null;  // Feature has been consumed.
    return ret;
  }

  @Override
  public void close() {
        origIter.close();
        finishCalcBounds();
    }

    /**
     * Returns the next point that satisfies the filter, or {@code null} if no such point exists.
     *
     * @return the next point that satisfies the filter, or {@code null} if no such point exists.
     */
    private PointFeature nextFilteredDataPoint() {
        while (origIter.hasNext()) {
            PointFeature pointFeat = origIter.next();
            if (filter.filter(pointFeat)) {
                return pointFeat;
            }
        }

        return null;
    }

    /**
     * A filter that only permits features whose lat/lon falls within a given bounding box AND whose
     * observation time falls within a given date range.
     */
    public static class SpaceAndTimeFilter implements PointFeatureIterator.Filter {
        private final LatLonRect filter_bb;
        private final CalendarDateRange filter_date;

      /**
       * @param filter_bb bounding box or null for all
       * @param filter_date date Range or null for all
       */
        public SpaceAndTimeFilter(LatLonRect filter_bb, CalendarDateRange filter_date) {
          this.filter_bb = filter_bb;
          this.filter_date = filter_date;
        }

        @Override
        public boolean filter(PointFeature pointFeat) {
          if ((filter_date != null) && !filter_date.includes(pointFeat.getObservationTimeAsCalendarDate()))
            return false;

          if ((filter_bb != null) && !filter_bb.contains(pointFeat.getLocation().getLatitude(), pointFeat.getLocation().getLongitude()))
            return false;

          return true;
        }
    }

}