/*
Copyright (C) 2002  Richard Eigenmann.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or any later version. This program is distributed
in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
more details. You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
The license is in gpl.txt.
See http://www.gnu.org/copyleft/gpl.html for the details.
*/
package ucar.nc2.ui.image;

import org.imgscalr.Scalr;

import javax.imageio.*;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Vector;

/*
ScalablePicture.java:  class that can load and save images

Copyright (C) 2002  Richard Eigenmann.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or any later version. This program is distributed
in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
more details. You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
The license is in gpl.txt.
See http://www.gnu.org/copyleft/gpl.html for the details.
*/

/**
 * a class to load and scale an image either immediately or in a seperate thread.
 */

public class ScalablePicture implements SourcePictureListener {
  /**
   * the source picture for the scalable picture
   */
  public SourcePicture sourcePicture;// = new SourcePicture();

  /**
   * The scaled version of the image
   */
  public BufferedImage scaledPicture = null;

  /**
   * The scaling factor
   */
  private double ScaleFactor;

  /**
   * the URL of the picture
   */
  public URL imageUrl = null;

  /**
   * variable to track the status of the picture
   */
  private int pictureStatusCode;

  /**
   * variable to compose te status message
   */
  private String pictureStatusMessage;

  /**
   * variable to record the size of the box that the scaled image must fit into.
   */
  private Dimension TargetSize;

  /**
   * status code used to signal that the picture is not loaded
   */
  public static final int UNINITIALISED = SourcePicture.LOADING_COMPLETED + 1;

  /**
   * status code used to signal that the picture is cleaning up memory
   */
  public static final int GARBAGE_COLLECTION = UNINITIALISED + 1;

  /**
   * status code used to signal that the thread is loading the image
   */
  public static final int LOADING = GARBAGE_COLLECTION + 1;

  /**
   * status code used to signal that the thread has finished loading the image
   */
  public static final int LOADED = LOADING + 1;

  /**
   * status code used to signal that the thread has loaded the tread is scaling the image
   */
  public static final int SCALING = LOADED + 1;

  /**
   * status code used to signal that the image is available.
   */
  public static final int READY = SCALING + 1;

  /**
   * status code used to signal that there was an error
   */
  public static final int ERROR = READY + 1;

  /**
   * the object to notify when the image operation changes the status.
   */
  private Vector scalablePictureStatusListeners = new Vector();

  /**
   * the quality with which the JPG pictures shall be written. 0 means poor 1 means great.
   */
  public float jpgQuality = 0.8f;

  /**
   * Rendering Hints object for good quality.
   */
  private static RenderingHints rh_quality = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

  /**
   * Rendering Hints object for using the bicubic method.
   */
  private static RenderingHints rh_bicubic = new RenderingHints(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);

  /**
   * flag that indicates that the image should be scaled after
   * a status message is received from the SourcePicture that the
   * picture was loaded.
   */
  public boolean scaleAfterLoad = false;

  /**
   * Constructor
   */
  public ScalablePicture() {
    setStatus(UNINITIALISED, "Creating uninitialised ScalablePicture object.");
    setScaleFactor((double) 1);
  }

  /**
   * method to invoke with a filename or URL of a picture that is to be loaded and scaled in
   * a new thread. This is handy to update the screen while the loading chuggs along in the background.
   * Make sure you invoked setScaleFactor or setScaleSize before
   * invoking this method.
   *

   * Step 1: Am I already loading what I need somewhere?
   * If yes -> use it.
   * Has it finished loading?
   * If no -> wait for it
   * If yes -> use it
   * Else -> load it
   *
   * @param priority The Thread priority
   * @param  imageUrl  The URL of the image you want to load
   * @param  rotation  The rotation 0-360 that the image should be put through
   * after loading.
   */
  public void loadAndScalePictureInThread(URL imageUrl, int priority, double rotation) {
    this.imageUrl = imageUrl;

    boolean alreadyLoading = false;
    Tools.log("ScalablePicture.loadAndScalePictureInThread: checking if picture " + imageUrl + " is already being loaded.");
    if ((sourcePicture != null) && (sourcePicture.getUrl().toString().equals(imageUrl.toString()))) {
      Tools.log("ScalablePicture.loadAndScalePictureInThread: the SourcePicture is already loading the sourcePictureimage");
      alreadyLoading = true;
    } else if (PictureCache.isInCache(imageUrl)) {
      // in case the old image has a listener connected remove it
      //  fist time round the sourcePicture is still null therefore the if.
      if (sourcePicture != null) sourcePicture.removeListener(this);

      sourcePicture = PictureCache.getSourcePicture(imageUrl);
      String status = sourcePicture.getStatusMessage();
      if (status == null) status = "";
      Tools.log("ScalablePicture.loadAndScalePictureInThread: Picture in cache! Status: " + status);

      if (sourcePicture.getRotation() == rotation) {
        alreadyLoading = true;
        Tools.log("ScalablePicture.loadAndScalePictureInThread: Picture was even rotated to the correct angle!");
      } else {
        alreadyLoading = false;
        Tools.log("ScalablePicture.loadAndScalePictureInThread: Picture was in cache but with wrong rotation. Forcing reload.");
      }
    }

    if (alreadyLoading) {
      switch (sourcePicture.getStatusCode()) {
        case SourcePicture.UNINITIALISED:
          alreadyLoading = false;
          Tools.log("ScalablePicture.loadAndScalePictureInThread: pictureStatus was: UNINITIALISED");
          break;
        case SourcePicture.ERROR:
          alreadyLoading = false;
          Tools.log("ScalablePicture.loadAndScalePictureInThread: pictureStatus was: ERROR");
          break;
        case SourcePicture.LOADING:
          Tools.log("ScalablePicture.loadAndScalePictureInThread: pictureStatus was: LOADING");
          sourcePicture.addListener(this);
          setStatus(LOADING, "Loading: " + imageUrl.toString());
          sourceLoadProgressNotification(SourcePicture.LOADING_PROGRESS, sourcePicture.getPercentLoaded());
          scaleAfterLoad = true;
          break;
        case SourcePicture.ROTATING:
          Tools.log("ScalablePicture.loadAndScalePictureInThread: pictureStatus was: ROTATING");
          setStatus(LOADING, "Rotating: " + imageUrl.toString());
          sourceLoadProgressNotification(SourcePicture.LOADING_PROGRESS, sourcePicture.getPercentLoaded());
          scaleAfterLoad = true;
          break;
        case SourcePicture.READY:
          Tools.log("ScalablePicture.loadAndScalePictureInThread: pictureStatus was: READY. Sending SCALING status.");
          setStatus(SCALING, "Scaling: " + imageUrl.toString());
          createScaledPictureInThread(priority);
          break;
        default:
          Tools.log("ScalablePicture.loadAndScalePictureInThread: Don't know what status this is:" + Integer.toString(sourcePicture.getStatusCode()));
          break;

      }
    }

    // if the image is not already there then load it.
    if (!alreadyLoading) {
      if (sourcePicture != null) sourcePicture.removeListener(this);
      sourcePicture = new SourcePicture();
      sourcePicture.addListener(this);
      setStatus(LOADING, "Loading: " + imageUrl.toString());
      scaleAfterLoad = true;
      sourcePicture.loadPictureInThread(imageUrl, priority, rotation);
      // when the thread is done it sends a sourceStatusChange message to us
    }
  }

  /**
   * Synchroneous method to load the image.
   * It should only be called by something which is a thread itself such as the HtmlDistillerThread.
   * Since this intended for large batch operations this bypasses the cache.
   *
   * @param imageUrl The Url of the image to be loaded
   * @param rotation The angle by which it is to be roated upon loading.
   */
  public void loadPictureImd(URL imageUrl, double rotation) {
    Tools.log("loadPictureImd invoked with URL: " + imageUrl.toString());
    if (sourcePicture != null) sourcePicture.removeListener(this);
    sourcePicture = new SourcePicture();
    sourcePicture.addListener(this);
    setStatus(LOADING, "Loading: " + imageUrl.toString());
    scaleAfterLoad = true;
    sourcePicture.loadPicture(imageUrl, rotation);
  }

  /**
   * stops all picture loading except if the Url we desire is being loaded
   *
   * @param url The URL of the image which is to be loaded.
   */
  public void stopLoadingExcept(URL url) {
    if (sourcePicture != null) {
      boolean isCurrentlyLoading = sourcePicture.stopLoadingExcept(url);
      if (!isCurrentlyLoading) {
        // sourcePicture.removeListener( this );
      }
      PictureCache.stopBackgroundLoadingExcept(url);
    }
  }

  /**
   * method that is invoked by the SourcePictureListener interface. Usually this
   * will be called by the SourcePicture telling the ScalablePicture that
   * it has completed loading. The ScalablePicture should then change it's own
   * status and tell the ScalableListeners what's up.
   */

  public void sourceStatusChange(int statusCode, String statusMessage, SourcePicture sp) {
    //Tools.log("ScalablePicture.sourceStatusChange: status received from SourceImage: " + statusMessage);

    switch (statusCode) {
      case SourcePicture.UNINITIALISED:
        Tools.log("ScalablePicture.sourceStatusChange: pictureStatus was: UNINITIALISED message: " + statusMessage);
        setStatus(UNINITIALISED, statusMessage);
        break;
      case SourcePicture.ERROR:
        Tools.log("ScalablePicture.sourceStatusChange: pictureStatus was: ERROR message: " + statusMessage);
        setStatus(ERROR, statusMessage);
        sourcePicture.removeListener(this);
        break;
      case SourcePicture.LOADING:
        Tools.log("ScalablePicture.sourceStatusChange: pictureStatus was: LOADING message: " + statusMessage);
        setStatus(LOADING, statusMessage);
        break;
      case SourcePicture.ROTATING:
        Tools.log("ScalablePicture.sourceStatusChange: pictureStatus was: ROTATING message: " + statusMessage);
        setStatus(LOADING, statusMessage);
        break;
      case SourcePicture.READY:
        Tools.log("ScalablePicture.sourceStatusChange: pictureStatus was: READY message: " + statusMessage);
        setStatus(LOADED, statusMessage);
        sourcePicture.removeListener(this);
        if (scaleAfterLoad) {
          createScaledPictureInThread(Thread.MAX_PRIORITY);
          scaleAfterLoad = false;
        }
        break;
      default:
        Tools.log("ScalablePicture.sourceStatusChange: Don't recognize this status: " + statusMessage);
        break;

    }
  }

  /**
   * pass on the update on the loading Progress to the listening objects
   */
  public void sourceLoadProgressNotification(int statusCode, int percentage) {
    Enumeration e = scalablePictureStatusListeners.elements();
    while (e.hasMoreElements()) {
      ((ScalablePictureListener) e.nextElement())
          .sourceLoadProgressNotification(statusCode, percentage);
    }

  }

