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

package ucar.nc2.ui.grib;

import thredds.featurecollection.FeatureCollectionConfig;
import thredds.inventory.CollectionAbstract;
import thredds.inventory.MCollection;
import thredds.inventory.MFile;
import ucar.ma2.DataType;
import ucar.nc2.grib.*;
import ucar.nc2.grib.grib2.*;
import ucar.nc2.grib.grib2.table.Grib2Tables;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.ui.widget.*;
import ucar.nc2.ui.widget.PopupMenu;
import ucar.nc2.util.Misc;
import ucar.unidata.geoloc.LatLonPoint;
import ucar.util.prefs.PreferencesExt;
import ucar.util.prefs.ui.BeanTable;

import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.io.*;
import java.nio.ByteOrder;
import java.util.*;
import java.util.List;

/**
 * Grib2 data reading
 *
 * @author caron
 * @since Sep 7, 2012
 */
public class Grib2DataPanel extends JPanel {
  static private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Grib2DataPanel.class);

  private PreferencesExt prefs;

  private BeanTable param2BeanTable, record2BeanTable;
  private JSplitPane split, split2;

  private TextHistoryPane infoPopup, infoPopup2, drsInfo;
  private IndependentWindow infoWindow, infoWindow2;
  private FileManager fileChooser;

  public Grib2DataPanel(PreferencesExt prefs) {
    this.prefs = prefs;

    PopupMenu varPopup;

    ////////////////
    param2BeanTable = new BeanTable(Grib2ParameterBean.class, (PreferencesExt) prefs.node("Param2Bean"), false,
            "UniquePDSVariables", "from Grib2Input.getRecords()", null);
    param2BeanTable.addListSelectionListener(new ListSelectionListener() {
      public void valueChanged(ListSelectionEvent e) {
        Grib2ParameterBean pb = (Grib2ParameterBean) param2BeanTable.getSelectedBean();
        if (pb != null)
          record2BeanTable.setBeans(pb.getRecordBeans());
      }
    });

    varPopup = new PopupMenu(param2BeanTable.getJTable(), "Options");
    varPopup.addAction("Show PDS", new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        Grib2ParameterBean pb = (Grib2ParameterBean) param2BeanTable.getSelectedBean();
        if (pb != null) {
          Formatter f = new Formatter();
          Grib2Show.showPdsTemplate(pb.gr.getPDSsection(), f, cust);
          infoPopup2.setText(f.toString());
          infoPopup2.gotoTop();
          infoWindow2.show();
        }
      }
    });

    record2BeanTable = new BeanTable(Grib2RecordBean.class, (PreferencesExt) prefs.node("Record2Bean"), false,
            "DataRepresentation", "from Grib2Input.getRecords()", null);
    record2BeanTable.addListSelectionListener(new ListSelectionListener() {
      public void valueChanged(ListSelectionEvent e) {
        Grib2RecordBean pb = (Grib2RecordBean) record2BeanTable.getSelectedBean();
        if (pb != null)
          drsInfo.setText(pb.drs.toString());
      }
    });

    varPopup = new PopupMenu(record2BeanTable.getJTable(), "Options");

    varPopup.addAction("Compare GridRecord", new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        List list = record2BeanTable.getSelectedBeans();
        if (list.size() == 2) {
          Grib2RecordBean bean1 = (Grib2RecordBean) list.get(0);
          Grib2RecordBean bean2 = (Grib2RecordBean) list.get(1);
          Formatter f = new Formatter();
          compare(bean1, bean2, f);
          infoPopup2.setText(f.toString());
          infoPopup2.gotoTop();
          infoWindow2.show();
        }
      }
    });

    varPopup.addAction("Show raw PDS bytes", new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        Formatter f = new Formatter();
        List list = record2BeanTable.getSelectedBeans();
        for (int i = 0; i < list.size(); i++) {
          Grib2RecordBean bean = (Grib2RecordBean) list.get(i);
          bean.toRawPdsString(f);
        }
        infoPopup.setText(f.toString());
        infoPopup.gotoTop();
        infoWindow.show();
      }
    });

    varPopup.addAction("Show complete GridRecord", new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        Grib2RecordBean bean = (Grib2RecordBean) record2BeanTable.getSelectedBean();
        if (bean != null) {
          Formatter f = new Formatter();
          Grib2Show.showCompleteGribRecord(f, fileList.get(bean.gr.getFile()).getPath(), bean.gr, cust);
          infoPopup.setText(f.toString());
          infoPopup.gotoTop();
          infoWindow.show();
        }
      }
    });

    varPopup.addAction("Show Processed GridRecord", new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        List list = record2BeanTable.getSelectedBeans();
        Formatter f = new Formatter();
        for (Object o : list) {
          Grib2RecordBean bean = (Grib2RecordBean) o;
          bean.showProcessedGridRecord(f);
        }
        infoPopup2.setText(f.toString());
        infoPopup2.gotoTop();
        infoWindow2.show();
      }
    });

    varPopup.addAction("Compare Data", new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        List list = record2BeanTable.getSelectedBeans();
        if (list.size() == 2) {
          Grib2RecordBean bean1 = (Grib2RecordBean) list.get(0);
          Grib2RecordBean bean2 = (Grib2RecordBean) list.get(1);
          Formatter f = new Formatter();
          compareData(bean1, bean2, f);
          infoPopup2.setText(f.toString());
          infoPopup2.gotoTop();
          infoWindow2.show();
        }
      }
    });

    varPopup.addAction("Show Data", new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        Grib2RecordBean bean = (Grib2RecordBean) record2BeanTable.getSelectedBean();
        if (bean != null) {
          Formatter f = new Formatter();
          showData(bean, f);
          infoPopup2.setText(f.toString());
          infoPopup2.gotoTop();
          infoWindow2.show();
        }
      }
    });

    varPopup.addAction("Extract GribRecord to File", new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        List beans = record2BeanTable.getSelectedBeans();
        if (beans.size() > 0)
          writeToFile(beans);
      }
    });

    varPopup.addAction("Show Bitmap", new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        Grib2RecordBean bean = (Grib2RecordBean) record2BeanTable.getSelectedBean();
        if (bean != null) {
          Formatter f = new Formatter();
          try {
            showBitmap(bean, f);
          } catch (IOException e1) {
            e1.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
          }
          infoPopup2.setText(f.toString());
          infoPopup2.gotoTop();
          infoWindow2.show();
        }
      }
    });

    varPopup.addAction("Compute Scale/offset of data", new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        Grib2RecordBean bean = (Grib2RecordBean) record2BeanTable.getSelectedBean();
        if (bean != null) {
          Formatter f = new Formatter();
          GribData.calcScaleOffset(bean, f);
          infoPopup2.setText(f.toString());
          infoPopup2.gotoTop();
          infoWindow2.show();
        }
      }
    });

    //gds2Table = new BeanTable(Gds2Bean.class, (PreferencesExt) prefs.node("Gds2Bean"), false, "Grib2GridDefinitionSection", "unique from Grib2Records");
    //varPopup = new PopupMenu(gds2Table.getJTable(), "Options");

    /////////////////////////////////////////
    // the info windows
    infoPopup = new TextHistoryPane();
    infoWindow = new IndependentWindow("Extra Information", BAMutil.getImage("netcdfUI"), infoPopup);
    infoWindow.setBounds((Rectangle) prefs.getBean("InfoWindowBounds", new Rectangle(300, 300, 500, 300)));

    infoPopup2 = new TextHistoryPane();
    infoWindow2 = new IndependentWindow("Extra Information", BAMutil.getImage("netcdfUI"), infoPopup2);
    infoWindow2.setBounds((Rectangle) prefs.getBean("InfoWindowBounds2", new Rectangle(300, 300, 500, 300)));

    drsInfo = new TextHistoryPane();

    setLayout(new BorderLayout());

    split2 = new JSplitPane(JSplitPane.VERTICAL_SPLIT, false, param2BeanTable, record2BeanTable);
    split2.setDividerLocation(prefs.getInt("splitPos2", 800));

    split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, false, split2, drsInfo);
    split.setDividerLocation(prefs.getInt("splitPos", 500));

    add(split, BorderLayout.CENTER);

  }

  public void save() {
    param2BeanTable.saveState(false);
    record2BeanTable.saveState(false);
    prefs.putBeanObject("InfoWindowBounds", infoWindow.getBounds());
    prefs.putBeanObject("InfoWindowBounds2", infoWindow2.getBounds());
    if (split != null) prefs.putInt("splitPos", split.getDividerLocation());
    if (split2 != null) prefs.putInt("splitPos2", split2.getDividerLocation());
  }

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

  private String spec;
  private MCollection dcm;
  private List<MFile> fileList;
  private Grib2Tables cust;
  // private Grib2Rectilyser rect2;

  /* public void setCollection(String filename) throws IOException {
    if (filename.endsWith(GribCollection.IDX_EXT)) {
      openIndex(filename);
    } else {
      openCollection(filename);
    }
  }

  private void openIndex(String filename) throws IOException {
    GribCollection gribCollection = new GribCollection(null);
    if (!gribCollection.readIndex(filename))
      throw new FileNotFoundException();

    this.cust = GribTables.factory(gribCollection.center, gribCollection.subcenter, gribCollection.master, gribCollection.local);

    java.util.List<Grib2ParameterBean> params = new ArrayList<Grib2ParameterBean>();
    java.util.List<Gds2Bean> gdsList = new ArrayList<Gds2Bean>();

    for (GribCollection.GroupHcs gHcs : gribCollection.getGroups()) {
      for (GribCollection.VariableIndex vi : gHcs.varIndex) {
        for (vi.records)
      }
    }
      addGroup(ncfile, g, useGroups);

    int fileno = 0;
    fileList = dcm.getFiles();
    for (MFile mfile : fileList) {
      f.format("%n %s%n", mfile.getPath());
      processGribFile(mfile.getPath(), fileno++, pdsSet, gdsSet, params);
    }
    param2BeanTable.setBeans(params);

    for (Grib2SectionGridDefinition gds : gdsSet.values()) {
      gdsList.add(new Gds2Bean( gds));
    }
    gds2Table.setBeans(gdsList);
  }  */

  public void setCollection(String spec) throws IOException {
    this.spec = spec;
    this.cust = null;

    Formatter f = new Formatter();
    this.dcm = scanCollection(spec, f);
    if (dcm == null) {
      javax.swing.JOptionPane.showMessageDialog(this, "Collection is null\n" + f.toString());
      return;
    }

    Map<Grib2Variable, Grib2ParameterBean> pdsSet = new HashMap<>();
    Map<Integer, Grib2SectionGridDefinition> gdsSet = new HashMap<>();

    java.util.List<Grib2ParameterBean> params = new ArrayList<>();

    int fileno = 0;
    for (MFile mfile : fileList) {
      f.format("%n %s%n", mfile.getPath());
      try (ucar.unidata.io.RandomAccessFile raf = new ucar.unidata.io.RandomAccessFile(mfile.getPath(), "r")) {
        raf.order(ByteOrder.BIG_ENDIAN);
        processGribFile(mfile, fileno++, raf, pdsSet, gdsSet, params, f);
      }
    }
    param2BeanTable.setBeans(params);
  }

  private void processGribFile(MFile mfile, int fileno, ucar.unidata.io.RandomAccessFile raf,
                               Map<Grib2Variable, Grib2ParameterBean> pdsSet,
                               Map<Integer, Grib2SectionGridDefinition> gdsSet,
                               List<Grib2ParameterBean> params, Formatter f) throws IOException {

    Grib2Index index = new Grib2Index();
    if (!index.readIndex(mfile.getPath(), mfile.getLastModified())) {
      index.makeIndex(mfile.getPath(), null);
    }

    for (Grib2SectionGridDefinition gds : index.getGds()) {
      int hash = gds.getGDS().hashCode();
      if (gdsSet.get(hash) == null)
        gdsSet.put(hash, gds);
    }

    for (Grib2Record gr : index.getRecords()) {
      if (cust == null)
        cust = Grib2Tables.factory(gr);

      gr.setFile(fileno);

      Grib2Variable gv = new Grib2Variable(cust, gr, 0, FeatureCollectionConfig.intvMergeDef, FeatureCollectionConfig.useGenTypeDef);
      Grib2ParameterBean bean = pdsSet.get(gv);
      if (bean == null) {
        bean = new Grib2ParameterBean(gr, gv);
        pdsSet.put(gv, bean);
        params.add(bean);
      }
      bean.addRecord(gr, raf);
    }
  }

  private MCollection scanCollection(String spec, Formatter f) {
    try (MCollection dc = CollectionAbstract.open(spec, spec, null, f)) {
      fileList = (List<MFile>) Misc.getList(dc.getFilesSorted());
      return dc;
    } catch (IOException e) {
      StringWriter sw = new StringWriter(5000);
      e.printStackTrace(new PrintWriter(sw));
      f.format(sw.toString());
      return null;
    }
  }

  /* public boolean writeIndex(Formatter f) throws IOException {
    MCollection dcm = scanCollection(spec, f);

    if (fileChooser == null)
      fileChooser = new FileManager(null, null, null, (PreferencesExt) prefs.node("FileManager"));
    String name = dcm.getCollectionName();
    int pos = name.lastIndexOf('/');
    if (pos < 0) pos = name.lastIndexOf('\\');
    if (pos > 0) name = name.substring(pos + 1);
    File def = new File(dcm.getRoot(), name + GribCollection.NCX_IDX);
    String filename = fileChooser.chooseFilename(def);
    if (filename == null) return false;
    if (!filename.endsWith(GribCollection.NCX_IDX))
      filename += GribCollection.NCX_IDX;
    File idxFile = new File(filename);

    Grib2CollectionBuilder.makeIndex(dcm, new Formatter(), logger);
    return true;
  }     */

  public void showInfo(Formatter f) {
    if (dcm == null) {
      if (spec == null) return;
      dcm = scanCollection(spec, f);
      if (dcm == null) return;
    }

    // just a list of the files
    f.format("dcm = %s%n", dcm);
    try {
      for (MFile mfile : dcm.getFilesSorted()) {
        f.format("  %s%n", mfile.getPath());
      }
    } catch (IOException e) {
      e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
    }

    // show nrecords, data size
    int nrecords = 0;
    long dataSize = 0;
    long msgSize = 0;
    for (Object o : param2BeanTable.getBeans()) {
      Grib2ParameterBean p = (Grib2ParameterBean) o;
      for (Grib2RecordBean r : p.getRecordBeans()) {
        nrecords++;
        dataSize += r.getDataLength();
        msgSize += r.getMsgLength();
      }
    }
    f.format("nrecords = %d, total grib data size = %d, total grib msg sizes = %d", nrecords, dataSize, msgSize);
  }

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

  public void checkProblems(Formatter f) {
    checkDuplicates(f);
    //checkRuntimes(f);
  }

  private static class DateCount implements Comparable<DateCount> {
    CalendarDate d;
    int count;

    private DateCount(CalendarDate d) {
      this.d = d;
    }

    @Override
    public int compareTo(DateCount o) {
      return d.compareTo(o.d);
    }
  }

  /* private void checkRuntimes(Formatter f) {
     Map<Date, DateCount> runs = new HashMap<Date, DateCount>();
     List<Grib2ParameterBean> params = param2BeanTable.getBeans();
     for (Grib2ParameterBean pb : params) {
       List<Grib2RecordBean> records = pb.getRecordBeans();
       for (Grib2RecordBean record : records) {
         Date d = record.getBaseTime();
         DateCount dc = runs.get(d);
         if (dc == null) {
           dc = new DateCount(d);
           runs.put(d, dc);
         }
         dc.count++;
       }
     }

     List<DateCount> dcList= new ArrayList<DateCount>(runs.values());
     Collections.sort(dcList);

     f.format("Run Dates%n");
     for (DateCount dc : dcList)
       f.format(" %s == %d%n", df.toDateTimeStringISO( dc.d), dc.count);
   } */

  private void checkDuplicates(Formatter f) {

    // how unique are the pds ?
    Set<Long> pdsMap = new HashSet<>();
    int dups = 0;
    int count = 0;

    // do all records have the same runtime ?
    Map<CalendarDate, DateCount> dateMap = new HashMap<>();

    List<Grib2ParameterBean> params = param2BeanTable.getBeans();
    for (Grib2ParameterBean param : params) {
      for (Grib2RecordBean record : param.getRecordBeans()) {
        CalendarDate d = record.gr.getReferenceDate();
        DateCount dc = dateMap.get(d);
        if (dc == null) {
          dc = new DateCount(d);
          dateMap.put(d, dc);
        }
        dc.count++;

        Grib2SectionProductDefinition pdss = record.gr.getPDSsection();
        long crc = pdss.calcCRC();
        if (pdsMap.contains(crc))
          dups++;
        else
          pdsMap.add(crc);
        count++;
      }
    }

    f.format("PDS duplicates = %d / %d%n%n", dups, count);

    List<DateCount> dcList = new ArrayList<>(dateMap.values());
    Collections.sort(dcList);

    f.format("Run Dates%n");
    int total = 0;
    for (DateCount dc : dcList) {
      f.format(" %s == %d%n", dc.d, dc.count);
      total += dc.count;
    }
    f.format("total records = %d%n", total);
  }

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

  private void writeToFile(List beans) {

    if (fileChooser == null)
      fileChooser = new FileManager(null, null, null, (PreferencesExt) prefs.node("FileManager"));

    FileOutputStream fos = null;
    RandomAccessFile raf = null;

    try {
      String filename = null;
      boolean append = false;
      int n = 0;
      MFile curr = null;

      for (Object o : beans) {
        Grib2RecordBean bean = (Grib2RecordBean) o;
        MFile mfile = fileList.get(bean.gr.getFile());
        if (curr == null || curr != mfile) {
          if (raf != null) raf.close();
          raf = new RandomAccessFile(mfile.getPath(), "r");
          curr = mfile;
        }

        if (fos == null) {
          String defloc = mfile.getPath();
          filename = fileChooser.chooseFilenameToSave(defloc + ".grib2");
          if (filename == null) return;
          File f = new File(filename);
          append = f.exists();
          fos = new FileOutputStream(filename, append);
        }

        Grib2SectionIndicator is = bean.gr.getIs();
        int size = (int) (is.getMessageLength());
        long startPos = is.getStartPos();
        if (startPos < 0) {
          JOptionPane.showMessageDialog(Grib2DataPanel.this, "Old index does not have message start - record not written");
        }

        byte[] rb = new byte[size];
        raf.seek(startPos);
        raf.readFully(rb);
        fos.write(rb);
        n++;
      }

      JOptionPane.showMessageDialog(Grib2DataPanel.this, filename + ": " + n + " records successfully written, append=" + append);

    } catch (Exception ex) {
      JOptionPane.showMessageDialog(Grib2DataPanel.this, "ERROR: " + ex.getMessage());
      ex.printStackTrace();

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

  private void compare(Grib2RecordBean bean1, Grib2RecordBean bean2, Formatter f) {
    Grib2SectionIndicator is1 = bean1.gr.getIs();
    Grib2SectionIndicator is2 = bean2.gr.getIs();
    f.format("Indicator Section%n");
    if (is1.getDiscipline() != is2.getDiscipline())
      f.format("getDiscipline differs %d != %d %n", is1.getDiscipline(), is2.getDiscipline());
    if (is1.getMessageLength() != is2.getMessageLength())
      f.format("getGribLength differs %d != %d %n", is1.getMessageLength(), is2.getMessageLength());

    f.format("%nId Section%n");
    Grib2SectionIdentification id1 = bean1.gr.getId();
    Grib2SectionIdentification id2 = bean2.gr.getId();
    if (id1.getCenter_id() != id2.getCenter_id())
      f.format("Center_id differs %d != %d %n", id1.getCenter_id(), id2.getCenter_id());
    if (id1.getSubcenter_id() != id2.getSubcenter_id())
      f.format("Subcenter_id differs %d != %d %n", id1.getSubcenter_id(), id2.getSubcenter_id());
    if (id1.getMaster_table_version() != id2.getMaster_table_version())
      f.format("Master_table_version differs %d != %d %n", id1.getMaster_table_version(), id2.getMaster_table_version());
    if (id1.getLocal_table_version() != id2.getLocal_table_version())
      f.format("Local_table_version differs %d != %d %n", id1.getLocal_table_version(), id2.getLocal_table_version());
    if (id1.getProductionStatus() != id2.getProductionStatus())
      f.format("ProductionStatus differs %d != %d %n", id1.getProductionStatus(), id2.getProductionStatus());
    if (id1.getTypeOfProcessedData() != id2.getTypeOfProcessedData())
      f.format("TypeOfProcessedData differs %d != %d %n", id1.getTypeOfProcessedData(), id2.getTypeOfProcessedData());
    if (!id1.getReferenceDate().equals(id2.getReferenceDate()))
      f.format("ReferenceDate differs %s != %s %n", id1.getReferenceDate(), id2.getReferenceDate());
    if (id1.getSignificanceOfRT() != id2.getSignificanceOfRT())
      f.format("getSignificanceOfRT differs %d != %d %n", id1.getSignificanceOfRT(), id2.getSignificanceOfRT());

    Grib2SectionLocalUse lus1 = bean1.gr.getLocalUseSection();
    Grib2SectionLocalUse lus2 = bean2.gr.getLocalUseSection();
    if (lus1 == null || lus2 == null) {
      if (lus1 == lus2)
        f.format("%nLus are both null%n");
      else
        f.format("%nLus are different %s != %s %n", lus1, lus2);
    } else {
      f.format("%nCompare LocalUseSection%n");
      Misc.compare(lus1.getRawBytes(), lus2.getRawBytes(), f);
    }

    compare(bean1.gr.getPDSsection(), bean2.gr.getPDSsection(), f);
    compare(bean1.gr.getGDSsection(), bean2.gr.getGDSsection(), f);
  }

  private void compare(Grib2SectionGridDefinition gdss1, Grib2SectionGridDefinition gdss2, Formatter f) {
    f.format("1 GribGDS hash = %s%n", gdss1.getGDS().hashCode());
    f.format("2 GribGDS hash = %s%n", gdss2.getGDS().hashCode());

    f.format("%nCompare Gds%n");
    byte[] raw1 = gdss1.getRawBytes();
    byte[] raw2 = gdss2.getRawBytes();
    Misc.compare(raw1, raw2, f);

    Grib2Gds gds1 = gdss1.getGDS();
    Grib2Gds gds2 = gdss2.getGDS();
    GdsHorizCoordSys gdsh1 = gds1.makeHorizCoordSys();
    GdsHorizCoordSys gdsh2 = gds2.makeHorizCoordSys();

    f.format("%ncompare gds1 - gds22%n");
    f.format(" Start x diff : %f%n", gdsh1.getStartX() - gdsh2.getStartX());
    f.format(" Start y diff : %f%n", gdsh1.getStartY() - gdsh2.getStartY());
    f.format(" End x diff : %f%n", gdsh1.getEndX() - gdsh2.getEndX());
    f.format(" End y diff : %f%n", gdsh1.getEndY() - gdsh2.getEndY());

    LatLonPoint pt1 = gdsh1.getCenterLatLon();
    LatLonPoint pt2 = gdsh2.getCenterLatLon();
    f.format(" Center lon diff : %f%n", pt1.getLongitude() - pt2.getLongitude());
    f.format(" Center lat diff : %f%n", pt1.getLatitude() - pt2.getLatitude());
  }

  private void compare(Grib2SectionProductDefinition pds1, Grib2SectionProductDefinition pds2, Formatter f) {
    f.format("%nCompare Pds%n");
    byte[] raw1 = pds1.getRawBytes();
    byte[] raw2 = pds2.getRawBytes();
    Misc.compare(raw1, raw2, f);
  }

  void compareData(Grib2RecordBean bean1, Grib2RecordBean bean2, Formatter f) {
    float[] data1 = null, data2 = null;
    try {
      data1 = bean1.readData();
      data2 = bean2.readData();
    } catch (IOException e) {
      f.format("IOException %s", e.getMessage());
      return;
    }

    Misc.compare(data1, data2, f);
  }

  void showData(Grib2RecordBean bean1, Formatter f) {
    float[] data;
    try {
      data = bean1.readData();
    } catch (IOException e) {
      f.format("IOException %s", e.getMessage());
      return;
    }

    int count = 0;
    for (float fd : data)
      f.format("%d: %f%n", count++, fd);
  }

  void showBitmap(Grib2RecordBean bean1, Formatter f) throws IOException {
    byte[] bitmap;
    ucar.unidata.io.RandomAccessFile raf = null;
    try {
      raf = bean1.getRaf();
      Grib2SectionBitMap bms = bean1.gr.getBitmapSection();
      f.format("%s%n", bms);
      bitmap = bms.getBitmap(raf);
    } finally {
      if (raf != null) raf.close();
    }

    if (bitmap == null) {
      f.format(" no bitmap%n");
      return;
    }

    int count = 0;
    int bits = 0;
    for (byte b : bitmap) {
      short s = DataType.unsignedByteToShort(b);
      bits += Long.bitCount(s);
      f.format("%8s", Long.toBinaryString(s));
      if (++count % 10 == 0)
        f.format("%n");
    }
    f.format("%n%n#bits on = %d%n", bits);
    f.format("total nbits = %d%n", 8 * count);
    f.format("bitmap nbytes = %d%n", bitmap.length);
  }

  /* void calcData(Grib2RecordBean bean1, Formatter f) {
    float[] data;
    try {
      data = bean1.readData();
    } catch (IOException e) {
      f.format("IOException %s", e.getMessage());
      return;
    }

    // calc scale/offset

    int nbits = bean1.getNBits();
    //int width = (2 << nbits) - 1;
    int width2 = (2 << (nbits-1)) - 1;
    f.format(" nbits = %d%n", nbits);
    //f.format(" width = %d (0x%s) %n", width, Long.toHexString(width));
    f.format(" width = %d (0x%s) %n", width2, Long.toHexString(width2));

    float dataMin = Float.MAX_VALUE;
    float dataMax = -Float.MAX_VALUE;
    for (float fd : data) {
      dataMin = Math.min(dataMin, fd);
      dataMax = Math.max(dataMax, fd);
    }
    f.format(" dataMin = %f%n", dataMin);
    f.format(" dataMax = %f%n", dataMax);
    f.format(" range = %f%n", (dataMax - dataMin));

    // scale_factor =(dataMax - dataMin) / (2^n - 1)
    // add_offset = dataMin + 2^(n-1) * scale_factor

    float scale_factor = (dataMax - dataMin) / width2;
    float add_offset = dataMin + width2 * scale_factor / 2;

    f.format(" scale_factor = %f%n", scale_factor);
    f.format(" add_offset = %f%n", add_offset);

    // unpacked_data_value = packed_data_value * scale_factor + add_offset
    // packed_data_value = nint((unpacked_data_value - add_offset) / scale_factor)

    int n = data.length;

    ByteBuffer bb = ByteBuffer.allocate(2*n);
    ShortBuffer sb = bb.asShortBuffer();
    float diffMax = -Float.MAX_VALUE;
    float diffTotal = 0;
    float diffTotal2 = 0;
    for (float fd : data) {
      short packed_data = (short) Math.round((fd - add_offset) / scale_factor);
      float unpacked_data = packed_data * scale_factor + add_offset;
      float diff = Math.abs(fd-unpacked_data);
      if (diff > 100)
        f.format("   org=%f, packed_data=%d unpacked=%f diff = %f%n",fd, packed_data, unpacked_data, diff);

      diffMax = Math.max(diffMax, diff);
      diffTotal += diff;
      diffTotal2 += diff*diff;
      sb.put(packed_data);
    }

    f.format("%n max_diff = %f%n", diffMax);
    f.format(" avg_diff = %f%n", diffTotal/data.length);

    // Math.sqrt( sumsq/n - avg * avg)
    float mean = diffTotal/n;
    float var = (diffTotal2/n - mean * mean);
    f.format(" std_diff = %f%n", Math.sqrt(var));

    f.format("%nCompression%n");
    f.format(" number of values = %d%n", n);
    f.format(" uncompressed as floats = %d%n", n*4);
    f.format(" uncompressed packed = %d%n", n*nbits/8);
    f.format(" grib compressed = %d%n",  bean1.getDataLength());

    f.format("%ndeflate%n");
    Deflater deflater = new Deflater();
    deflater.setInput(bb.array());
    deflater.finish();
    int compressedSize = deflater.deflate(new byte[10*n]);
    deflater.end();

    f.format(" compressedSize = %d%n", compressedSize);
    f.format(" compressedRatio = %f%n", (float) compressedSize / (n*nbits/8));
    f.format(" ratio with grib = %f%n", (float) compressedSize / bean1.getDataLength());

    try {
      f.format("%nbzip2%n");
      ByteArrayOutputStream out = new ByteArrayOutputStream(2*compressedSize);
      org.itadaki.bzip2.BZip2OutputStream zipper = new org.itadaki.bzip2.BZip2OutputStream(out);
      InputStream fin = new ByteArrayInputStream(bb.array());
      IO.copy(fin, zipper);
      zipper.close();
      compressedSize = out.size();
      f.format(" compressedSize = %d%n", compressedSize);
      f.format(" compressedRatio = %f%n", (float) compressedSize / (n*nbits/8));
      f.format(" ratio with grib = %f%n", (float) compressedSize / bean1.getDataLength());

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

    /* try {
      f.format("%nbzip2%n");
      ByteArrayOutputStream out = new ByteArrayOutputStream(2*compressedSize);
      BZip2CompressorOutputStream zipper = new BZip2CompressorOutputStream(out);
      InputStream fin = new ByteArrayInputStream(bb.array());
      IO.copy(fin, zipper);
      zipper.close();
      compressedSize = out.size();
      f.format(" compressedSize = %d%n", compressedSize);
      f.format(" compressedRatio = %f%n", (float) compressedSize / (n*nbits/8));
      f.format(" ratio with grib = %f%n", (float) compressedSize / bean1.getDataLength());

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

    try {
      f.format("%nLZMA2%n");
      ByteArrayOutputStream out = new ByteArrayOutputStream(2*compressedSize);
      XZCompressorOutputStream zipper = new XZCompressorOutputStream(out);
      InputStream fin = new ByteArrayInputStream(bb.array());
      IO.copy(fin, zipper);
      zipper.close();
      compressedSize = out.size();
      f.format(" compressedSize = %d%n", compressedSize);
      f.format(" compressedRatio = %f%n", (float) compressedSize / (n*nbits/8));
      f.format(" ratio with grib = %f%n", (float) compressedSize / bean1.getDataLength());

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

  }  */

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

  /*
  /* private String doAccumAlgo(Grib2ParameterBean pb) {
    List<Grib2RecordBean> all = new ArrayList<Grib2RecordBean>(pb.getRecordBeans());
    List<Grib2RecordBean> hourAccum = new ArrayList<Grib2RecordBean>(all.size());
    List<Grib2RecordBean> runAccum = new ArrayList<Grib2RecordBean>(all.size());

    Set<Integer> ftimes = new HashSet<Integer>();

    for (Grib2RecordBean rb : pb.getRecordBeans()) {
      int ftime = rb.getForecastTime();
      ftimes.add(ftime);

      int start = rb.getStartInterval();
      int end = rb.getEndInterval();
      if (end - start == 1) hourAccum.add(rb);
      if (start == 0) runAccum.add(rb);
    }

    int n = ftimes.size();

    Formatter f = new Formatter();
    f.format("      all: ");
    showList(all, f);
    f.format("%n");

    if (hourAccum.size() > n - 2) {
      for (Grib2RecordBean rb : hourAccum) all.remove(rb);
      f.format("hourAccum: ");
      showList(hourAccum, f);
      f.format("%n");
    }

    if (runAccum.size() > n - 2) {
      for (Grib2RecordBean rb : runAccum) all.remove(rb);
      f.format(" runAccum: ");
      showList(runAccum, f);
      f.format("%n");
    }

    if (all.size() > 0) {
      boolean same = true;
      int intv = -1;
      for (Grib2RecordBean rb : all) {
        int start = rb.getStartInterval();
        int end = rb.getEndInterval();
        int intv2 = end - start;
        if (intv < 0) intv = intv2;
        else same = (intv == intv2);
        if (!same) break;
      }

      f.format("remaining: ");
      showList(all, f);
      f.format("%s %n", same ? " Interval=" + intv : " Mixed");
    }

    return f.toString();
  }

  private void showList(List<Grib2RecordBean> list, Formatter f) {
    f.format("(%d) ", list.size());
    for (Grib2RecordBean rb : list)
      f.format(" %d-%d", rb.getStartInterval(), rb.getEndInterval());
  }  */

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

  public class Grib2ParameterBean {
    Grib2Record gr;
    Grib2SectionIdentification id;
    Grib2Pds pds;
    List<Grib2RecordBean> records;
    int discipline;
    Grib2Variable gv;

    // no-arg constructor

    public Grib2ParameterBean() {
    }

    public Grib2ParameterBean(Grib2Record r, Grib2Variable gv) throws IOException {
      this.gr = r;
      this.gv = gv;

      // long refTime = r.getId().getReferenceDate().getMillis();
      pds = r.getPDS();
      id = r.getId();
      discipline = r.getDiscipline();
      records = new ArrayList<>();
      //gdsKey = r.getGDSsection().calcCRC();
    }

    public String getCdmHash() {
      return Integer.toHexString(gv.hashCode());
    }

    void addRecord(Grib2Record r, ucar.unidata.io.RandomAccessFile raf) throws IOException {
      records.add(new Grib2RecordBean(r, raf));
    }

    List<Grib2RecordBean> getRecordBeans() {
      return records;
    }

    public String getParamNo() {
      return discipline + "-" + pds.getParameterCategory() + "-" + pds.getParameterNumber();
    }

    public int getPDS() {
      return gr.getPDSsection().getPDSTemplateNumber();
    }

    public int getN() {
      return records.size();
    }

    public int getLevelType() {
      return pds.getLevelType1();
    }

    public String getLevelName() {
      return cust.getLevelNameShort(pds.getLevelType1());
    }

    public int getGDS() {
      return gr.getGDSsection().getGDS().hashCode();
    }

    public String toString() {
      Formatter f = new Formatter();
      Grib2Show.showPdsTemplate(gr.getPDSsection(), f, cust);
      return f.toString();
    }

    public String toProcessedString() {
      Formatter f = new Formatter();
      Grib2Show.showProcessedPds(cust, pds, discipline, f);
      return f.toString();
    }

    public String getNBits() {
      calcBits();
      if (minBits == maxBits) return Integer.toString(minBits);
      return minBits+"-"+maxBits;
    }

    public String getScale() {
      calcBits();
      Formatter f= new Formatter();
      if (minScale == Double.MAX_VALUE)
        f.format("N/A");
      if (Misc.nearlyEquals(minScale, maxScale))
        f.format("%g",minScale);
      else
        f.format("(%g,%g)",minScale,maxScale);
      return f.toString();
    }

    public float getAvgBits() {
      calcBits();
      return avgbits;
    }

    public float getCompress() {
      calcBits();
      return compress;
    }

    private double minScale, maxScale;
    private int minBits, maxBits;
    private float nbits = -1;
    private float avgbits;
    private float compress;
    private void calcBits() {
      if (nbits >= 0) return;
      nbits = 0;
      int count = 0;
      minScale = Float.MAX_VALUE;
      maxScale = -Float.MAX_VALUE;
      minBits = Integer.MAX_VALUE;
      for (Grib2RecordBean bean : records) {
        minBits = Math.min(minBits, bean.getNBits());
        maxBits = Math.max(maxBits, bean.getNBits());
        if (0 != bean.getNBits()) {
          minScale = Math.min(minScale, bean.getScale());
          maxScale = Math.max(maxScale, bean.getScale());
        }
        nbits += bean.getNBits();
        avgbits += bean.getAvgBits();
        count++;
      }
      compress = nbits / avgbits;
      if (count > 0) {
        nbits /= count;
        avgbits /= count;
      }
    }

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

    public String getName() {
      return GribUtils.makeNameFromDescription(cust.getVariableName(gr));
    }

    public String getUnits() {
      Grib2Tables.Parameter p = cust.getParameter(discipline, pds.getParameterCategory(), pds.getParameterNumber());
      return (p == null) ? "?" : p.getUnit();
    }

    public final String getCenter() {
      //String center = CommonCodeTable.getCenterName(id.getCenter_id(), 2);
      //String subcenter = cust.getSubCenterName(id.getCenter_id(), id.getSubcenter_id());
      return id.getCenter_id() + "/" + id.getSubcenter_id(); // + " (" + center + "/" + subcenter + ")";
    }

    public final String getTable() {
      return id.getMaster_table_version() + "-" + id.getLocal_table_version();
    }

  }

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

  public class Grib2RecordBean implements GribData.Bean {
    Grib2Record gr;
    Grib2Pds pds;
    Grib2Drs drs;
    Grib2SectionData dataSection;
    long drsLength;

    GribData.Info info;
    double minimum, maximum, scale;

    public Grib2RecordBean() {
    }

    public Grib2RecordBean(Grib2Record m, ucar.unidata.io.RandomAccessFile raf) throws IOException {
      this.gr = m;
      this.pds = gr.getPDS();
      Grib2SectionDataRepresentation drss = gr.getDataRepresentationSection();
      this.drs = drss.getDrs(raf);
      this.drsLength = drss.getLength(raf);
      this.dataSection = gr.getDataSection();

      info = gr.getBinaryDataInfo(raf);

      double pow10 =  Math.pow(10.0, -getDecScale());        // 1/10^D
      minimum = (float) (pow10 * info.referenceValue);          // R / 10^D
      scale = (float) (pow10 * Math.pow(2.0, getBinScale()));  // 2^E / 10^D

      double maxPacked = Math.pow(2.0, getNBits()) - 1;
      maximum = minimum +  scale * maxPacked;
    }

   // public String getHeader() { return Grib2Utils.cleanupHeader(gr.getHeader()); }

    public int getDrsTemplate() {
      return gr.getDataRepresentationSection().getDataTemplate();
    }

    public int getNDataPoints() {
      return info.ndataPoints;
    }

    public int getNPoints() {
       return info.nPoints;
     }

    public int getNGroups() {
      return drs.getNGroups();
    }

    public float getAvgBits() {
      float len = getDataLength();
      int n = getNDataPoints();
      return len * 8 / n;
    }

    public int getNBits() {
      return info.numberOfBits;
    }

    public long getMsgLength() {
      return info.msgLength;
    }

    public long getDataLength() {
      return info.dataLength;
    }

    public int getBinScale() {
      return info.binaryScaleFactor;
    }

    public int getDecScale() {
      return info.decimalScaleFactor;
    }

    public double getMinimum() {
      return minimum;
    }

    public double getMaximum() {
      return maximum;
    }

    public double getScale() {
      return scale;
    }

    public String getPrecision() {
      Formatter f= new Formatter();
      f.format("%.5g", scale/2);
      return f.toString();
    }

    public int getBitMap() {
      return gr.getBitmapSection().getBitMapIndicator();
    }

    public boolean getBitMapReplaced() {
      return gr.isBmsReplaced();
    }

    public long getStartPos() {
      return gr.getIs().getStartPos();
    }

    public String getHeader() {
      return Grib2Utils.cleanupHeader(gr.getHeader());
    }

    public final int getTime() {
      return pds.getForecastTime();
    }

    public String getLevel() {
      int v1 = pds.getLevelType1();
      int v2 = pds.getLevelType2();
      if (v1 == 255) return "";
      if (v2 == 255) return "" + pds.getLevelValue1();
      if (v1 != v2) return pds.getLevelValue1() + "-" + pds.getLevelValue2() + " level2 type= " + v2;
      return pds.getLevelValue1() + "-" + pds.getLevelValue2();
    }

    public void toRawPdsString(Formatter f) {
      byte[] bytes = gr.getPDSsection().getRawBytes();
      int count = 1;
      for (byte b : bytes) {
        short s = DataType.unsignedByteToShort(b);
        f.format(" %d : %d%n", count++, s);
      }
    }

    public void showProcessedGridRecord(Formatter f) {
      f.format("%nFile=%s (%d)%n", fileList.get(gr.getFile()).getPath(), gr.getFile());
      Grib2Show.showProcessedGridRecord(cust, gr, f);
    }

    public float[] readData() throws IOException {
      ucar.unidata.io.RandomAccessFile raf = getRaf();
      try {
        raf.order(ucar.unidata.io.RandomAccessFile.BIG_ENDIAN);
        return gr.readData(raf);
      } finally {
        if (raf != null)
          raf.close();
      }
    }

    ucar.unidata.io.RandomAccessFile getRaf() throws IOException {
      int fileno = gr.getFile();
      MFile mfile = fileList.get(fileno);
      return new ucar.unidata.io.RandomAccessFile(mfile.getPath(), "r");
    }

  }

}


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

import ucar.nc2.ui.gis.AbstractGisFeature;
import ucar.nc2.ui.gis.GisPart;
import ucar.unidata.io.BeLeDataInputStream;

import java.awt.geom.Rectangle2D;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * EsriShapefile.java
 *

 * Encapsulates details of ESRI Shapefile format, documented at
 * http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf
 *
 * @author Russ Rew
 */
public class EsriShapefile {
  // these are only shape types handled by this package, so far
  enum Type {none, point, polyline, polygon, multipoint}

  private final static int SHAPEFILE_CODE = 9994; // shapefile magic number
  private final static double defaultCoarseness = 0.0;

  private BeLeDataInputStream bdis; // the shapefile data stream
  private int fileBytes;         // bytes in file, according to header
  private int bytesSeen = 0;         // so far, in bytes.
  private int version;         // of shapefile format (currently 1000)
  private Type type;
  private boolean debug = false;

  private List<EsriFeature> features;        // EsriFeatures in List
  private Rectangle2D listBounds;    // bounds from shapefile
  private double resolution;         // computed from coarseness

  /*
    * Relative accuracy of plotting.  Larger means coarser but
    * faster, 0.0 is all available resolution.  Anything less than 1
    * is wasted sub-pixel plotting, but retains quality for closer
    * zooming.  Anything over about 2.0 is ugly.  1.50 makes things
    * faster than 1.0 at the cost of barely discernible ugliness, but
    * for best quality (without zooms), set to 1.0.  If you still
    * want quality at 10:1 zooms, set to 1/10, etc.  */
  private double coarseness = defaultCoarseness;

  /**
   * Read an ESRI shapefile and extract all features into
   * an in-memory structure.
   *
   * @param filename name of ESRI shapefile (typically has ".shp"
   *                 extension)
   */
  public EsriShapefile(String filename) throws IOException {
    this(filename, null);
  }

  /**
   * Read an ESRI shapefile from a URL and extract all features into
   * an in-memory structure.
   *
   * @param url URL of ESRI shapefile
   */
  public EsriShapefile(URL url) throws IOException {
    this(url, null);
  }

  /**
   * Read an ESRI shapefile and extract all features into
   * an in-memory structure, with control of time versus resolution.
   *
   * @param filename   name of ESRI shapefile (typically has ".shp"
   *                   extension)
   * @param coarseness to tradeoff plot quality versus speed.
   */
  public EsriShapefile(String filename, double coarseness) throws IOException {
    this(filename, null, coarseness);
  }

  /**
   * Read an ESRI shapefile from a URL and extract all features into
   * an in-memory structure, with control of time versus resolution.
   *
   * @param url        URL of ESRI shapefile
   * @param coarseness to tradeoff plot quality versus speed.
   */
  public EsriShapefile(URL url, double coarseness) throws IOException {
    this(url, null, coarseness);
  }

  /**
   * Read an ESRI shapefile and extract the subset of features that have
   * bounding boxes that intersect a specified bounding box
   *
   * @param url        URL of ESRI shapefile
   * @param bBox       bounding box specifying which features to select,
   *                   namely those whose bounding boxes intersect this one. If null,
   *                   bounding box of whole shapefile is used
   * @param coarseness to tradeoff plot quality versus speed.
   */
  public EsriShapefile(URL url, Rectangle2D bBox, double coarseness) throws IOException {
    this(new DataInputStream(url.openStream()), bBox, coarseness);
  }

  /**
   * Read an ESRI shapefile and extract all features into an in-memory
   * structure, with control of time versus resolution.
   *
   * @param filename   name of ESRI shapefile
   * @param bBox       bounding box specifying which features to select,
   *                   namely those whose bounding boxes intersect this one. If null,
   *                   bounding box of whole shapefile is used
   * @param coarseness to tradeoff plot quality versus speed.
   */
  public EsriShapefile(String filename, Rectangle2D bBox, double coarseness) throws IOException {
    this(new FileInputStream(filename), bBox, coarseness);
  }

  /**
   * Read an ESRI shapefile and extract the subset of features that have
   * bounding boxes that intersect a specified bounding box
   *
   * @param url  URL of ESRI shapefile
   * @param bBox bounding box specifying which features to select,
   *             namely those whose bounding boxes intersect this one. If null,
   *             bounding box of whole shapefile is used
   */
  public EsriShapefile(URL url, Rectangle2D bBox) throws IOException {
    this(new DataInputStream(url.openStream()), bBox, 0.0f);
  }

  /**
   * Read an ESRI shapefile and extract the subset of features that have
   * bounding boxes that intersect a specified bounding box.
   *
   * @param filename name of ESRI shapefile
   * @param bBox     bounding box specifying which features to select,
   *                 namely those whose bounding boxes intersect this one. If null,
   *                 bounding box of whole shapefile is used
   */
  public EsriShapefile(String filename, Rectangle2D bBox) throws IOException {
    this(new FileInputStream(filename), bBox, 0.0f);
  }

  /**
   * Read an ESRI shapefile and extract the subset of features that
   * have bounding boxes that intersect a specified bounding box,
   * with control of time versus resolution.
   *
   * @param iStream input from which to read
   * @param bBox    bounding box specifying which features to select,
   *                namely those whose bounding boxes intersect this one. If null,
   *                bounding box of whole shapefile is used
   */
  public EsriShapefile(InputStream iStream, Rectangle2D bBox, double coarseness) throws IOException {
    BufferedInputStream bin = new BufferedInputStream(iStream);

    if (coarseness < 0.0f)
      this.coarseness = defaultCoarseness;
    else
      this.coarseness = coarseness;

    if (isZipStream(bin)) {
      ZipInputStream zin = new ZipInputStream(bin);
      ZipEntry ze;
      while ((ze = zin.getNextEntry()) != null) {
        if (ze.getName().endsWith(".shp") || ze.getName().endsWith(".SHP")) {
          bdis = new BeLeDataInputStream(zin);
          init(bBox);
          return;
        }
        zin.closeEntry();
      }
      throw new IOException("no .shp entry found in zipped input");
    } else {
      bdis = new BeLeDataInputStream(bin);
      init(bBox);
    }
  }

  static boolean isZipStream(InputStream is) throws IOException {
    is.mark(5);
    int c1 = is.read();
    int c2 = is.read();
    int c3 = is.read();
    int c4 = is.read();
    is.reset();
    if (c1 == 'P' && c2 == 'K' && c3 == 0x03 && c4 == 0x04)
      return true;
    return false;
  }

  private void init(Rectangle2D bBox) throws IOException {
    int fileCode = readInt();
    if (fileCode != SHAPEFILE_CODE)
      throw (new IOException("Not a shapefile"));

    skipBytes(20);    // 5 unused ints
    fileBytes = 2 * readInt();
    version = readLEInt();

    type = assignType(readLEInt());
    listBounds = readBoundingBox();

    // if no bounds specified, use shapefile bounds
    if (bBox == null)
      bBox = listBounds;

    double xu = bBox.getMaxX();
    double yu = bBox.getMaxY();
    double xl = bBox.getMinX();
    double yl = bBox.getMinY();
    double w = 1000; // for resolution, just assume 1000x1000 display
    double h = 1000;
    resolution = 1.0 / (coarseness * Math.min(Math.abs(xu - xl) / w, Math.abs(yu - yl) / h));

    skipBytes(32); // skip to start of first record header

    /* Read through file, filtering out features that don't
       intersect bounding box. */
    features = new ArrayList<>();
    while (bytesSeen < fileBytes) {
      EsriFeature gf = nextFeature();
      if (gf != null && gf.getBounds2D().intersects(bBox))
        features.add(gf);
    }
  }

  private Type assignType(int type) {
    switch (type) {
      case 1 : return Type.point;
      case 3 : return Type.polyline;
      case 5 : return Type.polygon;
      case 8 : return Type.multipoint;
      default: return Type.none;
    }
  }

  /**
   * Return percent of file read, so far.
   *
   * @return percent of file read, so far.
   */
  public double percentRead() {
    return (double) bytesSeen / (double) fileBytes;
  }

  /**
   * @return number of features in shapefile
   */
  public int getNumFeatures() {
    return features.size();
  }

  private Rectangle2D readBoundingBox() throws IOException {

    double xMin = readLEDouble();
    double yMin = readLEDouble();
    double xMax = readLEDouble();
    double yMax = readLEDouble();
    double width = xMax - xMin;
    double height = yMax - yMin;

    return new Rectangle2D.Double(xMin, yMin, width, height);
  }

  private EsriFeature nextFeature() throws IOException {
    int startAt = bytesSeen;
    int recordNumber = readInt(); // starts at 1, not 0
    int contentLength = readInt(); // in 16-bit words
    if (debug) System.out.printf("Shapefile start=%d record=%d len=%d%n", startAt, recordNumber, contentLength);
    int type = readLEInt();
    Type featureType = assignType(type);
    switch (featureType) {
      case point:
        return new EsriPoint();
      case multipoint:    // multipoint, only 1 part
        return new EsriMultipoint();
      case polyline:
        return new EsriPolyline();
      case polygon:
        return new EsriPolygon();
      default:
        System.out.printf("Skip shapefile record=%d pos =%d len=%d%n", recordNumber, startAt, contentLength);
        skipBytes(2*(contentLength-2));
        return null;
    }
  }

  private int readLEInt() throws IOException {
    bytesSeen += 4;
    return bdis.readLEInt();
  }

  private int readInt() throws IOException {
    bytesSeen += 4;
    return bdis.readInt();
  }

  private double readLEDouble() throws IOException {
    bytesSeen += 8;
    return bdis.readLEDouble();
  }

  private void readLEDoubles(double[] d, int n) throws IOException {
    bdis.readLEDoubles(d, n);
    bytesSeen += 8 * n;
  }

  private void skipBytes(int n) throws IOException {
    long skipped = 0;
    while (skipped < n)
      skipped += bdis.skip(n-skipped);
    bytesSeen += n;
  }

  /**
   * Returns shapefile format version (currently 1000)
   *
   * @return version, as stored in shapefile.
   */
  public int getVersion() {
    return version;
  }

  /**
   * Get bounding box, according to file (not computed from features)
   *
   * @return bounding box for shapefilew, as stored in header.
   */
  public Rectangle2D getBoundingBox() {
    return listBounds;
  }

  /**
   * Get a List of all the GisFeatures in the shapefile.  This is
   * very fast after the constructor has been called, since it is
   * created during construction.
   *
   * @return a List of features
   */
  public java.util.List getFeatures() {
    return features;
  }

  /**
   * Get a List of all the features in the shapefile that intersect
   * the specified bounding box.  This requires testing every
   * feature in the List created at construction, so it's faster to
   * just give a bounding box o the constructor if you will only do
   * this once.
   *
   * @param bBox specifying the bounding box with which all
   *             the returned features bounding boxes have a non-empty
   *             intersection.
   * @return a new list of features in the shapefile whose bounding
   *         boxes intersect the specified bounding box.
   */
  public List<EsriFeature> getFeatures(Rectangle2D bBox) {
    if (bBox == null)
      return features;

    List<EsriFeature> list = new ArrayList<>();
    for (EsriFeature gf : features) {
      if (gf.getBounds2D().intersects(bBox))
        list.add(gf);
    }
    return list;
  }

  /**
   * EsriFeature.java
   *

   *

   * Created: Sat Feb 20 17:19:53 1999
   *
   * @author Russ Rew
   */
  public abstract class EsriFeature extends AbstractGisFeature {
    protected Rectangle2D bounds;
    int numPoints;
    int numParts;
    List<GisPart> partsList = new ArrayList<>();

    // private DbaseFile dbfile;
    // private int recordNumber;
    // Together these can be used to access more info about feature,
    // using the associated .dbf Dbase file.
    // TODO: extend interface to permit access to this info

    /**
     * Get bounding rectangle for this feature.
     *
     * @return bounding rectangle for this feature.
     */
    public Rectangle2D getBounds2D() {
      return bounds;
    }

    /**
     * Get total number of points in all parts of this feature.
     *
     * @return total number of points in all parts of this feature.
     */
    public int getNumPoints() {
      return numPoints;
    }

    /**
     * Get number of parts comprising this feature.
     *
     * @return number of parts comprising this feature.
     */
    public int getNumParts() {
      return numParts;
    }

    /**
     * Get the parts of this feature, in the form of an iterator.
     *
     * @return the iterator over the parts of this feature.  Each part
     *         is a GisPart.
     */
    public java.util.Iterator getGisParts() {
      return partsList.iterator();
    }

    @Override
    public String toString() {
      return "EsriFeature{" +
              "numPoints=" + numPoints +
              ", numParts=" + numParts +
      //        ", npartsList=" + partsList +
              ", bounds=" + bounds +
              '}';
    }
  } // EsriFeature

  private double[] xyPoints = new double[100]; // buffer for points input

  /**
   * Discretize elements of array to a lower resolution.  For
   * example, if resolution = 100., the value 3.14159265358979 will
   * be changed to 3.14.
   *
   * @param d array of values to discretize to lower resolution
   * @param n number of values in array to discretize
   */
  private void discretize(double[] d, int n) {
    if (coarseness == 0.0)
      return;
    for (int i = 0; i < n; i++) {
      d[i] = (Math.rint(resolution * d[i]) / resolution);
    }
  }

  /**
   * Represents a Polygon in an ESRI shapefile as a List of
   * GisParts.  A Polygon is just an ordered set of vertices of 1 or
   * more parts, where a part is a closed connected sequence of
   * points.  A state boundary might be represented by a Polygon,
   * for example, where each part might be the main part or islands.
   *

   * Created: Sat Feb 20 17:19:53 1999
   *
   * @author Russ Rew
   */
  public class EsriPolygon extends EsriFeature {
    EsriPolygon()
            throws java.io.IOException {
      bounds = readBoundingBox();
      numParts = readLEInt();
      numPoints = readLEInt();
      int[] parts = new int[numParts + 1];
      for (int j = 0; j < numParts; j++) {
        parts[j] = readLEInt();
      }
      parts[numParts] = numPoints;

      if (xyPoints.length < 2 * numPoints)
        xyPoints = new double[2 * numPoints];
      readLEDoubles(xyPoints, 2 * numPoints);
      discretize(xyPoints, 2 * numPoints); // overwrites xyPoints

      /* numPoints is reduced by removing dupl. discretized points */
      numPoints = 0;
      int ixy = 0;
      int numPartsLeft = 0; // may be < numParts after eliminating 1-point parts
      for (int part = 0; part < numParts; part++) {
        int pointsInPart = parts[part + 1] - parts[part];
        /* remove duplicate discretized points in part constructor */
        GisPart gp = new EsriPart(pointsInPart, xyPoints, ixy);
        /* Only add a part if it has 2 or more points, after duplicate
      point removal */
        if (gp.getNumPoints() > 1) {
          partsList.add(gp);
          numPoints += gp.getNumPoints();
          numPartsLeft++;
        }
        ixy += 2 * pointsInPart;
      }
      numParts = numPartsLeft;
    }
  } // EsriPolygon

  /**
   * Represents a Polyline in an ESRI shapefile as a List of
   * GisParts.  A Polyline is just an ordered set of vertices of 1
   * or more parts, where a part is a connected sequence of points.
   * A river including its tributaries might be represented by a
   * Polyine, for example, where each part would be a branch of the
   * river.
   *

   * Created: Sat Feb 20 17:19:53 1999
   *
   * @author Russ Rew
   */
  public class EsriPolyline extends EsriFeature {

    EsriPolyline() throws java.io.IOException {
      bounds = readBoundingBox();
      numParts = readLEInt();
      numPoints = readLEInt();
      int[] parts = new int[numParts + 1];
      for (int j = 0; j < numParts; j++) {
        parts[j] = readLEInt();
      }
      parts[numParts] = numPoints;

      if (xyPoints.length < 2 * numPoints)
        xyPoints = new double[2 * numPoints];
      readLEDoubles(xyPoints, 2 * numPoints);
      discretize(xyPoints, 2 * numPoints); // overwrites xyPoints

      /* numPoints is reduced by removing dupl. discretized points */
      numPoints = 0;
      int ixy = 0;
      int numPartsLeft = 0; // may be < numParts after eliminating 1-point parts
      for (int part = 0; part < numParts; part++) {
        int pointsInPart = parts[part + 1] - parts[part];
        /* remove duplicate discretized points in part constructor */
        GisPart gp = new EsriPart(pointsInPart, xyPoints, ixy);
        /* Only add a part if it has 2 or more points, after duplicate
      point removal */
        if (gp.getNumPoints() > 1) {
          partsList.add(gp);
          numPoints += gp.getNumPoints();
          numPartsLeft++;
        }
        ixy += 2 * pointsInPart;
      }
      numParts = numPartsLeft;
    }
  } // EsriPolyline

  /**
   * Represents a Multipoint in an ESRI shapefile.  A
   * Multipoint is a set of 2D points.
   *

   * Created: Sat Feb 20 17:19:53 1999
   *
   * @author Russ Rew
   */
  public class EsriMultipoint extends EsriFeature {

    EsriMultipoint() throws java.io.IOException {
      bounds = readBoundingBox();
      int numPoints = readLEInt();
      if (xyPoints.length < 2 * numPoints)
        xyPoints = new double[2 * numPoints];
      readLEDoubles(xyPoints, 2 * numPoints);
      discretize(xyPoints, 2 * numPoints);
      GisPart gp = new EsriPart(numPoints, xyPoints, 0);
      partsList.add(gp);
    }
  } // EsriMultipoint

  /**
   * Represents a single point in an ESRI shapefile.
   *

   * Created: Sat Feb 20 17:19:53 1999
   *
   * @author Russ Rew
   */
  public class EsriPoint extends EsriFeature {

    EsriPoint() throws java.io.IOException {

      int numPoints = 1;
      readLEDoubles(xyPoints, 2 * numPoints);
      discretize(xyPoints, 2 * numPoints);
      GisPart gp = new EsriPart(numPoints, xyPoints, 0);
      partsList.add(gp);
      bounds = new Rectangle2D.Double(xyPoints[0], xyPoints[1], 0., 0.);
    }
  } // EsriPoint

  /**
   * A NULL shape in an ESRI shapefile.
   *

   * Created: Sat Feb 20 17:19:53 1999
   *
   * @author Russ Rew
   */
  public class EsriNull extends EsriFeature {
  }

  private static class EsriPart implements GisPart {
    private int numPoints;
    private double[] x;
    private double[] y;

    /**
     * Construct an EsriPart by eliding duplicates from array
     * representing points.
     *
     * @param num      number of input points to use
     * @param xyPoints array containing consecutive (x,y) pair for
     *                 each point.
     * @param xyOffset index in array from which to start
     */
    EsriPart(int num, double[] xyPoints, int xyOffset) {
      double xi, yi;
      /* In first pass over data, just count nonduplicated points */
      int ixy = xyOffset;
      numPoints = 1;
      double xx = xyPoints[ixy++];
      double yy = xyPoints[ixy++];
      for (int i = 1; i < num; i++) {
        xi = xyPoints[ixy++];
        yi = xyPoints[ixy++];
        if (xi != xx || yi != yy) {
          numPoints++;
          xx = xi;
          yy = yi;
        }
      }

      /* second pass: store nonduplicated points */
      x = new double[numPoints];
      y = new double[numPoints];
      ixy = xyOffset;
      int j = 0;
      x[j] = xyPoints[ixy++];
      y[j] = xyPoints[ixy++];
      xx = x[j];
      yy = y[j];
      for (int i = 1; i < num; i++) {
        xi = xyPoints[ixy++];
        yi = xyPoints[ixy++];
        if (xi != xx || yi != yy) {
          j++;
          x[j] = xi;
          y[j] = yi;
          xx = x[j];
          yy = y[j];
        }
      }
    }

    public int getNumPoints() {
      return numPoints;
    }

    public double[] getX() {
      return x;
    }

    public double[] getY() {
      return y;
    }

  }

  @Override
  public String toString() {
    Formatter f = new Formatter();
    f.format("Type=%s version=%d bounds=%s resolution=%f%n", type, version, listBounds, resolution);
    f.format("#Features = %d%n", features.size());
    if (debug) {
      for (EsriFeature ft : features)
        f.format(" %s%n", ft);
    }
    return f.toString();
  }
}


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

package ucar.nc2.dataset.conv;

import ucar.nc2.NetcdfFile;
import ucar.nc2.constants.AxisType;
import ucar.nc2.dataset.CoordSysBuilder;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dataset.VariableEnhanced;

/**
 * Created by IntelliJ IDEA.
 * User: yuanho
 * Date: Mar 17, 2010
 * Time: 2:27:03 PM
 * To change this template use File | Settings | File Templates.
 */
public class Jason2Convention  extends CoordSysBuilder {
      /**
     * @param ncfile the NetcdfFile to test
     * @return true if we think this is a Zebra file.
     */
    public static boolean isMine(NetcdfFile ncfile) {

        //   :start_time = 9.17028312E8; // double
        // :stop_time = 9.170284104681826E8; // double

        if ((null == ncfile.findDimension("time"))) {
            return false;
        }
        // if (null == ncfile.findGlobalAttribute( "start_time")) return false;
        // if (null == ncfile.findGlobalAttribute( "stop_time")) return false;

        String center = ncfile.findAttValueIgnoreCase(null, "processing_center", null);
        if( (center != null) && center.equals("ESPC")) {
          String mission = ncfile.findAttValueIgnoreCase(null, "mission_name", null);
          return mission != null && mission.equals("OSTM/Jason-2");
        } else
          return false;
    }

    /**
     * _more_
     */
    public Jason2Convention() {
        this.conventionName = "Jason2";
    }

    /**
     * _more_
     *
     * @param ncDataset _more_
     * @param v _more_
     *
     * @return _more_
     */
    protected AxisType getAxisType(NetcdfDataset ncDataset,
                                   VariableEnhanced v) {
        String name = v.getShortName();
        if (name.equals("time")) {
            return AxisType.Time;
        }
        if (name.equals("lat")) {
            return AxisType.Lat;
        }
        if (name.equals("lon")) {
            return AxisType.Lon;
        }
        // if (name.equals("xLeo") ) return AxisType.GeoX;
        // if (name.equals("yLeo") ) return AxisType.GeoY;
        if (name.equals("alt")) {
            return AxisType.Height;
        }
        return null;
    }

}


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

import ucar.nc2.constants.CF;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.constants.AxisType;
import ucar.nc2.dataset.CoordSysBuilder;
import ucar.nc2.dataset.VariableEnhanced;
import ucar.nc2.util.CancelTask;
import ucar.nc2.Variable;
import ucar.nc2.Attribute;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.units.SimpleUnit;

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

/**
 * Unidata Observation Dataset v1.0
 * Use CF for new files.
 * @see "http://www.unidata.ucar.edu/software/netcdf-java/formats/UnidataObsConvention.html"
 * @author caron
 */
public class UnidataObsConvention extends CoordSysBuilder {

  public UnidataObsConvention() {
    this.conventionName = "Unidata Observation Dataset v1.0";
  }

  /** create a NetcdfDataset out of this NetcdfFile, adding coordinates etc. */
  public void augmentDataset( NetcdfDataset ds, CancelTask cancelTask) throws IOException {

    // latitude
    if (!hasAxisType( ds, AxisType.Lat)) { // already has _CoordinateAxisType

      if ( !addAxisType( ds, "latitude", AxisType.Lat)) { // directly named

        String vname = ds.findAttValueIgnoreCase(null, "latitude_coordinate", null);
        if (!addAxisType( ds, vname, AxisType.Lat)) { // attribute named

          Variable v = hasUnits(ds, "degrees_north,degrees_N,degreesN,degree_north,degree_N,degreeN");
          if (v != null)
            addAxisType( v, AxisType.Lat); // CF-1
        }
      }
    }

    // longitude
    if (!hasAxisType( ds, AxisType.Lon)) { // already has _CoordinateAxisType

      if ( !addAxisType( ds, "longitude", AxisType.Lon)) { // directly named

        String vname = ds.findAttValueIgnoreCase(null, "longitude_coordinate", null);
        if (!addAxisType( ds, vname, AxisType.Lon)) { // attribute named

          Variable v = hasUnits(ds, "degrees_east,degrees_E,degreesE,degree_east,degree_E,degreeE");
          if (v != null)
            addAxisType( v, AxisType.Lon); // CF-1
        }
      }
    }

      // altitude
      if (!hasAxisType(ds, AxisType.Height)) { // already has _CoordinateAxisType

        if (!addAxisType(ds, "altitude", AxisType.Height)) { // directly named
          if (!addAxisType(ds, "depth", AxisType.Height)) { // directly named

            String vname = ds.findAttValueIgnoreCase(null, "altitude_coordinate", null);
            if (!addAxisType(ds, vname, AxisType.Height)) { // attribute named

              for (int i = 0; i < ds.getVariables().size(); i++) {
                VariableEnhanced ve = (VariableEnhanced) ds.getVariables().get( i );
                String positive = ds.findAttValueIgnoreCase((Variable) ve, CF.POSITIVE, null);
                if (positive != null) {
                  addAxisType((Variable) ve, AxisType.Height); // CF-1
                  break;
                }
              }
            }
          }
        }
      }

     // time
    if (!hasAxisType( ds, AxisType.Time)) { // already has _CoordinateAxisType

      if ( !addAxisType( ds, "time", AxisType.Time)) { // directly named

        String vname = ds.findAttValueIgnoreCase(null, "time_coordinate", null);
        if (!addAxisType( ds, vname, AxisType.Time)) { // attribute named

          for (int i = 0; i < ds.getVariables().size(); i++) {
            VariableEnhanced ve = (VariableEnhanced) ds.getVariables().get(i);
            String unit = ve.getUnitsString();
            if (unit == null) continue;
            if (SimpleUnit.isDateUnit(unit)) {
              addAxisType( (Variable) ve, AxisType.Time); // CF-1
              break;
            }
          }
        }
      }
    }

  }

  private boolean hasAxisType(NetcdfDataset ds, AxisType a) {
    List<Variable> varList = ds.getVariables();
    for (Variable v : varList) {
      String axisType = ds.findAttValueIgnoreCase(v, "CoordinateAxisType", null);
      if ((axisType != null) && axisType.equals(a.toString()))
        return true;
    }
    return false;
  }

  private Variable hasUnits(NetcdfDataset ds, String unitList) {
    List<Variable> varList = ds.getVariables();
    StringTokenizer stoker = new StringTokenizer(unitList, ",");
    while (stoker.hasMoreTokens()) {
      String unit = stoker.nextToken();

      for (Variable ve : varList) {
        String hasUnit = ve.getUnitsString();
        if (hasUnit == null) continue;
        if (hasUnit.equalsIgnoreCase(unit))
          return ve;
      }
    }
    return null;
  }

  private boolean addAxisType(NetcdfDataset ds, String vname, AxisType a) {
    if (vname == null) return false;
    Variable v = ds.findVariable(vname);
    if (v == null) return false;
    addAxisType( v, a);
    return true;
  }

  private void addAxisType(Variable v, AxisType a) {
    v.addAttribute( new Attribute(_Coordinate.AxisType, a.toString()));
  }

}