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

import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.ui.event.ActionSourceListener;
import ucar.nc2.ui.event.ActionValueEvent;
import ucar.nc2.ui.event.ActionValueListener;
import ucar.nc2.ui.geoloc.*;
import ucar.nc2.ui.util.Renderer;
import ucar.nc2.ui.widget.BAMutil;
import ucar.nc2.ui.widget.IndependentDialog;
import ucar.nc2.ui.widget.PopupManager;
import ucar.nc2.ui.widget.RangeDateSelector;
import ucar.nc2.units.DateRange;
import ucar.unidata.geoloc.LatLonPointImpl;
import ucar.unidata.geoloc.LatLonRect;
import ucar.unidata.geoloc.ProjectionRect;
import ucar.unidata.geoloc.Station;
import ucar.util.prefs.ui.Field;
import ucar.util.prefs.ui.PrefPanel;

import javax.swing.*;
import javax.swing.border.EtchedBorder;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

/**
 * A Swing widget for THREDDS clients to choose a station and/or a region from navigatable map.
 *

 * Typically a user listens for property change events:
 *

 *   stationRegionDateChooser.addPropertyChangeListener( new PropertyChangeListener() {
 * public void propertyChange(java.beans.PropertyChangeEvent e) {
 * if (e.getPropertyName().equals("Station")) {
 * selectedStation = (Station) e.getNewValue();
 * ...
 * }
 * else if (e.getPropertyName().equals("GeoRegion")) {
 * geoRegion = (ProjectionRect) e.getNewValue();
 * ...
 * }
 * }
 * });
 *

 *
 * @author caron
 * @version $Revision: 50 $ $Date: 2006-07-12 16:30:06Z $
 */

/* implementation note:
 * do we wnat to remove actionSource ? we have setSelectedStation instead.
 */
public class StationRegionDateChooser extends NPController {
  private boolean regionSelect = true, stationSelect = true, dateSelect = true;

  // station
  private StationRenderer stnRender = null;
  private Station selectedStation;

  // region
  private ProjectionRect geoBounds;
  private ProjectionRect geoSelection;
  private boolean geoSelectionMode = false;
  private Color outlineColor = Color.black;
  private int nfracDig = 3;

  // date
  private RangeDateSelector dateSelector;
  private IndependentDialog dateWindow;
  private AbstractAction dateAction;

  // prefs
  private PrefPanel minmaxPP;
  private Field.Double minLonField, maxLonField, minLatField, maxLatField;

  // events
  private ActionSourceListener actionSource;

  // local caches
  private PopupManager popupInfo = new PopupManager("Station Info");
  private StringBuffer sbuff = new StringBuffer();

  // debugging
  private boolean debugEvent = false;

  /**
   * Default Contructor, allow both region and station selection.
   */
  public StationRegionDateChooser() {
    this(true, true, true);
  }

  /**
   * Constructor
   *
   * @param regionSelect  allow selecting a region
   * @param stationSelect allow selecting a station
   * @param dateSelect    allow selecting a date range
   */
  public StationRegionDateChooser(boolean stationSelect, boolean regionSelect, boolean dateSelect) {
    super();

    this.regionSelect = regionSelect;
    this.stationSelect = stationSelect;
    this.dateSelect = dateSelect;

    np.setGeoSelectionMode(regionSelect && geoSelectionMode);
    // setGeoBounds( np.getMapArea());

    if (stationSelect) {
      stnRender = new StationRenderer();
      addRenderer(stnRender);

      // get Pick events from the navigated panel: mouse click
      np.addPickEventListener(new PickEventListener() {
        public void actionPerformed(PickEvent e) {
          selectedStation = stnRender.pick(e.getLocationPoint());
          if (selectedStation != null) {
            redraw();
            firePropertyChangeEvent(selectedStation, "Station");
            actionSource.fireActionValueEvent(ActionSourceListener.SELECTED, selectedStation);
          }
        }
      });

      // get mouse motion events
      np.addMouseMotionListener(new MouseMotionAdapter() {
        public void mouseMoved(MouseEvent e) {
          Point p = e.getPoint();
          StationRenderer.StationUI sui = stnRender.isOnStation(p);

          if (sui != null) {
            ucar.unidata.geoloc.Station s = sui.getStation();
            sbuff.setLength(0);
            sbuff.append(s.getName());
            sbuff.append(" ");
            sbuff.append("\n");
            if (null != s.getDescription())
              sbuff.append(s.getDescription()).append("\n");
            sbuff.append(LatLonPointImpl.latToString(s.getLatitude(), 4));
            sbuff.append(" ");
            sbuff.append(LatLonPointImpl.lonToString(s.getLongitude(), 4));
            sbuff.append(" ");
            double alt = s.getAltitude();
            if (!Double.isNaN(alt)) {
              sbuff.append(ucar.unidata.util.Format.d(alt, 0));
              sbuff.append(" m");
            }

            popupInfo.show(sbuff.toString(), p, StationRegionDateChooser.this, s);
          } else
            popupInfo.hide();
        }
      });

      // get mouse exit events
      np.addMouseListener(new MouseAdapter() {
        public void mouseExited(MouseEvent e) {
          popupInfo.hide();
        }
      });

      // station was selected
      actionSource = new ActionSourceListener("station") {
        public void actionPerformed(ActionValueEvent e) {
          if (debugEvent) System.out.println(" StationdatasetChooser: actionSource event " + e);
          selectedStation = (ucar.unidata.geoloc.Station) e.getValue();
          redraw();
        }
      };
    }

    if (regionSelect) {
      double defArea = 1.0 / 8; // default area is 1/4 total
      LatLonRect llbb = np.getProjectionImpl().getDefaultMapAreaLL();
      LatLonPointImpl left = llbb.getLowerLeftPoint();
      LatLonPointImpl right = llbb.getUpperRightPoint();
      double centerLon = llbb.getCenterLon();
      double width = llbb.getWidth();
      double centerLat = (right.getLatitude() + left.getLatitude()) / 2;
      double height = right.getLatitude() - left.getLatitude();
      right = new LatLonPointImpl(centerLat + height * defArea, centerLon + width * defArea);
      left = new LatLonPointImpl(centerLat - height * defArea, centerLon - width * defArea);
      LatLonRect selected = new LatLonRect(left, right);
      setGeoSelection(selected);

      // get GeoSelectionEvents from the navigated panel
      np.addGeoSelectionListener(new GeoSelectionListener() {
        public void actionPerformed(GeoSelectionEvent e) {
          setGeoSelection(e.getProjectionRect());
          if (debugEvent) System.out.println("GeoSelectionEvent=" + geoSelection);
          firePropertyChangeEvent(geoSelection, "GeoRegion");
          redraw();
        }
      });
    }

    if (dateSelect) {
      // date selection
      DateRange range = null;
      try {
        range = new DateRange(); // phony
      } catch (Exception e) {
        e.printStackTrace();
      }
      dateSelector = new RangeDateSelector(null, range, true, false, null, false, true);
      dateWindow = new IndependentDialog(null, false, "Date Selection", dateSelector);
      dateAction = new AbstractAction() {
        public void actionPerformed(ActionEvent e) {
          dateWindow.setVisible(true);
        }
      };
      BAMutil.setActionProperties(dateAction, "selectDate", "select date range", false, 'D', -1);
    }

    makeMyUI();
  }

  protected void makeUI() {
  } // override superclass