  /**
   * method that creates the scaled image in the background in it's own thread.
   *
   * @param priority The priority this image takes relative to the others.
   */
  public void createScaledPictureInThread(int priority) {
    setStatus(SCALING, "Scaling picture.");
    ScaleThread t = new ScaleThread(this);
    t.setPriority(priority);
    t.start();
  }

  public void scalePicture() {
    if ((sourcePicture == null) || (sourcePicture.getSourceBufferedImage() == null)) {
      ScaleFactor = 1.0;
      return;
    }

    setStatus(SCALING, "Scaling picture.");
    BufferedImage org = sourcePicture.getSourceBufferedImage();
    int w = sourcePicture.getWidth();
    int h = sourcePicture.getHeight();

    calcScale();
    scaledPicture =
      Scalr.resize(org, Scalr.Method.QUALITY, Scalr.Mode.AUTOMATIC,
              (int) (ScaleFactor * w), (int) (ScaleFactor * h), Scalr.OP_ANTIALIAS);

    // System.out.printf("scaled=%d,%d%n", scaledPicture.getWidth(), scaledPicture.getHeight());
    setStatus(READY, "Scaled Picture is ready.");
  }

  private void calcScale() {
    if (TargetSize == null) return;

    int WindowWidth = TargetSize.width;
    int WindowHeight = TargetSize.height;
    int PictureWidth = sourcePicture.getWidth();
    int PictureHeight = sourcePicture.getHeight();

    // Scale so that the entire picture fits in the component.
    if ((WindowWidth == 0) || (WindowHeight == 0)) { // jc added: window not ready
      ScaleFactor = 1.0;
    } else if (((double) PictureHeight / WindowHeight) > ((double) PictureWidth / WindowWidth)) {
      // Vertical scaling
      ScaleFactor = PictureHeight == 0 ? 0 : ((double) WindowHeight / PictureHeight);
    } else {
      // Horizontal scaling
      ScaleFactor = PictureWidth == 0 ? 0 : ((double) WindowWidth / PictureWidth);
    }

    //System.out.printf("pix=%d,%d%n", sourcePicture.getWidth(), sourcePicture.getHeight());
    //System.out.printf("target=%d,%d%n", TargetSize.width, TargetSize.height);
    //System.out.printf("ScaleFactor=%f%n", ScaleFactor);
  }

  /**
   * set the scale factor to the new desired value. The scale factor is a multiplier by which the original picture
   * needs to be multiplied to get the size of the picture on the screen. You must
   * call {@link #createScaledPictureInThread(int)} to
   * make anything happen.

   *

   * Example: Original is 3000 x 2000 --> Scale Factor 0.10  --> Target Picutre is 300 x 200
   */
  public void setScaleFactor(double newFactor) {
    TargetSize = null;
    ScaleFactor = newFactor;
  }

  /**
   * invoke this method to tell the scale process to figure out the scal factor
   * so that the image fits either by height or by width into the indicated dimension.
   */
  public void setScaleSize(Dimension newSize) {
    TargetSize = newSize;
  }

  /**
   * return the current scale factor
   */
  public double getScaleFactor() {
    return ScaleFactor;
  }

  /**
   * return the current scale size. This is the area that the picture
   * ist fitted into. Since the are could be wider or taller than the picture
   * will be scaled to there is a different mehtod getScaledSize
   * that will return the size of the picture.
   */
  public Dimension getScaleSize() {
    return TargetSize;
  }

  /**
   * return the scaled image
   */
  public BufferedImage getScaledPicture() {
    return scaledPicture;
  }

  /**
   * return the size of the scaled image or Zero if there is none
   */
  public Dimension getScaledSize() {
    if (scaledPicture != null)
      return new Dimension(scaledPicture.getWidth(), scaledPicture.getHeight());
    else
      return new Dimension(0, 0);
  }

  /**
   * return the size of the scaled image as a neatly formatted text or Zero if there is none
   */
  public String getScaledSizeString() {
    if (scaledPicture != null)
      return Integer.toString(scaledPicture.getWidth())
          + " x "
          + Integer.toString(scaledPicture.getHeight());
    else
      return "0 x 0";
  }

  /**
   * return the height of the scaled image or Zero if there is none
   */
  public int getScaledHeight() {
    if (scaledPicture != null)
      return scaledPicture.getHeight();
    else
      return 0;
  }

  /**
   * return the width of the scaled image or Zero if there is none
   */
  public int getScaledWidth() {
    if (scaledPicture != null)
      return scaledPicture.getWidth();
    else
      return 0;
  }

  /**
   * return the image in the original size
   */
  public BufferedImage getOriginalImage() {
    return sourcePicture.getSourceBufferedImage();
  }

  /**
   * return the image in the original size
   */
  public SourcePicture getSourcePicture() {
    return sourcePicture.getSourcePicture();
  }

  public SourcePicture setSourcePicture(SourcePicture source) {
    return this.sourcePicture = source;
  }

  /**
   * return the size of the original image or Zero if there is none
   */
  public Dimension getOriginalSize() {
    return sourcePicture.getSize();
  }

  /**
   * return the height of the original image or Zero if there is none
   */
  public int getOriginalHeight() {
    return sourcePicture.getHeight();
  }

  /**
   * return the width of the original image or Zero if there is none
   */
  public int getOriginalWidth() {
    return sourcePicture.getWidth();
  }

  /**
   * return the filename of the original image
   */
  public String getFilename() {
    return (imageUrl == null) ? "" : imageUrl.toString();
  }

  /**
   * This method allows the ScalablePicture's scaled BufferedImage to be written
   * to the desired file.
   *
   * @param  writeFile  The File that shall receive the jpg data
   */
  public void writeScaledJpg(File writeFile) {
    writeJpg(writeFile, scaledPicture, jpgQuality);
  }

  /**
   * This static method writes the indicated renderedImage (BufferedImage)
   * to the indicated file.
   *
   * @param  writeFile  The File that shall receive the jpg data
   * @param  renderedImage  The RenderedImage (BufferedImage) to be written
   * @param  jpgQuality  The quality with which to compress to jpg
   */
  public static void writeJpg(File writeFile, RenderedImage renderedImage, float jpgQuality) {
    Iterator writers = ImageIO.getImageWritersByFormatName("jpg");
    ImageWriter writer = (ImageWriter) writers.next();
    JPEGImageWriteParam params = new JPEGImageWriteParam(null);
    params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
    params.setCompressionQuality(jpgQuality);
    params.setProgressiveMode(ImageWriteParam.MODE_DISABLED);
    params.setDestinationType(new ImageTypeSpecifier(java.awt.image.IndexColorModel.getRGBdefault(),
        IndexColorModel.getRGBdefault().createCompatibleSampleModel(16, 16)));

    try(ImageOutputStream ios = ImageIO.createImageOutputStream(new FileOutputStream(writeFile))) {
      writer.setOutput(ios);
      writer.write(null, new IIOImage(renderedImage, null, null), params);
      ios.close();
    } catch (IOException e) {
      //Tools.log("ScalablePicture.writeJpg caught IOException: " +  e.getMessage() + "\nwhile writing " + writeFile.toString());
      e.printStackTrace();
    }
    //writer = null;
    writer.dispose(); //1.4.1 documentation says to do this.
  }

  /**
   * method to register the listening object of the status events
   */
  public void addStatusListener(ScalablePictureListener listener) {
    scalablePictureStatusListeners.add(listener);
  }

  /**
   * method to register the listening object of the status events
   */
  public void removeStatusListener(ScalablePictureListener listener) {
    scalablePictureStatusListeners.remove(listener);
  }

  /**
   * Method that sets the status of the ScalablePicture object and notifies
   * intereasted objects of a change in status (not built yet).
   */
  private void setStatus(int statusCode, String statusMessage) {
    String filename = (imageUrl == null) ? "" : imageUrl.toString();
    Tools.log("ScalablePicture.setStatus: sending: " + statusMessage + " to all Listeners from Image: " + filename);

    pictureStatusCode = statusCode;
    pictureStatusMessage = statusMessage;

    Enumeration e = scalablePictureStatusListeners.elements();
    while (e.hasMoreElements()) {
      ((ScalablePictureListener) e.nextElement())
          .scalableStatusChange(pictureStatusCode, pictureStatusMessage);
    }
  }

  /**
   * Method that returns the status code of the picture loading.
   */
  public int getStatusCode() {
    return pictureStatusCode;
  }

  /**
   * Method that returns the status code of the picture loading.
   */
  public String getStatusMessage() {
    return pictureStatusMessage;
  }

  /**
   * accessor method to set the quality that should be used on jpg write operations.
   */
  public void setJpgQuality(float quality) {
    //Tools.log( "setJpgQuality requested with " + Float.toString( quality ) );
    if (quality >= 0f && quality <= 1f) {
      //Tools.log( "Quality set." );
      jpgQuality = quality;
    }
  }
}


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

package ucar.nc2.iosp.gempak;

import ucar.unidata.io.RandomAccessFile;
import ucar.unidata.util.Format;
import ucar.unidata.util.StringUtil2;

import java.io.*;

import java.util.ArrayList;
import java.util.List;

/**
 * Read a Gempak sounding file
 *
 * @author IDV Development Team
 */
public class GempakSoundingFileReader extends AbstractGempakStationFileReader {

  /**
   * Surface Text identifier
   */
  public static final String SNDT = "SNDT";

  /**
   * date key identifier
   */
  public static final String DATE = "DATE";

  /**
   * time key identifier
   */
  public static final String TIME = "TIME";

  /**
   * PRES vertical coordinate
   */
  public static final int PRES_COORD = 1;

  /**
   * THTA vertical coordinate
   */
  public static final int THTA_COORD = 2;

  /**
   * HGHT vertical coordinate
   */
  public static final int HGHT_COORD = 3;

  /**
   * standard surface file id
   */
  public static final String MERGED = "merged";

  /**
   * climate surface file id
   */
  public static final String UNMERGED = "unmerged";

  /**
   * vertical coordinate
   */
  private int ivert = -1;

  /**
   * list of unmerged parts
   */
  private List<String> unmergedParts;

  /**
   * array of manadatory parameters
   */
  private final String[] mandpp = {"PRES", "TEMP", "DWPT", "DRCT", "SPED", "HGHT"};

  /**
   * array of significant temperature parameters
   */
  private final String[] sigtpp = {"PRES", "TEMP", "DWPT"};

  /**
   * array of significant wind parameters
   */
  private final String[] sigwpp = {"HGHT", "DRCT", "SPED"};

  /**
   * array of tropopause parameters
   */
  private final String[] troppp = {"PRES", "TEMP", "DWPT", "DRCT", "SPED"};

