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

import ucar.nc2.ui.event.UIChangeEvent;
import ucar.nc2.ui.event.UIChangeListener;
import ucar.nc2.ui.widget.BAMutil;
import ucar.nc2.ui.widget.PopupMenu;
import ucar.nc2.util.ListenerManager;
import ucar.nc2.util.NamedObject;

import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;

  /**
    JTableSorted adds sorting functionality to a JTable.
    It also wraps the JTable in a JScrollPane.
    Note that JTableSorted is a JPanel, and has-a JTable.
    It throws ListSelectionEvent events when the selection changes.
    It throws a UIChangeEvent, property = "sort" just before a sort is going to happen.
  */

public class JTableSorted extends JPanel {
      // for HeaderRenderer
  static private Icon sortDownIcon = BAMutil.getIcon( "SortDown", true);
  static private Icon sortUpIcon = BAMutil.getIcon( "SortUp", true);
  static private Icon threadSortIcon = BAMutil.getIcon( "ThreadSorted", true);
  static private Icon threadUnSortIcon = BAMutil.getIcon( "ThreadUnsorted", true);

  private ArrayList list;
  private String[] colName;
  private String objectName;

  private JTable jtable;
  private JScrollPane scrollPane;
  private TableRowModel model;
  private ucar.nc2.ui.widget.PopupMenu popupMenu = null;
  private PopupAction[] acts;

  private boolean debug = false;
  private boolean sortOK = true;
  private ThreadSorter threadSorter = null;
  private int threadCol = -1;

  private ListenerManager lm;

  /**
    Constructor.

    @param colName list of column names
    @param listRT list of rows. This must contain objects that implement
      the TableRow interface. May be null or empty.
  */
  public JTableSorted(String[] colName, ArrayList listRT) {
    this( colName, listRT, false, null);
  }

  /**
    Constructor.

    @param columnName list of column names
    @param listRT  list of rows. This must contain objects that implement
      the TableRow interface. May be null or empty.
    @param enableColumnManipulation  allow columns to be added, deleted via click-right popup
    @param threadSorter  if not null, add a "thread sorting" column

  */
  public JTableSorted(String[] columnName, ArrayList listRT, boolean enableColumnManipulation,
    ThreadSorter threadSorter) {
    this.colName = columnName;
    this.list = (listRT == null) ? new ArrayList() : listRT;
    this.threadSorter = threadSorter;

      // create the ui
    jtable = new JTable();
    jtable.setDefaultRenderer(Object.class, new MyTableCellRenderer() );
    ToolTipManager.sharedInstance().registerComponent(jtable);

    model = new TableRowModel();

    setLayout(new BorderLayout());
    scrollPane = new JScrollPane(jtable);
    add(scrollPane, BorderLayout.CENTER);
    //add(jtable, BorderLayout.CENTER);

      // add a column if it has a ThreadSorter
    boolean hasThreads = (threadSorter != null);
    if (hasThreads) {
      String[] newColName = new String[ colName.length + 1];
      for (int i=0; i<colName.length; i++)
        newColName[i] = colName[i];

      threadCol = colName.length;
      newColName[threadCol] = "Threads";
      colName = newColName;
    }

    jtable.setModel( model);
    jtable.setSelectionMode( ListSelectionModel.SINGLE_SELECTION);
    jtable.setAutoResizeMode( JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS);
    setColumnWidths( null);

        // now set the header renderers
    TableColumnModel tcm = jtable.getColumnModel();
    int ncolwt = hasThreads ? jtable.getColumnCount()-1 : jtable.getColumnCount();
    for (int i=0; i<ncolwt; i++) {
      TableColumn tc = tcm.getColumn(i);
      tc.setHeaderRenderer( new SortedHeaderRenderer(colName[i], i));
    }
    if (hasThreads) {
      TableColumn tc = tcm.getColumn(ncolwt);
      tc.setHeaderRenderer( new ThreadHeaderRenderer(ncolwt));
    }

    if (enableColumnManipulation) {
      // popupMenu
      popupMenu = new PopupMenu(jtable.getTableHeader(), "Visible");
      int ncols = colName.length;
      acts = new PopupAction[ncols];
      for (int i=0; i<ncols; i++) {
        acts[i] = new PopupAction(colName[i]);
        popupMenu.addActionCheckBox( this.colName[i], acts[i], true);
      }
    }

    // set sorting behavior
    JTableHeader hdr = jtable.getTableHeader();
    hdr.addMouseListener(new MouseAdapter() {
      public void mouseClicked(MouseEvent e) {
        if (!sortOK)
          return;
        TableColumnModel tcm2 = jtable.getColumnModel();
        int colIdx = tcm2.getColumnIndexAtX(e.getX());
        int colNo = jtable.convertColumnIndexToModel(colIdx);

          // keep current selection selected
        int selidx = jtable.getSelectedRow();
        Object selected = null;
        if (selidx >= 0)
          selected = list.get( selidx);

          // notify listsners of impending sort
        if (lm.hasListeners())
          lm.sendEvent( new UIChangeEvent(this, "sort", null, new Integer(colNo)));

          // sort
        model.sort(colNo);

          /* keep current selection selected */
        if (selidx >= 0) {
          int newSelectedRow = list.indexOf( selected);
          jtable.setRowSelectionInterval(newSelectedRow, newSelectedRow);
          ensureRowIsVisible(newSelectedRow);
        }
        repaint();
      }
    });

    // event manager
    lm = new ListenerManager(
        "ucar.nc2.ui.event.UIChangeListener",
        "ucar.nc2.ui.event.UIChangeEvent",
        "processChange");

  }

  /**
    Set the state from the last saved in the PersistentStore.
    @param String name  object name
    @param PersistentStore store ok if null or empty
  *
  public void restoreStateFromStore(String objectName, PersistentStore store) {
    if (store == null)
      return;
    this.objectName = objectName;

    int [] modelIndex = (int []) store.get(objectName+"ColumnOrder" );
    if (modelIndex == null)
      return;

    // make invisible any not stored
    int ncols = jtable.getColumnCount();
    boolean [] visible = new boolean[ncols];
    for (int i=0; i<modelIndex.length; i++)
      if (modelIndex[i] < ncols)
        visible[ modelIndex[i]] = true;
    for (int i=0; i<ncols; i++)
      if (!visible[i]) {
        //System.out.println( colName[i]+" hide "+i);
        acts[i].hideColumn();
        acts[i].putValue(BAMutil.STATE, new Boolean(false));
      }

    // now set the header order
    TableColumnModel tcm = jtable.getColumnModel();
    int n = Math.min( modelIndex.length, jtable.getColumnCount());
    for (int i=0; i<n; i++) {
      TableColumn tc = tcm.getColumn(i);
      tc.setModelIndex(modelIndex[i]);
      String name = colName[modelIndex[i]];
      tc.setHeaderValue(name);
      tc.setIdentifier(name);
      if (modelIndex[i] == threadCol)
        tc.setHeaderRenderer( new ThreadHeaderRenderer(threadCol));
      else
        tc.setHeaderRenderer( new SortedHeaderRenderer(name, modelIndex[i]));
    }

    // set the column widths
    int [] size = (int []) store.get(objectName+"ColumnWidths" );
    if (size != null)
      setColumnWidths( size);
    if (debug) {
      System.out.println(objectName+" read widths = ");
      for (int i=0; i<size.length; i++)
        System.out.print(" "+size[i]);
      System.out.println();
    }

    if ( null != store.get( objectName+"SortOnCol")) {
      model.sortCol = ((Integer) store.get( objectName+"SortOnCol")).intValue();
      model.reverse = ((Boolean) store.get( objectName+"SortReverse")).booleanValue();
      model.sort();
    }
  }

  public void setFontSize( int size) {
    jtable.setFont( jtable.getFont().deriveFont( (float) size));
  } */