  private void makeMyUI() {

    AbstractAction incrFontAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        stnRender.incrFontSize();
        redraw();
      }
    };
    BAMutil.setActionProperties(incrFontAction, "FontIncr", "increase font size", false, 'I', -1);

    AbstractAction decrFontAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        stnRender.decrFontSize();
        redraw();
      }
    };
    BAMutil.setActionProperties(decrFontAction, "FontDecr", "decrease font size", false, 'D', -1);

    JCheckBox declutCB = new JCheckBox("Declutter", true);
    declutCB.addActionListener(e -> {
        setDeclutter(((JCheckBox) e.getSource()).isSelected());
    });

    AbstractAction bbAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        geoSelectionMode = !geoSelectionMode;
        np.setGeoSelectionMode(geoSelectionMode);
        redraw();
      }
    };
    BAMutil.setActionProperties(bbAction, "geoselect", "select geo region", true, 'B', -1);
    bbAction.putValue(BAMutil.STATE, geoSelectionMode ? Boolean.TRUE : Boolean.FALSE);

    // the fields use a PrefPanel
    if (regionSelect) {
      minmaxPP = new PrefPanel(null, null);
      minLonField = minmaxPP.addDoubleField("minLon", "minLon", geoSelection.getMinX(), nfracDig, 0, 0, null);
      maxLonField = minmaxPP.addDoubleField("maxLon", "maxLon", geoSelection.getMaxX(), nfracDig, 2, 0, null);
      minLatField = minmaxPP.addDoubleField("minLat", "minLat", geoSelection.getMinY(), nfracDig, 4, 0, null);
      maxLatField = minmaxPP.addDoubleField("maxLat", "maxLat", geoSelection.getMaxY(), nfracDig, 6, 0, null);

      minmaxPP.finish(true, BorderLayout.EAST);
      minmaxPP.addActionListener(e -> {
          // "Apply" was called
          double minLon = minLonField.getDouble();
          double minLat = minLatField.getDouble();
          double maxLon = maxLonField.getDouble();
          double maxLat = maxLatField.getDouble();
          LatLonRect llbb = new LatLonRect(new LatLonPointImpl(minLat, minLon),
                  new LatLonPointImpl(maxLat, maxLon));
          setGeoSelection(llbb);
          redraw();
      });

    }

    // assemble
    setLayout(new BorderLayout());

    if (stationSelect) {
      BAMutil.addActionToContainer(toolPanel, incrFontAction);
      BAMutil.addActionToContainer(toolPanel, decrFontAction);
      toolPanel.add(declutCB);
    }

    if (regionSelect) BAMutil.addActionToContainer(toolPanel, bbAction);
    if (dateSelect) BAMutil.addActionToContainer(toolPanel, dateAction);

    JPanel upperPanel = new JPanel(new BorderLayout());
    if (regionSelect) upperPanel.add(minmaxPP, BorderLayout.NORTH);
    upperPanel.add(toolPanel, BorderLayout.SOUTH);

    JPanel statusPanel = new JPanel(new BorderLayout());
    statusPanel.setBorder(new EtchedBorder());
    JLabel positionLabel = new JLabel("position");
    statusPanel.add(positionLabel, BorderLayout.CENTER);

    np.setPositionLabel(positionLabel);
    add(upperPanel, BorderLayout.NORTH);
    add(np, BorderLayout.CENTER);
    add(statusPanel, BorderLayout.SOUTH);
  }

  /**
   * Add an action to the toolbar.
   *
   * @param act add this action
   */
  public void addToolbarAction(AbstractAction act) {
    BAMutil.addActionToContainer(toolPanel, act);
  }

  /**
   * Fires a PropertyChangeEvent:
   *

   *
propertyName = "Station", getNewValue() = Station
   *
propertyName =  "GeoRegion", getNewValue() = ProjectionRect
   *

   */
  private void firePropertyChangeEvent(Object newValue, String propertyName) {
    firePropertyChange(propertyName, null, newValue);
  }

  public void addActionValueListener(ActionValueListener l) {
    actionSource.addActionValueListener(l);
  }

  public void removeActionValueListener(ActionValueListener l) {
    actionSource.removeActionValueListener(l);
  }

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

  public void setMapArea(ProjectionRect ma) {
    np.getProjectionImpl().setDefaultMapArea(ma);
    //np.setMapArea(ma);
  }

  /**
   * Set the list of Stations.
   *
   * @param stns list of Station
   */
  public void setStations(java.util.List stns) {
    stnRender.setStations(stns);
    redraw(true);
  }

  /**
   * Looks for the station with given id. If found, makes it current. Redraws.
   *
   * @param id must match stationIF.getID().
   */
  public void setSelectedStation(String id) {
    stnRender.setSelectedStation(id);
    selectedStation = stnRender.getSelectedStation();
    assert selectedStation != null;
    np.setLatLonCenterMapArea(selectedStation.getLatitude(), selectedStation.getLongitude());
    redraw();
  }

  /**
   * Get currently selected station, or null if none.
   *
   * @return selected station
   */
  public ucar.unidata.geoloc.Station getSelectedStation() {
    return selectedStation;
  }

  /**
   * Access to the navigated panel.
   *
   * @return navigated panel object
   */
  public NavigatedPanel getNavigatedPanel() {
    return np;
  }

  /**
   * Change the state of decluttering
   *
   * @param declut if true, declutter
   */
  public void setDeclutter(boolean declut) {
    stnRender.setDeclutter(declut);
    redraw();
  }

  /**
   * Get the state of the declutter flag.
   *
   * @return the state of the declutter flag.
   */
  public boolean getDeclutter() {
    return stnRender.getDeclutter();
  }

  /**
   * Redraw the graphics on the screen.
   */
  protected void redraw() {
    long tstart = System.currentTimeMillis();

    java.awt.Graphics2D gNP = np.getBufferedImageGraphics();
    if (gNP == null) // panel not drawn on screen yet
      return;

    // clear it
    gNP.setBackground(np.getBackgroundColor());
    java.awt.Rectangle r = gNP.getClipBounds();
    gNP.clearRect(r.x, r.y, r.width, r.height);

    if (regionSelect && geoSelectionMode) {
      if (geoSelection != null) drawBB(gNP, geoSelection, Color.cyan);
      if (geoBounds != null) drawBB(gNP, geoBounds, null);
      // System.out.println("GeoRegionChooser.redraw geoBounds= "+geoBounds);

      if (geoSelection != null) {
        // gNP.setColor( Color.orange);
        Navigation navigate = np.getNavigation();
        double handleSize = RubberbandRectangleHandles.handleSizePixels / navigate.getPixPerWorld();
        Rectangle2D rect = new Rectangle2D.Double(geoSelection.getX(), geoSelection.getY(), geoSelection.getWidth(), geoSelection.getHeight());
        RubberbandRectangleHandles.drawHandledRect(gNP, rect, handleSize);
        if (debug) System.out.println("GeoRegionChooser.drawHandledRect=" + handleSize + " = " + geoSelection);
      }
    }

    for (int i = 0; i < renderers.size(); i++) {
      ucar.nc2.ui.util.Renderer rend = (Renderer) renderers.get(i);
      rend.draw(gNP, atI);
    }
    gNP.dispose();

    if (debug) {
      long tend = System.currentTimeMillis();
      System.out.println("StationRegionDateChooser draw time = " + (tend - tstart) / 1000.0 + " secs");
    }

    // copy buffer to the screen
    np.repaint();
  }

  private void drawBB(java.awt.Graphics2D g, ProjectionRect bb, Color fillColor) {
    Rectangle2D rect = new Rectangle2D.Double(bb.getX(), bb.getY(), bb.getWidth(), bb.getHeight());
    if (null != fillColor) {
      g.setColor(fillColor);
      g.fill(rect);
    }
    g.setColor(outlineColor);
    g.draw(rect);
  }

  private boolean debug = false;

  public void setGeoBounds(LatLonRect llbb) {
    np.setMapArea(llbb);
    geoBounds = np.getProjectionImpl().latLonToProjBB(llbb);
    np.getProjectionImpl().setDefaultMapArea(geoBounds);
    setGeoSelection(geoBounds);
  }

  public void setGeoBounds(ProjectionRect bb) {
    geoBounds = new ProjectionRect(bb);
    np.setMapArea(bb);
    np.getProjectionImpl().setDefaultMapArea(geoBounds);
  }

  public void setGeoSelection(LatLonRect llbb) {
    np.setGeoSelection(llbb);
    setGeoSelection(np.getGeoSelection());
  }

  public void setGeoSelection(ProjectionRect bb) {
    geoSelection = bb;
    if (minLonField != null) {
      minLonField.setDouble(geoSelection.getMinX());
      minLatField.setDouble(geoSelection.getMinY());
      maxLonField.setDouble(geoSelection.getMaxX());
      maxLatField.setDouble(geoSelection.getMaxY());
    }
    np.setGeoSelection(geoSelection);
  }

  public LatLonRect getGeoSelectionLL() {
    return np.getGeoSelectionLL();
  }

  public ProjectionRect getGeoSelection() {
    return np.getGeoSelection();
  }

  public boolean getGeoSelectionMode() {
    return geoSelectionMode;
  }

  public DateRange getDateRange() {
    if (!dateSelect || !dateWindow.isShowing() || !dateSelector.isEnabled())
      return null;
    return dateSelector.getDateRange();
  }

  public void setDateRange(DateRange range) {
    dateSelector.setDateRange(range);
  }

  /**
   * Wrap this in a JDialog component.
   *
   * @param parent JFrame (application) or JApplet (applet) or null
   * @param title  dialog window title
   * @param modal  is modal
   * @return the JDialog widget
   */
  public JDialog makeDialog(RootPaneContainer parent, String title, boolean modal) {
    return new Dialog(parent, title, modal);
  }

  private class Dialog extends JDialog {

    private Dialog(RootPaneContainer parent, String title, boolean modal) {
      super(parent instanceof Frame ? (Frame) parent : null, title, modal);

      // L&F may change
      UIManager.addPropertyChangeListener(new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent e) {
          if (e.getPropertyName().equals("lookAndFeel"))
            SwingUtilities.updateComponentTreeUI(StationRegionDateChooser.Dialog.this);
        }
      });

      // add a dismiss button
      JPanel buttPanel = new JPanel();
      JButton dismissButton = new JButton("Dismiss");
      dismissButton.addActionListener(e -> {
          setVisible(false);
      });
      buttPanel.add(dismissButton, null);

      // add it to contentPane
      Container cp = getContentPane();
      cp.setLayout(new BorderLayout());
      cp.add(StationRegionDateChooser.this, BorderLayout.CENTER);
      cp.add(buttPanel, BorderLayout.SOUTH);
      pack();
    }
  }

  public static void main(String[] args) {
    StationRegionDateChooser slm = new StationRegionDateChooser();
    slm.setBounds(new Rectangle(10, 10, 400, 200));

    JFrame frame = new JFrame("StationRegionChooser Test");
    frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    });
    frame.getContentPane().add(slm);
    frame.pack();
    frame.setVisible(true);
  }
}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
// $Id: SingleTrajectoryObsDataset.java 63 2006-07-12 21:50:51Z edavis $
package ucar.nc2.dt.trajectory;

import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.dataset.VariableDS;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.StructurePseudo;
import ucar.nc2.VariableSimpleIF;
import ucar.nc2.dt.VariableSimpleSubclass;
import ucar.nc2.dt.*;
import ucar.nc2.units.DateUnit;
import ucar.nc2.units.SimpleUnit;
import ucar.unidata.geoloc.LatLonRect;

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

/**
 * Superclass for for implementations of TrajectoryObsDataset using a
 * NetcdfFile underneath that contains a single trajectory. The file
 * must have a single coordinate variable for time. The time dimension
 * may be UNLIMITED (if time is not UNLIMITED, there must be no UNLIMITED
 * dimension). The file must also have a latitude variable, a longitude
 * variable, and an elevation variable each on the time dimension only.
 * The data variables also must be on the time dimension but they can
 * also have other dimensions.
 * For instance:
 * <pre>
 * time( time)  - convertable to -> double
 * lat( time)   - convertable to -> double
 * lon( time)   - convertable to -> double
 * elev( time)  - convertable to -> double
 * var1( time[, dim#]*)
 * ...
 * varN( time[, dim#]*)
 * </pre>
 *
 * @deprecated use ucar.nc2.ft.*
 * @author edavis
 * @since 5 May 2005 10:12 -0600
 */