  /**
   * array of maximum wind parameters
   */
  private final String[] maxwpp = {"PRES", "DRCT", "SPED"};

  /**
   * array of groups below 100 mb
   */
  private final String[] belowGroups = {"TTAA", "TRPA", "MXWA", "PPAA", "TTBB", "PPBB"};

  /**
   * array of groups above 100 mb
   */
  private final String[] aboveGroups = {"TTCC", "TRPC", "MXWC", "PPCC", "TTDD", "PPDD"};

  /**
   * list of valid params for each group
   */
  private final String[][] parmLists = {mandpp, troppp, maxwpp, maxwpp, sigtpp, sigwpp};

  /**
   * Default ctor
   */
  GempakSoundingFileReader() {
  }

  /**
   * Initialize the file, read in all the metadata (ala DM_OPEN)
   *
   * @param raf       RandomAccessFile to read.
   * @param fullCheck if true, check entire structure
   * @return A GempakSoundingFileReader
   * @throws IOException problem reading file
   */
  public static GempakSoundingFileReader getInstance(RandomAccessFile raf, boolean fullCheck) throws IOException {
    GempakSoundingFileReader gsfr = new GempakSoundingFileReader();
    gsfr.init(raf, fullCheck);
    return gsfr;
  }

  /**
   * Initialize this reader.  Read all the metadata
   *
   * @return true if successful
   * @throws IOException problem reading the data
   */
  protected boolean init() throws IOException {
    return init(true);
  }

  /**
   * Initialize this reader.  Get the Grid specific info
   *
   * @param fullCheck check to make sure there are grids we can handle
   * @return true if successful
   * @throws IOException problem reading the data
   */
  protected boolean init(boolean fullCheck) throws IOException {

    if (!super.init(fullCheck)) {
      return false;
    }

    // Modeled after SN_OFIL
    if (dmLabel.kftype != MFSN) {
      logError("not a sounding data file ");
      return false;
    }

    DMPart part = getPart(SNDT);

    if (part != null) {  // merged file
      subType = MERGED;
      String vertName = part.params.get(0).kprmnm;
      switch (vertName) {
        case "PRES":
          ivert = PRES_COORD;
          break;
        case "THTA":
          ivert = THTA_COORD;
          break;
        case "HGHT":
        case "MHGT":
        case "DHGT":
          ivert = HGHT_COORD;
          break;
        default:
          logError("unknown vertical coordinate in merged file");
          return false;
      }

    } else {
      unmergedParts = SN_CKUA();
      boolean haveUnMerged = !unmergedParts.isEmpty();
      if (!haveUnMerged) {
        logError("unknown sounding file type - not merged/unmerged");
        return false;
      } else {
        ivert = PRES_COORD;
        subType = UNMERGED;
      }
    }
    if (!readStationsAndTimes(true)) {
      logError("Unable to read stations and times");
      return false;
    }

    return true;

  }

  /**
   * Get the vertical coordinate
   *
   * @return the vertical coordinate (PRES_COORD, THTA_COORD, HGHT_COORD)
   */
  public int getVerticalCoordinate() {
    return ivert;
  }

  /**
   * Get the list of merged parts in this file
   *
   * @return a list of the unmerged parts (only SNDT)
   */
  public List<String> getMergedParts() {
    List<String> list = new ArrayList<>(1);
    list.add(SNDT);
    return list;
  }

  /**
   * Get the list of unmerged parts in this file
   *
   * @return a list of the unmerged parts
   */
  public List<String> getUnmergedParts() {
    return new ArrayList<>(unmergedParts);
  }

  /**
   * Make the file subtype
   */
  protected void makeFileSubType() {
  }  // already set in init()

  /**
   * Print the list of dates in the file
   *
   * @param row ob row
   * @param col ob column
   */
  public void printOb(int row, int col) {
    GempakStation station = getStations().get(col - 1);
    String time = getDateString(row - 1);
    StringBuilder builder = new StringBuilder("\n");
    builder.append(makeHeader(station, time));
    builder.append("\n");
    boolean merge = getFileSubType().equals(MERGED);
    List<String> parts;
    if (merge) {
      parts = new ArrayList<>();
      parts.add(SNDT);
    } else {
      parts = unmergedParts;
    }
    for (String part : parts) {
      RData rd;
      try {
        rd = DM_RDTR(row, col, part);
      } catch (IOException ioe) {
        ioe.printStackTrace();
        rd = null;
      }
      if (rd == null) {
        continue;
      }
      if (!merge) {
        builder.append("    ");
        builder.append(part);
        builder.append("    ");
        builder.append(time.substring(time.indexOf("/") + 1));
      }
      builder.append("\n");
      if (!merge) {
        builder.append("\t");
      }
      List<GempakParameter> params = getParameters(part);
      for (GempakParameter parm : params) {
        builder.append(StringUtil2.padLeft(parm.getName(), 7));
        builder.append("\t");
      }
      builder.append("\n");
      if (!merge) {
        builder.append("\t");
      }
      float[] data = rd.data;
      int numParams = params.size();
      int numLevels = data.length / numParams;
      for (int j = 0; j < numLevels; j++) {
        for (int i = 0; i < numParams; i++) {
          builder.append(
                  StringUtil2.padLeft(
                          Format.formatDouble(
                                  data[j * numParams + i], 7, 1), 7));
          builder.append("\t");
        }
        builder.append("\n");
        if (!merge) {
          builder.append("\t");
        }
      }
      builder.append("\n");
    }
    builder.append("\n");
    System.out.println(builder.toString());
  }

  /**
   * Make the header for the text report
   *
   * @param stn  the station
   * @param date the date
   * @return the header
   */
  private String makeHeader(GempakStation stn, String date) {
    StringBuilder builder = new StringBuilder();
    builder.append("STID = ");
    builder.append(StringUtil2.padRight((stn.getSTID().trim()
            + stn.getSTD2().trim()), 8));
    builder.append("\t");
    builder.append("STNM = ");
    builder.append(Format.i(stn.getSTNM(), 6));
    builder.append("\t");
    builder.append("TIME = ");
    builder.append(date);
    builder.append("\n");
    builder.append("SLAT = ");
    builder.append(Format.d(stn.getLatitude(), 5));
    builder.append("\t");
    builder.append("SLON = ");
    builder.append(Format.d(stn.getLongitude(), 5));
    builder.append("\t");
    builder.append("SELV = ");
    builder.append(Format.d(stn.getAltitude(), 5));
    builder.append("\n");
    return builder.toString();
  }

  /**
   * This subroutine checks the parts in a sounding data set for the
   * unmerged data types.
   *
   * @return list of part names
   */
  private List<String> SN_CKUA() {
    List<String> types = new ArrayList<>();
    boolean above = false;
    boolean done = false;
    String partToCheck;
    while (!done) {
      // check for mandatory groups
      for (int group = 0; group < belowGroups.length; group++) {
        if (above) {
          partToCheck = aboveGroups[group];
        } else {
          partToCheck = belowGroups[group];
        }
        if (checkForValidGroup(partToCheck, parmLists[group])) {
          types.add(partToCheck);
        }
      }
      if (!above) {
        above = true;
      } else {
        done = true;
      }
    }
    return types;
  }

  /**
   * Check for valid groups
   *
   * @param partToCheck the part name
   * @param params      the parameters that are supposed to be there
   * @return true if the part is there and has the right params
   */
  private boolean checkForValidGroup(String partToCheck, String[] params) {
    DMPart part = getPart(partToCheck);
    if (part == null) {
      return false;
    }
    int i = 0;
    for (DMParam parm : part.params) {
      if (!(parm.kprmnm.equals(params[i++]))) {
        return false;
      }
    }
    return true;
  }

  /**
   * Run the program
   *
   * @param args [0] filename (required),
   *             [1] variable name (X for default),
   *             [2] X to not list grids
   * @throws IOException problem reading the file
   */
  public static void main(String[] args) throws IOException {
    if (args.length == 0) {
      System.out.println("need to supply a GEMPAK sounding file name");
      System.exit(1);
    }
    try {
      GempakParameters.addParameters(
              "resources/nj22/tables/gempak/params.tbl");
    } catch (Exception e) {
      System.out.println("unable to init param tables");
    }

    GempakSoundingFileReader gsfr = getInstance(getFile(args[0]), true);
    System.out.println("Type = " + gsfr.getFileType());
    gsfr.printFileLabel();
    gsfr.printKeys();
    gsfr.printHeaders();
    gsfr.printParts();
    gsfr.printDates();
    gsfr.printStations(false);
    int row = 1;
    int col = 1;
    if (args.length > 1) {
      row = Integer.parseInt(args[1]);
    }
    if (args.length > 2) {
      try {
        col = Integer.parseInt(args[2]);
      } catch (Exception npe) {
        col = gsfr.findStationIndex(args[2]);
        if (col == -1) {
          System.out.println("couldn't find station " + args[2]);
          System.exit(1);
        }
        System.out.println("found station at column " + col);
      }
    }
    gsfr.printOb(row, col);
  }

}


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

import com.google.common.escape.Escaper;
import com.google.common.net.UrlEscapers;
import ucar.unidata.util.StringUtil2;

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

/**
 * This is a general purpose utility for determining a place to write files and cache them, eg for
 * uncompressing files. This class does not scour itself.
 *

 * Note that when a file in the cache is accessed, its lastModified date is set, which is used for the LRU scouring.
 *
 The cdm library sometimes needs to write files, eg
 * to uncompress them, or for grib index files, etc. The first choice is to write these files
 * in the same directory that the original file lives in. However, that directory may not be
 * writeable, so we need to find a place to write them to.
 * We also want to use the file if it already exists.
 *

 *
 A writeable cache "root directory" is set:

 *
  explicitly by setRootDirectory()
 *
  through the system property "nj22.cache" if it exists
 *
  by appending "/nj22/cache" to the system property "user.home" if it exists
 *
  by appending "/nj22/cache" to the system property "user.dir" if it exists
 *
  use "./nj22/cache" if none of the above
 *

 *

 *
 Scenario 1: want to uncompress a file; check to see if already have done so,
 * otherwise get a File that can be written to.
 *

 * // see if already uncompressed
 * File uncompressedFile = FileCache.getFile( uncompressedFilename, false);
 * if (!uncompressedFile.exists()) {
 *   // nope, uncompress it
 *   UncompressInputStream.uncompress( uriString, uncompressedFile);
 * }
 * doSomething( uncompressedFile);
 *

 *

 *
 Scenario 2: want to write a derived file always in the cache.
 *

 * File derivedFile = FileCache.getCacheFile( derivedFilename);
 * if (!derivedFile.exists()) {
 *   createDerivedFile( derivedFile);
 * }
 * doSomething( derivedFile);
 *

 *

 *
 Scenario 3: same as scenario 1, but use the default Cache policy:
 *

 * File wf = FileCache.getFileStandardPolicy( uncompressedFilename);
 * if (!wf.exists()) {
 *   writeToFile( wf);
 *   wf.close();
 * }
 * doSomething( wf);
 *

 *
 * @author jcaron
 */
