package nxm.sys.libg;

import java.awt.Color;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import nxm.sys.inc.Drawable;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MessageHandler;
import nxm.sys.inc.PlotFile;
import nxm.sys.lib.Args;
import nxm.sys.lib.GeoJSONFile;
import nxm.sys.lib.GeoJSONObjects;
import nxm.sys.lib.GeoJSONObjects.GeoJSONFeature;
import nxm.sys.lib.GeoJSONObjects.GeoJSONGroup;
import nxm.sys.lib.GeoJSONObjects.GeoJSONLineString;
import nxm.sys.lib.GeoJSONObjects.GeoJSONObject;
import nxm.sys.lib.GeoJSONObjects.GeoJSONPoint;
import nxm.sys.lib.GeoJSONObjects.GeoJSONPolygon;
import nxm.sys.lib.GeodeticUtil;
import nxm.sys.lib.Message;
import nxm.sys.lib.Position;
import nxm.sys.lib.Table;
import nxm.sys.prim.plot;

/** A class to hold and display GeoJSON Features.

    @author Jeanette McCall
    @since NeXtMidas 4.1.3
 */
@InternalUseOnly("Experimental Prototype in NeXtMidas 4.1.3")
public class LayerGeoJSON extends Layer {

  // Strategy - read in GeoJSON into MapKBT
  // form the different objects from the KBT

  /** List of currently supported GeoJSON element tags.  */
  public static final String supportedElements =
    "coordinates,feature,features,featurecollection,"
    + "geometries,geometry,geometrycollection,linestring,"
    + "multipoint,multilinestring,multipolygon,point,polygon,properties,name,type";

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

  GeoJSONFile jsonFile;

  boolean tryingKMLApproach = false;
  boolean fillInnerPolysToo = true;

  private static final double PIXEL_MAX_RANGE = 625;

  private final int      DEFAULT_LINE_WIDTH         = 2;
  private final int      DEFAULT_TEXT_SIZE          = 12;
  private final int      DEFAULT_POINT_SIZE         = 4;
  private final String   DEFAULT_POINT_SYMBOL       = "Circle";

  /** The default colors */
  static final Color DEFAULT_POINT_COLOR = Color.GREEN;
  static final Color DEFAULT_LINE_COLOR = DEFAULT_POINT_COLOR;
  static final Color DEFAULT_POLYGON_COLOR = DEFAULT_POINT_COLOR;
  static final Color DEFAULT_POLYGON_FILLED_COLOR = new Color(0x66cdaa); //mediumaquamarine
  static final Color DEFAULT_POLYGON_INNER_FILLED_COLOR = Color.BLACK;

  private final String[] LABEL_POSITIONS            = {"TOPR","TOP","BOTTOMR","BOTTOM","RIGHT"};
  private final String   DEFAULT_LABEL_POS          = "TOPR";

    /** 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;  // TODO currently unused

  /* The four variables below are for MPLOT's drawLine method, which is used for most drawing */
  private int      npts;         // Number of points. This is (buf.length / 3)

  private boolean  polygonFill     = false;
  private boolean  polygonOutline  = true;
  private Color    polyFillColor   = DEFAULT_POLYGON_FILLED_COLOR;
  private Color    polyInFillColor = DEFAULT_POLYGON_INNER_FILLED_COLOR;
  private Color    lineColor       = DEFAULT_LINE_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 String   pointSymbol     = DEFAULT_POINT_SYMBOL; // When GeoJSON Icon is a known Symbol
  private int      pointSize       = DEFAULT_POINT_SIZE;
  private boolean  showLabel       = true;         // whether or not to draw the label next to the point
  private boolean  allowPopups     = true;         // allow balloon popups for GeoJSON points
  private GeoJSONObjects objects   = null;

  private boolean fileProcessed = false; // has the file been processed and bounds set

  /** Color of the selected point. */
  private Color selectColor = Color.RED;

  private PopUpHTML htmlPopup;

  /** point popup is active */
  private boolean activePopup; // keep track if there was an active popup when layer was hidden

  private GeoJSONObject selectedObject;

  private GeoJSONGroup selectedGroup;

  private Pix pix;

  /** Messages send upon selection of a point or deselection of all points */
  private boolean sendSelectMessages = false;

  private MessageHandler mwMsgHandler = null;