public class SingleTrajectoryObsDataset
        extends TypedDatasetImpl
        implements TrajectoryObsDataset
{
  protected String trajectoryId; // the Id String for the trajectory.
  protected int trajectoryNumPoint; // the num of points in the trajectory.
  protected HashMap trajectoryVarsMap; // a Map of all the TypedDataVariables in the trajectory keyed by their names.

  protected Dimension timeDim;
  protected Variable timeVar;
  protected Structure recordVar;
  protected Variable latVar;
  protected Variable lonVar;
  protected Variable elevVar;

  protected String timeVarUnitsString;

  protected double elevVarUnitsConversionFactor;

  protected TrajectoryObsDatatype trajectory;

  public SingleTrajectoryObsDataset() {}

  public SingleTrajectoryObsDataset( NetcdfDataset ncfile)
  {
    super( ncfile);
  }

  /** Setup needed for all SingleTrajectoryObsDatatypes. Can only be called once.
   *
   * Units of time varible must be udunits time units.
   * Units of latitude variable must be convertible to "degrees_north" by udunits.
   * Units of longitude variable must be convertible to "degrees_east" by udunits.
   * Units of altitude variable must be convertible to "meters" by udunits.
   *
   * @throws IllegalArgumentException if units of time, latitude, longitude, or altitude variables are not as required.
   * @throws IllegalStateException if this method has already been called.
   */
  public void setTrajectoryInfo( Config trajConfig )
          throws IOException
  {
    if ( timeDim != null )
      throw new IllegalStateException( "The setTrajectoryInfo() method can only be called once.");

    this.trajectoryId = trajConfig.getTrajectoryId();
    this.timeDim = trajConfig.getTimeDim();
    this.timeVar = trajConfig.getTimeVar();
    this.latVar = trajConfig.getLatVar();
    this.lonVar = trajConfig.getLonVar();
    this.elevVar = trajConfig.getElevVar();

    trajectoryNumPoint = this.timeDim.getLength();
    timeVarUnitsString = this.timeVar.findAttribute( "units" ).getStringValue();

    // Check that time, lat, lon, elev units are acceptable.
    if ( DateUnit.getStandardDate( timeVarUnitsString ) == null )
    {
      throw new IllegalArgumentException( "Units of time variable <" + timeVarUnitsString + "> not a date unit." );
    }
    String latVarUnitsString = this.latVar.findAttribute( "units").getStringValue();
    if ( ! SimpleUnit.isCompatible( latVarUnitsString, "degrees_north" ) )
    {
      throw new IllegalArgumentException( "Units of lat var <" + latVarUnitsString + "> not compatible with \"degrees_north\"." );
    }
    String lonVarUnitsString = this.lonVar.findAttribute( "units" ).getStringValue();
    if ( !SimpleUnit.isCompatible( lonVarUnitsString, "degrees_east" ) )
    {
      throw new IllegalArgumentException( "Units of lon var <" + lonVarUnitsString + "> not compatible with \"degrees_east\"." );
    }
    String elevVarUnitsString = this.elevVar.findAttribute( "units" ).getStringValue();
    if ( !SimpleUnit.isCompatible( elevVarUnitsString, "meters" ) )
    {
      throw new IllegalArgumentException( "Units of elev var <" + elevVarUnitsString + "> not compatible with \"meters\"." );
    }

    try
    {
      elevVarUnitsConversionFactor = getMetersConversionFactor( elevVarUnitsString);
    }
    catch ( Exception e )
    {
      throw new IllegalArgumentException( "Exception on getMetersConversionFactor() for the units of elev var <" + elevVarUnitsString + ">." );
    }

    if ( this.netcdfDataset.hasUnlimitedDimension() && this.netcdfDataset.getUnlimitedDimension().equals( timeDim ) )
    {
      Object result = this.netcdfDataset.sendIospMessage(NetcdfFile.IOSP_MESSAGE_ADD_RECORD_STRUCTURE);
      if ((result != null) && (Boolean) result )
        this.recordVar = (Structure) this.netcdfDataset.getRootGroup().findVariable( "record");
      else
        this.recordVar = new StructurePseudo( this.netcdfDataset, null, "record", timeDim );
    } else {
      this.recordVar = new StructurePseudo( this.netcdfDataset, null, "record", timeDim);
    }

    // @todo HACK, HACK, HACK - remove once addRecordStructure() deals with ncd attribute changes.
    Variable elevVarInRecVar = this.recordVar.findVariable( this.elevVar.getFullNameEscaped());
    if ( ! elevVarUnitsString.equals( elevVarInRecVar.findAttribute( "units").getStringValue()))
    {
      elevVarInRecVar.addAttribute( new Attribute( "units", elevVarUnitsString));
    }

    trajectoryVarsMap = new HashMap();
    //for ( Iterator it = this.recordVar.getVariables().iterator(); it.hasNext(); )
    for ( Iterator it = this.netcdfDataset.getRootGroup().getVariables().iterator(); it.hasNext(); )
    {
      Variable curVar = (Variable) it.next();
      if ( curVar.getRank() > 0 &&
           !curVar.equals( this.timeVar) &&
           ! curVar.equals( this.latVar) &&
           ! curVar.equals( this.lonVar) &&
           ! curVar.equals( this.elevVar) &&
           ( this.recordVar == null ? true : ! curVar.equals( this.recordVar)))
      {
        MyTypedDataVariable typedVar = new MyTypedDataVariable( new VariableDS(  null, curVar, true ) );
        dataVariables.add( typedVar);
        trajectoryVarsMap.put( typedVar.getShortName(), typedVar);
      }
    }

    trajectory = new SingleTrajectory( this.trajectoryId, trajectoryNumPoint,
                                       this.timeVar, timeVarUnitsString,
                                       this.latVar, this.lonVar, this.elevVar,
                                       dataVariables, trajectoryVarsMap);

    startDate = trajectory.getTime( 0);
    endDate = trajectory.getTime( trajectoryNumPoint - 1);

    ( (SingleTrajectory) trajectory).setStartDate( startDate );
    ( (SingleTrajectory) trajectory).setEndDate( endDate );
  }

  protected static double getMetersConversionFactor( String unitsString ) throws Exception
  {
    SimpleUnit unit = SimpleUnit.factoryWithExceptions( unitsString );
    return unit.convertTo( 1.0, SimpleUnit.meterUnit );
  }

  protected void setStartDate() {}

  protected void setEndDate() {}

  protected void setBoundingBox() {}

  public List getTrajectoryIds()
  {
    List l = new ArrayList();
    l.add( trajectoryId);
    return( l);
  }

  public List getTrajectories() // throws IOException;
  {
    List l = new ArrayList();
    l.add( trajectory);
    return( l);
  }

  public TrajectoryObsDatatype getTrajectory( String trajectoryId ) // throws IOException;
  {
    if ( ! trajectoryId.equals( this.trajectoryId)) return( null);
    return( trajectory);
  }

  public String getDetailInfo()
  {
    StringBuffer sbuff = new StringBuffer();
    sbuff.append( "TrajectoryObsDataset\n" );
    sbuff.append( "  adapter   = " + getClass().getName() + "\n" );
    sbuff.append( "  trajectories:" + "\n" );
    for ( Iterator it = this.getTrajectoryIds().iterator(); it.hasNext(); )
    {
      sbuff.append( "      " + (String) it.next() + "\n" );
    }
    sbuff.append( super.getDetailInfo() );

    return sbuff.toString();
  }

  public boolean syncExtend()
  {
    if ( ! this.netcdfDataset.hasUnlimitedDimension()) return false;
    try
    {
      if ( ! this.netcdfDataset.syncExtend() ) return false;
    }
    catch ( IOException e )
    {
      return false;
    }

    // Update number of points in this TrajectoryObsDataset and in the child TrajectoryObsDatatype.
    int newNumPoints = this.timeDim.getLength();
    if ( this.trajectoryNumPoint >= newNumPoints )
      return false;
    this.trajectoryNumPoint = newNumPoints;
    ( (SingleTrajectory) this.trajectory).setNumPoints( this.trajectoryNumPoint);

    // Update end date in this TrajectoryObsDataset and in the child TrajectoryObsDatatype.
    try
    {
      endDate = trajectory.getTime( trajectoryNumPoint - 1 );
    }
    catch ( IOException e )
    {
      return false;
    }
    ( (SingleTrajectory) trajectory ).setEndDate( endDate );

    return true;
  }

  public static class Config
  {
    protected String trajectoryId; // the Id String for the trajectory.

    protected Dimension timeDim;
    protected Variable timeVar;
    protected Variable latVar;
    protected Variable lonVar;
    protected Variable elevVar;

    public Config() {}

    public Config( String trajectoryId, Dimension timeDim, Variable timeVar,
                   Variable latVar, Variable lonVar, Variable elevVar )
    {
      this.trajectoryId = trajectoryId;
      this.timeDim = timeDim;
      this.timeVar = timeVar;
      this.latVar = latVar;
      this.lonVar = lonVar;
      this.elevVar = elevVar;
    }

    public void setTrajectoryId( String trajectoryId )
    {
      this.trajectoryId = trajectoryId;
    }

    public void setTimeDim( Dimension timeDim )
    {
      this.timeDim = timeDim;
    }

    public void setTimeVar( Variable timeVar )
    {
      this.timeVar = timeVar;
    }

    public void setLatVar( Variable latVar )
    {
      this.latVar = latVar;
    }

    public void setLonVar( Variable lonVar )
    {
      this.lonVar = lonVar;
    }

    public void setElevVar( Variable elevVar )
    {
      this.elevVar = elevVar;
    }

    public String getTrajectoryId()
    {
      return trajectoryId;
    }

    public Dimension getTimeDim()
    {
      return timeDim;
    }

    public Variable getTimeVar()
    {
      return timeVar;
    }

    public Variable getLatVar()
    {
      return latVar;
    }

    public Variable getLonVar()
    {
      return lonVar;
    }

    public Variable getElevVar()
    {
      return elevVar;
    }
  }

  private class SingleTrajectory implements TrajectoryObsDatatype
  {

    private String id;
    private String description;
    private int numPoints;
    private Date startDate;
    private Date endDate;
    private String timeVarUnitsString;
    private Variable timeVar;
    private Variable latVar;
    private Variable lonVar;
    private Variable elevVar;
    private List variables;
    private HashMap variablesMap;

    //private Structure struct;

    private SingleTrajectory( String id, int numPoints,
                              Variable timeVar, String timeVarUnitsString,
                              Variable latVar, Variable lonVar, Variable elevVar,
                              List variables, HashMap variablesMap )
    {
      this.description = null;
      this.id = id;
      this.numPoints = numPoints;
      this.timeVarUnitsString = timeVarUnitsString;
      this.timeVar = timeVar;
      this.variables = variables;
      this.variablesMap = variablesMap;
      this.latVar = latVar;
      this.lonVar = lonVar;
      this.elevVar = elevVar;

      //this.struct = new Structure( ncfile, ncfile.getRootGroup(), null, "struct for building StructureDatas");
    }

    protected void setNumPoints( int numPoints )
    {
      this.numPoints = numPoints;
    }
    protected void setStartDate( Date startDate )
    {
      if ( this.startDate != null ) throw new IllegalStateException( "Can only call setStartDate() once." );
      this.startDate = startDate;
    }
    protected void setEndDate( Date endDate )
    {
      //if ( this.endDate != null ) throw new IllegalStateException( "Can only call setEndDate() once.");
      this.endDate = endDate;
    }

    public String getId()
    {
      return( id);
    }

    public String getDescription()
    {
      return( description);
    }

    public int getNumberPoints()
    {
      return( numPoints);
    }

    public List getDataVariables()
    {
      return ( variables );
    }

    public VariableSimpleIF getDataVariable( String name )
    {
      return ( (VariableSimpleIF) variablesMap.get( name ) );
    }

    public PointObsDatatype getPointObsData( int point ) throws IOException
    {
      return( new MyPointObsDatatype( point));
    }

    public Date getStartDate()
    {
      return( startDate);
    }

    public Date getEndDate()
    {
      return( endDate);
    }

    public LatLonRect getBoundingBox()
    {
      return null;
    }

    public Date getTime( int point ) throws IOException
    {
      return ( DateUnit.getStandardDate( getTimeValue( point ) + " " + timeVarUnitsString ) );
    }

    public ucar.unidata.geoloc.EarthLocation getLocation( int point ) throws IOException
    {
      return ( new MyEarthLocation( point ) );
    }

    public String getTimeUnitsIdentifier()
    {
      return( timeVarUnitsString);
    }

    public double getTimeValue( int point ) throws IOException
    {
      Array array = null;
      try
      {
        array = getTime( this.getPointRange( point ) );
      }
      catch ( InvalidRangeException e )
      {
        IllegalArgumentException iae = new IllegalArgumentException( "Point <" + point + "> not in valid range <0, " + ( this.getNumberPoints() - 1 ) + ">: " + e.getMessage() );
        iae.initCause( e );
        throw iae;
      }
      if ( array instanceof ArrayDouble)
      {
        return( array.getDouble( array.getIndex()));
      }
      else if ( array instanceof ArrayFloat)
      {
        return( array.getFloat( array.getIndex()));
      }
      else if ( array instanceof ArrayInt )
      {
        return ( array.getInt( array.getIndex() ) );
      }
      else
      {
        throw new IOException( "Time variable not float, double, or integer <" + array.getElementType().toString() + ">.");
      }
    }

    // @todo Make sure units are degrees_north
    public double getLatitude( int point ) throws IOException // required, units degrees_north
    {
      Array array = null;
      try
      {
        array = getLatitude( this.getPointRange( point));
      }
      catch ( InvalidRangeException e )
      {
        IllegalArgumentException iae = new IllegalArgumentException( "Point <" + point + "> not in valid range <0, " + (this.getNumberPoints()-1) + ">: " + e.getMessage());
        iae.initCause( e);
        throw iae;
      }
      if ( array instanceof ArrayDouble)
      {
        return( array.getDouble( array.getIndex()));
      }
      else if ( array instanceof ArrayFloat)
      {
        return( array.getFloat( array.getIndex()));
      }
      else
      {
        throw new IOException( "Latitude variable not float or double <" + array.getElementType().toString() + ">.");
      }
    }

    // @todo Make sure units are degrees_east
    public double getLongitude( int point ) throws IOException // required, units degrees_east
    {
      Array array = null;
      try
      {
        array = getLongitude( this.getPointRange( point));
      }
      catch ( InvalidRangeException e )
      {
        IllegalArgumentException iae = new IllegalArgumentException( "Point <" + point + "> not in valid range <0, " + (this.getNumberPoints()-1) + ">: " + e.getMessage());
        iae.initCause( e);
        throw iae;
      }
      if ( array instanceof ArrayDouble)
      {
        return( array.getDouble( array.getIndex()));
      }
      else if ( array instanceof ArrayFloat)
      {
        return( array.getFloat( array.getIndex()));
      }
      else
      {
        throw new IOException( "Longitude variable not float or double <" + array.getElementType().toString() + ">.");
      }
    }

    // @todo Make sure units are meters
    public double getElevation( int point ) throws IOException // optional; units meters;  missing = NaN.
    {
      Array array = null;
      try
      {
        array = getElevation( this.getPointRange( point));
      }
      catch ( InvalidRangeException e )
      {
        IllegalArgumentException iae = new IllegalArgumentException( "Point <" + point + "> not in valid range <0, " + (this.getNumberPoints()-1) + ">: " + e.getMessage());
        iae.initCause( e);
        throw iae;
      }
      if ( array instanceof ArrayDouble)
      {
        return array.getDouble( array.getIndex());
      }
      else if ( array instanceof ArrayFloat)
      {
        return array.getFloat( array.getIndex());
      }
      else
      {
        throw new IOException( "Elevation variable not float or double <" + array.getElementType().toString() + ">.");
      }
    }

    public StructureData getData( int point ) throws IOException, InvalidRangeException
    {
      return SingleTrajectoryObsDataset.this.recordVar.readStructure( point );
    }

    public Array getData( int point, String parameterName ) throws IOException
    {
      try
      {
        return( getData( this.getPointRange( point), parameterName));
      }
      catch ( InvalidRangeException e )
      {
        IllegalArgumentException iae = new IllegalArgumentException( "Point <" + point + "> not in valid range <0, " + (this.getNumberPoints()-1) + ">: " + e.getMessage());
        iae.initCause( e);
        throw iae;
      }
    }

    public Range getFullRange()
    {
      Range range = null;
      try
      {
        range = new Range(0, this.getNumberPoints()-1);
      }
      catch ( InvalidRangeException e )
      {
        IllegalStateException ise = new IllegalStateException( "Full trajectory range invalid <0, " + (this.getNumberPoints()-1) + ">: " + e.getMessage());
        ise.initCause( e);
        throw( ise);
      }
      return range;
    }

    public Range getPointRange( int point ) throws InvalidRangeException
    {
      if ( point >= this.getNumberPoints()) throw new InvalidRangeException( "Point <" + point + "> not in acceptible range <0, " + (this.getNumberPoints()-1) + ">.");
      return( new Range(point, point));
    }

    public Range getRange( int start, int end, int stride ) throws InvalidRangeException
    {
      if ( end >= this.getNumberPoints()) throw new InvalidRangeException( "End point <" + end + "> not in acceptible range <0, " + (this.getNumberPoints()-1) + ">.");
      return( new Range( start, end, stride));
    }

    public Array getTime( Range range ) throws IOException, InvalidRangeException
    {
      List section = new ArrayList(1);
      section.add( range);
      return( timeVar.read( section));
    }

    // @todo Make sure units are degrees_north
    public Array getLatitude( Range range ) throws IOException, InvalidRangeException
    {
      List section = new ArrayList(1);
      section.add( range);
      return( latVar.read( section));
    }

    // @todo Make sure units are degrees_east
    public Array getLongitude( Range range ) throws IOException, InvalidRangeException
    {
      List section = new ArrayList(1);
      section.add( range);
      return( lonVar.read( section));
    }

    // @todo Make sure units are meters
    public Array getElevation( Range range ) throws IOException, InvalidRangeException
    {
      List section = new ArrayList(1);
      section.add( range);
      Array a = elevVar.read( section);
      if ( elevVarUnitsConversionFactor == 1.0) return( a);
      for ( IndexIterator it = a.getIndexIterator(); it.hasNext(); )
      {
        if ( elevVar.getDataType() == DataType.DOUBLE )
        {
          double val = it.getDoubleNext();
          it.setDoubleCurrent( val * elevVarUnitsConversionFactor );
        }
        else if ( elevVar.getDataType() == DataType.FLOAT )
        {
          float val = it.getFloatNext();
          it.setFloatCurrent( (float) ( val * elevVarUnitsConversionFactor ) );
        }
        else if ( elevVar.getDataType() == DataType.INT )
        {
          int val = it.getIntNext();
          it.setIntCurrent( (int) ( val * elevVarUnitsConversionFactor ) );
        }
        else if ( elevVar.getDataType() == DataType.LONG )
        {
          long val = it.getLongNext();
          it.setLongCurrent( (long) ( val * elevVarUnitsConversionFactor ) );
        }
        else
        {
          throw new IllegalStateException( "Elevation variable type <" + elevVar.getDataType().toString() + "> not double, float, int, or long." );
        }
      }
      return( a);
    }

    public Array getData( Range range, String parameterName ) throws IOException, InvalidRangeException
    {
      Variable variable = netcdfDataset.getRootGroup().findVariable( parameterName );
      int varRank = variable.getRank();
      int [] varShape = variable.getShape();
      List section = new ArrayList( varRank);
      section.add( range);
      for ( int i = 1; i < varRank; i++)
      {
        section.add( new Range( 0, varShape[i]-1 ) );
      }
      Array array = variable.read( section);
      if ( array.getShape()[0] == 1 )
      {
        return ( array.reduce( 0 ) );
      }
      else
      {
        return ( array );
      }
      //return( array.getShape()[0] == 1 ? array.reduce( 0 ) : array);
    }

    public DataIterator getDataIterator( int bufferSize ) throws IOException
    {
      return new PointDatatypeIterator( recordVar, bufferSize );
    }

    private class PointDatatypeIterator extends DatatypeIterator
    {
      protected Object makeDatatypeWithData( int recnum, StructureData sdata )
      {
        return new MyPointObsDatatype( recnum, sdata );
      }

      PointDatatypeIterator( Structure struct, int bufferSize )
      {
        super( struct, bufferSize );
      }
    }

    // PointObsDatatype implementation used by SingleTrajectory.
    private class MyPointObsDatatype implements PointObsDatatype
    {
      private int point;
      private StructureData sdata;
      private double time;
      private ucar.unidata.geoloc.EarthLocation earthLoc;

      private MyPointObsDatatype( int point) throws IOException
      {
        this.point = point;
        this.time = SingleTrajectory.this.getTimeValue( point);
        this.earthLoc = SingleTrajectory.this.getLocation( point);
      }

      private MyPointObsDatatype( int point, StructureData sdata )
      {
        this.point = point;
        this.sdata = sdata;
        this.time = sdata.convertScalarDouble( SingleTrajectory.this.timeVar.getShortName());
        this.earthLoc = new MyEarthLocation( sdata);
      }

      public double getNominalTime()
      {
        return ( this.time );
      }

      public double getObservationTime()
      {
        return ( this.time );
      }

      public Date getNominalTimeAsDate() {
        String dateStr = getNominalTime() + " " + timeVarUnitsString;
        return DateUnit.getStandardDate( dateStr );
      }

      public Date getObservationTimeAsDate() {
        String dateStr = getObservationTime() + " " + timeVarUnitsString;
        return DateUnit.getStandardDate( dateStr );
      }

      public ucar.unidata.geoloc.EarthLocation getLocation()
      {
        return( this.earthLoc);
      }

      public StructureData getData() throws IOException
      {
        if ( sdata != null ) return sdata;
        try {
          return( SingleTrajectory.this.getData( point));
        } catch (InvalidRangeException e) {
          throw new IllegalStateException( e.getMessage());
        }
      }
    }

    // EarthLocation implementation used by SingleTrajectory.
    private class MyEarthLocation extends ucar.unidata.geoloc.EarthLocationImpl
    {
      private double latitude;
      private double longitude;
      private double elevation;

      private MyEarthLocation( int point) throws IOException
      {
        this.latitude = SingleTrajectory.this.getLatitude( point);
        this.longitude = SingleTrajectory.this.getLongitude( point);
        this.elevation = SingleTrajectory.this.getElevation( point);
      }

      private MyEarthLocation( StructureData sdata )
      {
        this.latitude = sdata.convertScalarDouble( SingleTrajectory.this.latVar.getShortName() );
        this.longitude = sdata.convertScalarDouble( SingleTrajectory.this.lonVar.getShortName() );
        this.elevation = sdata.convertScalarDouble( SingleTrajectory.this.elevVar.getShortName() );
        if ( elevVarUnitsConversionFactor != 1.0 ) this.elevation *= elevVarUnitsConversionFactor;
      }

      public double getLatitude()
      {
        return( this.latitude);
      }

      public double getLongitude()
      {
        return( this.longitude);
      }

      public double getAltitude()
      {
        return( this.elevation);
      }
    }
  }

  private class MyTypedDataVariable extends VariableSimpleSubclass
  {
    private int rank;
    private int[] shape;
    private MyTypedDataVariable( VariableDS v )
    {
      super( v );

      // Calculate the rank and shape of the variable, removing trajectory dimesion.
      rank = super.getRank() - 1;
      int[] varShape = super.getShape();
      shape = new int[ varShape.length - 1];
      int trajDimIndex = v.findDimensionIndex( SingleTrajectoryObsDataset.this.timeDim.getShortName());
      for ( int i = 0, j = 0; i < varShape.length; i++)
      {
        if ( i == trajDimIndex) continue;
        shape[ j++] = varShape[ i];
      }
    }

    public int getRank()
    {
      return( rank);
    }

    public int[] getShape()
    {
      return( shape);
    }
  }

}


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