  /**
    Save the state in the PersistentStore passed to getState().
  *
  public void saveState(PersistentStore store) {
    if (store == null)
      return;

    int ncols = jtable.getColumnCount();
    int [] size = new int[ncols];
    int [] modelIndex = new int[ncols];

    TableColumnModel tcm = jtable.getColumnModel();
    for (int i=0; i<ncols; i++) {
      TableColumn tc = tcm.getColumn(i);
      size[i] = tc.getWidth();
      modelIndex[i] = tc.getModelIndex();
    }
    store.put( objectName+"ColumnWidths", size);
    store.put( objectName+"ColumnOrder", modelIndex);

    store.put( objectName+"SortOnCol", new Integer(model.sortCol));
    store.put( objectName+"SortReverse", new Boolean(model.reverse));

    store.put( objectName+"SortReverse", new Boolean(model.reverse));

    if (debug) {
      System.out.println(objectName+" store widths = ");
      for (int i=0; i<size.length; i++)
        System.out.print(" "+size[i]);
      System.out.println();
    }

  } */

  /**
    Sort the rowList: note rowList changed, not a copy of it.
    @param colNo sort on this column
    @param reverse if true, reverse sort
  */
   public void sort(int colNo, boolean reverse) {
    model.sort(colNo, reverse);
    jtable.setRowSelectionInterval(0, 0);
    ensureRowIsVisible(0);
  }

  /**
    Replace the rowList with this one.
    @param rowList list of rows
  */
  public void setList ( ArrayList rowList) {
    this.list = rowList;
    if (list.size() > 0)
      jtable.setRowSelectionInterval(0, 0);
    else
      jtable.clearSelection();

    model.sort();
    jtable.revalidate();
  }

  /**
    Remove elem from rowList, update the table.
    Searches for match using object identity (==)
    @param elem which element
  */
  public void removeRow ( Object elem) {
    Iterator iter = list.iterator();
    while (iter.hasNext()) {
      Object row = iter.next();
      if (row == elem) {
        iter.remove();
        break;
      }
    }
    jtable.revalidate();
  }

  /** add ListSelectionEvent listener */
  public void addListSelectionListener( ListSelectionListener l) {
    jtable.getSelectionModel().addListSelectionListener(l);
  }
  /** remove ListSelectionEvent listener */
  public void removeListSelectionListener( ListSelectionListener l) {
    jtable.getSelectionModel().removeListSelectionListener(l);
  }

  /** add UIChangeEvent listener */
  public void addUIChangeListener( UIChangeListener l) {
    lm.addListener(l);
  }
  /** remove UIChangeEvent listener */
  public void removeUIChangeListener( UIChangeListener l) {
    lm.removeListener(l);
  }

  // public int getRowCount() { return table.getRowCount(); }
  public int getSelectedRowIndex() { return jtable.getSelectedRow(); }   // for SuperComboBox
  public void setSortOK(boolean sortOK) { this.sortOK = sortOK; }       // for SuperComboBox

  /**
    Get the currently selected row.
    @return selected TableRow
  */
  public TableRow getSelected() {
    if (list.size() == 0)
      return null;
    int sel = jtable.getSelectedRow();
    if (sel >= 0)
      return (TableRow) list.get(sel);
    else
      return null;
  }

  /**
    Set the current selection to this row.
    @param row index into rowList
  */
  public void setSelected(int row) {
    if ((row < 0) || (row >= list.size()))
      return;
    if (debug) System.out.println("JTableSorted setSelected "+row);
    jtable.setRowSelectionInterval(row, row);
    ensureRowIsVisible(row);
  }

  /**
    Increment or decrement the current selection by one row.
    @param increment true=increment, false=decrement
  */
  public void incrSelected(boolean increment) {
    if (list.size() == 0)
      return;
    int curr = jtable.getSelectedRow();
    if (increment && (curr < list.size()-1))
      setSelected(curr+1);
    else if (!increment && (curr > 0))
      setSelected(curr-1);
  }

  /** Get the JTable delegate so you can do nasty things to it */
  public JTable getTable() { return jtable; }

  /** for each column, get the model index */
  public int[] getModelIndex() {
    int [] modelIndex = new int[colName.length];

    TableColumnModel tcm = jtable.getColumnModel();
    for (int i=0; i<colName.length; i++) {
      TableColumn tc = tcm.getColumn(i);
      modelIndex[i] = tc.getModelIndex();
    }

    return modelIndex;
  }

/////////////////////////////////////////////////////////////////////////////////
  private void ensureRowIsVisible(int nRow) {
    Rectangle visibleRect = jtable.getCellRect(nRow, 0, true);
    if (visibleRect != null)  {
      visibleRect.x = scrollPane.getViewport().getViewPosition().x;
      jtable.scrollRectToVisible(visibleRect);
      jtable.repaint();
    }
  }

  private void setColumnWidths(int[] sizes) {
    TableColumnModel tcm = jtable.getColumnModel();
    for (int i=0; i< jtable.getColumnCount(); i++) {
      TableColumn tc = tcm.getColumn(i);
      int maxw = ((sizes == null) || (i >= sizes.length))  ?
        model.getPreferredWidthForColumn(tc) : sizes[i];
      tc.setPreferredWidth(maxw);
    }
    //table.sizeColumnsToFit(0);     //  must be called due to a JTable bug
  }

  private void setSortCol( int sortCol, boolean reverse) {
    TableColumnModel tcm = jtable.getColumnModel();
    for (int i=0; i<jtable.getColumnCount(); i++) {
      TableColumn tc = tcm.getColumn(i);
      SortedHeaderRenderer shr = (SortedHeaderRenderer) tc.getHeaderRenderer();
      shr.setSortCol( sortCol, reverse);
    }
  }

  private class PopupAction extends AbstractAction {
    private String id;
    private TableColumn tc = null;

    PopupAction(String id) { this.id = id; }

    public void actionPerformed(ActionEvent e) {
      boolean state = ((Boolean) getValue(BAMutil.STATE)).booleanValue();
      TableColumnModel tcm = jtable.getColumnModel();

      if (state)
        tcm.addColumn(tc);
      else
        hideColumn();

      JTableSorted.this.revalidate();
      //System.out.println(id+" "+state);
    }

    public void hideColumn() {
      TableColumnModel tcm = jtable.getColumnModel();
      int idx = tcm.getColumnIndex(id);
      tc = tcm.getColumn(idx);
      tcm.removeColumn(tc);
    }
  }

