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

package ucar.nc2.units;

import ucar.nc2.time.CalendarDateRange;

import java.util.Date;
import java.text.ParseException;

/**
 * Implements a range of dates, using DateType and/or TimeDuration.
 * You can use a DateType = "present" and a time duration to specify "real time" intervals, eg
 * "last 3 days" uses endDate = "present" and duration = "3 days".
 *
 * @author john caron
 */

public class DateRange {
  private DateType start, end;
  private TimeDuration duration, resolution;
  private boolean isEmpty, isMoving, useStart, useEnd, useDuration, useResolution;

  /**
   * default Constructor
   *
   * @throws java.text.ParseException artifact, cant happen
   */
  public DateRange() throws ParseException {
    this(null, new DateType(false, new Date()), new TimeDuration("1 day"), new TimeDuration("15 min"));
  }

  public DateRange(CalendarDateRange cdr) {
    this(new DateType(cdr.getStart()), new DateType(cdr.getEnd()), null, null);
  }

  /**
    * Create Date Range from a start and end date
    *
    * @param start start of range
    * @param end   end of range
    */
   public DateRange(Date start, Date end) {
     this(new DateType(false, start), new DateType(false, end), null, null);
   }

  /**
    * Create Date Range from a start date and duration
    *
    * @param start start of range
    * @param duration   duration of range
    */
   public DateRange(Date start, TimeDuration duration) {
     this(new DateType(false, start), null, duration, null);
   }

   /**
   * Create DateRange from another DateRange, with a different units of resolution.
   *
   * @param range     copy start and end from here
   * @param timeUnits make resolution using new TimeDuration( timeUnits)
   * @throws ParseException is units are not valid time units
   */
  public DateRange(DateRange range, String timeUnits) throws ParseException {
    this(new DateType(false, range.getStart().getDate()), new DateType(false, range.getEnd().getDate()), null, new TimeDuration(timeUnits));
  }

  /**
   * Encapsulates a range of dates, using DateType start/end, and/or a TimeDuration.
   * A DateRange can be specified in any of the following ways:
   * <ol>
   * <li> a start date and end date
   * <li> a start date and duration
   * <li> an end date and duration
   * </ol>
   *
   * @param start      starting date
   * @param end        ending date
   * @param duration   time duration; may be null
   * @param resolution time resolution; may be null
   */
  public DateRange(DateType start, DateType end, TimeDuration duration, TimeDuration resolution) {
    this.start = start;
    this.end = end;
    this.duration = duration;
    this.resolution = resolution;

    useStart = (start != null) && !start.isBlank();
    useEnd = (end != null) && !end.isBlank();
    useDuration = (duration != null) && !duration.isBlank();
    useResolution = (resolution != null);

    boolean invalid = true;
    if (useStart && useEnd) {
      invalid = false;
      this.isMoving = this.start.isPresent() || this.end.isPresent();
      useDuration = false;
      recalcDuration();

    } else if (useStart && useDuration) {
      invalid = false;
      this.isMoving = this.start.isPresent();
      this.end = this.start.add(duration);

    } else if (useEnd && useDuration) {
      invalid = false;
      this.isMoving = this.end.isPresent();
      this.start = this.end.subtract(duration);
    }

    if (invalid)
      throw new IllegalArgumentException("DateRange must have 2 of start, end, duration");

    checkIfEmpty();
    hashCode = 0;
  }

  private void checkIfEmpty() {
    if (this.start.isPresent() && this.end.isPresent())
      isEmpty = true;
    else if (this.start.isPresent() || this.end.isPresent())
      isEmpty = duration.getValue() <= 0;
    else
      isEmpty = this.end.before(this.start);

    if (isEmpty)
      duration.setValueInSeconds(0);
  }

  // choose a resolution based on # seconds
  private String chooseResolution(double time) {
    if (time < 180) // 3 minutes
      return "secs";
    time /= 60; // minutes
    if (time < 180) // 3 hours
      return "minutes";
    time /= 60; // hours
    if (time < 72) // 3 days
      return "hours";
    time /= 24; // days
    if (time < 90) // 3 months
      return "days";
    time /= 30; // months
    if (time < 36) // 3 years
      return "months";
    return "years";
  }

  /**
   * Determine if the given date is included in this date range.
   * The date range includes the start and end dates.
   *
   * @param d date to check
   * @return true if date in inside this range
   */
  public boolean included(Date d) {
    if (isEmpty) return false;

    if (getStart().after(d)) return false;
    if (getEnd().before(d)) return false;

    return true;
  }

  /**
   * Same as included()
   * @param d date to check
   * @return true if date in inside this range
   */
  public boolean contains(Date d) {
    return included(d);
  }

  /**
   * Determine if the given range intersects this date range.
   *
   * @param start_want range starts here
   * @param end_want   range ends here
   * @return true if ranges intersect
   */
  public boolean intersects(Date start_want, Date end_want) {
    if (isEmpty) return false;

    if (getStart().after(end_want)) return false;
    if (getEnd().before(start_want)) return false;

    return true;
  }

  /**
   * Determine if the given range intersects this date range.
   *
   * @param other date range
   * @return true if ranges intersect
   */
  public boolean intersects(DateRange other) {
    return intersects(other.getStart().getDate(), other.getEnd().getDate());
  }

  /**
   * Intersect with another date range
   *
   * @param clip intersect with this date range
   * @return new date range that is the intersection
   */
  public DateRange intersect(DateRange clip) {
    if (isEmpty) return this;
    if (clip.isEmpty) return clip;

    DateType ss = getStart();
    DateType s = ss.before(clip.getStart()) ? clip.getStart() : ss;

    DateType ee = getEnd();
    DateType e = ee.before(clip.getEnd()) ? ee : clip.getEnd();

    return new DateRange(s, e, null, resolution);
  }

  /**
   * Extend this date range by the given one.
   *
   * @param dr given DateRange
   */
  public void extend(DateRange dr) {
    boolean localEmpty = isEmpty;
    if (localEmpty || dr.getStart().before(getStart()))
      setStart(dr.getStart());
    if (localEmpty || getEnd().before(dr.getEnd()))
      setEnd(dr.getEnd());
  }

  /**
   * Extend this date range by the given Date.
   *
   * @param d given Date
   */
  public void extend(Date d) {
    if (d.before(getStart().getDate()))
      setStart( new DateType(false, d));
    if (getEnd().before(d))
      setEnd(new DateType(false, d));
  }

  /**
   * Get the starting Date.
   *
   * @return starting Date
   */
  public DateType getStart() {
    return (isMoving && !useStart) ? this.end.subtract(duration) : start;
  }

  /**
   * Set the starting Date. Makes useStart true.
   * If useEnd, recalculate the duration, else recalculate end.
   *
   * @param start starting Date
   */
  public void setStart(DateType start) {
    this.start = start;
    useStart = true;

    if (useEnd) {
      this.isMoving = this.start.isPresent() || this.end.isPresent();
      useDuration = false;
      recalcDuration();

    } else {
      this.isMoving = this.start.isPresent();
      this.end = this.start.add(duration);
    }
    checkIfEmpty();
  }

  /**
   * Get the ending Date.
   *
   * @return ending Date
   */
  public DateType getEnd() {
    return (isMoving && !useEnd) ? this.start.add(duration) : end;
  }

  /**
   * Set the ending Date. Makes useEnd true.
   * If useStart, recalculate the duration, else recalculate start.
   *
   * @param end ending Date
   */
  public void setEnd(DateType end) {
    this.end = end;
    useEnd = true;

    if (useStart) {
      this.isMoving = this.start.isPresent() || this.end.isPresent();
      useDuration = false;
      recalcDuration();

    } else {
      this.isMoving = this.end.isPresent();
      this.start = this.end.subtract(duration);
    }
    checkIfEmpty();
  }

  /**
   * Get the duration of the interval
   *
   * @return duration of the interval
   */
  public TimeDuration getDuration() {
    if (isMoving && !useDuration)
      recalcDuration();
    return duration;
  }

  /**
   * Set the duration of the interval. Makes useDuration true.
   * If useStart, recalculate end, else recalculate start.
   *
   * @param duration duration of the interval
   */
  public void setDuration(TimeDuration duration) {
    this.duration = duration;
    useDuration = true;

    if (useStart) {
      this.isMoving = this.start.isPresent();
      this.end = this.start.add(duration);
      useEnd = false;

    } else {
      this.isMoving = this.end.isPresent();
      this.start = this.end.subtract(duration);
    }
    checkIfEmpty();
  }

  // assumes not moving
  private void recalcDuration() {
    long min = getStart().getDate().getTime();
    long max = getEnd().getDate().getTime();
    double secs = .001 * (max - min);
    if (secs < 0)
      secs = 0;

    if (duration == null) {
      try {
        duration = new TimeDuration(chooseResolution(secs));
      } catch (ParseException e) {
        // cant happen
        throw new RuntimeException(e);
      }
    }

    if (resolution == null) {
      duration.setValueInSeconds(secs);

    } else {
      // make it a multiple of resolution
      double resSecs = resolution.getValueInSeconds();
      double closest = Math.round(secs / resSecs);
      secs = closest * resSecs;
      duration.setValueInSeconds(secs);
    }

    hashCode = 0;
  }

  /**
   * Get the time resolution.
   *
   * @return time resolution as a duration
   */
  public TimeDuration getResolution() {
    return resolution;
  }

  /**
   * Set the time resolution.
   *
   * @param resolution the time resolution
   */
  public void setResolution(TimeDuration resolution) {
    this.resolution = resolution;
    useResolution = true;
  }

  /**
   * Get if the start is fixed.
   * @return if start is fixed
   */
  public boolean useStart() {
    return useStart;
  }

  /**
   * Get if the end is fixed.
   * @return if end is fixed
   */
  public boolean useEnd() {
    return useEnd;
  }

  /**
   * Get if the duration is fixed.
   * @return if duration is fixed
   */
  public boolean useDuration() {
    return useDuration;
  }

  /**
   * Get if the resolution is set.
   * @return if resolution is fixed
   */
  public boolean useResolution() {
    return useResolution;
  }

  /**
   * Return true if start date equals end date, so date range is a point.
   *
   * @return true if start = end
   */
  public boolean isPoint() {
    return !isEmpty && start.equals(end);
  }

  /**
   * If the range is empty
   *
   * @return if the range is empty
   */
  public boolean isEmpty() {
    return isEmpty;
  }

  @Override
  public String toString() {
    return "start= " + start + " end= " + end + " duration= " + duration
        + " resolution= " + resolution;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof DateRange)) return false;
    DateRange oo = (DateRange) o;
    if (useStart && !start.equals(oo.start)) return false;
    if (useEnd && !end.equals(oo.end)) return false;
    if (useDuration && !duration.equals(oo.duration)) return false;
    if (useResolution && !resolution.equals(oo.resolution)) return false;
    return true;
  }

  /**
   * Override Object.hashCode() to implement equals.
   */
  @Override
  public int hashCode() {
    if (hashCode == 0) {
      int result = 17;
      if (useStart)
        result = 37 * result + start.hashCode();
      if (useEnd)
        result = 37 * result + end.hashCode();
      if (useDuration)
        result = 37 * result + duration.hashCode();
      if (useResolution)
        result = 37 * result + resolution.hashCode();
      hashCode = result;
    }
    return hashCode;
  }

  private int hashCode = 0; // Bloch, item 8

}


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

import thredds.client.catalog.ServiceType;
import thredds.client.catalog.Dataset;
import thredds.client.catalog.tools.DataFactory;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.dataset.CoordinateAxis1D;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dt.GridCoordSystem;
import ucar.nc2.dt.GridDataset;
import ucar.nc2.dt.GridDatatype;
import ucar.nc2.ui.geoloc.NavigatedPanel;
import ucar.nc2.ui.geoloc.ProjectionManager;
import ucar.nc2.ui.gis.MapBean;
import ucar.nc2.ui.util.Renderer;
import ucar.nc2.ui.widget.*;
import ucar.nc2.ui.widget.PopupMenu;
import ucar.nc2.ui.widget.ProgressMonitor;
import ucar.nc2.util.NamedObject;
import ucar.unidata.geoloc.ProjectionImpl;
import ucar.util.prefs.PreferencesExt;

import javax.swing.*;
import javax.swing.border.EtchedBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;

