package nxm.sys.libg;

import java.awt.Color;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import nxm.sys.inc.Drawable;
import nxm.sys.inc.PlotFile;
import nxm.sys.lib.Args;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Position;
import nxm.sys.lib.ShapeFile;
import nxm.sys.lib.ShapeFile.Shape;
import nxm.sys.lib.Table;
import nxm.sys.prim.plot;

/**
  A class to hold a ShapeFile layer.

  @author Jeff Schoen
*/
public class LayerShape 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";

  /** 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 = MColor.getColor(00,75,00);

  private Color     colorBorder      = DEFAULT_BORDER_COLOR; // Shape polygon border color
  private boolean   needsColorUpdate = true;           // Need color update.
  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            = 255;            // True alpha level (0..255). May be computed from the opaqueness.
  private int       selectedIndex    = -1;             // Index of selected item (-1=NoSelection)
  private Color[]   adjustedColors   = null;           // Used as buffer to hold Draw values (Added for Bug 1174)
  /** buffer holding coordinates */  public  float[]   fbuf;
  /** dimensions */                  public  int       dim              = 2;
  private Shape[]   shapes;
  private double[]  dtmp             = new double[6];
  private ShapeFile sf;
  private Pix       pix;
  private ShapePix  spix;
  private int       maxPixels;
  private String    label;
  private double    xmin,ymin,xmax,ymax,lat,lon;
  private double    tlreg = 10.0;
  private int       debug = 0;

  /** Default constructor, uses {@link MColor#MAPLINE} color map. */
  public LayerShape () { this(MColor.MAPLINE); }

  /** Creates a new instance. This constructor mirrors the one in {@link LayerPoly}
      that takes in a color map and is public for the benefit of subclasses of
      {@link LayerShape}.
      @param cmap The color map to use.
   */
  public LayerShape (int cmap) {
    super("SHAPE");
    setColorMap(cmap);
  }

  @Override
  protected void init (plot p, String layerName, int layerIndex, Layer oldLayer) { // since 3.3.2
    super.init(p, layerName, layerIndex, oldLayer);
    if (!(Theme.getOption(Theme.SWING) && Theme.getOption(Theme.SWINGDB))) {
      // Gives user option of turning off backing store and not having it turned back on by layerpoly automatically since 3.9.3
      MP.setOptions("+BS");
    }
  }

  @Override
  public int getDefaultView () { // since 3.3.2
    return MPlot.V_LATLON;
  }

  @Override
  public boolean setFile (PlotFile pf, Table tab, Args MA) {
    if (tab != null) {
      label        = tab.getS("LABEL");
      coloringMode = (tab.get("COLOR") != null)? COLORS_MONOCHROME: COLORS_DEFAULT;

      if (label != null) {
        tab.remove("LABEL");
      } else {
        label = "";
      }

      if (tab.get("DEBUG") != null) {
        debug = tab.getL("DEBUG", -1);
        tab.remove("DEBUG");
      }

    } else {
       label        = "";
       coloringMode = COLORS_DEFAULT;
    }
    super.setFile(pf);
    sf = (ShapeFile)pf;
    if (sf.shapeType==ShapeFile.POINT) {
      line.setRadius(3);
      line.setSymbol("CROSS");
      line.getSymbol().setSize(4);
      line.setType(Line.SOLID|Line.SYMBOL);
      line.setColor(DEFAULT_POINT_COLOR);
      if (coloringMode == COLORS_MONOCHROME) { line.setColor(Color.RED); }
      if (coloringMode == COLORS_MIN_ADJ   ) { coloringMode = COLORS_MOD_N; }
    } else if (sf.shapeType==ShapeFile.POLYLINE) {
      if (coloringMode == COLORS_MIN_ADJ   ) { coloringMode = COLORS_MOD_N; }
    }
    xmin = 0;
    xmax = 0;
    ymin = 0;
    ymax = 0;
    lat  = -1;
    lon  = -1;

    needsColorUpdate = true;

    return true;
  }

  @Override
  @SuppressWarnings("deprecation") // since 3.9.0
  public int getData (int mode) {
    if (pf==null || !pf.isOpen() || ndata>0) return 0;
    shapes = sf.getShapes();
    ndata = shapes.length;

    for (int i=0; i<ndata; i++) {
      if (shapes[i].maxPartLength>maxPixels) maxPixels=shapes[i].maxPartLength;
    }
    pix = new Pix(maxPixels,0);

    if (label.length() > 0) {
      String labs[] = sf.dbf.getElementArray(label);
      if (labs != null) {
        if (labs.length != ndata) {
          sf.M.warning ("Number of DBF fields " + labs.length + " does not match shapes " + ndata);
          label = "";
        } else {
          for (int i = 0; i < ndata; i++) {
            shapes[i].label = labs[i];
          }
        }
      } else {
        sf.M.warning("Could not find label field: " + label);
        label = "";
      }
    }
    needsColorUpdate = true;

    return Drawable.DATA;
  }

  @Override
  public void process () {
    // TODO: Why is this here??? We probably want the default implementation from Layer
  }

  @Override
  public void findRange() {
    x1 = sf.x1;
    x2 = sf.x2;
    y1 = sf.y1;
    y2 = sf.y2;
    // Set Minimum and Maximum actual auto-scale value (since 3.7.0 for NXM-2984)
    a1 = y1;
    a2 = y2;
  }

  @Override
  @SuppressWarnings("deprecation") // Since 3.9.0
  public void draw (int flag) {
    super.draw(flag);
    if (MP.gc==null) return;
    if (needsColorUpdate) { updateColors(); }

    boolean recalcPolys = false;
    int i,j,npts;
    Line  lin   = line.copy();
    Color color = colors[1];

    if (ndata<=0) return;
    if (label.length() > 0) {
      if (MP.view==MPlot.V_GEODETIC) {
        if (lat != MP.viewGeo.lat || lon != MP.viewGeo.lon) {
          lat = MP.viewGeo.lat;
          lon = MP.viewGeo.lon;
          recalcPolys = true;
        }
      }
      if (xmin != MP.rx1 || xmax != MP.rx2 ||
          ymin != MP.ry1 || ymax != MP.ry2 ) {
        xmin = MP.rx1;
        xmax = MP.rx2;
        ymin = MP.ry1;
        ymax = MP.ry2;
        recalcPolys = true;
      }
      if (recalcPolys) {
        if (debug > 0) System.out.println("Creating new ShapePix, must recalc polys");
        spix = new ShapePix(MP.ix1, MP.iy1, MP.ix2, MP.iy2, ndata, debug);
      }
    }
    for (int ishape=0; ishape<ndata && !MP.cancel; ishape++) {
      Shape s = shapes[ishape];
      if (s.shapeType == ShapeFile.POINT) {
        MP.gc.setColor(getColor());
        if (MP.is2D) {
          int ix = MP.getix(s.x1);
          int iy = MP.getiy(s.y1);
          lin.getSymbol().draw (ix, iy, MP.gc);
          if (label.length() > 0 && s.label != null) {
            double vreg;
            vreg = Math.min(MP.rx2-MP.rx1,MP.ry2-MP.ry1);
            if (vreg <= tlreg) {
              int r = lin.getRadius();
              int xoff = 2 + r;
              MP.gc.drawString(s.label, ix + xoff, iy + r);
            }
          }
        } if (MP.view==MPlot.V_GEODETIC || MP.view==MPlot.V_XYZ) {
          dtmp[0] = s.x1;
          dtmp[1] = s.y1;
          MP.viewer.rwc2pix(dtmp,0,3,1,pix);
          lin.getSymbol().draw (pix.x[0], pix.y[0], MP.gc);
          if (label.length() > 0 && s.label != null) {
            double vreg;
            vreg = 50.0*(Math.min(MP.rx2-MP.rx1,MP.ry2-MP.ry1));
            if (vreg <= tlreg) {
              int r = lin.getRadius();
              int xoff = 2 + r;
              MP.gc.drawString(s.label,pix.x[0] + xoff, pix.y[0] + r);
            }
          }
        }
      } else if (s.shapeType == ShapeFile.NULLSHAPE) {
          // nothing to do for a null shape
      } else {
        int bbox[] = {0,0,0,0};
        for (int ipart=0; ipart<s.numParts; ipart++) {
          j = s.parts[ipart];
          if (ipart<s.numParts-1) npts = s.parts[ipart+1]-j;
          else npts = s.numPoints-j;
          if (MP.view==MPlot.V_LATLONCONTINUOUS) {
            // Lat Lon Continuous mode (added in 3.9.0) requires a different in-bounds check
            if (s.x1>=MP.rx2) continue;
            if (s.y1>=MP.ry2) continue;
            if ((s.x2<=MP.rx1) && ((s.x1 + MPlot.LONGITUDE_RANGE) >= MP.rx2)) continue;
            if (s.y2<=MP.ry1) continue;
          }
          else if (MP.is2D) {
            if (s.x1>=MP.rx2) continue;
            if (s.y1>=MP.ry2) continue;
            if (s.x2<=MP.rx1) continue;
            if (s.y2<=MP.ry1) continue;
          } else if (MP.view==MPlot.V_GEODETIC || MP.view==MPlot.V_XYZ) {
            dtmp[0]=s.x1; dtmp[1]=s.y1;
            dtmp[2]=s.x2; dtmp[3]=s.y2;
            i = MP.viewer.rwc2pix (dtmp,0,dim,2,MP.tpix);
            if (i!=0) { // need to draw, else try alternate corners
              // pass
            }
            else if (dim==2) {
              dtmp[0]=s.x1; dtmp[1]=s.y2;
              dtmp[2]=s.x2; dtmp[3]=s.y1;
              i = MP.viewer.rwc2pix (dtmp,0,dim,2,MP.tpix);
            }
            if (i==0) continue;
          }

          // Bug 1174 - jwb - Changed to use adjustedColors array
          if (s.getColor() != null) color = adjustedColors[ishape];

          if (s.shapeType==ShapeFile.POLYGON) {
            lin.setType(Line.SOLID);
            lin.setColor(colorBorder);
            if ((color != null) && (alpha != 0)) {
              lin.setType(Line.SOLID|Line.FILL);
              lin.setFillColor(color);
            }
          } else {
            lin.setType(Line.SOLID);
            lin.setColor(color);
          }

          if (MP.view!=MPlot.V_LATLONCONTINUOUS) {
            for (int off=j*dim; npts>0; npts-=pix.off, off+=(dim*pix.off)) {
              int n = MP.viewer.rwc2pix(s.points,off,dim,npts,pix);
              bbox = pix.getXYMinMax(bbox, n);
              if (MP.xvy) MP.drawPixels (pix.y,pix.x,n,lin);
              else        MP.drawPixels (pix.x,pix.y,n,lin);
              if (pix.save) {
                if (MP.xvy) MP.drawPixels (pix.sy,pix.sx,pix.n,lin);
                else        MP.drawPixels (pix.sx,pix.sy,pix.n,lin);
              }
            }
          }
          else {
            // Changed code to resolve scenario where
            double[] rwcView = new double[] {MP.rx1, MP.ry1, MP.rx2, MP.ry2}; // current view bounds (may be shifted beyond +180)
            double[] rwcViewShift = new double[] {MP.rx1-360, MP.rx2-360}; // potential 2nd shape region (when view is +360 standard view area)
            for (int off=j*dim; npts>0; npts-=pix.off, off+=(dim*pix.off)) {
              double[] shapeBounds = new double[] {s.x1, s.y1, s.x2, s.y2};
              if (!GeometricUtil.boxIntersect(shapeBounds, rwcView) && !GeometricUtil.xIntersect(shapeBounds, rwcViewShift)) {
                // shape is not visible .. no need to do further calculations or display
                continue;
              }
              // specialized fill based on whether the line is to be filled or not since 3.9.2
              int n = MP.viewer.rwc2pix(s.points,off,dim,npts,pix, lin.isTypeSet(Line.FILL));
              bbox = pix.getXYMinMax(bbox, n);
              if (MP.xvy) MP.drawPixels (pix.y,pix.x,n,lin);
              else        MP.drawPixels (pix.x,pix.y,n,lin);

              if (pix.save) {
                if (MP.xvy) MP.drawPixels (pix.sy,pix.sx,pix.n,lin);
                else        MP.drawPixels (pix.sx,pix.sy,pix.n,lin);
                pix.save = false;
              }
            }
          }
        }
        if (recalcPolys) {
          boolean added = spix.addPoly(bbox, ishape);
          if (added && debug > 0) System.out.println("Added: " + s.label);
        }
      }
    }
  }

  /** Adjusts the color array which will be drawn.
      Added for Bug 1174 to allow for adjusting contrast, brightness,
      and alpha (transparency) of shapes.
  */
  private void updateAdjustedColors () {
    // check for initial, otherwise will fail if opaq < 100 (default)
    if (shapes ==  null) {
      needsColorUpdate = true;
      return;
    }
    for (int i = 0; i < shapes.length; i++) {
      Color color = shapes[i].getColor(); // reset to original color for shape
      int rgb = color.getRGB();
      int x = MColor.adjustColor(rgb, contrast, brightness, alpha, false);

      adjustedColors[i] = new Color(x, true);
    }

  }

  /** Get the toggle Label region.
   *  @return the toggle Label region */
  public double getTLReg () { return tlreg; }

  /** Set the toggle Label region.
   *  @param value the toggle Label region */
  public void setTLReg (double value) { tlreg = value; }

  @SuppressWarnings("deprecation") // Since 3.9.0
  @Override
  public String getReadOut (Position mp) {
    int i,j,npts;
    boolean recalcPolys = false;
    if (label.length() == 0 || spix == null || ndata <= 0) { return null; }
    /* ---------- new code ---------- */
    if (MP.view==MPlot.V_GEODETIC) {
      if ( lat != MP.viewGeo.lat || lon != MP.viewGeo.lon) {
        lat = MP.viewGeo.lat;
        lon = MP.viewGeo.lon;
        recalcPolys = true;
      }
    }
    if (xmin != MP.rx1 || xmax != MP.rx2 ||
        ymin != MP.ry1 || ymax != MP.ry2) {
      xmin = MP.rx1;
      xmax = MP.rx2;
      ymin = MP.ry1;
      ymax = MP.ry2;
      recalcPolys = true;
    }
    if (recalcPolys) {
      if (debug>0)System.out.println("getReadOut: Creating new ShapePix, must recalc polys");
      spix = new ShapePix(MP.ix1, MP.iy1, MP.ix2, MP.iy2, ndata, debug);
      for (int ishape=0; ishape<ndata; ishape++) {
        Shape s = shapes[ishape];
        if (s.shapeType != ShapeFile.POINT && s.shapeType != ShapeFile.NULLSHAPE) {
          int bbox[] = {0,0,0,0};
          for (int ipart=0; ipart<s.numParts; ipart++) {
            j = s.parts[ipart];
            if (ipart<s.numParts-1) npts = s.parts[ipart+1]-j;
            else npts = s.numPoints-j;
            if (MP.is2D) {
              if (s.x1>=MP.rx2) continue;
              if (s.y1>=MP.ry2) continue;
              if (s.x2<=MP.rx1) continue;
              if (s.y2<=MP.ry1) continue;
            }
            else if (MP.view == MPlot.V_GEODETIC || MP.view == MPlot.V_XYZ) {
              dtmp[0]=s.x1; dtmp[1]=s.y2;
              dtmp[2]=s.x2; dtmp[3]=s.y1;
              i = MP.viewer.rwc2pix(dtmp,0,dim,2,MP.tpix);
              if (i!=0) { //need to draw, else try alternate corners
                // pass
              }
              else if (dim==2) {
                dtmp[0]=s.x1; dtmp[1]=s.y2;
                dtmp[2]=s.x2; dtmp[3]=s.y1;
                i = MP.viewer.rwc2pix(dtmp,0,dim,2,MP.tpix);
              }
              if (i==0) continue;
            }
            for (int off=j*dim; npts>0; npts-=pix.off, off+=(dim*pix.off)) {
              int n = MP.viewer.rwc2pix(s.points,off,dim,npts,pix);
              bbox = pix.getXYMinMax(bbox, ishape);
            }
          }
          boolean added = spix.addPoly(bbox, ishape);
          if (added&&debug>0) System.out.println("Added: " + s.label);
        }
      }
    }
    /* ---------- end new code ---------- */
    if (MP.view==MPlot.V_GEODETIC) {
      dtmp[0] = mp.lon;
      dtmp[1] = mp.lat;
    } else {
      dtmp[0] = mp.x;
      dtmp[1] = mp.y;
    }
    MPoint pt = new MPoint();
    MP.viewer.rwc2pix(dtmp, pt);
    if (debug > 2) {
      System.out.println("GetReadOut Point  [ " + mp + " ] = [" + pt.x + "," + pt.y + "]");
    }
    // Bug 1440 - jwb - Changed the For statement below to use the entire existing shapes
    //                  array instead of a subset, since the subset did not always pick up
    //                  certain countries (e.g. Afghanistan, Aruba, Austria)
    for (i = 0; i < shapes.length; i++) {
      Shape s = shapes[i];
      if (debug > 2) System.out.println("checking : " + s.label);
      for (int ipart=0; ipart<s.numParts; ipart++) {
        j = s.parts[ipart];
        if (ipart<s.numParts-1) npts = s.parts[ipart+1]-j;
        else npts = s.numPoints-j;
        int off=j*dim;
        int n = MP.viewer.rwc2pix(s.points,off,dim,npts,pix);
        Polygon p = new Polygon (pix.x, pix.y, n);
        if (p.contains(pt.x, pt.y)) {
          return (s.label);
        }
      }
    }
    return (null);
  }

  /** Updates the colors of each of the shapes. */
  private void updateColors() {
    if ((shapes == null) || !needsColorUpdate || (alpha == 0)) { return; }

    switch(coloringMode) {
      case COLORS_MONOCHROME:
        for (int i = 0; i < shapes.length; i++) { shapes[i].setColor(colors[1]); }
        break;
      case COLORS_MOD_5:
        for (int i = 0; i < shapes.length; i++) { shapes[i].setColor(colors[i%5+1]); }
        break;
      case COLORS_MOD_N:
        int n = colors.length - 1;
        for (int i = 0; i < shapes.length; i++) { shapes[i].setColor(colors[i%n+1]); }
        break;
      case COLORS_MIN_ADJ:
        updateAdjColors();
        break;
      case COLORS_ON_SELECT:
        for (int i = 0; i < shapes.length; i++) { shapes[i].setColor(colors[0]); }
        if ((selectedIndex >= 0) && (selectedIndex < shapes.length)) {
          shapes[selectedIndex].setColor(colors[1]);
        }
        break;
      default:
        warning("LayerShape: Unknown coloring mode given ("+coloringMode+"), using default.");
        coloringMode = COLORS_DEFAULT;
        updateColors();
    }

    // Added for Bug 1174. Sets the initial value of the draw buffer
    if (adjustedColors == null) adjustedColors = new Color[shapes.length];
    updateAdjustedColors();

    needsColorUpdate = false;
  }

  /** Updates the colors of the shapes using an approach that minimizes the number of adjacent
      shapes with common colors. This should only be called by updateColors().
   */
  @SuppressWarnings("deprecation") // Since 3.9.0
  private void updateAdjColors () {
    double proximityFactor = 1/0.1;

    /* Start by clearing the old colors. */
    for (int i = 0; i < shapes.length; i++) {
      shapes[i].setColor(null);
    }

    /* Build the list of adjacencies (i.e. neighbors) based on common points. Where two shapes have
       a common point, the later shape will be marked as adjacent to the earlier one.
     */
    final Map<Point,ArrayList<Object>> points = new HashMap<>();
    @SuppressWarnings("unchecked")
    final ArrayList<Object>[] neighbors = (ArrayList<Object>[]) new ArrayList<?>[shapes.length];

    for (int i = 0; i < shapes.length; i++) {
      for (int j = 0; (shapes[i].points != null) && (j < shapes[i].points.length); j+=2) {
        int x = (int)(proximityFactor * shapes[i].points[j+0]);
        int y = (int)(proximityFactor * shapes[i].points[j+1]);

        Point             point   = new Point(x,y);
        ArrayList<Object> matches = points.get(point);
        Number            index   = Integer.valueOf(i);

        if (matches == null) { /* A new point */
          matches = new ArrayList<>();
          points.put(point, matches);
        } else { /* A shared point - update adjacencies */
          if (neighbors[i] == null) { neighbors[i] = new ArrayList<>(); }

          for (Object match : matches) {
            if (!match.equals(index) && !neighbors[i].contains(match)) {
              neighbors[i].add(match);
            }
          }
        }
        matches.add(index);
      }
    }

    /* Working from the last shape use a DFS to color the shapes. */
    for (int i = shapes.length-1; i >= 0; i--) {
      if (shapes[i].getColor() == null) { /* If null it has not been colored yet. */
        colorAdj(neighbors, i, null);
      }
    }
  }

  /** Colors the adjacencies. This is used ONLY by updateAdjColors(), do not call directly.
      @param neighbors The table of neighbors.
      @param index     The current index.
      @return true if a good color was used (no adjacencies with same colors), false if a bad
              color was used (unable to color without adjacencies with matching colors).
   */
  private boolean colorAdj (ArrayList<Object>[] neighbors, int index, Color badColor) {
    for (int i = 1; i < colors.length; i++) {
      if (colors[i] != badColor) {
        boolean ok = true;

        if (neighbors[index] != null) {
          for (int j = 0; (j < neighbors[index].size()) && ok; j++) {
            int next = ((Number)neighbors[index].get(j)).intValue();

            if (shapes[next].getColor() == null) {
              ok = colorAdj(neighbors, next, colors[i]);
            } else {
              ok = (shapes[next].getColor() != colors[i]);
            }
          }
        }

        if (ok) {
          shapes[index].setColor(colors[i]);
          return true;
        } /* Good color found. */
      }
    }

    shapes[index].setColor(colors[colors.length-1]);
    return false; /* No good color available. */
  }

  /** Sets the mode to be used for coloring the shapes.
      @param mode The coloring mode.
   */
  public void setColoringMode(int mode) {
    if (coloringMode != mode) {
      coloringMode     = mode;
      needsColorUpdate = true;
      refresh();
    }
  }

  /** Sets the mode to be used for coloring the shapes.
      @param mode The coloring mode.
   */
  public void setColoringMode(String mode) {
    int i;

    if (mode.toUpperCase().startsWith("DEF")) {
      i = COLORS_DEFAULT;
    } else {
      i = Parser.find(coloringModes, mode, COLORS_DEFAULT);
    }

    if (i <= 0) {
      warning("LayerShape: Unknown coloring mode ("+mode+"), using default.");
      warning("LayerShape: Valid coloring modes are " + coloringModes + ".");
      i = COLORS_DEFAULT;
    }

    setColoringMode(i);
  }

  /** Gets the mode to be used for coloring the shapes.
      @return The coloring mode.
   */
  public int getColoringMode () {
    return coloringMode;
  }

  /** Set the color map to be used for for filling. */
  @Override
  public void setColorMap (Color[] colors) {
    super.setColorMap(colors);
    needsColorUpdate = true;
    refresh();
  }

  /** Set the color of polygon borders, points and more.  Any omitted
      color keys will not be affected. Valid keys are:
      <pre>
        POINT       - For the point color
        BORDER      - For the polygon borders color
      </pre>
      @param colTab Table of colors
  */
  public void setColorTable (Table colTab) {
    for (Table.Iterator ti = colTab.iterator(); ti.getNext();) {
      String key = ti.getKey();
      Object obj = ti.getValue();
      try {
        if      (key.equals("POINT"))      { line.setColor(MColor.getColor(obj)); }
        else if (key.equals("BORDER"))     { colorBorder = MColor.getColor(obj);  }
        else    { warning(" "+this+" setColorTable: Invalid color key "+key); }
        refresh();
      } catch (Exception e) {
        warning("Could not convert "+obj+" to a color"); // no stack trace necessary
      }
    }
  }

  /** Get all the current color settings as a table of Strings.  See {@link #setColorTable(Table)}
      for the list of keys in the table. To get as a table of Color objects
      See {@link #getColorTable(boolean)}
      @return the current color settings as a table of Strings
  */
  public Table getColorTable () {
    return getColorTable(false);
  }
  /** Get all the current color settings as a table.  See {@link #setColorTable(Table)}
      for the list of keys in the table.
      @param doColorObj Get the colors as objects
      @return the current color settings as a table
  */
  public Table getColorTable (boolean doColorObj) {
    Table colTab = new Table();
    if (doColorObj) {
      colTab.put("POINT",  getColor());
      colTab.put("BORDER", colorBorder);
    } else {
      colTab.put("POINT",  MColor.toString(getColor()));
      colTab.put("BORDER", MColor.toString(colorBorder));
    }
    return colTab;
  }

  /** Sets the contrast to the given percent then redraws.
      As of 2.7.0, we now allow for changing the contrast of a shape.
      @param percent Percentage of contrast (0..100)
      @since NeXtMidas 2.7.0
   */
  public void setContrast (int percent) {
    if (percent != contrast) {
      contrast = Math.max(0,Math.min(100,percent));
      updateAdjustedColors(); // apply change

      refresh();
    }
  }

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

  /** Sets the brightness to the given percent then redraws.
      As of 2.7.0, we now allow for changing the brightness of a shape.
      @param percent Percentage of brightness (0..100)
      @since NeXtMidas 2.7.0
   */
  public void setBrightness (int percent) {
    if (percent != brightness) {
      brightness = Math.max(0,Math.min(100,percent));
      updateAdjustedColors(); // apply change

      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.
      As of 2.7.0, we now allow for a true percent alpha (opaqueness),
      therefore setOpaqueness simply adjusts alpha.
      The name of this method was chosen to match
      <code>LayerToc</code>.
      @param percent Percentage of opaqueness (0..100).
      @since NeXtMidas 2.1.0
   */
  public void setOpaqueness (int percent) {
    int val = MColor.pct2alpha(percent); // convert from % (0..100) to 0..255
    setAlpha(val); // Changed for 2757
  }

  /** Gets the opaqueness of the layer, which is the percentage of alpha.
      @return Percentage of opaqueness (0..100).
      @since NeXtMidas 2.1.0
   */
  public int getOpaqueness () {
    return MColor.alpha2pct(alpha); // Changed for 2757
  }

  /** Sets the alpha either directly or called from setOpaqueness.
      As of 2.7.0, we now allow for a true percent alpha (opaqueness).
      The name of this method was chosen to match
      <code>LayerToc</code>.
      @param newAlpha New alpha value (0..255).
      @since NeXtMidas 2.7.0
   */
  public void setAlpha (int newAlpha) {
    if (newAlpha != alpha) {
      alpha = Math.max(0,Math.min(255,newAlpha));
      updateAdjustedColors(); // apply change
      refresh();
    }
  }

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

  /** Sets the index of the selected object for ColoringMode=OnSelect.
      @param index The index of the shape to color or -1 to select nothing.
      @since NeXtMidas 2.1.2
   */
  public void setSelectedIndex (int index) {
    if (selectedIndex != index) {
      selectedIndex    = index;
      needsColorUpdate = true;
      refresh();
    }
  }

  /** Gets the index of the selected object for ColoringMode=OnSelect.
      @return The index of the shape to color or -1 to select nothing.
      @since NeXtMidas 2.1.2
   */
  public int getSelectedIndex () {
    return selectedIndex;
  }

  /** Sets the selected item by value for ColoringMode=OnSelect. This method assumes
      that LABEL has been set and that the comparison is case-insensitive. Use the
      setSelectedValue(String,String,boolean) version for more options.
      @param value The value matching the label of the shape.
      @since NeXtMidas 2.1.2
   */
  public void setSelectedValue (String value) {
    setSelectedValue(label, value, true);
  }

  /** Sets the selected item by value for ColoringMode=OnSelect. This method requires
      that the accompanying .dbf file be available (see EXPLAIN PLOT for details).
      @param tag        The column name in the .dbf file to check against.
      @param value      The value to check for.
      @param ignoreCase Set this to true to do a case-insensitive check.
      @since NeXtMidas 2.1.2
   */
  public void setSelectedValue (String tag, String value, boolean ignoreCase) {
    int index = -1;

    if (tag.length() <= 0) {
      sf.M.warning("Can not select value '"+value+"', no tag/label specified.");

    } else {
      String labs[] = sf.dbf.getElementArray(tag);
      if (labs != null) {
        if (labs.length != ndata) {
          sf.M.warning ("Number of DBF fields " + labs.length + " does not match shapes " + ndata);
        } else {
          for (int i = 0; (i < ndata) && (index == -1); i++) {
            if (labs[i].equals(value) || (ignoreCase && labs[i].equalsIgnoreCase(value))) {
              index = i;
            }
          }
        }
      } else {
        sf.M.warning("Could not find label field: " + tag);
      }
    }
    setSelectedIndex(index);
  }

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

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // NESTED CLASSES
  //////////////////////////////////////////////////////////////////////////////////////////////////
  private static class ShapePix {
    int npoly;
    int xmin, xmax, ymin, ymax;
    int[][] v;
    Rectangle[] r;
    private int debug = 0;

    public ShapePix(int ix1, int iy1, int ix2, int iy2, int n, int dbg) {
      debug = dbg;
      int x = ix2 - ix1 + 1;
      int y = iy2 - iy1 + 1;
      xmin = ix1;
      xmax = ix2;
      ymin = iy1;
      ymax = iy2;

      npoly = n;
      v = new int[4][npoly + 1];
      r = new Rectangle[4];
      int k = 0;
      int dw = x / 2;
      int dh = y / 2;
      int ys = iy1;
      for (int i = 0; i < 2; i++) {
        int xs = ix1;
        for (int j = 0; j<2 && k<r.length && k<v.length; j++) {
          r[k] = new Rectangle(xs, ys, dw, dh);
          if (debug > 0) System.out.println("Creating rectangle: " + k + " = [ " + xs + "," + ys + "] "
                                           +"width = " + dw + ", height = " + dh);
          v[k][0] = 1;
          xs += dw;
          k++;
        }
        ys += dh;
      }
    }

    public boolean addPoly (int[] bbox, int idx) {
      int x1 = bbox[0];
      int y1 = bbox[1];
      int x2 = bbox[2];
      int y2 = bbox[3];
      boolean retVal = false;
      int h = Math.abs(y2 - y1 + 1);
      int w =  Math.abs(x2 - x1 + 1);
      Rectangle test = new Rectangle(x1, y1, w, h);
      if (debug > 0) {
        System.out.println("poly " + idx + " x1 = " + x1 + ", y1 = " + y1+ ","
                                         + " x2 = " + x2 + ", y2 = " + y2);
      }
      for (int i = 0; i<4 && i<v.length && i<r.length; i++) {
        if (test.intersects(r[i])) {
          v[i][v[i][0]] = idx;
          v[i][0]++;
          if (debug > 0) {
            System.out.println("adding poly " + idx + " to " + i + ", total = " + v[i][0]
                                              + " max = " + npoly);
          }
          retVal = true;
        }
      }
      return retVal;
    }

    public int[] getIndexArray (int ix, int iy) {
      if (ix < xmin || ix > xmax || iy < ymin || iy > ymax) return null;
      for (int i = 0; i < 4 && i<r.length && i<v.length; i++) {
        if (r[i].contains(ix, iy)) return v[i];
      }
      return null;
    }

    public void setDebug (int d) {
      debug = d;
    }
  }
}