public class DiskCache {
  private static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger("cacheLogger");
  static private String root = null;
  static private boolean standardPolicy = false;
  static private boolean checkExist = false;

  /**
   * debug only
   */
  static public boolean simulateUnwritableDir = false;

  static {
    root = System.getProperty("nj22.cache");

    if (root == null) {
      String home = System.getProperty("user.home");

      if (home == null)
        home = System.getProperty("user.dir");

      if (home == null)
        home = ".";

      root = home + "/.unidata/cache/";
    }

    String policy = System.getProperty("nj22.cachePolicy");
    if (policy != null)
      standardPolicy = policy.equalsIgnoreCase("true");
  }

  /**
   * Set the cache root directory. Create it if it doesnt exist.
   *
   * @param cacheDir the cache directory
   */
  static public void setRootDirectory(String cacheDir) {
    if (!cacheDir.endsWith("/"))
      cacheDir = cacheDir + "/";
    root = StringUtil2.replace(cacheDir, '\\', "/"); // no nasty backslash

    makeRootDirectory();
  }

  /**
   * Make sure that the current root directory exists.
   */
  static public void makeRootDirectory() {
    File dir = new File(root);
    if (!dir.exists())
      if (!dir.mkdirs())
        throw new IllegalStateException("DiskCache.setRootDirectory(): could not create root directory <" + root + ">.");
    checkExist = true;
  }

  /**
   * Get the name of the root directory
   *
   * @return name of the root directory
   */
  static public String getRootDirectory() {
    return root;
  }

  /**
   * Set the standard policy used in getWriteableFileStandardPolicy().
   * Default is to try to create the file in the same directory as the original.
   * Setting alwaysInCache to true means to always create it in the cache directory.
   *
   * @param alwaysInCache make this the default policy
   */
  static public void setCachePolicy(boolean alwaysInCache) {
    standardPolicy = alwaysInCache;
  }

  /**
   * Get a File if it exists. If not, get a File that can be written to.
   * Use the standard policy to decide where to place it.
   *

   * Things are a bit complicated, because in order to guarantee a file in
   * an arbitrary location can be written to, we have to try to open it as a
   * FileOutputStream. If we do, we don't want to open it twice,
   * so we return a WriteableFile that contains an opened FileOutputStream.
   * If it already exists, or we get it from cache, we don't need to open it.
   *

   * In any case, you must call WriteableFile.close() to make sure its closed.
   *
   * @param fileLocation normal file location
   * @return WriteableFile holding the writeable File and a possibly opened FileOutputStream (append false)
   */
  static public File getFileStandardPolicy(String fileLocation) {
    return getFile(fileLocation, standardPolicy);
  }

  /**
   * Get a File if it exists. If not, get a File that can be written to.
   * If alwaysInCache, look only in the cache, otherwise, look in the "normal" location first,
   * then in the cache.
   *

   *
   * @param fileLocation  normal file location
   * @param alwaysInCache true if you want to look only in the cache
   * @return a File that either exists or is writeable.
   */
  static public File getFile(String fileLocation, boolean alwaysInCache) {
    if (alwaysInCache) {
      return getCacheFile(fileLocation);

    } else {
      File f = new File(fileLocation);
      if (f.exists())
        return f;

      // now comes the tricky part to make sure we can open and write to it
      try {
        if (!simulateUnwritableDir && f.createNewFile()) {
          boolean ret = f.delete();
          assert ret;
          return f;
        }
      } catch (IOException e) {
        // cant write to it - drop through
      }

      return getCacheFile(fileLocation);
    }
  }

  /**
   * Get a file in the cache.
   * File may or may not exist. We assume its always writeable.
   * If it does exist, set its LastModifiedDate to current time.
   *
   * @param fileLocation normal file location
   * @return equivalent File in the cache.
   */
  static public File getCacheFile(String fileLocation) {
    File f = new File(makeCachePath(fileLocation));
    if (f.exists()) {
      if (!f.setLastModified(System.currentTimeMillis()))
        logger.warn("Failed to setLastModified on " + f.getPath());
    }

    if (!checkExist) {
      File dir = f.getParentFile();
      if (!dir.exists() && !dir.mkdirs())
        logger.warn("Failed to mkdirs on " + dir.getPath());
      checkExist = true;
    }
    return f;
  }

  /**
   * Make the cache filename
   *
   * @param fileLocation normal file location
   * @return cache filename
   */
  static private String makeCachePath(String fileLocation) {
    Escaper urlPathEscaper = UrlEscapers.urlPathSegmentEscaper();

    fileLocation = fileLocation.replace('\\', '/');  // LOOK - use better normalization code  eg Spring StringUtils
    String cachePath = urlPathEscaper.escape(fileLocation);

    return root + cachePath;
  }

  static public void showCache(PrintStream pw) {
    pw.println("Cache files");
    pw.println("Size   LastModified       Filename");
    File dir = new File(root);
    File[] children = dir.listFiles();
    if (children == null) return;
    for (File file : children) {
      String org = EscapeStrings.urlDecode(file.getName());
      pw.println(" " + file.length() + " " + new Date(file.lastModified()) + " " + org);
    }
  }

  /**
   * Remove all files with date < cutoff.
   *
   * @param cutoff earliest date to allow
   * @param sbuff  write results here, null is ok.
   */
  static public void cleanCache(Date cutoff, StringBuilder sbuff) {
    if (sbuff != null) sbuff.append("CleanCache files before ").append(cutoff).append("\n");
    File dir = new File(root);
    File[] children = dir.listFiles();
    if (children == null) return;
    for (File file : children) {
      Date lastMod = new Date(file.lastModified());
      if (lastMod.before(cutoff)) {
        boolean ret = file.delete();
        if (sbuff != null) {
          sbuff.append(" delete ").append(file).append(" (").append(lastMod).append(")\n");
          if (!ret) sbuff.append("Error deleting ").append(file).append("\n");
        }
      }
    }
  }

  /**
   * Remove files if needed to make cache have less than maxBytes bytes file sizes.
   * This will remove oldest files first.
   *
   * @param maxBytes max number of bytes in cache.
   * @param sbuff    write results here, null is ok.
   */
  static public void cleanCache(long maxBytes, StringBuilder sbuff) {
    cleanCache(maxBytes, new FileAgeComparator(), sbuff);
  }

  /**
   * Remove files if needed to make cache have less than maxBytes bytes file sizes.
   * This will remove files in sort order defined by fileComparator.
   * The first files in the sort order are kept, until the max bytes is exceeded, then they are deleted.
   *
   * @param maxBytes       max number of bytes in cache.
   * @param fileComparator sort files first with this
   * @param sbuff          write results here, null is ok.
   */
  static public void cleanCache(long maxBytes, Comparator<File> fileComparator, StringBuilder sbuff) {
    if (sbuff != null) sbuff.append("DiskCache clean maxBytes= ")
            .append(maxBytes).append("on dir ").append(root).append("\n");

    File dir = new File(root);
    long total = 0, total_delete = 0;

    File[] files = dir.listFiles();
    if (files != null) {
      List<File> fileList = Arrays.asList(files);
      Collections.sort(fileList, fileComparator);

      for (File file : fileList) {
        if (file.length() + total > maxBytes) {
          total_delete += file.length();
          if (sbuff != null)
            sbuff.append(" delete ").append(file).append(" (")
                    .append(file.length()).append(")\n");
          if (!file.delete() && sbuff != null)
            sbuff.append("Error deleting ").append(file).append("\n");
        } else {
          total += file.length();
        }
      }
    }
    if (sbuff != null) {
      sbuff.append("Total bytes deleted= ").append(total_delete).append("\n");
      sbuff.append("Total bytes left in cache= ").append(total).append("\n");
    }
  }

  // reverse sort - latest come first
  static private class FileAgeComparator implements Comparator<File> {
    public int compare(File f1, File f2) {
      long f1Age = f1.lastModified();
      long f2Age = f2.lastModified();
      return (f1Age < f2Age) ? 1 : (f1Age == f2Age ? 0 : -1);  // Steve Ansari 6/3/2010
    }
  }

  /**
   * debug
   *
   * @param filename look for this file
   * @throws java.io.IOException if read error
   */
  static void make(String filename) throws IOException {
    File want = DiskCache.getCacheFile(filename);
    System.out.println("make=" + want.getPath() + "; exists = " + want.exists());
    if (!want.exists()) {
      boolean ret = want.createNewFile();
      assert ret;
    }
    System.out.println(" canRead= " + want.canRead() + " canWrite = " + want.canWrite() + " lastMod = " + new Date(want.lastModified()));
    System.out.println(" original=" + filename);
  }

  static public void main(String[] args) throws IOException {
    DiskCache.setRootDirectory("C:/temp/chill/");
    make("C:/junk.txt");
    make("C:/some/enchanted/evening/joots+3478.txt");
    make("http://www.unidata.ucar.edu/some/enc hanted/eve'ning/nowrite.gibberish");

    showCache(System.out);
    StringBuilder sbuff = new StringBuilder();
    cleanCache(1000 * 1000 * 10, sbuff);
    System.out.println(sbuff);
  }

}


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

import ucar.nc2.ui.util.Resource;

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

/**
 * Button, Action and Menu utilities:
 * static stationHelper methods for building ucar.unidata.UI's.
 *
 *
 Example for Toggle Action
 *

 * AbstractAction dsAction =  new AbstractAction() {
    public void actionPerformed(ActionEvent e) {
     Boolean state = (Boolean) getValue( BAMutil.STATE);
     addCoords = state.booleanValue();
     String tooltip = addCoords ? "add Coordinates in ON" : "add Coordinates is OFF";
     dsButt.setToolTipText(tooltip);
    }
   };
   BAMutil.setActionProperties( dsAction, "Dataset", "add Coordinates is OFF", true, 'D', -1);
   addCoords = prefs.getBoolean( "dsState", false);
   dsAction.putValue(BAMutil.STATE, new Boolean(addCoords));
   AbstractButton dsButt = BAMutil.addActionToContainer(buttPanel, dsAction);

   ...
   prefs.putBoolean("dsState", dsButt.getModel().isSelected());

 *
 * @author John Caron
 */
public class BAMutil {
    /** Action Property specifies Selected icon name */
  public static final String SELECTED_ICON = "SelectedIcon";
    /** Action Property specifies is its a toggle */
  public static final String TOGGLE = "isToggle";
    /** Action Property specifies menu mneumonic */
  public static final String MNEMONIC = "mnemonic";
    /** Action Property specifies menu accelerator */
  public static final String ACCEL = "accelerator";
    /** the state of "toggle" actions = Boolean */
  public static final String STATE = "state";