/**
 * This is the thredds Data Viewer application User Interface for Grids.
 *
 * @author caron
 */
public class GridUI extends JPanel {
  static private final String DATASET_URL = "DatasetURL";
  static private final String GEOTIFF_FILECHOOSER_DEFAULTDIR = "geotiffDefDir";

  //private TopLevel topLevel;
  private PreferencesExt store;
  private FileManager fileChooser;

  // Package private access
  SuperComboBox fieldChooser, levelChooser, timeChooser, ensembleChooser, runtimeChooser;
  NavigatedPanel panz = new NavigatedPanel();
  VertPanel vertPanel;
  GridTable gridTable;
  JLabel dataValueLabel, positionLabel;

  // the main components
  private GridController controller;
  private ColorScale.Panel colorScalePanel;

  private GeoGridTable dsTable;

  // UI components that need global scope
  private TextHistoryPane datasetInfoTA, ncmlTA;
  private JPanel drawingPanel;
  private JSplitPane splitDraw;
  private JComboBox csDataMinMax;
  private PopupMenu mapBeanMenu;

  private JLabel datasetNameLabel;
  // private Field.TextCombo gridUrlIF;
  // private PrefPanel gridPP;

  // the various managers and dialog boxes
  private ProjectionManager projManager;
  //private ColorScaleManager csManager;
  private IndependentWindow infoWindow = null;
  private IndependentWindow ncmlWindow = null;
  private IndependentWindow gtWindow = null;
  private JDialog dsDialog = null;
  private FileManager geotiffFileChooser;

  // toolbars
  private JPanel fieldPanel, toolPanel;
  private JToolBar navToolbar, moveToolbar;
  private AbstractAction navToolbarAction, moveToolbarAction;
  private JMenu configMenu;

  // actions
  private AbstractAction redrawAction;
  private AbstractAction showDatasetInfoAction;
  private AbstractAction showNcMLAction;
  private AbstractAction showGridTableAction;
  private AbstractAction showGridDatasetInfoAction;
  private AbstractAction showNetcdfDatasetAction;
  private AbstractAction minmaxHorizAction, minmaxLogAction, minmaxHoldAction;
  private AbstractAction fieldLoopAction, levelLoopAction, timeLoopAction;
  private AbstractAction chooseProjectionAction, saveCurrentProjectionAction;

  // state
  private boolean selected = false;
  private int mapBeanCount = 0;

  // debugging
  private boolean debugBeans = false, debugChooser = false, debugPrint = false, debugHelp = false;
  private boolean debugTask = false;

  public GridUI(PreferencesExt pstore, RootPaneContainer root, FileManager fileChooser, int defaultHeight) {
    // this.topUI = topUI;
    this.store = pstore;
    this.fileChooser = fileChooser;

    try  {
      choosers = new ArrayList();
      fieldChooser = new SuperComboBox(root, "field", true, null);
      choosers.add( new Chooser("field", fieldChooser, true));
      runtimeChooser = new SuperComboBox(root, "runtime", false, null);
      choosers.add( new Chooser("runtime", runtimeChooser, false));
      timeChooser = new SuperComboBox(root, "time", false, null);
      choosers.add( new Chooser("time", timeChooser, false));
      ensembleChooser = new SuperComboBox(root, "ensemble", false, null);
      choosers.add( new Chooser("ensemble", ensembleChooser, false));
      levelChooser = new SuperComboBox(root, "level", false, null);
      choosers.add( new Chooser("level", levelChooser, false));

      makeActionsDataset();
      makeActionsToolbars();

      gridTable = new GridTable("field");
      gtWindow = new IndependentWindow("Grid Table Information", BAMutil.getImage( "GDVs"), gridTable.getPanel());

      PreferencesExt dsNode = (PreferencesExt) pstore.node("DatasetTable");
      dsTable = new GeoGridTable(dsNode, true);
      dsDialog = dsTable.makeDialog(root, "NetcdfDataset Info", false);
      //dsDialog.setIconImage( BAMutil.getImage( "GDVs"));
      Rectangle bounds = (Rectangle) dsNode.getBean("DialogBounds", new Rectangle(50, 50, 800, 450));
      dsDialog.setBounds( bounds);

      controller = new GridController( this, store);
      makeUI(defaultHeight);
      controller.finishInit();

          // other components
      geotiffFileChooser = new FileManager(null);
      geotiffFileChooser.setCurrentDirectory( store.get(GEOTIFF_FILECHOOSER_DEFAULTDIR, "."));

    } catch (Exception e) {
      System.out.println("UI creation failed");
      e.printStackTrace();
    }
  }

    // give access to the Controller
    /* NavigatedPanel getNavigatedPanel() { return panz; }
    VertPanel getVertPanel() { return vertPanel; }
    SuperComboBox getFieldChooser() { return fieldChooser; }
    SuperComboBox getLevelChooser() { return levelChooser; }
    SuperComboBox getTimeChooser() { return timeChooser; }
    GridTable getGridTable() { return gridTable; }
    JLabel getDataValueLabel() { return dataValueLabel; }
    JLabel getPositionLabel() { return positionLabel; } */

      /** save all data in the PersistentStore */
  public void storePersistentData() {
    store.putInt( "vertSplit", splitDraw.getDividerLocation());

    store.putBoolean( "navToolbarAction", ((Boolean)navToolbarAction.getValue(BAMutil.STATE)).booleanValue());
    store.putBoolean( "moveToolbarAction", ((Boolean)moveToolbarAction.getValue(BAMutil.STATE)).booleanValue());

    if (projManager != null)
      projManager.storePersistentData();
    /* if (csManager != null)
      csManager.storePersistentData();
    if (sysConfigDialog != null)
      sysConfigDialog.storePersistentData(); */

    dsTable.save();
    dsTable.getPrefs().putBeanObject("DialogBounds", dsDialog.getBounds());

    store.put(GEOTIFF_FILECHOOSER_DEFAULTDIR, geotiffFileChooser.getCurrentDirectory());

    controller.storePersistentData();
  }

 /* private boolean chooseDataset(String url) {
    InvDataset invDs = new InvDatasetImpl( fname, ServerType.NETCDF);
    return chooseDataset( invDs);
  } */

  boolean isSelected() { return selected; }
  void setSelected( boolean b) {
    selected = b;

    showGridTableAction.setEnabled( b);
    showNcMLAction.setEnabled( b);
    showNcMLAction.setEnabled( b);
    showNetcdfDatasetAction.setEnabled( b);
    showGridDatasetInfoAction.setEnabled( b);
    //showNetcdfXMLAction.setEnabled( b);

    navToolbarAction.setEnabled( b);
    moveToolbarAction.setEnabled( b);

    controller.showGridAction.setEnabled( b);
    controller.showContoursAction.setEnabled( b);
    controller.showContourLabelsAction.setEnabled( b);
    redrawAction.setEnabled( b);

    minmaxHorizAction.setEnabled( b);
    minmaxLogAction.setEnabled( b);
    minmaxHoldAction.setEnabled( b);

    fieldLoopAction.setEnabled( b);
    levelLoopAction.setEnabled( b);
    timeLoopAction.setEnabled( b);

    panz.setEnabledActions( b);
  }

           // add a MapBean to the User Interface
  public void addMapBean( MapBean mb) {
    mapBeanMenu.addAction( mb.getActionDesc(), mb.getIcon(), mb.getAction());

    // first one is the "default"
    if (mapBeanCount == 0) {
      setMapRenderer( mb.getRenderer());
    }
    mapBeanCount++;

   mb.addPropertyChangeListener( new PropertyChangeListener() {
     public void propertyChange( java.beans.PropertyChangeEvent e) {
       if (e.getPropertyName().equals("Renderer")) {
         setMapRenderer( (Renderer) e.getNewValue());
       }
     }
   });
  }

  void setMapRenderer( Renderer mapRenderer) {
    controller.setMapRenderer( mapRenderer);
  }

  public void clear() {
    controller.clear();
    gridTable.clear();
  }

  public void setDataset(Dataset ds) {
     if (ds == null) return;

     OpenDatasetTask openTask = new OpenDatasetTask(ds);
     ProgressMonitor pm = new ProgressMonitor(openTask);
     pm.addActionListener(e -> {
         if (e.getActionCommand().equals("success")) {
           controller.showDataset();
           gridTable.setDataset(controller.getFields());
           datasetNameLabel.setText("Dataset:  "+ controller.getDatasetUrlString());
           setSelected(true);
           gtWindow.hide();
         }
     });
     pm.start( this, "Open Dataset "+ds.getName(), 100);
   }

  public void setDataset(GridDataset ds) {
     controller.setGridDataset( ds);
     controller.showDataset();
     datasetNameLabel.setText("Dataset:  "+ controller.getDatasetUrlString());
     gridTable.setDataset(controller.getFields());
   }

   void setFields( java.util.List fields) {
    fieldChooser.setCollection(fields.iterator());
  }

  void setField(GridDatatype field) {
    /*int idx = fieldChooser.setSelectedByName(field.toString());
    if (idx < 0)
      fieldChooser.setSelectedByIndex(0); */
    fieldChooser.setToolTipText( field.getName());

    GridCoordSystem gcs = field.getCoordinateSystem();

      // levels
    CoordinateAxis1D axis = gcs.getVerticalAxis();
    setChooserWanted("level", axis != null);
    if (axis != null) {
      List<NamedObject> levels = axis.getNames();
      levelChooser.setCollection(levels.iterator(), true);
      NamedObject no = levels.get( controller.getCurrentLevelIndex());
      levelChooser.setSelectedByName(no.getName());
    }

      // times
    if (gcs.hasTimeAxis()) {
      axis = gcs.hasTimeAxis1D() ? gcs.getTimeAxis1D() : gcs.getTimeAxisForRun(0);
      setChooserWanted("time", axis != null);
      if (axis != null) {
        List<NamedObject> names = axis.getNames();
        timeChooser.setCollection(names.iterator(), true);
        NamedObject no =  names.get(controller.getCurrentTimeIndex());
        timeChooser.setSelectedByName(no.getName());
      }
    } else {
      setChooserWanted("time", false);
    }

    axis = gcs.getEnsembleAxis();
    setChooserWanted("ensemble", axis != null);
    if (axis != null) {
      List<NamedObject> names = axis.getNames();
      ensembleChooser.setCollection(names.iterator(), true);
      NamedObject no =  names.get(controller.getCurrentEnsembleIndex());
      ensembleChooser.setSelectedByName(no.getName());
    }

    axis = gcs.getRunTimeAxis();
    setChooserWanted("runtime", axis != null);
    if (axis != null) {
      List<NamedObject> names = axis.getNames();
      runtimeChooser.setCollection(names.iterator(), true);
      NamedObject no = names.get(controller.getCurrentRunTimeIndex());
      runtimeChooser.setSelectedByName(no.getName());
    }

    setChoosers();

    colorScalePanel.setUnitString( field.getUnitsString());
  }

  void setDrawHorizAndVert( boolean drawHoriz, boolean drawVert) {
    drawingPanel.removeAll();
    if (drawHoriz && drawVert) {
      splitDraw.setTopComponent(panz);
      splitDraw.setBottomComponent(vertPanel);
      drawingPanel.add( splitDraw,  BorderLayout.CENTER);
    } else if (drawHoriz) {
      drawingPanel.add( panz,  BorderLayout.CENTER);
    } else if (drawVert) {
      drawingPanel.add( splitDraw,  BorderLayout.CENTER);
    }
  }