  private class TableRowModel extends AbstractTableModel {
    private boolean reverse = false;
    private int sortCol = -1;

    // AbstractTableModel methods
    public int getRowCount() { return list.size(); }
    public int getColumnCount() { return colName.length; }
    public String getColumnName(int col) { return colName[col]; }
    public Object getValueAt (int row, int col) {
      TableRow selectedRow = (TableRow) list.get( row);

      if (col == threadCol) {
        if (null == threadSorter)
          return "";
        else
          return threadSorter.isTopThread(selectedRow) ? " * " : ""; // ??
      }

      return selectedRow.getValueAt(col);
    }
      // sort using current
    void sort() {
      sort(sortCol, reverse);
    }

    void sort( int sortCol) {
      if (sortCol == this.sortCol)
        reverse = (!reverse);
      else
        reverse = false;
      sort(sortCol, reverse);
    }

    void sort( int sortCol, boolean reverse) {
      this.reverse = reverse;
      if ((sortCol == threadCol) && (threadSorter != null)) {
        list = threadSorter.sort( sortCol, reverse, list);
      } else if (sortCol >= 0) {
        java.util.Collections.sort( list, new SortList(sortCol, reverse));
      }
      JTableSorted.this.setSortCol( sortCol, reverse);
      this.sortCol = sortCol; // keep track of last sort
    }

      // trying to get the auto - size right: not very successful
    public int getPreferredWidthForColumn(TableColumn col) {
      int hw = columnHeaderWidth(col);   // hw = header width
      int cw = widestCellInColumn(col);  // cw = column width

      return hw > cw ? hw : cw;
    }
    private int columnHeaderWidth(TableColumn col) {
      TableCellRenderer renderer = col.getHeaderRenderer();
      if (renderer == null) return 10;
      Component comp = renderer.getTableCellRendererComponent(
        jtable, col.getHeaderValue(), false, false, 0, 0);

      return comp.getPreferredSize().width;
    }
    private int widestCellInColumn(TableColumn col) {
      int c = col.getModelIndex(), width=0, maxw=0;

      for(int r=0; r < getRowCount(); ++r) {
        TableCellRenderer renderer = jtable.getCellRenderer(r,c);
        Component comp = renderer.getTableCellRendererComponent(
                jtable, getValueAt(r,c), false, false, r, c);
        width = comp.getPreferredSize().width;
              maxw = width > maxw ? width : maxw;
      }
      return maxw;
    }
  }

  private static class SortList implements Comparator, Serializable {
    private int col;
    private boolean reverse;

    SortList( int col, boolean reverse) {
      this.col = col;
      this.reverse = reverse;
    }

    public int compare(Object o1, Object o2) {
      TableRow row1 = (TableRow) o1;
      TableRow row2 = (TableRow) o2;
      return reverse ? row2.compare(row1, col) : row1.compare(row2, col);
    }

    public boolean equals(Object obj) { return this == obj; }
  }

  // add tooltips
  private static class MyTableCellRenderer extends javax.swing.table.DefaultTableCellRenderer {

    public Component getTableCellRendererComponent( JTable table, Object value, boolean isSelected,
        boolean hasFocus, int row, int column) {

      Component c = super.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column);

      // System.out.println(" MyTableCellRenderer comp = "+c.hashCode()+" "+c.getClass().getName());
      if ((c instanceof JComponent) && (value instanceof NamedObject)) {
        ((JComponent)c).setToolTipText(((NamedObject)value).getDescription());
      } // LOOK!! should turn tip off if there is none !!
      return c;
    }

    public Point getToolTipLocation(MouseEvent e) {
      System.out.println(" cellR getToolTipLocation "+e.getPoint());
      return e.getPoint();
    }
  }

    // add tooltips
  private static class MyJTable extends javax.swing.JTable {

    public Point getToolTipLocation(MouseEvent e) {
      // System.out.println(" JTable getToolTipLocation "+e.getPoint());
      return e.getPoint();
    }
  }

  private class SortedHeaderRenderer implements TableCellRenderer {
    int modelCol;
    Component comp;

    JPanel compPanel;
    JLabel upLabel, downLabel;
    boolean hasSortIndicator = false;
    boolean reverse = false;

    protected SortedHeaderRenderer(int modelCol) {
      this.modelCol = modelCol;
    }

    SortedHeaderRenderer(String name, int modelCol) {
      this.modelCol = modelCol;
      upLabel = new JLabel(sortUpIcon);
      downLabel = new JLabel(sortDownIcon);

      compPanel = new JPanel(new BorderLayout());
      compPanel.setBorder(new BevelBorder(BevelBorder.RAISED));
      compPanel.add(new JLabel( name), BorderLayout.CENTER);
      comp = compPanel;
    }

    void setSortCol( int sortCol, boolean reverse) {
      if (sortCol == modelCol) {

        if (!hasSortIndicator)
          compPanel.add(reverse? upLabel : downLabel, BorderLayout.EAST);
        else if (reverse != this.reverse) {
          compPanel.remove(1);
          compPanel.add(reverse? upLabel : downLabel, BorderLayout.EAST);
        }

        this.reverse = reverse;
        hasSortIndicator = true;

        //System.out.println("setSortCol on "+modelCol+" "+sortCol+" "+reverse);
      } else if (hasSortIndicator) {
        compPanel.remove(1);
        hasSortIndicator = false;
        //System.out.println("setSortCol off "+modelCol+" "+sortCol+" "+reverse);
      }
    }

    public Component getTableCellRendererComponent(JTable table, Object value,
      boolean isSelected, boolean hasFocus, int row, int column) {

      return comp;
    }

  }

  private class ThreadHeaderRenderer extends SortedHeaderRenderer {
    JPanel sort, unsort;

    ThreadHeaderRenderer(int modelCol) {
      super( modelCol);

      sort = new JPanel(new BorderLayout());
      sort.setBorder(new BevelBorder(BevelBorder.RAISED));
      sort.add(new JLabel( threadSortIcon), BorderLayout.CENTER);

      unsort = new JPanel(new BorderLayout());
      unsort.setBorder(new BevelBorder(BevelBorder.RAISED));
      unsort.add(new JLabel( threadUnSortIcon), BorderLayout.CENTER);

      comp = unsort;
    }

   void setSortCol( int sortCol, boolean reverse) {
      comp = (sortCol == modelCol) ? sort : unsort;
    }

  }

}


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

package thredds.server.radarServer2;

import ucar.nc2.constants.FeatureType;
import ucar.nc2.dt.RadialDatasetSweep;
import ucar.nc2.ft.FeatureDatasetFactoryManager;
import ucar.nc2.time.*;
import ucar.nc2.units.DateRange;
import ucar.unidata.geoloc.EarthLocation;

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Class to manage generating an inventory of radar data and providing a way
 * to query what data are available.
 *
 * @author rmay
 * @since 01/15/2015
 */

public class RadarDataInventory {
    public enum DirType {
        Station, Variable, Date
    }