package dap4.core.dmr;

import dap4.core.util.DapSort;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This reifies all of the atomic types
 * and specifically all enumeration declarations
 * as specific objects denoting a type.
 * Structures and Sequence are specifically excluded
 * WARNING: these DapType instances have a fake parent Dataset group
 */

public class DapType extends DapNode implements DapDecl
{
    /**
     * Define instances of DapType for every TypeSort.
     * Watch out: the static lists must be immutable
     * because they might be shared by the server and client
     * during testing
     */

    static public final DapType CHAR;
    static public final DapType INT8;
    static public final DapType UINT8;
    static public final DapType INT16;
    static public final DapType UINT16;
    static public final DapType INT32;
    static public final DapType UINT32;
    static public final DapType INT64;
    static public final DapType UINT64;
    static public final DapType FLOAT32;
    static public final DapType FLOAT64;
    static public final DapType STRING;
    static public final DapType URL;
    static public final DapType OPAQUE;

    // Non-primitives
    static public final DapType STRUCTURE;
    static public final DapType SEQUENCE;

    static final protected DapDataset pseudoroot = new DapDataset("");

    /**
     * Define a map from the Atomic Type Sort to the
     * corresponding DapType primitive.
     */

    static final Map<TypeSort, DapType> typemap;

    static {
        typemap = new HashMap<TypeSort, DapType>();

        CHAR = new DapType(TypeSort.Char);
        INT8 = new DapType(TypeSort.Int8);
        UINT8 = new DapType(TypeSort.UInt8);
        INT16 = new DapType(TypeSort.Int16);
        UINT16 = new DapType(TypeSort.UInt16);
        INT32 = new DapType(TypeSort.Int32);
        UINT32 = new DapType(TypeSort.UInt32);
        INT64 = new DapType(TypeSort.Int64);
        UINT64 = new DapType(TypeSort.UInt64);
        FLOAT32 = new DapType(TypeSort.Float32);
        FLOAT64 = new DapType(TypeSort.Float64);
        STRING = new DapType(TypeSort.String);
        URL = new DapType(TypeSort.URL);
        OPAQUE = new DapType(TypeSort.Opaque);
        STRUCTURE = new DapType(TypeSort.Structure);
        SEQUENCE = new DapType(TypeSort.Sequence);

        typemap.put(TypeSort.Char, DapType.CHAR);
        typemap.put(TypeSort.Int8, DapType.INT8);
        typemap.put(TypeSort.UInt8, DapType.UINT8);
        typemap.put(TypeSort.Int16, DapType.INT16);
        typemap.put(TypeSort.UInt16, DapType.UINT16);
        typemap.put(TypeSort.Int32, DapType.INT32);
        typemap.put(TypeSort.UInt32, DapType.UINT32);
        typemap.put(TypeSort.Int64, DapType.INT64);
        typemap.put(TypeSort.UInt64, DapType.UINT64);
        typemap.put(TypeSort.Float32, DapType.FLOAT32);
        typemap.put(TypeSort.Float64, DapType.FLOAT64);
        typemap.put(TypeSort.String, DapType.STRING);
        typemap.put(TypeSort.URL, DapType.URL);
        typemap.put(TypeSort.Opaque, DapType.OPAQUE);

        typemap.put(TypeSort.Structure, DapType.STRUCTURE);
        typemap.put(TypeSort.Sequence, DapType.SEQUENCE);

	for(Map.Entry<TypeSort,DapType> entry: typemap.entrySet()) {
        entry.getValue().setDataset(pseudoroot);
        entry.getValue().setParent(pseudoroot);
	}

    }

    //////////////////////////////////////////////////
    // Static methods

    static public DapType lookup(TypeSort atomic)
    {
        if(atomic == TypeSort.Enum)
            return null;// we need more info
        return typemap.get(atomic);
    }

    //////////////////////////////////////////////////
    // Instance variables

    protected TypeSort typesort = null;

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

    // Only used in static block
    protected DapType(TypeSort typesort)
    {
        this(typesort.name(),typesort);
    }

    public DapType(String name, TypeSort typesort)
    {
        super(name);
        if(sort == DapSort.ENUMERATION) {
            setTypeSort(TypeSort.Enum); // enum is (currently)
            // the only user-extendible atomic type
        } else
            setTypeSort(typesort);
    }