  // actions that control the dataset
  private void makeActionsDataset() {

      // choose local dataset
    AbstractAction chooseLocalDatasetAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        String filename = fileChooser.chooseFilename();
        if (filename == null) return;

        Dataset invDs;
        try {
          invDs = Dataset.makeStandalone(filename, FeatureType.GRID.toString(), "", ServiceType.File.toString());
        } catch (Exception ue) {
          JOptionPane.showMessageDialog(GridUI.this, "Invalid filename = <" + filename + ">\n" + ue.getMessage());
          ue.printStackTrace();
          return;
        }
        setDataset(invDs);
      }
    };
    BAMutil.setActionProperties(chooseLocalDatasetAction, "FileChooser", "open Local dataset...", false, 'L', -1);

    /* saveDatasetAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        String fname = controller.getDatasetName();
        if (fname != null) {
          savedDatasetList.add( fname);
          BAMutil.addActionToMenu( savedDatasetMenu, new DatasetAction( fname), 0);
        }
      }
    };
    BAMutil.setActionProperties( saveDatasetAction, null, "save dataset", false, 'S', 0);
    */

      // Configure
    chooseProjectionAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        getProjectionManager().setVisible();
      }
    };
    BAMutil.setActionProperties( chooseProjectionAction, null, "Projection Manager...", false, 'P', 0);

    saveCurrentProjectionAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        getProjectionManager();
          // set the bounding box
        ProjectionImpl proj = panz.getProjectionImpl().constructCopy();
        proj.setDefaultMapArea( panz.getMapArea());
        //if (debug) System.out.println(" GV save projection "+ proj);

        // projManage.setMap(renderAll.get("Map"));   LOOK!
        //projManager.saveProjection( proj);
      }
    };
    BAMutil.setActionProperties( saveCurrentProjectionAction, null, "save Current Projection", false, 'S', 0);

    /* chooseColorScaleAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        if (null == csManager) // lazy instantiation
          makeColorScaleManager();
        csManager.show();
      }
    };
    BAMutil.setActionProperties( chooseColorScaleAction, null, "ColorScale Manager...", false, 'C', 0);

    */
      // redraw
    redrawAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        repaint();
        controller.start(true);
        controller.draw(true);
      }
    };
    BAMutil.setActionProperties( redrawAction, "alien", "RedRaw", false, 'W', 0);

    showDatasetInfoAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        if (infoWindow == null) {
          datasetInfoTA = new TextHistoryPane();
          infoWindow = new IndependentWindow("Dataset Information", BAMutil.getImage( "GDVs"), datasetInfoTA);
          infoWindow.setSize(700,700);
          infoWindow.setLocation(100,100);
        }

        datasetInfoTA.clear();
        datasetInfoTA.appendLine( controller.getDatasetInfo());
        datasetInfoTA.gotoTop();
        infoWindow.show();
      }
    };
    BAMutil.setActionProperties( showDatasetInfoAction, "Information", "Show info...", false, 'S', -1);

    showNcMLAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        if (ncmlWindow == null) {
          ncmlTA = new TextHistoryPane();
          ncmlWindow = new IndependentWindow("Dataset NcML", BAMutil.getImage( "GDVs"), ncmlTA);
          ncmlWindow.setSize(700,700);
          ncmlWindow.setLocation(200, 70);
        }

        ncmlTA.clear();
        //datasetInfoTA.appendLine( "GeoGrid XML for "+ controller.getDatasetName()+"\n");
        ncmlTA.appendLine( controller.getNcML());
        ncmlTA.gotoTop();
        ncmlWindow.show();
      }
    };
    BAMutil.setActionProperties( showNcMLAction, null, "Show NcML...", false, 'X', -1);

    showGridDatasetInfoAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        if (ncmlWindow == null) {
          ncmlTA = new TextHistoryPane();
          ncmlWindow = new IndependentWindow("Dataset NcML", BAMutil.getImage( "GDVs"), ncmlTA);
          ncmlWindow.setSize(700,700);
          ncmlWindow.setLocation(200, 70);
        }

        ncmlTA.clear();
        //datasetInfoTA.appendLine( "GeoGrid XML for "+ controller.getDatasetName()+"\n");
        ncmlTA.appendLine( controller.getDatasetXML());
        ncmlTA.gotoTop();
        ncmlWindow.show();
      }
    };
    BAMutil.setActionProperties( showGridDatasetInfoAction, null, "Show GridDataset Info XML...", false, 'X', -1);

      // show gridTable
    showGridTableAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        gtWindow.show();
      }
    };
    BAMutil.setActionProperties( showGridTableAction, "Table", "grid Table...", false, 'T', -1);

      // show netcdf dataset Table
    showNetcdfDatasetAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        NetcdfDataset netcdfDataset = controller.getNetcdfDataset();
        if (null != netcdfDataset) {
          try {
            dsTable.setDataset(netcdfDataset, null);
          } catch (IOException e1) {
            e1.printStackTrace();
            return;
          }
          dsDialog.show();
        }
      }
    };
    BAMutil.setActionProperties( showNetcdfDatasetAction, "netcdf", "NetcdfDataset Table Info...", false, 'D', -1);

      /* write geotiff file
    geotiffAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        GeoGrid grid = controller.getCurrentField();
        ucar.ma2.Array data = controller.getCurrentHorizDataSlice();
        if ((grid == null) || (data == null)) return;

        String filename = geotiffFileChooser.chooseFilename();
        if (filename == null) return;

        GeoTiff geotiff = null;
        try {
          /* System.out.println("write to= "+filename);
          ucar.nc2.geotiff.Writer.write2D(grid, data, filename+".tfw");
          geotiff = new GeoTiff(filename); // read back in
          geotiff.read();
          System.out.println( geotiff.showInfo());
          //geotiff.testReadData();
          geotiff.close(); * /

          // write two
          ucar.nc2.geotiff.GeotiffWriter writer = new ucar.nc2.geotiff.GeotiffWriter(filename);
          writer.writeGrid(grid, data, false);
          geotiff = new GeoTiff(filename); // read back in
          geotiff.read();
          System.out.println( "*************************************");
          System.out.println( geotiff.showInfo());
          //geotiff.testReadData();
          geotiff.close();

        } catch (IOException ioe) {
          ioe.printStackTrace();

        } finally {
          try {
            if (geotiff != null) geotiff.close();
          } catch (IOException ioe) { }
        }

      }
    };
    BAMutil.setActionProperties( geotiffAction, "Geotiff", "Write Geotiff file", false, 'G', -1);
    */

    minmaxHorizAction =  new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        csDataMinMax.setSelectedItem(ColorScale.MinMaxType.horiz);
        controller.setDataMinMaxType(ColorScale.MinMaxType.horiz);
      }
    };
    BAMutil.setActionProperties( minmaxHorizAction, null, "Horizontal plane", false, 'H', 0);

    minmaxLogAction =  new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        csDataMinMax.setSelectedItem(ColorScale.MinMaxType.log);
        controller.setDataMinMaxType(ColorScale.MinMaxType.log);
      }
    };
    BAMutil.setActionProperties( minmaxLogAction, null, "log horiz plane", false, 'V', 0);

    /* minmaxVolAction =  new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        csDataMinMax.setSelectedIndex(GridRenderer.VOL_MinMaxType);
        controller.setDataMinMaxType(GridRenderer.MinMaxType.vert;
      }
    };
    BAMutil.setActionProperties( minmaxVolAction, null, "Grid volume", false, 'G', 0); */

    minmaxHoldAction =  new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        csDataMinMax.setSelectedItem(ColorScale.MinMaxType.hold);
        controller.setDataMinMaxType(ColorScale.MinMaxType.hold);
      }
    };
    BAMutil.setActionProperties( minmaxHoldAction, null, "Hold scale constant", false, 'C', 0);

    fieldLoopAction = new LoopControlAction(fieldChooser);
    levelLoopAction = new LoopControlAction(levelChooser);
    timeLoopAction = new LoopControlAction(timeChooser);
  }

  private void makeActionsToolbars() {

    navToolbarAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        Boolean state = (Boolean) getValue(BAMutil.STATE);
        if (state.booleanValue())
          toolPanel.add(navToolbar);
        else
          toolPanel.remove(navToolbar);
      }
    };
    BAMutil.setActionProperties( navToolbarAction, "MagnifyPlus", "show Navigate toolbar", true, 'M', 0);
    navToolbarAction.putValue(BAMutil.STATE, new Boolean(store.getBoolean( "navToolbarAction", true)));

    moveToolbarAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        Boolean state = (Boolean) getValue(BAMutil.STATE);
        if (state.booleanValue())
          toolPanel.add(moveToolbar);
        else
          toolPanel.remove(moveToolbar);
      }
    };
    BAMutil.setActionProperties( moveToolbarAction, "Up", "show Move toolbar", true, 'M', 0);
    moveToolbarAction.putValue(BAMutil.STATE, new Boolean(store.getBoolean( "moveToolbarAction", true)));
  }

 /*  private void makeSysConfigWindow() {
    sysConfigDialog = new ucar.unidata.ui.PropertyDialog(topLevel.getRootPaneContainer(), true,
        "System Configuration", store, "HelpDir");     // LOOK KLUDGE
    sysConfigDialog.pack();
    sysConfigDialog.setSize(500,200);
    sysConfigDialog.setLocation(300,300);
  }

  private void makeColorScaleManager() {
    csManager = new ColorScaleManager(topLevel.getRootPaneContainer(), store);
    csManager.addPropertyChangeListener(  new java.beans.PropertyChangeListener() {
      public void propertyChange( java.beans.PropertyChangeEvent e) {
        if (e.getPropertyName().equals("ColorScale")) {
          ColorScale cs = (ColorScale) e.getNewValue();
          cs = (ColorScale) cs.clone();
          //System.out.println("UI: new Colorscale got "+cs);
          colorScalePanel.setColorScale(cs);
          controller.setColorScale(cs);
        }
      }
    });
  } */

  public ProjectionManager getProjectionManager() {
    if (null != projManager)
      return projManager;

    projManager = new ProjectionManager(null, store);
    projManager.addPropertyChangeListener(  new java.beans.PropertyChangeListener() {
      public void propertyChange( java.beans.PropertyChangeEvent e) {
        if (e.getPropertyName().equals("ProjectionImpl")) {
          ProjectionImpl p = (ProjectionImpl) e.getNewValue();
          p = p.constructCopy();
          //System.out.println("UI: new Projection "+p);
          controller.setProjection( p);
        }
      }
    });

    return projManager;
  }

  private void makeUI(int defaultHeight) {

    datasetNameLabel = new JLabel();
    /* gridPP = new PrefPanel("GridView", (PreferencesExt) store.node("GridViewPrefs"));
    gridUrlIF = gridPP.addTextComboField("url", "Gridded Data URL", null, 10, false);
    gridPP.addButton( BAMutil.makeButtconFromAction( chooseLocalDatasetAction ));
    gridPP.finish(true, BorderLayout.EAST);
    gridPP.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        InvDatasetImpl ds = new InvDatasetImpl( gridUrlIF.getText(), thredds.catalog.DataType.GRID, ServiceType.NETCDF);
        setDataset( ds);
      }
    }); */

    // top tool panel
    toolPanel = new JPanel();
    toolPanel.setBorder(new EtchedBorder());
    toolPanel.setLayout(new MFlowLayout(FlowLayout.LEFT, 0, 0));

    // menus
    JMenu dataMenu = new JMenu("Dataset");
    dataMenu.setMnemonic('D');
    configMenu = new JMenu("Configure");
    configMenu.setMnemonic('C');
    JMenu toolMenu = new JMenu("Controls");
    toolMenu.setMnemonic( 'T');
    addActionsToMenus(dataMenu, configMenu, toolMenu);
    JMenuBar menuBar = new JMenuBar();
    menuBar.add(dataMenu);
    menuBar.add(configMenu);
    menuBar.add(toolMenu);
    toolPanel.add(menuBar);

    // field choosers
    fieldPanel = new JPanel();
    fieldPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
    toolPanel.add( fieldPanel);

    // stride
    toolPanel.add( controller.strideSpinner);

    // buttcons
    BAMutil.addActionToContainer( toolPanel, controller.drawHorizAction);
    BAMutil.addActionToContainer( toolPanel, controller.drawVertAction);
    mapBeanMenu = MapBean.makeMapSelectButton();
    toolPanel.add( mapBeanMenu.getParentComponent());

    // the Navigated panel and its toolbars
    panz.setLayout(new FlowLayout());
    navToolbar = panz.getNavToolBar();
    moveToolbar = panz.getMoveToolBar();
    if (((Boolean)navToolbarAction.getValue(BAMutil.STATE)).booleanValue())
      toolPanel.add(navToolbar);
    if (((Boolean)moveToolbarAction.getValue(BAMutil.STATE)).booleanValue())
      toolPanel.add(moveToolbar);

    BAMutil.addActionToContainer( toolPanel, panz.setReferenceAction);
    BAMutil.addActionToContainer( toolPanel, controller.dataProjectionAction);
    BAMutil.addActionToContainer( toolPanel, controller.showGridAction);
    BAMutil.addActionToContainer( toolPanel, controller.showContoursAction);
    BAMutil.addActionToContainer( toolPanel, controller.showContourLabelsAction);

    BAMutil.addActionToContainer( toolPanel, redrawAction);

      //  vertical split
    vertPanel = new VertPanel();
    splitDraw = new JSplitPane(JSplitPane.VERTICAL_SPLIT, panz, vertPanel);
    int divLoc = store.getInt( "vertSplit", 2*defaultHeight/3);
    splitDraw.setDividerLocation(divLoc);
    drawingPanel = new JPanel(new BorderLayout()); // filled later

    // status panel
    JPanel statusPanel = new JPanel(new BorderLayout());
    statusPanel.setBorder(new EtchedBorder());
    positionLabel = new JLabel("position");
    positionLabel.setToolTipText("position at cursor");
    dataValueLabel = new JLabel("data value", SwingConstants.CENTER);
    dataValueLabel.setToolTipText("data value (double click on grid)");
    statusPanel.add(positionLabel, BorderLayout.WEST);
    statusPanel.add(dataValueLabel, BorderLayout.CENTER);
    panz.setPositionLabel( positionLabel);

    // colorscale panel
    colorScalePanel = new ColorScale.Panel(this, controller.getColorScale());
    csDataMinMax = new JComboBox( ColorScale.MinMaxType.values());
    csDataMinMax.setToolTipText("ColorScale Min/Max setting");
    csDataMinMax.addActionListener(e -> {
        controller.setDataMinMaxType( ( ColorScale.MinMaxType) csDataMinMax.getSelectedItem());
    });
    JPanel westPanel = new JPanel(new BorderLayout());
    westPanel.add( colorScalePanel, BorderLayout.CENTER);
    westPanel.add( csDataMinMax, BorderLayout.NORTH);

    // lay it out
    JPanel northPanel = new JPanel();
    //northPanel.setLayout( new BoxLayout(northPanel, BoxLayout.Y_AXIS));
    northPanel.setLayout( new BorderLayout());
    northPanel.add( datasetNameLabel, BorderLayout.NORTH);
    northPanel.add( toolPanel, BorderLayout.SOUTH);

    setLayout(new BorderLayout());
    add(northPanel, BorderLayout.NORTH);
    add(statusPanel, BorderLayout.SOUTH);
    add(westPanel, BorderLayout.WEST);
    add(drawingPanel, BorderLayout.CENTER);

    setDrawHorizAndVert( controller.drawHorizOn, controller.drawVertOn);
  }

  private ArrayList choosers;
  private void setChoosers() {
    fieldPanel.removeAll();
    for (int i = 0; i < choosers.size(); i++) {
      Chooser c = (Chooser) choosers.get(i);
      if (c.isWanted)
        fieldPanel.add(c.field);
    }
  }

  private static class Chooser {
    Chooser(String name, SuperComboBox field, boolean want){
      this.name = name;
      this.field = field;
      this.isWanted = want;
    }
    boolean isWanted;
    String  name;
    SuperComboBox field;
  }

  private void setChooserWanted(String name, boolean want) {
    for (int i = 0; i < choosers.size(); i++) {
      Chooser chooser = (Chooser) choosers.get(i);
      if (chooser.name.equals(name)) chooser.isWanted = want;
    }
  }

  private void addToolbarOption(String toolbarName, JToolBar toolbar, AbstractAction act) {
    boolean wantsToolbar = store.getBoolean( toolbarName, true);
    if (wantsToolbar)
      toolPanel.add(toolbar);
 }

  void addActionsToMenus(JMenu datasetMenu, JMenu configMenu, JMenu toolMenu) {
      // Info
    BAMutil.addActionToMenu( datasetMenu, showGridTableAction);
    BAMutil.addActionToMenu( datasetMenu, showDatasetInfoAction);
    BAMutil.addActionToMenu( datasetMenu, showNcMLAction);
    BAMutil.addActionToMenu( datasetMenu, showGridDatasetInfoAction);
    BAMutil.addActionToMenu( datasetMenu, showNetcdfDatasetAction);
    // BAMutil.addActionToMenu( datasetMenu, geotiffAction);
    //BAMutil.addActionToMenu( infoMenu, showNetcdfXMLAction);

    /// Configure
    JMenu toolbarMenu = new JMenu("Toolbars");
    toolbarMenu.setMnemonic( 'T');
    configMenu.add(toolbarMenu);
    BAMutil.addActionToMenu( toolbarMenu, navToolbarAction);
    BAMutil.addActionToMenu( toolbarMenu, moveToolbarAction);

    BAMutil.addActionToMenu( configMenu, chooseProjectionAction);
    BAMutil.addActionToMenu( configMenu, saveCurrentProjectionAction);

    /* BAMutil.addActionToMenu( configMenu, chooseColorScaleAction);
    BAMutil.addActionToMenu( configMenu, controller.dataProjectionAction);
    */

    //// tools menu
    JMenu displayMenu = new JMenu("Display control");
    displayMenu.setMnemonic( 'D');

    BAMutil.addActionToMenu( displayMenu, controller.showGridAction);
    BAMutil.addActionToMenu( displayMenu, controller.showContoursAction);
    BAMutil.addActionToMenu( displayMenu, controller.showContourLabelsAction);
    BAMutil.addActionToMenu( displayMenu, redrawAction);
    toolMenu.add(displayMenu);

    // Loop Control
    JMenu loopMenu = new JMenu("Loop control");
    loopMenu.setMnemonic( 'L');

    BAMutil.addActionToMenu( loopMenu, fieldLoopAction);
    BAMutil.addActionToMenu( loopMenu, levelLoopAction);
    BAMutil.addActionToMenu( loopMenu, timeLoopAction);
    toolMenu.add(loopMenu);

    // MinMax Control
    JMenu mmMenu = new JMenu("ColorScale min/max");
    mmMenu.setMnemonic('C');
    BAMutil.addActionToMenu( mmMenu, minmaxHorizAction);
    BAMutil.addActionToMenu( mmMenu, minmaxLogAction);
    BAMutil.addActionToMenu( mmMenu, minmaxHoldAction);
    toolMenu.add(mmMenu);

    // Zoom/Pan
    JMenu zoomMenu = new JMenu("Zoom/Pan");
    zoomMenu.setMnemonic('Z');
    panz.addActionsToMenu( zoomMenu); // items are added by NavigatedPanelToolbar
    toolMenu.add(zoomMenu);
  }

  private static class LoopControlAction extends AbstractAction {
    SuperComboBox scbox;
    LoopControlAction( SuperComboBox cbox) {
      this.scbox = cbox;
      BAMutil.setActionProperties( this, null, cbox.getName(), false, 0, 0);
    }
    public void actionPerformed(ActionEvent e) {
      scbox.getLoopControl().show();
    }
  }

  private class OpenDatasetTask extends ProgressMonitorTask implements ucar.nc2.util.CancelTask {
    DataFactory factory;
    Dataset invds;

    OpenDatasetTask(Dataset ds) {
      factory = new DataFactory();
      this.invds = ds;
    }

    public void run() {
      NetcdfDataset dataset = null;
      GridDataset gridDataset = null;
      Formatter errlog = new Formatter();

      try {
        dataset = factory.openDataset( invds, true, this, errlog);
        gridDataset = new ucar.nc2.dt.grid.GridDataset(dataset);

      } catch (IOException e) {
        setError("Failed to open datset: "+errlog);
      }

      success = !cancel && (gridDataset != null);
      if (success) controller.setGridDataset( gridDataset);
      done = true;
    }
  }

}


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