    private static final long updateIntervalMsec = 600000;
    private EnumMap<DirType, Set<String>> items;
    private Path collectionDir;
    private DirectoryStructure structure;
    private String fileTimeFmt, dataFormat;
    private java.util.regex.Pattern fileTimeRegex;
    private boolean dirty;
    private CalendarDate lastUpdate;
    private int maxCrawlItems;
    private StationList stations;
    private CalendarPeriod nearestWindow, rangeAdjustment;
    private String name, description;
    private DateRange timeCoverage;
    private RadarServerConfig.RadarConfigEntry.GeoInfo geoCoverage;

    public RadarDataInventory(Path datasetRoot, int numCrawl) {
        items = new EnumMap<>(DirType.class);
        collectionDir = datasetRoot;
        structure = new DirectoryStructure(collectionDir);
        dirty = true;
        maxCrawlItems = numCrawl;
        stations = new StationList();
        nearestWindow = CalendarPeriod.of(1, CalendarPeriod.Field.Hour);
    }

    public Path getCollectionDir()
    {
        return collectionDir;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    CalendarDate getLastUpdate() {
        return this.lastUpdate;
    }

    // TODO: Can we pull this from data?
    public FeatureType getFeatureType() {
        return FeatureType.RADIAL;
    }

    public void setDataFormat(String format) {
        this.dataFormat = format;
    }

    public String getDataFormat() {
        return dataFormat;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public void setTimeCoverage(DateRange range) {
        this.timeCoverage = range;
    }

    public DateRange getTimeCoverage() {
        return this.timeCoverage;
    }

    public void setGeoCoverage(RadarServerConfig.RadarConfigEntry.GeoInfo info) {
        this.geoCoverage = info;
    }

    public RadarServerConfig.RadarConfigEntry.GeoInfo getGeoCoverage() {
        return this.geoCoverage;
    }

    public boolean needsVar() {
        return items.containsKey(DirType.Variable);
    }

    public StationList getStationList() {
        return stations;
    }

    public List<String> getVariableList() {
        return listItems(DirType.Variable);
    }

    public void setNearestWindow(CalendarPeriod pd) {
        nearestWindow = pd;
    }

    public static class DirectoryStructure {
        int maxCrawlDepth = 1;
        private static class DirEntry {
            public DirType type;
            public String fmt;
            public DirEntry(DirType type, String fmt) {
                this.type = type;
                this.fmt = fmt;
            }
        }

        private class DirectoryDateMatcher {
            // Map a directory level to a date format
            List<Integer> levels;
            String fmt;

            public DirectoryDateMatcher() {
                levels = new ArrayList<>(4);
                fmt = "";
            }

            public void add(int level, String value) {
                levels.add(level);
                fmt += value;
            }

            public SimpleDateFormat getFormat() {
                SimpleDateFormat sdf = new SimpleDateFormat(fmt);
                sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
                return sdf;
            }

            public Date getDate(Path path) {
                Path relPath = base.relativize(path);
                StringBuilder sb = new StringBuilder("");
                for (Integer l: levels) {
                    sb.append(relPath.getName(l));
                }
                try {
                    SimpleDateFormat fmt = getFormat();
                    return fmt.parse(sb.toString());
                } catch (ParseException e) {
                    return null;
                }
            }
        }

        private Path base;

        private List<DirEntry> order;
        private List<Integer> keyIndices;

        public DirectoryStructure(Path dir) {
            base = dir;
            order = new ArrayList<>();
            keyIndices = new ArrayList<>();
        }

        public void addSubDir(DirType type, String fmt) {
            if (type == DirType.Station || type == DirType.Variable) {
                maxCrawlDepth = order.size() + 1;
                keyIndices.add(order.size());
            }
            order.add(new DirEntry(type, fmt));
        }

        // Get a key for a path based on station/var
        public String getKey(Path path) {
            Path relPath = base.relativize(path);
            StringBuilder sb = new StringBuilder("");
            for (int ind: keyIndices) {
                sb.append(relPath.getName(ind));
            }
            return sb.toString();
        }

        public DirectoryDateMatcher matcher() { return new DirectoryDateMatcher(); }
    }

    public void addStationDir() {
        structure.addSubDir(DirType.Station, null);
        dirty = true;
    }

    public void addVariableDir() {
        structure.addSubDir(DirType.Variable, null);
        dirty = true;
    }

    public void addDateDir(String fmt) {
        structure.addSubDir(DirType.Date, fmt);
        CalendarPeriod adjust = findRangeAdjustment(fmt);
        if (rangeAdjustment == null) {
            rangeAdjustment = adjust;
        } else if (adjust.getConvertFactor(rangeAdjustment) > 1.0) {
            rangeAdjustment = adjust;
        }
        dirty = true;
    }

    // Trying to figure out how much to fudge the date range to compensate for
    // the fact that when parsing a path for a date, such as YYYYMMDD, we
    // have essentially truncated a portion of the date. While parsing the
    // start time the same way will give a wider start point, the end
    // ends up going the wrong way. Adding this adjustment should correct for
    // this.
    private CalendarPeriod findRangeAdjustment(String fmtString) {
        if (fmtString.contains("H") || fmtString.contains("k"))
            return CalendarPeriod.of(1, CalendarPeriod.Field.Hour);
        else if (fmtString.contains("d"))
            return CalendarPeriod.of(1, CalendarPeriod.Field.Day);
        else if (fmtString.contains("M"))
            return CalendarPeriod.of(31, CalendarPeriod.Field.Day);
        else
            return CalendarPeriod.of(366, CalendarPeriod.Field.Day);
    }

    public void addFileTime(String regex, String fmt) {
        fileTimeRegex = java.util.regex.Pattern.compile(regex);
        fileTimeFmt = fmt;
    }

    private void findItems(Path start, int level) {
        // Add each entry from this level to the appropriate item box
        // and recurse
        if (level >= structure.order.size() || level >= structure.maxCrawlDepth)
            return;

        DirectoryStructure.DirEntry entry = structure.order.get(level);
        Set<String> values;

        if (!items.containsKey(entry.type)) {
            values = new TreeSet<>();
            items.put(entry.type, values);
        } else {
            values = items.get(entry.type);
        }

        int crawled = 0;
        try(DirectoryStream<Path> dirStream = Files.newDirectoryStream(start)) {
            for (Path p : dirStream) {
                if (Files.isDirectory(p)) {
                    String item = p.getFileName().toString();
                    values.add(item);
                    // Try to grab station info from some file
                    // TODO: Fix or remove
//                    if (entry.type == DirType.Station)
//                        updateStations(item, p);
                    if (crawled < maxCrawlItems) {
                        findItems(p, level + 1);
                        ++crawled;
                    }
                }
            }
        } catch (IOException e) {
            System.out.println("findItems(): Error reading directory: " + start.toString());
        }
    }

    private class StationVisitor extends SimpleFileVisitor<Path> {
        @Override
        public FileVisitResult visitFile(Path file,
                                         BasicFileAttributes attrs) {
            try (RadialDatasetSweep rds = (RadialDatasetSweep)
                    FeatureDatasetFactoryManager.open(FeatureType.RADIAL,
                            file.toString(), null, new Formatter())){
                if (rds == null) {
                    return FileVisitResult.CONTINUE;
                }

                EarthLocation loc = rds.getCommonOrigin();
                if (loc == null) {
                    return FileVisitResult.CONTINUE;
                }

                StationList.Station added = stations.addStation(
                        rds.getRadarID(), loc.getLatLon());
                added.setElevation(loc.getAltitude());
                added.setName(rds.getRadarName());
                return FileVisitResult.TERMINATE;
            } catch (IOException e) {
                return FileVisitResult.CONTINUE;
            }
        }
    }

    private void updateStations(String station, Path path) {
        StationList.Station match = stations.get(station);
        if (match == null) {
            try {
                Files.walkFileTree(path, new StationVisitor());
            } catch (IOException e) {
                System.out.println("Error walking to find station info: " +
                        station);
            }
        }
    }

    private void update() {
        if (dirty || timeToUpdate()) {
            findItems(structure.base, 0);
            dirty = false;
            lastUpdate = CalendarDate.present();
        }
    }

    boolean timeToUpdate() {
        // See if it's been more than enough time since the last update
        CalendarDate now = CalendarDate.present();
        return now.getDifferenceInMsecs(lastUpdate) > updateIntervalMsec;
    }

    public List<String> listItems(DirType type) {
        update();
        Set<String> vals = items.get(type);
        if (vals == null) {
            return new ArrayList<>();
        } else {
            return new ArrayList<>(vals);
        }
    }

    public Query newQuery() {
        update();
        return new Query();
    }

    public class Query {
        public class QueryResultItem {
            private QueryResultItem(Path f, CalendarDate cd)
            {
                file = f;
                time = cd;
            }
            public Path file;
            public CalendarDate time;
        }

        private EnumMap<DirType, List<Object>> q;

        public Query()
        {
            q = new EnumMap<>(DirType.class);
        }

        public void addCriteria(DirType type, Object val)
        {
            List<Object> curVals = q.get(type);
            if (curVals == null)
            {
                curVals = new ArrayList<>();
                q.put(type, curVals);
            }
            curVals.add(val);
        }

        public void addStation(String stID) {
            addCriteria(DirType.Station, stID);
        }

        public void addVariable(String varName) {
            addCriteria(DirType.Variable, varName);
        }

        public void addDateRange(CalendarDateRange range) {
            addCriteria(DirType.Date, range);
        }

        private CalendarDateRange rangeFromFormat(SimpleDateFormat fmt,
                                                  CalendarDateRange range) {
            if (range == null) return null;
            CalendarDate newStart = reparseDate(fmt, range.getStart());
            CalendarDate newEnd = reparseDate(fmt, range.getEnd());
            return CalendarDateRange.of(newStart, newEnd.add(rangeAdjustment));
        }

        private CalendarDate reparseDate(SimpleDateFormat fmt, CalendarDate d) {
            try {
                Date newDate = fmt.parse(fmt.format(d.toDate()));
                return CalendarDate.of(newDate);
            } catch (ParseException e) {
                return d; // Better than just returning null
            }
        }

        private boolean checkDate(CalendarDateRange range, CalendarDate d) {
            // Range null indicates no time filter
            return range == null || range.includes(d);
        }

        public Collection<QueryResultItem> results() {
            List<Path> results = new ArrayList<>();
            DirectoryStructure.DirectoryDateMatcher matcher = structure.matcher();
            results.add(structure.base);

            // Grab the range of dates up front
            List<Object> dates = q.get(DirType.Date);
            CalendarDateRange range = (CalendarDateRange) dates.get(0);

            // If we're given a single point for time, signifying we are looking
            // for the file nearest, turn it into a window for query purposes.
            if (range != null && range.isPoint()) {
                range = CalendarDateRange.of(
                        range.getStart().subtract(nearestWindow),
                        range.getEnd().add(nearestWindow));
            }

            // Loop over each entry in the directory structure and handle
            // as appropriate. For stn/var we check if the desired item
            // exists. For dates, add the items that are within the filter
            for (int i = 0; i < structure.order.size(); ++i) {
                DirectoryStructure.DirEntry entry = structure.order.get(i);
                List<Path> newResults = new ArrayList<>();
                List<Object> queryItem = q.get(entry.type);
                switch (entry.type) {
                    // Loop over results and add subdirs that are within the
                    // appropriate range, which is found by successively adding
                    // the date format to a matcher string
                    case Date:
                        matcher.add(i, entry.fmt);
                        SimpleDateFormat fmt = matcher.getFormat();
                        CalendarDateRange dirRange = rangeFromFormat(fmt, range);

                        for (Path p : results)
                            try(DirectoryStream<Path> dirStream = Files.newDirectoryStream(p)) {
                                for (Path sub : dirStream) {
                                    Date d = matcher.getDate(sub);
                                    if (d != null && checkDate(dirRange, CalendarDate.of(d)))
                                        newResults.add(sub);
                                }
                            } catch (IOException e) {
                                System.out.println("results(): Error reading dir: " + p.toString());
                            }
                        break;

                    // Add to results and prune non-existent
                    // Add station/var name to matcher string
                    case Station:
                    case Variable:
                    default:
                        for (Object next: queryItem) {
                            for (Path p : results) {
                                Path nextPath = p.resolve(next.toString());
                                if (Files.exists(nextPath))
                                    newResults.add(nextPath);
                            }
                        }
                }
                results = newResults;
            }

            // Now get the contents of the remaining directories
            Collection<QueryResultItem> filteredFiles = new ArrayList<>();
            for(Path p : results) {
                try(DirectoryStream<Path> dirStream = Files.newDirectoryStream(p)) {
                    for (Path f: dirStream) {
                        java.util.regex.Matcher regexMatcher = fileTimeRegex.matcher(f.toString());
                        if (!regexMatcher.find()) continue;

                        try {
                            SimpleDateFormat fmt = new SimpleDateFormat(fileTimeFmt);
                            fmt.setTimeZone(TimeZone.getTimeZone("UTC"));
                            Date d = fmt.parse(regexMatcher.group());
                            if (d != null) {
                                CalendarDate cd = CalendarDate.of(d);
                                if (checkDate(range, cd))
                                    filteredFiles.add(new QueryResultItem(f, cd));
                            }
                        } catch (ParseException e) {
                            // Ignore file
                        }
                    }
                } catch (IOException e) {
                    System.out.println("results(): Error getting files for: " + p.toString());
                }
            }

            // If only looking for nearest, perform that reduction now
            CalendarDateRange originalRange = (CalendarDateRange) dates.get(0);
            if (originalRange != null && originalRange.isPoint()) {
                Map<String, Long> offsets = new TreeMap<>();
                Map<String, QueryResultItem> best = new TreeMap<>();

                for (QueryResultItem it: filteredFiles) {
                    String key = structure.getKey(it.file);
                    Long offset = offsets.get(key);
                    if (offset == null) offset = Long.MAX_VALUE;

                    long check = Math.abs(it.time.getDifferenceInMsecs(
                            originalRange.getStart()));
                    if (check < offset) {
                        offsets.put(key, check);
                        best.put(key, it);
                    }
                }

                // Return the best matches in the map
                filteredFiles = best.values();
            }

            return filteredFiles;
        }
    }
}


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

package ucar.nc2.ft.point.bufr;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import org.jdom2.Element;
import thredds.client.catalog.Catalog;
import ucar.ma2.Array;
import ucar.ma2.ArraySequence;
import ucar.ma2.DataType;
import ucar.ma2.StructureData;
import ucar.ma2.StructureDataComposite;
import ucar.ma2.StructureDataIterator;
import ucar.ma2.StructureDataProxy;
import ucar.ma2.StructureDataW;
import ucar.ma2.StructureMembers;
import ucar.nc2.Attribute;
import ucar.nc2.Structure;
import ucar.nc2.Variable;
import ucar.nc2.VariableSimpleIF;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dataset.SequenceDS;
import ucar.nc2.dataset.VariableDS;
import ucar.nc2.ft.FeatureDataset;
import ucar.nc2.ft.FeatureDatasetFactory;
import ucar.nc2.ft.PointFeature;
import ucar.nc2.ft.PointFeatureCollection;
import ucar.nc2.ft.PointFeatureIterator;
import ucar.nc2.ft.point.*;
import ucar.nc2.iosp.IOServiceProvider;
import ucar.nc2.iosp.bufr.BufrIosp2;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.time.CalendarDateUnit;
import ucar.nc2.util.CancelTask;
import ucar.nc2.util.Indent;
import ucar.unidata.geoloc.EarthLocation;
import ucar.unidata.geoloc.LatLonRect;

/**
 * Use BufrConfig to make BUFR files into PointFeatureDataset
 *
 * @author caron
 * @since 8/14/13
 */
public class BufrFeatureDatasetFactory implements FeatureDatasetFactory {
  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BufrFeatureDatasetFactory.class);
  static private CalendarDateUnit bufrDateUnits = CalendarDateUnit.of(null, "msecs since 1970-01-01T00:00:00");
  static private String bufrAltUnits = "m"; // LOOK fake

  @Override
  public Object isMine(FeatureType wantFeatureType, NetcdfDataset ncd, Formatter errlog) throws IOException {
    IOServiceProvider iosp = ncd.getIosp();
    return (iosp != null && iosp instanceof BufrIosp2) ? true : null;
  }

  @Override
  public FeatureType[] getFeatureTypes() {
    return new FeatureType[]{FeatureType.ANY_POINT};
  }

  @Override
  public FeatureDataset open(FeatureType ftype, NetcdfDataset ncd, Object analysis, CancelTask task, Formatter errlog) throws IOException {
    /* BufrIosp2 iosp = (BufrIosp2) ncd.getIosp();
    BufrConfig config = iosp.getConfig();
    Formatter f = new Formatter();
    config.show(f);
    System.out.printf("%s%n", f);

    Element iospParam = iosp.getElem();
    if (iospParam != null) {
      Element parent = iospParam.getChild("bufr2nc", NcMLReader.ncNS);
      show(parent, new Indent(2));

      processSeq((Structure) ncd.findVariable(BufrIosp2.obsRecord), parent);
    }  */

    // must have an index file
    File indexFile = BufrCdmIndex.calcIndexFile(ncd.getLocation());
    if (indexFile == null) return null;

    BufrCdmIndex index = BufrCdmIndex.readIndex(indexFile.getPath());
    return new BufrStationDataset(ncd, index);
  }

  private void show(Element parent, Indent indent) {
    if (parent == null) return;
    for (Element child : parent.getChildren("fld", Catalog.ncmlNS)) {
      String idx = child.getAttributeValue("idx");
      String fxy = child.getAttributeValue("fxy");
      String name = child.getAttributeValue("name");
      String action = child.getAttributeValue("action");
      System.out.printf("%sidx='%s' fxy='%s' name='%s' action='%s'%n", indent, idx, fxy, name, action);
      indent.incr();
      show(child, indent);
      indent.decr();
    }
  }

  private void processSeq(Structure struct, Element parent) throws IOException {
    if (parent == null || struct == null) return;
    List<Variable> vars = struct.getVariables();
    for (Element child : parent.getChildren("fld", Catalog.ncmlNS)) {
      String idxS = child.getAttributeValue("idx");
      int idx = Integer.parseInt(idxS);
      if (idx < 0 || idx >= vars.size()) {
        log.error("Bad index = %s", child);
        continue;
      }
      Variable want = vars.get(idx);
      struct.removeMemberVariable(want);
      System.out.printf("removed %s%n", want);
    }
  }

  private static class BufrStationDataset extends PointDatasetImpl {
    private Munge munger;
    private BufrCdmIndex index;
    private SequenceDS obs;

    private BufrStationDataset(NetcdfDataset ncfile, BufrCdmIndex index) {
      super(ncfile, FeatureType.STATION);
      this.index = index;

       // create the list of data variables
      munger = new Munge();
      obs = (SequenceDS) ncfile.findVariable(BufrIosp2.obsRecord);
      this.dataVariables = munger.makeDataVariables(index, obs);

      BufrStationCollection bufrCollection = new BufrStationCollection(ncfile.getLocation());
      setPointFeatureCollection(bufrCollection);

      CalendarDateRange dateRange = CalendarDateRange.of(CalendarDate.of(index.start), CalendarDate.of(index.end));
      setDateRange(dateRange);
    }

    @Override
    public FeatureType getFeatureType() {
      return FeatureType.STATION;
    }

    @Override
    public void getDetailInfo(java.util.Formatter sf) {
      super.getDetailInfo(sf);
      index.showIndex(sf);
    }

    private class BufrStationCollection extends StationTimeSeriesCollectionImpl {
      StandardFields.StandardFieldsFromStructure extract;

      private BufrStationCollection(String name) {
        super(name, null, null);

        // need  the center id to match the standard fields
        Attribute centerAtt = netcdfDataset.findGlobalAttribute(BufrIosp2.centerId);
        int center = (centerAtt == null) ? 0 : centerAtt.getNumericValue().intValue();
        this.extract = new StandardFields.StandardFieldsFromStructure(center, obs);

        try {
          this.timeUnit = bufrDateUnits;
        } catch (Exception e) {
          e.printStackTrace();  //cant happen
        }

        this.altUnits = "m"; // LOOK fake units
      }

      @Override
      protected StationHelper createStationHelper() throws IOException {
        StationHelper stationHelper = new StationHelper();
        for (BufrCdmIndexProto.Station s : index.stations)
          stationHelper.addStation(new BufrStation(s));

        return stationHelper;
      }

      private class BufrStation extends StationTimeSeriesFeatureImpl {
        private BufrStation(BufrCdmIndexProto.Station proto) {
          super(proto.getId(), proto.getDesc(), proto.getWmoId(), proto.getLat(), proto.getLon(), proto.getAlt(),
                  bufrDateUnits, bufrAltUnits, proto.getCount(), StructureData.EMPTY);
        }

        @Override
        public PointFeatureIterator getPointFeatureIterator() throws IOException {
          return new BufrStationIterator(obs.getStructureIterator(), null);
        }

        @Nonnull
        @Override
        public StructureData getFeatureData() throws IOException {
          return StructureData.EMPTY;
        }

        // iterates over the records for this station
        public class BufrStationIterator extends PointIteratorFromStructureData {
          public BufrStationIterator(StructureDataIterator structIter, PointFeatureIterator.Filter filter) throws IOException {
            super(structIter, filter);
          }

          @Override
          protected PointFeature makeFeature(int recnum, StructureData sdata) throws IOException {
            extract.extract(sdata);
            String stationId = extract.getStationId();
            //System.out.printf("  '%s' '%s' (%d) %n", s.getName(), stationId, countRecords++);
            //if (count > 10) return null;
            if (!stationId.equals(s.getName()))
              return null;
            CalendarDate date = extract.makeCalendarDate();
            return new BufrStationPoint(s, date.getMillis(), 0, munger.munge(sdata));  // LOOK  obsTime, nomTime
          }
        }

        public class BufrStationPoint extends PointFeatureImpl implements StationFeatureHas {
          StructureData sdata;

          public BufrStationPoint(EarthLocation location, double obsTime, double nomTime, StructureData sdata) {
            super(BufrStation.this, location, obsTime, nomTime, bufrDateUnits);
            this.sdata = sdata;
          }

          @Nonnull
          @Override
          public StructureData getDataAll() throws IOException {
            return sdata;
          }
          @Nonnull
          @Override
           public StructureData getFeatureData() throws IOException {
             return sdata;
           }

          @Override
          public StationFeature getStationFeature() {
            return BufrStation.this;
          }
        }
      }

      // flatten into a PointFeatureCollection
      // if empty, may return null
      @Override
      public PointFeatureCollection flatten(LatLonRect boundingBox, CalendarDateRange dateRange) throws IOException {
        return new BufrPointFeatureCollection(boundingBox, dateRange);
      }

      private class BufrPointFeatureCollection extends PointCollectionImpl {
        StationHelper stationsWanted;
        PointFeatureIterator.Filter filter;

        BufrPointFeatureCollection(LatLonRect boundingBox, CalendarDateRange dateRange) throws IOException {
          super("BufrPointFeatureCollection", bufrDateUnits, bufrAltUnits);
          setBoundingBox(boundingBox);
          if (dateRange != null) {
            getInfo();
            info.setCalendarDateRange(dateRange);
          }
          createStationHelper();
          stationsWanted = getStationHelper().subset(boundingBox);
          if (dateRange != null) filter = new PointIteratorFiltered.SpaceAndTimeFilter(null, dateRange);
        }

        @Override
        public PointFeatureIterator getPointFeatureIterator() throws IOException {
          return new BufrRecordIterator(obs.getStructureIterator(), filter);
        }

        // iterates once over all the records
        public class BufrRecordIterator extends PointIteratorFromStructureData {
          int countHere = 0;

          public BufrRecordIterator(StructureDataIterator structIter, PointFeatureIterator.Filter filter) throws IOException {
            super(structIter, filter);
          }

          @Override
          protected PointFeature makeFeature(int recnum, StructureData sdata) throws IOException {
            extract.extract(sdata);
            String stationId = extract.getStationId();
            StationFeature want = stationsWanted.getStation(stationId);
            if (want == null)
              return null;
            CalendarDate date = extract.makeCalendarDate();
            countHere++;
            return new BufrPoint(want, date.getMillis(), 0, munger.munge(sdata));
          }

          @Override
          public void close() {
            System.out.printf("BufrRecordIterator passed %d features super claims %d%n", countHere, getInfo().nfeatures);
            super.close();
          }

        }

        public class BufrPoint extends PointFeatureImpl implements StationPointFeature {
          StructureData sdata;

          public BufrPoint(StationFeature want, double obsTime, double nomTime, StructureData sdata) {
            super(BufrPointFeatureCollection.this, want, obsTime, nomTime, bufrDateUnits);
            this.sdata = sdata;
          }

          @Nonnull
          @Override
          public StructureData getDataAll() throws IOException {
            return sdata;
          }
          @Nonnull
          @Override
          public StructureData getFeatureData() throws IOException {
             return sdata;
           }

          @Override
          public StationFeature getStation() {
            return (StationFeature) location;
          }
        }
      }

    }
  }

  private static class Action {
    BufrCdmIndexProto.FldAction what;
    private Action(BufrCdmIndexProto.FldAction what) {
      this.what = what;
    }
  }

  private static class Munge {
    String sdataName;
    boolean needed;
    protected Map<String, Action> actions = new HashMap<>(32);
    protected Map<String, StructureData> missingData = new HashMap<>(32);
    protected Map<String, VariableDS> vars = new HashMap<>(32);

    List<VariableSimpleIF> makeDataVariables(BufrCdmIndex index, Structure obs) {
      this.sdataName = obs.getShortName()+"Munged";

      List<Variable> members = obs.getVariables();
      List<VariableSimpleIF> result = new ArrayList<>(members.size());

      List<BufrCdmIndexProto.Field> flds = index.root.getFldsList();
      int count = 0;
      for (Variable v : members) {
        BufrCdmIndexProto.Field fld = flds.get(count++);
        if (fld.getAction() != null && fld.getAction() != BufrCdmIndexProto.FldAction.none) {
          needed = true;
          Action act = new Action(fld.getAction());
          actions.put(v.getShortName(), act);

          if (fld.getAction() == BufrCdmIndexProto.FldAction.remove) {
            continue; // skip

          } else if (fld.getAction() == BufrCdmIndexProto.FldAction.asMissing) {
            // promote the children
            Structure s = (Structure) v;
            for (Variable child : s.getVariables()) {
              result.add(child);
              vars.put(child.getShortName(), (VariableDS) child); // track ones we may have to create missing values for
            }
            continue;
          }
        }

        if (v.getDataType() == DataType.SEQUENCE) continue;
        result.add(v);
      }
      return result;
    }

    StructureData munge(StructureData org) throws IOException {
      return needed ? new StructureDataMunged2(org) : org;
    }

    class StructureDataMunged extends StructureDataProxy {

      StructureDataMunged(StructureData sdata) throws IOException {
        super(sdata);

        // munge the StructureMembers
        StructureMembers sm = new StructureMembers(sdataName);

        for (StructureMembers.Member m : sdata.getMembers()) {
          Action act = actions.get(m.getName());
          if (act == null) {
            sm.addMember(m);

          } else if (act.what == BufrCdmIndexProto.FldAction.remove) {
            continue;

          } else if (act.what == BufrCdmIndexProto.FldAction.asMissing) {
            ArraySequence seq = sdata.getArraySequence(m);
            StructureDataIterator iter = seq.getStructureDataIterator();
            if (!iter.hasNext()) {
              for (StructureMembers.Member childMember : seq.getMembers()) {
                 sm.addMember(childMember); // LOOK ??
               }
            } else {
              StructureData childStruct = iter.next();
              for (StructureMembers.Member childMember : childStruct.getMembers()) {
                sm.addMember(childMember); // LOOK ??
              }

            }

          } else {
            sm.addMember(m);
          }
        }

        this.members = sm;
      }
    } // StructureDataMunged

    class StructureDataMunged2 extends StructureDataComposite {

      StructureDataMunged2(StructureData sdata) throws IOException {

        add(sdata);
        for (StructureMembers.Member m : sdata.getMembers()) {
          Action act = actions.get(m.getName());
          if (act == null) {
            continue;

          } else if (act.what == BufrCdmIndexProto.FldAction.remove) {
            this.members.hideMember(m) ;

          } else if (act.what == BufrCdmIndexProto.FldAction.asMissing) {  // 0 or 1
            int pos = this.members.hideMember(m) ;
            ArraySequence seq = sdata.getArraySequence(m);
            StructureDataIterator iter = seq.getStructureDataIterator();
            if (iter.hasNext()) {
              add(pos, iter.next());
            } else {
              // missing data
              add(pos, makeMissing(m, seq));
            }
          }
        }
      }
    }

    StructureData makeMissing(StructureMembers.Member seqm, ArraySequence seq) {
      StructureData result = missingData.get(seqm.getName());
      if (result != null) return result;

      StructureMembers sm = new StructureMembers(seq.getStructureMembers());
      StructureDataW resultW = new StructureDataW(sm);
      for (StructureMembers.Member m : sm.getMembers()) {
        VariableDS var = vars.get(m.getName());
        Array missingData = var.getMissingDataArray(m.getShape());
        resultW.setMemberData(m, missingData);
      }

      missingData.put(seqm.getName(), resultW);
      return resultW;
    }

  } // Munge

}


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

import ucar.nc2.ui.event.ActionSourceListener;
import ucar.nc2.dataset.*;

import ucar.nc2.ui.event.ActionValueEvent;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

/**
 * Slider for Vertical scale
 *
 * @author caron
 */

public class VertScaleSlider extends JPanel {
  private JSlider slider;