    //////////////////////////////////////////////////
    // Accessors

    /**
     * Return the lowest possible TypeSort.
     * This is the same as getTypeSort()
     * except for enums, where it returns the
     * basetype of the enum.
     *
     * @return lowest level atomic type
     */
    public TypeSort getAtomicType()
    {
        if(this.typesort == TypeSort.Enum) {
            return ((DapEnumeration) this).getBaseType().getTypeSort();
        } else
            return getTypeSort();
    }

    public TypeSort getTypeSort()
    {
        return this.typesort;
    }

    public String getTypeName()
    {
        return (typesort == TypeSort.Enum ? this.getFQN() : this.getShortName());
    }

    protected void setTypeSort(TypeSort typesort)
    {
        this.typesort = typesort;
    }

    public boolean isUnsigned()
    {
        if(typesort == TypeSort.Enum)
            return ((DapEnumeration) this).getBaseType().isUnsigned();
        else
            return typesort.isUnsigned();
    }

    public boolean isAtomic()
    {
        return getTypeSort().isAtomic();
    }

    // Pass thru to atomictype
    public boolean isIntegerType()
    {
        return typesort.isIntegerType();
    }

    public boolean isFloatType()
    {
        return typesort.isFloatType();
    }

    public boolean isNumericType()
    {
        return typesort.isNumericType();
    }

    public boolean isStringType()
    {
        return typesort.isStringType();
    }

    public boolean isEnumType()
    {
        return typesort.isEnumType();
    }

    public boolean isCharType()
    {
        return typesort.isCharType();
    }

    public boolean isOpaqueType()
    {
        return typesort.isOpaqueType();
    }

    public boolean isFixedSize()
    {
        return typesort.isFixedSize();
    }

    public boolean isStructType()
    {
        return typesort.isStructType();
    }

    public boolean isSeqType()
    {
        return typesort.isSeqType();
    }

    public boolean isCompoundType()
    {
        return typesort.isCompoundType();
    }

    public boolean isLegalAttrType()
    {
        return typesort.isLegalAttrType();
    }

    public boolean isCompound()
    {
        return typesort.isCompound();
    }

    public int getSize()
    {
        return TypeSort.getSize(getAtomicType());
    }

    // Reflective operations

    public Object vector(int n)
    {
        if(n < 0)
            throw new IllegalArgumentException();
        return Array.newInstance(this.getClass(), n);
    }

    public Object vectorget(Object vector, int n)
    {
        if(n < 0)
            throw new IllegalArgumentException();
        assert (vector.getClass().isArray()) : "Attempt to apply index to non-vector";
        return Array.get(vector, n);
    }

}


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

package ucar.unidata.geoloc.projection;

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

/**
 * Mercator projection, spherical earth.
 * Projection plane is a cylinder tangent to the earth at tangentLon.
 * See John Snyder, Map Projections used by the USGS, Bulletin 1532, 2nd edition (1983), p 43-47
 *
 * @author John Caron
 * @see Projection
 * @see ProjectionImpl
 */

public class Mercator extends ProjectionImpl {

  /**
   * Convert "scale at standard parellel" to "standard parellel"
   *
   * @param scale scale at standard parallel
   * @return standard parallel in degrees
   */
  public static double convertScaleToStandardParallel(double scale) {
    // k = 1 / cos (par); snyder p 44
    // par = arccos(1/k);
    double par = Math.acos(1.0 / scale);
    return Math.toDegrees(par);
  }

  /////////////////////////////////////////////////////////////////////
  private final double earthRadius;
  private double lon0; // longitude of the origin in degrees
  private double par; // standard parallel in degrees
  private double falseEasting, falseNorthing;

  private double par_r; // standard parallel in radians
  private double A;

  @Override
  public ProjectionImpl constructCopy() {
    ProjectionImpl result =  new Mercator(getOriginLon(), getParallel(), getFalseEasting(), getFalseNorthing(), getEarthRadius());
    result.setDefaultMapArea(defaultMapArea);
    result.setName(name);
    return result;
  }

  /**
   * Constructor with default parameters
   */
  public Mercator() {
    this(-105, 20.0, 0.0, 0.0, EARTH_RADIUS);
  }

  /**
   * Construct a Mercator Projection.
   *
   * @param lon0 longitude of origin (degrees)
   * @param par  standard parallel (degrees). cylinder cuts earth at this latitude.
   */
  public Mercator(double lon0, double par) {
    this(lon0, par, 0.0, 0.0, EARTH_RADIUS);
  }

  public Mercator(double lon0, double par, double false_easting, double false_northing) {
    this(lon0, par, false_easting, false_northing, EARTH_RADIUS);
  }

  /**
   * Construct a Mercator Projection.
   *
   * @param lon0           longitude of origin (degrees)
   * @param par            standard parallel (degrees). cylinder cuts earth at this latitude.
   * @param false_easting  false_easting in km
   * @param false_northing false_northing in km
   * @param radius         earth radius in km
   */
  public Mercator(double lon0, double par, double false_easting, double false_northing, double radius) {
    super("Mercator", false);

    this.lon0 = lon0;
    this.par = par;
    this.falseEasting = false_easting;
    this.falseNorthing = false_northing;
    this.earthRadius = radius;

    this.par_r = Math.toRadians(par);

    precalculate();

    addParameter(CF.GRID_MAPPING_NAME, CF.MERCATOR);
    addParameter(CF.LONGITUDE_OF_PROJECTION_ORIGIN, lon0);
    addParameter(CF.STANDARD_PARALLEL, par);
    addParameter(CF.EARTH_RADIUS, earthRadius * 1000);
    if ((false_easting != 0.0) || (false_northing != 0.0)) {
      addParameter(CF.FALSE_EASTING, false_easting);
      addParameter(CF.FALSE_NORTHING, false_northing);
      addParameter(CDM.UNITS, "km");
    }

  }

  /**
   * Precalculate some params
   */
  private void precalculate() {
    A = earthRadius * Math.cos(par_r);  // incorporates the scale factor at par
  }

  /**
   * Get the first standard parallel
   *
   * @return the first standard parallel
   */
  public double getParallel() {
    return par;
  }

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

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

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

  public double getEarthRadius() {
    return earthRadius;
  }

  //////////////////////////////////////////////
  // setters for IDV serialization - do not use except for object creating

  /**
   * Set the first standard parallel
   *
   * @param par the first standard parallel
   */
  public void setParallel(double par) {
    this.par = par;
    this.par_r = Math.toRadians(par);
    precalculate();
  }

  /**
   * Set the origin longitude.
   *
   * @param lon the origin longitude.
   */
  public void setOriginLon(double lon) {
    lon0 = lon;
    precalculate();
  }

  /**
   * Set the false_easting, in km.
   * natural_x_coordinate + false_easting = x coordinate
   *
   * @param falseEasting x offset
   */
  public void setFalseEasting(double falseEasting) {
    this.falseEasting = falseEasting;
  }

  /**
   * Set the false northing, in km.
   * natural_y_coordinate + false_northing = y coordinate
   *
   * @param falseNorthing y offset
   */
  public void setFalseNorthing(double falseNorthing) {
    this.falseNorthing = falseNorthing;
  }

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

  /**
   * Get the parameters as a String
   *
   * @return the parameters as a String
   */
  @Override
  public String paramsToString() {
    return toString();
  }

  @Override
  public String toString() {
    return "Mercator{" +
            "earthRadius=" + earthRadius +
            ", lon0=" + lon0 +
            ", par=" + par +
            ", falseEasting=" + falseEasting +
            ", falseNorthing=" + falseNorthing +
            '}';
  }

  /**
   * Does the line between these two points cross the projection "seam".
   *
   * @param pt1 the line goes between these two points
   * @param pt2 the line goes between these two points
   * @return false if there is no seam
   */
  @Override
  public boolean crossSeam(ProjectionPoint pt1, ProjectionPoint pt2) {
    // either point is infinite
    if (ProjectionPointImpl.isInfinite(pt1) || ProjectionPointImpl.isInfinite(pt2)) {
      return true;
    }

    // opposite signed long lines: LOOK ????
    return (pt1.getX() * pt2.getX() < 0);
  }

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

    Mercator mercator = (Mercator) o;