package ucar.unidata.geoloc.vertical;

import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.constants.CDM;
import ucar.unidata.util.Parameter;

import java.io.IOException;
import java.util.List;

/**
 * Create a 3D height(z,y,x) array using the CF formula for
 * "ocean s vertical coordinate g1".
 * standard name: ocean_s_coordinate_g1
 *
 * @author Sachin (skbhate@ngi.msstate.edu)
 * @see http://cf-pcmdi.llnl.gov/
 * @see https://www.myroms.org/wiki/index.php/Vertical_S-coordinate#Metadata_Considerations
 */

public class OceanSG1 extends VerticalTransformImpl {

    /**
     * The eta variable name identifier
     */
    public static final String ETA = "Eta_variableName";

    /**
     * The "s" variable name identifier
     */
    public static final String S = "S_variableName";

    /**
     * The "depth" variable name identifier
     */
    public static final String DEPTH = "Depth_variableName";

    /**
     * The "depth c" variable name identifier
     */
    public static final String DEPTH_C = "Depth_c_variableName";

    /**
     * The "C" variable name identifier
     */
    public static final String C = "c_variableName";
    /**
     * the values of depth_c
     */
    private double depth_c;

    /**
     * The eta, s, C and depth variables
     */
    private Variable etaVar, sVar, depthVar, cVar, depthCVar;

    /**
     * Create a new vertical transform for Ocean_S_coordinate_g1
     *
     * @param ds      dataset
     * @param timeDim time dimension
     * @param params  list of transformation Parameters
     */
    public OceanSG1(NetcdfFile ds, Dimension timeDim, List<Parameter> params) {

        super(timeDim);
        String etaName = getParameterStringValue(params, ETA);
        String sName = getParameterStringValue(params, S);
        String depthName = getParameterStringValue(params, DEPTH);
        String depthCName = getParameterStringValue(params, DEPTH_C);
        String cName = getParameterStringValue(params, C);

        etaVar    = ds.findVariable(etaName);
        sVar      = ds.findVariable(sName);
        depthVar  = ds.findVariable(depthName);
        depthCVar = ds.findVariable(depthCName);
        cVar = ds.findVariable(cName);

        units     = ds.findAttValueIgnoreCase(depthVar, CDM.UNITS, "none");
    }

    /**
     * Get the 3D vertical coordinate array for this time step.
     *
     * @param timeIndex the time index. Ignored if !isTimeDependent().
     * @return vertical coordinate array
     * @throws java.io.IOException problem reading data
     * @throws ucar.ma2.InvalidRangeException _more_
     */
   public ArrayDouble.D3 getCoordinateArray(int timeIndex)
            throws IOException, InvalidRangeException {
        Array etaArray   = readArray(etaVar, timeIndex);
        Array sArray     = readArray(sVar, timeIndex);
        Array depthArray = readArray(depthVar, timeIndex);
        Array cArray = readArray(cVar, timeIndex);

       depth_c = depthCVar.readScalarDouble();

       return makeHeight(etaArray, sArray, depthArray, cArray, depth_c);
    }

    /** Add new method for retrieving 1D Z values for specified indices for lat/lon. */
    /* -Sachin */
    /**
        * Get the 1D vertical coordinate array for this time step and
        * the specified X,Y index for Lat-Lon point.
        *
        * @param timeIndex the time index. Ignored if !isTimeDependent().
        * @param xIndex    the x index
        * @param yIndex    the y index
        * @return vertical coordinate array
        * @throws java.io.IOException problem reading data
        * @throws ucar.ma2.InvalidRangeException _more_
        */
      public ArrayDouble.D1 getCoordinateArray1D(int timeIndex, int xIndex, int yIndex)
               throws IOException, InvalidRangeException {
           Array etaArray   = readArray(etaVar, timeIndex);
           Array sArray     = readArray(sVar, timeIndex);
           Array depthArray = readArray(depthVar, timeIndex);
            Array cArray = readArray(cVar, timeIndex);

               depth_c = depthCVar.readScalarDouble();

           return makeHeight1D(etaArray, sArray, depthArray, cArray, depth_c, xIndex, yIndex);
       }

