package nxm.sys.libg;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import java.util.Vector;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.regex.Pattern;

import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import nxm.sys.inc.Drawable;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.PlotFile;
import nxm.sys.lib.Args;
import nxm.sys.lib.Cache;
import nxm.sys.lib.FileName;
import nxm.sys.lib.FileUtil;
import nxm.sys.lib.GeodeticUtil;
import nxm.sys.lib.IOResource;
import nxm.sys.lib.KmlFile;
import nxm.sys.lib.LogUtilities;
import nxm.sys.lib.Message;
import nxm.sys.lib.Position;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.Time;

/**
  A class to hold and display a KML layer.
  For complete documentation on KML documents, see the Google KML reference.
  <br><br>
  This class consists of the Layer itself, the Parser, a class to hold the
  collection of KML objects parsed, and classes for each of the KML objects
  themselves. <br>
  <br>
  The process flow is as follows:<br>
    1. The user plots a *.kml file using the plot command.<br>
    2. Plot (and MPlot) open the file and call {@link #setFile(PlotFile, Table, Args)}<br>
    3. <code>setFile</code> calls the parser on the file.<br>
    4. The parser (which uses the Java SAX XML parser) reads the the file line
       by line. When it encounters a start tag for an element which is
       supported by the Layer, it does one of two things: <br>
       1) If the element is a 'parent' element, which means it has its own kml
       object class and contains child nested data, the parser creates a new kml
       object of that element type and puts it on the stack.<br>
       2) If the element only contains data (such as name or description),it
       creates a StringBuffer to hold the data, and that StringBuffer is put on
       the stack. Also, a flag is set to indicate that the next data to arrive
       needs to be stored. Data is characterized as any text not contained
       within element tags. This data is stored in the StringBuffer on the stack.<br>
    5. When the parser encounters a end tag for a supported element, it does one
       of two things: <br>
       1) If the element is a 'parent' element, with no parent above it (such as
       a document tag), it is popped off the stack and the object is placed in
       the kmlObjects class.<br>
       2) If the element has a parent element above it, the child element is
       popped off the stack and added to the parent kml Object (which is right
       above it on the stack) via a 'set' method.<br>
    6. When the parser reaches the end of the kml file (the closing kml tag), it
       sends the collection of kml Objects back to the Layer.<br>
    7. The Layer draw method is called automatically by MPlot. The Layer draw
       initiates the processing to break down the collection of kml Objects.
       Some objects are stored (such as kml Styles) for later use, while the
       drawable objects (kml Geometry objects such as linnear rings, polygons,
       points, etc) are drawn.<br>
       <br>
       <br>
       Current supported elements are found in {@link #supportedElements}.
       <br>

  @author James Bauer
  @since NeXtMidas 2.7.0
 */
public class LayerKML extends Layer {

  /** Coloring mode: Monochrome (1-color) (see {@link #coloringModes}). */
  public static final int COLORS_MONOCHROME = 1;
  /** Coloring mode: Mod 5 (NXM200 default) (see {@link #coloringModes}). */
  public static final int COLORS_MOD_5      = 2;
  /** Coloring mode: Mod N (N = num colors) (see {@link #coloringModes}). */
  public static final int COLORS_MOD_N      = 3;
  /** Coloring mode: Minimize adj with same color (see {@link #coloringModes}). */
  public static final int COLORS_MIN_ADJ    = 4;
  /** Coloring mode: Color selected item only (see {@link #coloringModes}). */
  public static final int COLORS_ON_SELECT  = 5;
  /** Default coloring mode. This may change between releases. */
  public static final int COLORS_DEFAULT = COLORS_MIN_ADJ;
  /** The coloring modes. */
  public static final String coloringModes = "Mono,Mod5,ModN,MinAdj,OnSelect";
  /** Alpha */
  public static final int OPAQ             = 255;
  /** The default border color */
  public static final Color DEFAULT_BORDER_COLOR = java.awt.Color.lightGray;
  /** The default point color */
  public static final Color DEFAULT_POINT_COLOR = java.awt.Color.WHITE;
  /** Shape polygon border color */
  private Color colorBorder = DEFAULT_BORDER_COLOR;

  /** List of currently supported KML element tags.  */
  public static final String supportedElements = "kml,coordinates,point,polygon,placemark,linearring,"  // TODO: organize this better
    + "linestring,name,description,document,style,key,linestyle,stylemap,pair,"
    + "polystyle,color,width,bgcolor,extrude,url,"
    + "altitudemode,heading,iconstyle,icon,scale,"
    + "outerboundaryis,fill,outline,multigeometry,"
    + "visibility,networklinkcontrol,folder,styleurl,"
    + "networklink,link,refreshinterval,groundoverlay,"
    + "overlay,lookat,minrefreshperiod,refreshmode,"
    + "longitude,latitude,rotation,range,heading,"
    + "tilt,range,altitude,href,north,south,east,west,"
    + "labelstyle,balloonstyle,text,bgcolor,textcolor,displaymode,"
    + "viewformat,schema";

  private static final Set<String> supportedElems = new HashSet<>(Arrays.asList(supportedElements.split("\\s*,\\s*")));

  /** Search pattern when breaking down coordinate sets */
  private static final Pattern EOL = Pattern.compile("\\s+");

  // === Types of Points ===
  /** Point Type of Symbol = 1 */
  private final int SYMBOL = 1;
  /** Point Type of Image = 2 */
  private final int IMAGE  = 2;

  private final int      MAX_ICON_SIZE              = 32; // Max height/width (in pixels) for icon images
  private final int      DEFAULT_LINE_WIDTH         = 2;
  private final int      DEFAULT_TEXT_SIZE          = 12;
  private final int      DEFAULT_POINT_SIZE         = 8;
  private final float    DEFAULT_ICON_SCALE         = 1.0f;
  private final int      DEFAULT_ICON_HEADING       = 0;
  private final String   DEFAULT_POINT_SYMBOL       = "Cross";
  private final int      DEFAULT_NON_ZOOM_ICON_SIZE = 20;
  private final int      DEFAULT_ICON_ZOOM_SIZE     = 64;
  private final float    DEFAULT_COLOR_BLEND_RATIO  = .8f;
  private final String[] LABEL_POSITIONS            = {"TOPR","TOP","BOTTOMR","BOTTOM","RIGHT"};
  private final String   DEFAULT_LABEL_POS          = "TOPR";

  /** Max height/width (in pixels) for non-Zoomed icon images */
  private int  iconSize     = DEFAULT_NON_ZOOM_ICON_SIZE;
  /** Max height/width (in pixels) for Zoomed icon images */
  private int  iconZoomSize = DEFAULT_ICON_ZOOM_SIZE;

  /** Ratio of Original color to blended color when coloring Images.
      Must be between 0 and 1. Use a low number (e.g. 0.1f) to heavily
      blend (tint) the original and a high number (e.g. 0.9f) to highly blend.*/
  private float   colorBlendRatio = DEFAULT_COLOR_BLEND_RATIO;

  /** Where to place the label for points.
      <pre> Acceptable values are:
            TOPR    - Label text starts near top right corner of point. (default)
            TOP     - Label text is centered above point.
            BOTTOMR - Label text starts near bottom right corner of plot.
            BOTTOM  - Label text is centered below point.
            RIGHT   - Label text starts to the right of the point.
      </pre>
   */
  private String  labelPos = DEFAULT_LABEL_POS;

  private int     coloringMode     = COLORS_DEFAULT; // The coloring mode being used.
  private int     contrast         = 50;             // Percentage of contrast (0..100).
  private int     brightness       = 50;             // Percentage of brightness (0..100).
  private int     alpha            = -1;             // True alpha level (0..255). May be computed from the opaqueness.

  private   KmlParser        kmlParser;              // Instance of the parser
  protected kmlObjects       objects;                // (Supported) KML objects in the file

  private HashMap<String,kmlStyle>     styles;       // KML Styles in the Layer
  private HashMap<String,kmlStyleMap>  styleMaps;    // Track Style IDs to the Style Map
  private ArrayList<kmlPoint>          points;       // All points that have been drawn on. Used in MousePopups
  // the next 3 were added in 3.9.1 for mouse popups
  private ArrayList<kmlPolygon>        polygons;     // All polygons that have been drawn on. Used in MousePopups
  private ArrayList<KmlImage>          images;       // All images that have been drawn on. Used in MousePopups
  private ArrayList<kmlLineString>     lineStrings;  // All the line strings that are not assigned to a placemark for drawing (schemas)
  /* The four variables below are for MPLOT's drawLine method, which is used for most drawing */
  private double[] buf;          // Holds the coordinates to be plotted
  private int      off  = 0;     // Starting point offset in buf. Usually 0.
  private int      dim  = 3;     // 3 = (x,y,z), 2 = (x,y). We usually use 3 (altitude default to 0).
  private int      npts;         // Number of points. This is (buf.length / 3)

  private boolean  polygonFill    = true;
  private boolean  polygonOutline = true;
  private Color    polygonColor   = DEFAULT_POINT_COLOR;
  private Color    lineColor      = DEFAULT_POINT_COLOR;
  private int      lineWidth      = DEFAULT_LINE_WIDTH;;
  private int      textSize       = DEFAULT_TEXT_SIZE;    // Size of the text to display next to the point (label)
  private float    labelScale     = 1;                    // Scale multiplier for labels next to points
  private Color    labelColor     = DEFAULT_POINT_COLOR;  // Color of text next to points
  private int      pointType      = SYMBOL;               // Default point type is SYMBOL
  private String   pointSymbol    = DEFAULT_POINT_SYMBOL; // When kml Icon is a known Symbol
  private BufferedImage             pointImage;           // When kml Icon is an image
  private int      pointSize      = DEFAULT_POINT_SIZE;
  private float    iconHeading    = DEFAULT_ICON_HEADING; // Default to North
  private float    iconScale      = DEFAULT_ICON_SCALE;   // Multiplier (x pointSize) for displaying Symbol/Icon
  private Color    iconColor      = DEFAULT_POINT_COLOR;
  private boolean  symbolFill     = true;
  private boolean  showLabel      = true;         // whether or not to draw the label next to the point
  private double[] pollRate;                      // 0.0 means no polling
  private double[] lastPoll;                      // time when linked file was last polled
  private double[] lastMods;                      // time (seconds) when linked file was last modified - since 3.9.1 for NXM-3343
  private double   minRefreshPeriod = 0;          // override using pollrate
  private String   currPath;                      // path to where the KML File resides
  private kmlBalloonStyle balloonStyle;           // the current kmlBalloonStyle
  private boolean  allowPopups    = true;         // allow balloon popups for KML points
  private boolean  keepPopupSmall = false;        // force balloon popups to stay small

  private boolean        activePopup;    // Keeps track if there was an active Popup when the Layer was hidden
  private kmlObject      selectedObject; // The selected kmlObject
  private PopupSelection popupSelection; // The PopupSelection info
  private Popup          kmlPopup;       // The KML Popup for the selected kmlObject

  private FileName fn;

  /** files used when using network polling */
  private KmlFile[] linkedFile     = null;
  /** ignore poll interval and pollrate for the first draw per file */
  private boolean[]  isFirstPass;
  /** Refresh the plot even if the file modified date has not changed (e.g. cgi scripts) */
  private boolean forceRefresh   = false;
  protected static final WeakHashMap<String,Cache.TimeStampedValue> SHARED_VCACHE = new WeakHashMap<>();  // Shared image cache.
  protected final  Cache            imgCache        = getDefImgCache();    // The image cache to use.
  private          int              imgCacheSize    = -1;                  // Size of image cache (-1=UseDefault)

  /** Default constructor. */
  public LayerKML() {
    super();

    /* Set up the Line to be used for drawing */
    line.setRadius(3);
    line.setSymbol(getPointSymbol());
    line.getSymbol().setSize(getPointSize());
    line.setType(Line.SOLID);
    line.setColor(coloringMode);

    /* Initialize objects */
    kmlParser    = new KmlParser();
    objects      = new kmlObjects();
    styles       = new HashMap<>();
    styleMaps    = new HashMap<>();

    setCacheSize(-1);  // set cache size default to -1

    points = new ArrayList<>();

    // make sure there is at least one boolean in the array
    isFirstPass = new boolean[1];
    isFirstPass[0] = true;

    // Set initial values (over-ridden by coordinate values)
    x1 =  180;   // max possible x value
    x2 = -180;   // min possible x value
    y1 =  90;    // max possible y value
    y2 = -90;    // min possible y value
    z1 = 0.0;    // default
    z2 = 10000;  // default
  }

  /** Creates a new instance.
      @param cmap The color map to use.
   */
  public LayerKML (int cmap) {
    this();
    setColorMap(cmap);
    name = "KML";
  }

  /** Extracts the filename and calls the parser */
  public boolean setFile (PlotFile pf, Table tab, Args MA) {
    boolean status = true;
    fn = FileName.toFileName(pf.getName());

    currPath =  fn.getPath();  // save path for later use when making URLs for local files

    // check for URL path added in 3.3.2 for seeing files inside KMZ dir
    if (currPath==null || currPath.isEmpty()) {
      try {
        String urlpath = fn.toURL().toString();
        urlpath = urlpath.substring(0,(urlpath.indexOf(fn.getBasename()))); // path is the full path+name minus filename
        currPath = urlpath;
      } catch (MalformedURLException e) {
        // do nothing
      }
    }

    /* Check to see if the file is a KML file. */
    status = checkFileType(pf);

    /* If it is a kml file, parse it */
    if (status) {
      super.setFile(pf);
      if (tab != null) {
        int iconSize = (tab.getL("ICONSIZE", 0));
        if (iconSize > 0) this.setIconSize(iconSize);

        int iconZoomSize = (tab.getL("ICONZOOMSIZE", 0));
        if (iconZoomSize > 0) this.setIconZoomSize(iconZoomSize);

        float ratio = (tab.getF("COLORBLENDRATIO", -1.0f));
        if (ratio >= 0) this.setColorBlendRatio(ratio);

        String pos = (tab.getS("LABELPOS", null));
        if (pos != null) setLabelPos(pos);

        boolean forceRefresh = (tab.getState("FORCEREFRESH",this.forceRefresh));
        setForceRefresh(forceRefresh);

        boolean allowPopups = (tab.getState("ALLOWPOPUPS",this.allowPopups));
        setAllowPopups(allowPopups);
      }
      pf.open(1);  // Open as input
      InputStream is = ((KmlFile)pf).getResource().getInputStream(); // Get file data as InputStream
      parseFile(is);                      // Parse the file

    } else {
      warning("File " +pf.getName()+ " is not recognized as a KML file");
    }
    return status;
  }

  /** Check the file type to see if it is KML */
  private boolean checkFileType (PlotFile pf) {
    if (pf instanceof KmlFile) return true;
    else return false;
  }