    if (Double.compare(mercator.earthRadius, earthRadius) != 0) return false;
    if (Double.compare(mercator.falseEasting, falseEasting) != 0) return false;
    if (Double.compare(mercator.falseNorthing, falseNorthing) != 0) return false;
    if (Double.compare(mercator.lon0, lon0) != 0) return false;
    if (Double.compare(mercator.par, par) != 0) return false;
    if ((defaultMapArea == null) != (mercator.defaultMapArea == null)) return false; // common case is that these are null
    if (defaultMapArea != null && !mercator.defaultMapArea.equals(defaultMapArea)) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result;
    long temp;
    temp = earthRadius != +0.0d ? Double.doubleToLongBits(earthRadius) : 0L;
    result = (int) (temp ^ (temp >>> 32));
    temp = lon0 != +0.0d ? Double.doubleToLongBits(lon0) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = par != +0.0d ? Double.doubleToLongBits(par) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = falseEasting != +0.0d ? Double.doubleToLongBits(falseEasting) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = falseNorthing != +0.0d ? Double.doubleToLongBits(falseNorthing) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    return result;
  }

  /**
   * Convert a LatLonPoint to projection coordinates
   *
   * @param latLon convert from these lat, lon coordinates
   * @param result the object to write to
   * @return the given result
   */
  @Override
  public ProjectionPoint latLonToProj(LatLonPoint latLon, ProjectionPointImpl result) {
    double toX, toY;
    double fromLat = latLon.getLatitude();
    double fromLon = latLon.getLongitude();
    double fromLat_r = Math.toRadians(fromLat);

    // infinite projection
    if ((Math.abs(90.0 - Math.abs(fromLat))) < TOLERANCE) {
      toX = Double.POSITIVE_INFINITY;
      toY = Double.POSITIVE_INFINITY;
    } else {
      toX = A * Math.toRadians(LatLonPointImpl.range180(fromLon - this.lon0));
      toY = A * SpecialMathFunction.atanh(Math.sin(fromLat_r)); // p 41 Snyder
    }

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

  /**
   * Convert projection coordinates to a LatLonPoint
   * Note: a new object is not created on each call for the return value.
   *
   * @param world  convert from these projection coordinates
   * @param result the object to write to
   * @return LatLonPoint convert to these lat/lon coordinates
   */
  @Override
  public LatLonPoint projToLatLon(ProjectionPoint world, LatLonPointImpl result) {
    double fromX = world.getX() - falseEasting;
    double fromY = world.getY() - falseNorthing;

    double toLon = Math.toDegrees(fromX / A) + lon0;

    double e = Math.exp(-fromY / A);
    double toLat = Math.toDegrees(Math.PI / 2 - 2 * Math.atan(e)); // Snyder p 44

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

}


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

import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.jdom2.output.XMLOutputter;

import thredds.client.catalog.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.units.DateRange;
import ucar.nc2.units.DateType;
import ucar.nc2.units.TimeDuration;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.List;

/**
 * Write client side catalogs out as XML.
 * Used for server catalogs too.
 * Note there is no reference to the catalog's baseURI.
 *
 * @author caron
 * @since 1/11/2015
 */
public class CatalogXmlWriter {
  static private boolean useBytesForDataSize = false;
  static public void useBytesForDataSize(boolean b) {
    useBytesForDataSize = b;
  }
  static private final String version = "1.2";

  ////////////////////////////////////////////////////////////////////////
  private boolean raw = false;

  /**
   * Write the catalog as an XML document to the specified stream.
   *
   * @param catalog write this catalog
   * @param os      write to this OutputStream
   * @param raw     write raw file if true (for server configuration)
   * @throws java.io.IOException
   */
  public void writeXML(Catalog catalog, OutputStream os, boolean raw) throws IOException {
    this.raw = raw;
    writeXML(catalog, os);
    this.raw = false;
  }

  public String writeXML(Catalog catalog) throws IOException {
    ByteArrayOutputStream bos = new ByteArrayOutputStream(100*1000);
    writeXML(catalog, bos);
    this.raw = false;
    return new String(bos.toByteArray(), CDM.utf8Charset);
  }

  /**
   * Write the catalog as an XML document to the specified stream.
   *
   * @param catalog write this catalog
   * @param os      write to this OutputStream
   * @throws IOException
   */
  public void writeXML(Catalog catalog, OutputStream os) throws IOException {
    // Output the document, use standard formatter
    //XMLOutputter fmt = new XMLOutputter();
    //fmt.setNewlines(true);
    //fmt.setIndent("  ");
    //fmt.setTrimAllWhite( true);
    XMLOutputter fmt = new XMLOutputter(org.jdom2.output.Format.getPrettyFormat());  // LOOK maybe compact ??
    fmt.output(writeCatalog(catalog), os);
  }

  public Document writeCatalog(Catalog cat) {
    Element rootElem = new Element("catalog", Catalog.defNS);
    Document doc = new Document(rootElem);

    // attributes
    if (cat.getName() != null)
      rootElem.setAttribute("name", cat.getName());
    rootElem.setAttribute("version", version);
    rootElem.addNamespaceDeclaration(Catalog.xlinkNS);
    if (cat.getExpires() != null)
      rootElem.setAttribute("expires", cat.getExpires().toString());

    // services
    Iterator iter = cat.getServices().iterator();
    while (iter.hasNext()) {
      Service service = (Service) iter.next();
      rootElem.addContent(writeService(service));
    }

    /* dataset roots
    if (raw) {
      iter = cat.getDatasetRoots().iterator();
      while (iter.hasNext()) {
        Property p = (Property) iter.next();
        rootElem.addContent(writeDatasetRoot(p));
      }
    } */

    // properties
    iter = cat.getProperties().iterator();
    while (iter.hasNext()) {
      Property p = (Property) iter.next();
      rootElem.addContent(writeProperty(p));
    }

    // datasets
    iter = cat.getDatasetsLocal().iterator();
    while (iter.hasNext()) {
      Dataset ds = (Dataset) iter.next();
      if (ds instanceof CatalogRef)
        rootElem.addContent(writeCatalogRef((CatalogRef) ds));
      else
        rootElem.addContent(writeDataset(ds));
    }

    return doc;
  }

  private Element writeAccess(Access access) {
    Element accessElem = new Element("access", Catalog.defNS);
    accessElem.setAttribute("urlPath", access.getUrlPath());
    if (access.getService() != null)
      accessElem.setAttribute("serviceName", access.getService().getName());
    if (access.getDataFormatName() != null)
      accessElem.setAttribute("dataFormat", access.getDataFormatName());

    if (access.getDataSize() > 0)
      accessElem.addContent(writeDataSize(access.getDataSize()));

    return accessElem;
  }

  private Element writeCatalogRef(CatalogRef catRef) {
    Element catrefElem = new Element("catalogRef", Catalog.defNS);
    catrefElem.setAttribute("href", catRef.getXlinkHref(), Catalog.xlinkNS);
    String name = catRef.getName() == null ? "" : catRef.getName();
    catrefElem.setAttribute("title", name, Catalog.xlinkNS);
    if (catRef.getId() != null)
      catrefElem.setAttribute("ID", catRef.getId());
    if (catRef.getRestrictAccess() != null)
      catrefElem.setAttribute("restrictAccess", catRef.getRestrictAccess());
    catrefElem.setAttribute("name", "");

    writeDatasetInfo(catRef, catrefElem, false, raw);

    return catrefElem;
  }

  protected Element writeContributor(ThreddsMetadata.Contributor c) {
    Element elem = new Element("contributor", Catalog.defNS);
    if (c.getRole() != null)
      elem.setAttribute("role", c.getRole());
    elem.setText(c.getName());
    return elem;
  }

  private Element writeControlledVocabulary(ThreddsMetadata.Vocab v, String name) {
    Element elem = new Element(name, Catalog.defNS);
    if (v.getVocabulary() != null)
      elem.setAttribute("vocabulary", v.getVocabulary());
    elem.addContent(v.getText());
    return elem;
  }

  private Element writeDataset(Dataset ds) {
    Element dsElem = new Element("dataset", Catalog.defNS);

    /* if (ds instanceof DatasetProxy) {
      dsElem.setAttribute("name", ((DatasetProxy) ds).getAliasName());
      dsElem.setAttribute("alias", ds.getID());
      return dsElem;
    } */

    writeDatasetInfo(ds, dsElem, true, raw);

    return dsElem;
  }

  private void writeDatasetInfo(Dataset ds, Element dsElem, boolean doNestedDatasets, boolean showNcML) {
    String name = ds.getName();
    if (name == null) name = ""; // eg catrefs
    dsElem.setAttribute("name", name);

    // other attributes, note the others get made into an element
    if (ds.getCollectionType() != null)
      dsElem.setAttribute("collectionType", ds.getCollectionType());
    if (ds.isHarvest())
      dsElem.setAttribute("harvest", "true");
    if (ds.getID() != null)
      dsElem.setAttribute("ID", ds.getID());
    if (ds.getUrlPath() != null)
      dsElem.setAttribute("urlPath", ds.getUrlPath());
    if (ds.getRestrictAccess() != null)
      dsElem.setAttribute("restrictAccess", ds.getRestrictAccess());

    /* services (local only)
    for (Service service : ds.getServices()) {
      dsElem.addContent(writeService(service));
    } */

    // thredds metadata
    writeThreddsMetadata(dsElem, ds);
    writeInheritedMetadata(dsElem, ds);

    // access  (local only)
    List<Access> access = (List<Access>) ds.getLocalFieldAsList(Dataset.Access);
    for (Access a : access) {
      dsElem.addContent(writeAccess( a));
    }

    /* if (showNcML && ds.getNcmlElement() != null) {
      org.jdom2.Element ncml = ds.getNcmlElement().clone();
      ncml.detach();
      dsElem.addContent(ncml);
    } */

    if (!doNestedDatasets) return;

    // nested datasets
    for (Dataset nested : ds.getDatasetsLocal()) {
     // if (nested instanceof DatasetScan)
     //   dsElem.addContent(writeDatasetScan((DatasetScan) nested));
      if (nested instanceof CatalogRef)
        dsElem.addContent(writeCatalogRef((CatalogRef) nested));
      else
        dsElem.addContent(writeDataset( nested));
    }
  }

  protected Element writeDate(String name, DateType date) {
    Element dateElem = new Element(name, Catalog.defNS);
    dateElem.addContent(date.getText());
    if (date.getType() != null)
      dateElem.setAttribute("type", date.getType());
    if (date.getFormat() != null)
      dateElem.setAttribute("format", date.getFormat());

    return dateElem;
  }

  private Element writeDocumentation(Documentation doc, String name) {
    Element docElem = new Element(name, Catalog.defNS);
    if (doc.getType() != null)
      docElem.setAttribute("type", doc.getType());

    if (doc.hasXlink()) {
      docElem.setAttribute("href", doc.getXlinkHref(), Catalog.xlinkNS);
      if (!doc.getXlinkTitle().equals(doc.getURI().toString()))
        docElem.setAttribute("title", doc.getXlinkTitle(), Catalog.xlinkNS);
    }

    String inline = doc.getInlineContent();
    if (inline != null)
      docElem.addContent(inline);
    return docElem;
  }

  public Element writeGeospatialCoverage(ThreddsMetadata.GeospatialCoverage gc) {
    Element elem = new Element("geospatialCoverage", Catalog.defNS);
    if (gc.getZPositive() != null)
      elem.setAttribute("zpositive", gc.getZPositive());

    if (gc.getNorthSouthRange() != null)
      writeGeospatialRange(elem, new Element("northsouth", Catalog.defNS), gc.getNorthSouthRange());
    if (gc.getEastWestRange() != null)
      writeGeospatialRange(elem, new Element("eastwest", Catalog.defNS), gc.getEastWestRange());
    if (gc.getUpDownRange() != null)
      writeGeospatialRange(elem, new Element("updown", Catalog.defNS), gc.getUpDownRange());

    /* serialize isGlobal
    java.util.List<ThreddsMetadata.Vocab> names = gc.getNames();
    ThreddsMetadata.Vocab global = new ThreddsMetadata.Vocab("global", null);
    if (gc.isGlobal() && !names.contains(global)) {
      names.add(global);
    } else if (!gc.isGlobal() && names.contains(global)) {
      names.remove(global);
    } */

    for (ThreddsMetadata.Vocab name : gc.getNames()) {
      elem.addContent(writeControlledVocabulary(name, "name"));
    }

    return elem;
  }

  private void writeGeospatialRange(Element parent, Element elem, ThreddsMetadata.GeospatialRange r) {
    if (r == null) return;

    elem.addContent(new Element("start", Catalog.defNS).setText(Double.toString(r.getStart())));
    elem.addContent(new Element("size", Catalog.defNS).setText(Double.toString(r.getSize())));
    if (r.hasResolution())
      elem.addContent(new Element("resolution", Catalog.defNS).setText(Double.toString(r.getResolution())));
    if (r.getUnits() != null)
      elem.addContent(new Element("units", Catalog.defNS).setText(r.getUnits()));

    parent.addContent(elem);
  }

  private Element writeMetadata(ThreddsMetadata.MetadataOther mdata) {
    Element mdataElem = new Element("metadata", Catalog.defNS);
    if (mdata.getType() != null)
      mdataElem.setAttribute("metadataType", mdata.getType());
    if (mdata.isInherited())
      mdataElem.setAttribute("inherited", "true");

    String ns = mdata.getNamespaceURI();
    if ((ns != null) && !ns.equals(Catalog.CATALOG_NAMESPACE_10)) {
      Namespace mdataNS = Namespace.getNamespace(mdata.getPrefix(), ns);
      mdataElem.addNamespaceDeclaration(mdataNS);
    }

    if (mdata.getXlinkHref() != null) {
      mdataElem.setAttribute("href", mdata.getXlinkHref(), Catalog.xlinkNS);
      if (mdata.getTitle() != null)
        mdataElem.setAttribute("title", mdata.getTitle(), Catalog.xlinkNS);

    } else if (mdata.getContentObject() != null && mdata.getContentObject() instanceof Element) {
      Element content = (Element) mdata.getContentObject();
      mdataElem.setContent(content);
    }

    return mdataElem;
  }

  private Element writeProperty(Property prop) {
    Element propElem = new Element("property", Catalog.defNS);
    propElem.setAttribute("name", prop.getName());
    propElem.setAttribute("value", prop.getValue());
    return propElem;
  }

  protected Element writeSource(String elementName, ThreddsMetadata.Source p) {
    Element elem = new Element(elementName, Catalog.defNS);

    elem.addContent(writeControlledVocabulary(p.getNameVocab(), "name"));

    Element contact = new Element("contact", Catalog.defNS);
    if (p.getUrl() != null)
      contact.setAttribute("url", p.getUrl());
    if (p.getEmail() != null)
      contact.setAttribute("email", p.getEmail());
    elem.addContent(contact);

    return elem;
  }

  private Element writeService(Service service) {
    Element serviceElem = new Element("service", Catalog.defNS);
    serviceElem.setAttribute("name", service.getName());
    String svctype = service.getServiceTypeName();
    serviceElem.setAttribute("serviceType", svctype);
    String base = service.getBase();
    if("compound".equalsIgnoreCase(svctype) && base == null)
      base = ""; // Add some error tolerance
    serviceElem.setAttribute("base", base);
    if ((service.getSuffix() != null) && (service.getSuffix().length() > 0))
      serviceElem.setAttribute("suffix", service.getSuffix());

    // properties
    for (Property p : service.getProperties()) {
      serviceElem.addContent(writeProperty(p));
    }

    // services
    for (Service nested : service.getNestedServices()) {
      serviceElem.addContent(writeService(nested));
    }

    /* dataset roots
    if (raw) {
      for (Property p : service.getDatasetRoots()) {
        serviceElem.addContent(writeDatasetRoot(p));
      }
    } */

    return serviceElem;
  }

  private Element writeDataSize(double size) {
    Element sizeElem = new Element("dataSize", Catalog.defNS);

    // want exactly the number of bytes
    if (useBytesForDataSize) {
      sizeElem.setAttribute("units", "bytes");
      long bytes = (long) size;
      sizeElem.setText(Long.toString(bytes));
      return sizeElem;
    }

    // otherwise choose appropriate unit
    String unit;
    if (size > 1.0e15) {
      unit = "Pbytes";
      size *= 1.0e-15;
    } else if (size > 1.0e12) {
      unit = "Tbytes";
      size *= 1.0e-12;
    } else if (size > 1.0e9) {
      unit = "Gbytes";
      size *= 1.0e-9;
    } else if (size > 1.0e6) {
      unit = "Mbytes";
      size *= 1.0e-6;
    } else if (size > 1.0e3) {
      unit = "Kbytes";
      size *= 1.0e-3;
    } else {
      unit = "bytes";
    }

    sizeElem.setAttribute("units", unit);
    sizeElem.setText(ucar.unidata.util.Format.d(size, 4));

    return sizeElem;
  }

  /* protected void writeCat6InheritedMetadata( Element elem, ThreddsMetadata tmi) {
    if ((tmi.getDataType() == null) && (tmi.getServiceName() == null) &&
        (tmi.getAuthority() == null) && ( tmi.getProperties().size() == 0))
      return;

    Element mdataElem = new Element("metadata", Catalog.defNS);
    mdataElem.setAttribute("inherited", "true");
    writeThreddsMetadata( mdataElem, tmi);
    elem.addContent( mdataElem);
  }  */

  protected void writeInheritedMetadata(Element elem, Dataset ds) {
    Element mdataElem = new Element("metadata", Catalog.defNS);
    mdataElem.setAttribute("inherited", "true");
    ThreddsMetadata tmi = (ThreddsMetadata) ds.getLocalField(Dataset.ThreddsMetadataInheritable);
    if (tmi == null) return;
    writeThreddsMetadata(mdataElem, tmi);
    if (mdataElem.getChildren().size() > 0)
      elem.addContent(mdataElem);
  }

  protected void writeThreddsMetadata(Element elem, ThreddsMetadataContainer ds) {

    String serviceName = (String) ds.getLocalField(Dataset.ServiceName);
    if (serviceName != null) {
      Element serviceNameElem = new Element("serviceName", Catalog.defNS);
      serviceNameElem.setText(serviceName);
      elem.addContent(serviceNameElem);
    }

    String authority = (String) ds.getLocalField(Dataset.Authority);
    if (authority != null) {
      Element authElem = new Element("authority", Catalog.defNS);
      authElem.setText(authority);
      elem.addContent(authElem);
    }

    String featureTypeName = (String) ds.getLocalField(Dataset.FeatureType);
    if (featureTypeName != null) {
      Element dataTypeElem = new Element("dataType", Catalog.defNS);
      dataTypeElem.setText(featureTypeName);
      elem.addContent(dataTypeElem);
    }

    String dataFormatName = (String) ds.getLocalField(Dataset.DataFormatType);
    if (dataFormatName != null) {
      Element dataFormatElem = new Element("dataFormat", Catalog.defNS);
      dataFormatElem.setText(dataFormatName);
      elem.addContent(dataFormatElem);
    }

    Long dataSize = (Long) ds.getLocalField(Dataset.DataSize);
    if (dataSize != null && dataSize > 0)
      elem.addContent(writeDataSize(dataSize));

    List<Documentation> docList =  (List<Documentation>) ds.getLocalFieldAsList(Dataset.Documentation);
    for (Documentation doc : docList) {
      elem.addContent(writeDocumentation(doc, "documentation"));
    }

    List<ThreddsMetadata.Contributor> contribList = (List<ThreddsMetadata.Contributor>) ds.getLocalFieldAsList(Dataset.Contributors);
    for (ThreddsMetadata.Contributor c : contribList) {
      elem.addContent(writeContributor(c));
    }

    List<ThreddsMetadata.Source> creatorList = (List<ThreddsMetadata.Source>) ds.getLocalFieldAsList(Dataset.Creators);
    for (ThreddsMetadata.Source p : creatorList) {
      elem.addContent(writeSource("creator", p));
    }

    List<ThreddsMetadata.Vocab> kewordList = (List<ThreddsMetadata.Vocab>) ds.getLocalFieldAsList(Dataset.Keywords);
    for (ThreddsMetadata.Vocab v : kewordList) {
      elem.addContent(writeControlledVocabulary(v, "keyword"));
    }

    List<ThreddsMetadata.MetadataOther> mdList = (List<ThreddsMetadata.MetadataOther>) ds.getLocalFieldAsList(Dataset.MetadataOther);
    for (ThreddsMetadata.MetadataOther m : mdList) {
      elem.addContent(writeMetadata(m));
    }

    List<ThreddsMetadata.Vocab> projList = (List<ThreddsMetadata.Vocab>) ds.getLocalFieldAsList(Dataset.Projects);
    for (ThreddsMetadata.Vocab v : projList) {
      elem.addContent(writeControlledVocabulary(v, "project"));
    }

    List<Property> propertyList = (List<Property>) ds.getLocalFieldAsList(Dataset.Properties);
    for (Property p : propertyList) {
      elem.addContent(writeProperty(p));
    }

    List<ThreddsMetadata.Source> pubList = (List<ThreddsMetadata.Source>) ds.getLocalFieldAsList(Dataset.Publishers);
    for (ThreddsMetadata.Source p : pubList) {
      elem.addContent(writeSource("publisher", p));
    }

    List<DateType> dateList = (List<DateType>) ds.getLocalFieldAsList(Dataset.Dates);
    for (DateType d : dateList) {
      elem.addContent(writeDate("date", d));
    }

    ThreddsMetadata.GeospatialCoverage gc = (ThreddsMetadata.GeospatialCoverage) ds.getLocalField(Dataset.GeospatialCoverage);
    if (gc != null)
      elem.addContent(writeGeospatialCoverage(gc));

    DateRange tc = (DateRange) ds.getLocalField(Dataset.TimeCoverage);
    if (tc != null)
      elem.addContent(writeTimeCoverage(tc));

    List<ThreddsMetadata.VariableGroup> varList = (List<ThreddsMetadata.VariableGroup>) ds.getLocalFieldAsList(Dataset.VariableGroups);
    for (ThreddsMetadata.VariableGroup v : varList) {
      elem.addContent(writeVariables(v));
    }

    // LOOK what about VariableMapLink ??
    ThreddsMetadata.UriResolved varMapLink = (ThreddsMetadata.UriResolved) ds.getLocalField(Dataset.VariableMapLinkURI);
    if (varMapLink != null) {
      Element velem = new Element("variableMap", Catalog.defNS);
      velem.setAttribute("title", "variables", Catalog.xlinkNS);
      velem.setAttribute("href", varMapLink.href, Catalog.xlinkNS);
      elem.addContent(velem);
    }
  }

  protected Element writeTimeCoverage(DateRange t) {
    Element elem = new Element("timeCoverage", Catalog.defNS);

    DateType start = t.getStart();
    DateType end = t.getEnd();
    TimeDuration duration = t.getDuration();
    TimeDuration resolution = t.getResolution();

    if (t.useStart() && (start != null) && !start.isBlank()) {
      Element startElem = new Element("start", Catalog.defNS);
      startElem.setText(start.toString());
      elem.addContent(startElem);
    }

    if (t.useEnd() && (end != null) && !end.isBlank()) {
      Element telem = new Element("end", Catalog.defNS);
      telem.setText(end.toString());
      elem.addContent(telem);
    }

    if (t.useDuration() && (duration != null) && !duration.isBlank()) {
      Element telem = new Element("duration", Catalog.defNS);
      telem.setText(duration.toString());
      elem.addContent(telem);
    }

    if (t.useResolution() && (resolution != null) && !resolution.isBlank()) {
      Element telem = new Element("resolution", Catalog.defNS);
      telem.setText(t.getResolution().toString());
      elem.addContent(telem);
    }

    return elem;
  }

  protected Element writeVariable(ThreddsMetadata.Variable v) {
    Element elem = new Element("variable", Catalog.defNS);
    if (v.getName() != null)
      elem.setAttribute("name", v.getName());
    if (v.getDescription() != null) {
      String desc = v.getDescription().trim();
      if (desc.length() > 0)
        elem.setText(v.getDescription());
    }
    if (v.getVocabularyName() != null)
      elem.setAttribute("vocabulary_name", v.getVocabularyName());
    if (v.getUnits() != null)
      elem.setAttribute("units", v.getUnits());
    String id = v.getVocabularyId();
    if (id != null)
      elem.setAttribute("vocabulary_id", id);

    return elem;
  }

  protected Element writeVariables(ThreddsMetadata.VariableGroup vs) {
    Element elem = new Element("variables", Catalog.defNS);
    if (vs.getVocabulary() != null)
      elem.setAttribute("vocabulary", vs.getVocabulary());
    if (vs.getVocabUri() != null)
      elem.setAttribute("href", vs.getVocabUri().resolved.toString(), Catalog.xlinkNS);

    if (vs.getVariableMap() != null) { // variable map
      Element mapElem = new Element("variableMap", Catalog.defNS);
      mapElem.setAttribute("href", vs.getVariableMap().resolved.toString(), Catalog.xlinkNS);
      elem.addContent(mapElem);

    } else { // inline variables
      List<ThreddsMetadata.Variable> varList = vs.getVariableList();
      for (ThreddsMetadata.Variable v : varList) {
        elem.addContent(writeVariable(v));
      }
    }

    return elem;
  }

}


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

import com.google.common.math.DoubleMath;
import ucar.nc2.util.Misc;

import java.io.*;

/**
 * Bounding box for ProjectionPoint's.
 * Note that getX() getY() really means getMinX(), getMinY(), rather than
 * "upper left point" of the rectangle.
 *
 * @author John Caron
 */
public class ProjectionRect implements java.io.Serializable {
  private double x, y, width, height;

  /**
   * default constructor, initialized to center (0,0) and width (10000, 10000)
   */
  public ProjectionRect() {
    this(-5000, -5000, 5000, 5000);
  }

  /**
   * Construct a ProjectionRect from any two opposite corner points.
   *
   * @param corner1  a corner.
   * @param corner2  the opposite corner.
   */
  public ProjectionRect(ProjectionPoint corner1, ProjectionPoint corner2) {
    this(corner1.getX(), corner1.getY(), corner2.getX(), corner2.getY());
  }

  /**
    * Construct a ProjectionRect from any two opposite corner points.
    *
    * @param minimum  lower left corner, ie the minimum x and y
    * @param width    x width.
    * @param height   y height
    */
   public ProjectionRect(ProjectionPoint minimum, double width, double height) {
     setRect(minimum.getX(), minimum.getY(), width, height);
   }

   /**
   * Copy Constructor
   *
   * @param r rectangle to copy
   */
  public ProjectionRect(ProjectionRect r) {
    this(r.getMinX(), r.getMinY(), r.getMaxX(), r.getMaxY());
  }

  /**
   * construct a MapArea from any two opposite corner points
   *
   * @param x1 x coord of any corner of the bounding box
   * @param y1 y coord of the same corner as x1
   * @param x2 x coord of opposite corner from x1,y1
   * @param y2 y coord of same corner as x2
   */
  public ProjectionRect(double x1, double y1, double x2, double y2) {
    double wx0 = 0.5 * (x1 + x2);
    double wy0 = 0.5 * (y1 + y2);
    double width = Math.abs(x1 - x2);
    double height = Math.abs(y1 - y2);
    setRect(wx0 - width / 2, wy0 - height / 2, width, height);
  }

  public double getX() {
    return x;
  }

  public double getY() {
    return y;
  }

  public double getWidth() {
    return width;
  }

  public double getHeight() {
    return height;
  }

  ////////////////////////////////////////////////////
  // taken from java.awt.geom.Rectangle2D, removed because awt missing on android

  /**
   * Returns the smallest X coordinate of the framing
   * rectangle of the Shape in double
   * precision.
   *
   * @return the smallest X coordinate of the framing
   *         rectangle of the Shape.
   * @since 1.2
   */
  public double getMinX() {
    return getX();
  }

  /**
   * Returns the smallest Y coordinate of the framing
   * rectangle of the Shape in double
   * precision.
   *
   * @return the smallest Y coordinate of the framing
   *         rectangle of the Shape.
   * @since 1.2
   */
  public double getMinY() {
    return getY();
  }

  /**
   * Returns the largest X coordinate of the framing
   * rectangle of the Shape in double
   * precision.
   *
   * @return the largest X coordinate of the framing
   *         rectangle of the Shape.
   * @since 1.2
   */
  public double getMaxX() {
    return getX() + getWidth();
  }

  /**
   * Returns the largest Y coordinate of the framing
   * rectangle of the Shape in double
   * precision.
   *
   * @return the largest Y coordinate of the framing
   *         rectangle of the Shape.
   * @since 1.2
   */
  public double getMaxY() {
    return getY() + getHeight();
  }

  /**
   * Returns the X coordinate of the center of the framing
   * rectangle of the Shape in double
   * precision.
   * @return the X coordinate of the center of the framing rectangle
   *          of the Shape.
   * @since 1.2
   */
  public double getCenterX() {
      return getX() + getWidth() / 2.0;
  }

  /**
   * Returns the Y coordinate of the center of the framing
   * rectangle of the Shape in double
   * precision.
   * @return the Y coordinate of the center of the framing rectangle
   *          of the Shape.
   * @since 1.2
   */
  public double getCenterY() {
      return getY() + getHeight() / 2.0;
  }

  /**
   * Adds a Rectangle2D object to this
   * Rectangle2D.  The resulting Rectangle2D
   * is the union of the two Rectangle2D objects.
   * @param r the Rectangle2D to add to this
   * Rectangle2D.
   * @since 1.2
   */
  public void add(ProjectionRect r) {
      double x1 = Math.min(getMinX(), r.getMinX());
      double x2 = Math.max(getMaxX(), r.getMaxX());
      double y1 = Math.min(getMinY(), r.getMinY());
      double y2 = Math.max(getMaxY(), r.getMaxY());
      setRect(x1, y1, x2 - x1, y2 - y1);
  }

  /**
   * Adds a point, specified by the double precision arguments
   * newx and newy, to this
   * Rectangle2D.  The resulting Rectangle2D
   * is the smallest Rectangle2D that
   * contains both the original Rectangle2D and the
   * specified point.
   *

   * After adding a point, a call to contains with the
   * added point as an argument does not necessarily return
   * true. The contains method does not
   * return true for points on the right or bottom
   * edges of a rectangle. Therefore, if the added point falls on
   * the left or bottom edge of the enlarged rectangle,
   * contains returns false for that point.
   * @param newx the X coordinate of the new point
   * @param newy the Y coordinate of the new point
   * @since 1.2
   */
  public void add(double newx, double newy) {
      double x1 = Math.min(getMinX(), newx);
      double x2 = Math.max(getMaxX(), newx);
      double y1 = Math.min(getMinY(), newy);
      double y2 = Math.max(getMaxY(), newy);
      setRect(x1, y1, x2 - x1, y2 - y1);
  }

  /**
   * Adds the Point2D object pt to this
   * Rectangle2D.
   * The resulting Rectangle2D is the smallest
   * Rectangle2D that contains both the original
   * Rectangle2D and the specified Point2D.
   *

   * After adding a point, a call to contains with the
   * added point as an argument does not necessarily return
   * true. The contains
   * method does not return true for points on the right
   * or bottom edges of a rectangle. Therefore, if the added point falls
   * on the left or bottom edge of the enlarged rectangle,
   * contains returns false for that point.
   * @param     pt the new Point2D to add to this
   * Rectangle2D.
   * @since 1.2
   */
  public void add(ProjectionPoint pt) {
      add(pt.getX(), pt.getY());
  }

  public boolean isEmpty() {
      return (width <= 0.0) || (height <= 0.0);
  }

  public boolean intersects(ProjectionRect r) {
      return intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight());
  }

  public boolean intersects(double x, double y, double w, double h) {
      if (isEmpty() || w <= 0 || h <= 0) {
          return false;
      }
      double x0 = getX();
      double y0 = getY();
      return (x + w > x0 &&
              y + h > y0 &&
              x < x0 + getWidth() &&
              y < y0 + getHeight());
  }

  /**
   * Intersects the pair of specified source Rectangle2D
   * objects and puts the result into the specified destination
   * Rectangle2D object.  One of the source rectangles
   * can also be the destination to avoid creating a third Rectangle2D
   * object, but in this case the original points of this source
   * rectangle will be overwritten by this method.
   * @param src1 the first of a pair of Rectangle2D
   * objects to be intersected with each other
   * @param src2 the second of a pair of Rectangle2D
   * objects to be intersected with each other
   * @param dest the Rectangle2D that holds the
   * results of the intersection of src1 and
   * src2
   * @since 1.2
   */
  public static void intersect(ProjectionRect src1, ProjectionRect src2, ProjectionRect dest) {
    double x1 = Math.max(src1.getMinX(), src2.getMinX());
      double y1 = Math.max(src1.getMinY(), src2.getMinY());
      double x2 = Math.min(src1.getMaxX(), src2.getMaxX());
      double y2 = Math.min(src1.getMaxY(), src2.getMaxY());
      dest.setRect(x1, y1, x2 - x1, y2 - y1);
  }

  /**
   * Returns {@code true} if this bounding box contains {@code point}.
   *
   * @param point a point in projection coordinates.
   * @return {@code true} if this bounding box contains {@code point}.
   */
  public boolean contains(ProjectionPoint point) {
    return DoubleMath.fuzzyCompare(point.getX(), getMinX(), 1e-6) >= 0 &&
           DoubleMath.fuzzyCompare(point.getX(), getMaxX(), 1e-6) <= 0 &&
           DoubleMath.fuzzyCompare(point.getY(), getMinY(), 1e-6) >= 0 &&
           DoubleMath.fuzzyCompare(point.getY(), getMaxY(), 1e-6) <= 0;
  }

  /**
   *
   * Returns {@code true} if this bounding box contains {@code rect}.
   *
   * @param rect a bounding box in projection coordinates
   * @return {@code true} if this bounding box contains {@code rect}.
   */
  public boolean contains(ProjectionRect rect) {
    boolean contained = false;
    // The old ProjectionRect class was based off of java.awt.Rectangle.
    // If the rectangles were the same, .contains(rect) returned true.
    // This check makes sure the old behavior is preserved.
    if (this.equals(rect)) {
      contained = true;
    } else {
      // Just check to see if corners of rect contained within this
      contained = (this.contains(rect.getMinPoint()) && this.contains(rect.getMaxPoint()));
    }
    return contained;
  }

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

  /**
   * Get the Lower Right Point
   *
   * @return the Lower Right Point
   */
  public ProjectionPoint getLowerRightPoint() {
    return new ProjectionPointImpl(getMaxPoint().getX(), getMinPoint().getY());
  }

  /**
   * Get the Upper Left Point (same as getMaxPoint)
   *
   * @return the Upper Left Point
   */
  public ProjectionPoint getUpperRightPoint() {
    return getMaxPoint();
  }

  /**
   * Get the Lower Right Point (same as getMinPoint)
   *
   * @return the Lower Right Point
   */
  public ProjectionPoint getLowerLeftPoint() {
    return getMinPoint();
  }

  /**
   * Get the Upper Left Point
   *
   * @return the Upper Left Point
   */
  public ProjectionPoint getUpperLeftPoint() {
    return new ProjectionPointImpl(getMinPoint().getX(),
            getMaxPoint().getY());
  }

  /**
   * Get the minimum corner of the bounding box.
   *
   * @return minimum corner of the bounding box
   */
  public ProjectionPoint getMinPoint() {
    return new ProjectionPointImpl(getX(), getY());
  }

  /**
   * Get the maximum corner of the bounding box.
   *
   * @return maximum corner of the bounding box
   */
  public ProjectionPoint getMaxPoint() {
    return new ProjectionPointImpl(getX() + getWidth(),
            getY() + getHeight());
  }

  /**
   * Get a String representation of this object.
   *
   * @return a String representation of this object.
   */
  public String toString() {
    return String.format("min: %.3f %.3f size: %.3f %.3f", getX(), getY(), getWidth(), getHeight());
  }

  public String toString2(int ndec) {
    String f = " %."+ndec+"f";
    return String.format("min:"+f+f+" max:"+f+f, getX(), getY(), getMaxX(), getMaxY());
  }

  /**
   * set minimum X
   *
   * @param x minimum x
   */
  public void setX(double x) {
    setRect(x, getY(), getWidth(), getHeight());
  }

  /**
   * set minimum Y
   *
   * @param y minimum y
   */
  public void setY(double y) {
    setRect(getX(), y, getWidth(), getHeight());
  }

  /**
   * set X width
   *
   * @param w x width
   */
  public void setWidth(double w) {
    setRect(getX(), getY(), w, getHeight());
  }

  /**
   * set Y height
   *
   * @param h Y height
   */
  public void setHeight(double h) {
    setRect(getX(), getY(), getWidth(), h);
  }

  public void setRect(ProjectionRect r) {
    setRect(r.getX(), r.getY(), r.getWidth(), r.getHeight());
  }

  public void setRect(double x, double y, double w, double h) {
    this.x = x;
    this.y = y;
    this.width = w;
    this.height = h;
  }

  // serialization

  /**
   * Read the object from the input stream of the serialized object
   *
   * @param s stream to read
   * @throws ClassNotFoundException couldn't file the class
   * @throws IOException            Problem reading from stream
   */
  private void readObject(ObjectInputStream s)
          throws IOException, ClassNotFoundException {
    double x = s.readDouble();
    double y = s.readDouble();
    double w = s.readDouble();
    double h = s.readDouble();
    setRect(x, y, w, h);
  }

  /**
   * Wrtie the object to the output stream
   *
   * @param s stream to write
   * @throws IOException Problem writing to stream
   */
  private void writeObject(ObjectOutputStream s) throws IOException {
    s.writeDouble(getX());
    s.writeDouble(getY());
    s.writeDouble(getWidth());
    s.writeDouble(getHeight());
  }

  // Exact comparison is needed in order to be consistent with hashCode().
  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    ProjectionRect that = (ProjectionRect) o;

    if (Double.compare(that.height, height) != 0) return false;
    if (Double.compare(that.width, width) != 0) return false;
    if (Double.compare(that.x, x) != 0) return false;
    if (Double.compare(that.y, y) != 0) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result;
    long temp;
    temp = Double.doubleToLongBits(x);
    result = (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(y);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(width);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(height);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    return result;
  }

  /**
   * Returns the result of {@link #nearlyEquals(ProjectionRect, double)}, with {@link Misc#defaultMaxRelativeDiffFloat}.
   */
  public boolean nearlyEquals(ProjectionRect other) {
    return nearlyEquals(other, Misc.defaultMaxRelativeDiffFloat);
  }

  /**
   * Returns {@code true} if this rectangle is nearly equal to {@code other}. The "near equality" of corners is
   * determined using {@link ProjectionPoint#nearlyEquals(ProjectionPoint, double)}, with the specified maxRelDiff.
   *
   * @param other    the other rectangle to check.
   * @param maxRelDiff  the maximum {@link Misc#relativeDifference relative difference} that two corners may have.
   * @return {@code true} if this rectangle is nearly equal to {@code other}.
   */
  public boolean nearlyEquals(ProjectionRect other, double maxRelDiff) {
    return this.getLowerLeftPoint() .nearlyEquals(other.getLowerLeftPoint(),  maxRelDiff) &&
           this.getUpperRightPoint().nearlyEquals(other.getUpperRightPoint(), maxRelDiff);
  }
}