    /**
     * Make height from the given data.

     *
     * height(x,y,z) =  S(x,y,z) + eta(x,y) * (1 + S(x,y,z) / depth([n],x,y) )
     *
     * where,
     * S(x,y,z) = depth_c*s(z) + (depth([n],x,y)-depth_c)*C(z)
     * /
     *
     * @param eta     eta Array
     * @param s       s Array
     * @param depth   depth Array
     * @param c       c Array
     * @param depth_c value of depth_c
     * @return height data
     */
   private ArrayDouble.D3 makeHeight(Array eta, Array s, Array depth,
                                      Array c, double depth_c) {
        int            nz         = (int) s.getSize();
        Index          sIndex     = s.getIndex();
        Index          cIndex     = c.getIndex();

        int[]          shape2D    = eta.getShape();
        int            ny         = shape2D[0];
        int            nx         = shape2D[1];
        Index          etaIndex   = eta.getIndex();
        Index          depthIndex = depth.getIndex();

        ArrayDouble.D3 height     = new ArrayDouble.D3(nz, ny, nx);

        for (int z = 0; z < nz; z++) {
            double sz    = s.getDouble(sIndex.set(z));
            double cz    = c.getDouble(cIndex.set(z));

            double term1 = depth_c * sz;

            for (int y = 0; y < ny; y++) {
                for (int x = 0; x < nx; x++) {

                    double fac1 = depth.getDouble(depthIndex.set(y, x));
                    double term2 = (fac1 - depth_c) * cz;

                    double Sterm = term1 + term2;

                    double term3 = eta.getDouble(etaIndex.set(y, x));
                    double term4 = 1 + Sterm/fac1;
                    double hterm = Sterm + term3 * term4;

                    height.set(z, y, x,hterm);

                }
            }
        }

        return height;
    }

  private ArrayDouble.D1 makeHeight1D(Array eta, Array s, Array depth,
                                      Array c, double depth_c, int x_index, int y_index) {
        int            nz         = (int) s.getSize();
        Index          sIndex     = s.getIndex();
        Index          cIndex     = c.getIndex();

        Index          etaIndex   = eta.getIndex();
        Index          depthIndex = depth.getIndex();

       ArrayDouble.D1 height     = new ArrayDouble.D1(nz);

        for (int z = 0; z < nz; z++) {
            double sz    = s.getDouble(sIndex.set(z));
            double cz    = c.getDouble(cIndex.set(z));

            double term1 = depth_c * sz;

            double fac1 = depth.getDouble(depthIndex.set(y_index, x_index));
            double term2 = (fac1 - depth_c) * cz;

            double Sterm = term1 + term2;

            double term3 = eta.getDouble(etaIndex.set(y_index, x_index));
            double term4 = 1 + Sterm/fac1;
            double hterm = Sterm + term3 * term4;

            height.set(z,hterm);
        }

        return height;
    }
}


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

import ucar.ma2.Index;
import ucar.ma2.Section;
import ucar.ma2.InvalidRangeException;

/**
 * LayoutRegularSegmented has data stored in segments that are regularly spaced.
 * This is now Netcdf-3 "record variables" are laid out.
 *
 * @author caron
 * @since Dec 31, 2007
 */
public class LayoutRegularSegmented implements Layout {
  private long total, done, innerNelems;
  private long startPos;
  private long recSize;
  private int elemSize;

  // outer chunk
  private IndexChunker chunker;
  private IndexChunker.Chunk chunkOuter;

  // inner chunk = deal with segmentation
  private IndexChunker.Chunk chunkInner = new IndexChunker.Chunk(0,0,0);

  private static final boolean debugNext = false;

  /**
   * Constructor.
   *
   * @param startPos starting address of the entire data array.
   * @param elemSize size of an element in bytes.
   * @param recSize  size of outer stride in bytes
   * @param srcShape    shape of the entire data array. must have rank > 0
   * @param wantSection the wanted section of data
   * @throws ucar.ma2.InvalidRangeException if ranges are misformed
   */
  public LayoutRegularSegmented(long startPos, int elemSize, long recSize, int[] srcShape, Section wantSection) throws InvalidRangeException {
    assert startPos > 0;
    assert elemSize > 0;
    assert recSize > 0;
    assert srcShape.length > 0;

    this.startPos = startPos;
    this.elemSize = elemSize;
    this.recSize = recSize;

    chunker = new IndexChunker(srcShape, wantSection);
    this.total = chunker.getTotalNelems();
    this.innerNelems = (srcShape[0] == 0) ? 0 : Index.computeSize(srcShape) / srcShape[0];
    this.done = 0;
  }

  public long getTotalNelems() {
    return total;
  }

  public int getElemSize() {
    return elemSize;
  }

  public boolean hasNext() {
    return done < total;
  }

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

  private long getFilePos(long elem) {
    long segno = elem / innerNelems;
    long offset = elem % innerNelems;
    return startPos + segno * recSize + offset * elemSize;
  }

  // how many more elements are in this segment ?
  private int getMaxElem(long startElem) {
    return (int) (innerNelems - startElem % innerNelems);
  }

  private int needInner = 0;
  private int doneInner = 0;

  public Chunk next() {
    IndexChunker.Chunk result;

    if (needInner > 0) {
      result = nextInner(false, 0);

    } else {
      result = nextOuter();
      int nelems = getMaxElem( result.getSrcElem());
      if (nelems < result.getNelems())
        result = nextInner(true, nelems);
    }

    done += result.getNelems();
    doneInner += result.getNelems();
    needInner -= result.getNelems();

    if (debugNext)
      System.out.println(" next chunk: " + result);

    return result;
  }

  private IndexChunker.Chunk nextInner(boolean first, int nelems) {
    if (first) {
      chunkInner.setNelems(nelems);
      chunkInner.setDestElem( chunkOuter.getDestElem());
      needInner = chunkOuter.getNelems();
      doneInner = 0;

    } else {
      chunkInner.incrDestElem( chunkInner.getNelems()); // increment using last chunks' value
      nelems = getMaxElem( chunkOuter.getSrcElem() + doneInner);
      nelems = Math.min(nelems, needInner);
      chunkInner.setNelems(nelems); // set this chunk's value
    }

    chunkInner.setSrcElem( chunkOuter.getSrcElem() + doneInner);
    chunkInner.setSrcPos( getFilePos( chunkOuter.getSrcElem() + doneInner));
    return chunkInner;
  }

  public IndexChunker.Chunk nextOuter() {
    chunkOuter = chunker.next();
    chunkOuter.setSrcPos( getFilePos( chunkOuter.getSrcElem()));
    return chunkOuter;
  }

}


/* Copyright 2012, UCAR/Unidata.
   See the LICENSE file for more information. */

/*****************************************/

package dap4.core.ce.parser;

import dap4.core.dmr.parser.ParseException;

class CELexer implements CEParserImpl.Lexer
{

    //////////////////////////////////////////////////
    // Constants

    /**
     * Single character delimiters. Note that '/' is not
     * included; it will be part of the leading name.
     */
    static final String DELIMS = "[].:,;|=<>!";

    /**
     * Escape Character
     */
    static final char ESCAPE = '\\';

    /**
     * End-of-string marker
     */
    static final char EOS = '\0';

    static final int CONTEXTLEN = 20; // yyerror shows last CONTEXTLEN characters of input

    /**
     * Hex digits
     */
    static final String hexdigits = "0123456789abcdefABCDEF";

    /**
     * Key words: none currently
     */
    static final String[] keywords = new String[]{
    };

    /**
     * Key word tokens: none currently
     */
    static final int[] keytokens = new int[]{
    };

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

    /**
     * Equivalent of StringReader that allows for better
     * access to position info
     */

    static class TextStream
    {

        // Don't bother with getters
        String text = null; // source of text to lex
        int mark = 0;
        int next = 0; // next unread character
        int textlen = 0;

        public TextStream()
        {
        }

        public String around()
        {
            String prefix = text.substring(mark - 10, mark);
            String suffix = text.substring(mark, mark + 10);
            return prefix + "|" + suffix;
        }

        public void setText(String text)
        {
            this.textlen = text.length();
            this.text = text + '\0'; // Null terminate
        }

        public String getText()
        {
            return text.substring(0, this.textlen);
        }

        public String toString()
        {
            return getText();
        }

        int peek()
        {
            return text.charAt(next);
        }

        void
        backup()
        {
            if(next <= 0) next = 0;
            else
                next--;
        }

        int
        read()
        {
            char c = text.charAt(next);
            if(c != EOS) next++;
            return c;
        }

        void
        mark()
        {
            this.mark = this.next;
        }

    }

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

    /**
     * Per-lexer state
     */

    CEParserImpl parsestate = null; // our parent parser

    Object lval = null;

    TextStream text = null;
    int charno = 0;
    //int lineno = 0; // not currently used
    StringBuilder yytext = null;

    //////////////////////////////////////////////////
    // Constructor(s)

    public CELexer(CEParserImpl state)
    {
        this();
        setParser(state);
    }

    public CELexer()
    {
        this.text = new TextStream();
        yytext = new StringBuilder();
        lval = null;
    }

    /* Get/Set */

    void setParser(CEParserImpl state)
    {
        this.parsestate = state;
    }

    void setText(String text)
    {
        this.text.setText(text);
    }

    public String getInput()
    {
        return text.getText();
    }

    /**
     * The Bison Lexer interface methods: yylex, getLval, yyerror.
     */

    /**
     * Entry point for the scanner.
     * Returns the token identifier corresponding
     * to the next token and prepares to return the semantic value
     * of the token.
     *
     * @return the token identifier corresponding to the next token.
     */

    public int
    yylex()
        throws ParseException
    {
        int token;
        int c = 0;
        token = 0;
        yytext.setLength(0);
        text.mark();

        token = -1;
        while(token < 0 && (c = text.read()) != EOS) {
            if(c <= ' ' || c == '\177') {
                /* whitespace: ignore */
            } else if(c == '"' || c == '\'') {
                int delim = c;
                boolean more = true;
                /* We have a string token; will be reported as STRINGCONST */
                while(more && (c = text.read()) > 0) {
                    switch (c) {
                    case EOS:
                        throw new ParseException("Unterminated character or string constant");
                    case '"':
                        more = (delim != c);
                        break;
                    case '\'':
                        more = (delim != c);
                        break;
                    case ESCAPE:
/* Suppress for now
            if(c >= '0' && c <= '9') {
			int decimal = 0;
		        do {
                            decimal = decimal * 10 + (c - '0');
                            c = text.read();
                        } while(c >= '0' && c <= '9');
                        c = decimal;
			text.backup();
                        break;
		    } else
*/
                        switch (c) {
                        case 'r':
                            c = '\r';
                            break;
                        case 'n':
                            c = '\n';
                            break;
                        case 'f':
                            c = '\f';
                            break;
                        case 't':
                            c = '\t';
                            break;
                        default:
                            break;
                        }
                        break;
                    default:
                        break;
                    }
                    if(more) yytext.append((char) c);
                }
                token = CEParserImpl.Lexer.STRING;
            } else if(DELIMS.indexOf(c) >= 0) {
                // Single char delimiter
                yytext.append((char) c);
                token = c;
            } else { // Assume we have a word or integer
                yytext.append((char) c);
                while((c = text.read()) > 0) {
                    if(c <= ' ' || c == '\177') break; // whitespace
                    if(DELIMS.indexOf(c) >= 0) break;
                    if(c == ESCAPE) {
                        c = text.read();
                        if(c == EOS)
                            throw new ParseException("Unterminated backslash escape");
                    }
                    yytext.append((char) c);
                }
                // pushback the delimiter
                if(c != EOS) text.backup();
                try {// See if this looks like an integer
                    long num = Long.parseLong(yytext.toString());
                    token = CEParserImpl.Lexer.LONG;
                } catch (NumberFormatException nfe) {
                    token = CEParserImpl.Lexer.NAME;
                }
            }
        }
        if(c == EOS && token < 0) {
            token = 0;
            lval = null;
        } else {
            lval = (yytext.length() == 0 ? (String) null : yytext.toString());
        }
        if(parsestate.getDebugLevel() > 0)
            dumptoken(token, (String) lval);
        return token; // Return the type of the token
    }

    void
    dumptoken(int token, String lval)
        throws ParseException
    {
        String stoken;
        if(token < '\177')
            stoken = Character.toString((char) token);
        else
            switch (token) {
            case CEParserImpl.Lexer.STRING:
                stoken = '"' + lval + '"';
                break;
            case CEParserImpl.Lexer.LONG:
                stoken = lval;
                break;
            case CEParserImpl.Lexer.DOUBLE:
                stoken = lval;
                break;
            case CEParserImpl.Lexer.BOOLEAN:
                stoken = lval;
                break;
            case CEParserImpl.Lexer.NAME:
                stoken = lval;
                break;
            default:
                stoken = "X" + Integer.toString(token);
            }
        System.err.println("TOKEN = |" + stoken + "|");

    }