  private static String defaultResourcePath = "/resources/nj22/ui/icons/";

  // Check if on a mac
  private static final String osName = System.getProperty("os.name").toLowerCase();
  private static final boolean isMacOs = osName.startsWith("mac os x");

  /**
   * Set the resource path for icons, images, cursors.
   * @param path reletive to the classpath
   */
  public static void setResourcePath( String path) { defaultResourcePath = path; }
  public static String getResourcePath( ) { return defaultResourcePath; }

  final private static int META_KEY = (isMacOs) ? java.awt.Event.META_MASK
                                                    : java.awt.Event.CTRL_MASK;

  private static boolean debug = false, debugToggle = false;

  /** Get the named Icon from the default resource (jar file).
    * @param name name of the Icon ( will look for <name>.gif)
    * @param errMsg true= print error message if not found
    * @return the Icon or null if not found
    */
  public static ImageIcon getIcon( String name, boolean errMsg) {
    ImageIcon ii = Resource.getIcon(defaultResourcePath+name+".gif", errMsg);
    if (ii == null) Resource.getIcon(defaultResourcePath+name+".png", errMsg);
    return ii;
  }

  /** Get the named Image from the default resource (jar file).
    * @param name name of the Image ( will look for <name>.gif)
    * @return the Image or null if not found
    */
  public static Image getImage( String name) {
    Image ii;
    if (name.endsWith(".png") || name.endsWith(".jpg") || name.endsWith(".gif"))
       ii = Resource.getImage(defaultResourcePath+name);
    else
       ii =  Resource.getImage(defaultResourcePath+name+".gif");
    return ii;
  }

  /** Make a cursor from the named Image in the default resource (jar file)
    * @param name name of the Image ( will look for <name>.gif)
    * @return the Cursor or null if failure
    */
  public static Cursor makeCursor( String name) {
    return Resource.makeCursor(defaultResourcePath+name+".gif");
  }

    /** Make a "buttcon" = button with an Icon
       @param icon the normal Icon
       @param selected the selected Icon
       @param tooltip the tooltip
       @param is_toggle if true, make JToggleButton, else JButton
       @return the buttcon (JButton or JToggleButton)
    */
  public static AbstractButton makeButtcon( Icon icon, Icon selected, String tooltip, boolean is_toggle) {
    AbstractButton butt;
    if (is_toggle)
      butt = new JToggleButton();
    else
      butt = new JButton();

    if (debug) System.out.println("   makeButtcon"+ icon+ " "+ selected+ " "+ tooltip+ " "+ is_toggle);

    if (icon != null)
      butt.setIcon(icon);

    if (selected != null) {
      if (is_toggle) {
        butt.setSelectedIcon( selected);
      } else {
        butt.setRolloverIcon(selected);
        butt.setRolloverSelectedIcon(selected);
        butt.setPressedIcon(selected);
        butt.setRolloverEnabled(true);
      }
    }

    butt.setMaximumSize(new Dimension(28,28));       // kludge
    butt.setPreferredSize(new Dimension(28,28));
    butt.setToolTipText(tooltip);
    butt.setFocusPainted(false);

    return butt;
  }

   /** Make a "buttcon" = button with an Icon
       @param iconName name of the Icon ( will look for <name>.gif)
       @param tooltip the tooltip
       @param is_toggle if true, make JToggleButton, else JButton
       @return the buttcon (JButton or JToggleButton)
    */
  public static AbstractButton makeButtcon( String iconName, String tooltip, boolean is_toggle) {
    Icon icon  = getIcon( iconName, false);
    Icon iconSel  = getIcon( iconName+"Sel", false);
    return makeButtcon( icon, iconSel, tooltip, is_toggle);
  }

   private static JMenuItem makeMenuItem( Icon icon, Icon rollover, String menu_cmd, boolean is_toggle,
      int mnemonic, int accel) {
    JMenuItem mi;
    if (is_toggle)
      mi = new JCheckBoxMenuItem(menu_cmd);
    else
      mi = new JMenuItem(menu_cmd);

    if (icon != null)
      mi.setIcon(icon);
    if (rollover != null) {
      mi.setRolloverIcon(rollover);
      mi.setRolloverSelectedIcon(rollover);
      mi.setPressedIcon(rollover);
      mi.setRolloverEnabled(true);
    }

    mi.setHorizontalTextPosition( SwingConstants.LEFT );
    if (mnemonic != 0)
      mi.setMnemonic( mnemonic);
    if (accel != 0)
      mi.setAccelerator(KeyStroke.getKeyStroke( accel, META_KEY));

    return mi;
  }

    // NB: doesnt add action to MenuItem
  private static JMenuItem makeMenuItemFromAction( Action act) {
    // this prevents null pointer exception if user didnt call setProperties()
    Boolean tog = (Boolean) act.getValue( BAMutil.TOGGLE);
    boolean is_toggle = (tog == null ) ? false : tog.booleanValue();
    Integer mnu = (Integer) act.getValue( BAMutil.MNEMONIC);
    int mnemonic = (tog == null ) ? -1 : mnu.intValue();
    Integer acc = (Integer) act.getValue( BAMutil.ACCEL);
    int accel = (acc == null ) ? 0 : acc.intValue();

    return makeMenuItem(
        (Icon) act.getValue( Action.SMALL_ICON),
        (Icon) act.getValue( BAMutil.SELECTED_ICON),
        (String) act.getValue( Action.SHORT_DESCRIPTION),
        is_toggle, mnemonic, accel < 0 ? 0 : accel);
  }

  /** creates a MenuItem using the given Action and adds it to the given Menu.
    Uses Properties that have been set on the Action (see setActionProperties()). All
    are optional except for Action.SHORT_DESCRIPTION:

        Action.SHORT_DESCRIPTION   String     MenuItem text (required)
        Action.SMALL_ICON          Icon       the Icon to Use
        BAMutil.SELECTED_ICON      Icon       the Icon when selected (optional)
        BAMutil.TOGGLE             Boolean    true if its a toggle
        BAMutil.MNEMONIC           Integer    menu item shortcut
        BAMutil.ACCEL              Integer    menu item global keyboard accelerator

    The Action is triggered when the MenuItem is selected. Enabling and disabling the Action
    does the same for the MenuItem. For toggles, state is maintained in the Action,
    and MenuItem state changes when the Action state changes.

    The point of all this is that once you set it up, you work exclusively with the action object,
    and all changes are automatically reflected in the UI.

    @param menu  add to this menu
    @param act the Action to make it out of
    @param menuPos if >= 0, add at this position, otherwise append
    @return the MenuItem created
  */
   public static JMenuItem addActionToMenu( JMenu menu, Action act, int menuPos) {
    JMenuItem mi = makeMenuItemFromAction( act);
    if (menuPos >= 0)
      menu.add( mi, menuPos);
    else
      menu.add( mi);

    Boolean tog = (Boolean) act.getValue( BAMutil.TOGGLE);
    boolean is_toggle = (tog == null ) ? false : tog.booleanValue();

    // set state for toggle buttons
    if (is_toggle) {
      if (debugToggle)
        System.out.println("addActionToMenu: "+ act.getValue( Action.SHORT_DESCRIPTION)+" "+act.getValue( BAMutil.STATE));
      Boolean state = (Boolean) act.getValue( BAMutil.STATE);
      if (state == null) state = Boolean.FALSE;
      act.putValue(BAMutil.STATE, state);
      mi.setSelected(state.booleanValue());
    }

      // add event listeners
    Action myAct = is_toggle ? new ToggleAction( act) : act;
    mi.addActionListener( myAct);
    act.addPropertyChangeListener(new myActionChangedListener(mi));

    return mi;
  }

  public static JMenuItem addActionToMenu( JMenu menu, Action act) {
    return addActionToMenu( menu, act, -1);
  }

  public static JMenuItem addActionToPopupMenu( JPopupMenu pmenu, Action act) {
    JMenuItem mi = makeMenuItemFromAction( act);
    pmenu.add( mi);

    Boolean tog = (Boolean) act.getValue( BAMutil.TOGGLE);
    boolean is_toggle = (tog == null ) ? false : tog.booleanValue();

    // set state for toggle buttons
    if (is_toggle) {
      if (debugToggle)
        System.out.println("addActionToMenu: "+ act.getValue( Action.SHORT_DESCRIPTION)+" "+act.getValue( BAMutil.STATE));
      Boolean state = (Boolean) act.getValue( BAMutil.STATE);
      if (state == null) state = Boolean.FALSE;
      act.putValue(BAMutil.STATE, state);
      mi.setSelected(state.booleanValue());
    }

      // add event listeners
    Action myAct = is_toggle ? new ToggleAction( act) : act;
    mi.addActionListener( myAct);
    act.addPropertyChangeListener(new myActionChangedListener(mi));

    return mi;
  }

    // NB: doesnt add action to button
  private static AbstractButton _makeButtconFromAction( Action act) {
    // this prevents null pointer exception if user didnt call setProperties()
    Boolean tog = (Boolean) act.getValue( BAMutil.TOGGLE);
    boolean is_toggle = (tog == null ) ? false : tog.booleanValue();

    return makeButtcon(
        (Icon) act.getValue( Action.SMALL_ICON),
        (Icon) act.getValue( BAMutil.SELECTED_ICON),
        (String) act.getValue( Action.SHORT_DESCRIPTION),
        is_toggle);
  }

  /** creates an AbstractButton using the given Action and adds it to the given Container at the position..
    Uses Properties that have been set on the Action (see setActionProperties()). All
    are optional except for Action.SMALL_ICON:

        Action.SMALL_ICON          Icon       the Icon to Use (required)
        BAMutil.SELECTED_ICON      Icon       the Icon when selected (optional)
        Action.SHORT_DESCRIPTION   String     tooltip
        BAMutil.TOGGLE             Boolean    true if its a toggle

    The Action is triggered when the Button is selected. Enabling and disabling the Action
    does the same for the Button. For toggles, state is maintained in the Action,
    and the Button state changes when the Action state changes.

    The point of all this is that once you set it up, you work exclusively with the action object,
    and all changes are automatically reflected in the UI.

    @param c  add to this Container
    @param act the Action to make it out of
    @param pos add to the container at this position (if pos < 0, add at the end)
    @return the AbstractButton created  (JButton or JToggleButton)
  */