  /** Default constructor. */
  public LayerGeoJSON() {
    super();
    name = "GeoJSON";
    /* 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(DEFAULT_POINT_COLOR);

    // 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

    pix = new Pix(1, 0);
  }

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

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

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

    /* If it is a GeoJSON file, parse it */
    if (status) {
      super.setFile(pf);
      jsonFile = (GeoJSONFile)pf;
      if (tab != null) {

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

      }

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

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

  /** Processes mouse messages for selection and popups
      @param msg  The Mouse message
      @param type Type of message
   */
  public void processMouseMessage(Message msg, int type) {
    if (!isVisible() || msg.data=="NULL") return;
    if (!(msg.quals instanceof Table)) return; // Need to have the mouse event to select by pixel.
    Table quals = (Table) msg.quals;
    if (!(quals.get("ORIG_MSG_DATA") instanceof MouseEvent)) return; // Need to have the mouse event to select by pixel.

    if (activePopup) closeCurrentPopup();
    MouseEvent me = (MouseEvent) quals.get("ORIG_MSG_DATA");
    Point selPoint = me.getPoint();
    Position pos = (Position) msg.getData();
    if (MP.isLatLonView()) {
      // checking for polygons only in LATLON views for now, as can not get lat/lon when in geodetic
      for (GeoJSONPolygon polygon : objects.getGeoJSONPolygons()) {
        boolean insidePoly = GeodeticUtil.insidePolygon(pos, polygon.getAtomsPerElem(), polygon.getOuterBoundary(), polygon.getInnerBoundaries());
        if (insidePoly) {
          setSelectedObject(polygon);
          if (GraphicsUtil.isCtrlLMB(msg)) { // Since 3.9.3 add a popup for the selected if necessary
            drawPopup(polygon);
          }
          return;
        }
      }
    }
    GeoJSONObject tempSelected = null;
    int mdist = (int)PIXEL_MAX_RANGE;

    for (GeoJSONLineString lineString : objects.getGeoJSONLineStrings()) {
      double[] lsCoords = lineString.getCoords();
      int ape = lineString.getAtomsPerElem();
      for (int lsIdx=0; lsIdx < lsCoords.length; lsIdx += ape) {
        int dist = getPointDistanceFromClick(selPoint.x,selPoint.y,
            Arrays.copyOfRange(lsCoords, lsIdx, lsIdx+ape));
        if (dist < mdist) {
          mdist = dist;
          tempSelected = lineString;
        }
      }
    }
    for (GeoJSONPoint point : objects.getGeoJSONPoints()) {
      int dist = getPointDistanceFromClick(selPoint.x,selPoint.y,point.getCoords());
      if (dist < mdist) {
        mdist = dist;
        tempSelected = point;
      }
    }

    setSelectedObject(tempSelected);
    if (selectedObject!=null && GraphicsUtil.isCtrlLMB(msg)) { // Since 3.9.3 add a popup for the selected if necessary
      drawPopup(tempSelected);
    }
  }

  private int getPointDistanceFromClick (int x, int y, double[] point) {
    int dist = Integer.MAX_VALUE;
    MP.viewer.rwc2pix(point, 0, point.length, 1, pix);
    int ix = pix.x[0];
    int iy = pix.y[0];
    if (ix >= MP.ix1 && ix <= MP.ix2 && iy >= MP.iy1 && iy <= MP.iy2) {
      int dx = ix - x, dy = iy - y;
      dist = dx * dx + dy * dy;
    }
    return dist;
  }

  private void drawPopup (GeoJSONObject geoJSONObject) {
    if (geoJSONObject == null) return;
    if (activePopup) closeCurrentPopup();
    if (selectedGroup != null) {
      Table popupTbl = new Table();
      popupTbl.setFlags(popupTbl.getFlagsInt()|Table.AUTOREADPREV|Table.CASESENSITIVE);
      popupTbl.addIfNotPresent(selectedObject.toTable());
      popupTbl.addIfNotPresent(selectedGroup.toTable());
      htmlPopup = new PopUpHTML(popupTbl, MP);
    }
    else {
      htmlPopup = new PopUpHTML(geoJSONObject.toTable(), MP);
    }
    activePopup = true;
  }

  private void closeCurrentPopup () {
    if (htmlPopup != null) htmlPopup.close();
    activePopup = false;
  }

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

  /** 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; }

  public int getData (int flag) {
    if (pf==null || !pf.isOpen()) return 0;
    if (!fileProcessed) {
      objects = jsonFile.getGeoJSONObjects();
      updateRange();
    }
    return Drawable.FULL;
  }

  protected void updateRange () {
    double coords[];
    for (GeoJSONPolygon poly : objects.getGeoJSONPolygons()) {
      coords  = poly.getCoords();
      int ape = poly.getAtomsPerElem();
      for (int i=0; i < coords.length; i+=ape) {
        updateXRange(coords[i]);
        updateYRange(coords[i+1]);
      }
    }
    for (GeoJSONLineString line : objects.getGeoJSONLineStrings()) {
      coords  = line.getCoords();
      int ape = line.getAtomsPerElem();
      for (int i=0; i < coords.length; i+=ape) {
        updateXRange(coords[i]);
        updateYRange(coords[i+1]);
      }
    }
    for (GeoJSONPoint point : objects.getGeoJSONPoints()) {
      coords  = point.getCoords();
      int ape = point.getAtomsPerElem();
      for (int i=0; i < coords.length; i+=ape) {
        updateXRange(coords[i]);
        updateYRange(coords[i+1]);
      }
    }
    if (!fileProcessed) {
      MP.rescale(true);
      fileProcessed = true;
    }
  }

  /** 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 = Math.floor(xval);
    if (xval > x2) x2 = Math.ceil(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 = Math.floor(yval);
    if (yval > y2) y2 = Math.ceil(yval);
  }

  /** Draw the GeoJSON objects from the file
      This method is called by MPLOT and represents the 'Launch Pad' for the
       drawing and handling of the GeoJSON Objects.
   */
  public void draw (int mode) {
    super.draw(mode);

    drawGeoJSONObjects(objects);   // Break apart the GeoJSON objects and draw them
  }

  /** Sets whether to use polygon outlines
   *  @param polygonOutline true if using polygon outlines
   */
  public void setPolygonOutline (boolean polygonOutline) {
    this.polygonOutline = polygonOutline;
  }

  /** Gets whether to use polygon outlines
   *  @return true if using polygon outlines
   */
  public boolean getPolygonOutline () {
    return polygonOutline;
  }

  /** 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();
  }

  private Color getColorFromObject (Object value) {
    if ((value instanceof String) && (value.toString().length()==0)) return null;
    if ((value instanceof String) && ("NONE".equalsIgnoreCase((String)value))) return null;
    else return MColor.getColor(value);
  }

  /** Sets the Color used for selected <code>GeoJSONObject</code>s
   *  @param value The color used for selected <code>GeoJSONObject</code>s
   */
  public void setSelectColor (Object value) {
    setSelectColor(getColorFromObject(value));
  }

  /** Sets the Color used for selected <code>GeoJSONObject</code>s.
   *  @param c The Color used for selected <code>GeoJSONObject</code>s
   */
  public void setSelectColor (Color c) {
    if (c == null) return;
    if(!selectColor.equals(c)){
      selectColor = c;
      refresh();
    }
  }

  /** Gets the Color used for selected <code>GeoJSONObject</code>s.
   *  @return The Color used for selected <code>GeoJSONObject</code>s
   */
  public Color getSelectColor(){
    return selectColor;
  }

  /** Sets the Color used for <code>GeoJSONPolygon</code> fills.
   *  @param value The Color used for <code>GeoJSONPolygon</code> fills
   */
  public void setPolygonFillColor (Object value) {
    setPolygonFillColor(getColorFromObject(value));
  }

  /** Sets the Color used for <code>GeoJSONPolygon</code> fills.
   *  @param color The Color used for <code>GeoJSONPolygon</code> fills
   */
  public void setPolygonFillColor (Color color) {
    polyFillColor = color;
  }

  /** Gets the Color used for <code>GeoJSONPolygon</code> fills.
   *  @return The Color used for <code>GeoJSONPolygon</code> fills
   */
  public Color getPolygonFillColor () {
    return polyFillColor;
  }

  /** Sets the Color used for <code>GeoJSONLineString</code>s (or borders for <code>GeoJSONPolygon</code>s).
   *  @param value The Color used for <code>GeoJSONLineString</code>s (or borders for <code>GeoJSONPolygon</code>s)
   */
  public void setLineColor (Object value) {
    setLineColor(getColorFromObject(value));
  }

  /** Sets the Color used for <code>GeoJSONLineString</code>s (or borders for <code>GeoJSONPolygon</code>s).
      @param color The Color used for <code>GeoJSONLineString</code>s (or borders for <code>GeoJSONPolygon</code>s)
   */
  public void setLineColor (Color color) {
    lineColor = color;
  }

  /** Gets the Color used for <code>GeoJSONLineString</code>s (or borders for <code>GeoJSONPolygon</code>s).
   *  @return The Color used for <code>GeoJSONLineString</code>s (or borders for <code>GeoJSONPolygon</code>s)
   */
  public Color getLineColor () {
    return lineColor;
  }

  /** Sets the width of the lines (or borders)
      @param width the width of lines (or borders)
   */
  @InternalUseOnly("currently unused")
  public void setLineWidth (int width) {
    lineWidth = width;
  }

  /** get line width
   *  @return line width
   */
  @InternalUseOnly("currently unused")
  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 */
  @InternalUseOnly("currently unused")
  public void setLabelScale (float scale) { labelScale = scale; }

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

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

  /** get label color
   *  @return label color
   */
  @InternalUseOnly("currently unused")
  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 */
  @InternalUseOnly("currently unused")
  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 */
  @InternalUseOnly("currently unused")
  public int getTextSize () { return textSize; }

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

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

  /** 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;
    } else {
      warning("Invalid symbol: " +symbol+ ". Symbol must be in " +Symbol.symbolList);
    }
  }

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

  /** set label position
   *  @param pos label position
   */
  @InternalUseOnly("currently unused")
  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)+".");
  }

   /** Get the size of the GeoJSON Points (Symbols)
   *  @return the size of the GeoJSON Points (Symbols) */
  @InternalUseOnly("currently unused")
  public int getPointSize () {
    return pointSize;
  }

  /** Set the size of the GeoJSON Points (Symbols)
   *  @param pointSize the size of the GeoJSON Points (Symbols) */
  @InternalUseOnly("currently unused")
  public void setPointSize (int pointSize) {
    this.pointSize = pointSize;
  }


  /** Get the GeoJSON Objects being used
   *  @return the GeoJSON objects being used */
  @InternalUseOnly
  public GeoJSONObjects getGeoJSONObjects () {
    return objects;
  }

  /** Set the GeoJSON objects
   *  @param objs GeoJSON objects */
  @InternalUseOnly
  public void setGeoJSONObjects (GeoJSONObjects objs) {
    objects = objs;
  }

  /** handle drawing of GeoJSONObjects by type */
  private void drawGeoJSONObjects (GeoJSONObjects objects) {
    if (objects==null) return;
    drawPoints(objects.getGeoJSONPoints());
    drawLineStrings(objects.getGeoJSONLineStrings());
    drawPolygons(objects.getGeoJSONPolygons());
  }

  private void drawPoints (ArrayList<GeoJSONPoint> points) {
    Line myLine = line.copy();
    for (GeoJSONObjects.GeoJSONPoint point : points) {
      int offset=0; npts=1;
      boolean selected = isInSelectedGroup(point) || isSelectedObject(point);
      Color _color = (selected) ? selectColor : line.getColor();
      myLine.setColor(_color);
      myLine.setType("Symbol");                    // Set the type to Symbol
      line.getSymbol().setSize(4);
      MP.drawLineLonLat(point.getCoords(), offset, point.getAtomsPerElem(), npts, myLine);  // Draw the point
    }
  }

  private void drawLineStrings (ArrayList<GeoJSONLineString> lineStrings) {
    Line myLine = line.copy();
    for (GeoJSONObjects.GeoJSONLineString lineString : lineStrings) {
      double[] flatCoords = lineString.getCoords();
      int offset=0; npts=flatCoords.length/lineString.getAtomsPerElem();
      boolean selected = isInSelectedGroup(lineString) || isSelectedObject(lineString);
      Color _color = (selected) ? selectColor : lineColor;
      myLine.setColor(_color);
      myLine.setType(Line.SOLID|Line.SYMBOL);
      line.getSymbol().setSize(4);
      MP.drawLineLonLat(flatCoords, offset, lineString.getAtomsPerElem(), npts, myLine);  // Draw the linestring
    }
  }

  private void drawPolygons (ArrayList<GeoJSONPolygon> polygons) {
    Line myOutlineLine = line.copy(); // one that gets changed upon selection
    myOutlineLine.setThickness(2);
    Line myFillLine = line.copy();
    myFillLine.setType(line.getType() + "Fill");
    myFillLine.setColor(polyFillColor);
    Line myInnerFillLine = myFillLine.copy();
    myInnerFillLine.setColor(polyInFillColor);
    for (GeoJSONObjects.GeoJSONPolygon polygon : polygons) {
      int ape = polygon.getAtomsPerElem();
      int offset=0;
      int numInnerRings = (polygon.getInnerBoundaries() == null) ?
          0 : polygon.getInnerBoundaries().length;
      boolean fillingPolygon = (polygon.getFill() || polygonFill);
      boolean selected = isInSelectedGroup(polygon) || isSelectedObject(polygon);
      if (!fillingPolygon || ((numInnerRings > 1) && fillInnerPolysToo)) {
        double[] outerBoundary = polygon.getCoords();
        npts=outerBoundary.length/ape;
        if (fillingPolygon) MP.drawLineLonLat(outerBoundary, offset, ape, npts, myFillLine);  // Draw the outer polygon
        // Do a second draw if drawing outlines on filled polygons
        if (!fillingPolygon || polygonOutline) {
          Color _color = (selected) ? selectColor : lineColor;
          myOutlineLine.setColor(_color);
          MP.drawLineLonLat(outerBoundary, offset, ape, npts, myOutlineLine);  // Draw the outer polygon
        }
        if (polygon.getInnerBoundaries() != null) {
          myOutlineLine.setThickness(1);
          for (double[] innerBoundary : polygon.getInnerBoundaries()) {
            npts = innerBoundary.length/ape;
            if (fillingPolygon) MP.drawLineLonLat(innerBoundary, offset, ape, npts, myInnerFillLine);  // Draw the inner polygon
            // Do a second draw if drawing outlines on filled polygons
            if (!fillingPolygon || polygonOutline) {
              Color _color = (selected) ? selectColor : lineColor;
              myOutlineLine.setColor(_color);
              MP.drawLineLonLat(innerBoundary, offset, ape, npts, myOutlineLine);  // Draw the outer polygon
            }
          }
        }
      }
      else {
        //  fill case with not filling inner polygons
        double[] boundaries;
        int totalLength = polygon.getCoords().length;
        if (polygon.getInnerBoundaries() == null) {
          boundaries = polygon.getCoords();
        }
        else {
          for (double[] innerBoundary : polygon.getInnerBoundaries()) {
            totalLength += innerBoundary.length;
          }
          boundaries = combineOuterInnerPolygons(polygon, numInnerRings, totalLength);
        }
        npts=totalLength/ape;
        MP.drawLineLonLat(boundaries, offset, ape, npts, myFillLine);
        if (polygonOutline) {
          double[] outerBoundary = polygon.getCoords();
          npts=outerBoundary.length/ape;
          Color _color = (selected) ? selectColor : lineColor;
          myOutlineLine.setColor(_color);
          MP.drawLineLonLat(outerBoundary, offset, ape, npts, myOutlineLine);
        }
      }
    }
  }

  private boolean isInSelectedGroup (GeoJSONFeature geoJsonFeature) {
    return (selectedGroup != null) && (geoJsonFeature.getGroup() == selectedGroup);
  }

  private boolean isSelectedObject (GeoJSONObject geoJsonObject) {
    return (geoJsonObject == selectedObject);
  }

  private double[] combineOuterInnerPolygons (GeoJSONPolygon polygon, int numInnerRings, int totalLength) {
    double[] coordsCombined;

    /* Send to other method if multiple inner rings */
    if ((numInnerRings > 1) && tryingKMLApproach) {
      coordsCombined = combinePolygons(polygon, numInnerRings, totalLength);
    }
    else {
        coordsCombined = new double[totalLength];
        System.arraycopy(polygon.getCoords(), 0, coordsCombined, 0, polygon.getCoords().length);
        int copyIdx = polygon.getCoords().length;
        for (double[] innerBoundary : polygon.getInnerBoundaries()) {
          System.arraycopy(innerBoundary, 0, coordsCombined, copyIdx, innerBoundary.length);
          copyIdx += innerBoundary.length;
        }
      }
    return coordsCombined;
  }

  /** KML based code, which does not seem to improve the situation */
  private double[] combinePolygons (GeoJSONPolygon polygon, int innerRings, int totalLength) {

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

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

    double[] coordsCombined;
    int tmpPos         = 0;

    int endPathCounter = 0;

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

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

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

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

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

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

    return coordsCombined;
  }

  /** Does nothing. Range values set when reading coordinates. */
  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;
  }

  private String getSelectionInfo () {
    StringBuilder selection = new StringBuilder(selectedObject.toString());
    if (selectedGroup != null) {
      selection.append(",\n").append(selectedGroup.toString());
    }
    return selection.toString();
  }

  /** Sets the selected point based on the index value of the point.
   *  @param geoJSONObject The GeoJSONObject to select.
   */
  private void setSelectedObject (GeoJSONObject geoJSONObject) {
    if (geoJSONObject != selectedObject) {
      String previousSelectionInfo =  (selectedObject != null) ? getSelectionInfo() : null; // save off previously selected
      selectedObject = geoJSONObject;
      boolean newObjectNull = geoJSONObject == null;
      selectedGroup = (!newObjectNull && (selectedObject instanceof GeoJSONFeature)) ? ((GeoJSONFeature)selectedObject).getGroup() : null;
      if (newObjectNull && activePopup) closeCurrentPopup (); // since 3.9.3 make sure popup closed // 3.9.4 update
      refresh();
      if (sendSelectMessages) { // send message with information about selected point 3.9.3 NXM-3582
        if (mwMsgHandler == null) mwMsgHandler = ((plot) MP.mh).getMessageHandler();
        if (mwMsgHandler != null) {
          // Send DeSelect message for previous selected point, if we have one
          if (previousSelectionInfo!=null) mwMsgHandler.processMessage(new Message("DESELECT",0,previousSelectionInfo,mwMsgHandler,MP.mh));
          if (selectedObject != null) {
            // If a new point is selected, send out a select message for it
            mwMsgHandler.processMessage(new Message("SELECT",0,getSelectionInfo(),mwMsgHandler,MP.mh));  //  + "/n" + getGroupInfo here and popup
          }
        }
      }
    }
  }

  // Need to override this so hiding of LayerGeoJSON causes any popup to
  // temporarily go away and a deselect message to be sent for any
  // selected points (if there is a message handler).
  // The popup should return and select message resent upon showing the layer,
  // if the point is still selected (not overridden or deselected)
  @Override
  public void setEnable (int value) {
    boolean layerHidden = (value&MPlot.EN_GLOBAL)==0;
    if ((enable&MPlot.EN_GLOBAL)==0 != layerHidden) {
      if (mwMsgHandler == null) mwMsgHandler = ((plot) MP.mh).getMessageHandler();
      if (selectedObject != null) {
        if (layerHidden) {
          if (activePopup && (htmlPopup != null)) {
            closeCurrentPopup();
            activePopup = true;
          }
          if (mwMsgHandler != null) mwMsgHandler.processMessage(new Message("DESELECT",0,getSelectionInfo(),mwMsgHandler,MP.mh));
        }
        else {
          if (activePopup) drawPopup(selectedObject);
          if (mwMsgHandler != null) mwMsgHandler.processMessage(new Message("SELECT",0,getSelectionInfo(),mwMsgHandler,MP.mh));
        }
      }
    }
    super.setEnable(value);
  }

  /** enable sending of messages when points are selected or deselected
   *  @param sendSelectMessages true if messages should be sent
   */
  public void setSelectMessages (String sendSelectMessages) {
    setSelectMessages(sendSelectMessages.toLowerCase().startsWith("t"));
  }

  /** Send SELECT and DESELECT messages
   *  @param sendSelectMessages true if SELECT and DESELECT messages should be sent
   */
  public void setSelectMessages (boolean sendSelectMessages) {
    this.sendSelectMessages = sendSelectMessages;
  }

  @Override
  public boolean clear() {
    objects = null;
    setSelectedObject(null);
    refresh();
    return true;
  }

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

} // end class LayerGeoJSON