    static int
    tohex(int c)
        throws ParseException
    {
        if(c >= 'a' && c <= 'f') return (c - 'a') + 0xa;
        if(c >= 'A' && c <= 'F') return (c - 'A') + 0xa;
        if(c >= '0' && c <= '9') return (c - '0');
        return -1;
    }

    /**
     * Method to retrieve the semantic value of the last scanned token.
     * Part of Lexer interface.
     *
     * @return the semantic value of the last scanned token.
     */
    public Object getLVal()
    {
        return this.lval;
    }

    /**
     * Entry point for error reporting.  Emits an error
     * in a user-defined way.
     * Part of Lexer interface.
     *
     * @param s The string for the error message.
     */
    public void yyerror(String s)
    {
        System.err.println("CEParserImpl.yyerror: " + s + "; parse failed at char: " + charno + "; near: ");
        String context = getInput();
        int show = (context.length() < CONTEXTLEN ? context.length() : CONTEXTLEN);
        System.err.println(context.substring(context.length() - show) + "^");
        new Exception().printStackTrace(System.err);
    }

}


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

import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import ucar.nc2.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.dataset.*;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;
import ucar.unidata.geoloc.LatLonRect;
import ucar.unidata.geoloc.ProjectionRect;

/**
 * fork ucar.nc2.dt.grid for adaption to Coverage (this class can evolve as needed and not worry about backwards compatibility).
 * uses NetcdfDataset / CoordinateSystem.
 *
 * @see DtCoverageAdapter
 * @author caron
 * @since 5/26/2015
 */
public class DtCoverageDataset implements Closeable {

  /**
   * Open a netcdf dataset, using NetcdfDataset.defaultEnhanceMode plus CoordSystems
   * and turn into a DtCoverageDataset.
   *
   * @param location netcdf dataset to open, using NetcdfDataset.acquireDataset().
   * @return GridDataset
   * @throws java.io.IOException on read error
   * @see ucar.nc2.dataset.NetcdfDataset#acquireDataset
   */
  static public DtCoverageDataset open(String location) throws java.io.IOException {
    DatasetUrl durl = DatasetUrl.findDatasetUrl(location);
    return open(durl, NetcdfDataset.getDefaultEnhanceMode());
  }

  static public DtCoverageDataset open(DatasetUrl durl) throws java.io.IOException {
    return open(durl, NetcdfDataset.getDefaultEnhanceMode());
  }

  /**
   * Open a netcdf dataset, using NetcdfDataset.defaultEnhanceMode plus CoordSystems
   * and turn into a DtCoverageDataset.
   *
   * @param durl    netcdf dataset to open, using NetcdfDataset.acquireDataset().
   * @param enhanceMode open netcdf dataset with this enhanceMode
   * @return GridDataset
   * @throws java.io.IOException on read error
   * @see ucar.nc2.dataset.NetcdfDataset#acquireDataset
   */
  static public DtCoverageDataset open(DatasetUrl durl, Set<NetcdfDataset.Enhance> enhanceMode) throws java.io.IOException {
    NetcdfDataset ds = ucar.nc2.dataset.NetcdfDataset.acquireDataset(null, durl, enhanceMode, -1, null, null);
    return new DtCoverageDataset(ds, null);
  }

  static public DtCoverageDataset open(NetcdfDataset ds) throws java.io.IOException {
    return new DtCoverageDataset(ds, null);
  }

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

  private NetcdfDataset ncd;
  private FeatureType coverageType;

  private ArrayList<DtCoverage> grids = new ArrayList<>();
  private Map<String, Gridset> gridsetHash = new HashMap<>();
  private List<Gridset> gridSets = new ArrayList<>();

  private LatLonRect llbbMax = null;
  private CalendarDateRange dateRangeMax = null;
  private ProjectionRect projBB = null;

  /**
   * Create a DtCoverageDataset from a NetcdfDataset.
   *
   * @param ncd underlying NetcdfDataset, will do Enhance.CoordSystems if not already done.
   * @throws java.io.IOException on read error
   */
  public DtCoverageDataset(NetcdfDataset ncd) throws IOException {
    this(ncd, null);
  }

  /**
   * Create a DtCoverageDataset from a NetcdfDataset.
   *
   * @param ncd       underlying NetcdfDataset, will do Enhance.CoordSystems if not already done.
   * @param parseInfo put parse info here, may be null
   * @throws java.io.IOException on read error
   */
  public DtCoverageDataset(NetcdfDataset ncd, Formatter parseInfo) throws IOException {
    this.ncd = ncd;

    // ds.enhance(EnumSet.of(NetcdfDataset.Enhance.CoordSystems));
    Set<NetcdfDataset.Enhance> enhance = ncd.getEnhanceMode();
    if (enhance == null || enhance.isEmpty()) enhance = NetcdfDataset.getDefaultEnhanceMode();
    ncd.enhance(enhance);

    DtCoverageCSBuilder facc = DtCoverageCSBuilder.classify(ncd, parseInfo);
    if (facc != null)
      this.coverageType = facc.type;

    Map<String, Gridset> csHash = new HashMap<>();
    for (CoordinateSystem cs : ncd.getCoordinateSystems()) {
      DtCoverageCSBuilder fac = new DtCoverageCSBuilder(ncd, cs, parseInfo);
      if (fac.type == null) continue;
      DtCoverageCS ccs = fac.makeCoordSys();
      if (ccs == null) continue;
      Gridset cset = new Gridset(ccs);
      gridSets.add(cset);
      gridsetHash.put(ccs.getName(), cset);
      csHash.put(cs.getName(), cset);
    }

    for (Variable v : ncd.getVariables()) {
      VariableEnhanced ve = (VariableEnhanced) v;
      List<CoordinateSystem> css = ve.getCoordinateSystems();
      if (css.size() == 0) continue;
      Collections.sort(css, (o1, o2) -> o2.getCoordinateAxes().size() - o1.getCoordinateAxes().size());
      CoordinateSystem cs = css.get(0);    // the largest one
      Gridset cset = csHash.get(cs.getName());
      if (cset == null) continue;
      DtCoverage ci = new DtCoverage(this, cset.gcc, (VariableDS) ve);
      cset.grids.add(ci);
      grids.add(ci);
    }
  }

  private void makeHorizRanges() {

    for (Gridset gset : getGridsets()) {
      DtCoverageCS gcs = gset.getGeoCoordSystem();

      ProjectionRect bb = gcs.getBoundingBox();
      if (projBB == null)
        projBB = bb;
      else if (bb != null)
        projBB.add(bb);

      LatLonRect llbb = gcs.getLatLonBoundingBox();
      if (llbbMax == null)
        llbbMax = llbb;
      else if (llbb != null)
        llbbMax.extend(llbb);
    }
  }

  private void makeTimeRanges() {

    for (Gridset gset : getGridsets()) {
      DtCoverageCS gcs = gset.getGeoCoordSystem();
      CalendarDateRange dateRange = gcs.getCalendarDateRange();
      if (dateRange != null) {
        if (dateRangeMax == null)
          dateRangeMax = dateRange;
        else
          dateRangeMax = dateRangeMax.extend(dateRange);
      }
    }
  }

  public FeatureType getCoverageType() {
    return coverageType;
  }

  // stuff to satisfy ucar.nc2.dt.TypedDataset
  public String getTitle() {
    String title = ncd.getTitle();
    if (title == null)
      title = ncd.findAttValueIgnoreCase(null, CDM.TITLE, null);
    if (title == null)
      title = getName();
    return title;
  }

  public String getDescription() {
    String desc = ncd.findAttValueIgnoreCase(null, "description", null);
    if (desc == null)
      desc = ncd.findAttValueIgnoreCase(null, CDM.HISTORY, null);
    return (desc == null) ? getName() : desc;
  }

  public String getLocation() {
    return ncd.getLocation();
  }

  public CalendarDateRange getCalendarDateRange() {
    if (dateRangeMax == null) makeTimeRanges();
    return dateRangeMax;
  }

  public CalendarDate getCalendarDateStart() {
    if (dateRangeMax == null) makeTimeRanges();
    return (dateRangeMax == null) ? null : dateRangeMax.getStart();
  }

  public CalendarDate getCalendarDateEnd() {
    if (dateRangeMax == null) makeTimeRanges();
    return (dateRangeMax == null) ? null : dateRangeMax.getEnd();
  }

  public LatLonRect getBoundingBox() {
    if (llbbMax == null) makeHorizRanges();
    return llbbMax;
  }

  public ProjectionRect getProjBoundingBox() {
    if (llbbMax == null) makeHorizRanges();
    return projBB;
  }

  public void calcBounds() throws java.io.IOException {
    // not needed
  }

  public List<Attribute> getGlobalAttributes() {
    return ncd.getGlobalAttributes();
  }

  public Dimension findDimension(String name) {
    return ncd.findDimension(name);
  }

  public Attribute findGlobalAttributeIgnoreCase(String name) {
    return ncd.findGlobalAttributeIgnoreCase(name);
  }

  public List<VariableSimpleIF> getDataVariables() {
    List<VariableSimpleIF> result = new ArrayList<>(grids.size());
    for (DtCoverage grid : getGrids()) {
      if (grid.getVariable() != null) // LOOK could make Adaptor if no variable
        result.add(grid.getVariable());
    }
    return result;
  }

  public VariableSimpleIF getDataVariable(String shortName) {
    return ncd.getRootGroup().findVariable(shortName);
  }

  public NetcdfFile getNetcdfFile() {
    return ncd;
  }

  /**
   * the name of the dataset is the last part of the location
   *
   * @return the name of the dataset
   */
  public String getName() {
    String loc = ncd.getLocation();
    int pos = loc.lastIndexOf('/');
    if (pos < 0)
      pos = loc.lastIndexOf('\\');
    return (pos < 0) ? loc : loc.substring(pos + 1);
  }

  /**
   * @return the underlying NetcdfDataset
   */
  public NetcdfDataset getNetcdfDataset() {
    return ncd;
  }

  /**
   * @return the list of GeoGrid objects contained in this dataset.
   */
  public List<DtCoverage> getGrids() {
    return new ArrayList<>(grids);
  }

  public DtCoverage findGridDatatype(String name) {
    return findGridByName(name);
  }

  /**
   * Return GeoGrid objects grouped by GeoGridCoordSys. All GeoGrid in a Gridset
   * have the same GeoGridCoordSys.
   *
   * @return List of type ucar.nc2.dt.GridDataset.Gridset
   */
  public List<Gridset> getGridsets() {
    return new ArrayList<>(gridsetHash.values());
  }

  /**
   * find the named GeoGrid.
   *
   * @param fullName find this GeoGrid by full name
   * @return the named GeoGrid, or null if not found
   */
  public DtCoverage findGridByName(String fullName) {
    for (DtCoverage ggi : grids) {
      if (fullName.equals(ggi.getFullName()))
        return ggi;
    }
    return null;
  }

  /**
   * find the named GeoGrid.
   *
   * @param shortName find this GeoGrid by short name
   * @return the named GeoGrid, or null if not found
   */
  public DtCoverage findGridByShortName(String shortName) {
    for (DtCoverage ggi : grids) {
      if (shortName.equals(ggi.getShortName()))
        return ggi;
    }
    return null;
  }

  public DtCoverage findGridByFullName(String fullName) {
    for (DtCoverage ggi : grids) {
      if (fullName.equals(ggi.getFullName()))
        return ggi;
    }
    return null;
  }

  public DtCoverage findGridDatatypeByAttribute(String attName, String attValue) {
    for (DtCoverage ggi : grids) {
      for (Attribute att : ggi.getAttributes())
        if (attName.equals(att.getShortName()) && attValue.equals(att.getStringValue()))
          return ggi;
    }
    return null;
  }

  /**
   * Get Details about the dataset.
   */
  public String getDetailInfo() {
    Formatter buff = new Formatter();
    getDetailInfo(buff);
    return buff.toString();
  }

  public void getDetailInfo(Formatter buff) {
    getInfo(buff);
    buff.format("%n%n----------------------------------------------------%n");
    try (NetcdfDatasetInfo info = new NetcdfDatasetInfo(ncd)) {
      buff.format("%s", info.getParseInfo());
    } catch (IOException e) {
      buff.format("NetcdfDatasetInfo failed");
    }
    buff.format("%n%n----------------------------------------------------%n");
    buff.format("%s", ncd.toString());
    buff.format("%n%n----------------------------------------------------%n");
  }