  public static AbstractButton addActionToContainerPos( Container c, Action act, int pos) {
    AbstractButton butt = _makeButtconFromAction( act);
    if (pos < 0)
      c.add( butt);
    else
      c.add(butt, pos);

    if (debug) System.out.println(" addActionToContainerPos "+ act+ " "+ butt+ " "+ pos);

    Boolean tog = (Boolean) act.getValue( BAMutil.TOGGLE);
    boolean is_toggle = (tog == null ) ? false : tog.booleanValue();

      // set state for toggle buttons
    if (is_toggle) {
      if (debugToggle)
        System.out.println("addActionToContainerPos: "+ act.getValue( Action.SHORT_DESCRIPTION)+" "+act.getValue( BAMutil.STATE));
      Boolean state = (Boolean) act.getValue( BAMutil.STATE);
      if (state == null) state = Boolean.FALSE;
      act.putValue(BAMutil.STATE, state);
      butt.setSelected(state.booleanValue());
    }

      // add event listsners
    Action myAct = is_toggle ? new ToggleAction( act) : act;
    butt.addActionListener( myAct);
    act.addPropertyChangeListener(new myActionChangedListener(butt));

    return butt;
  }

  public static AbstractButton makeButtconFromAction( Action act) {
    AbstractButton butt = _makeButtconFromAction( act);

    Boolean tog = (Boolean) act.getValue( BAMutil.TOGGLE);
    boolean is_toggle = (tog == null ) ? false : tog.booleanValue();

      // set state for toggle buttons
    if (is_toggle) {
      if (debugToggle)
        System.out.println("addActionToContainerPos: "+ act.getValue( Action.SHORT_DESCRIPTION)+" "+act.getValue( BAMutil.STATE));
      Boolean state = (Boolean) act.getValue( BAMutil.STATE);
      if (state == null) state = Boolean.FALSE;
      act.putValue(BAMutil.STATE, state);
      butt.setSelected(state.booleanValue());
    }

      // add event listsners
    Action myAct = is_toggle ? new ToggleAction( act) : act;
    butt.addActionListener( myAct);
    act.addPropertyChangeListener(new myActionChangedListener(butt));

    return butt;
  }

  /** Same as addActionToContainerPos, but add to end of Container
   * @param c add to this container
   * @param act add this action
   * @return the Button that triggers this action, in case you want to decorate it
   */
  public static AbstractButton addActionToContainer( Container c, Action act) {
    return addActionToContainerPos( c, act, -1);
  }

 /** Standard way to set Properties for Actions.
  * This also looks for an Icon "<icon_name>Sel" and if it exists:
  *   1) sets SelectedIcon if its a toggle, or
  *   2) sets the Icon when selected (optional) if its not a toggle
  *
  *  If is_toggle, a toggle button is created (in addActionToContainer()), default state false
  *  To get or set the state of the toggle button:
  *    Boolean state = (Boolean) action.getValue(BAMutil.STATE);
  *    action.putValue(BAMutil.STATE, new Boolean(true/false));
  *
  * @param act  add properties to this action
  * @param icon_name name of icon (or null).
  * @param action_name menu name / tooltip
  * @param is_toggle true if its a toggle
  * @param mnemonic  menu item shortcut
  * @param accel menu item global keyboard accelerator
  **/
  public static void setActionProperties( AbstractAction act, String icon_name, String action_name,
    boolean is_toggle, int mnemonic, int accel ) {
    if (icon_name != null) {
      act.putValue( Action.SMALL_ICON, getIcon( icon_name, true));
      act.putValue( BAMutil.SELECTED_ICON, getIcon( icon_name + "Sel", false));
    }
    act.putValue( Action.SHORT_DESCRIPTION, action_name);
    act.putValue( Action.LONG_DESCRIPTION, action_name);
    act.putValue( BAMutil.TOGGLE, new Boolean(is_toggle));
    act.putValue( BAMutil.MNEMONIC, new Integer(mnemonic));
    act.putValue( BAMutil.ACCEL, new Integer(accel));
  }

 /** Standard way to set Properties and state for "Toggle" Actions.  *
  * @param act  add properties to this action
  * @param icon_name  name of icon (or null).
  * @param action_name menu name / tooltip
  * @param toggleValue default value of toggle
  * @param mnemonic  menu item shortcut
  * @param accel menu item global keyboard accelerator
  **/
  public static void setActionPropertiesToggle( AbstractAction act, String icon_name, String action_name,
    boolean toggleValue, int mnemonic, int accel ) {
    setActionProperties( act, icon_name, action_name, true, mnemonic, accel);
    act.putValue( BAMutil.STATE, new Boolean(toggleValue));
  }

  /*
  public static void addActionToMenuAndToolbar( JMenu menu, JToolBar toolbar, Action act,
      String name, String icon_name, boolean is_toggle, int mnemonic, int accel) {

    AbstractButton butt = addAction( toolbar, act, name, icon_name, is_toggle);
    JMenuItem mi = addActionToMenu( menu, act, name, icon_name, is_toggle, mnemonic, accel);
  }

  public static void addActionToMenuAndToolbar2( JMenu menu, JComponent toolbar,
      int pos, Action act) {
    String name = (String) act.getValue( Action.SHORT_DESCRIPTION);
    Icon icon = (Icon) act.getValue( Action.SMALL_ICON);

    JMenuItem mi = new JMenuItem(name);
    mi.setIcon(icon);
    mi.setHorizontalTextPosition( SwingConstants.LEFT );
    mi.addActionListener( act);
    menu.add(mi);

    JButton butt = new JButton();
    butt.setIcon(icon);
    butt.setMaximumSize(new Dimension(24,24));       // kludge
    butt.setPreferredSize(new Dimension(24,24));
    butt.setToolTipText(name);
    butt.setFocusPainted(false);
    butt.addActionListener( act);
    toolbar.add(butt, pos);
  }   */

  // used by PopopMenu
 /** This wraps a regular action and makes it into a "toggle action",
   * and associates it with an AbstractButton.
   * Fetch/set the toggle state on the original action using:
   *
Boolean state = (Boolean) act.getValue(BAMutil.STATE);
   * act.putValue(BAMutil.STATE, new Boolean(state));

   * It will automatically change the button state if the action state changes.
   */
  public static class ActionToggle extends AbstractAction {
    private Action orgAct;
    private AbstractButton button;

    public ActionToggle(Action oa, AbstractButton b) {
      this.orgAct = oa;
      this.button = b;
      orgAct.putValue(STATE, new Boolean(true)); // state is kept with original action

      orgAct.addPropertyChangeListener(new java.beans.PropertyChangeListener() {
        public void propertyChange(java.beans.PropertyChangeEvent e) {
          String propertyName = e.getPropertyName();
          if (debugToggle) System.out.println("propertyChange "+ propertyName+ " "+ ((Boolean) e.getNewValue()));
          if (propertyName.equals(Action.NAME)) {
            String text = (String) e.getNewValue();
            button.setText(text);
            button.repaint();
          } else if (propertyName.equals("enabled")) {
            Boolean enabledState = (Boolean) e.getNewValue();
            button.setEnabled(enabledState.booleanValue());
            button.repaint();
          } else if (propertyName.equals(STATE)) {
            Boolean state = (Boolean) e.getNewValue();
            button.setSelected(state.booleanValue());
            button.repaint();
          }
        }
      });
    }

    public void actionPerformed( java.awt.event.ActionEvent e) {
      Boolean state = (Boolean) orgAct.getValue(BAMutil.STATE);
      orgAct.putValue(STATE, new Boolean(!state.booleanValue()));
      orgAct.actionPerformed(e);
    }
  }

   private static class ToggleAction extends AbstractAction {
    private Action orgAct;

    ToggleAction(Action orgAct) {
      this.orgAct = orgAct;
      // orgAct.putValue(STATE, new Boolean(false)); // state is kept with original action
    }

    public void actionPerformed( java.awt.event.ActionEvent e) {
      Boolean state = (Boolean) orgAct.getValue(STATE);
      orgAct.putValue(STATE, new Boolean(!state.booleanValue()));
      if (debugToggle)
        System.out.println("ToggleAction: "+ orgAct.getValue( Action.SHORT_DESCRIPTION)+" "+orgAct.getValue( BAMutil.STATE));
      orgAct.actionPerformed(e);
    }
  }

  private static class myActionChangedListener implements java.beans.PropertyChangeListener {
    private AbstractButton button;

    myActionChangedListener(AbstractButton b) {
      button = b;
    }
    public void propertyChange(java.beans.PropertyChangeEvent e) {
      String propertyName = e.getPropertyName();
      if (debugToggle) {
        System.out.print("myActionChangedListener "+ propertyName+ " "+ ((Boolean) e.getNewValue()));
        AbstractAction act = (AbstractAction) button.getAction();
        System.out.print(" "+ ((act == null) ? "" : act.getValue( Action.SHORT_DESCRIPTION)));
      }

      if (propertyName.equals(Action.NAME)) {
        String text = (String) e.getNewValue();
        button.setText(text);
        button.repaint();
      } else if (propertyName.equals("enabled")) {
        Boolean enabledState = (Boolean) e.getNewValue();
        button.setEnabled(enabledState.booleanValue());
        button.repaint();
      } else if (propertyName.equals(STATE)) {
        Boolean state = (Boolean) e.getNewValue();
        button.setSelected(state.booleanValue());
        button.repaint();
      }
    }
  }

}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package thredds.server.ncss.view.dsg.station;

import thredds.server.ncss.exception.FeaturesNotFoundException;
import thredds.server.ncss.exception.NcssException;
import thredds.server.ncss.view.dsg.DsgSubsetWriter;
import ucar.ma2.StructureData;
import ucar.nc2.ft.*;
import ucar.nc2.ft.point.PointIteratorFiltered;
import ucar.nc2.ft.point.StationFeature;
import ucar.nc2.ft.point.StationPointFeature;
import ucar.nc2.ft.point.StationTimeSeriesFeatureImpl;
import ucar.nc2.ft2.coverage.SubsetParams;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;
import ucar.unidata.geoloc.LatLonPoint;
import ucar.unidata.geoloc.LatLonPointImpl;
import ucar.unidata.geoloc.LatLonRect;
import ucar.unidata.geoloc.Station;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by cwardgar on 2014/05/20.
 */
public abstract class AbstractStationSubsetWriter extends DsgSubsetWriter {
  protected final StationTimeSeriesFeatureCollection stationFeatureCollection;
  protected final List<StationFeature> wantedStations;
  protected boolean headerDone = false;

  public AbstractStationSubsetWriter(FeatureDatasetPoint fdPoint, SubsetParams ncssParams)
          throws NcssException, IOException {
    super(fdPoint, ncssParams);

    List<DsgFeatureCollection> featColList = fdPoint.getPointFeatureCollectionList();
    assert featColList.size() == 1 : "Is there ever a case when this is NOT 1?";
    assert featColList.get(0) instanceof StationTimeSeriesFeatureCollection :
            "This class only deals with StationTimeSeriesFeatureCollections.";

    this.stationFeatureCollection = (StationTimeSeriesFeatureCollection) featColList.get(0);
    this.wantedStations = getStationsInSubset(stationFeatureCollection, ncssParams);

    if (this.wantedStations.isEmpty()) {
      throw new FeaturesNotFoundException("No stations found in subset.");
    }
  }