    // event management
  private ActionSourceListener actionSource;
  private String actionName = "level";
  private boolean eventOK = true;
  private int incrY = 1;

    // state
  private int currentIdx = -1;
  private double min, max, scale = 1.0;
  private CoordinateAxis1D zAxis;

  private static boolean debugEvent = false, debugLevels = false;

  public VertScaleSlider() {

    setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
    slider = new JSlider(JSlider.VERTICAL, 0, 100, 0);
    //slider.setPaintTrack(false);
    //s.setMajorTickSpacing(20);
    //s.setMinorTickSpacing(5);
    slider.setBorder(BorderFactory.createEmptyBorder(0,0,0,10));

      // listen for changes from user manupulation
    slider.addChangeListener(e -> {
        if (eventOK && (zAxis != null) && !slider.getValueIsAdjusting()) {
          int pos = slider.getValue();
          int idx = slider2index(pos);
          if (idx == currentIdx) return;
          currentIdx = idx;

              // gotta do this after the dust settles
          SwingUtilities.invokeLater(new Runnable() {
            public void run() {
              setSelectedIndex( currentIdx);
              actionSource.fireActionValueEvent(actionName, zAxis.getCoordName(currentIdx));
            } // run
          }); // invokeLater

        } // eventPOk
    }); //add ChangeListener

      // listen for outside changes
    actionSource = new ActionSourceListener(actionName) {
      public void actionPerformed( ActionValueEvent e) {
        if (debugEvent) System.out.println(" actionSource event "+e);
        setSelectedByName( e.getValue().toString());
      }
    };

        // catch resize events on the slider
    slider.addComponentListener( new ComponentAdapter() {
      public void componentResized( ComponentEvent e) {
        setLabels();
      }
    });

    //this.add(Box.createRigidArea(10));
    this.add(slider);
    //this.add(Box.createRigidArea(10));
  }

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