  /** Calls the kmlParser on the file */
  private void parseFile (InputStream fname) {
    try {
      getKmlParser().parseKmlFile(fname);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /** Processes mouse movement messages to create balloon popups.
      @param msg  The Mouse message
      @param type Type of message
      @since NeXtMidas 2.7.2
   */
  public void processMouseMessage(Message msg, int type) {
    Position pos = (Position)msg.getData();
    if (msg.name.equals("HOVER")) {
      if (!testForPopup(pos)) super.processMouseMessage(msg, type);
    }
  }

  /** set whether popups are allowed
   *  @param val true to allow popups
   */
  public void setAllowPopups (boolean val) { allowPopups = val; }

  /** are popups allowed
   *  @return true if popups are allowed
   */
  public boolean isAllowPopups () { return allowPopups; }

  /** Gets the closest point to the given Position and displays a <code>Popup</code> for it */
  private boolean testForPopup (Position pos) {
    // logic for this method re-written in 3.3.2 to improve performance and selection
    if (!allowPopups || (points.size()==0 && getImages().size() == 0 && getPolygons().size() == 0 && getLineStrings().size() == 0)) return false;

    double lonRange = Math.abs(MP.rx2 - MP.rx1);  // width of the plot in LON
    double step     = lonRange / 200;             // how far to increment when looking for points
    double range    = step * 20;                  // how close the mouse must be to the point
    //String[] keys   = points.;
    for (double ii=0.0; ii<=range; ii+=step) {
      // spiral out from the mouse click
      for (kmlPoint p : points) {
        double xdist = Math.abs(pos.x - p.getCoords().lon);
        if (xdist <= ii) {
          double ydist = Math.abs(pos.y - p.getCoords().lat);
          if (ydist <= ii) {
            // Do not popup if the BalloonStyle displayMode is set to 'hide'
            kmlBalloonStyle style = p.getBalloonStyle();
            if (style != null) {
              String mode = style.getDisplayMode();
              if (mode != null && mode.equalsIgnoreCase("hide")) return false;
            }
            // The next two lines find the mouse pos
            MPoint p1 = MP.getPoint(pos);
            Point  p2 = MP.panel.getLocationOnScreen();

            setSelectedObject(p);
            popupSelection = new PopupSelection(p,(p1.x+p2.x),(p1.y+p2.y),null);
            drawPopup(); // popup a frame showing info about the kmlPoint
            return true;  // only get the closest point to the mouse
          }
        }
      }

      for (KmlImage ti : getImages()) {
        double [] polygon = new double[] {ti.west, ti.north, ti.east, ti.north, ti.east, ti.south, ti.west, ti.south, ti.west, ti.north};
        if (GeodeticUtil.insidePolygon(pos, 2, polygon)) {
          MPoint p1 = MP.getPoint(pos);
          Point  p2 = MP.panel.getLocationOnScreen();
          kmlPoint p = new kmlPoint(ti);
          p.setLine(line.copy());
          p.setBalloonStyle(balloonStyle);
          p.setCoordinates(ti.west + "," + ti.north);
          p.setImage(ti.img);
          setSelectedObject(ti);
          popupSelection = new PopupSelection(p,(p1.x+p2.x),(p1.y+p2.y),ti.href);
          drawPopup(); // popup a frame showing info about the kmlPoint
          return true;  // only get the closest point to the mouse
        }
      }

      for (kmlPolygon polygon : getPolygons()) {
        if (GeodeticUtil.insidePolygon(pos, dim, polygon.outerBoundary.getCoordsAsDouble())) {
          MPoint p1 = MP.getPoint(pos);
          Point  p2 = MP.panel.getLocationOnScreen();
          kmlPoint p = new kmlPoint(polygon);
          p.setLine(line.copy());
          p.setBalloonStyle(balloonStyle);
          p.setCoordinates(pos.lon + "," + pos.lat);
          setSelectedObject(polygon);
          popupSelection = new PopupSelection(p,(p1.x+p2.x),(p1.y+p2.y),polygon.getParentFeature().name);
          drawPopup(); // popup a frame showing info about the kmlPoint
          return true; // only get the closest point to the mouse
        }
      }

      for (kmlLineString lineString : getLineStrings()) {
        if (GeodeticUtil.insidePolygon(pos, dim, lineString.getCoordsAsDouble())) {
          MPoint p1 = MP.getPoint(pos);
          Point  p2 = MP.panel.getLocationOnScreen();
          kmlPoint p = new kmlPoint(lineString);
          p.setLine(line.copy());
          p.setBalloonStyle(balloonStyle);
          p.setCoordinates(pos.lon + "," + pos.lat);
          setSelectedObject(lineString);
          popupSelection = new PopupSelection(p,(p1.x+p2.x),(p1.y+p2.y),lineString.getParentFeature().name);
          drawPopup(); // popup a frame showing info about the kmlPoint
          return true; // only get the closest point to the mouse
        }
      }
    }

    return false;  // no point in range
  }

  /** Check {@link #linkedFile} to see if it has been modified. */
  private boolean linkedFileModified (KmlFile linkedFile, int index) {
    long lastMod = -1;
    URL url = null;
    try {
      url = (new URI(linkedFile.getURL())).toURL();
      // This is the first time with this file so it needs to be loaded
      // save off the current modified time for future checks
      // since 3.9.1
      if (isFirstPass[index]) {
        lastMods[index] = FileUtil.lastModified(url);
        return true;
      }
    } catch (MalformedURLException | URISyntaxException e1) {
      LogUtilities.log(Level.WARNING, "Error checking if LayerKML file has been changed.", e1);
    }
    if (url != null) lastMod = FileUtil.lastModified(url);
    // Changed to check the diff based on previous modified time and if it is changed.
    double fileChangeDif = lastMod - (lastMods[index]);
    if ( fileChangeDif > 0){
      lastMods[index] = lastMod;
      return true;
    } else return false;
  }

  /** Calls setFile (which will parse and plot a file) on the file set in
      {@link #handleKmlNetworkLink(kmlNetworkLink, int)}
   */
  private void pollData (KmlFile linkedFile, double pollRate, int index) {
    if (linkedFile != null) {
      if (linkedFileModified(linkedFile, index) || forceRefresh) {
        if (index>0) {    // not the 'base' kml file
          boolean newLayer = true;
          Layer   lay;
          PlotFile file = null;
          for (int idx=0; idx<MP.layers.size(); idx++) {
            lay = (Layer)MP.layers.get(idx);
            if (lay != null) file = lay.getFile();
            if (file != null && linkedFile.equals(file)) newLayer = false;
          }
          if (newLayer) {
            linkedFile.setQualifier("FG", "KML");   // tell Plot to open as a KML file ...
            // Added in 3.9.1 to propogate the popups to any linked files
            Table tbl = new Table();
            tbl.addIfNotPresent("ALLOWPOPUPS", allowPopups);
            linkedFile.setQualifier("LAYER",tbl );
            MP.mh.processMessage(new Message("OPENFILE",0,linkedFile.getName().toString()+linkedFile.getQualifiers()));
          }
        } else setFile(linkedFile, null, null);
        isFirstPass[index] = false;
        super.needRefresh = true;  // refreshes the display with the new data
      }
    }
  }

  /** Checks the Poll Rate and calls pollData if the rate has expired */
  public int getData (int flag) {
    if (linkedFile != null) {
      for (int idx=0; idx<linkedFile.length; idx++) {
        if (Time.current() >= (pollRate[idx] + lastPoll[idx])) {
          pollData(linkedFile[idx], pollRate[idx], idx);
          lastPoll[idx] = Time.current();
        }
      }
    }
    return Drawable.FULL;
  }

  /** Draw the KML objects from the file
      This method is called by MPLOT and represents the 'Launch Pad' for the
       drawing and handling of KML Objects.
   */
  public void draw (int mode) {
    super.draw(mode);
    points = new ArrayList<>();   // clear out references to previously drawn kmlPoints
    polygons = new ArrayList<>();
    images = new ArrayList<>();
    lineStrings = new ArrayList<>();
    handleKmlObjects(objects);   // Break apart the KML objects and draw them
  }

  /** This method is called by the <code>drawPolygon</code> methods since the
       setup for the Line is the same for each of them.
      @return The Line to draw.
   */
  private Line getPolyLine () {
    Line myLine = line.copy();

    Color oldColor = myLine.getColor();        // Current Color
    Color newColor = getLineColor();           // Color specified in kml LineStyle, [def=white]

    /* Check for NULL (should not happen because def=white) and check against
        current color (to avoid needless refresh)  */
    if (newColor != null) {
      if (!oldColor.equals(newColor)) myLine.setColor(newColor);
    }

    oldColor = myLine.getFillColor();     // Current fill color
    newColor = getPolygonColor();         // Fill color specified in kml PolyStyle, [def=white]

    /* Check for NULL (should not happen because def=white) and check for
        polygonFill is true before setting fill color and Line type to 'FILL' */
    if (newColor != null && isPolygonFill() == true) {
      if (!oldColor.equals(newColor)) myLine.setFillColor(newColor);
      myLine.setType(myLine.getType()+"Fill");   // Fill in the polygons if polygonFill is true
    }

    /* Set the thickness */
    if (myLine.getThickness() != getLineWidth()) myLine.setThickness(getLineWidth());

    return myLine;
  }

  /** Draw a kml Point */
  private int drawPoint (kmlPoint point) {
    int status = 0;
    npts = 1;   // Only one point at a time

    /* Load the coordinates of the point into a double[] */
    buf    = new double[3];
    buf[0] = point.getCoords().getLon();
    buf[1] = point.getCoords().getLat();
    buf[2] = point.getCoords().getAltitude();

    Line myLine = line.copy();                   // Copy the Line for drawing

    /* Line and Symbol setup. These are adjusted in the kml source file using the
        LineStyle and IconStyle tags.  */
    myLine.setType("Symbol");                    // Set the type to Symbol
    BufferedImage drawableImage = null;    // mods done to new image to preserve old one
    if (pointType == IMAGE && pointImage==null) {
      setPointType(SYMBOL);
      setPointSymbol(DEFAULT_POINT_SYMBOL);
    }
    if (pointType == IMAGE) {
      drawableImage = getDrawableImage();  // mods done to new image to preserve old one
      if (iconColor != null) applyPointImageColor(drawableImage);
      if (iconScale != 1.0f) drawableImage = scalePointImage(drawableImage, iconScale);  // apply scale to image icon, if necessary
      // logic for when to shrink icon changed in 3.3.2 - remove these two lines if no issues with users
//      if (MP.getZoomLevel() == 0) drawableImage = adjustPointImageSize(drawableImage, iconSize);
//      else                        drawableImage = adjustPointImageSize(drawableImage, iconZoomSize);
      if (Math.abs(MP.rx2-MP.rx1)>15)  drawableImage = adjustPointImageSize(drawableImage, iconSize); // since 3.3.2

      myLine.setSymbol(drawableImage);          // If an icon, just set the symbol to the image
      setPointSize(drawableImage.getHeight(null));
    } else {
      myLine.setSymbol(getPointSymbol());          // Set the Symbol type
      myLine.setSymbolFill(isSymbolFill());
      myLine.setColor(getIconColor());
      myLine.setThickness(getLineWidth());
      int symbolSize = (int)(getPointSize() * getIconScale());
      if (symbolSize > MAX_ICON_SIZE) symbolSize = MAX_ICON_SIZE;  // prevent giant symbols
      myLine.getSymbol().setSize(symbolSize); // Set the size of the Symbol
    }

    myLine.setSymbolHeading(getIconHeading());

    MP.drawLineLonLat(buf, off, dim, npts, myLine);  // Draw the point

    /* Display the Name of the point next to it */
    String label = point.getParentFeature().getName();      // The 'name' tag is one level higher
    if (isShowLabel() && label != null && MP.getZoomLevel() > 0) {
      MPoint pix = new MPoint();
      MP.getPoint(buf[0], buf[1], buf[2], pix);
      //MP.drawString(label, pix.x, pix.y, textSize, MP.RIGHT);

      // Use Graphics2D to work around bug where label would not display if
      // any map was plotted underneath it (which there usually is).
      Graphics2D g2d = (Graphics2D)MP.gc;
      g2d.setColor(this.getLabelColor());
      int fontStyle = (MP.font==null) ? Font.PLAIN : MP.font.getStyle();
      Font f = new Font (label, fontStyle, (int)(textSize * labelScale));
      g2d.setFont(f);
      pix = adjustLabelPosition(g2d, f, label, labelPos, pix, myLine.getSymbolSize());
      g2d.drawString(label, pix.x, pix.y);
    }

   // saveKmlPointData (buf[0],buf[1],label,myLine,currDesc,balloonStyle,pointType == IMAGE? pointImage: null);

    if (!points.contains(point)) {
      point.setBalloonStyle(balloonStyle);
      point.setLine(myLine);
      point.setImage(pointType == IMAGE? pointImage: null);
      points.add(point);
    }

    return status;
  }

  /** Draw a Polygon on the layer when given coordinate set
      This is the simplest drawPolygon method. No checks are made on the data,
       so it should be called after the checks are made.
      NOTE: This method assumes an offset of 0 and dim of 3.
   */
  private void drawPolygon (double[] coords) {

    buf      = coords;
    npts     = (coords.length / 3);
    off      = 0;
    dim      = 3;

    Line myLine = getPolyLine();  // Get the Line to draw

    MP.drawLineLonLat(buf, off, dim, npts, myLine);  // draw the Polygon
  }

  /** Draw a kml Polygon
      This is the first method called when handling the drawing of a kml polygon.
      This method makes some decisions about the polygon and then passes it on
       to other draw methods.
   */
  private void drawPolygon (kmlPolygon polygon) {

    int rings = polygon.innerBoundary.size();   // Number of KML Linear Rings in the Inner Polygons

    if (!isPolygonFill()) {
      drawPolygonOutlines(polygon, rings);   // Polygon not filled. (Contours or rings)
    } else {
      if (rings > 1) {
        buf = combinePolygons(polygon, rings);        // Multiple inner rings
      } else if (rings > 0) {
        buf = this.combineSinglePolygons(polygon);    // Single inner ring
      } else {
        buf = polygon.getOuterBoundary().getCoordsAsDouble();  // No inner rings
      }
      if(buf != null) {       // cannot attempt to draw buf if no coords specified
        if (polygon.isExtrude()) {
          drawExtrudedPolygon(buf);   // extruded polygon
        } else  drawPolygon (buf);     // non-extruded polygon (kml default)
      }
      else {
        warning("Polygon has no coordinates, so will not be drawn");
      }
    }
    // Added for mouse popups since 3.9.1
    if (!getPolygons().contains(polygon)) {
      getPolygons().add(polygon);
    }
  }

  /** Draw KML Polygon that is not filled.
      Since the polygon is not filled, we can actually draw the inner rings as
       separate polygons, which eliminates the line connecting the outer and
       inner rings found in Filled polygons with inner rings.
     @param polygon The KML Polygon
     @param innerRings Number of inner rings (holes)
   */
  private void drawPolygonOutlines (kmlPolygon polygon, int innerRings) {

    /* Parameters for the drawLine method */
    buf = polygon.getOuterBoundary().getCoordsAsDouble();

    /* Draw the outer polygon */
    if (polygon.isExtrude()) {
      drawExtrudedPolygon(buf);  // Extruded
    } else  drawPolygon(buf);    // Non-extruded (default)

    /* Draw the inner polygons */
    if (innerRings > 0) {
      for (int xx=0; xx<innerRings; xx++) {
        buf = ((polygon.innerBoundary.get(xx))).getCoordsAsDouble();
        if (polygon.isExtrude()) {
          drawExtrudedPolygon(buf);   // extruded
        }
        else drawPolygon(buf);        // non-extruded (default)
      }
    }
  }

  /** This method uses an algorithm that combines the coordinate sets but leaves
       a 'path' to retrace. This path reduces the chance that a polygon will
       draw over top of an existing inner ring. It is definitely not a perfect
       algorithm, but should eliminate most occurrences.
      NOTE: If the line color (set in the kml LineStyle object) is a different
       color than the fill (set in the kml PolyStyle object), then you will
       see a line connecting the outer and inner polygons.
     @param polygon The KML Polygon
     @param innerRings The number of inner rings
     @return The double[] array of (non-extruded) coordinates to draw.
   */
  private double[] combinePolygons (kmlPolygon polygon, int innerRings) {

    /* Outer Polygon (linear ring) coordinates */
    double[] outerCoords    =  polygon.getOuterBoundary().getCoordsAsDouble();
    int outerLength =  outerCoords.length;

    if (innerRings == 0) return outerCoords;    // If no inner rings, just need outer

    double[] coords;
    int totalLength    = 0;
    int tmpPos         = 0;
    int innerLength    = 0;
    int endPathCounter = 0;

    /* Determine the combined length (# of coords) for all the inner rings */
    for (int xx=0; xx< innerRings; xx++) {
      innerLength += (polygon.innerBoundary.get(xx)).getCoordsAsDouble().length;
    }

    /* Add an extra coord set between the inner rings */
    totalLength = outerLength + innerLength + (3 * (innerRings-1));

    coords = new double[totalLength];  // Set the return coords size

    double[] endPath = new double[(innerRings-1) * 3];

    for (int xx=0; xx<outerLength; xx++, tmpPos++) {
      coords[tmpPos] =  outerCoords[xx];            // Append the outer coordinates
    }

    for (int xx=0; xx< innerRings; xx++) {
      double[] tmpCoords = (polygon.innerBoundary.get(xx)).getCoordsAsDouble();
      for (int yy=0; yy<tmpCoords.length; yy++, tmpPos++) {
        coords[tmpPos] = tmpCoords[yy];        // Append the inner coordinates
      }
      if (xx < (innerRings -1)) {
        endPathCounter = ((endPath.length) - (3 * xx) -3);       // Set the retrace path
        endPath[endPathCounter]    = coords[tmpPos-3];  // lon
        endPath[endPathCounter +1] = coords[tmpPos-2];  // lat
        endPath[endPathCounter +2] = coords[tmpPos-1];  // alt
      }
    }

    for (int xx=0; xx< endPath.length; xx++, tmpPos++) {
      coords[tmpPos] =  endPath[xx];                 // Append the retrace path
    }

    return coords;
  }

  /** This method is used for the case of a polygon with a single
       inner ring, since that case allows for a faster algorithm.
      NOTE: If the line color (set in the kml LineStyle object) is a different
       color than the fill (set in the kml PolyStyle object), then you will
       see a line connecting the outer and inner polygons.
      @param polygon The <code>kmlPolygon</code>
      @return The combined coordinates of the two rings (outer + inner)
   */
  private double[] combineSinglePolygons (kmlPolygon polygon) {
    double[] coords;
    int innerRings       = polygon.innerBoundary.size();

    /* Send to other method if multiple inner rings */
    if (innerRings > 1) {
      coords = combinePolygons(polygon, innerRings);
    } else {
      /* Combine the two polygons, starting with the outer */
      double[] outerCoords = polygon.getOuterBoundary().getCoordsAsDouble();
      double[] innerCoords = (polygon.innerBoundary.get(0)).getCoordsAsDouble();
      int length = (outerCoords.length + innerCoords.length);
      coords = new double[length];
      int counter = 0;
      for (int xx=0; xx<outerCoords.length; xx++, counter++) {
        coords[counter] = outerCoords[xx];
      }
      for (int xx=0; xx<innerCoords.length; xx++,counter++) {
        coords[counter] = innerCoords[xx];
      }
    }
    return coords;
  }

  /** This method provides no real attempt to prevent the polygon from filling
       incorrectly. This method simply combines all the polygon rings into
       a single polygon.
      NOTE: If the line color (set in the kml LineStyle object) is a different
       color than the fill (set in the kml PolyStyle object), then you will
       see a line connecting the outer and inner polygons.
     @param polygon The <code>kmlPolygon</code>
     @param innerRings Number of inner boundaries
     @return Single double array of coordinates to draw.
   */
  private double[] combinePolygonsFast (kmlPolygon polygon, int innerRings) {
    double[] coords;

    if (innerRings == 0) return polygon.getOuterBoundary().getCoordsAsDouble();

    /* Combine the two polygons, starting with the outer */
    double[] outerCoords = polygon.getOuterBoundary().getCoordsAsDouble();
    int innerLength = 0;
    int innerCounter = 0;
    for (int xx=0; xx< innerRings; xx++) {
      innerLength +=  (polygon.innerBoundary.get(xx)).getCoordsAsDouble().length;
    }
    double[] innerCoords = new double[innerLength];
    for (int xx=0; xx<innerRings; xx++) {
      double[] tmpCoords = (polygon.innerBoundary.get(xx)).getCoordsAsDouble();
      for (int yy=0; yy< tmpCoords.length; yy++, innerCounter++) {
        innerCoords[innerCounter] = tmpCoords[yy];
      }
    }

    int length = (outerCoords.length + innerLength);  // 3 added to return to start point
    coords = new double[length];
    int counter = 0;
    for (int xx=0; xx<outerCoords.length; xx++, counter++) {
      coords[counter] = outerCoords[xx];
    }
    for (int xx=0; xx<innerCoords.length; xx++,counter++) {
      coords[counter] = innerCoords[xx];
    }
    return coords;
  }

  /** Draw a kml Line String */
  private int drawLineString (kmlLineString lineString) {
    int status = 1;
    buf = lineString.getCoordsAsDouble();  // Get the Line String coordinates in double[] form
    // Added for mouse popups since 3.9.1
    if (!getLineStrings().contains(lineString)) getLineStrings().add(lineString);
    npts = (buf.length / 3);           // 3 values (x,y,z) per point
    dim = 3;
    off = 0;
    Line myLine = line.copy();             // Line to draw
    Color currColor = myLine.getColor();
    Color newColor = getLineColor();
    if (!currColor.equals(newColor)) myLine.setColor(newColor);
    if (myLine.getThickness() != getLineWidth()) myLine.setThickness(getLineWidth());

    MP.drawLineLonLat(buf, off, dim, npts, myLine);    // Draw the lines
    return status;
  }

  /** Draw a kml Linear Ring */
  private int drawLinearRing (kmlLinearRing linearRing) {
    int status =1;
    return status;
  }

  /** Set the poll rate used when polling external file
   *  @param rate the poll rate used when polling external file */
  public void setPollRate (double rate) {
    setPollRate(0,rate);
  }

  /** Set the poll rate used when polling external files
      @param index The index of the file (if it's a linked file)
      @param rate  The interval between poll cycles
   */
  public void setPollRate (int index, double rate) {
    if (rate >= minRefreshPeriod) pollRate[index] = rate;
  }

  /** Get the poll rate used when polling external file
   *  @return the poll rates used when polling external file */
  public double getPollRate () {
    return getPollRate(0);   // base file
  }

  /** Get the poll rates used when polling external file
   *  @return the poll rates used when polling external file */
  public double[] getPollRates () {
    return pollRate;
  }

  /** Get the poll rate used when polling external file
      @param index The index of the file to get the rate for
      @return the poll rate used when polling external file
   */
  public double getPollRate (int index) {
    return pollRate[index];
  }

  /** Set whether to just draw the outline of the polygon
   *  @param outline true if should draw lines on polygons */
  public void setPolygonOutline (boolean outline) {
    polygonOutline = outline;
  }

  /** Get whether to just draw the outlines of the polygons
   *  @return true if outlines are to be draw on polygons */
  public boolean getPolygonOutline () {
    return polygonOutline;
  }

  /** Convenience method
   *  @return true if outlines should be drawn on polygons */
  public boolean isPolygonOutline () {
    return getPolygonOutline();
  }

  /** set whether to fill the polygon
   *  @param doFill true if the polygon is to be filled */
  public void setPolygonFill (boolean doFill) {
    polygonFill = doFill;
  }

  /** get whether to fill the polygon
   *  @return true if polygon is filled */
  public boolean getPolygonFill () {
    return polygonFill;
  }

  /** Convenience method
   *  @return true if polygons are filled */
  public boolean isPolygonFill () {
    return getPolygonFill();
  }

  /** Sets the color of the <code>kmlPolygon</code> fill.
      NOTE: This method does not call refresh(), since setting the color of
       the polygon is called when the <code>kmlPolyStyle</code> tag is
       handled, which is before the actual drawing of the objects.
      @param color The new color
   */
  public void setPolygonColor (Color color) {
    polygonColor = color;
  }

  /** Get the color of the fill for <code>kmlPolygon</code>s.
   *  @return the color of the fill */
  public Color getPolygonColor () {
    return polygonColor;
  }

  /** Set the color of the lines (or borders for polygons)
      NOTE: This method does not call refresh(), since setting the color of
       the lines is called when the <code>kmlLineStyle</code> tag is
       handled, which is before the actual drawing of the objects.
      @param color the color of lines
   */
  public void setLineColor (Color color) {
    lineColor = color;
  }

  /** Get the color of the lines (or borders for polygons)
   *  @return the color of lines */
  public Color getLineColor () {
    return lineColor;
  }

  /** Sets the width of the lines (or borders)
      NOTE: This method does not call refresh(), since setting the color of
       the lines is called when the <code>kmlLineStyle</code> tag is
       handled, which is before the actual drawing of the objects.
      @param width the width of lines (or borders)
   */
  public void setLineWidth (int width) {
    lineWidth = width;
  }

  /** get line width
   *  @return line width
   */
  public int getLineWidth () {
    return lineWidth;
  }

  /** Set the scale multiplier of the text displayed next to points
   *  @param scale the scale multiplier of the text displayed next to points */
  public void setLabelScale (float scale) { labelScale = scale; }

  /** get scale the scale multiplier of the text displayed next to points
   *  @return label scale
   */
  public float getLabelScale () { return labelScale; }

  /** set label color
   *  @param color label color
   */
  public void setLabelColor (Color color) { labelColor = color; }

  /** get label color
   *  @return label color
   */
  public Color getLabelColor () { return labelColor; }

  /** Set the size of the text displayed next to points
   *  @param size the size of the text displayed next to points */
  public void setTextSize (int size) { textSize = size; }

  /** Get the size of the text displayed next to points
   *  @return the size of the text displayed next to points */
  public int getTextSize () { return textSize; }

  /** are labels of points shown
   *  @return true if labels show
   */
  public boolean isShowLabel () { return showLabel; }

  /** set if labels are shown
   *  @param show true to show labels
   */
  public void setShowLabel (boolean show) { showLabel = show; }

  /** set point type
   *  @param type point type
   */
  public void setPointType (int type) { pointType = type; }
  /** get point type
   *  @return point type
   */
  public int getPointType ()  { return pointType; }

  /** Get the type of kmlPoint (SYMBOL or IMAGE) in the given kmlStyle id
      @param styleId The id attribute of the Style
      @return The type of point (IMAGE or SYMBOL)
   */
  private int getPointType (String styleId) {
    if (styles.containsKey(styleId)) {
      String href = (styles.get(styleId)).iconStyle.icon.getHref().toLowerCase();
      if (href.startsWith("http")  ||
          href.startsWith("file")  ||
          isImage(href))     {
        return IMAGE; //TODO Make this smarter
      }
    }

    return SYMBOL;
  }

  private boolean isImage(String filename) {
    filename = filename.toLowerCase();
    if (filename.endsWith("gif")  ||
        filename.endsWith("tif")  ||
        filename.endsWith("tiff") ||
        filename.endsWith("jpg")  ||
        filename.endsWith("jpeg") ||
        filename.endsWith("bmp")  ||
        filename.endsWith("wbmp") ||
        filename.endsWith("pdf")) {
      return true;
    } else {
      return false;
    }
  }

  /** Get the current Symbol type being used
   *  @return the current Symbol type being used */
  public String getPointSymbol () {
    return pointSymbol;
  }

  /** Set the Symbol and Symbol Type type
   *  @param symbol String representation fo the Symbol */
  public void setPointSymbol (String symbol) {
    if (Symbol.symbolList.indexOf(symbol) >= 0) {
      pointSymbol = symbol;
      setPointType(SYMBOL);
    } else {
      warning("Invalid symbol: " +symbol+ ". Symbol must be in " +Symbol.symbolList);
    }
  }

  /** Get the Image being used for kml Points
   *  @return the Image being used for KML points */
  public Image getPointImage () {
    return pointImage;
  }

  /** Set the Image being used for kml Points
   *  @param url the Image being used for KML points */
  public void setPointImage (URL url) {
    pointImage = addToImgCache(url.toString());
    setPointType(IMAGE);  // Set the type to IMAGE
  }

  /** Set the Image being used for kml Points
   *  @param urlString the Image being used for KML points */
  public void setPointImage (String urlString) {
    pointImage = addToImgCache(urlString);
    setPointType(IMAGE);  // Set the type to IMAGE
  }

  /** Checks the image cache to see if the image already exists there. If not, it adds it.
      @param urlString The url of the image (in String form)
      @return The BufferedImage or null if the given urlstring cannot be resolved to an image
      @since NeXtMidas 2.7.3
   */
  private BufferedImage addToImgCache (String urlString) {
    // the check below was added in 3.3.1 for Bug 2376
    BufferedImage img = null;
    Object obj = imgCache.get(urlString);  // first check to see if it is already there
    if (obj==null)                         img = null;                // first time checking for the URL (or old one expired)
    else if (obj instanceof BufferedImage) img = (BufferedImage)obj;  // previosuly entered img
    else                                   return null;               // invalid url

    if (img == null) {
      URL url = getURL(urlString);
      try {
        img = ImageIO.read(url);
        if (img != null) imgCache.put(urlString, img);
      } catch (IIOException ioe) {
        warning("Unable to retrieve image from url: ["+urlString+"]");
        imgCache.put(urlString,"badurl");   // store the url as a non-img, which is checked the next time through
      } catch (Exception e) {
        Shell.printStackTrace(e);
      }
    }
    return img;
  }

  /** Set the max size (in pixels) that an un-zoomed Icon can be.
      The same number applies to both icon Height and Width.
      @param size the max size (in pixels) that an un-zoomed Icon can be
      @since NeXtMidas 2.7.1
   */
  public void setIconSize (int size) { iconSize = size; }

  /** get Max height/width (in pixels) for non-Zoomed icon images
   *  @return Max height/width (in pixels) for non-Zoomed icon images
   */
  public int getIconSize () { return iconSize; }

  /** Set the max size (in pixels) that a zoomed Icon can be.
      The same number applies to both icon Height and Width.
      @param size the max size (in pixels) that a zoomed Icon can be
      @since NeXtMidas 2.7.1
   */
  public void setIconZoomSize (int size) { iconZoomSize = size; }

  /** get Max height/width (in pixels) for Zoomed icon images
   *  @return Max height/width (in pixels) for Zoomed icon images
   */
  public int getIconZoomSize () { return iconZoomSize; }

  /** Set whether or not to ignore re-draws ONLY on the file changing .
      @param val true if should force refresh
      @since NextMidas 2.7.1
   */
  public void setForceRefresh ( boolean val) { forceRefresh = val; }
  /** get force refresh
   *  @return whether or not to ignore re-draws ONLY on the file changing
   */
  public boolean getForceRefresh () { return forceRefresh; }

  /** This is used internally to reduce the size of the icon if zoomed out. */
  private BufferedImage adjustPointImageSize (BufferedImage img, int maxSize) {
    if (img != null) {
      float h = img.getHeight();
      float w = img.getWidth();

      boolean adjustH = (h > maxSize);
      boolean adjustW = (w > maxSize);

      // Do the check below to avoid scaling the icon twice if height and width are both over the max
      if (adjustH && !adjustW) img = scalePointImage(img, ((float)maxSize / h));
      else if (adjustW)        img = scalePointImage(img, ((float)maxSize / w));
    }
    return img;
  }

  /** Blend the color given in iconStyle with color already in the image
      @param img The BufferedImage to apply the icon color to
      @since NeXtMidas 2.7.1
   */
  private void applyPointImageColor (BufferedImage img) {
    applyPointImageColor(img, iconColor);
  }

  /** Blend the color given in iconStyle with color already in the image
      @param img The BufferedImage to apply the icon color to
      @param color The color from iconStyle
      @since NeXtMidas 2.7.1
   */
  private void applyPointImageColor (BufferedImage img, Color color) {
    if (color != null && img != null) {
      MColor.colorImage(img, color, colorBlendRatio);
    }
  }

  /** adjust label position
   *  @param g2d      graphics object
   *  @param f        font
   *  @param label    label
   *  @param pos      relative position: TOP, TOPR, BOTTOM, BOTTOMR, RIGHT
   *  @param pix      center location
   *  @param iconSize icon size
   *  @return new MPoint
   */
  public MPoint adjustLabelPosition (Graphics g2d, Font f, String label, String pos, MPoint pix, int iconSize) {
    int posx = pix.x; // x pos of label
    int posy = pix.y; // y pos of label
    FontMetrics metrics = g2d.getFontMetrics(f);
    int textWidth  = metrics.stringWidth(label);
    int textHeight = metrics.getHeight();
    if (pos.equals("TOP")) {
      posx = pix.x - (textWidth/2);
      posy = pix.y - (iconSize/2) - 1;
    } else if (pos.equals("TOPR")) {
      posx = pix.x + (iconSize/2);
      posy = pix.y - (iconSize/2) - 1;
    } else if (pos.equals("BOTTOM")) {
      posx = pix.x - (textWidth/2);
      posy = pix.y + (iconSize/2) + textHeight;
    } else if (pos.equals("BOTTOMR")) {
      posx = pix.x + (iconSize/2);
      posy = pix.y + (iconSize/2) + textHeight;
    } else if (pos.equals("RIGHT")) {
      posx = pix.x + (iconSize/2);
      posy = pix.y + textHeight/2;
    }
    MPoint newPoint = new MPoint(posx, posy);
    return newPoint;
  }

  /** get label position
   *  @return label position
   */
  public String getLabelPos () { return labelPos; }

  /** set label position
   *  @param pos label position
   */
  public void setLabelPos (String pos) {
    if (pos == null) return;
    pos = pos.toUpperCase();
    for (int index=0; index < LABEL_POSITIONS.length; index++) {
      if (LABEL_POSITIONS[index].equals(pos)) { labelPos=pos; return; }
    }
    String labelString = "'";
    for (int index=0; index < LABEL_POSITIONS.length; index++) {
      labelString += (LABEL_POSITIONS[index] + "', '");
    }
    warning("Invalid Label Position: " +pos+". Position must be in "
        +labelString.substring(0, labelString.length()-3)+".");
  }

  /** Makes a copy of the current point image. This is used to prevent over-
      blending on re-draws if using a network link file
      @return The image to draw
   */
  private BufferedImage getDrawableImage () {
    if (pointImage==null) return null;  // Bug 2360
    int w = pointImage.getWidth();
    int h = pointImage.getHeight();

    BufferedImage newImg = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g = newImg.createGraphics();
    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
        RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    g.drawImage(pointImage, 0, 0, w, h, 0, 0, w, h, null);
    g.dispose();
    return newImg;
  }

  /** Apply a given scale to an Image .
      @param scale The factor to scale
   */
  private BufferedImage scalePointImage (BufferedImage img, float scale) {
    if (scale != 1.0f && img != null) {
      int oldW = img.getWidth();
      int oldH = img.getHeight();
      int newH = (int)(oldH * scale);
      int newW = (int)(oldW * scale);
      BufferedImage scaled = new BufferedImage(newW, newH, BufferedImage.TYPE_INT_ARGB);
      Graphics2D g = scaled.createGraphics();
      g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
          RenderingHints.VALUE_INTERPOLATION_BILINEAR);
      g.drawImage(img, 0, 0, newW, newH, 0, 0, oldW, oldH, null);
      g.dispose();
      return scaled;
    } else {
      warning("Unable to scale image due to NULL image or scale factor of 100%");
      return img;
    }
  }

  /** Apply the scale factor to an Image Icon */
  public void scalePointImage () {
    if (iconScale != 1.0f && pointImage != null) {
      pointImage = scalePointImage(pointImage, iconScale);
    }
  }

  /** Get the size of the KML Points (Symbols)
   *  @return the size of the KML Points (Symbols) */
  public int getPointSize () {
    return pointSize;
  }

  /** set color blend ratio
   *  @param ratio color blend ratio
   */
  public void setColorBlendRatio (float ratio) {
    if (ratio <= 1.0f && ratio >= 0.0f) colorBlendRatio = ratio;
    else warning("Invalid Image Color ratio of " + ratio + ". Ratio must be between 0.0 and 1.0");
  }

  /** get color blend ratio
   *  @return color blend ratio
   */
  public float getColorBlendRatio () { return colorBlendRatio; }

  /** Set the size of the KML Points (Symbols)
   *  @param pointSize the size of the KML Points (Symbols) */
  public void setPointSize (int pointSize) {
    this.pointSize = pointSize;
  }

  /** Get the direction the Symbol is facing
   *  @return the direction the Symbol is facing */
  public float getIconHeading () {
    return iconHeading;
  }

  /** Set the direction the Symbol is facing
   *  @param heading the direction the Symbol is facing */
  public void setIconHeading (float heading) {
    iconHeading = heading;
  }

  /** Get the scale (Magnification) of the current Symbol
   *  @return the scale of the current symbol */
  public float getIconScale () {
    return iconScale;
  }

  /** Set the scale (Magnification) of the current Symbol
   *  @param scale the scale of the current symbol */
  public void setIconScale (float scale) {
    iconScale = scale;
  }

  /** set icon color
   *  @param color icon color
   */
  public void setIconColor (Color color) { iconColor = color; }

  /** get icon color
   *  @return icon color
   */
  public Color getIconColor () { return iconColor; }

  /** Gets the appropriate image cache to use for the layer. */
  private Cache getDefImgCache () {
    return new Cache(Cache.SLRU, 4, 3600.0, SHARED_VCACHE);
  }

  /** Sets the size of the local image cache.
      @param size The cache size or -1 if the size should be set automatically.
   */
  public void setCacheSize (int size) {
    setCacheSize(size,0,4);
  }

  /** Sets the size of the local image cache.
      @param size The cache size or -1 if the size should be set automatically.
      @param min  The minimum allowable size.
      @param def  The default size to use when <code>size=-1</code> (ignored if <code>def&lt;min</code>).
   */
  protected final void setCacheSize (int size, int min, int def) {
    if (size == -1) {
      imgCache.setMaxSize(Math.max(def, min));
      imgCacheSize = -1;
    } else {
      if (size < min) {
        size = min;
      }
      imgCache.setMaxSize(size);
      imgCacheSize = imgCache.getMaxSize(); // cache may internally restrict size.
    }
  }

  /** Gets the size of the local image cache.
      @return The cache size or -1 if the size is set automatically.
      @see #getCurrentCacheSize()
   */
  public final int getCacheSize () {
    return imgCacheSize;
  }

  /** Gets the current size of the local image cache.
      @return The cache size currently in use (if the cache size is set automatically, this method
              will return the current size of the cache).
      @see #getCacheSize()
   */
  public final int getCurrentCacheSize () {
    return imgCache.getMaxSize();
  }

  /** get if symbols are filled fill
   *  @return true if symbols are filled
   */
  public boolean isSymbolFill () {
    return symbolFill;
  }

  /** Convenience method
   *  @return true if symbol is filled */
  public boolean getSymbolFill () {
    return isSymbolFill();
  }

  /** set if symbols are filled
   *  @param fill true if should fill symbols
   */
  public void setSymbolFill (boolean fill) {
    symbolFill = fill;
  }

  /** Get the KML Parser
   *  @return the KML Parser */
  public KmlParser getKmlParser () {
    return kmlParser;
  }

  /** Set the KML parser to use
   *  @param parser the KML parser to use */
  public void setKmlParser (KmlParser parser) {
    kmlParser = parser;
  }

  /** Get the vector with the KML Objects being used
   *  @return the vector with the KML obkjects being used */
  public kmlObjects getKmlObjects () {
    return objects;
  }

  /** Set the Vector of KML objects
   *  @param objs Vector of KML objects */
  public void setKmlObjects (kmlObjects objs) {
    objects = objs;
  }

  /** Break apart the objects in the kml file into their components
      Usually called by the draw method.

      Note: The number of objects passed to this method will usually be quite
       small, since they are rolled up to a parent object. The parent object is
       usually a <code>kmlDocument</code>, but this is not strictly enforced.
   */
  private void handleKmlObjects (kmlObjects objects) {

    if (!objects.networkLinks.isEmpty()) {
      int numObjs = objects.networkLinks.size();
      int adjIndex = createOrUpdateLinkedFiles(numObjs);
      for (int idx=0; idx<numObjs; idx++) {
        handleKmlNetworkLink(objects.networkLinks.get(idx), idx+adjIndex);
      }
    }

    if (!objects.networkLinkControls.isEmpty()) {
      handleKmlNetworkLinkControl(objects.networkLinkControls.get(0));  // only gets first one
    }

    for (kmlDocument  doc    : objects.documents)      {  handleKmlDocument(doc);      }
    for (kmlPlacemark pm     : objects.placemarks)     {  handleKmlPlacemark(pm);      }
    for (kmlFolder    f      : objects.folders)        {  handleKmlFolder(f);          }
    for (kmlGroundOverlay go : objects.groundOverlays) {  handleKmlGroundOverlay(go);  }
  }

  /** Common method to either create a new {@link #linkedFile} object and associated objects
      ({@link #pollRate}, {@link #isFirstPass}, and {@link #lastPoll}) or, if they
      already exist, extend them to include new elements. This occurs when the NetworkLinks
      are in different folders or levels of the kml hierarchy.
      @param numLinks Number of elements to create or add in the new arrays.
      @return The size of the current {@link #linkedFile} array, or 0 if {@link #linkedFile} has not been created yet.
      @since NeXtMidas 2.9.0 (Bug 2237)
   */
  private int createOrUpdateLinkedFiles (int numLinks) {
    int adjIndex = 0;
    if (linkedFile==null) {    // folder is the top-level container
      linkedFile        = new KmlFile[numLinks];
      pollRate          = new double[numLinks];
      lastPoll          = new double[numLinks];
      isFirstPass       = new boolean[numLinks];
      lastMods          = new double[numLinks];
    } else {                  // Network Links are in different folders / levels
      adjIndex = linkedFile.length;
      KmlFile[] tmpF = new KmlFile[adjIndex+numLinks];
      for (int xx=0; xx<adjIndex; xx++) { tmpF[xx] = linkedFile[xx]; }
      linkedFile = tmpF;
      double[] tmpD = new double[linkedFile.length];
      for (int xx=0; xx<adjIndex; xx++) { tmpD[xx] = pollRate[xx]; }
      pollRate = tmpD;
      double[] tmpD2 = new double[linkedFile.length];
      for (int xx=0; xx<adjIndex; xx++) { tmpD2[xx] = lastPoll[xx]; }
      lastPoll = tmpD2;
      boolean[] tmpB = new boolean[linkedFile.length];
      for (int xx=0; xx<adjIndex; xx++) { tmpB[xx] = isFirstPass[xx]; }
      isFirstPass = tmpB;
      double[] tmpL = new double[linkedFile.length];
      for (int xx=0; xx<adjIndex; xx++) { tmpL[xx] = lastMods[xx]; }
      lastMods = tmpL;
    }
    return adjIndex;
  }

  /** Check to see if a given String is in URL format. If not, make it so */
  private URL getURL (String href) {
    URL url = null;
    FileName fn = new FileName(href);
    String path = fn.getPath();

    // find files within KMZ folder
    if (currPath!=null && currPath.startsWith("jar:")) fn.setPath(currPath + path);
    else if (path.equals("")) fn.setPath(currPath);

    try {
      return fn.toURL(IOResource.DIR_INPUT);
    } catch (MalformedURLException e) {
      e.printStackTrace();
    }
    return url;
  }

  /** Get the image and location for the overlay and draw it */
  private void handleKmlGroundOverlay (kmlGroundOverlay overlay) {
    kmlIcon icon = overlay.getIcon();       // get the kmlIcon used by the Overlay
    Color color = overlay.getColor();
    int h;                                 // img height
    int w;                                 // img width
    String href        = icon.getHref();
    double north       = overlay.getNorth();  // north lat
    double south       = overlay.getSouth();  // south lat
    double east        = overlay.getEast();   // east lon
    double west        = overlay.getWest();   // west lon

    // viewformat needs to get added to the end of the url
    if ((href.startsWith("http") || href.startsWith("HTTP"))  && icon.getViewFormat()!=null) {
      href = href + "&" + icon.parseViewFormat(north, south, east, west, MP.panel.getWidth(), MP.panel.getHeight());
    }

    BufferedImage img = addToImgCache(href); // get the image from the cache
    if (img == null) return;    // href did not work

    int alpha = color.getAlpha();
    h = img.getHeight();
    w = img.getWidth();
    if (alpha != OPAQ && alpha != -1) {
      img = MColor.adjustImage(img, 0, 0, w, h, 50, 50, alpha, false);
    }

    /* Adjust for rotation, if necessary */
    double rotation = overlay.getRotation();
    if (rotation != 0) {
      double rads     = Math.toRadians(rotation);
      img = GraphicsUtil.rotateImage(img, rads);   // Adjust the rotation of the img
    }

    MP.drawImage(img, north, south, east, west);
    // Added for mouse popups since 3.9.1
    KmlImage ti = new KmlImage(img, north, south, east, west, href);
    if (!getImages().contains(ti)) {
      getImages().add(ti);
    }
  }
  // Added for mouse popups since 3.9.1
  private class KmlImage extends kmlObject {
    BufferedImage img;
    double north;
    double south;
    double east;
    double west;
    String href;

    public KmlImage (BufferedImage img, double north, double south, double east, double west, String href) {
      this.img = img;
      this.north = north;
      this.south = south;
      this.east = east;
      this.west = west;
      this.href = href;
    }

    public String toString() {
      String res = "";
      res += "north = "+north;
      res += ",south = "+south;
      res += ",east = "+east;
      res += ",west = "+west;
      res += ",href="+href;
      return res;

    }
  }

  /** Break apart a KML Document Object. */
  private void handleKmlDocument (kmlDocument document) {
    String styleId          = document.getStyleUrl();

    for (kmlStyleMap sm : document.getStyleMaps()) { styleMaps.put(sm.getId(),sm);     }
    for (kmlStyle style : document.getStyles())    { styles.put(style.getId(), style);  }
    for (kmlFeature feat : document.features)      { handleKmlFeature(feat);  }

    if (styleId != null) { handleKmlStyle(styleId); }
    else                 { setStyleDefaults();      }
  }

  /** Sets Style (color, width, etc) back to defaults so the next object being
       drawn does not inherit unique style from the previous object.
   */
  private void setStyleDefaults () {
    setPolygonFill    (false);   // TODO: figure out the best default for this
    setPolygonOutline (true);
    setLineWidth      (DEFAULT_LINE_WIDTH);
    setLineColor      (DEFAULT_POINT_COLOR);
    setPolygonColor   (DEFAULT_POINT_COLOR);
    setIconHeading    (DEFAULT_ICON_HEADING);
    setIconScale      (DEFAULT_ICON_SCALE);
    setPointSymbol    (DEFAULT_POINT_SYMBOL);
    setPointSize      (DEFAULT_POINT_SIZE);
  }

  /** Looks up the Style based on the given id and sends it to
       {@link #handleKmlStyle(kmlStyle)}.
      @param styleId
   */
  private void handleKmlStyle (String styleId) {
    // if directing to a StyleMap get the Normal Style URL from the styleMap  - since 3.3.2
    if (styleMaps.containsKey(styleId)) { styleId = styleMaps.get(styleId).getNormalStyleUrl(); }

    if (styles.containsKey(styleId)) { handleKmlStyle(styles.get(styleId)); }
  }

  /** Break down the <code>kmlStyle</code> element into the respective styles
       and handle them. This usually involves setting certain attributes (such
       as color, width, etc) of the objects to be drawn prior to the drawing.
     @param style The <code>kmlStyle</code.
   */
  private void handleKmlStyle (kmlStyle style) {
    setStyleDefaults();

    if (style.hasLineStyle) {
      handleKmlLineStyle(style.getLineStyle());
    }
    if (style.hasPolyStyle) {
      handleKmlPolyStyle(style.getPolyStyle());
    }
    if (style.hasIconStyle) {
      handleKmlIconStyle(style.getIconStyle());
    }
    if (style.hasLabelStyle) {
      handleKmlLabelStyle(style.getLabelStyle());
    }

    // Load the balloon Style (null will use defaults)
    handleKmlBalloonStyle(style.getBalloonStyle());

  }

  /** Load the balloon style info for later use. null==defaults */
  private void handleKmlBalloonStyle (kmlBalloonStyle balloonStyle) {
    if (balloonStyle==null) this.balloonStyle = new kmlBalloonStyle();  // null == defaults
    else this.balloonStyle = balloonStyle;
  }

  /** Set Width and line color, if given */
  private void handleKmlLineStyle (kmlLineStyle lineStyle) {
    setLineWidth((int)lineStyle.getWidth());
    setLineColor(lineStyle.getColor());
  }

  private void handleKmlPolyStyle (kmlPolyStyle polyStyle) {
    setPolygonFill(polyStyle.isFill());
    setPolygonOutline(polyStyle.isOutline());
    setPolygonColor(polyStyle.getColor());
  }

  private void handleKmlIconStyle (kmlIconStyle iconStyle) {
    setIconHeading (iconStyle.getHeading());
    setIconScale   (iconStyle.getScale());     // Adjusts size displayed
    setIconColor   (iconStyle.getColor());
    handleKmlIcon  (iconStyle.getIcon());      // Symbol or image
  }

  private void handleKmlIcon (kmlIcon icon) {
    String url = icon.getHref();

    // TODO: Add logic to append parseViewFormat to the url...requires reference to parent geometry

    if (Symbol.symbolList.indexOf(url) >= 0) {
      setPointSymbol(url);
    } else if (url.toLowerCase().startsWith("http") || url.toLowerCase().startsWith("file")) {
      setPointImage(url);
    } else {
      setPointImage(url);
    }
  }

  private void handleKmlLabelStyle (kmlLabelStyle labelStyle) {
    setLabelColor (labelStyle.getColor());
    setLabelScale (labelStyle.getScale());
  }

  private void handleKmlFolder (kmlFolder folder) {
    int numLinks      = folder.networkLinks.size();

    for (kmlFolder f : folder.getFolders()) { handleKmlFolder(f);  }

    if (numLinks > 0) {
      int adjIndex = createOrUpdateLinkedFiles(numLinks);   // adjust index for multiple links but not at same level
      for (int idx=0; idx<numLinks; idx++) {
        handleKmlNetworkLink(folder.networkLinks.get(idx), idx+adjIndex);
      }
    }

    if (folder.getStyleUrl() != null) {
      handleKmlStyle(folder.getStyleUrl());
    } else {
      setStyleDefaults();
    }

    for (kmlFeature feat : folder.features) {  handleKmlFeature(feat);  }
  }

  /** Break apart KML Placemark objects and draw them */
  private void handleKmlPlacemark (kmlPlacemark placemark) {
    String styleId = placemark.getStyleUrl();
    if (styleId != null) {
      handleKmlStyle(styleId);
    } else if (placemark.getStyles().size() > 0) {
      for (kmlStyle style :placemark.getStyles() ) {
        handleKmlStyle(style);
      }
    } else {
      setStyleDefaults();
    }

    handleKmlGeometry(placemark.getGeometry());
  }

  /** Take a Generic kmlGeometry object and send to the precise handler for it
      @param geometry The kmlGeometry object
   */
  private void handleKmlGeometry (kmlGeometry geometry) {

    if (geometry instanceof kmlLineString) {
      handleKmlLineString((kmlLineString)geometry);
    } else if (geometry instanceof kmlLinearRing) {
      handleKmlLinearRing((kmlLinearRing)geometry);
    } else if (geometry instanceof kmlPoint) {
      handleKmlPoint((kmlPoint)geometry);
    } else if (geometry instanceof kmlPolygon) {
      handleKmlPolygon((kmlPolygon)geometry);
    } else if (geometry instanceof kmlMultiGeometry) {
      handleKmlMultiGeometry((kmlMultiGeometry)geometry);
    }
  }

  /** Break apart a KML Line String object and draw it */
  private void handleKmlLineString (kmlLineString lineString) {

    /* If the LineString extrudes, it becomes a polygon */
    if (lineString.isExtrude()) {
      drawExtrudedPolygon(lineString.getCoordsAsDouble());
    } else {
      drawLineString(lineString);
    }
  }

  /** Used when Polling a file. Sets the layer poll rate and file to be polled. */
  private void handleKmlNetworkLink (kmlNetworkLink networkLink, int index) {
    String mode   =  networkLink.link.getRefreshMode().toLowerCase();
    if (mode.equals("oninterval")) {
      pollRate[index]      =  networkLink.link.getRefreshInterval();
      if (pollRate[index] < minRefreshPeriod) pollRate[index] = minRefreshPeriod;  // keep pollrate within bounds
    } else {
      pollRate[index]      = 3600;    // check every hour even if not specified
    }
    linkedFile[index]    = new KmlFile(networkLink.link.getHref());
    isFirstPass[index]   = true;
  }

  /** sets some limits on what the network Link is allowed to do */
  private void handleKmlNetworkLinkControl (kmlNetworkLinkControl control) {
    minRefreshPeriod = control.getMinRefreshPeriod();
    if (pollRate==null) { pollRate = new double[1]; pollRate[0]=3600; return; }  // polling done by base/1st KML layer
    for (int idx=0; idx<pollRate.length; idx++) {
      if (pollRate[idx] < minRefreshPeriod) pollRate[idx] = minRefreshPeriod;
    }
  }

  /** Creates and returns the kml Coordinates for extruded objects */
  private double[] getExtrudeCoords (double[] coords) {
    int length = ((coords.length * 2) + 3);
    double[] polyCoords = new double[length];
    int coordsCounter = 0;

    for (int xx=0; xx<coords.length; xx++, coordsCounter++) {
      polyCoords[coordsCounter] = coords[xx];
    }

    for (int xx=(coords.length -1); xx>=0; xx -= 3) {
      polyCoords[coordsCounter++] = coords[xx - 2];
      polyCoords[coordsCounter++] = coords[xx - 1];
      polyCoords[coordsCounter++] = 0;
    }

    /* Close the loop by returning to the starting point */
    polyCoords[coordsCounter++] = coords[0];
    polyCoords[coordsCounter++] = coords[1];
    polyCoords[coordsCounter++] = coords[2];

    return polyCoords;
  }

  private void drawExtrudedPolygon (double[] points) {
    int length = points.length;

    drawPolygon(points);

    for (int xx=0; xx<length-3; xx+=3) {
      double[] poly = {points[xx],   points[xx+1], points[xx+2],
          points[xx+3], points[xx+4], points[xx+5],
          points[xx+3], points[xx+4], 0,
          points[xx],   points[xx+1], 0,
          points[xx],   points[xx+1], points[xx+2]};

      drawPolygon(poly);
    }

  }

  /** Break apart a KML Linear Ring object and draw it */
  private void handleKmlLinearRing (kmlLinearRing linearRing) {
    if (linearRing.isExtrude()) {
      drawExtrudedPolygon(linearRing.getCoordsAsDouble());
    } else {
      drawLinearRing(linearRing);
    }
  }

  /** Break apart a KML Point object and draw it */
  private void handleKmlPoint (kmlPoint point) {
    drawPoint(point);
  }

  /** Break apart a KML Polygon object and draw it */
  private void handleKmlPolygon (kmlPolygon polygon) {
    drawPolygon(polygon);
  }

  /** Break apart a kmlMultiGeometry into separate kmlGeometrys and send to
       {@link #handleKmlGeometry(kmlGeometry)}.
      @param multiGeometry The kmlMultiGeometry object
   */
  private void handleKmlMultiGeometry (kmlMultiGeometry multiGeometry) {
    int geoCount = multiGeometry.geometrys.size();
    for (int xx=0; xx<geoCount; xx++) {
      handleKmlGeometry(multiGeometry.geometrys.get(xx));
    }
  }

  /** Break apart a KML Feature object and draw it */
  private void handleKmlFeature (kmlFeature feature) {
    String featureType = feature.getClass().getName();

    if      (featureType.indexOf("kmlPlacemark")     >= 0) {  handleKmlPlacemark ((kmlPlacemark)feature); }
    else if (featureType.indexOf("kmlFolder")        >= 0) {  handleKmlFolder ((kmlFolder)feature);       }
    else if (featureType.indexOf("kmlDocument")      >= 0) {  handleKmlDocument((kmlDocument)feature);    }
    else if (featureType.indexOf("kmlGroundOverlay") >= 0) {  handleKmlGroundOverlay((kmlGroundOverlay)feature); }
  }

  /** Break apart a KML Region object and draw it */
  private void handleKmlRegion (kmlRegion region) {
    // to be added later as required
  }

  /** Break apart a KML Overlay object and draw it. Not implemented yet...*/
  private void handleKmlOverlay (kmlOverlay overlay) {
    // to be added later as required
  }

  /** Check the given value and if less than the current X Min, set as X Min.
      If greater than the current X Max, then set as X Max.
   */
  protected void updateXRange (double xval) {
    if (xval < x1) x1 = xval;
    if (xval > x2) x2 = xval;
  }

  /** Check the given value and if less than the current Y Min, set as Y Min.
      If greater than the current Y Max, then set as Y Max.
   */
  protected void updateYRange (double yval) {
    if (yval < y1) y1 = yval;
    if (yval > y2) y2 = yval;
  }

  /** Since NeXtMidas 2.9.0, does nothing. Range values set when reading coordinates.
      @see #updateXRange(double)
      @see #updateYRange(double)
    */
  public void findRange() {

  }

  /**
   * Find full range for layer bounds.
   * Updated method to set it own bound, rather than using Layer.findFullRange(PlotFile) - 3.7.1 for NXM-2938
   */
  @Override
  public void findFullRange() {
    fx1 = x1; fx2 = x2;
    fy1 = y1; fy2 = y2;
    fz1 = z1; fz2 = z2;
  }

  /** Not implemented yet */
  private void adjustColors () {

  }

  /** Sets the contrast to the given percent then redraws.
      @param percent Percentage of contrast (0..100)
   */
  public void setContrast (int percent) {
    if (percent != contrast) {
      contrast = Math.max(0,Math.min(100,percent));
      //needRefresh = true;
      //refresh();
    }
  }

  /** Gets the contrast of the layer.
      @return Percentage of contrast (0..100).
   */
  public float getContrast() { return contrast; }

  /** Sets the brightness to the given percent then redraws.
      @param percent Percentage of brightness (0..100)
   */
  public void setBrightness (int percent) {
    if (percent != brightness) {
      brightness = Math.max(0,Math.min(100,percent));
      //needRefresh = true;
      //refresh();
    }
  }

  /** Gets the brightness of the layer.
      @return Percentage of brightness (0..100).
      @since NeXtMidas 2.7.0
   */
  public float getBrightness() { return brightness; }

  /** Sets the opaqueness to the given percent then sets the alpha.
      @param percent Percentage of opaqueness (0..100).
   */
  public void setOpaqueness (int percent) {
    int val = MColor.pct2alpha(percent); // convert from % (0..100) to 0..255
    setAlpha(val); // Changed for BUG 2757
  }

  /** Gets the opaqueness of the layer, which is the percentage of alpha.
      @return Percentage of opaqueness (0..100).
   */
  public int getOpaqueness () {
    return MColor.alpha2pct(alpha); // Changed for BUG 2757
  }
  /** Sets the alpha either directly or called from setOpaqueness.
      @param newAlpha New alpha value (0..255).
   */
  public void setAlpha (int newAlpha) {
    if (newAlpha != alpha) {
      alpha = Math.max(0,Math.min(255,newAlpha));
      //needRefresh = true;
      //refresh();
    }
  }

  /** Gets the alpha of the layer.
      @return Alpha value (0..255).
      @since NeXtMidas 2.7.0
   */
  public int getAlpha() { return alpha; }

  @Override
  public void setEnable (int value) {
    boolean layerHidden = (value&MPlot.EN_GLOBAL)==0;
    if ((enable&MPlot.EN_GLOBAL)==0 != layerHidden) {
      if (selectedObject != null) {
        if (layerHidden) {
          if (activePopup && (kmlPopup != null)) {
            closeCurrentPopup();
            activePopup = true;
          }
        }
        else {
          if (activePopup) drawPopup();
        }
      }
    }
    super.setEnable(value);
  }

  /** Sets the selected kmlObject.
   *
   *  @param kmlObject The selected kmlObject
   *  @since NeXtMidas 4.1.3
   */
  private void setSelectedObject (kmlObject kmlObject) {
    if (kmlObject != selectedObject) {
      selectedObject = kmlObject;
      if (kmlObject == null && activePopup) closeCurrentPopup();
      refresh();
    }
  }

  /** Draw the Popup for the selected kmlObject.
   *  @since NeXtMidas 4.1.3
   */
  private void drawPopup () {
    if (selectedObject == null) return;
    if (activePopup) closeCurrentPopup();
    kmlPopup = new Popup(popupSelection);
    activePopup = true;
  }

  /** Close the current Popup.
   *  @since NeXtMidas 4.1.3
   */
  private void closeCurrentPopup () {
    if (kmlPopup != null) kmlPopup.close();
    activePopup = false;
  }

  @Override
  public boolean clear() {
    // Clear out everything, this is basically a reset
    // Since 3.9.0
    objects.documents.clear();
    objects.folders.clear();
    objects.groundOverlays.clear();
    objects.networkLinkControls.clear();
    objects.networkLinks.clear();
    objects.placemarks.clear();
    setSelectedObject(null);
    popupSelection = null;
    refresh();
    return true;
  }

  @Override
  public void close() {
    super.close();
    setSelectedObject(null);
    popupSelection = null;
    activePopup = false;
    closeCurrentPopup();
  }

  //////////////////////////////////////////////////////////////////////////////
  /////////////////////// NESTED CLASSES ///////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////

  /** KML Parser
      This parser is a basic JAVA SAX XML parser.
      As it parses through the KML, it reads the type of tag, then sends the data
       within the tag to the appropriate handler.
   <pre>
   To add support for a new element or object, do the following:

    1. Add the tag for the element to {@link #supElemList}.

    2. Determine if the element(tag) is an object that will have nested tags.
       If so, add the element to the "IF" list in
       {@link #startElement(String, String, String, Attributes)}. This pushes
       the object to the stack when the start tag is found. If the element just
       contains data (e.g. coordinates), then you do not need to do this step.

    3. Next, (for <b>ALL</b> tags) add the element to the "IF" list in
       {@link #endElement(String, String, String)}. Then add the necessary
       'addTo' method to add the object to its parent. This is where you pop
       the object or data off the stack and add it to its parent object. Make
       sure you have the appropriate "SET" methods in parent objects.
       Note: you must handle all possible parent cases, for casting purposes.
   </pre>
   */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class KmlParser extends DefaultHandler {

    int DEBUG = 0;       // for debugging purposes

    /** Flag to indicate we should capture the data in the characters method.
       This flag is set when the startElement method recognizes an element that
        contains data we are interested in (e.g. coordinates).
     */
    boolean processData;

    final String kmlFile;
    final LayerKML layer = LayerKML.this;  // The layer

    /**  tag is the current element. */
    String tag          = new String();

    /** List of currently supported elements.  */
    Set<String> supElemList = LayerKML.supportedElems;

    /* The stack is used to keep track of nested elements */
    private Stack<Object> stack;
    private Locator locator;

    /* kmlObjs is an object view of the elements and data in the file */
    private kmlObjects       kmlObjs;
    boolean hasKmlStartTag = false;
    boolean hasKmlEndTag   = false;
    boolean objects        = false;
    String  parent;   // Current parent object. Used when keeping track of nested objects.

    private HashMap<String,kmlSchema> schemas;

    /** Constructor   */
    public KmlParser () {
      super();
      stack       = new Stack<>();   // Reset stack
      processData = false;         // Set to true when we want to capture element data
      parent      = new String();
      schemas     = new HashMap<>();
      PlotFile pf = (layer != null)? layer.getFile() : null;
      kmlFile     = (pf != null)? pf.getURL() : null;
    }

    /** Parse a kml file. Called by <code>LayerKML.setFile</code> method.
        This method invokes the SAX XML parser methods and calls the SAX parser
         on the file. When parsing the file, the SAX parser automatically calls
         a number of methods (some of which we override).
        @param filename The name of the file to parse
        @throws Exception &nbsp;
     */
    public void parseKmlFile (InputStream filename) throws Exception {

      /* SAX parser invocation */
//      XMLReader xr = XMLReaderFactory.createXMLReader();      // create XML reader
      XMLReader xr =  SAXParserFactory.newInstance().newSAXParser().getXMLReader(); // Changed to solve deprecation warning Since 3.9.0
//      KmlParser myHandler = new KmlParser();                  // create Handler
      xr.setContentHandler(kmlParser);                        // set Content handler - changed to use the current handler rather than a new instance Since 3.9.0
      xr.setErrorHandler(kmlParser);                          // set error Handler

      try {
        Reader in = new InputStreamReader(filename);
        if (in != null) xr.parse(new InputSource(in));
      }
      catch (Exception e) {
        String popupMsg = "Bad KML: "+e.getMessage()+" Click OK to continue Plot (excluding this KML) or Cancel to abort";
        String boo = MJPopup.showOkCancel(MP.getPanel(), "Bad KML found.", JOptionPane.OK_CANCEL_OPTION, popupMsg);
        String wmswarn = "LayerKML:"+name+" Bad KML "+e.getMessage();
        if (boo == null){         // null means they hit cancel button
          wmswarn += " and cancelling this plot.";
          MP.status = MWindow.EXIT;
          MP.MW.refresh();
        }
        else {
          wmswarn += " but continuing to plot excluding this KML.";
        }

        layer.warning(wmswarn);
        e.printStackTrace();
      }
    }

    /** Called automatically by the SAX parser when the <code>xml</code>
        tag is encountered.
     */
    public void startDocument () {
      if (DEBUG > 3) System.out.println("Start document");
    }

    /** Called automatically by the SAX parser when the <code>&lt;/xml&gt;</code>
         tag is encountered or document ends. If it has reached the end of
         the file without encountering the closing <code>kml</code> tag,
         it is assumed that the file does not have one and the "wrapper"
         <code>kmlObjects</code> object is created. Since the call to this method
         signifies that the file is done parsing, we can then examine the
         objects created from the file and plot them.
     */
    public void endDocument () {
      if (!hasKmlEndTag ) {
        Object obj = null;
        if (!stack.isEmpty()) obj= stack.pop();
        if (obj instanceof kmlObjects) {
          layer.warning("Missing KML tags in file " + kmlFile + " obj=" + obj);
          addToKml((kmlObjects)obj);
        }
      }
      layer.setKmlObjects(getKmlObjects());     // Sets the kml Objects in LayerKML class
    }

    /** Get the kmlObjs object.
        @return kmlObjs The kmlObjects.
     */
    public kmlObjects getKmlObjects () { return kmlObjs; }

    /** Set the Locator   */
    public void setDocumentLocator( Locator rhs ) { locator = rhs; }

    /** Called automatically by the SAX parser when it encounters an opening
         tag/element. For our purposes, we then look at the element to
         determine if we support it. If so, we create a new kml object for
         'parent' objects such as <code>Placemark</code> and <code>Point</code>
         or a StringBuffer for actual data, such as <code>name</code>. All of
         the objects are put on the stack for future use. For data
         elements we set the {@link #processData} flag to true.
        @param uri         The xml uri
        @param localName   The short name of the element
        @param qName       The full name of the element
        @param attributes  Any attributes associated with the element
        @since updated in 3.9.0 to use qName instead of localName for the parsing.
     */
    public void startElement (String uri, String localName, String qName, Attributes attributes) {
      if (DEBUG > 3) System.out.println("startElement: uri=" +uri+ ", localName=" +localName+", qName = "+qName);

      /* For standardization purposes, convert all element names to lower case */
      String element = qName.toLowerCase();

      if (element.equals("url")) element = "link";   // convert url tag to link tag

      /* If the element is one of the tags we parse, capture the data
         Complex (nested) elements first) */
      if (supElemList.contains(element)) {
        if (!hasKmlStartTag || (element.equalsIgnoreCase("kml"))) {
          kmlObjs = new kmlObjects();
          stack.push(kmlObjs);               // Used to hold the kml Objects
          hasKmlStartTag = true;
        }
        if (element.equalsIgnoreCase("kml")) {} // do nothing. Handled above
        else if (element.equalsIgnoreCase("networklinkcontrol")) {
          stack.push(new kmlNetworkLinkControl()); // kmlNetworkLinkControl
        } else if (element.equalsIgnoreCase("networklink")) {
          stack.push(new kmlNetworkLink());        // kmlNetworkLink
        } else if (element.equalsIgnoreCase("link")) {
          stack.push(new kmlLink());              // kmlLink
        } else if (element.equals("document")) {
          stack.push(new kmlDocument());           // new kmlDocument object
        } else if (element.equals("schema")) {
          kmlSchema schema = new kmlSchema();      // new kmlSchema object
          stack.push(schema);
          for (int xx=0; xx<attributes.getLength(); xx++) {
            String attr = attributes.getLocalName(xx).toLowerCase();
            if (attr.equals("name")) {
              String name = attributes.getValue(xx).toLowerCase();
              schema.setName(name);
              schemas.put(name, schema);
              supElemList.add(name); // add to list of tags we support
            } else if (attr.equals("parent")) {
              schema.setParent(attributes.getValue(xx).toLowerCase());
            }
          }
        } else if (element.equalsIgnoreCase("folder")) {
          stack.push(new kmlFolder());             // kmlFolder
        } else if (element.equals("placemark")) {
          stack.push(new kmlPlacemark());          // kmlPlacemark object
        } else if (element.equals("multigeometry")) {
          stack.push(new kmlMultiGeometry());      // new kmlMultiGeometry object
        } else if (element.equals("point")) {
          Object obj = stack.peek();
          if (!(obj instanceof kmlFeature)) {
            Object tmpObj = stack.pop();
            obj = stack.peek();        // Go up another level to get the Feature
            stack.push(tmpObj);
          }
          stack.push(new kmlPoint(obj));
        } else if (element.equals("polygon")) {
          stack.push(new kmlPolygon());            // new kmlPolygon object
        } else if (element.equals("linearring")) {
          stack.push(new kmlLinearRing());         // new kmlLinearRing object
        } else if (element.equals("linestring")) {
          stack.push(new kmlLineString());         // new kmlLineString object
        } else if (element.equals("outerboundaryis")) {
          ((kmlPolygon)stack.peek()).setOuter(true);  // set outer ring flag to true
        } else if (element.equalsIgnoreCase("groundoverlay")) {
          stack.push(new kmlGroundOverlay());        // kmlGroundOverlay
        } else if (element.equals("lookat")) {
          stack.push(new kmlLookAt());             // kmlLookAt
        } else if (element.equals("polystyle")) {
          stack.push(new kmlPolyStyle());          // new kmlPolyStyle object
        } else if (element.equals("style")) {
          stack.push(new kmlStyle());              // new kmlStyle object
          for (int xx=0; xx< attributes.getLength(); xx++) {
            if (attributes.getLocalName(xx).equalsIgnoreCase("id")) {
              ((kmlStyle)stack.peek()).setId(attributes.getValue(xx));  // Set the Style Id
            }
          }
        } else if (element.equals("stylemap")) {   // new kmmlStyleMap object
          stack.push(new kmlStyleMap());
          for (int xx=0; xx< attributes.getLength(); xx++) {
            if (attributes.getLocalName(xx).equalsIgnoreCase("id")) {
              ((kmlStyleMap)stack.peek()).setId(attributes.getValue(xx));  // Set the StyleMap Id
            }
          }
        } else if (element.equals("pair"))     {   // new kmlStylePair object
          stack.push(new kmlStyleMapPair());
        } else if (element.equals("labelstyle")) {
          stack.push(new kmlLabelStyle());          // new kmlLineStyle object
        } else if (element.equals("linestyle")) {
          stack.push(new kmlLineStyle());          // new kmlLineStyle object
        } else if (element.equals("iconstyle")) {
          stack.push(new kmlIconStyle());          // new kmlIconStyle object
        } else if (element.equals("balloonstyle")) {
          stack.push(new kmlBalloonStyle());      // new kmlBalloonStyle
        } else if (element.equals("icon")) {
          stack.push(new kmlIcon());               // new kmlIcon object
        } else if (element.equals("latlonbox")) {
          // recognize tag but do nothing
        } else if (schemas.keySet().contains(element)) {   // within the schema
          if (schemas.get(element).parent.equals("placemark")) {   // right now we only support placemarks
            stack.push(new kmlPlacemark());
          }
        }
        else {
          stack.push(new StringBuffer());          // Used to hold element data
          processData = true;                      // Indicates that next set of data is relevant
        }
      }
    }

    /** Called automatically by the SAX parser when it encounters the closing
         tag for an element, such as <code>/coordinates</code>. For our
         purposes, we can then add the nested objects to their parents, such as
         <code>coordinates</code> to their <code>point</code>.
     */
    public void endElement (String uri, String localName, String rawName) {
      processData     = false;  // Set to false since data element is closing

      /* For standardization purposes, convert all element names to lower case */
      String element = rawName.toLowerCase(); // Since 3.9.0 use rawName vs localName, localName comes in empty when using the new parser.

      if (element.equals("url")) element = "link";  // convert url tag to link tag

      if (supElemList.contains(element) && !element.equals("outerboundaryis")) {
        Object obj = stack.pop();    // Get the newest object off the stack
        obj = StringUtil.trimIfString(obj); // fix for Bug 1825 (to trim String values within tags)

        if (!element.equals("kml"))  {
          parent = stack.peek().getClass().getName();
          int i  = parent.indexOf("$");
          parent = parent.substring(i+4).toLowerCase();
        }

        //System.out.println("LayKML endElem() Popped a "+localName+" nested in "+ parent+ " from the stack");

        /* The IF statements below evaluate the tag and put the resulting child
              (nested) data on the parent object. Parent objects themselves are
              added to the appropriate Vector of objects in the kmlObjects class.
         */
        if (element.equals("kml"))                  { hasKmlEndTag=true; addToKml((kmlObjects)obj); }
        else if (element.equals("networklink"))       addToContainer(element, obj);
        else if (element.equals("style"))             addToFeature(element, obj);
        else if (element.equals("stylemap"))          addToFeature(element, obj);
        else if (parent.equals("stylemap"))           addToStyleMap(element, obj);
        else if (parent.equals("stylemappair"))       addToPair(element, obj);
        else if (parent.equals("objects"))            addToKmlObjects(obj);
        else if (parent.equals("networklinkcontrol")) addToNetworkLinkControl(element, obj);
        else if (parent.equals("networklink"))        addToNetworkLink(element, obj);
        else if (parent.equals("link"))               addToLink(element, obj);
        else if (parent.equals("document"))           addToDocument(element, obj);
        else if (parent.equals("folder"))             addToFolder(element, obj);
        else if (parent.equals("placemark"))          addToPlacemark(element, obj);
        else if (parent.equals("multigeometry"))      addToMultiGeometry(element, obj);
        else if (parent.equals("point"))              addToPoint(element, obj);
        else if (parent.equals("polygon"))            addToPolygon(element, obj);
        else if (parent.equals("linearring"))         addToLinearRing(element, obj);
        else if (parent.equals("linestring"))         addToLineString(element, obj);
        else if (parent.equals("groundoverlay"))      addToGroundOverlay(element, obj);
        else if (parent.equals("lookat"))             addToLookAt(element, obj);
        else if (parent.equals("polystyle"))          addToPolyStyle(element, obj);
        else if (parent.equals("style"))              addToStyle(element, obj);
        else if (parent.equals("labelstyle"))         addToLabelStyle(element, obj);
        else if (parent.equals("linestyle"))          addToLineStyle(element, obj);
        else if (parent.equals("iconstyle"))          addToIconStyle(element, obj);
        else if (parent.equals("balloonstyle"))       addToBalloonStyle(element, obj);
        else if (parent.equals("icon"))               addToIcon(element, obj);
        else if (parent.equals("schema"))             addToSchema(element, obj);

        else { } // Should not enter this but if so...
      }
    }

    private void addToKml (kmlObjects obj) {
      kmlObjs = obj;
      objects = true;
    }

    private void addToKmlObjects (Object obj) {
      ((kmlObjects)stack.peek()).addKmlObject(obj);
    }

    private void addToNetworkLinkControl (String element, Object obj) {
      if (element.equals("minrefreshperiod")) {
        ((kmlNetworkLinkControl)stack.peek()).setMinRefreshPeriod(Float.parseFloat(obj.toString()));
      } else if (element.equals("lookat")) {
        ((kmlNetworkLinkControl)stack.peek()).setAbstractView((kmlLookAt)obj);
      }
    }

    private void addToNetworkLink (String element, Object obj) {
      if (element.equals("link")) {
        ((kmlNetworkLink)stack.peek()).setLink((kmlLink)obj);
      } else if (element.equals("styleurl")) {
        ((kmlNetworkLink)stack.peek()).setStyleUrl(obj.toString());
      } else if (element.equals("name")) {
        ((kmlNetworkLink)stack.peek()).setName(obj.toString());
      }
    }

    private void addToLink (String element, Object obj) {
      if (element.equals("refreshinterval")) {
        ((kmlLink)stack.peek()).setRefreshInterval(Float.parseFloat(obj.toString()));
      } else if (element.equals("refreshmode")) {
        ((kmlLink)stack.peek()).setRefreshMode((obj.toString()));
      } else if (element.equals("href")) {
        ((kmlLink)stack.peek()).setHref(obj.toString());
      }
    }

    private void addToDocument (String element, Object obj) {
      if (element.equals("folder")) {
        ((kmlDocument)stack.peek()).addFeature((kmlFolder)obj);
      } else if (element.equals("placemark")) {
        ((kmlDocument)stack.peek()).addFeature((kmlPlacemark)obj);
      } else if (element.equals("groundoverlay")) {
        ((kmlDocument)stack.peek()).addFeature((kmlGroundOverlay)obj);
      } else if (element.equals("visibility")) {
        boolean booTmp = !LayerKML.isKMLFalse(obj.toString());
        ((kmlDocument)stack.peek()).setVisibility(booTmp);
      } else if (element.equals("styleurl")) {
        String styleId = obj.toString();
        if (styleId.startsWith("#")) styleId = styleId.substring(1);
        ((kmlDocument)stack.peek()).setStyleUrl(styleId);
      } else if (element.equals("name")) {
        ((kmlDocument)stack.peek()).setName(obj.toString());
      } else if (element.equals("description")) {
        ((kmlDocument)stack.peek()).setDescription(obj.toString());
      } else if (schemas.keySet().contains(element)) {
        addToDocument(schemas.get(element).parent, obj);  // see if the element is custom schema
      }
    }

    private void addToFolder (String element, Object obj) {
      if (element.equals("folder")) {
        ((kmlFolder)stack.peek()).addFolder((kmlFolder)obj);
      } else if (element.equals("document")) {
        ((kmlFolder)stack.peek()).addFeature((kmlDocument)obj);
      } else if (element.equals("placemark")) {
        ((kmlFolder)stack.peek()).addFeature((kmlPlacemark)obj);
      } else if (element.equals("groundoverlay")) {
        ((kmlFolder)stack.peek()).addFeature((kmlGroundOverlay)obj);
      } else if (element.equals("visibility")) {
        boolean booTmp = !LayerKML.isKMLFalse(obj.toString());
        ((kmlFolder)stack.peek()).setVisibility(booTmp);
      } else if (element.equals("styleurl")) {
        String styleId = obj.toString();
        if (styleId.startsWith("#")) styleId = styleId.substring(1);
        ((kmlFolder)stack.peek()).setStyleUrl(styleId);
      } else if (element.equals("name")) {
        ((kmlFolder)stack.peek()).setName(obj.toString());
      }
    }

    private void addToSchema (String element, Object obj) {
      // placeholder for future support
    }

    private void addToPlacemark (String element, Object obj) {
      if (element.equals("multigeometry")) {
        ((kmlPlacemark)stack.peek()).setGeometry((kmlMultiGeometry)obj);
      } else if (element.equals("polygon")) {
        ((kmlPlacemark)stack.peek()).setGeometry((kmlPolygon)obj);
      } else if (element.equals("point")) {
        ((kmlPlacemark)stack.peek()).setGeometry((kmlPoint)obj);
      } else if (element.equals("linearring")) {
        ((kmlPlacemark)stack.peek()).setGeometry((kmlLinearRing)obj);
      } else if (element.equals("linestring")) {
        ((kmlPlacemark)stack.peek()).setGeometry((kmlLineString)obj);
      } else if (element.equals("styleurl")) {
        String styleId = obj.toString();
        if (styleId.startsWith("#")) styleId = styleId.substring(1);
        ((kmlPlacemark)stack.peek()).setStyleUrl(styleId);
      } else if (element.equals("name")) {
        ((kmlPlacemark)stack.peek()).setName(obj.toString());
      } else if (element.equals("description")) {
        ((kmlPlacemark)stack.peek()).setDescription(obj.toString());
      }
    }

    private void addToMultiGeometry (String element, Object obj) {
      if (element.equals("polygon")) {
        ((kmlMultiGeometry)stack.peek()).addGeometry((kmlPolygon)obj);
      } else if (element.equals("point")) {
        ((kmlMultiGeometry)stack.peek()).addGeometry((kmlPoint)obj);
      } else if (element.equals("linearring")) {
        ((kmlMultiGeometry)stack.peek()).addGeometry((kmlLinearRing)obj);
      } else if (element.equals("linestring")) {
        ((kmlMultiGeometry)stack.peek()).addGeometry((kmlGeometry)obj);
      }
    }

    private void addToPoint (String element, Object obj) {
      if (element.equals("extrude")) {
        boolean boo = LayerKML.isKMLTrue(obj.toString());  // anything but "true" or "1" is FALSE
        ((kmlPoint)stack.peek()).setExtrude(boo);
      }  else if (element.equals("coordinates")) {
        ((kmlPoint)stack.peek()).setCoordinates(obj.toString());
      }
    }

    private void addToPolygon (String element, Object obj) {
      if (element.equals("linearring")) {
        if (((kmlPolygon)stack.peek()).isOuter()) {
          ((kmlPolygon)stack.peek()).setOuterBoundary((kmlLinearRing)obj);
          ((kmlPolygon)stack.peek()).setOuter(false);
        } else {
          ((kmlPolygon)stack.peek()).addInnerBoundary((kmlLinearRing)obj);
        }
      } else if (element.equals("extrude")) {
        boolean boo = LayerKML.isKMLTrue(obj.toString());  // anything but "true" or "1" is FALSE
        ((kmlPolygon)stack.peek()).setExtrude(boo);
      } else if (element.equals("altitudemode")) {
        ((kmlPolygon)stack.peek()).setAltitudeMode(obj.toString());
      } else if (element.equals("tessellate")) {
        boolean boo = LayerKML.isKMLTrue(obj.toString());  // anything but "true" or "1" is FALSE
        ((kmlPolygon)stack.peek()).setTessellate(boo);
      } else if (element.equals("coordinates")) {
        ((kmlPolygon)stack.peek()).addCoordinates(obj.toString());
      }
    }

    private void addToLinearRing (String element, Object obj) {
      if (element.equals("extrude")) {
        boolean boo = LayerKML.isKMLTrue(obj.toString());  // anything but "true" or "1" is FALSE
        ((kmlLinearRing)stack.peek()).setExtrude(boo);
      } else if (element.equals("coordinates")) {
        ((kmlLinearRing)stack.peek()).setCoordsVector(obj.toString());
      }
    }

    private void addToLineString (String element, Object obj) {
      if (element.equals("extrude")) {
        boolean boo = LayerKML.isKMLTrue(obj.toString());  // anything but "true" or "1" is FALSE
        ((kmlLineString)stack.peek()).setExtrude(boo);
      } else if (element.equals("coordinates")) {
        ((kmlLineString)stack.peek()).setCoordsVector(obj.toString());
      }
    }

    private void addToGroundOverlay (String element, Object obj) {
      if (element.equals("north")) {
        ((kmlGroundOverlay)stack.peek()).setNorth(Double.parseDouble(obj.toString()));
      } else if (element.equals("south")) {
        ((kmlGroundOverlay)stack.peek()).setSouth(Double.parseDouble(obj.toString()));
      } else if (element.equals("east"))  {
        ((kmlGroundOverlay)stack.peek()).setEast(Double.parseDouble(obj.toString()));
      } else if (element.equals("west")) {
        ((kmlGroundOverlay)stack.peek()).setWest(Double.parseDouble(obj.toString()));
      } else if (element.equals("rotation")) {
        ((kmlGroundOverlay)stack.peek()).setRotation(Double.parseDouble(obj.toString()));
      } else if (element.equals("icon")) {
        ((kmlOverlay)stack.peek()).setIcon((kmlIcon)obj);
      }  else if (element.equals("color")) {
        Color newColor = toColor(obj);
        ((kmlGroundOverlay)stack.peek()).setColor(newColor);
      }
    }

    private void addToLookAt (String element, Object obj) {
      if (element.equals("longitude")) {
        ((kmlLookAt)stack.peek()).setLon(Double.parseDouble(obj.toString()));
      } else if (element.equals("latitude")) {
        ((kmlLookAt)stack.peek()).setLat(Double.parseDouble(obj.toString()));
      } else if (element.equals("altitude")) {
        ((kmlLookAt)stack.peek()).setAlt(Double.parseDouble(obj.toString()));
      } else if (element.equals("heading")) {
        ((kmlLookAt)stack.peek()).setHeading(Double.parseDouble(obj.toString()));
      } else if (element.equals("range")) {
        ((kmlLookAt)stack.peek()).setRange(Double.parseDouble(obj.toString()));
      } else if (element.equals("tilt")) {
        ((kmlLookAt)stack.peek()).setTilt(Double.parseDouble(obj.toString()));
      }
    }

    /** Converts input KML-formatted color string (ABGR) to Color (ARGB).
        @param obj Should be String, StringBuffer, StingBuilder object (cannot be null)
        @return Color (ARGB)
        @todo JWB: Move to MColor like getColor(String, ABGR)
     */
    private Color toColor(Object obj) {
      /* Check formatting of color to ensure it is recognized */
      String color = obj.toString().toUpperCase();
      if (!color.startsWith("#") && !color.startsWith("0X")) {
        try {
          // most common case is HEX String minus the '0X'
          color = "0X" + color;
          Color newColor = MColor.BGRtoRGB(MColor.getColor(color));
          return newColor;
        } catch (Exception e) {
          color = obj.toString().toUpperCase(); // further errors caught in return statement
          return MColor.getColor(color);
        }
      }
      return MColor.BGRtoRGB(MColor.getColor(color));
    }

    private void addToPolyStyle (String element, Object obj) {
      if (element.equals("fill")) {
        boolean booTmp = !LayerKML.isKMLFalse(obj.toString());
        ((kmlPolyStyle)stack.peek()).setFill(booTmp);
      } else if (element.equals("outline")) {
        boolean booTmp = !LayerKML.isKMLFalse(obj.toString());
        ((kmlPolyStyle)stack.peek()).setOutline(booTmp);
      } else if (element.equals("color")) {
        Color newColor = toColor(obj);
        ((kmlPolyStyle)stack.peek()).setColor(newColor);
      }
    }

    private void addToStyleMap (String element, Object obj) {
      if (element.equals("pair")) {
        ((kmlStyleMap)stack.peek()).setStyleMapPair((kmlStyleMapPair)obj);
      }
    }

    private void addToPair (String element, Object obj) {

      if (element.equals("key")) {
        ((kmlStyleMapPair)stack.peek()).setKey(obj.toString());
      } else if (element.equals("styleurl")) {
        String val = obj.toString();
        if (val.startsWith("#")) val = val.substring(1);  // remove the elading '#'
        ((kmlStyleMapPair)stack.peek()).setStyleUrl(val);
      }
    }

    private void addToStyle (String element, Object obj) {
      if (element.equals("linestyle")) {
        ((kmlStyle)stack.peek()).setLineStyle((kmlLineStyle)obj);
      } else if (element.equals("labelstyle")) {
        ((kmlStyle)stack.peek()).setLabelStyle((kmlLabelStyle)obj);
      } else if (element.equals("polystyle")) {
        ((kmlStyle)stack.peek()).setPolyStyle((kmlPolyStyle)obj);
      } else if (element.equals("iconstyle")) {
        ((kmlStyle)stack.peek()).setIconStyle((kmlIconStyle)obj);
      } else if (element.equals("balloonstyle")) {
        ((kmlStyle)stack.peek()).setBalloonStyle((kmlBalloonStyle)obj);
      }
    }

    private void addToLabelStyle (String element, Object obj) {
      if (element.equals("color")) {
        Color newColor = toColor(obj);
        ((kmlLabelStyle)stack.peek()).setColor(newColor);
      } else if (element.equals("scale")) {
        ((kmlLabelStyle)stack.peek()).setScale(Float.parseFloat(obj.toString()));
      }
    }

    private void addToLineStyle (String element, Object obj) {
      if (element.equals("color")) {
        Color newColor = toColor(obj);
        ((kmlLineStyle)stack.peek()).setColor(newColor);
      } else if (element.equals("width")) {
        ((kmlLineStyle)stack.peek()).setWidth(Float.parseFloat(obj.toString()));
      }
    }

    private void addToIconStyle (String element, Object obj) {
      if (element.equals("icon")) {
        ((kmlIconStyle)stack.peek()).setIcon((kmlIcon)obj);
      } else if (element.equals("color")) {
        Color newColor = toColor(obj);
        ((kmlIconStyle)stack.peek()).setColor(newColor);
      } else if (element.equals("heading")) {
        ((kmlIconStyle)stack.peek()).setHeading(Float.parseFloat(obj.toString()));
      } else if (element.equals("scale")) {
        ((kmlIconStyle)stack.peek()).setScale(Float.parseFloat(obj.toString()));
      }
    }

    private void addToBalloonStyle (String element, Object obj) {
      if (element.equals("bgcolor")) {
        Color color = toColor(obj);
        ((kmlBalloonStyle)stack.peek()).setBgColor(color);
      } else if (element.equals("textcolor")) {
        Color color = toColor(obj);
        ((kmlBalloonStyle)stack.peek()).setTextColor(color);
      } else if (element.equals("text")) {
        ((kmlBalloonStyle)stack.peek()).setText(obj.toString());
      } else if (element.equals("displaymode")) {
        ((kmlBalloonStyle)stack.peek()).setDisplayMode(obj.toString());
      }
    }

    private void addToIcon (String element, Object obj) {
      if (element.equals("href")) {
        ((kmlIcon)stack.peek()).setHref(obj.toString());
      } else if (element.equals("viewformat")) {
        ((kmlIcon)stack.peek()).setViewFormat(obj.toString());
      }
    }

    private void addToContainer (String element, Object obj) {
      if (element.equals("networklink")) {
        if (stack.peek() instanceof kmlContainer) {
          ((kmlContainer)stack.peek()).addNetworkLink((kmlNetworkLink)obj);
        } else if (stack.peek() instanceof kmlObjects) {
          ((kmlObjects)stack.peek()).addNetworkLink((kmlNetworkLink)obj);
        }
      }
    }

    private void addToFeature (String element, Object obj) {
      Object parent = stack.peek();
      if (parent instanceof kmlSchema) {
        addToSchema(element, obj);
        return;
      }
      if (element.equals("style")) {
        ((kmlFeature)parent).addStyle((kmlStyle)obj);
      } else if (element.equals("lookat")) {
        ((kmlFeature)parent).setAbstractView((kmlLookAt)obj);
      } else if (element.equals("stylemap")) {
        ((kmlFeature)parent).addStyleMap((kmlStyleMap)obj);
      }
    }

    /** Convert a KML-formatted color (aabbggrr) to standard (aarrggbb)
        @param kmlColor The color in the KML file to be converted
        @return The KML color in standard aarrggbb format
        @deprecated Since NeXtMidas 3.3.2. Use {@link MColor#BGRtoRGB(int)}
     */
    @Deprecated
    public int convertColor (int kmlColor) {
     return MColor.BGRtoRGB(kmlColor);
    }

    /** Convert a KML-formatted color (aabbggrr) to standard (aarrggbb)
        @param kmlColor The color in the KML file to be converted
        @return The KML color in standard aarrggbb format
        @deprecated Since NeXtMidas 3.3.2. Use {@link MColor#BGRtoRGB(Color)}
     */
    @Deprecated
    public Color convertColor (Color kmlColor) {
      return MColor.BGRtoRGB(kmlColor);
    }

    /** Called automatically by the SAX parser when it encounters data outside
         of an element tag, which it assumes to be data relevant to the most
         recent open element. In order to avoid processing data that we do not
         want or support, we use the flag {@link #processData} to indicate
         whether the data should be captured. If so, we load that data into the
         current item on the stack, which should always be a <code>StringBuffer
         </code>.
     */
    public void characters (char[] ch, int start, int length) {

      if (processData) {
        if (DEBUG > 1) System.out.println("Adding char data to StringBuffer");
        ((StringBuffer)stack.peek()).append( ch, start, length );
      } else {}    // ignore unwanted data
    }

  }

  ////////////////////////////////////////////////////////////////////
  /** This class is used to store the objects created during the parsing of the
       kml file. It contains Vectors for each of the objects and methods to add
       the parsed objects to their associated Vector.
       <pre>
      Note: Most of the Vectors will be empty, since most kml objects are
             nested. Only the Vector for the top-most parent objects
             (such as <code>Placemarks</code> and <code>Documents</code>) will
             have data. But since this is not enforced in kml, all the objects
             are available.
     </pre>
   */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlObjects {

    /* Supported kml Objects */
    private Vector<kmlPlacemark>          placemarks;
    private Vector<kmlFolder>             folders;
    private Vector<kmlDocument>           documents;
    private Vector<kmlNetworkLinkControl> networkLinkControls;
    private Vector<kmlGroundOverlay>      groundOverlays;
    private Vector<kmlNetworkLink>        networkLinks;

    /** Constructor instantiates the Vectors   */
    public kmlObjects () {
      placemarks            = new Vector<>();
      folders               = new Vector<>();
      documents             = new Vector<>();
      networkLinkControls   = new Vector<>();
      groundOverlays        = new Vector<>();
      networkLinks          = new Vector<>();
    }

    /** generic method to add a Kml object
     *  @param obj the KML object to add */
    public void addKmlObject (Object obj) {
      if      (obj instanceof kmlPlacemark)          addPlacemark((kmlPlacemark)obj);
      else if (obj instanceof kmlDocument)           addDocument((kmlDocument)obj);
      else if (obj instanceof kmlFolder)             addFolder((kmlFolder)obj);
      else if (obj instanceof kmlNetworkLinkControl) addNetworkLinkControl((kmlNetworkLinkControl)obj);
      else if (obj instanceof kmlGroundOverlay)      addGroundOverlay((kmlGroundOverlay)obj);
      else if (obj instanceof kmlNetworkLink)        addNetworkLink((kmlNetworkLink)obj);
      else warning("Unknown object " +obj.getClass().getSimpleName()+". Not adding to List");
    }

    /** Add KML Placemark
     *  @param p kmlPlacemark to add to placemarks
     */
    public void addPlacemark (kmlPlacemark p) { placemarks.add(p);  }

    /** Add KML Folder
     *  @param folder kmlFolder to add to folders
     */
    public void addFolder (kmlFolder folder) { folders.add(folder);  }

    /** Add KML Document
     *  @param d kmlDocument to add to documents
     */
    public void addDocument (kmlDocument d) {  documents.add(d);   }

    /** Add KML Ground Overlay
     *  @param overlay kmlGroundOverlay to add to ground ovelays
     */
    public void addGroundOverlay (kmlGroundOverlay overlay) { groundOverlays.add(overlay); }

    /** Add a KML Network Link Control
     *  @param control kmlNetworkLinkControl to add to network link controls
     */
    public void addNetworkLinkControl (kmlNetworkLinkControl control) {
      networkLinkControls.add(control);
    }

    /** Add a KML Network link
     *  @param link kmlNetworkLink to add to network links
     */
    public void addNetworkLink (kmlNetworkLink link) {
      networkLinks.add(link);
    }

    public String toString() {
      String res = "************************************\n";
      res += "placemarks.size()=" + placemarks.size();
      res += "\nfolders.size()=" + folders.size();
      res += "\ndocuments.size()=" + documents.size();
      res += "\nnetworkLinkControls.size()=" + networkLinkControls.size();
      res += "\ngroundOverlays.size()=" + groundOverlays.size();
      res += "\nnetworkLinks.size()=" + networkLinks.size();
      return res;
    }

    // public void addRegion (kmlRegion r) { regions.add(r); }
  }

  ///////////////////////////////////////////////////////////////////
  /** KML Abstract Object */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlObject {
    String id;          // optional
    String targetId;    // optional

    /** default constructor */
    public kmlObject () {}

    /** get ID
     *  @return ID
     */
    public String getId () { return id; }
    /** set ID
     *  @param i ID
     */
    public void setId (String i) { id = i; }

    /** get target ID
     *  @return target ID
     */
    public String getTargetId () { return targetId; }
    /** set target ID
     *  @param t target ID
     */
    public void setTargetId (String t) { targetId = t; }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Feature */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlFeature extends kmlObject {
    String              name;
    boolean             visibility            = true;
    boolean             open                  = false;
    String              address;
    String              phoneNumber;
    String              snippet;
    int                 snippetMaxLines       = 2;
    String              description;
    kmlTimePrimitive    timePrimitive;
    String              styleUrl;
    kmlStyleSelector    styleSelector;
    kmlRegion           region;
    kmlExtendedData     extendedData;
    Vector<kmlStyle>    styles;
    Vector<kmlStyleMap> styleMaps;
    kmlAbstractView     abstractView;

    /** kmlFeature Default Constructor */
    public kmlFeature () {
      name          = new String();
      address       = new String();
      phoneNumber   = new String();
      snippet       = new String();
      description   = new String();
      timePrimitive = new kmlTimePrimitive();
      styleUrl      = null;
      styleSelector = new kmlStyleSelector();
      region        = new kmlRegion();
      extendedData  = new kmlExtendedData();
      styles        = new Vector<>();
      abstractView  = new kmlAbstractView();
      styleMaps     = new Vector<>();
    }

    /** add style
     *  @param style KML style
     */
    public void addStyle (kmlStyle style) {
      styles.add(style);
    }
    /** get styles
     *  @return KML styles
     */
    public Vector<kmlStyle> getStyles () {
      return styles;
    }

    /** add style map
     *  @param styleMap KML style map
     */
    public void addStyleMap (kmlStyleMap styleMap) {
      styleMaps.add(styleMap);
    }
    /** get style maps
     *  @return KML style maps
     */
    public Vector<kmlStyleMap> getStyleMaps () {
      return styleMaps;
    }

    /** get style url
     *  @return style url
     */
    public String getStyleUrl() { return styleUrl; }
    /** set style url
     *  @param styleUrl style url
     */
    public void setStyleUrl(String styleUrl) { this.styleUrl = styleUrl;}

    /** get name
     *  @return name
     */
    public String getName () { return name; }
    /** set name
     *  @param n name
     */
    public void setName (String n) { name = n; }

    /** get visibility
     *  @return visibility
     */
    public boolean getVisibility () { return visibility; }
    /** set visibility
     *  @param v visibility
     */
    public void setVisibility (boolean v) { visibility = v; }

    /** get open
     *  @return open
     */
    public boolean getOpen () { return open; }
    /** set open
     *  @param o open
     */
    public void setOpen (boolean o) { open = o; }

    /** get address
     *  @return address
     */
    public String getAddress () { return address; }
    /** set address
     *  @param a address
     */
    public void setAddress (String a) { address = a; }

    /** get phone number
     *  @return phone number
     */
    public String getPhoneNumber () { return phoneNumber; }
    /** set phone number
     *  @param p phone number
     */
    public void setPhoneNumber (String p) { phoneNumber = p; }

    /** get snippet max lines
     *  @return snippet max lines
     */
    public int getSnippetMaxLines () { return snippetMaxLines; }
    /** set snippet max lines
     *  @param lines snippet max lines
     */
    public void setSnippetMaxLines (int lines) { snippetMaxLines = lines; }

    /** get snippet
     *  @return snippet
     */
    public String getSnippet () { return snippet; }
    /** set snippet
     *  @param s snippet
     */
    public void setSnippet ( String s) { snippet = s; }

    /** get description
     *  @return description
     */
    public String getDescription () { return description; }
    /** set description
     *  @param s description
     */
    public void setDescription (String s) { description = s; }

    /** get time primitive
     *  @return KML time primitive
     */
    public kmlTimePrimitive getTimePrimitive () { return timePrimitive; }
    /** set time primitive
     *  @param tp KML time primitive
     */
    public void setTimePrimitive (kmlTimePrimitive tp) { timePrimitive = tp; }

    /** get style selector
     *  @return KML style selector
     */
    public kmlStyleSelector getStyleSelector () { return styleSelector; }
    /** set style selector
     *  @param ss KML style selector
     */
    public void setStyleSelector (kmlStyleSelector ss) { styleSelector = ss; }

    /** get extended data
     *  @return KML extended data
     */
    public kmlExtendedData getExtendedData () { return extendedData; }
    /** set extended data
     *  @param ed KML extended data
     */
    public void setExtendedData (kmlExtendedData ed) { extendedData = ed; }

    /** get region
     *  @return KML region
     */
    public kmlRegion getRegion () { return region; }
    /** set region
     *  @param r KML region
     */
    public void setRegion (kmlRegion r) { region = r; }

    /** get abstract view
     *  @return KML abstract view
     */
    public kmlAbstractView getAbstractView() { return abstractView;  }
    /** set abstract view
     *  @param abstractView KML abstract view
     */
    public void setAbstractView(kmlAbstractView abstractView) { this.abstractView = abstractView; }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Geometry  - Abstract -  */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlGeometry extends kmlObject {

    private kmlFeature      parentFeature = new kmlFeature();
    private kmlBalloonStyle balloonStyle;
    private Line            line;
    private BufferedImage   img;

    /** default constructor */
    public kmlGeometry () {}

    /** set KML parent feature
     *  @param feat KML parent feature
     */
    public void setParentFeature (kmlFeature feat) {
      parentFeature = feat;
    }

    /** get KML parent feature
     *  @return KML parent feature
     */
    public kmlFeature getParentFeature () {
      return parentFeature;
    }

    /** get balloon style
     *  @return KML balloon style
     */
    public kmlBalloonStyle getBalloonStyle () { return balloonStyle; }

    /** set balloon style
     *  @param bs KML balloon style
     */
    public void setBalloonStyle (kmlBalloonStyle bs) { balloonStyle = bs; }

    /** get line
     *  @return line
     */
    public Line getLine () { return line; }
    /** set line
     *  @param line KML line
     */
    public void setLine (Line line) { this.line = line; }

    /** get buffered image
     *  @return buffered image
     */
    public BufferedImage getImage () { return img; }
    /** set buffered image
     *  @param image buffered image
     */
    public void setImage (BufferedImage image) { img = image; }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Abstract View
      Defines which view to use (Camera or LookAt)
   */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlAbstractView extends kmlObject {

    /** default constructor */
    public kmlAbstractView () {}

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Balloon Style
      Specifies how the description balloon for a kml Feature is drawn.
   */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlBalloonStyle extends kmlObject {
    private Color   bgColor   = Color.white;
    private Color   textColor = Color.black;
    private String  text;

    /** Must be 'default' or 'hide' */
    private String displayMode  = "default";

    /** kmlBalloonStyle Default Constructor */
    public kmlBalloonStyle () {
      text = new String();
    }

    /** get background color
     *  @return background color
     */
    public Color getBgColor() { return bgColor;  }
    /** set background color
     *  @param bgColor background color
     */
    public void setBgColor(Color bgColor) { this.bgColor = bgColor; }

    /** get text color
     *  @return text color
     */
    public Color getTextColor() { return textColor; }
    /** set text color
     *  @param textColor text color
     */
    public void setTextColor(Color textColor) { this.textColor = textColor;  }

    /** get text
     *  @return text
     */
    public String getText() { return text;  }
    /** set text
     *  @param text text
     */
    public void setText(String text) { this.text = text; }

    /** get display mode
     *  @return display mode
     */
    public String getDisplayMode() { return displayMode; }

    /** set display mode
     *  @param displayMode display mode
     */
    public void setDisplayMode(String displayMode) {
      if (displayMode.equalsIgnoreCase("default")) {
        this.displayMode = "default";
      } else if (displayMode.equalsIgnoreCase("hide")) {
        this.displayMode = "hide";
      } else {
        warning("Invalid display mode: " +displayMode+ ". Mode must be 'hide' or 'default'");
      }
    }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Camera */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlCamera extends kmlAbstractView {
    double  lon      = 0;      // 0 to (+/-) 180 Degrees
    double  lat      = 0;      // 0 to (+/-) 90  Degrees
    double  altitude = 0;      //
    double  heading  = 0;      // 0 to 360 degrees
    double  tilt     = 0;      // 0 to 180 degrees
    double  roll     = 0;      // 0 to (+/-) 180 degrees

    /** Must be either 'clampToGround' (default), 'relativeToGround', or 'absolute'.  */
    String altitudeMode    = "clampToGround";

    /** kmlCamera Default Constructor */
    public kmlCamera () {}

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Color Style */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlColorStyle extends kmlObject {
    Color   color = Color.white;

    /** Must be 'normal' (default) or 'random' */
    String colorMode            = "normal";

    /** default constructor */
    public kmlColorStyle () {}

    /** get color
     *  @return KML color
     */
    public Color getColor() { return color; }
    /** set color
     *  @param color KML color
     */
    public void setColor(Color color) { this.color = color; }

    /** get color mode
     *  @return KML color mode
     */
    public String getColorMode() { return colorMode; }

    /** set color mode
     *  @param colorMode KML color mode
     */
    public void setColorMode(String colorMode) {
      if (colorMode.equalsIgnoreCase("normal")) {
        this.colorMode = "normal";
      } else if (colorMode.equalsIgnoreCase("random")) {
        this.colorMode = "random";
      } else {
        warning("Invalid color mode: " +colorMode+ ". Mode must be 'normal' or 'random'.");
      }
    }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Coordinates
      Creating this class enables us to get around the KML ordering of
       comma-separated lat,lon,alt and white-space-separated coordinate sets
   */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlCoordinates extends kmlObject {
    double lat;              // 0 to (+/-) 90 degrees
    double lon;              // 0 to (+/-) 180 degrees
    double alt       = 0;    // meters

    /** kmlCoordinates Default Constructor */
    public kmlCoordinates () {}

    /**  Convert the String coords to the doubles
         @param coords String form of coordinates
         @return true if successfully set
         @since NeXtMidas 3.3.2 this will return false if the given coord set is invalid
     */
    public boolean setCoordinates (String coords) {
      int i = coords.indexOf(',');
      int j = coords.indexOf(',',i+1);

      if (j>0) {
        lon = Double.parseDouble(coords.substring(0,i));
        lat = Double.parseDouble(coords.substring(i+1,j));
        alt = Double.parseDouble(coords.substring(j+1));
      } else if (i>0) {
        lon = Double.parseDouble(coords.substring(0,i));
        lat = Double.parseDouble(coords.substring(i+1));
        alt = 0;
      } else {
        warning("Invalid coordinate set: " + coords);
        return false;
      }

      if (lat>90 || lat<-90 || lon>180 || lon<-180) {
        lat=90; lon=180;
        warning("Invalid coordinates set ["+coords+"]. Setting coordinates to lon="+lon+", lat="+lat);
        return false; // do not update with bogus values
      }

      // update file X and Y values ranges
      updateXRange(lon);
      updateYRange(lat);

      return true;
    }

    /** get latitude
     *  @return latitude
     */
    public double getLat() { return lat; }

    /** set latitude
     *  @param lat latitude
     */
    public void setLat (double lat) { this.lat = lat; }

    /** get longitude
     *  @return longitude
     */
    public double getLon() { return lon;  }

    /** set longitude
     *  @param lon longitude
     */
    public void setLon (double lon) { this.lon = lon; }

    /** get altitude
     *  @return altitude
     */
    public double getAltitude () { return alt;  }

    /** set altitude
     *  @param altitude altitude
     */
    public void setAltitude (double altitude) { alt = altitude;  }

    public String toString () {
      String res = "";
      res += "lat="+lat;
      res+= ",lon="+lon +"\n";
      return res;
    }
  }

  ///////////////////////////////////////////////////////////////////
  /** KML Container. Abstract Only */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlContainer extends kmlFeature {
    Vector<kmlFeature>         features;
    Vector<kmlNetworkLink>     networkLinks;
    Vector<kmlFolder>          folders;
    ArrayList<kmlContainer>    subContainers;

    /** kmlContainer Default Constructor */
    public kmlContainer () {
      features      = new Vector<>();
      networkLinks  = new Vector<>();
      folders       = new Vector<>();
      subContainers = new ArrayList<>();
    }

    /** add feature
     *  @param feature KML feature
     */
    public void addFeature (kmlFeature feature) {
      features.add(feature);
    }

    /** @deprecated As of NeXtMidas 2.9.0, use {@link #getNetworkLinks()}
     *  @return first Network link */
    @Deprecated public kmlNetworkLink getNetworkLink () {  return networkLinks.get(0);}

    /** @deprecated As of NeXtMidas 2.9.0, use addNetworkLink(kmlNetworkLink)
     *  @param networkLink network link */
    @Deprecated public void setNetworkLink (kmlNetworkLink networkLink) { addNetworkLink(networkLink);}

    /** get network links
     *  @return KML network links
     */
    public Vector<kmlNetworkLink> getNetworkLinks () {
      return networkLinks;
    }

    /** add network link
     *  @param link KML network link
     */
    public void addNetworkLink (kmlNetworkLink link) {
      networkLinks.add(link);
    }

    /** set folder
     *  @param folder KML folder
     */
    public void addFolder (kmlFolder folder) {
      folders.add(folder);
    }

    /** get folders
     *  @return KML folders
     */
    public Vector<kmlFolder> getFolders () {
      return folders;
    }
    /** add sub-container
     *  @param cont KML sub-container
     */
    public void addSubContainer (kmlContainer cont) { subContainers.add(cont); }
    /** get sub-containers
     *  @return KML sub-containers
     */
    public ArrayList<kmlContainer> getSubContainers () { return subContainers; }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Document */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlDocument extends kmlContainer {

    Vector<kmlSchema>   schemas;

    /** kmlDocument Default Constructor */
    public kmlDocument () {
      schemas       = new Vector<>();
    }

    /** add schema
     *  @param schema KML schema
     */
    public void addSchema (kmlSchema schema) {
      schemas.add(schema);
    }
    /** get schemas
     *  @return KML schemas
     */
    public Vector<kmlSchema> getSchemas () {
      return schemas;
    }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Extended Data */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlExtendedData extends kmlObject {
    String dataName;
    String dataDisplayName;
    String dataValue;
    String schemaDataUrl;
    String simpleDataName;

    /** default constructor */
    public kmlExtendedData () {}

    /** get data name
     *  @return data name
     */
    public String getDataName()                { return dataName; }
    /** set data name
     *  @param dataName data name
     */
    public void   setDataName(String dataName) { this.dataName = dataName; }

    /** get data display name
     *  @return data display name
     */
    public String getDataDisplayName()         { return dataDisplayName; }
    /** set data display name
     *  @param dataDisplayName data display name
     */
    public void   setDataDisplayName(String dataDisplayName) { this.dataDisplayName = dataDisplayName; }

    /** get data value
     *  @return data value
     */
    public String getDataValue()               { return dataValue; }
    /** set data value
     *  @param dataValue data value
     */
    public void   setDataValue(String dataValue) { this.dataValue = dataValue; }

    /** get schema data url
     *  @return schema data url
     */
    public String getSchemaDataUrl()           { return schemaDataUrl; }
    /** set schema data url
     *  @param schemaDataUrl schema data url
     */
    public void   setSchemaDataUrl(String schemaDataUrl) { this.schemaDataUrl = schemaDataUrl; }

    /** get simple data name
     *  @return simple data name
     */
    public String getSimpleDataName()          { return simpleDataName; }
    /** set simple data name
     *  @param simpleDataName simple data name
     */
    public void   setSimpleDataName(String simpleDataName) { this.simpleDataName = simpleDataName; }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Folder */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlFolder extends kmlContainer {

    /** kmlFolder Default Constructor */
    public kmlFolder () {

    }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Ground Overlay */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlGroundOverlay extends kmlOverlay {

    double           altitude             = 0;

    /** Must be either 'clampToGround' (default), 'relativeToGround', or 'absolute'.  */
    String altitudeMode    = "clampToGround";

    /* The Lat Lon Box specifies the boundaries of a bounding box for the overlay */
    double           north;             // Lat Lon Box: 0 to +/- 90 degrees
    double           south;             // Lat Lon Box: 0 to +/- 90 degrees
    double           east;              // Lat Lon Box: 0 to +/- 180 degrees
    double           west;              // Lat Lon Box: 0 to +/- 180 degrees
    double           rotation = 0;      // Lat Lon Box: 0 (North) to +/- 180 degrees

    /** kmlGroundOverlay Default Constructor */
    public kmlGroundOverlay() {

    }

    /** get altitude
     *  @return altitude
     */
    public double getAltitude() {  return altitude; }
    /** set altitude
     *  @param altitude altitude
     */
    public void setAltitude(double altitude) { this.altitude = altitude; }

    /** get altitude mode
     *  @return altitude mode
     */
    public String getAltitudeMode() { return altitudeMode; }

    /** set altitude mode
     *  @param altitudeMode altitude mode clamptoground, relativetoground, or absolute
     */
    public void setAltitudeMode(String altitudeMode) {
      if (altitudeMode.equalsIgnoreCase("clamptoground")) {
        this.altitudeMode = "clampToGround";
      } else if (altitudeMode.equalsIgnoreCase("relativetoground")) {
        this.altitudeMode = "relativeToGround";
      } else if (altitudeMode.equalsIgnoreCase("absolute")) {
        this.altitudeMode = "absolute";
      } else {
        warning("Invalid altitude mode: " +altitudeMode+ ". Mode must be 'clampToGround', 'relativeToGround', or 'absolute'");
      }
    }

    /** get north value of y range
     *  @return north value (as double)
     */
    public double getNorth () { return north; }
    /** set north value of y range
     *  @param north double value of north
     */
    public void setNorth (double north) {
      this.north = north;
      updateYRange(north);  // update layer Y value range
    }

    /** get south value of y range
     *  @return south value (as double)
     */
    public double getSouth () { return south;}
    /** set south value of y range
     *  @param south double value of south
     */
    public void setSouth (double south) {
      this.south = south;
      updateYRange(south);  // update layer Y value range
    }

    /** get east value of x range
     *  @return east value (as double)
     */
    public double getEast () {  return east; }
    /** set east value of x range
     *  @param east double value of east
     */
    public void setEast (double east) {
      this.east = east;
      updateXRange(east);  // update layer X value range
    }

    /** get west value of x range
     *  @return west value (as double)
     */
    public double getWest () { return west; }
    /** set west value of x range
     *  @param west double value of west
     */
    public void setWest (double west) {
      this.west = west;
      updateXRange(west);  // update layer X value range
    }

    /** get rotation
     *  @return rotation
     */
    public double getRotation () { return rotation; }
    /** set rotation
     *  @param rotation rotation
     */
    public void setRotation (double rotation) { this.rotation = rotation; }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Icon */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlIcon extends kmlObject {
    private String     href        = "None";       // default to none/blank

    /** Must be 'onChange', 'onInterval', or 'onExpire' */
    String     refreshMode       = "onChange";

    float      refreshInterval   = 4;

    /** Must be  'never', 'onStop', 'onRequest', or 'onRegion' */
    String     viewRefreshMode   = "never";

    float      viewRefreshTime   = 4;
    float      viewBoundScale    = 1;
    String     viewFormat;
    String     httpQuery;

    /** default constructor */
    public kmlIcon ( ) {}

    /** get href
     *  @return href - page URL as String
     */
    public String getHref () {  return href; }
    /** set href
     *  @param url href page URL as String
     */
    public void setHref (String url) { this.href = url; }

    /** get refresh mode
     *  @return refresh mode: onChange, onInterval, or onExpire
     */
    public String getRefreshMode() { return refreshMode; }

    /** set refresh mode
     *  @param mode refresh mode: onChange, onInterval, or onExpire
     */
    public void setRefreshMode (String mode) {
      if (mode.equalsIgnoreCase("onchange")) {
        this.refreshMode = "onChange";
      } else if (mode.equalsIgnoreCase("oninterval")) {
        this.refreshMode = "onInterval";
      } else if (mode.equalsIgnoreCase("onexpire")) {
        this.refreshMode = "onExpire";
      } else {
        warning("Invalid Refresh Mode: " +mode+ ". Mode must be 'onChange', 'onInterval', or 'onExpire'.");
      }
    }

    /** get refresh interval
     *  @return refresh interval in seconds
     */
    public float getRefreshInterval () { return refreshInterval; }
    /** set refresh interval
     *  @param refreshInterval refresh interval in seconds
     */
    public void setRefreshInterval (float refreshInterval) { this.refreshInterval = refreshInterval; }

    /** get view refresh mode
     *  @return view refresh mode
     */
    public String getViewRefreshMode () { return viewRefreshMode;  }

    /** set view refresh mode
     * @param mode view refresh mode: never, onstop, onrequest, or onregion
     */
    public void setViewRefreshMode (String mode) {
      if (mode.equalsIgnoreCase("never")) {
        this.viewRefreshMode = "never";
      }  else if (mode.equalsIgnoreCase("onstop")) {
        this.viewRefreshMode = "onStop";
      } else if (mode.equalsIgnoreCase("onrequest")) {
        this.viewRefreshMode = "onRequest";
      } else if (mode.equalsIgnoreCase("onregion")) {
        this.viewRefreshMode = "onRegion";
      } else {
        warning("Invalid View Refresh Mode: " +mode+ ". Mode must be 'never', 'onStop', 'onRequest', or 'onRegion'.");
      }
    }

    /** get view fresh time
     *  @return view refresh time
     */
    public float getViewRefreshTime () { return viewRefreshTime; }
    /** set view refresh time
     *  @param viewRefreshTime view refresh time
     */
    public void setViewRefreshTime (float viewRefreshTime) { this.viewRefreshTime = viewRefreshTime; }

    /** get view bound scale
     *  @return view bound scale
     */
    public float getViewBoundScale () { return viewBoundScale; }
    /** set view bound scale
     *  @param viewBoundScale view bound scale
     */
    public void setViewBoundScale (float viewBoundScale) { this.viewBoundScale = viewBoundScale; }

    /** get view format
     *  @return view format
     */
    public String getViewFormat () { return viewFormat; }
    /** set view format
     *  @param viewFormat view format
     */
    public void setViewFormat (String viewFormat) { this.viewFormat = viewFormat; }

    /** get HTTP query string
     *  @return HTTP query string
     */
    public String getHttpQuery () { return httpQuery;}
    /** set HTTP query string
     *  @param httpQuery HTTP query string
     */
    public void setHttpQuery (String httpQuery) { this.httpQuery = httpQuery; }

    /** Parse the Icon's viewFormat, if it exists, replacing the parameters given.<br><br>
        <b>Note: This method may change during the beta series to incorporate more view format elements</b>
        @param north North side of BBOX
        @param south South side of BBOX
        @param east  East side of BBOX
        @param west  West side of BBOX
        @param width Width of panel
        @param height Height of panel
        @return A String (in http format) of the viewFormat
        @since NeXtMidas 2.9.0
     */
    public String parseViewFormat (double north, double south, double east, double west, int width, int height) {
      String raw = getViewFormat();
      if (raw==null) return null;  // no view format
      StringBuffer result = new StringBuffer();
      String[] elems = StringUtil.split(raw, "&");  // split apart the viewFormat
      for (String elem : elems) {
        int indexEQ = elem.indexOf('=');    // make key/value pairs
        if (indexEQ>0) {                                     // have equal sign (get value after it)
          String key = elem.substring(0,indexEQ).trim();     // trim
          String value = elem.substring(indexEQ + 1).trim(); // +1 to skip equal sign

          if (key.equalsIgnoreCase("BBOX")) {   // handle BBOX=
            String[] bbelems = StringUtil.split(value, ",");
            for (int i=0; i<bbelems.length; i++) {
              String bbelem = bbelems[i];
              if (bbelem.startsWith("[")) {
                String dir = bbelem.substring(5,9);
                if      (dir.equalsIgnoreCase("west")) bbelems[i] = ""+west;
                else if (dir.equalsIgnoreCase("sout")) bbelems[i] = ""+south;
                else if (dir.equalsIgnoreCase("east")) bbelems[i] = ""+east;
                else if (dir.equalsIgnoreCase("nort")) bbelems[i] = ""+north;
              }
            }
            result.append("BBOX="+StringUtil.join(bbelems, "%2C"));  // %2C is to comma-serarate the value
          } else if (key.equalsIgnoreCase("width")) {   // handle width=
            if (value.equalsIgnoreCase("[horizPixels]")) value = ""+width;
            result.append("&width="+value);
          } else if (key.equalsIgnoreCase("height")) {  // handle height=
            if (value.equalsIgnoreCase("[vertPixels]")) value = ""+height;
            result.append("&height="+value);
          }
        }
      } // end FOR loop of elements
      return result.toString();
    }
  }

  //////////////////////////////////////////////////////////////////
  /** KML Icon Style <br>
      For NeXtMidas purposes, we treat this as a Symbol
   */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlIconStyle extends kmlColorStyle {
    float     scale     = 1;
    float     heading   = 0;
    kmlIcon   icon;
    double    hotSpotX  = .5;
    double    hotSpotY  = .5;

    /** Must be 'fraction', 'pixels', 'insetPixels' */
    String    hotSpotXUnits = "fraction";
    /** Must be 'fraction', 'pixels', 'insetPixels' */
    String    hotSpotYUnits = "fraction";

    /** kmlIconStyle Default Constructor */
    public kmlIconStyle () {
      icon = new kmlIcon();
    }

    /** get scale
     *  @return scale
     */
    public float getScale () {  return scale;  }
    /** set scale
     *  @param scale KML scale
     */
    public void setScale (float scale) {  this.scale = scale;  }

    /** get KML heading
     *  @return KML heading
     */
    public float getHeading () { return heading; }
    /** set KML heading
     *  @param heading KML heading
     */
    public void setHeading (float heading) { this.heading = heading; }

    /** get icon
     *  @return KML icon
     */
    public kmlIcon getIcon () { return icon; }
    /** set KML icon
     *  @param icon KML icon
     */
    public void setIcon (kmlIcon icon) { this.icon = icon; }

    /** get hot spot x
     *  @return hot spot x
     */
    public double getHotSpotX () {  return hotSpotX;  }
    /** set hot spot x
     *  @param hotSpotX hot spot x
     */
    public void setHotSpotX (double hotSpotX) { this.hotSpotX = hotSpotX; }

    /** get hot spot y
     *  @return hot spot y
     */
    public double getHotSpotY () { return hotSpotY; }
    /** set hot spot y
     *  @param hotSpotY hot spot y
     */
    public void setHotSpotY (double hotSpotY) { this.hotSpotY = hotSpotY; }

    /** get hot spot x units
     *  @return hot spot x units
     */
    public String getHotSpotXUnits () { return hotSpotXUnits; }

    /** set hot spot x units
     *  @param hotSpotXUnits hot spot y units
     */
    public void setHotSpotXUnits (String hotSpotXUnits) {
      this.hotSpotXUnits = validateUnits(hotSpotXUnits);
    }

    /** get hot spot y units
     *  @return hot spot y units
     */
    public String getHotSpotYUnits () {  return hotSpotYUnits; }

    /** set hot spot y units
     *  @param hotSpotYUnits hot spot y units
     */
    public void setHotSpotYUnits (String hotSpotYUnits) {
      this.hotSpotYUnits = validateUnits(hotSpotYUnits);
    }

    private String validateUnits (String units) {
      String results = new String();;

      if (units.equalsIgnoreCase("fraction")) {
        results = "fraction";
      } else if (units.equalsIgnoreCase("pixels")) {
        results = "pixels";
      } else if (units.equalsIgnoreCase("insetpixels")) {
        results = "insetPixels";
      } else {
        warning("Invalid units: " +units+ ". Setting Units to default (fraction).");
        results = "fraction";
      }
      return results;
    }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Label Style */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlLabelStyle extends kmlColorStyle {
    float scale  = 1 ;     // scale factor applied to the label

    /** default constructor */
    public kmlLabelStyle () {}

    /** get scale
     *  @return scale
     */
    public float getScale() { return scale;  }

    /** set scale
     *  @param scale scale
     */
    public void setScale(float scale) { this.scale = scale; }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Linear Ring */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlLinearRing extends kmlGeometry {

    /** Specifies whether to connect to the ground.
        To extrude, altitudeMode shall be either 'relativeToGround',
         or 'absolute', and the altitude should be > 0.
     */
    boolean   extrude = false;

    /** Specifies whether to drape over the terrain.
        To enable, altitudeMode shall be 'clampToGround'
     */
    boolean   tessellate = false;

    /** Must be either 'clampToGround' (default), 'relativeToGround', or 'absolute'.  */
    String altitudeMode    = "clampToGround";

    Vector<kmlCoordinates> coordsVector;
    String                 coordsString;

    /** default constructor */
    public kmlLinearRing () {
      coordsVector = new Vector<>();
      coordsString = new String();
    }

    /** constructor that sets parent feature
     *  @param feat parent KML Feature
     */
    public kmlLinearRing (kmlFeature feat) {
      this();
      setParentFeature(feat);
    }

    /** is extruding (connecting to ground) enabled
     *  @return true if enabled
     */
    public boolean isExtrude() { return extrude; }
    /** Specifies whether to connect to the ground.
     *  To extrude, altitudeMode shall be either 'relativeToGround',
     *  or 'absolute', and the altitude should be greater than 0.
     * @param extrude true if should extrude
     */
    public void setExtrude(boolean extrude) { this.extrude = extrude;  }

    /** is draping over the terrain enabled
     *  @return true if should drape over the terrain
     */
    public boolean isTessellate() { return tessellate;   }
    /** Specifies whether to drape over the terrain. To enable, altitudeMode shall be 'clampToGround'
     *  @param tessellate true if should drape over the terrain
     */
    public void setTessellate(boolean tessellate) { this.tessellate = tessellate; }

    /** get altitude mode
     *  @return altitude mode
     */
    public String getAltitudeMode() {  return altitudeMode;  }

    /** set altitude mode
     *  @param altitudeMode altitude mode clamptoground, relativetoground, or absolute
     */
    public void setAltitudeMode(String altitudeMode) {
      if (altitudeMode.equalsIgnoreCase("clamptoground")) {
        this.altitudeMode = "clampToGround";
      } else if (altitudeMode.equalsIgnoreCase("relativetoground")) {
        this.altitudeMode = "relativeToGround";
      } else if (altitudeMode.equalsIgnoreCase("absolute")) {
        this.altitudeMode = "absolute";
      } else {
        warning("Invalid altitude mode: " +altitudeMode+ ". Mode must be 'clampToGround', 'relativeToGround', or 'absolute'");
      }
    }

    /** set coordinates string
     *  @param coords coordinates string
     */
    public void setCoordsString (String coords) {  coordsString = coords; }

    /** get coordinates string
     *  @return coordinates string
     */
    public String getCoordsString () { return coordsString;  }

    /** Converts all the sets of coordinates for the LineString into a single
         double array. This is done for performance reasons when drawing the
         lines.
        @return All the coordinates as a single double array.
     */
    public double[] getCoordsAsDouble () {
      double[] coords=null;

      if (coordsVector.isEmpty()) {                 // make sure it is not empty
        warning("Coordinates not set");
        return null;
      } else {
        int size = coordsVector.size();    // get the number of coordinate sets
        coords = new double[size*3];       // x3 (Lon/Lat/Alt in each set)
        int coordsIndex = 0;

        for (int xx=0; xx< size; xx++) {
          coords[coordsIndex] = coordsVector.get(xx).getLon();
          coordsIndex++;
          coords[coordsIndex] = coordsVector.get(xx).getLat();
          coordsIndex++;
          coords[coordsIndex] = coordsVector.get(xx).getAltitude();
          coordsIndex++;
        }
        return coords;
      }
    }

    /** Set the coordinates Vector from String form.
        This method converts the coordinates from their raw from into
         the Vector (lon,lat,alt) used by the class, stripping away carriage
         returns and whitespace.
        @param coordinates the coordinates vector in String form
     */
    public void setCoordsVector (String coordinates) {
      String[] tmp = EOL.split(coordinates);
      for (int xx=0; xx< tmp.length; xx++) {
        //tmp[xx] = tmp[xx].trim();
        if (!tmp[xx].isEmpty()) {
          kmlCoordinates coords = new kmlCoordinates();
          boolean valid = coords.setCoordinates(tmp[xx]);
          if (valid) coordsVector.add(coords);  // do not add bigus coords to the vector
        }
      }
      //System.out.println("Number of coord sets=" + counter);
    }
  }

  ///////////////////////////////////////////////////////////////////
  /** KML Line String */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlLineString extends kmlGeometry {

    /** Specifies whether to connect to the ground.
        To extrude, altitudeMode shall be either 'relativeToGround',
         or 'absolute', and the altitude should be > 0.
     */
    boolean   extrude    = false;

    /** Specifies whether to drape over the terrain.
        To enable, altitudeMode shall be 'clampToGround'
     */
    boolean   tessellate = false;

    /** Must be either 'clampToGround' (default), 'relativeToGround', or 'absolute'.  */
    String altitudeMode    = "clampToGround";

    String                 coordsString = new String();
    Vector<kmlCoordinates> coordsVector;   // Holds as many coordinate sets as needed

    /** default constructor */
    public kmlLineString () {
      coordsVector = new Vector<>();
    }

    /** constructor that sets parent feature
     *  @param feat parent KML Feature
     */
    public kmlLineString (kmlFeature feat) {
      this();
      setParentFeature(feat);
    }

    /** is extruding (connecting to ground) enabled
     *  @return true if enabled
     */
    public boolean isExtrude() { return extrude; }
    /** convenience method for isExtrude()
      * @return true if enabled
      */
    public boolean getExtrude() { return isExtrude(); }
    /** Specifies whether to connect to the ground.
     *  To extrude, altitudeMode shall be either 'relativeToGround',
     *  or 'absolute', and the altitude should be greater than 0.
     * @param extrude true if should extrude
     */
    public void setExtrude(boolean extrude) { this.extrude = extrude; }

    /** is draping over the terrain enabled
     *  @return true if should drape over the terrain
     */
    public boolean isTessellate() { return tessellate; }
    /** get tessellate
     *  @return true if should drape over the terrain
     */
    public boolean getTessellate() { return isTessellate(); }
    /** Specifies whether to drape over the terrain. To enable, altitudeMode shall be 'clampToGround'
     *  @param tessellate true if should drape over the terrain
     */
    public void setTessellate(boolean tessellate) { this.tessellate = tessellate;  }

    /** get altitude mode
     *  @return altitude mode
     */
    public String getAltitudeMode () { return altitudeMode; }

    /** set altitude mode
     *  @param altitudeMode altitude mode clamptoground, relativetoground, or absolute
     */
    public void setAltitudeMode(String altitudeMode) {
      if (altitudeMode.equalsIgnoreCase("clamptoground")) {
        this.altitudeMode = "clampToGround";
      } else if (altitudeMode.equalsIgnoreCase("relativetoground")) {
        this.altitudeMode = "relativeToGround";
      } else if (altitudeMode.equalsIgnoreCase("absolute")) {
        this.altitudeMode = "absolute";
      } else {
        warning("Invalid altitude mode: " +altitudeMode+ ". Mode must be 'clampToGround', 'relativeToGround', or 'absolute'");
      }
    }

    /** set coordinates string
     *  @param coords coordinates string
     */
    public void setCoordsString (String coords) { coordsString = coords;  }

    /** get coordinates string
     *  @return coordinates string
     */
    public String getCoordsString () {  return coordsString;  }

    /** Converts all the sets of coordinates for the LineString into a single
         double array. This is done for performance reasons when drawing the
         lines.
        @return All the coordinates as a single double array.
     */
    public synchronized double[] getCoordsAsDouble () {
      double[] coords=null;

      if (coordsVector.isEmpty()) {                 // make sure it is not empty
        warning("Coordinates not set");
        return null;
      } else {
        int size = coordsVector.size();    // get the number of coordinate sets
        coords = new double[size*3];       // x3 (Lon/Lat/Alt in each set)
        int coordsIndex = 0;

        for (int xx=0; xx< size; xx++) {
          coords[coordsIndex++] = coordsVector.get(xx).getLon();
          coords[coordsIndex++] = coordsVector.get(xx).getLat();
          coords[coordsIndex++] = coordsVector.get(xx).getAltitude();
        }

        return coords;
      }
    }

    /** Set the coordinates Vector from String form.
        This method converts the coordinates from their raw form into
         the Vector (lon,lat,alt) used by the class, stripping away carriage
         returns and whitespace.
        @param coordinates the coordinates Vector in String form
     */
    public synchronized void setCoordsVector (String coordinates) {

      String[] tmp = EOL.split(coordinates);
      for (int xx=0; xx< tmp.length; xx++) {
       // tmp[xx] = tmp[xx].trim();
        if (!tmp[xx].isEmpty()) {
          kmlCoordinates coords = new kmlCoordinates();
          boolean valid = coords.setCoordinates(tmp[xx]);
          if (valid) coordsVector.add(coords);  // do not add bogus coords to the vector
        }
      }
      //System.out.println("Number of coord sets=" + tmp.length);
    }
  }

  ///////////////////////////////////////////////////////////////////
  /** KML Line Style */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlLineStyle extends kmlColorStyle {
    float width = 1;     // width in pixels

    /** default constructor */
    public kmlLineStyle () {}

    /** set width
     *  @param w width of line
     */
    public void setWidth (float w) { width = w; }
    /** get width
     *  @return width of line
     */
    public float getWidth () {return width; }
  }

  ///////////////////////////////////////////////////////////////////
  /** KML Link <br>
      Note: refreshInterval deals with time (i.e. poll rate), while
       viewRefreshInterval deals with the "camera" view.
   */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlLink extends kmlObject {
    String href;                           // URL to a location

    /** Time-based refresh mode. MUST be 'onChange' (default), 'onInterval', or 'onExpire' */
    String refreshMode       = "onChange";
    float  refreshInterval   = 4;          // Refresh the file every N seconds

    /** Must be 'never' (default), 'onStop', 'onRequest', 'onRegion' */
    String viewRefreshMode   = "never";
    float  viewRefreshTime   = 4;          // Seconds to wait after camera stops to refresh
    float  viewBoundScale    = 1;          // Scale of the bounding box.
    //  < 1 = smaller than current view
    //  > 1 = larger then current view
    String viewFormat;
    String httpQuery;

    /** default constructor */
    public kmlLink () {
      href       = new String();
      viewFormat = new String();
      httpQuery  = new String();
    }

    /** get href
     *  @return href - page URL as String
     */
    public String getHref() { return href;  }
    /** set href
     *  @param href page URL as String
     */
    public void setHref(String href) {  this.href = href; }

    /** get refresh mode
     *  @return refresh mode: onChange, onInterval, or onExpire
     */
    public String getRefreshMode() { return refreshMode; }

    /** set refresh mode
     *  @param mode refresh mode: onChange, onInterval, or onExpire
     */
    public void setRefreshMode (String mode) {
      if (mode.equalsIgnoreCase("onchange")) {
        this.refreshMode = "onChange";
      } else if (mode.equalsIgnoreCase("oninterval")) {
        this.refreshMode = "onInterval";
      } else if (mode.equalsIgnoreCase("onexpire")) {
        this.refreshMode = "onExpire";
      } else {
        warning("Invalid Refresh Mode: " +mode+ ". Mode must be 'onChange', 'onInterval', or 'onExpire'.");
      }
    }

    /** get refresh interval
     *  @return refresh interval in seconds
     */
    public float getRefreshInterval () { return refreshInterval; }
    /** set refresh interval
     *  @param refreshInterval refresh interval in seconds
     */
    public void setRefreshInterval (float refreshInterval) { this.refreshInterval = refreshInterval; }

    /** get view refresh mode
     *  @return view refresh mode
     */
    public String getViewRefreshMode () { return viewRefreshMode;  }

    /** set view refresh mode
     * @param mode view refresh mode: never, onstop, onrequest, or onregion
     */
    public void setViewRefreshMode (String mode) {
      if (mode.equalsIgnoreCase("never")) {
        this.viewRefreshMode = "never";
      }  else if (mode.equalsIgnoreCase("onstop")) {
        this.viewRefreshMode = "onStop";
      } else if (mode.equalsIgnoreCase("onrequest")) {
        this.viewRefreshMode = "onRequest";
      } else if (mode.equalsIgnoreCase("onregion")) {
        this.viewRefreshMode = "onRegion";
      } else {
        warning("Invalid View Refresh Mode: " +mode+ ". Mode must be 'never', 'onStop', 'onRequest', or 'onRegion'.");
      }
    }

    /** get view bound scale
     *  @return view bound scale
     */
    public float getViewRefreshTime () { return viewRefreshTime; }
    /** set view refresh time
     * @param viewRefreshTime view refresh time
     */
    public void setViewRefreshTime (float viewRefreshTime) { this.viewRefreshTime = viewRefreshTime; }

    /** get view bound scale
     *  @return view bound scale
     */
    public float getViewBoundScale () { return viewBoundScale; }
    /** set view bound scale
     *  @param viewBoundScale view bound scale
     */
    public void setViewBoundScale (float viewBoundScale) { this.viewBoundScale = viewBoundScale; }

    /** get view format
     *  @return view format
     */
    public String getViewFormat () { return viewFormat; }
    /** set view format
     *  @param viewFormat view format
     */
    public void setViewFormat (String viewFormat) { this.viewFormat = viewFormat; }

    /** get HTTP query string
     *  @return HTTP query string
     */
    public String getHttpQuery () { return httpQuery;}
    /** set HTTP query string
     *  @param httpQuery HTTP query string
     */
    public void setHttpQuery (String httpQuery) { this.httpQuery = httpQuery; }
  }

  ///////////////////////////////////////////////////////////////////
  /** KML List Style */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlListStyle extends kmlObject {
    Color  bgColor = Color.white;   // Default to white

    /** Must be  'check' (default), 'checkOffOnly', 'checkHideChildren', or
         'radioFolder' */
    String listItemType  = "check";

    Vector<kmlIcon> icons;

    /** default constructor */
    public kmlListStyle () {
      icons = new Vector<>();
    }

    /** add icon
     *  @param icon KML icon to be added to icons
     */
    public void addIcon (kmlIcon icon) {
      icons.add(icon);
    }

    /** get background color
     *  @return Color object specifying background color
     */
    public Color getBgColor() {  return bgColor; }
    /** set background color
     *  @param bgColor Color object specifying background color
     */
    public void setBgColor(Color bgColor) { this.bgColor = bgColor; }

    /** get list item type
     *  'check' (default), 'checkOffOnly', 'checkHideChildren', or 'radioFolder'
     *  @return item type
     */
    public String getListItemType() { return listItemType; }

    /** Must be  'check' (default), 'checkOffOnly', 'checkHideChildren', or 'radioFolder'
     *  @param listItemType list type item */
    public void setListItemType(String listItemType) {
      if (listItemType.equalsIgnoreCase("check")) {
        this.listItemType = "check";
      } else if (listItemType.equalsIgnoreCase("checkoffonly")) {
        this.listItemType = "checkOffOnly";
      } else if (listItemType.equalsIgnoreCase("checkHideChildren")) {
        this.listItemType = "checkHideChildren";
      } else if (listItemType.equalsIgnoreCase("radioFolder")) {
        this.listItemType = "radioFolder";
      } else {
        warning("Invalid List Item Type: " +listItemType+
        ". Type must be 'check' (default), 'checkOffOnly', 'checkHideChildren', or 'radioFolder'");
      }
    }

    /** get Icons
     *  @return Vector containing KML Icons
     */
    public Vector<kmlIcon> getIcons() { return icons; }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML LookAt */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlLookAt extends kmlAbstractView {
    double lon     = 0;       // 0 to +/- 180 degrees
    double lat     = 0;       // 0 to +/-  90 degrees
    double alt     = 0;       // meters
    double heading = 0;       // 0 to 360 degrees
    double tilt    = 0;       // 0 to 90 degrees
    double range;

    /** default constructor */
    public kmlLookAt () {}

    /** Must be 'clampToGround' (default), 'relativeToGround', or 'absolute' */
    String altitudeMode = "clampToGround";

    /** default constructor
    public kmlLookAt () {}

    /** get longitude
     *  @return longitude
     */
    public double getLon() {  return lon; }
    /** set longitude
     *  @param lon longitude
     */
    public void setLon(double lon) { this.lon = lon; }

    /** get latitude
     *  @return latitude
     */
    public double getLat() {  return lat;  }
    /** set latitude
     *  @param lat latitude
     */
    public void setLat(double lat) { this.lat = lat; }

    /** get altitude
     *  @return altitude
     */
    public double getAlt() { return alt; }
    /** set altitude
     *  @param alt altitude
     */
    public void setAlt(double alt) { this.alt = alt; }

    /** get heading
     *  @return heading
     */
    public double getHeading() { return heading;  }
    /** set heading
     *  @param heading heading
     */
    public void setHeading(double heading) { this.heading = heading; }

    /** get tilt
     *  @return tilt
     */
    public double getTilt() { return tilt; }
    /** set tilt
     *  @param tilt tilt
     */
    public void setTilt(double tilt) { this.tilt = tilt; }

    /** get range
     *  @return range as double
     */
    public double getRange() { return range; }
    /** set range
     *  @param range range as double
     */
    public void setRange(double range) { this.range = range; }

    /** get altitude mode
     *  @return altitude mode
     */
    public String getAltitudeMode() { return altitudeMode; }
    /** set altitude mode
     *  @param altitudeMode altitude mode clamptoground, relativetoground, or absolute
     */
    public void setAltitudeMode(String altitudeMode) { this.altitudeMode = altitudeMode; }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Model */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlModel extends kmlGeometry {

    /** Must be 'clampToGround' (default), 'relativeToGround', or 'absolute' */
    String altitudeMode = "clampToGround";

    kmlCoordinates coords;                      // location

    /* Orientation */
    double heading           = 0;               // 0 to 360 degrees
    double tilt              = 0;               // 0 to 360 degrees
    double roll              = 0;               // 0 to 360 degrees

    /* Scale */
    float scaleX            = 1;
    float scaleY            = 1;
    float scaleZ            = 1;

    kmlLink link;

    /* Resource Map Alias */
    /* TODO: NEEDS TO BE OWN CLASS TO ALLOW FOR MULTIPLE MAPS */
    String targetHref;
    String sourceHref;

    /** default constructor */
    public kmlModel () {
      link = new kmlLink();
    }

    /** constructor that sets parent feature
     *  @param feat parent KML Feature
     */
    public kmlModel (kmlFeature feat) {
      this();
      setParentFeature(feat);
    }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Multi Geometry */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlMultiGeometry extends kmlGeometry {

    Vector<kmlGeometry> geometrys;     // vector to hold the kmlGeometry objects

    /** default constructor */
    public kmlMultiGeometry () {
      geometrys = new Vector<>();    // instantiate the Vector
    }

    /** constructor that sets parent feature
     *  @param feat parent KML Feature
     */
    public kmlMultiGeometry (kmlFeature feat) {
      this();
      setParentFeature(feat);
    }

    /** Add a kmlGeometry object to the Vector
        @param geometry The kmlGeometry object
     */
    public void addGeometry (kmlGeometry geometry) {
      geometrys.add(geometry);
    }

  }

  ///////////////////////////////////////////////////////////////////
  /** KMl Network Link */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlNetworkLink extends kmlFeature {
    boolean refreshVisibility  = false;
    boolean flyToView          = false;
    kmlLink link;

    /** default constructor */
    public kmlNetworkLink () {
      link = new kmlLink();
    }

    /** set KML link
     *  @param link KML link
     */
    public void setLink (kmlLink link) {
      this.link = link;
    }
  }

  ///////////////////////////////////////////////////////////////////
  /** KMl Network Link Control */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlNetworkLinkControl {

    float minRefreshPeriod = 0;      // seconds between fetches of the file
    float maxSessionLength = -1;     // seconds to stay connected. '-1' = do not disconnect
    String cookie;
    String message;
    String linkName;
    String linkDescription;
    String linkSnippet;
    int linkSnippetMaxLines = 2;
    Date expires;

    /** Must be 'Change', 'Create', or 'Delete' */
    String update = new String();

    /** Will be a <code>kmlLookAt</code> or <code>kmlCamera</code>.  */
    kmlAbstractView abstractView;

    /** default constructor */
    public kmlNetworkLinkControl () {
      cookie           = new String();
      message          = new String();
      linkName         = new String();
      linkDescription  = new String();
      linkSnippet      = new String();

      abstractView     = new kmlAbstractView();
    }

    /** get Minimum Refresh Period
     *  @return minimum refresh period in seconds
     */
    public float getMinRefreshPeriod() {  return minRefreshPeriod; }
    /** set Minimum Refresh Period
     *  @param minRefreshPeriod minimum refresh period in seconds
     */
    public void setMinRefreshPeriod (float minRefreshPeriod) { this.minRefreshPeriod = minRefreshPeriod; }

    /** get Maximum Session Length
     *  @return maximum session length in seconds
     */
    public float getMaxSessionLength () { return maxSessionLength; }
    /** set Maximum Session length in seconds
     * @param maxSessionLength Maximum Session length in seconds
     */
    public void setMaxSessionLength (float maxSessionLength) {this.maxSessionLength = maxSessionLength;}

    /** get Cookie
     *  @return cookie
     */
    public String getCookie () { return cookie;}
    /** set Cookie
     *  @param cookie cookie
     */
    public void setCookie (String cookie) { this.cookie = cookie; }

    /** get Message
     *  @return message
     */
    public String getMessage () { return message; }
    /** set Message
     *  @param message message
     */
    public void setMessage (String message) { this.message = message; }

    /** get Link Name
     *  @return link name
     */
    public String getLinkName () { return linkName; }
    /** set Link Name
     *  @param linkName link name
     */
    public void setLinkName (String linkName) { this.linkName = linkName;  }

    /** get Link Description
     *  @return link description
     */
    public String getLinkDescription () {  return linkDescription;  }
    /** set Link Description
     *  @param linkDescription link description
     */
    public void setLinkDescription (String linkDescription) { this.linkDescription = linkDescription; }

    /** get Link Snippet
     *  @return Link Snippet
     */
    public String getLinkSnippet () {  return linkSnippet; }
    /** set Link Snippet
     *  @param linkSnippet link snippet String
     */
    public void setLinkSnippet (String linkSnippet) { this.linkSnippet = linkSnippet; }

    /** get Link Snippet Maximum number of Lines
     *  @return Maximum number of Lines
     */
    public int getLinkSnippetMaxLines () { return linkSnippetMaxLines; }
    /** set Link Snippet Maximum number of Lines
     *  @param linkSnippetMaxLines number of lines
     */
    public void setLinkSnippetMaxLines (int linkSnippetMaxLines) { this.linkSnippetMaxLines = linkSnippetMaxLines; }

    /** get expiration Date
     *  @return Date containing expiration date
     */
    public Date getExpires () { return expires; }
    /** set expiration date
     *  @param expires expiration Date
     */
    public void setExpires (Date expires) { this.expires = expires;  }

    /** get update state
     *  @return update state: Change,Create, or Delete
     */
    public String getUpdate () {  return update; }

    /** set update
     *  @param update Change,Create, or Delete
     */
    public void setUpdate (String update) {
      if (update.equalsIgnoreCase("change")){
        this.update = "Change";
      } else if (update.equalsIgnoreCase("create")) {
        this.update = "Create";
      } else if (update.equalsIgnoreCase("delete")) {
        this.update = "Delete";
      } else {
        warning("Invalid Update entry: " +update+ ". Update must be 'Change', 'Create', or 'Delete'");
      }
    }

    /** get Abstract View
     *  @return KML Abstract View
     */
    public kmlAbstractView getAbstractView () { return abstractView; }
    /** set Abstract View
     *  @param abstractView kmlAbstractView
     */
    public void setAbstractView (kmlAbstractView abstractView) { this.abstractView = abstractView; }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Overlay - Abstract Element */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlOverlay extends kmlFeature {

    private  Color    color;
    private  int      drawOrder;
    private  kmlIcon  icon;          // Image associated with the overlay

    /** default constructor */
    public kmlOverlay () {
      color     = Color.white;    // white by default
      drawOrder = 0;
      icon      = new kmlIcon();
    }

    /** get Color
     *  @return Color
     */
    public Color getColor() { return color;  }
    /** set color
     *  @param color Color
     */
    public void setColor(Color color) { this.color = color;  }

    /** get draw order
     *  @return draw order
     */
    public int getDrawOrder() {  return drawOrder;  }
    /** set draw order
     *  @param drawOrder draw order
     */
    public void setDrawOrder(int drawOrder) { this.drawOrder = drawOrder; }

    /** get icon
     *  @return KML icon
     */
    public kmlIcon getIcon() { return icon; }
    /** set KML icon
     *  @param icon KML icon
     */
    public void setIcon(kmlIcon icon) { this.icon = icon; }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Photo Overlay */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlPhotoOverlay extends kmlOverlay {

    /** Must be  'rectangle' (default), 'cylinder', or 'sphere' */
    String shape = "rectangle";

    /* View Volume (How much of the current scene is visible) */
    double  leftFov   = 0;          // Left Field of View. 0 to 180 degrees
    double  rightFov  = 0;          // Right Field of View. 0 to 180 degrees
    double  bottomFov = 0;          // Right Field of View. 0 to 90 degrees
    double  topFov    = 0;          // Top Field of View. 0 to 90 degrees
    double  near      = 0;          // Meters from the camera

    float  roll;                   // Adjusts the photo inside the field of view. 0 to 180 degrees

    /* Image Pyramid (Heirarchical set of images, increasing in resolution */
    int    tileSize   = 256;       // Size in pixels. Must be a square (power of 2)
    int    maxWidth;
    int    maxHeight;

    /** Must be 'lowerLeft' (default) or 'upperLeft' */
    String gridOrigin = "lowerleft"; //

    kmlPoint point;                //

    /** default constructor */
    public kmlPhotoOverlay() {
      point = new kmlPoint();
    }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Placemark */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlPlacemark extends kmlFeature {

    kmlGeometry geometry;

    /** default constructor */
    public kmlPlacemark () {
      geometry = new kmlGeometry();
    }

    /** get KML geometry
     *  @return KML geometry
     */
    public kmlGeometry getGeometry() { return geometry; }
    /** Set geometry to allow mouse popups
     *  @param geometry KML geometry
     *  @since NeXtMidas 3.9.1
     */
    public void setGeometry(kmlGeometry geometry) { this.geometry = geometry; geometry.setParentFeature(this);}

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Point */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlPoint extends kmlGeometry {

    /** Specifies whether to connect to the ground.
        To extrude, altitudeMode shall be either 'relativeToGround',
         or 'absolute', and the altitude should be > 0.
     */
    boolean   extrude    = false;

    /** Must be either 'clampToGround' (default), 'relativeToGround', or 'absolute'.  */
    String altitudeMode    = "clampToGround";

    kmlCoordinates coords;

    /** Default constructor */
    public kmlPoint () {
      coords = new kmlCoordinates();   // instantiate kmlCoordinates
    }

    /** Keep track of the feature so we can call things like the name and desc
     *  @param obj object to be set as parent feature (if it is a kmlFeature) */
    public kmlPoint (Object obj) {
      this();
      if (obj instanceof kmlFeature) {
        setParentFeature((kmlFeature)obj);
      } else if (obj instanceof kmlMultiGeometry) {
        // possible future use
      }
    }

    /** get extruding (connecting to ground) enabled
     *  @return true if enabled
     */
    public boolean getExtrude () { return isExtrude(); }
    /** is extruding (connecting to ground) enabled
     *  @return true if enabled
     */
    public boolean isExtrude () { return extrude;  }

    /** Specifies whether to connect to the ground.
     *  To extrude, altitudeMode shall be either 'relativeToGround',
     *  or 'absolute', and the altitude should be greater than 0.
     * @param extrude true if should extrude
     */
    public void setExtrude (boolean extrude) { this.extrude = extrude; }

    /** get altitude mode
     *  @return  altitude mode
     */
    public String getAltitudeMode () { return altitudeMode; }

    /** set altitude mode
     *  @param altitudeMode altitude mode clamptoground, relativetoground, or absolute
     */
    public void setAltitudeMode (String altitudeMode) {
      if (altitudeMode.equalsIgnoreCase("clamptoground")) {
        this.altitudeMode = "clampToGround";
      } else if (altitudeMode.equalsIgnoreCase("relativetoground")) {
        this.altitudeMode = "relativeToGround";
      } else if (altitudeMode.equalsIgnoreCase("absolute")) {
        this.altitudeMode = "absolute";
      } else {
        warning("Invalid altitude mode: " +altitudeMode+ ". Mode must be 'clampToGround', 'relativeToGround', or 'absolute'");
      }
    }

    /** get coordinates
     *  @return coordinates
     */
    public kmlCoordinates getCoords () { return coords; }

    /** Set the kmlCoordinates for the point based on an input String
        @param string The coordinates in String form, which is how they appear
                      to the parser.
     */
    public void setCoordinates (String string) {
      //System.out.println("Coords= " +string);    // Debug Statement
      coords.setCoordinates(string);
    }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Polygon */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlPolygon extends kmlGeometry {

    /** Specifies whether to connect to the ground.
        To extrude, altitudeMode shall be either 'relativeToGround',
         or 'absolute', and the altitude should be > 0.
     */
    boolean   extrude    = false;

    /** Specifies whether to drape over the terrain.
        To enable, altitudeMode shall be 'clampToGround'
     */
    boolean   tessellate = false;

    /** Must be either 'clampToGround' (default), 'relativeToGround', or 'absolute'.  */
    String altitudeMode    = "clampToGround";

    kmlLinearRing         outerBoundary;  // only 1
    boolean               outer;          // flag to show whether the current linear ring is the outer ring
    Vector<kmlLinearRing> innerBoundary;  // can be 0 or more

    /** default constructor */
    public kmlPolygon () {
      outerBoundary = new kmlLinearRing();
      innerBoundary = new Vector<>();
      // Added for mouse popups since 3.9.1
      getPolygons().add(this);
    }

    /** constructor setting parent feature
     *  @param feat parent kmlFeature
     */
    public kmlPolygon (kmlFeature feat) {
      this();
      setParentFeature(feat);
    }

    /** add inner boundary
     *  @param linearRing kmlLinearRing specifying inner boundary
     */
    public void addInnerBoundary (kmlLinearRing linearRing) {
      innerBoundary.add(linearRing);
    }

    /** set outer boundary
     *  @param outerBoundary kmlLinearRing specifying outer boundary
     */
    public void setOuterBoundary (kmlLinearRing outerBoundary) {
      this.outerBoundary = outerBoundary;
    }

    /** get outer boundary
     *  @return kmlLinearRing with outer boundary
     */
    public kmlLinearRing getOuterBoundary () {
      return outerBoundary;
    }

    /** is this an outer boundary
     *  @return true if is outer boundary
     */
    public boolean isOuter () {return outer;}
    /** get if this is outer boundary
     *  @return true if is outer boundary
     */
    public boolean getOuter () { return outer; }
    /** set if is outer boundary
     *  @param outer true if is outer boundary
     */
    public void setOuter (boolean outer) { this.outer = outer; }

    /** is extruding (connecting to ground) enabled
     *  @return true if enabled
     */
    public boolean isExtrude () { return extrude;  }
    /** get extruding (connecting to ground) enabled
     *  @return true if enabled
     */
    public boolean getExtrude () { return isExtrude(); }
    /** Specifies whether to connect to the ground.
     *  To extrude, altitudeMode shall be either 'relativeToGround',
     *  or 'absolute', and the altitude should be greater than 0.
     * @param extrude true if should extrude
     */
    public void setExtrude (boolean extrude) {  this.extrude = extrude;  }

    /** is draping over the terrain enabled
     *  @return true if should drape over the terrain
     */
    public boolean isTessellate () { return tessellate; }
    /** Specifies whether to drape over the terrain. To enable, altitudeMode shall be 'clampToGround'
     *  @param tessellate true if should drape over the terrain
     */
    public void setTessellate (boolean tessellate) { this.tessellate = tessellate; }

    /** get altitude mode
     *  @return  altitude mode
     */
    public String getAltitudeMode () { return altitudeMode; }

    /** set altitude mode
     *  @param altitudeMode altitude mode clamptoground, relativetoground, or absolute
     */
    public void setAltitudeMode(String altitudeMode) {
      if (altitudeMode.equalsIgnoreCase("clamptoground")) {
        this.altitudeMode = "clampToGround";
      } else if (altitudeMode.equalsIgnoreCase("relativetoground")) {
        this.altitudeMode = "relativeToGround";
      } else if (altitudeMode.equalsIgnoreCase("absolute")) {
        this.altitudeMode = "absolute";
      } else {
        warning("Invalid altitude mode: " +altitudeMode+ ". Mode must be 'clampToGround', 'relativeToGround', or 'absolute'");
      }
    }

    /** add coordinates
     *  @param coords coordiantes to add
     */
    public void addCoordinates(String coords) {
      kmlLinearRing linearRing = new kmlLinearRing();
      linearRing.setCoordsVector(coords);
      innerBoundary.add(linearRing);
    }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Poly Style */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlPolyStyle extends kmlColorStyle {
    boolean fill         = true;    // Whether to fill the polygon
    boolean outline      = true;    // Whether to outline the polygon

    /** default constructor */
    public kmlPolyStyle () {}

    /** is fill active
     *  @return true if fill is active
     */
    public boolean isFill () { return fill; }
    /** set fill
     *  @param fill true if fill is active
     */
    public void setFill (boolean fill) { this.fill = fill; }

    /** is outlining active
     *  @return true if outlining is active
     */
    public boolean isOutline () { return outline; }
    /** set if outlining is on
     *  @param outline true if outlining
     */
    public void setOutline (boolean outline) { this.outline = outline; }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Screen Overlay */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlScreenOverlay extends kmlOverlay {

    /* OverlayXY (Point on the overlay mapped to screen coordinates) */
    float  overlayX;
    float  overlayY;
    /** MUST be 'fraction' (default), 'pixels', or 'insetPixels' */
    String    overlayXUnits    = "fraction";
    /** MUST be 'fraction' (default), 'pixels', or 'insetPixels' */
    String    overlayYUnits    = "fraction";   //

    /* ScreenXY (Point relative to the screen that the overlay is mapped to) */
    float  screenX;
    float  screenY;
    /** MUST be 'fraction' (default), 'pixels', or 'insetPixels' */
    String    screenXUnits     = "fraction";   //
    /** MUST be 'fraction' (default), 'pixels', or 'insetPixels' */
    String    screenYUnits     = "fraction";   //

    /* RotationXY (Point relative to the screen about which the overlay is rotated) */
    float  rotationX;
    float  rotationY;
    /** MUST be 'fraction' (default), 'pixels', or 'insetPixels' */
    String    rotationXUnits   = "fraction";   //
    /** MUST be 'fraction' (default), 'pixels', or 'insetPixels' */
    String    rotationYUnits   = "fraction";   //

    /** Size (Size of the image for the overlay)
       '-1' = Use the native dimension
       '0'  = Maintain the aspect ratio
        n   = Sets the value to the dimension to n
     */
    float  sizeX;
    float  sizeY;
    /** MUST be 'fraction' (default), 'pixels', or 'insetPixels' */
    String    sizeXUnits       = "fraction";   //
    /** MUST be 'fraction' (default), 'pixels', or 'insetPixels' */
    String    sizeYUnits       = "fraction";   //

    float  rotation         = 0;   // 0 to +/- 180 degrees

    /** default constructor */
    public kmlScreenOverlay () {}
  }

  ///////////////////////////////////////////////////////////////////
  /** KML Schema */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlSchema {

    String name;
    String schemaID;
    String simpleFieldType;
    String simpleFieldName;
    String displayName;

    /** what the Schema is extending */
    String parent;   // since 3.3.2

    /** default constructor */
    public kmlSchema () {
      name            = new String();
      schemaID        = new String();
      simpleFieldType = new String();
      simpleFieldName = new String();
      displayName     = new String();
      parent          = "placemark";  // default
    }

    /** get name
     *  @return name
     */
    public String getName() { return name;  }
    /** set name
     *  @param name name
     */
    public void setName(String name) { this.name = name; }

    /** get target ID
     *  @return target ID
     */
    public String getTargetID() { return schemaID; }
    /** set target ID
     *  @param targetID target ID
     */
    public void setTargetID(String targetID) { this.schemaID = targetID;  }

    /** get simple field type
     *  @return simple field type
     */
    public String getSimpleFieldType() { return simpleFieldType; }
    /** set simple field type
     *  @param simpleFieldType simple field type
     */
    public void setSimpleFieldType(String simpleFieldType) { this.simpleFieldType = simpleFieldType; }

    /** get simple field name
     *  @return simple field name
     */
    public String getSimpleFieldName() { return simpleFieldName; }
    /** set simple field name
     *  @param simpleFieldName simple field name
     */
    public void setSimpleFieldName(String simpleFieldName) { this.simpleFieldName = simpleFieldName;  }

    /** get display name
     *  @return display name
     */
    public String getDisplayName() { return displayName; }
    /** set display name
     *  @param displayName display name
     */
    public void setDisplayName(String displayName) { this.displayName = displayName;  }

    /** Set what the Schema is extending
     *  @param parent what the Schema is extending
     *  @since NeXtMidas 3.3.2
     */
    public void setParent (String parent) { this.parent = parent; }
    /** Get what the Schema is extending
     *  @return what the Schema is extending
     *  @since NeXtMidas 3.3.2
     */
    public String getParent () { return parent; }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Style */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlStyle extends kmlStyleSelector {

    /* The boolean variables are used to check for existence of styles later */
    private kmlBalloonStyle balloonStyle;
    private boolean         hasBalloonStyle = false;
    private kmlIconStyle    iconStyle;
    private boolean         hasIconStyle    = false;
    private kmlLabelStyle   labelStyle;
    private boolean         hasLabelStyle   = false;
    private kmlLineStyle    lineStyle;
    private boolean         hasLineStyle    = false;
    private kmlListStyle    listStyle;
    private boolean         hasListStyle    = false;
    private kmlPolyStyle    polyStyle;
    private boolean         hasPolyStyle    = false;

    /** default constructor */
    public kmlStyle () {
      balloonStyle  = new kmlBalloonStyle();
      iconStyle     = new kmlIconStyle();
      labelStyle    = new kmlLabelStyle();
      lineStyle     = new kmlLineStyle();
      listStyle     = new kmlListStyle();
      polyStyle     = new kmlPolyStyle();
    }

    /** get KML Balloon Style
     *  @return KML Balloon Style
     */
    public kmlBalloonStyle getBalloonStyle() {  return balloonStyle; }
    /** set KML Balloon Style
     *  @param balloonStyle KML Balloon Style
     */
    public void setBalloonStyle(kmlBalloonStyle balloonStyle) {
      this.balloonStyle = balloonStyle;
      hasBalloonStyle = true;
    }

    /** get KML Icon Style
     *  @return KML Icon Style
     */
    public kmlIconStyle getIconStyle() { return iconStyle; }
    /** set KML Icon Style
     *  @param iconStyle KML Icon Style
     */
    public void setIconStyle(kmlIconStyle iconStyle) {
      this.iconStyle = iconStyle;
      hasIconStyle = true;
    }

    /** get KML Label Style
     *  @return KML Label Style
     */
    public kmlLabelStyle getLabelStyle() { return labelStyle; }
    /** set KML Label Style
     *  @param labelStyle KML Label Style
     */
    public void setLabelStyle(kmlLabelStyle labelStyle) {
      this.labelStyle = labelStyle;
      hasLabelStyle = true;
    }

    /** get KML Line Style
     *  @return KML Line Style
     */
    public kmlLineStyle getLineStyle() { return lineStyle; }
    /** set KML Line Style
     *  @param lineStyle KML Line Style
     */
    public void setLineStyle(kmlLineStyle lineStyle) {
      this.lineStyle = lineStyle;
      hasLineStyle = true;
    }

    /** get KML List Style
     *  @return KML List Style
     */
    public kmlListStyle getListStyle() { return listStyle; }
    /** set KML List Style
     *  @param listStyle KML List Style
     */
    public void setListStyle(kmlListStyle listStyle) {
      this.listStyle = listStyle;
      hasListStyle = true;
    }

    /** get KML Ploy Style
     *  @return KML Poly Style
     */
    public kmlPolyStyle getPolyStyle() { return polyStyle; }
    /** set kml ploy style
     *  @param polyStyle KML Poly Style
     */
    public void setPolyStyle(kmlPolyStyle polyStyle) {
      this.polyStyle = polyStyle;
      hasPolyStyle = true;
    }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Style Selector */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlStyleSelector extends kmlObject {

    /** default constructor */
    public kmlStyleSelector () {}

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Style Map */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlStyleMap extends kmlStyleSelector {
    /** Must be 'normal' (default) or 'highlight' */
    String normalKey    = "normal";
    String highlightKey = "highlight";

    String normalStyleUrl;
    String highlightStyleUrl;

    /** default constructor */
    public kmlStyleMap () {}

    /** set normal style URL
     *  @param url normal style URL
     */
    public void setNormalStyleUrl (String url) { normalStyleUrl = url; }
    /** get normal sytle URL
     *  @return normal sytle URL
     */
    public String getNormalStyleUrl () { return normalStyleUrl; }

    /** set highlight style URL
     *  @param url highlight style URL
     */
    public void setHighlightStyleUrl (String url) { highlightStyleUrl = url; }
    /** get highlight sytle URL
     *  @return highlight sytle URL
     */
    public String getHighlightStyleUrl () { return highlightStyleUrl; }

    /** set style map pair
     *  @param pair kml style map pair
     */
    public void setStyleMapPair (kmlStyleMapPair pair) {
      if (pair.getKey().equals("normal")) setNormalStyleUrl(pair.getStyleUrl());
      else                                setHighlightStyleUrl(pair.getStyleUrl());
    }
  }

  ///////////////////////////////////////////////////////////////////
  /** KML Pair (Used in Style Map) */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlStyleMapPair {
    private String key;
    private String styleUrl;

    /** default constructor */
    public kmlStyleMapPair () {}

    /** set key
     * @param key key value
     */
    public void setKey (String key) {
      if (key.equals("normal")||key.equals("highlight")) this.key = key;
      else {
        warning("Illegal Style Map Pair Key: "+key+". Setting to 'normal'.");
      }
    }

    /** get key
     *  @return key if not null, otherwise 'normal'
     */
    public String getKey () { return (key!=null)? key : "normal"; }

    /** set style URL
     *  @param url style URL
     */
    public void setStyleUrl (String url) { styleUrl = url; }
    /** get style URL
     *  @return style URL
     */
    public String getStyleUrl () { return styleUrl; }
  }

  ///////////////////////////////////////////////////////////////////
  /** KML Region
      A Region is 'active' when the bounding box is within view and the user's
       LOD requirements are met.
      Features associated with a Region are only drawn if a Region is active.
   */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlRegion extends kmlObject {

    /* Bounding Box (Area of Interest) */
    double  north;                            // 0 to 90 degrees
    double  south;                            // 0 to 90 degrees
    double  east;                             // 0 to 180 degrees
    double  west;                             // 0 to 180 degrees
    double  minAltitude  = 0;
    double  maxAltitude  = 0;

    /** Must be 'clampToGround' (default), 'relativeToGround', or 'absolute' */
    String altitudeMode = "clampToGround";

    /* Lod (Validity Range in Terms of Projected Screen Size) */
    float minLodPixels  = 0;
    float maxLodPixels  = -1;
    float minFadeExtant = 0;                // Opaqueness / distance
    float maxFadeExtant = 0;

    /** default constructor */
    public kmlRegion () {}

    /** get north value of y range
     *  @return north value (as double)
     */
    public double getNorth() { return north; }
    /** set north value of y range
     *  @param north double value of north
     */
    public void setNorth(double north) {
      this.north = north;
      updateYRange(north);   // update layer Y value range
    }

    /** get south value of y range
     *  @return south value (as double)
     */
    public double getSouth() {  return south;  }
    /** set south value of y range
     *  @param south double value of south
     */
    public void setSouth(double south) {
      this.south = south;
      updateYRange(south);  // update layer Y value range
    }

    /** get east value of x range
     *  @return east value (as double)
     */
    public double getEast() {  return east; }
    /** set east value of x range
     *  @param east double value of east
     */
    public void setEast(double east) {
      this.east = east;
      updateXRange(east);  // update layer X value range
    }

    /** get west value of x range
     *  @return west value (as double)
     */
    public double getWest() {  return west; }
    /** set west value of x range
     *  @param west double value of west
     */
    public void setWest(double west) {
      this.west = west;
      updateXRange(west);  // update layer X value range
    }

    /** get minimum altitude
     *  @return minimum altitude
     */
    public double getMinAltitude() {  return minAltitude;  }
    /** set minimum altitude
     *  @param minAltitude minimum altitude
     */
    public void setMinAltitude(double minAltitude) { this.minAltitude = minAltitude; }

    /** get maximum altitude
     *  @return maximum altitude
     */
    public double getMaxAltitude() {  return maxAltitude; }
    /** set maximum altitude
     *  @param maxAltitude maximum altitude
     */
    public void setMaxAltitude(double maxAltitude) { this.maxAltitude = maxAltitude; }

    /** get altitude mode
     *  @return altitude mode
     */
    public String getAltitudeMode() { return altitudeMode;  }

    /** set altitude mode
     *  @param altitudeMode altitude mode clampToGround, relativeToGround, or absolute
     */
    public void setAltitudeMode(String altitudeMode) {
      if (altitudeMode.equalsIgnoreCase("clamptoground")) {
        this.altitudeMode = "clampToGround";
      } else if (altitudeMode.equalsIgnoreCase("relativetoground")) {
        this.altitudeMode = "relativeToGround";
      } else if (altitudeMode.equalsIgnoreCase("absolute")) {
        this.altitudeMode = "absolute";
      } else {
        warning("Invalid altitude mode: " +altitudeMode+ ". Mode must be 'clampToGround', 'relativeToGround', or 'absolute'");
      }
    }

    /** get min lod pixels
     *  @return min lod pixels
     */
    public float getMinLodPixels() { return minLodPixels;  }
    /** set min lod pixels
     *  @param minLodPixels min lod pixels
     */
    public void setMinLodPixels(float minLodPixels) { this.minLodPixels = minLodPixels;  }

    /** get max lod pixels
     *  @return max lod pixels
     */
    public float getMaxLodPixels() { return maxLodPixels; }
    /** set max lod pixels
     *  @param maxLodPixels max lod pixels
     */
    public void setMaxLodPixels(float maxLodPixels) { this.maxLodPixels = maxLodPixels;}

    /** get min fade extant
     *  @return min fade extant
     */
    public float getMinFadeExtant() { return minFadeExtant; }
    /** set min fade extant
     *  @param minFadeExtant min fade extant
     */
    public void setMinFadeExtant(float minFadeExtant) { this.minFadeExtant = minFadeExtant; }

    /** get max fade extant
     *  @return max fade extant
     */
    public float getMaxFadeExtant() { return maxFadeExtant; }
    /** set max fade extant
     *  @param maxFadeExtant max fade extant
     */
    public void setMaxFadeExtant(float maxFadeExtant) { this.maxFadeExtant = maxFadeExtant; }

    /** Set behavior of KML Popup to either revert to old size limited
     *  behavior or to new user contributed expandable size behavior
     *  @param keepSmall true if should be previous size limited popup
     */
    public void setKeepPopupSmall (boolean keepSmall) {
      keepPopupSmall = keepSmall;
    }

    /** is Popup forced to stay small (old behavior)
     *  @return true if popup is forced to stay small
     */
    public boolean isKeepPopupSmall () {
      return keepPopupSmall;
    }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML Time Primitive */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlTimePrimitive extends kmlObject {

    /** default constructor */
    public kmlTimePrimitive () {}
  }

  ///////////////////////////////////////////////////////////////////
  /** KML Time Span */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlTimeSpan extends kmlTimePrimitive {
    // redo with NXM timestamp, if applicable
    Time begin;    // time to begin
    Time end;      // time to end

    /** default constructor */
    public kmlTimeSpan () {}

    /** get begin time
     *  @return begin time
     */
    public Time getBegin() { return begin; }
    /** set begin time
     *  @param begin begin Time
     */
    public void setBegin(Time begin) { this.begin = begin;  }

    /** get end time
     *  @return end time
     */
    public Time getEnd() { return end;  }
    /** set end time
     *  @param end end Time
     */
    public void setEnd(Time end) { this.end = end; }

  }

  ///////////////////////////////////////////////////////////////////
  /** KML TimeStamp */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class kmlTimeStamp extends kmlTimePrimitive {
    Time when;    // REDO with NXM timeStamp, if applicable

    /** default constructor */
    public kmlTimeStamp () {}

    /** get value (when) of kml time stamp
     *  @return Time
     */
    public Time getWhen() {  return when; }

    /** set when on kml time stamp
     *  @param when Time
     */
    public void setWhen(Time when) { this.when = when; }
  }

  ///////////////////////////////////////////////////////////////////
  /** A class to create a popup panel, which is used to display
      detailed information about <code>kmlPoint</code>s
      @since NeXtMidas 2.7.2
   */
  @InternalUseOnly("since NeXtMidas 3.5.3")
  public class Popup {

    private static final int DEFAULT_XPOS = 200;
    private static final int DEFAULT_YPOS = 200;
    JFrame frame;
    private PopupSelection popupSelection;

    /** Popup constructor to display KML point information
    *
    *  @param p     kml point
    */
    public Popup (kmlPoint p) {
      this(p, DEFAULT_XPOS, DEFAULT_YPOS);
    }

    /** Popup constructor to display KML point information
    *
    *  @param p     kml point
    *  @param xPos  x position of popup
    *  @param yPos  y position of popup
    */
    public Popup (kmlPoint p, int xPos, int yPos) {
      this(p, xPos, yPos, null);
    }

    /** Popup constructor to display KML point information
     *
     *  @param p     kml point
     *  @param xPos  x position of popup
     *  @param yPos  y position of popup
     *  @param label label
     */
    public Popup (kmlPoint p, int xPos, int yPos, String label) {
      this(new PopupSelection(p,xPos,yPos,label));
    }

    /** Popup constructor to display KML point information
     *
     *  @param popupSelection The PopupSelection Object containing Popup information
     *  @since NeXtMidas 4.1.3
     */
    private Popup (PopupSelection popupSelection) {
      this.popupSelection = popupSelection;
      kmlPoint p = popupSelection.getKmlPoint();
      int xPos = popupSelection.getXPos();
      int yPos = popupSelection.getYPos();
      String label = popupSelection.getLabel();
      String newLabel = label != null ? label : p.getParentFeature().getName();
      popupSelection.setLabel(newLabel);
      frame      = new JFrame(newLabel);

      frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
      PopupPanel myPopup = new PopupPanel(p);

      if (keepPopupSmall) {
        // Pre-existing behavior
        setPopupSmall(frame,myPopup,xPos,yPos);
      }
      else {
        // Expands to allow better rendering of large description
        setPopupFramePack(frame,myPopup,xPos,yPos);
      }

      frame.setVisible(true);
    }

    private void setPopupSmall (JFrame frame, PopupPanel myPopup, int xPos, int yPos) {
      // keep the popup size between 250 - 600 pixels
      int w = Math.min(600, Math.max(200, myPopup.getPreferredSize().width));
      int h = Math.min(600, Math.max(250, myPopup.getPreferredSize().height));
      frame.setSize(w,h);

      frame.setLocation(xPos - (w/2), yPos - (h/2));  // where to popup on the screen
      frame.setContentPane(myPopup);
    }

    /** Set Popup size via JFrame.pack, as this works better with
     *  large descriptions<br>
     *  This is a code contribution from Jeremy
     *
     *  @param frame   JFrame
     *  @param myPopup PopupPanel
     *  @param xPos    x coordinate of center of popup
     *  @param yPos    y coordinate of center of popup
     *  @since NeXtMidas 3.9.4 for NXM-3630
     */
    private void setPopupFramePack (JFrame frame, PopupPanel myPopup, int xPos, int yPos) {
      frame.setContentPane(myPopup);
      frame.pack();
      // Usually you use setLocationReletiveTo but there is no reference to LayerKML
      // So do it the hard way
      frame.setLocation(xPos - frame.getWidth()/2, yPos - frame.getHeight()/2);  // where to popup on the screen
    }

    /** Close the Popup.
     *  @since NeXtMidas 4.1.3
     */
    private void close() {
      frame.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING));
      if (Shell.needRemoveNotifyToAvoidJVMHang()) { // NXM-3986
        frame.removeNotify();
      }
    }

    /** A class to popup a panel on the screen with information about a <code>kmlPoint</code>
        @since NeXtMidas 2.7.2
     */
    @SuppressWarnings("serial")
    @InternalUseOnly("since NeXtMidas 3.5.3")
    public class PopupPanel extends JPanel {

      /** size (in pixels) to draw Images     */ private static final int DEFAULT_SIZE = 100;
      /** max size allowable for popup images */ private static final int MAX_SIZE     = 130;
      /** min size allowable for popup images */ private static final int MIN_SIZE     =  10;

      /** the Symbol that was drawn               */ private Symbol        symbol;
      /** the Image used in imageIcon             */ private BufferedImage img;
      /** Line used to draw the point             */ private Line          line;
      /** The ImageIcon to draw                   */ private ImageIcon     icon = null;
      /** The scrollpane used to display the text */ private JScrollPane   scrollPane;
      /** The text to display                     */ private String        text;

      /** Create Popup Panel to display Point information
       *
       *  @param p           KML Point
       */
      public PopupPanel(kmlPoint p) {
        this(p, DEFAULT_SIZE);
      }

      /** Create Popup Panel to display Point information
       *
       *  @param p           KML Point
       *  @param symbolSize  symbol size
       */
      public PopupPanel(kmlPoint p, int symbolSize) {
        setLayout(new GridLayout(1,1));

        String label          = Popup.this.popupSelection.getLabel();
        double xPos           = p.getCoords().lon;
        double yPos           = p.getCoords().lat;
        String desc           = p.getParentFeature().description;
        line                  = p.getLine();
        symbol                = line.getSymbol();
        float heading         = symbol.getHeading();
        kmlBalloonStyle style = p.getBalloonStyle();


        // Set Up the look and fell of the popup based on data in the kmlBalloonStyle
        setupStyle(style, label, desc);

        // Setup the image of the point to draw
        setupImage(p.getImage(), heading, symbolSize);

        // Setup the text to display under the point image
        String labelText = setupText(label, xPos, yPos, heading, text);

        // display the image or symbol if the style is not set to "hide'
        if (!style.getDisplayMode().equalsIgnoreCase("hide")) display (labelText);
      }

      private void setupStyle (kmlBalloonStyle style, String name, String desc) {
        text = style.getText();
        if (text==null || text.isEmpty()) { text = desc; }
        else {
          text = text.replaceAll("\\$\\[name\\]", name);
          text = text.replaceAll("\\$\\[description\\]", desc);
        }
        if (text == null || text.isEmpty()) text = "None";
      }

      /** Prepare the image to be displayed.
          If the image is already provided (i.e. the point uses an ImageIcon), then
          we simply use that image and rotate and resize it. if there is no existing
          image, we get one from the Symbol class. Note that images obtained from
          Symbol do not need rotating. */
      private void setupImage (Object obj, float heading, int newSize) {
        if (obj != null && obj instanceof BufferedImage) {
          img = (BufferedImage)obj;
          if (heading != 0) {
            double rads     = Math.toRadians(heading);
            img = GraphicsUtil.rotateImage(img, rads);   // Adjust the rotation of the img
          }
        }
        else {
          int h = symbol.getIconHeight();
          int w = symbol.getIconWidth();
          img   = symbol.getImage(w, h);
        }

        resize(newSize);  // resize (usually enlarge) the image
      }

      /** Resize the existing image to the size given.
          This method will look at the Height and width of the image or symbol,
          find the largest value, and scale the image or symbol based on the size given.
          Note that size is still restrained by {@link #MAX_SIZE} & {@link #MIN_SIZE}
          @param newSize The size to make the popup image or symbol
       */
      private void resize (float newSize) {
        if (newSize > MAX_SIZE) newSize = MAX_SIZE; // keep sizes in check
        else if (newSize < MIN_SIZE) newSize = MIN_SIZE;
        float ratio;  // scale difference between current size and new size

        float oldH = img.getHeight();
        float oldW = img.getWidth();

        // use the max dimension to calc scale multiplier
        ratio = (oldH > oldW)? (newSize / oldH): (newSize / oldW);
        int newH = (int) (oldH * ratio); // the height of the new Image (in pixels)
        int newW = (int) (oldW * ratio); // the width  of the new Image (in pixels)

        //Get a scaled version of the image and set the imageIcon to it
        Image i = ((Image)img).getScaledInstance(newW, newH, Image.SCALE_SMOOTH);
        icon = new ImageIcon(i);

        //System.out.println("LayKML: newSize=" +newSize+", oldH= " +oldH+", newH=  " +newH+ ", ratio= "+ratio+", Img h = " +img.getHeight());
      }

      /** Format the text to be displayed below the image */
      private String setupText (String label, double xPos, double yPos, double heading, String text) {
        String lcText = text.toLowerCase();
        // Put the common items in the string first
        String prefix = "Name: <font color=red><b>"+label+"</b></font>"+
        "<p>Lat: <font color=red><b>"+yPos+"</b></font>"+
        "<p>Lon: <font color=red><b>"+xPos+"</b></font>"+
        "<p>Heading: "+"<font color=red><b>"+heading+"</b></font>"+
        "<p>Description: ";
        String displayText = "";
        // Add the description or text field of the style
        // Jeremy contributed changes below for NXM-3630 3.9.4
        // handle scenario description has style tag
        if (lcText.startsWith("<html")) {
          // Attempt to go past the body tag before we insert our prefix
          int bIndex = lcText.indexOf("<body");
          if (bIndex != -1) {
            // Get the index of > after the body index
            int lIndex = lcText.indexOf(">", bIndex);
            if (lIndex != -1) {
              // Insert prefix after the body section
              displayText = text.substring(0, lIndex + 1) + prefix + text.substring(lIndex + 1);
            } else {
              // The text is messed up and has no > after <body.  Add Our prefix after <html>
              displayText = "<html>" + prefix + text.substring(text.indexOf(">")+1);
            }
          } else {  // No <body> element
            // Attempt to go past the style tag before we insert our prefix
            bIndex = lcText.indexOf("</style>");
            if (bIndex != -1) {
              bIndex += "</style>".length();
              displayText = text.substring(0, bIndex) + prefix + text.substring(bIndex);
            } else {  // No style!
              displayText = "<html>" + prefix + text.substring(text.indexOf(">")+1);
            }
          }
        } else {
          displayText += "<html>" + prefix + "<font color=red><b>"+ text + "</b></font>"+
          "</b></font></html>";
        }
        return  displayText;
      }

      /** Display the ImageIcon or Symbol.
          When using <code>ImageIcons</code>, we can simply make a single <code>JLabel</code>
          that contains the </code>ImageIcon</code> and the text to display beneath it.
          When just using the <code>Symbol</code>, we need to draw the <code>Symbol</code>
          first and then add the text beneath it on a seperate label
       */
      private void display (String labelText) {
        JLabel textDisplay = new JLabel(labelText, icon, JLabel.CENTER);
        textDisplay.setHorizontalTextPosition(JLabel.CENTER);
        textDisplay.setVerticalTextPosition(JLabel.BOTTOM);
        scrollPane = new JScrollPane(textDisplay);
        add(scrollPane);

        setVisible(true);
      }
    } // end class PopupPanel

  } // end class Popup

  ///////////////////////////////////////////////////////////////////
  /** A class to hold Popup selection info.
   *
   *  @author Zachary Matthews
   *  @since NeXtMidas 4.1.3
   */
  private class PopupSelection {
    private kmlPoint kmlPoint;
    private int xPos;
    private int yPos;
    private String label;

    /** PopupSelection Constructor
     *
     *  @param kmlPoint The Popup kmlPoint
     *  @param xPos     The Popup x position
     *  @param yPos     The Popup y position
     *  @param label    the Popup label
     */
    private PopupSelection (kmlPoint kmlPoint, int xPos, int yPos, String label) {
      this.kmlPoint = kmlPoint;
      this.xPos = xPos;
      this.yPos = yPos;
      this.label = label;
    }

    /** Get the Popup kmlPoint.
     *  @return The Popup kmlPoint
     */
    private kmlPoint getKmlPoint () {
      return kmlPoint;
    }

    /** Get the Popup x position.
     *  @return The Popup x position
     */
    private int getXPos () {
      return xPos;
    }

    /** Get the Popup y position.
     *  @return The Popup y position
     */
    private int getYPos() {
      return yPos;
    }

    /** Get the Popup label.
     *  @return The Popup label
     */
    private String getLabel () {
      return label;
    }

    /** Set the Popup label.
     *  @param label The new Popup label
     */
    private void setLabel (String label) {
      this.label = label;
    }
  }

  /** Get KML Images
   *  @return ArrayList with the KML Images
   */
  public ArrayList<KmlImage> getImages () {
    if (images==null)images = new ArrayList<>();
    return images;
  }

  /** Get the polygons
   *  @return the polygons
   */
  public ArrayList<kmlPolygon> getPolygons () {
    if (polygons==null) polygons = new ArrayList<>();
    return polygons;
  }

  /** Get the line Strings
   *  @return the line Strings
   */
  public ArrayList<kmlLineString> getLineStrings () {
    if (lineStrings==null) lineStrings = new ArrayList<>();
    return lineStrings;
  }

  /** Utility method to return a new Layer.
   *  @return new {@link LayerKML}
   *  @since NeXtMidas 3.9.2
   */
  public static Layer getLayer() {
    return new LayerKML();
  }

  /** KML false is '0' or 'false'
   *
   *  @param value String value to check
   *  @return true if the value matches one of the valid KML false designations
   *  @since NeXtMidas 4.0.0 for NXM-3701
   */
  private static boolean isKMLFalse (String value) {
    return "false".equalsIgnoreCase(value) || "0".equals(value);
  }

  /** KML true is '1' or 'true'
  *
  *  @param value String value to check
  *  @return true if the value matches one of the valid KML true designations
  *  @since NeXtMidas 4.0.0 for NXM-3701
  */
  private static boolean isKMLTrue (String value) {
    return "true".equalsIgnoreCase(value) || "1".equals(value);
  }

} // end class LayerKML