  protected abstract void writeHeader(StationPointFeature stationPointFeat) throws Exception;

  protected abstract void writeStationPointFeature(StationPointFeature stationPointFeat) throws Exception;

  protected abstract void writeFooter() throws Exception;

  @Override
  public void write() throws Exception {

    // Perform spatial subset.
    StationTimeSeriesFeatureCollection subsettedStationFeatCol = stationFeatureCollection.subsetFeatures(wantedStations);
    int count = 0;

    for (StationTimeSeriesFeature stationFeat : subsettedStationFeatCol) {

      // Perform temporal subset. We do this even when a time instant is specified, in which case wantedRange
      // represents a sanity check (i.e. "give me the feature closest to the specified time, but it must at
      // least be within an hour").
      StationTimeSeriesFeature subsettedStationFeat = stationFeat.subset(wantedRange);

      if (ncssParams.getTime() != null) {
        CalendarDate wantedTime = ncssParams.getTime();
        subsettedStationFeat = new ClosestTimeStationFeatureSubset(
                (StationTimeSeriesFeatureImpl) subsettedStationFeat, wantedTime);
      }

      count += writeStationTimeSeriesFeature(subsettedStationFeat);
    }

    if (count == 0)
      throw new NcssException("No features are in the requested subset");

    writeFooter();
  }

  protected int writeStationTimeSeriesFeature(StationTimeSeriesFeature stationFeat)
          throws Exception {
    int count = 0;
    for (PointFeature pointFeat : stationFeat) {
      assert pointFeat instanceof StationPointFeature :
              "Expected pointFeat to be a StationPointFeature, not a " + pointFeat.getClass().getSimpleName();

      if (!headerDone) {
        writeHeader((StationPointFeature) pointFeat);
        headerDone = true;
      }
      writeStationPointFeature((StationPointFeature) pointFeat);
      count++;
    }
    return count;
  }

  protected static class ClosestTimeStationFeatureSubset extends StationTimeSeriesFeatureImpl {
    private final StationTimeSeriesFeature stationFeat;
    private CalendarDate closestTime;

    protected ClosestTimeStationFeatureSubset(
            StationTimeSeriesFeatureImpl stationFeat, CalendarDate wantedTime) throws IOException {
      super(stationFeat, stationFeat.getTimeUnit(), stationFeat.getAltUnits(), -1);
      this.stationFeat = stationFeat;
      CalendarDateRange cdr = stationFeat.getCalendarDateRange();
      if (cdr != null) {
        getInfo();
        info.setCalendarDateRange(cdr);
      }

      long smallestDiff = Long.MAX_VALUE;

      stationFeat.resetIteration();
      try {
        while (stationFeat.hasNext()) {
          PointFeature pointFeat = stationFeat.next();
          CalendarDate obsTime = pointFeat.getObservationTimeAsCalendarDate();
          long diff = Math.abs(obsTime.getMillis() - wantedTime.getMillis());

          if (diff < smallestDiff) {
            closestTime = obsTime;
          }
        }
      } finally {
        stationFeat.finish();
      }
    }

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

    // Filter out PointFeatures that don't have the wantedTime.
    protected static class TimeFilter implements PointFeatureIterator.Filter {
      private final CalendarDate wantedTime;

      protected TimeFilter(CalendarDate wantedTime) {
        this.wantedTime = wantedTime;
      }

      @Override
      public boolean filter(PointFeature pointFeature) {
        return pointFeature.getObservationTimeAsCalendarDate().equals(wantedTime);
      }
    }

    @Override
    public PointFeatureIterator getPointFeatureIterator() throws IOException {
      if (closestTime == null) {
        return stationFeat.getPointFeatureIterator();
      } else {
        return new PointIteratorFiltered(
                stationFeat.getPointFeatureIterator(), new TimeFilter(closestTime));
      }
    }
  }

  // LOOK could do better : "all", and maybe HashSet<Name>
  public static List<StationFeature> getStationsInSubset(
          StationTimeSeriesFeatureCollection stationFeatCol, SubsetParams ncssParams) throws IOException {

    List<StationFeature> wantedStations;

    // verify SpatialSelection has some stations
    if (ncssParams.getStations() != null) {
      List<String> stnNames = ncssParams.getStations();

      if (stnNames.get(0).equals("all")) {
        wantedStations = stationFeatCol.getStationFeatures();
      } else {
        wantedStations = stationFeatCol.getStationFeatures(stnNames);
      }
    } else if (ncssParams.getLatLonBoundingBox() != null) {
      LatLonRect llrect = ncssParams.getLatLonBoundingBox();
      wantedStations = stationFeatCol.getStationFeatures(llrect);

    } else if (ncssParams.getLatLonPoint() != null) {
      Station closestStation = findClosestStation(stationFeatCol, ncssParams.getLatLonPoint());
      List<String> stnList = new ArrayList<>();
      stnList.add(closestStation.getName());
      wantedStations = stationFeatCol.getStationFeatures(stnList);

    } else { // Want all.
      wantedStations = stationFeatCol.getStationFeatures();
    }

    return wantedStations;
  }

  /*
   * Find the station closest to the specified point.
   * The metric is (lat-lat0)**2 + (cos(lat0)*(lon-lon0))**2
   *
   * @param lat latitude value
   * @param lon longitude value
   * @return name of station closest to the specified point
   * @throws IOException if read error
   */
  public static Station findClosestStation(StationTimeSeriesFeatureCollection stationFeatCol, LatLonPoint pt)
          throws IOException {
    double lat = pt.getLatitude();
    double lon = pt.getLongitude();
    double cos = Math.cos(Math.toRadians(lat));
    List<StationFeature> stations = stationFeatCol.getStationFeatures();
    Station min_station = stations.get(0);
    double min_dist = Double.MAX_VALUE;

    for (Station s : stations) {
      double lat1 = s.getLatitude();
      double lon1 = LatLonPointImpl.lonNormal(s.getLongitude(), lon);
      double dy = Math.toRadians(lat - lat1);
      double dx = cos * Math.toRadians(lon - lon1);
      double dist = dy * dy + dx * dx;
      if (dist < min_dist) {
        min_dist = dist;
        min_station = s;
      }
    }
    return min_station;
  }
}


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

import ucar.nc2.iosp.bufr.tables.TableB;
import ucar.nc2.iosp.bufr.tables.TableC;
import ucar.nc2.iosp.bufr.tables.TableD;

import java.util.Formatter;

/**
 * Static methods to manipulate the f-x-y descriptors
 * @author caron
 * @since Oct 25, 2008
 */
public class Descriptor {

  static public String makeString(short fxy) {
    int f = (fxy & 0xC000) >> 14;
    int x = (fxy & 0x3F00) >> 8;
    int y = fxy & 0xFF;
    return makeString(f,x,y);
  }

  static public String makeString(int f, int x, int y) {
    Formatter out = new Formatter();
    out.format("%d-%d-%d",f,x,y);
    return out.toString();
  }

  static public boolean isWmoRange(short fxy) {
    int x = (fxy & 0x3F00) >> 8;
    int y = fxy & 0xFF;
    return (x < 48 && y < 192);
  }

  static public short getFxy(String name) {
    String[] tok = name.split("-");
    int f = (tok.length > 0) ? Integer.parseInt(tok[0]) : 0;
    int x = (tok.length > 1) ? Integer.parseInt(tok[1]) : 0;
    int y = (tok.length > 2) ? Integer.parseInt(tok[2]) : 0;
    return (short) ((f << 14) + (x << 8) + (y));
  }

  static public short getFxy2(String fxxyyy) {
    int fxy = Integer.parseInt(fxxyyy.trim());
    int y = fxy % 1000;
    fxy /= 1000;
    int x = fxy % 100;
    int f1 = fxy / 100;
    return (short) ((f1 << 14) + (x << 8) + (y));
  }

  // contains a BUFR table entry
  static public boolean isBufrTable(short fxy) {
    int f = (fxy & 0xC000) >> 14;
    int x = (fxy & 0x3F00) >> 8;
    int y = (fxy & 0xFF);
    return (f == 0) && (x == 0) && (y < 13);
  }

  static public short getFxy(short f, short x, short y) {
    return (short) ((f << 14) + (x << 8) + (y));
  }

  static private final String[] descType = {"tableB", "replication", "tableC-operators", "tableD"};

  static public void show(Formatter out, short fxy, BufrTableLookup lookup) {
    int f = (fxy & 0xC000) >> 14;

    if (f == 0) {
      TableB.Descriptor b = lookup.getDescriptorTableB( fxy);
      if (b == null) out.format("%-8s: NOT FOUND!!", makeString(fxy) );
      else out.format("%-8s: %s", b.getFxy(), b.getName());

    } else if (f == 1)
      out.format("%-8s: %s", makeString(fxy), descType[1]);

    else if (f == 2) {
      int x = (fxy & 0x3F00) >> 8;
      out.format("%-8s: Operator= %s", makeString(fxy), TableC.getOperatorName(x));

    } else if (f == 3) {
      TableD.Descriptor d = lookup.getDescriptorTableD( fxy);
      if (d == null) out.format("%-8s: NOT FOUND!!", makeString(fxy) );
      else out.format("%-8s: %s", d.getFxy(), d.getName());
    }
  }

  static public String getName(short fxy, BufrTableLookup lookup) {
    int f = (fxy & 0xC000) >> 14;

    if (f == 0) {
      TableB.Descriptor b = lookup.getDescriptorTableB( fxy);
      if (b == null) return("**NOT FOUND!!");
      else return b.getName();

    } else if (f == 1)
      return descType[1];

    else if (f == 2) {
      int x = (fxy & 0x3F00) >> 8;
      return TableC.getOperatorName(x);

    } else if (f == 3) {
      TableD.Descriptor d = lookup.getDescriptorTableD( fxy);
      if (d == null) return "**NOT FOUND!!";
      else return d.getName();
    }

    return "illegal F="+f;
  }
}


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

package ucar.nc2.dataset;

import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.constants.AxisType;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.dataset.conv.CF1Convention;
import ucar.nc2.dataset.conv.COARDSConvention;
import ucar.nc2.time.Calendar;

import java.io.IOException;
import java.util.Formatter;