  /**
   * Show Grids and coordinate systems.
   *
   * @param buf put info here
   */
  private void getInfo(Formatter buf) {
    int countGridset = 0;

    for (Gridset gs : gridsetHash.values()) {
      DtCoverageCS gcs = gs.getGeoCoordSystem();
      buf.format("%nGridset %d  coordSys=%s", countGridset, gcs);
      buf.format(" LLbb=%s ", gcs.getLatLonBoundingBox());
      if ((gcs.getProjection() != null) && !gcs.getProjection().isLatLon())
        buf.format(" bb= %s", gcs.getBoundingBox());
      buf.format("%n");
      buf.format("Name__________________________Unit__________________________hasMissing_Description%n");
      for (DtCoverage grid : gs.getGrids()) {
        buf.format("%s%n", grid.getInfo());
      }
      countGridset++;
      buf.format("%n");
    }

    buf.format("%nGeoReferencing Coordinate Axes%n");
    buf.format("Name__________________________Units_______________Type______Description%n");
    for (CoordinateAxis axis : ncd.getCoordinateAxes()) {
      if (axis.getAxisType() == null) continue;
      axis.getInfo(buf);
      buf.format("%n");
    }
  }

  /**
   * This is a set of GeoGrids with the same GeoCoordSys.
   */
  public static class Gridset {

    private DtCoverageCS gcc;
    private List<DtCoverage> grids = new ArrayList<>();

    private Gridset(DtCoverageCS gcc) {
      this.gcc = gcc;
    }

    private void add(DtCoverage grid) {
      grids.add(grid);
    }

    /**
     * Get list of GeoGrid objects
     */
    public List<DtCoverage> getGrids() {
      return grids;
    }

    /**
     * all GeoGrid point to this GeoGridCoordSys
     */
    public DtCoverageCS getGeoCoordSystem() {
      return gcc;
    }

    /**
     * all GeoGrids point to this GeoCoordSysImpl.
     *
     * @deprecated use getGeoCoordSystem() if possible.
     */
    public DtCoverageCS getGeoCoordSys() {
      return gcc;
    }
  }

  public synchronized void close() throws java.io.IOException {
    try {
      if (ncd != null) ncd.close();
    } finally {
      ncd = null;
    }
  }
}


/*
 * Copyright (c) 1998 - 2009. University Corporation for Atmospheric Research/Unidata
 * Portions of this software were developed by the Unidata Program at the
 * University Corporation for Atmospheric Research.
 *
 * Access and use of this software shall impose the following obligations
 * and understandings on the user. The user is granted the right, without
 * any fee or cost, to use, copy, modify, alter, enhance and distribute
 * this software, and any derivative works thereof, and its supporting
 * documentation for any purpose whatsoever, provided that this entire
 * notice appears in all copies of the software, derivative works and
 * supporting documentation.  Further, UCAR requests that the user credit
 * UCAR/Unidata in any publications that result from the use of this
 * software or in any product that includes this software. The names UCAR
 * and/or Unidata, however, may not be used in any advertising or publicity
 * to endorse or promote any products or commercial entity unless specific
 * written permission is obtained from UCAR/Unidata. The user also
 * understands that UCAR/Unidata is not obligated to provide the user with
 * any support, consulting, training or assistance of any kind with regard
 * to the use, operation and performance of this software nor to provide
 * the user with any updates, revisions, new versions or "bug fixes."
 *
 * THIS SOFTWARE IS PROVIDED BY UCAR/UNIDATA "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL UCAR/UNIDATA BE LIABLE FOR ANY SPECIAL,
 * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
 * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 * WITH THE ACCESS, USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package ucar.unidata.geoloc.projection.proj4;

import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.unidata.geoloc.*;
import ucar.unidata.util.Parameter;

import java.util.Formatter;

/**
 * Adapted from com.jhlabs.map.proj.LambertConformalConicProjection
 *
 * @see "http://www.jhlabs.com/java/maps/proj/index.html"
 * @see "http://trac.osgeo.org/proj/"
 *
 * @author caron
 * @since Oct 20, 2009
 */
public class LambertConformalConicEllipse extends ProjectionImpl {
  private final static double TOL = 1.0e-10;

  // projection parameters
  private double lat0deg, lon0deg; // projection origin, degrees
  private double lat0rad, lon0rad; // projection origin, radians
  private double par1deg, par2deg; // standard parellels, degrees
  private double par1rad, par2rad; // standard parellels, radians
  private double falseEasting, falseNorthing; // km

  // earth shape
  private Earth earth;
  private double e;   // earth.getEccentricitySquared
  private double es;  // earth.getEccentricitySquared
  private double totalScale; // scale to convert cartesian coords in km

  private double n;
  private double c;
  private double rho0;

  // spherical vs ellipsoidal
  private boolean isSpherical;

  @Override
  public ProjectionImpl constructCopy() {
    ProjectionImpl result =  new LambertConformalConicEllipse(getOriginLat(), getOriginLon(), getParallelOne(), getParallelTwo(),
            getFalseEasting(), getFalseNorthing(), getEarth());
    result.setDefaultMapArea(defaultMapArea);
    result.setName(name);
    return result;
  }

  /**
   * Constructor with default parameters
   */
  public LambertConformalConicEllipse() {
    this(23.0, -96.0, 29.5, 45.5, 0, 0, new Earth(6378137.0, 0.0, 298.257222101));
  }

  /**
   * Construct a LambertConformal Projection, two standard parellels.
   * For the one standard parellel case, set them both to the same value.
   *
   * @param lat0           lat origin of the coord. system on the projection plane
   * @param lon0           lon origin of the coord. system on the projection plane
   * @param par1           standard parallel 1
   * @param par2           standard parallel 2
   * @param falseEasting  natural_x_coordinate + false_easting = x coordinate in km
   * @param falseNorthing natural_y_coordinate + false_northing = y coordinate  in km
   * @param earth shape of the earth
   * @throws IllegalArgumentException if lat0, par1, par2 = +/-90 deg
   */
  public LambertConformalConicEllipse(double lat0, double lon0, double par1, double par2, double falseEasting, double falseNorthing, Earth earth) {

    super("LambertConformalConicEllipse", false);

    this.lat0deg = lat0;
    this.lon0deg = lon0;

    this.lat0rad = Math.toRadians(lat0);
    this.lon0rad = Math.toRadians(lat0);

    this.par1deg = par1;
    this.par2deg = par2;

    this.par1rad = Math.toRadians(par1);
    this.par2rad = Math.toRadians(par2);

    this.falseEasting = falseEasting;
    this.falseNorthing = falseNorthing;

    this.earth = earth;
		this.e = earth.getEccentricity();
		this.es = earth.getEccentricitySquared();
    this.isSpherical = (e == 0.0);
		this.totalScale = earth.getMajor() * .001; // scale factor for cartesion coords in km.

    initialize();

    addParameter(CF.GRID_MAPPING_NAME, CF.LAMBERT_CONFORMAL_CONIC);
    addParameter(CF.LATITUDE_OF_PROJECTION_ORIGIN, lat0);
    addParameter(CF.LONGITUDE_OF_CENTRAL_MERIDIAN, lon0);
    if (par2 == par1) {
      addParameter(CF.STANDARD_PARALLEL, par1);
    } else {
      double[] data = new double[2];
      data[0] = par1;
      data[1] = par2;
      addParameter(new Parameter(CF.STANDARD_PARALLEL, data));
    }

    if ((falseEasting != 0.0) || (falseNorthing != 0.0)) {
      addParameter(CF.FALSE_EASTING, falseEasting);
      addParameter(CF.FALSE_NORTHING, falseNorthing);
      addParameter(CDM.UNITS, "km");
    }

    addParameter(CF.SEMI_MAJOR_AXIS, earth.getMajor());
    addParameter(CF.INVERSE_FLATTENING, 1.0/earth.getFlattening());

  }

  private void initialize() {

    if ( par1rad == 0 )
      par1rad = par2rad = lat0rad;

    if (Math.abs(par1rad + par2rad) < TOL)
      throw new IllegalArgumentException("par1rad + par2rad < TOL");

    double sinphi = Math.sin(par1rad);
    double cosphi = Math.cos(par1rad);
    boolean isSecant = Math.abs(par1rad - par2rad) >= TOL;
    n = sinphi;

    if (!isSpherical) {
      double ml1, m1;

      m1 = MapMath.msfn(sinphi, cosphi, es);
      ml1 = MapMath.tsfn(par1rad, sinphi, e);
      if (isSecant) {
        n = Math.log(m1 /
           MapMath.msfn(sinphi = Math.sin(par2rad), Math.cos(par2rad), es));
        n /= Math.log(ml1 / MapMath.tsfn(par2rad, sinphi, e));
      }
      c = (rho0 = m1 * Math.pow(ml1, -n) / n);
      rho0 *= (Math.abs(Math.abs(lat0rad) - MapMath.HALFPI) < TOL) ? 0. :
        Math.pow(MapMath.tsfn(lat0rad, Math.sin(lat0rad), e), n);

    } else {
      if (isSecant)
        n = Math.log(cosphi / Math.cos(par2rad)) /
           Math.log(Math.tan(MapMath.QUARTERPI + .5 * par2rad) /
           Math.tan(MapMath.QUARTERPI + .5 * par1rad));

      c = cosphi * Math.pow(Math.tan(MapMath.QUARTERPI + .5 * par1rad), n) / n;
      rho0 = (Math.abs(Math.abs(lat0rad) - MapMath.HALFPI) < TOL) ? 0. :
        c * Math.pow(Math.tan(MapMath.QUARTERPI + .5 * lat0rad), -n);
    }
  }

  /* private void precalculate() {
    if (Math.abs(lat0 - PI_OVER_2) < TOLERANCE) {
      throw new IllegalArgumentException("LambertConformal lat0 = 90");
    }
    if (Math.abs(lat0 + PI_OVER_2) < TOLERANCE) {
      throw new IllegalArgumentException("LambertConformal lat0 = -90");
    }
    if (Math.abs(par1 - 90.0) < TOLERANCE) {
      throw new IllegalArgumentException("LambertConformal par1 = 90");
    }
    if (Math.abs(par1 + 90.0) < TOLERANCE) {
      throw new IllegalArgumentException("LambertConformal par1 = -90");
    }
    if (Math.abs(par2 - 90.0) < TOLERANCE) {
      throw new IllegalArgumentException("LambertConformal par2 = 90");
    }
    if (Math.abs(par2 + 90.0) < TOLERANCE) {
      throw new IllegalArgumentException("LambertConformal par2 = -90");
    }

    double par1r = Math.toRadians(this.par1);
    double par2r = Math.toRadians(this.par2);

    double t1 = Math.tan(Math.PI / 4 + par1r / 2);
    double t2 = Math.tan(Math.PI / 4 + par2r / 2);

    if (Math.abs(par2 - par1) < TOLERANCE) {  // single parallel
      n = Math.sin(par1r);
    } else {
      n = Math.log(Math.cos(par1r) / Math.cos(par2r))
          / Math.log(t2 / t1);
    }

    double t1n = Math.pow(t1, n);
    F = Math.cos(par1r) * t1n / n;
    earthRadiusTimesF = EARTH_RADIUS * F;

    double t0n = Math.pow(Math.tan(Math.PI / 4 + lat0 / 2), n);
    rho = EARTH_RADIUS * F / t0n;

    lon0Degrees = Math.toDegrees(lon0);
    // need to know the pole value for crossSeam
    //Point2D pt = latLonToProj( 90.0, 0.0);
    //maxY = pt.getY();
    //System.out.println("LC = " +pt);
  }  */

  //   public LambertConformalConicEllipse(double lat0, double lon0, double par1, double par2, double falseEasting, double falseNorthing, Earth earth) {

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

    LambertConformalConicEllipse that = (LambertConformalConicEllipse) o;