  public void setLevels( ucar.nc2.dt.GridCoordSystem gcs, int current) {
    this.zAxis = gcs.getVerticalAxis();
    if (zAxis == null) {
      slider.setEnabled( false);
      return;
    }
      // set up the slider and conversion
    slider.setEnabled( true);
    slider.setInverted( !gcs.isZPositive());
    slider.setToolTipText( zAxis.getUnitsString() );
    setSelectedIndex(current);

    int n = (int) zAxis.getSize();
    if (zAxis.isContiguous()) {
      min = Math.min(zAxis.getCoordEdge(0), zAxis.getCoordEdge(n));
      max = Math.max(zAxis.getCoordEdge(0), zAxis.getCoordEdge(n));
    } else {
      min = zAxis.getMinValue();
      max = zAxis.getMaxValue();
    }

    setLabels();
    slider.setPaintLabels( true );
  }

  private void setLabels() {
    Rectangle bounds = slider.getBounds();
    if (debugEvent) System.out.println(" setLevels Bounds= "+bounds);

    double h = (bounds.getHeight() > 0) ? bounds.getHeight() : 100.0;
    double wh = (max - min) > 0.0 ? (max - min) : 1.0;
    scale = 100.0 / wh;
    double slider2pixel = h/100.0;

    Font font = slider.getFont();
    FontMetrics fontMetrics = Toolkit.getDefaultToolkit().getFontMetrics( font);
    if (fontMetrics != null)
      incrY = fontMetrics.getAscent(); // + fontMetrics.getDescent();
    if (debugEvent) System.out.println(" scale= "+scale+ " incrY = "+incrY);

    java.util.Hashtable labelTable = new java.util.Hashtable();

    if (zAxis == null) return;
    int n = (int) zAxis.getSize();
    int last = world2slider(zAxis.getCoordValue(n-1));  // always
    labelTable.put( new Integer( last), new JLabel(zAxis.getCoordName(n-1)) ); // always
    int next = world2slider(zAxis.getCoordValue(0));
    labelTable.put( new Integer( next), new JLabel(zAxis.getCoordName(0)) ); // always
    if (debugLevels) System.out.println("level = "+slider2pixel*next+" "+ zAxis.getCoordName(0)+" added ");

    for (int i=1; i< n-1; i++) {
      int ival = world2slider(zAxis.getCoordValue(i));
      if (debugLevels) System.out.println("level = "+slider2pixel*ival+" "+ zAxis.getCoordName(i));
      if ((slider2pixel*Math.abs(ival-last) > incrY) &&
          (slider2pixel*Math.abs(ival-next) > incrY)) {
        labelTable.put( new Integer( ival), new JLabel(zAxis.getCoordName(i)) );
        next = ival;
        if (debugLevels) System.out.println("  added ");
      }
    }
    if (debugLevels) System.out.println("level = "+slider2pixel*last+" "+ zAxis.getCoordName(n-1)+" added ");

    slider.setLabelTable( labelTable );
  }

  private int world2slider( double val) {
    return ((int) (scale * (val - min)));
  }

  private double slider2world( int pval) {
    return pval/scale + min;
  }

  private int pos = -1; // optimization
  private int slider2index( int pval) {
    pos = zAxis.findCoordElement(slider2world(pval), pos);
    return pos;
  }

  private void setSelectedByName( String name) {
    if (zAxis == null) return;
    for (int i=0; i< zAxis.getSize(); i++)
      if (name.equals(zAxis.getCoordName(i))) {
        setSelectedIndex(i);
        return;
      }
    System.out.println("ERROR VertScaleSlider cant find = "+name);
  }

      // set current value - no event
  private void setSelectedIndex( int idx) {
    if (zAxis == null)
      return;
    eventOK = false;
    currentIdx = idx;
    slider.setValue( world2slider(zAxis.getCoordValue(currentIdx)));
    eventOK = true;
  }

}