/**
 * A Coordinate Axis is a Variable that specifies one of the coordinates of a CoordinateSystem.
 * Mathematically it is a scalar function F from index space to S:
 *

 *  F:D -> S
 *  where D is a product set of dimensions (aka index space), and S is the set of reals (R) or Strings.
 *

 *

 * If its element type is char, it is considered a string-valued Coordinate Axis and rank is reduced by one,
 * since the outermost dimension is considered the string length: v(i, j, .., strlen).
 * If its element type is String, it is a string-valued Coordinate Axis.
 * Otherwise it is numeric-valued, and isNumeric() is true.
 *

 * The one-dimensional case F(i) -> R is the common case which affords important optimizations.
 * In that case, use the subtype CoordinateAxis1D. The factory methods will return
 * either a CoordinateAxis1D if the variable is one-dimensional, a CoordinateAxis2D if its 2D, or a
 * CoordinateAxis for the general case.
 *

 * A CoordinateAxis is optionally marked as georeferencing with an AxisType. It should have
 * a units string and optionally a description string.
 *

 * A Structure cannot be a CoordinateAxis, although members of Structures can.
 *
 * @author john caron
 */

public class CoordinateAxis extends VariableDS {
  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CoordinateAxis.class);
  static public int axisSizeToCache = 100 * 1000; // bytes

  protected NetcdfDataset ncd; // container dataset
  protected AxisType axisType = null;
  protected String positive = null;
  protected String boundaryRef = null;
  protected boolean isContiguous = true;

  /**
   * Create a coordinate axis from an existing Variable.
   *
   * @param ncd the containing dataset
   * @param vds an existing Variable in dataset.
   * @return CoordinateAxis or one of its subclasses (CoordinateAxis1D, CoordinateAxis2D, or CoordinateAxis1DTime).
   */
  static public CoordinateAxis factory(NetcdfDataset ncd, VariableDS vds) {
    if ((vds.getRank() == 0) || (vds.getRank() == 1) || (vds.getRank() == 2 && vds.getDataType() == DataType.CHAR)) {
        return new CoordinateAxis1D(ncd, vds);
    } else if (vds.getRank() == 2)
      return new CoordinateAxis2D(ncd, vds);
    else
      return new CoordinateAxis(ncd, vds);
  }

  /**
   * Create a coordinate axis from an existing Variable.
   * General case.
   *
   * @param ncd the containing dataset
   * @param vds an existing Variable
   */
  protected CoordinateAxis(NetcdfDataset ncd, VariableDS vds) {
    super(vds, false);
    this.ncd = ncd;

    if (vds instanceof CoordinateAxis) {
      CoordinateAxis axis = (CoordinateAxis) vds;
      this.axisType = axis.axisType;
      this.boundaryRef = axis.boundaryRef;
      this.isContiguous = axis.isContiguous;
      this.positive = axis.positive;
    }
    setSizeToCache(axisSizeToCache);
  }

  /**
   * Constructor when theres no underlying variable. You better set the values too!
   *
   * @param ds        the containing dataset.
   * @param group     the containing group; if null, use rootGroup
   * @param shortName axis name.
   * @param dataType  data type
   * @param dims      list of dimension names
   * @param units     units of coordinates, preferably udunit compatible.
   * @param desc      long name.
   */
  public CoordinateAxis(NetcdfDataset ds, Group group, String shortName, DataType dataType, String dims, String units, String desc) {
    super(ds, group, null, shortName, dataType, dims, units, desc);
    this.ncd = ds;
    setSizeToCache(axisSizeToCache);
  }

  /**
   * Make a copy, with an independent cache.
   *
   * @return copy of this CoordinateAxis
   */
  public CoordinateAxis copyNoCache() {
    CoordinateAxis axis = new CoordinateAxis(ncd, getParentGroup(), getShortName(), getDataType(), getDimensionsString(),
            getUnitsString(), getDescription());

    // other state
    axis.axisType = this.axisType;
    axis.boundaryRef = this.boundaryRef;
    axis.isContiguous = this.isContiguous;
    axis.positive = this.positive;

    axis.cache = new Variable.Cache(); // decouple cache
    return axis;
  }

  // for section and slice

  @Override
  protected Variable copy() {
    return new CoordinateAxis(this.ncd, this);
  }

  /**
   * Get type of axis
   *
   * @return type of axis, or null if none.
   */
  public AxisType getAxisType() {
    return axisType;
  }

  /**
   * Set type of axis, or null if none. Default is none.
   *
   * @param axisType set to this value
   */
  public void setAxisType(AxisType axisType) {
    this.axisType = axisType;
  }

  @Override
  public String getUnitsString() {
    String units = super.getUnitsString();
    return units == null ? "" : units;
  }

  /**
   * Does the axis have numeric values.
   *
   * @return true if the CoordAxis is numeric, false if its string valued ("nominal").
   */
  public boolean isNumeric() {
    return (getDataType() != DataType.CHAR) &&
            (getDataType() != DataType.STRING) &&
            (getDataType() != DataType.STRUCTURE);
  }

  /**
   * If the edges are contiguous or disjoint
   * Caution: many datasets do not explicitly specify this info, this is often a guess; default is true.
   *
   * @return true if the edges are contiguous or false if disjoint. Assumed true unless set otherwise.
   */
  public boolean isContiguous() {
    return isContiguous;
  }

  /**
   * An interval coordinate consists of two numbers, bound1 and bound2.
   * The coordinate value must lie between them, but otherwise is somewhat arbitrary.
   * If not interval, then it has one number, the coordinate value.
   * @return true if its an interval coordinate.
   */
  public boolean isInterval() {
    return false; // interval detection is done in subclasses
  }

  public boolean isIndependentCoordinate() {
    if (isCoordinateVariable()) return true;
    return null != findAttribute(_Coordinate.AliasForDimension);
  }

  /*
   * Set if the edges are contiguous or disjoint.
   *
   * @param isContiguous true if the adjacent edges touch
   *
  protected void setContiguous(boolean isContiguous) {
    this.isContiguous = isContiguous;
  } */

  /**
   * Get the direction of increasing values, used only for vertical Axes.
   *
   * @return POSITIVE_UP, POSITIVE_DOWN, or null if unknown.
   */
  public String getPositive() {
    return positive;
  }

  /**
   * Set the direction of increasing values, used only for vertical Axes.
   *
   * @param positive POSITIVE_UP, POSITIVE_DOWN, or null if you dont know..
   */
  public void setPositive(String positive) {
    this.positive = positive;
  }

  /**
   * The name of this coordinate axis' boundary variable
   *
   * @return the name of this coordinate axis' boundary variable, or null if none.
   */
  public String getBoundaryRef() {
    return boundaryRef;
  }

  /**
   * Set a reference to a boundary variable.
   *
   * @param boundaryRef the name of a boundary coordinate variable in the same dataset.
   */
  public void setBoundaryRef(String boundaryRef) {
    this.boundaryRef = boundaryRef;
  }

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

  private MAMath.MinMax minmax = null;

  private void init() {
    try {
      Array data = read();
      minmax = MAMath.getMinMax(data);
    } catch (IOException ioe) {
      log.error("Error reading coordinate values ", ioe);
      throw new IllegalStateException(ioe);
    }
  }

  /**
   * The smallest coordinate value. Only call if isNumeric.
   *
   * @return the minimum coordinate value
   */
  public double getMinValue() {
    if (minmax == null) init();
    return minmax.min;
  }

  /**
   * The largest coordinate value. Only call if isNumeric.
   *
   * @return the maximum coordinate value
   */
  public double getMaxValue() {
    if (minmax == null) init();
    return minmax.max;
  }

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

  /**
   * Get a string representation
   *
   * @param buf place info here
   */
  public void getInfo(Formatter buf) {
    buf.format("%-30s", getNameAndDimensions());
    buf.format("%-20s", getUnitsString());
    if (axisType != null) {
      buf.format("%-10s", axisType.toString());
    }
    buf.format("%s", getDescription());

    /* if (isNumeric) {
     boolean debugCoords = ucar.util.prefs.ui.Debug.isSet("Dataset/showCoordValues");
     int ndigits = debugCoords ? 9 : 4;
     for (int i=0; i< getNumElements(); i++) {
       buf.append(Format.d(getCoordValue(i), ndigits));
       buf.append(" ");
     }
     if (debugCoords) {
       buf.append("\n      ");
       for (int i=0; i<=getNumElements(); i++) {
         buf.append(Format.d(getCoordEdge(i), ndigits));
         buf.append(" ");
       }
     }
   } else {
     for (int i=0; i< getNumElements(); i++) {
       buf.append(getCoordName(i));
       buf.append(" ");
     }
   } */

    //buf.append("\n");
  }

  /**
   * Standard  sort on Coordinate Axes
   */
  static public class AxisComparator implements java.util.Comparator<CoordinateAxis> {
    public int compare(CoordinateAxis c1, CoordinateAxis c2) {

      AxisType t1 = c1.getAxisType();
      AxisType t2 = c2.getAxisType();

      if ((t1 == null) && (t2 == null))
        return c1.getShortName().compareTo(c2.getShortName());
      if (t1 == null)
        return -1;
      if (t2 == null)
        return 1;

      return t1.axisOrder() - t2.axisOrder();
    }

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

  /**
   * Instances which have same content are equal.
   */
  public boolean equals(Object oo) {
    if (this == oo) return true;
    if (!(oo instanceof CoordinateAxis)) return false;
    if (!super.equals(oo)) return false;
    CoordinateAxis o = (CoordinateAxis) oo;

    if (getAxisType() != null)
      if (!getAxisType().equals(o.getAxisType())) return false;

    if (getPositive() != null)
      if (!getPositive().equals(o.getPositive())) return false;

    return true;
  }

  /**
   * Override Object.hashCode() to implement equals.
   */
  public int hashCode() {
    int result = super.hashCode();
    if (getAxisType() != null)
      result = 37 * result + getAxisType().hashCode();
    if (getPositive() != null)
      result = 37 * result + getPositive().hashCode();
    return result;
  }

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

  // needed by time coordinates
  public ucar.nc2.time.Calendar getCalendarFromAttribute() {
    Attribute cal = findAttribute(CF.CALENDAR);
    String s = (cal == null) ? null : cal.getStringValue();
    if (s == null) {     // default for CF and COARDS
      Attribute convention = (ncd == null) ? null : ncd.getRootGroup().findAttribute(CDM.CONVENTIONS);
      if (convention != null) {
        String hasName = convention.getStringValue();
        int version = CF1Convention.getVersion(hasName);
        if (version >= 0) {
          return Calendar.gregorian;
          //if (version < 7 ) return Calendar.gregorian;
          //if (version >= 7 ) return Calendar.proleptic_gregorian; //
        }
        if (COARDSConvention.isMine(hasName)) return Calendar.gregorian;
      }
    }
    return ucar.nc2.time.Calendar.get(s);
  }

  /* @Override
  public boolean isCoordinateVariable() {
    return true;
  } */

}