    if (Double.compare(that.falseEasting, falseEasting) != 0) return false;
    if (Double.compare(that.falseNorthing, falseNorthing) != 0) return false;
    if (Double.compare(that.lat0rad, lat0rad) != 0) return false;
    if (Double.compare(that.lon0rad, lon0rad) != 0) return false;
    if (Double.compare(that.par1rad, par1rad) != 0) return false;
    if (Double.compare(that.par2rad, par2rad) != 0) return false;
    if (!earth.equals(that.earth)) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result;
    long temp;
    temp = Double.doubleToLongBits(lat0rad);
    result = (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(lon0rad);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(par1rad);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(par2rad);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(falseEasting);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(falseNorthing);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    result = 31 * result + earth.hashCode();
    return result;
  }

  /*
   * Check for equality with the Object in question
   *
   * @param proj object to check
   * @return true if they are equal
   *
  public boolean equals2(Object proj) {
    if (!(proj instanceof LambertConformalConicEllipse))
      return false;

    LambertConformalConicEllipse oo = (LambertConformalConicEllipse) proj;

    if ((this.getDefaultMapArea() == null) != (oo.defaultMapArea == null)) return false; // common case is that these are null
    if (this.getDefaultMapArea() != null && !this.defaultMapArea.equals(oo.defaultMapArea)) return false;
    return ((this.getParallelOne() == oo.getParallelOne())
            && (this.getParallelTwo() == oo.getParallelTwo())
            && (this.getOriginLat() == oo.getOriginLat())
            && (this.getOriginLon() == oo.getOriginLon())
            && this.earth.equals(oo.earth));
  }  */

  // bean properties

  public Earth getEarth() { return earth; }

  /**
   * Get the second standard parallel
   *
   * @return the second standard parallel in degrees
   */
  public double getParallelTwo() {
    return par2deg;
  }

  /**
   * Get the first standard parallel
   *
   * @return the first standard parallel in degrees
   */
  public double getParallelOne() {
    return par1deg;
  }

  /**
   * Get the origin longitude.
   *
   * @return the origin longitude in degrees
   */
  public double getOriginLon() {
    return lon0deg;
  }

  /**
   * Get the origin latitude.
   *
   * @return the origin latitude in degrees
   */
  public double getOriginLat() {
    return lat0deg;
  }

     /**
   * Get the false easting, in km.
   *
   * @return the false easting in km
   */
  public double getFalseEasting() {
    return falseEasting;
  }

  /**
   * Get the false northing, in km.
   *
   * @return the false northing in km
   */
  public double getFalseNorthing() {
    return falseNorthing;
  }

   /**
   * Get the label to be used in the gui for this type of projection
   *
   * @return Type label
   */
  public String getProjectionTypeLabel() {
    return "Lambert Conformal Conic Ellipsoidal Earth";
  }

  /**
   * Create a String of the parameters.
   *
   * @return a String of the parameters
   */
  public String paramsToString() {
    Formatter f = new Formatter();
    f.format("origin lat,lon=%f,%f parellels=%f,%f earth=%s", lat0deg, lon0deg, par1deg, par2deg, earth );
    return f.toString();
  }

  /*
   * Create a WKS string  LOOK NOT DONE
   *
   * @return WKS string
   *
  public String toWKS() {
    Formatter sbuff = new Formatter();
    sbuff.format("PROJCS[\"%s\",", getName());
    if (isSpherical) {
      sbuff.format("GEOGCS[\"Normal Sphere (r=6371007)\",");
      sbuff.format("DATUM[\"unknown\",");
      sbuff.format("SPHEROID[\"sphere\",6371007,0]],");
    } else {
      sbuff.format("GEOGCS[\"WGS 84\",");
      sbuff.format("DATUM[\"WGS_1984\",");
      sbuff.format("SPHEROID[\"WGS 84\",6378137,298.257223563],");
      sbuff.format("TOWGS84[0,0,0,0,0,0,0]],");
    }
    sbuff.format("PRIMEM[\"Greenwich\",0],");
    sbuff.format("UNIT[\"degree\",0.0174532925199433]],");
    sbuff.format("PROJECTION[\"Lambert_Conformal_Conic_1SP\"],");
    sbuff.format("PARAMETER[\"latitude_of_origin\",").append(getOriginLat()).append("],");  // LOOK assumes getOriginLat = getParellel
    sbuff.format("PARAMETER[\"central_meridian\",").append(getOriginLon()).append("],");
    sbuff.format("PARAMETER[\"scale_factor\",1],");
    sbuff.format("PARAMETER[\"false_easting\",").append(falseEasting).append("],");
    sbuff.format("PARAMETER[\"false_northing\",").append(falseNorthing).append("],");

    return sbuff.toString();
  } */

  /**
   * This returns true when the line between pt1 and pt2 crosses the seam.
   * When the cone is flattened, the "seam" is lon0 +- 180.
   *
   * @param pt1 point 1
   * @param pt2 point 2
   * @return true when the line between pt1 and pt2 crosses the seam.
   */
  public boolean crossSeam(ProjectionPoint pt1, ProjectionPoint pt2) {
    if (ProjectionPointImpl.isInfinite(pt1) || ProjectionPointImpl.isInfinite(pt2)) {
      return true;
    }

    return false;

    /* opposite signed X values, larger then 5000 km  LOOK ????
    return (pt1.getX() * pt2.getX() < 0)
            && (Math.abs(pt1.getX() - pt2.getX()) > 5000.0); */
  }

  /*
  public ProjectionPoint latLonToProj(LatLonPoint latLon,
                                      ProjectionPointImpl result) {
    double toX, toY;
    double fromLat = latLon.getLatitude();
    double fromLon = latLon.getLongitude();

    fromLat = Math.toRadians(fromLat);
    double dlon = LatLonPointImpl.lonNormal(fromLon - lon0Degrees);
    double theta = n * Math.toRadians(dlon);
    double tn = Math.pow(Math.tan(PI_OVER_4 + fromLat / 2), n);
    double r = earthRadiusTimesF / tn;
    toX = r * Math.sin(theta);
    toY = rho - r * Math.cos(theta);

    result.setLocation(toX + falseEasting, toY + falseNorthing);
    return result;
  }

  /*
	public Point2D.Double project(double x, double y, Point2D.Double out) {
		double rho;
		if (Math.abs(Math.abs(y) - MapMath.HALFPI) < 1e-10)
			rho = 0.0;
		else
			rho = c * (spherical ?
			Math.pow(Math.tan(MapMath.QUARTERPI + .5 * y), -n) :
			Math.pow(MapMath.tsfn(y, Math.sin(y), e), n));
		out.x = scaleFactor * (rho * Math.sin(x *= n));
		out.y = scaleFactor * (rho0 - rho * Math.cos(x));
		return out;
	}
   */

  private double computeTheta(double lon) {
    double dlon = LatLonPointImpl.lonNormal(lon - lon0deg);
    return n * Math.toRadians(dlon);
  }

  // also see Snyder p 101
  public ProjectionPoint latLonToProj(LatLonPoint latLon, ProjectionPointImpl result) {
    double fromLat = Math.toRadians(latLon.getLatitude());
    double theta = computeTheta(latLon.getLongitude());

		double rho = 0.0;
		if (Math.abs(Math.abs(fromLat) - MapMath.HALFPI) >= TOL) {
			double term;
      if (isSpherical)
        term = Math.pow(Math.tan(MapMath.QUARTERPI + .5 * fromLat), -n);
      else
        term = Math.pow(MapMath.tsfn(fromLat, Math.sin(fromLat), e), n);
			rho = c * term;
    }

		double toX = (rho * Math.sin(theta));
		double toY = (rho0 - rho * Math.cos(theta));

    result.setLocation(totalScale * toX + falseEasting, totalScale * toY + falseNorthing);
    return result;
  }

  /*
   public LatLonPoint projToLatLon(ProjectionPoint world,
                                   LatLonPointImpl result) {
     double toLat, toLon;
     double fromX = world.getX() - falseEasting;
     double fromY = world.getY() - falseNorthing;
     double rhop = rho;

     if (n < 0) {
       rhop *= -1.0;
       fromX *= -1.0;
       fromY *= -1.0;
     }

     double yd = (rhop - fromY);
     double theta = Math.atan2(fromX, yd);
     double r = Math.sqrt(fromX * fromX + yd * yd);
     if (n < 0.0) {
       r *= -1.0;
     }

     toLon = (Math.toDegrees(theta / n + lon0));

     if (Math.abs(r) < TOLERANCE) {
       toLat = ((n < 0.0)
           ? -90.0
           : 90.0);
     } else {
       double rn = Math.pow(EARTH_RADIUS * F / r, 1 / n);
       toLat = Math.toDegrees(2.0 * Math.atan(rn) - Math.PI / 2);
     }

     result.setLatitude(toLat);
     result.setLongitude(toLon);
     return result;
   }

	public Point2D.Double projectInverse(double x, double y, Point2D.Double out) {
		x /= scaleFactor;
		y /= scaleFactor;
		double rho = MapMath.distance(x, y = rho0 - y);
		if (rho != 0) {
			if (n < 0.0) {
				rho = -rho;
				x = -x;
				y = -y;
			}
			if (spherical)
				out.y = 2.0 * Math.atan(Math.pow(c / rho, 1.0/n)) - MapMath.HALFPI;
			else
				out.y = MapMath.phi2(Math.pow(rho / c, 1.0/n), e);
			out.x = Math.atan2(x, y) / n;
		} else {
			out.x = 0.0;
			out.y = n > 0.0 ? MapMath.HALFPI : -MapMath.HALFPI;
		}
		return out;
	}
}  */

  public LatLonPoint projToLatLon(ProjectionPoint world, LatLonPointImpl result) {
    double toLat, toLon;
    double fromX = (world.getX() - falseEasting) / totalScale;  // assumes cartesion coords in km
    double fromY = (world.getY() - falseNorthing) / totalScale;

    fromY = rho0 - fromY;
    double rho = MapMath.distance(fromX, fromY);
		if (rho != 0) {
			if (n < 0.0) {
				rho = -rho;
				fromX = -fromX;
				fromY = -fromY;
			}
			if (isSpherical)
				toLat = 2.0 * Math.atan(Math.pow(c / rho, 1.0/n)) - MapMath.HALFPI;
			else
				toLat = MapMath.phi2(Math.pow(rho / c, 1.0/n), e);

			toLon = Math.atan2(fromX, fromY) / n;
       //coverity[swapped_arguments]

		} else {
			toLon = 0.0;
			toLat = n > 0.0 ? MapMath.HALFPI : -MapMath.HALFPI;
		}

    result.setLatitude(Math.toDegrees(toLat));
    result.setLongitude(Math.toDegrees(toLon) + lon0deg);
    return result;
  }

 ////////////////////////////////////////////////////////
  // test

  /*
   proj +inv +proj=aea +lat_0=23.0 +lat_1=29.5 +lat_2=45.5 +a=6378137.0 +rf=298.257222101 +b=6356752.31414 +lon_0=-96.0

Input X,Y:     1730692.593817677      1970917.991173046

results in:

Output Lon,Lat:     -75.649278      39.089117
   *
   */

  private static void toProj(ProjectionImpl p, double lat, double lon) {
    System.out.printf("lon,lat = %f %f%n", lon, lat);
    ProjectionPoint pt = p.latLonToProj(lat, lon);
    System.out.printf("x,y     = %f %f%n", pt.getX(), pt.getY());
    LatLonPoint ll = p.projToLatLon(pt);
    System.out.printf("lon,lat = %f %f%n%n", ll.getLongitude(), ll.getLatitude());
  }

  private static void fromProj(ProjectionImpl p, double x, double y) {
    System.out.printf("x,y     = %f %f%n", x,y);
    LatLonPoint ll = p.projToLatLon(x, y);
    System.out.printf("lon,lat = %f %f%n", ll.getLongitude(), ll.getLatitude());
    ProjectionPoint pt = p.latLonToProj(ll);
    System.out.printf("x,y     = %f %f%n%n", pt.getX(), pt.getY());
  }

  public static void main(String[] args) {
    LambertConformalConicEllipse a = new LambertConformalConicEllipse(23.0, -96.0, 29.5, 45.5, 0, 0, new Earth(6378137.0, 0.0, 298.257222101));
    System.out.printf("proj = %s %s%n%n", a.getName(), a.paramsToString());
    //fromProj(a, 1730.692593817677, 1970.917991173046);
    //toProj(a, 39.089117, -75.649278);

    fromProj(a, 5747, 13470);

  }

}