/*
  This work was originally done by Rob Hackett (R.Hackett@bom.gov.au) at
  the Australian Bureau of Meteorology in the au.gov.bom.aifs.osa.charts
  package.  Jeff McWhirter (jeffmc@unidata.ucar.edu) refactored it to
  remove dependencies on the charts package and have it be a stand-alone
  scene graph renderer for visad Displays

Copyright (C) 2017 Australian Bureau of Meteorology.

This library is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the
Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.

This library is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this library; if not, write to the Free Software Foundation, Inc.,
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/


package visad.bom;


import visad.*;

import visad.java2d.DisplayImplJ2D;
import visad.java2d.DisplayRendererJ2D;

import visad.java3d.DisplayImplJ3D;
import visad.java3d.DisplayRendererJ3D;


import java.awt.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.awt.print.PageFormat;
import java.awt.print.Printable;

import java.rmi.RemoteException;

import java.text.SimpleDateFormat;

import java.util.ArrayList;

import javax.media.j3d.*;

import javax.swing.*;

import javax.vecmath.Color3f;



/**
 * Render the non-texture components of a scene graph to a Graphics2D.
 * This does not handle any 3D aspect, rotation, etc.
 */
public class SceneGraphRenderer {

  /** default width */
  private final int DEFAULT_WIDTH = 640;

  /** default height */
  private final int DEFAULT_HEIGHT = 512;

  /** actual width */
  protected int width = DEFAULT_WIDTH;

  /** actual height */
  protected int height = DEFAULT_HEIGHT;

  /** 2D mode */
  public static final int MODE_2D = 2;

  /** 3D mode */
  public static final int MODE_3D = 3;

  // Default to 3d mode

  /** actual mode */
  private int mode = MODE_3D;

  /** pixel width */
  protected float pixelWidth = 1.0f;

  /** page color */
  private Color pageColour;

  /** frame color */
  private Color frameColour = Color.black;


  /** Hatching */
  Hatching hatching = new Hatching();



  /** flag for monochrome */
  protected boolean monochrome;

  /** flag for gradient fill */
  protected boolean gradientFill = false;

  /** flag for transparency */
  protected boolean useTransparency = true;

  /** list of colours */
  protected ArrayList colours = new ArrayList();

  /** the line path */
  private GeneralPath linePath;

  /** plot map flag */
  protected boolean plotMap = true;

  /** the viewport */
  protected AffineTransform viewPort;

  /** display side coordinate system */
  CoordinateSystem coordSys;

  /** Mouse behavior */
  MouseBehavior behavior;

  /** line thickness */
  private double lineThickness = 1;

  /** transform to screen */
  private boolean transformToScreenCoords = false;


  /**
   * Default constructor
   */
  public SceneGraphRenderer() {}


  /**
   * Get the display side CoordinateSystem
   *
   * @return  the display side CoordinateSystem or null
   */
  private CoordinateSystem getCoordinateSystem() {
    return coordSys;
  }


  /**
   * Get the viewport
   *
   * @param displayImpl   the display
   * @return the viewport
   */
  private AffineTransform getViewport(DisplayImpl displayImpl) {
    ProjectionControl pc = displayImpl.getProjectionControl();
    behavior = displayImpl.getDisplayRenderer().getMouseBehavior();

    double[] tstart = pc.getMatrix();
    double[] rotArray = new double[3];
    double[] scaleArray = new double[3];
    double[] transArray = new double[3];
    behavior.instance_unmake_matrix(rotArray, scaleArray, transArray, tstart);

    float lScaleX = (float)scaleArray[0];
    float lScaleY = (float)scaleArray[1];
    float lTransX = (float)transArray[0];
    float lTransY = (float)transArray[1];


    // Get the panning/scanning of the location
    // Convert the 3D viewport from the Location, into a 2D
    // Affine transform. 
    // get translation, then append scale NOT other way round
    viewPort = AffineTransform.getTranslateInstance(lTransX, lTransY);
    AffineTransform scale = AffineTransform.getScaleInstance(lScaleX,
                              lScaleY);
    viewPort.concatenate(scale);


    AffineTransform rot =
      AffineTransform.getRotateInstance(Math.toRadians(-rotArray[2]));
    viewPort.concatenate(rot);

    // Create another transform which matches the viewport
    // coordinates to the device coordinates
    float deviceScaleX = (float)width / 2.0f;
    float deviceTransX = width / 2.0f;
    float deviceTransY = height / 2.0f;
    AffineTransform deviceTrans =
      AffineTransform.getTranslateInstance(deviceTransX, deviceTransY);
    AffineTransform deviceScale =
      AffineTransform.getScaleInstance(deviceScaleX, -deviceScaleX);
    deviceTrans.concatenate(deviceScale);

    // Join the two together so that coordinates in viewport space
    // line up with the output coordinates
    viewPort.preConcatenate(deviceTrans);



    return viewPort;
  }



  /**
   * Return an array of Longitudes/Latitudes corresponding to the
   * pixels of the chart. Based on the Charts CoordinateSystem and
   * ViewPort.
   *      In the case of an A0 chart, the number of pixels may be
   * huge, so supply an X and Y size allowing a smaller subset to be used
   * if necessary
   * The purpose of this method is to allow an image to be
   * interpolated onto the chart as an overlay (eg. sat image)
   * @param xSize The size of the target X dimension
   * @param ySize The size of the target Y dimension
   * @return An array of Longitudes/Latitudes corresponding to the
   * pixels of the chart, sampled to match the target x/y size.
   * Based on the Charts CoordinateSystem and ViewPort
   */
  public float[][] getLonLatSamples(int xSize, int ySize) {
    float xScale = (float)width / (float)xSize;
    float yScale = (float)height / (float)ySize;
    CoordinateSystem coordSys = getCoordinateSystem();
    float[][] lonLatSamples = null;
    try {
      AffineTransform inverse = viewPort.createInverse();
      // Get the locations of each pixel of the chart
      float[] pixelSamples = getPixelSamples(xScale, yScale);
      // Normalise these using the inverse of the ViewPort
      float[] normSamples = new float[xSize * ySize * 2];
      inverse.transform(pixelSamples, 0, normSamples, 0, xSize * ySize);

      // Convert normal coordinates to lons/lats, using the
      // chart coordinate system
      float[][] xys = new float[3][normSamples.length / 2];
      for (int i = 0; i < normSamples.length / 2; i++) {
        xys[0][i] = normSamples[i * 2];
        xys[1][i] = normSamples[i * 2 + 1];
        xys[2][i] = 0.0f;
      }

      float[][] lonLats3d = coordSys.fromReference(xys);
      // Finally, filter out the extraneous 3rd dimension
      float[][] lonLats2D = {
        lonLats3d[1], lonLats3d[0]
      };
      lonLatSamples = lonLats2D;
    }
    catch (NoninvertibleTransformException e) {
      System.err.println("Chart.getLonLatSamples: " + e);
    }
    catch (VisADException e) {
      System.err.println("Chart.getLonLatSamples: " + e);
    }
    return lonLatSamples;
  }

  /**
   * Get the x/y coordinate of every pixel in the chart
   *
   * @param xScale the x scale
   * @param yScale the y scale
   * @return
   */
  private float[] getPixelSamples(float xScale, float yScale) {
    int xSize = (int)((float)width / xScale);
    int ySize = (int)((float)height / yScale);
    float[] samples = new float[xSize * ySize * 2];
    int cnt = 0;
    for (int i = 0; i < xSize; i++) {
      for (int j = 0; j < ySize; j++) {
        samples[cnt] = i * xScale;
        samples[cnt + 1] = j * yScale;
        cnt += 2;
      }
    }

    return samples;
  }


  /**
   * Get a list of the colours used in the chart
   * This may be necessary if the chart is being plotted to a medium
   * with limited colours, eg. 8 bit PNG. The antialiasing and gradient
   * fill used in some charts can easily grab all the available colours
   * By taking the ones used specifically by the chart, the important
   * colours can be reserved in the colour table ensuring that any
   * colour loss has only minimal cosmetic effect
   * @return The colours used by this chart
   */
  public Color[] getColours() {
    Color[] colourArr = new Color[colours.size()];
    colours.toArray(colourArr);

    return colourArr;
  }

  /**
   * Get the line width
   *
   * @return the line width
   */
  public float getLineWidth() {
    return (float)lineThickness;
  }

  /**
   * Create a gradient fill. If this is set with graphics.setPaint()
   * then any shapes which are filled will have a slight gradient
   * from darker to lighter along the line specified by x1, y1 to x2, y2
   * @param colour The colour of the middle point of the gradient
   * @param x1 The X coordinate of the point where the darkest part of
   *  the gradient will be
   * @param y1 The Y coordinate of the point where the darkest part of
   *  the gradient will be
   * @param x2 The X coordinate of the point where the lightest part of
   *  the gradient will be
   * @param y2 The Y coordinate of the point where the lightest part of
   *  the gradient will be
   * @return
   */
  private GradientPaint makeGradient(Color colour, float x1, float y1,
                                     float x2, float y2) {
    float[] rgb = new float[4];
    colour.getRGBColorComponents(rgb);

    // Define the difference between the lightest and darkest
    // colours
    float diff = 0.05f;
    float[] darkRGB = new float[4];
    darkRGB[0] = rgb[0] * (1.0f - diff);
    darkRGB[1] = rgb[1] * (1.0f - diff);
    darkRGB[2] = rgb[2] * (1.0f - diff);
    darkRGB[3] = rgb[3];
    float[] lightRGB = new float[4];
    lightRGB[0] = rgb[0] * (1.0f + diff);
    lightRGB[1] = rgb[1] * (1.0f + diff);
    lightRGB[2] = rgb[2] * (1.0f + diff);
    lightRGB[3] = rgb[3];
    for (int i = 0; i < 4; i++) {
      if (lightRGB[i] > 1.0) {
        lightRGB[i] = 1.0f;
      }
    }
    Color dark = new Color(darkRGB[0], darkRGB[1], darkRGB[2]);
    Color light = new Color(lightRGB[0], lightRGB[1], lightRGB[2]);
    GradientPaint gradient = new GradientPaint(x1, y1, dark, x2, y2, light);

    return gradient;
  }



  /**
   * Render the display to the graphics
   *
   * @param graphics  the graphics object
   * @param display   the display to render
   */
  private void render(Graphics2D graphics, DisplayImpl display) {
    copyVisadDisplay(display, graphics);
  }


  /**
   * Implements the Plottable interface. This will plot the chart
   * to a vector graphics file
   * Don't use this method directly. It is called by the Plotter class
   *
   * @param graphics The java2d object used by the third party graphics
   * library to render the graphics file
   * @param display the display to render
   * @param cs the display side coordinate system
   * @param width   the width of the output
   * @param height   the height of the output
   */
  public void plot(Graphics2D graphics, DisplayImpl display,
                   CoordinateSystem cs, int width, int height) {

    this.width = width;
    this.height = height;

    coordSys = cs;
    viewPort = getViewport(display);

    /*jeffmc: We don't do this for now
    // If a pre rendered background has been supplied
    if ( !plotMap) {
        // Add this to the start of the SVG
        graphics.setColor(Color.BLACK);
        //            graphics.setColor(Color.WHITE);
        graphics.fillRect(0, 0, width, height);
    } else {
        // Otherwise
        if (gradientFill) {
            GradientPaint oceanGradient = makeGradient(pageColour, width,
                                              height, 0, 0);
            graphics.setPaint(oceanGradient);
        } else {
            graphics.setColor(pageColour);
        }
        graphics.fillRect(0, 0, width, height);
        }*/

    int lineThick = (int)lineThickness;
    graphics.setClip(0, 0, width, height);

    // Render the chart
    render(graphics, display);

    // Draw a frame around the page
    /*
    drawEdge((Graphics2D) graphics, width, height,
             (float) lineThickness * 2);
    ((Graphics2D) graphics).setColor(frameColour);
    ((Graphics2D) graphics).setStroke(getStroke(lineThick));
    */
    colours.add(frameColour);
  }

  /**
   * Print the display
   *
   * @param graphics  Graphics to print to
   * @param pageFormat the page format
   * @param pageIndex  the page index
   * @param display the display to render
   * @param cs the display side coordinate system
   *
   * @return  flag for success
   */
  public int print(Graphics graphics, PageFormat pageFormat, int pageIndex,
                   DisplayImpl display, CoordinateSystem cs) {

    coordSys = cs;
    viewPort = getViewport(display);
    // Charts are ALL only 1 page
    // You MUST do this, otherwise it prints infinite copies
    // which can be a bad thing
    if (pageIndex > 0) {
      return Printable.NO_SUCH_PAGE;
    }

    // DO NOT use trasparency if chart is to be printed
    // It causes the image to be rasterized which takes
    // up lots of memory
    useTransparency = false;

    // Assuming a 2d graphics ...
    if (graphics instanceof Graphics2D) {
      Graphics2D graphics2D = (Graphics2D)graphics;
      // Scale the chart to the page format
      float scale = scaleToPage(pageFormat, graphics2D, width, height);

      // Reduce the line thinkness to take advantage of the extra
      // resolution of the printer
      lineThickness = 1.0 / scale;

      graphics2D.setColor(pageColour);
      if (plotMap) {
        graphics2D.fillRect(0, 0, width, height);
      }
      graphics2D.setClip(0, 0, width, height);

      // Render the chart
      render(graphics2D, display);

      boolean transparent = !monochrome && useTransparency;
      float legendScale = (float)lineThickness * 4;

      drawEdge(graphics2D, width, height, (float)lineThickness * 4);

      colours.add(frameColour);
    }
    else {
      System.err.println("Wrong graphics type!" + " How did THAT happen?");
    }

    return Printable.PAGE_EXISTS;
  }

  /**
   * Draw a frame around the page
   * @param graphics
   * @param width
   * @param height
   * @param thickness
   */
  private void drawEdge(Graphics2D graphics, int width, int height,
                        float thickness) {
    graphics.setColor(frameColour);
    graphics.setStroke(getStroke(thickness));

    float top = 0;
    float bottom = (float)height;
    float left = 0;
    float right = (float)width;

    // Cant use draw rect because we need float parameters
    GeneralPath linePath = new GeneralPath();
    linePath.moveTo(left, top);
    linePath.lineTo(right, top);
    linePath.lineTo(right, bottom);
    linePath.lineTo(left, bottom);
    linePath.lineTo(left, top);

    graphics.draw(linePath);
  }

  /**
   * Scale the chart so that it will fit onto the page
   * @param pageFormat The specifications of the page
   * @param graphics The graphics context
   * @param xSize The width of the chart
   * @param ySize The height of the chart
   * @return The amount that the chart has to be rescaled to match the
   * resolution of the printer
   */
  private float scaleToPage(PageFormat pageFormat, Graphics2D graphics,
                            int xSize, int ySize) {
    // Assume printers have a DPI of 300
    // TODO Can this be inferred from the PrintService?
    final int DPI = 300;

    // Find out the size of the imageable part of the paper
    // (in units of 1/72 inches)
    double pageWidth72 = pageFormat.getImageableWidth();
    double pageHeight72 = pageFormat.getImageableHeight();

    // Scale the chart to fit this
    double scaleX = pageWidth72 / xSize;
    double scaleY = pageHeight72 / ySize;
    // Choose the smallest of the 2 dimension scales 
    // Ensures that both dimensions will fit
    double scale = scaleX;
    if (scaleY < scaleX) {
      scale = scaleY;
    }

    // Translate past the non-imageable bit
    double transX = pageFormat.getImageableX() / scale;
    double transY = pageFormat.getImageableY() / scale;

    // Stretch the chart to match the dimensions of the paper
    graphics.scale(scale, scale);
    graphics.translate(transX, transY);

    // Calculate how small a pixel is relative to the basic
    // unit of size
    // There are 300 pixels per inch
    // One unit of size is 1/72 inch
    // Dots per unit tells us how much to shrink the line width down
    // to make it use a single dot
    float dotsPerUnit = (DPI / 72);

    return dotsPerUnit * (float)scale;
  }

  /**
   * Draw a reprojected shape
   *
   * @param vertices  vertices
   * @param colour    default color
   * @param width     line width
   * @param graphics  the graphics
   */
  public void drawShapeReprojected(float[][] vertices, Color colour,
                                   float width, Graphics2D graphics) {
    drawShapeReprojected(vertices, colour, width, 0, graphics);
  }

  /**
   * Draw a reprojected shape
   *
   * @param vertices  vertices
   * @param colour    default color
   * @param width     line width
   * @param dashStyle line dash style
   * @param graphics  the graphics
   */
  public void drawShapeReprojected(float[][] vertices, Color colour,
                                   float width, int dashStyle,
                                   Graphics2D graphics) {
    drawShapeReprojected(vertices, colour, width, dashStyle, graphics, false);
  }


  /**
   * Draw a reprojected shape
   *
   * @param vertices  vertices
   * @param colour    default color
   * @param width     line width
   * @param dashStyle line dash style
   * @param graphics  the graphics
   * @param isScreen  vertices are in screen coordinates
   */
  public void drawShapeReprojected(float[][] vertices, Color colour,
                                   float width, int dashStyle,
                                   Graphics2D graphics, boolean isScreen) {

    graphics.setColor(colour);
    /*
    if (dashed) {
        float[] dash = { 2.0f * pixelWidth, 6.0f * pixelWidth };
        graphics.setStroke(getStroke(width, dash));
    } else {
        graphics.setStroke(getStroke(width));
    }
    */
    graphics.setStroke(
      getStroke(width, getStrokeDash(dashStyle, pixelWidth)));


    GeneralPath line = new GeneralPath();
    line.moveTo(vertices[0][0], vertices[1][0]);
    for (int j = 1; j < vertices[0].length; j++) {
      line.lineTo(vertices[0][j], vertices[1][j]);
    }
    if (!isScreen) line.transform(viewPort);
    line = clip(line);
    graphics.setColor(colour);
    //TODO: the colour have a 0 alpha
    /*
    graphics.setColor(
      new Color(colour.getRed(), colour.getGreen(), colour.getBlue()));
      */
    graphics.draw(line);
  }

  /**
   * Draw the outline of a shape onto the chart
   * @param vertices The vertices of the shape. A 2-Dimensional array.
   * The first dimension contains the longitude values of the shape
   * The second dimension contains the latitude values of the shape
   * @param colour The colour of the shape
   * @param width The thickness of the outline
   * @param dashStyle Set to 0 if the outline is to be drawn as dashes
   * @param graphics The java2d graphics object supplied by the
   * plotting/printing medium
   * @throws VisADException
   */
  public void drawShape(float[][] vertices, Color colour, float width,
                        int dashStyle, Graphics2D graphics)
          throws VisADException {
    // Get the Coordinate System of the current location
    CoordinateSystem coordSys = getCoordinateSystem();

    // Filter out shapes which straddle the discontinuity of
    // a mercator projection. 
    boolean crosses = crossesDiscontinuity(coordSys, vertices);
    if (!crosses) {
      // Reproject the coordinates to the native coordinate system
      // eg. Longitude, Latitude -> X, Y
      float[][] reprojected = coordSys.toReference(vertices);

      drawShapeReprojected(reprojected, colour, width, dashStyle, graphics);
    }
  }

  /**
   * Draw the outline of a shape onto the chart
   * @param vertices The vertices of the shape. A 2-Dimensional array.
   * The first dimension contains the longitude values of the shape
   * The second dimension contains the latitude values of the shape
   * @param colour The colour of the shape
   * @param width The thickness of the outline
   * @param graphics The java2d graphics object supplied by the
   * plotting/printing medium
   * @throws VisADException
   */
  public void drawShape(float[][] vertices, Color colour, float width,
                        Graphics2D graphics)
          throws VisADException {
    drawShape(vertices, colour, width, 0, graphics);
  }

  /**
   * Fill a reprojected shape
   *
   * @param vertices  the vertices of the shape
   * @param colour    the color
   * @param graphics  the graphics
   */
  public void fillShapeReprojected(float[][] vertices, Color colour,
                                   Graphics2D graphics) {
    fillShapeReprojected(vertices, colour, graphics, false);
  }

  /**
   * Fill a reprojected shape
   *
   * @param vertices  the vertices of the shape
   * @param colour    the color
   * @param graphics  the graphics
   * @param isScreen  true if vertices are in screen (AWT) coordinates
   */
  public void fillShapeReprojected(float[][] vertices, Color colour,
                                   Graphics2D graphics, boolean isScreen) {
    if (gradientFill) {
      GradientPaint gradient = makeGradient(colour, 0, 0, width, height);
      graphics.setPaint(gradient);
    }
    else {
      graphics.setColor(colour);
    }
    GeneralPath shape = new GeneralPath();
    if (vertices[0].length > 0) {
      shape.moveTo(vertices[0][0], vertices[1][0]);
      for (int j = 1; j < vertices[0].length; j++) {
        shape.lineTo(vertices[0][j], vertices[1][j]);
      }
      if (!isScreen) shape.transform(viewPort);
      shape = clip(shape);
      graphics.fill(shape);
    }
  }

  /**
   * Fill a shape onto the chart
   * @param vertices  The vertices of the shape. A 2-Dimensional array.
   * The first dimension contains the longitude values of the shape
   * The second dimension contains the latitude values of the shape
   * @param colour The colour of the shape
   * @param graphics The java2d graphics object supplied by the
   * plotting/printing medium
   * @throws VisADException
   */
  public void fillShape(float[][] vertices, Color colour, Graphics2D graphics)
          throws VisADException {
    // Reproject the lats/lons into device coordinates
    CoordinateSystem coordSys = getCoordinateSystem();

    // Filter out shapes which straddle the discontinuity of
    // a mercator projection.               
    boolean crosses = crossesDiscontinuity(coordSys, vertices);
    if (!crosses) {
      float[][] reprojected = coordSys.toReference(vertices);

      // Fill the shape in device coordinates
      fillShapeReprojected(reprojected, colour, graphics);
    }
  }

  /**
   *  Test a polygon to see if it crosses the discontinuity of a
   *  Mercator projection. Shapes that do this, streak across the entire
   *  width of the chart and need to be filtered out
   * @param coordSys The CoordinateSystem of the chart
   * @param vertices The vertices of the polygon being tested
   * @return true if the shape crosses the discontinuity of a
   *  Mercator projection
   */
  private boolean crossesDiscontinuity(CoordinateSystem coordSys,
                                       float[][] vertices) {
    boolean crosses = false;
    /*TODO:
    if (coordSys instanceof visad.earthmap.MercatorCoordinateSystem) {
        visad.earthmap.MercatorCoordinateSystem merc =
            (visad.earthmap.MercatorCoordinateSystem) coordSys;
        double  disco    = merc.getCentreLongitude() + 180.0;

        float[] lonRange = range(vertices[0]);
        if ((lonRange[0] < disco) && (lonRange[1] > disco)) {
            return true;
        }
        }*/

    return crosses;
  }

  /**
   * Get the range of an array of floats
   * @param array
   * @return
   */
  private float[] range(float[] array) {
    int numPoints = array.length;

    if (numPoints <= 1) {
      float[] range = {array[0], array[0]};
      return range;
    }
    float[] clone = (float[])array.clone();
    java.util.Arrays.sort(clone);

    float[] range = new float[2];
    range[0] = clone[0];
    range[1] = clone[clone.length - 1];

    return range;
  }

  /**
   * Fill a reprojected shape from a texture
   *
   * @param vertices  the shape vertices
   * @param texture The hatching texture to fill the shape with
   * can be Hatching.DIAGONAL1, Hatching.DIAGONAL2,
   * Hatching.DIAGONAL_BOTH, Hatching.HORIZONTAL = 3, Hatching.VERTICAL
   * or Hatching.SQUARE
   * @param graphics The java2d graphics object supplied by the
   * plotting/printing medium
   */
  public void fillShapeReprojected(float[][] vertices, int texture,
                                   Graphics2D graphics) {
    fillShapeReprojected(vertices, texture, graphics, false);
  }

  /**
   * Fill a reprojected shape from a texture
   *
   * @param vertices  the shape vertices
   * @param texture The hatching texture to fill the shape with
   * can be Hatching.DIAGONAL1, Hatching.DIAGONAL2,
   * Hatching.DIAGONAL_BOTH, Hatching.HORIZONTAL = 3, Hatching.VERTICAL
   * or Hatching.SQUARE
   * @param graphics The java2d graphics object supplied by the
   * plotting/printing medium
   * @param isScreen  true if vertices are in screen (AWT) coordinates
   */
  public void fillShapeReprojected(float[][] vertices, int texture,
                                   Graphics2D graphics, boolean isScreen) {
    BufferedImage fillTexture = hatching.getPattern(texture);
    Rectangle anchor = new Rectangle(30, 30);
    TexturePaint hatching = new TexturePaint(fillTexture, anchor);
    graphics.setPaint(hatching);

    GeneralPath shape = new GeneralPath();
    shape.moveTo(vertices[0][0], vertices[1][0]);
    for (int j = 1; j < vertices[0].length; j++) {
      shape.lineTo(vertices[0][j], vertices[1][j]);
    }
    graphics.setPaint(hatching);
    if (!isScreen) shape.transform(viewPort);
    shape = clip(shape);
    graphics.fill(shape);
  }

  /**
   * Fill a shape onto the chart
   * @param data  The vertices of the shape. A 2-Dimensional array.
   * The first dimension contains the longitude values of the shape
   * The second dimension contains the latitude values of the shape
   * @param texture The hatching texture to fill the shape with
   * can be Hatching.DIAGONAL1, Hatching.DIAGONAL2,
   * Hatching.DIAGONAL_BOTH, Hatching.HORIZONTAL = 3, Hatching.VERTICAL
   * or Hatching.SQUARE
   * @param graphics The java2d graphics object supplied by the
   * plotting/printing medium
   * @throws VisADException  problem transforming from lat/lon
   *                         to display space
   */
  public void fillShape(float[][] data, int texture, Graphics2D graphics)
          throws VisADException {
    CoordinateSystem coordSys = getCoordinateSystem();
    float[][] reprojected = coordSys.toReference(data);

    fillShapeReprojected(reprojected, texture, graphics);
  }

  /**
   * Draw text onto the chart
   * @param text The text to draw onto the chart
   * @param font The font of the text
   * @param colour The colour of the text
   * @param x The x position of the text (longitude)
   * @param y The y position of the text (latitude)
   * @param graphics The java2d graphics object supplied by the
   * plotting/printing medium
   * @throws VisADException
   */
  public void drawString(String text, Font font, Color colour, float x,
                         float y, Graphics2D graphics)
          throws VisADException {
    if (text.length() < 1) {
      return;
    }

    graphics.setColor(colour);

    // Scale the font to match the pixel size of the display
    int size = font.getSize();
    Font scaledFont = font.deriveFont(size * pixelWidth);

    double[][] coords = new double[3][1];
    coords[0][0] = x;
    coords[1][0] = y;
    coords[2][0] = 0.0f;

    graphics.setFont(scaledFont);

    // Reproject the user coordinates to Normal X, Y coordinates
    CoordinateSystem coordSys = getCoordinateSystem();
    double[][] reprojected = coordSys.toReference(coords);

    double[] normCoords = {reprojected[0][0], reprojected[1][0]};
    float[] devCoords = new float[2];
    viewPort.transform(normCoords, 0, devCoords, 0, 1);

    if ((devCoords[0] > 0) && (devCoords[0] < width) && (devCoords[1] > 0) &&
        (devCoords[1] < height)) {
      graphics.drawString(text, devCoords[0], devCoords[1]);
    }

  }

  /**
   * Extract the Geometries from the Visad display and render them to the
   * Graphics2D
   *
   * @param display  the display to copy
   * @param graphics the graphics to render to
   */
  private void copyVisadDisplay(DisplayImpl display, Graphics2D graphics) {
    int mode = -1;

    if (display instanceof DisplayImplJ3D) {
      mode = MODE_3D;
    }
    else if (display instanceof DisplayImplJ2D) {
      mode = MODE_2D;
    }
    DisplayRenderer displayRenderer = null;
    Object root = null;
    // If the display is 2D
    if (mode == MODE_2D) {
      // Get the root VisADGroup from the display renderer
      displayRenderer = (DisplayRendererJ2D)display.getDisplayRenderer();
      root = (VisADGroup)((DisplayRendererJ2D)displayRenderer).getRoot();
    }
    else {
      // Otherwise get the Java3d Group
      displayRenderer = (DisplayRendererJ3D)display.getDisplayRenderer();
      root = (Group)((DisplayRendererJ3D)displayRenderer).getRoot();
    }
    /*
    try {
        displayRenderer.setBoxOn(false);
        displayRenderer.setScaleOn(false);
    } catch (VisADException e) {
        System.err.println("VectorPlotter.generate: " + e);
    } catch (RemoteException e) {
        System.err.println("VectorPlotter.generate: " + e);
    }
    */

    // Recurse the plotDisplay, converting all plotted
    // objects to vectors rendered through a Graphics2D object
    if (mode == MODE_2D) {
      copyGroup((VisADGroup)root, graphics);
    }
    else if (mode == MODE_3D) {
      copyGroup((Group)root, graphics);
    }
  }

  /**
   * Recursively process each VisAD group in the display, If the group is
   * a geometry array, stop recursing and plot it
   *
   * @param root - The root VisADGroup from which to recurse
   * @param graphics  the graphics to render to
   */
  private void copyGroup(VisADGroup root, Graphics2D graphics) {
    // Loop over eah of the VisAD groups children
    for (int i = 0; i < root.numChildren(); i++) {
      // Get the next Child
      VisADSceneGraphObject child = root.getChild(i);

      // If this child is a VisADAppearance
      if (child instanceof VisADAppearance) {
        VisADAppearance appearance = (VisADAppearance)child;
        // Plot it's vertices
        VisADGeometryArray geometry = appearance.array;
        Color[] colours = getColours(appearance, monochrome);
        float fsize = (geometry instanceof VisADPointArray)
                      ? appearance.pointSize
                      : appearance.lineWidth;
        float thickness = fsize / 2.0f;

        plot(geometry, colours, thickness, graphics);
      }

      // If this child is a VisADGroup
      if (child instanceof VisADGroup) {
        // Recurse this group
        copyGroup((VisADGroup)child, graphics);
      }
    }
  }

  /**
   * Recursively process each Java3d group in the display, If the group is
   * a geometry array, stop recursing and plot it
   *
   * @param root - The root Java 3D Group from which to recurse
   * @param graphics  the graphics to render to
   */
  private void copyGroup(Group root, Graphics2D graphics) {
    int numChildren = 0;
    if (root.getCapability(Group.ALLOW_CHILDREN_READ)) {
      numChildren = root.numChildren();
    }

    // Check to see which children are rendered
    int rendered = -1;
    if (root instanceof Switch) {
      rendered = ((Switch)root).getWhichChild();
      if (rendered == -1) return; // means it's not rendered? 
    }
    // Loop over each of the VisAD groups children
    for (int i = 0; i < numChildren; i++) {
      Node child = root.getChild(i);

      // Only render this node if it is Switched on
      if ((rendered >= 0) && (rendered != i)) {
        continue;
      }
      if (child instanceof Group) {
        // Todo
        // Check for Switches here to support layers?
        copyGroup((Group)child, graphics);
      }
      else if (child instanceof Shape3D) {
        Shape3D shape = (Shape3D)child;
        int numGeoms = 0;
        if (shape.getCapability(Shape3D.ALLOW_GEOMETRY_READ)) {
          numGeoms = shape.numGeometries();
        }

        Appearance appearance = shape.getAppearance();

        Color[] colours = getColours(appearance);
        float thickness = getLineThickness(appearance);
        int lineStyle = getLineStyle(appearance);
        Texture texture = appearance.getTexture();
        for (int j = 0; j < numGeoms; j++) {
          GeometryArray geom = (GeometryArray)shape.getGeometry(j);
          plot(geom, colours, thickness, texture, lineStyle, graphics);
        }
      }
      else {
        // System.err.println ("Unknown scene graph node:" + child.getClass().getName());
      }
    }
  }

  /**
   * Get the colours from the Appearance
   *
   * @param appearance the appearance
   * @param monochrome true  for monochrome
   *
   * @return  the array of colours for the Appearance
   */
  private Color[] getColours(VisADAppearance appearance, boolean monochrome) {
    Color[] colours = null;
    VisADGeometryArray geometry = appearance.array;

    // If the geometry stores it's colours ...
    if (geometry.colors != null) {
      // Get each individual colour
      int numColours = geometry.colors.length;
      int numCoords = geometry.coordinates.length;
      // Get the ratio of colors to points to distinguish RGB
      // from RGBA. This is a hack until I understand why some
      // LineArrays from a 2D display contain Alpha values
      int cr = 3;
      if (numColours != numCoords) {
        cr = numColours / (numColours - numCoords);
      }
      colours = new Color[numColours / cr];
      for (int j = 0; j < numColours; j += cr) {
        float red = 0.0f;
        float green = 0.0f;
        float blue = 0.0f;
        if (!monochrome) {
          red = byteToFloat(geometry.colors[j]);
          green = byteToFloat(geometry.colors[j + 1]);
          blue = byteToFloat(geometry.colors[j + 2]);
        }
        colours[j / cr] = new Color(red, green, blue);
      }
    }
    else {
      // Otherwise fill the array with the global colour
      float red = 0.0f;
      float green = 0.0f;
      float blue = 0.0f;
      if (!monochrome) {
        red = appearance.red;
        green = appearance.green;
        blue = appearance.blue;
      }
      colours = new Color[1];
      colours[0] = new Color(red, green, blue);
    }

    return colours;
  }

  /**
   * Get the colors from the Appearance
   *
   * @param appearance   the Appearance
   *
   * @return the colours
   */
  private Color[] getColours(Appearance appearance) {
    Color[] colours = null;
    ColoringAttributes colourAttr = appearance.getColoringAttributes();
    int colourFlag = ColoringAttributes.ALLOW_COLOR_READ;
    TransparencyAttributes transAttr = appearance.getTransparencyAttributes();
    int transMFlag = TransparencyAttributes.ALLOW_MODE_READ;
    int transVFlag = TransparencyAttributes.ALLOW_VALUE_READ;

    if ((colourAttr != null) && (colourAttr.getCapability(colourFlag))) {
      Color3f color3f = new Color3f();
      colourAttr.getColor(color3f);
      colours = new Color[1];
      Color color3 = color3f.get();
      float[] colourComps = new float[4];
      colourComps = color3.getColorComponents(colourComps);
      colourComps[3] = 1.0f;
      /*
      if (transAttr != null &&
          transAttr.getCapability(transMFlag) &&
          transAttr.getCapability(transVFlag) &&
          transAttr.getTransparencyMode() != TransparencyAttributes.NONE) {
          colourComps[3] = transAttr.getTransparency();
      }
      */
      colours[0] = new Color(colourComps[0], colourComps[1], colourComps[2],
                             colourComps[3]);
    }

    return colours;
  }

  /**
   * Get the line thickness from the Appearance
   *
   * @param appearance  the Appearance
   *
   * @return  the line thickness
   */
  private float getLineThickness(Appearance appearance) {
    float thickness = 0.0f;
    LineAttributes lineAttr = appearance.getLineAttributes();
    if (lineAttr != null && lineAttr.getCapability(LineAttributes.ALLOW_WIDTH_READ)) {
      thickness = lineAttr.getLineWidth();
    }

    return thickness;
  }

  /**
   * Get the line style from the Appearance
   *
   * @param appearance  the Appearance
   *
   * @return  the line style
   */
  private int getLineStyle(Appearance appearance) {
    LineAttributes lineAttr = appearance.getLineAttributes();
    int lineStyle = LineAttributes.PATTERN_SOLID;
    if (lineAttr != null && lineAttr.getCapability(LineAttributes.ALLOW_PATTERN_READ)) {
      lineStyle = lineAttr.getLinePattern();
    }

    return lineStyle;
  }

  /**
   * Convert an unsigned byte into a float
   *
   * @param byteVal - A number between 0 and 255
   * @return the number converted to a signed floating point number
   */
  private float byteToFloat(byte byteVal) {
    float floatVal = 0.0f;

    if (byteVal >= 0) {
      floatVal = ((float)byteVal) / 256.0f;
    }
    else {
      floatVal = ((float)(byteVal + 256)) / 256.0f;
    }

    return floatVal;
  }

  /**
   * Convert a geometry array into plottable vectors
   *
   * @param geometryArray   The definition of the shape of the object to be
   *                        plotted
   * @param colours         A list of the colours of each vertex of the object
   * @param thickness       The line thickness with which to draw the object
   * @param graphics        The graphics to plot to
   */
  private void plot(VisADGeometryArray geometryArray, Color[] colours,
                    float thickness, Graphics2D graphics) {
    // Draw a VisADPointArray
    if (geometryArray instanceof VisADPointArray) {
      VisADPointArray pointArray = (VisADPointArray)geometryArray;
      plot(pointArray, colours, thickness, graphics);
      // Draw a VisADLineStripArray
    }
    else if (geometryArray instanceof VisADLineStripArray) {
      VisADLineStripArray lineArray = (VisADLineStripArray)geometryArray;
      plot(lineArray, colours, thickness, graphics);
      // Draw a VisADLineArray
    }
    else if (geometryArray instanceof VisADLineArray) {
      VisADLineArray lineArray = (VisADLineArray)geometryArray;
      plot(lineArray, colours, thickness, graphics);
      // Draw a VisADTriangleStripArray
    }
    else if (geometryArray instanceof VisADTriangleStripArray) {
      VisADTriangleStripArray triangleArray =
        (VisADTriangleStripArray)geometryArray;
      plot(triangleArray, colours, thickness, graphics);
      // Draw a VisADIndexedTriangleStripArray
    }
    else if (geometryArray instanceof VisADIndexedTriangleStripArray) {
      VisADIndexedTriangleStripArray triangleArray =
        (VisADIndexedTriangleStripArray)geometryArray;
      plot(triangleArray, colours, thickness, graphics);
      // Draw a VisADTriangleArray
    }
    else if (geometryArray instanceof VisADTriangleArray) {
      VisADTriangleArray triangleArray = (VisADTriangleArray)geometryArray;
      plot(triangleArray, colours, thickness, graphics);
    }
    else {
      // Other geometries go here
    }
  }

  /**
   * Convert a geometry array into plottable vectors
   *
   * @param geometryArray   The definition of the shape of the object to be
   *                        plotted
   * @param colours         A list of the colours of each vertex of the object
   * @param thickness       The line thickness with which to draw the object
   * @param texture         The hatching texture
   * @param lineStyle       The line style
   * @param graphics        The graphics to plot to
   */
  private void plot(GeometryArray geometryArray, Color[] colours,
                    float thickness, Texture texture, int lineStyle,
                    Graphics2D graphics) {
    //System.err.println("plot:" + geometryArray.getClass().getName());
    if (geometryArray instanceof LineArray) {
      LineArray lineArray = (LineArray)geometryArray;
      plot(lineArray, colours, thickness, lineStyle, graphics);
    }
    else if (geometryArray instanceof TriangleArray) {
      TriangleArray triangleArray = (TriangleArray)geometryArray;
      plot(triangleArray, colours, thickness, graphics);
    }
    else if (geometryArray instanceof QuadArray) {
      QuadArray quadArray = (QuadArray)geometryArray;
      if (texture == null) {
        plot(quadArray, colours, thickness, graphics);
      }
      else {
        //Don't do textures
      }
    }
    else if (geometryArray instanceof LineStripArray) {
      LineStripArray lineStripArray = (LineStripArray)geometryArray;
      plot(lineStripArray, colours, thickness, lineStyle, graphics);
    }
    else if (geometryArray instanceof TriangleStripArray) {
      TriangleStripArray triangleArray = (TriangleStripArray)geometryArray;
      // Treat geometries with texture separately
      if (texture == null) {
        plot(triangleArray, colours, thickness, graphics);
      }
    }
    else if (geometryArray instanceof IndexedTriangleStripArray) {
      IndexedTriangleStripArray triangleArray =
        (IndexedTriangleStripArray)geometryArray;
      plot(triangleArray, colours, thickness, graphics);
    } // Draw a VisADPointArray
    else if (geometryArray instanceof PointArray) {
      PointArray pointArray = (PointArray)geometryArray;
      plot(pointArray, colours, thickness, graphics);
    }
    else {
      // Other geometries go here
      // System.err.println ("Unknown geometry:" + geometryArray.getClass().getName());
    }
  }

  /**
   * Plot a VisADPointArray into postscript format
   *
   * @param pointArray  The pointArray to be plotted
   * @param colours     The colour to plot the points
   * @param size        Size of the points
   * @param graphics    The graphics to plot to
   */
  private void plot(VisADPointArray pointArray, Color[] colours, float size,
                    Graphics2D graphics) {
    graphics.setColor(colours[0]);
    graphics.setStroke(getStroke(pixelWidth));
    float[] coordinates = pointArray.coordinates;

    float hsize = 0.5f * size;
    // Loop over each point
    for (int i = 0; i < coordinates.length / 3; i++) {
      float normalX = coordinates[i * 3];
      float normalY = coordinates[i * 3 + 1];
      //graphics.fillRect((int)normalX, (int)normalY, (int)size, (int)size);
      graphics.fill(
        new Rectangle2D.Float(normalX - hsize, normalY - hsize, size, size));
    }
  }

  /**
   * Convert a VisADLineStripArray into plottable vectors
   *
   * @param lineArray   The VisADLineStripArray
   * @param colours     The list of colors
   * @param thickness   The line thickness
   * @param graphics    The graphics
   */
  private void plot(VisADLineStripArray lineArray, Color[] colours,
                    float thickness, Graphics2D graphics) {
    graphics.setColor(colours[0]);
    graphics.setStroke(getStroke(thickness));
    float[] coordinates = lineArray.coordinates;

    // Get the sizes of all the "chunks"
    int[] vertexCounts = lineArray.stripVertexCounts;

    int base = 0;

    // Loop over each chunk
    for (int i = 0; i < vertexCounts.length; i++) {
      int numCoords = vertexCounts[i];

      if (i < colours.length) {
        graphics.setColor(colours[i]);
      }

      GeneralPath path = new GeneralPath();
      // Store the starting position of this chunk
      path.moveTo(coordinates[base], coordinates[base + 1]);

      boolean visible = false;
      float nxLast = coordinates[base];
      float nyLast = coordinates[base + 1];
      // Loop over all the points in this chunk
      for (int j = 0; j < numCoords; j++) {
        // Get the (normalised) display coordinates
        float nX = coordinates[base + j * 3];
        float nY = coordinates[base + j * 3 + 1];
        // If the line is visible, draw it
        if (visible(nxLast, nyLast, nX, nY, graphics)) {
          visible = true;
        }
        path.lineTo((float)nX, (float)nY);
        nxLast = nX;
        nyLast = nY;
      }
      if (visible) {
        // Convert display coordinates to device coords
        path.transform(viewPort);
        graphics.draw(path);
      }
      base += 3 * numCoords;

    }
  }

  /**
   * Convert a VisADLineArray into plottable vectors
   *
   * @param lineArray   The VisADLineArray
   * @param colours     The list of colors
   * @param thickness   The line thickness
   * @param graphics    The graphics
   */
  private void plot(VisADLineArray lineArray, Color[] colours,
                    float thickness, Graphics2D graphics) {
    graphics.setColor(colours[0]);
    graphics.setStroke(getStroke(thickness));
    float[] coordinates = lineArray.coordinates;
    int numCoords = lineArray.vertexCount;

    for (int j = 0; j < numCoords; j += 2) {
      if (j < colours.length) {
        graphics.setColor(colours[j]);
      }

      // Get (normalised) display coordinates
      float nX1 = coordinates[j * 3];
      float nY1 = coordinates[j * 3 + 1];
      float nX2 = coordinates[j * 3 + 3];
      float nY2 = coordinates[j * 3 + 4];

      // If the line is visible, draw it
      if (visible(nX1, nY1, nX2, nY2, graphics)) {
        GeneralPath path = new GeneralPath();
        path.moveTo(nX1, nY1);
        path.lineTo(nX2, nY2);

        // Convert them to device coords, and plot
        path.transform(viewPort);
        graphics.draw(path);
      }

    }

  }

  /**
   * Convert a VisADTriangleStripArray into plottable vectors
   *
   *
   * @param triangleArray   The VisADTriangleStripArray
   * @param colours         The list of colors at each vertex
   * @param thickness       The line thickness
   * @param graphics        The graphics
   */
  private void plot(VisADTriangleStripArray triangleArray, Color[] colours,
                    float thickness, Graphics2D graphics) {
    graphics.setColor(colours[0]);
    graphics.setStroke(getStroke(thickness));
    float[] coordinates = triangleArray.coordinates;

    // Get the sizes of all the "chunks"
    int[] vertexCounts = triangleArray.stripVertexCounts;

    int base = 0;

    // Loop over each chunk
    for (int i = 0; i < vertexCounts.length; i++) {
      int numCoords = vertexCounts[i];

      // Store the starting position of this chunk
      float normalLastX = coordinates[base];
      float normalLastY = coordinates[base + 1];
      GeneralPath path = new GeneralPath();
      path.moveTo(normalLastX, normalLastY);

      // Loop over all the points in this chunk
      for (int j = 0; j < numCoords; j++) {
        float normalX = coordinates[base + j * 3];
        float normalY = coordinates[base + j * 3 + 1];
        normalLastX = normalX;
        normalLastY = normalY;
        path.lineTo(normalX, normalY);
      }
      path.closePath();
      path.transform(viewPort);
      graphics.fill(path);

      base += 3 * numCoords;

    }

  }

  /**
   * Convert a VisADTriangleArray into plottable vectors
   *
   * @param triangleArray   The VisADTriangleArray
   * @param colours         The list of colors at each vertex
   * @param thickness       The line thickness
   * @param graphics        The graphics
   */
  private void plot(VisADTriangleArray triangleArray, Color[] colours,
                    float thickness, Graphics2D graphics) {
    float[] colour = new float[4];
    if (colours != null) {
      colour[0] = ((float)colours[0].getRed()) / 255.0f;
      colour[1] = ((float)colours[0].getGreen()) / 255.0f;
      colour[2] = ((float)colours[0].getBlue()) / 255.0f;
      colour[3] = ((float)colours[0].getAlpha()) / 255.0f;
      if (!useTransparency) {
        colour[3] = 1.0f;
      }
    }

    graphics.setStroke(getStroke(thickness));

    int vertexCount = triangleArray.vertexCount;
    float[] coordinates = triangleArray.coordinates;

    for (int i = 0; i < 3 * vertexCount; i += 9) {
      // If monochrome
      if (monochrome) {
        // Set everything except white to black
        monochromatise(colour);
      }
      Color color = new Color(colour[0], colour[1], colour[2], colour[3]);
      graphics.setColor(color);
      float[][] vertices = new float[3][3];
      vertices[0][0] = coordinates[i];
      vertices[1][0] = coordinates[i + 1];
      vertices[0][1] = coordinates[i + 3];
      vertices[1][1] = coordinates[i + 4];
      vertices[0][2] = coordinates[i + 6];
      vertices[1][2] = coordinates[i + 7];
      int clockwise = clockwise(vertices[0], vertices[1]);
      if (clockwise >= 0) {
        vertices[0] = reverseDirection(vertices[0]);
        vertices[1] = reverseDirection(vertices[1]);
      }
      fillShapeReprojected(vertices, color, graphics);
    }

  }

  /**
   * Convert a VisADIndexedTriangleStripArray into plottable vectors
   *
   * @param triangleArray   The VisADIndexedTriangleStripArray
   * @param colours         The list of colors at each vertex
   * @param thickness       The line thickness
   * @param graphics        The graphics
   */
  private void plot(VisADIndexedTriangleStripArray triangleArray,
                    Color[] colours, float thickness, Graphics2D graphics) {
    graphics.setColor(colours[0]);
    graphics.setStroke(getStroke(thickness));

    float[] coordinates = triangleArray.coordinates;

    int[] indices = triangleArray.indices;
    int[] stripVertexCounts = triangleArray.stripVertexCounts;

    int base = 0;

    for (int strip = 0; strip < stripVertexCounts.length; strip++) {
      if (strip < colours.length) {
        graphics.setColor(colours[strip]);
      }

      int count = stripVertexCounts[strip];
      int index0 = indices[base];
      int index1 = indices[base + 1];

      GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
      boolean visible = false;
      for (int i = base + 2; i < base + count; i++) {
        int index2 = indices[i];

        float normalX0 = coordinates[3 * index0];
        float normalY0 = coordinates[3 * index0 + 1];
        float normalX1 = coordinates[3 * index1];
        float normalY1 = coordinates[3 * index1 + 1];
        float normalX2 = coordinates[3 * index2];
        float normalY2 = coordinates[3 * index2 + 1];

        // If any of the triangle is within the
        // area of interest, plot it
        // if (visible(xCoords, yCoords, graphics)) {
        path.moveTo(normalX0, normalY0);
        path.lineTo(normalX1, normalY1);
        path.lineTo(normalX2, normalY2);
        visible = true;
        //                              }

        index0 = index1;
        index1 = index2;
      }
      if (visible) {
        path.transform(viewPort);
        path.closePath();
        ////////                                graphics.fill(path);
      }
      base += count;
    }
  }

  /**
   * Convert a LineStripArray into plottable vectors
   *
   * @param lineArray   The LineStripArray
   * @param colours     The list of colors at each vertex
   * @param thickness   The line thickness
   * @param lineStyle   The line style
   * @param graphics    The graphics
   */
  private void plot(LineStripArray lineArray, Color[] colours,
                    float thickness, int lineStyle, Graphics2D graphics) {

    // Temporary variables for retrieving coordinates
    int vertexCount = lineArray.getVertexCount();
    boolean hasAlpha = hasAlpha(lineArray);
    boolean byRef = isByReference(lineArray);
    int nCoords = 3 * vertexCount;
    float[] coordinates = null;

    // Get the coordinates
    if (byRef) {
      coordinates = lineArray.getCoordRefFloat();
    }
    else {
      coordinates = new float[nCoords];
      lineArray.getCoordinates(0, coordinates);
    }

    coordinates = transformToScreen(coordinates);

    int numStrips = lineArray.getNumStrips();
    int[] vertexCounts = new int[numStrips];
    lineArray.getStripVertexCounts(vertexCounts);

    int base = 0;
    int baseColor = 0;

    float[] colour = new float[4];
    byte[] refColours = null;
    int numRefColours = (hasAlpha)
                        ? 4
                        : 3;
    // If a colour was supplied, use it
    if (colours != null) {
      colour[0] = ((float)colours[0].getRed()) / 255.0f;
      colour[1] = ((float)colours[0].getGreen()) / 255.0f;
      colour[2] = ((float)colours[0].getBlue()) / 255.0f;
      colour[3] = ((float)colours[0].getAlpha()) / 255.0f;
    }
    else {
      // Get the color array if by ref
      if (byRef) {
        // DisplayImplJ3D stores ref as bytes
        refColours = lineArray.getColorRefByte();
        for (int i = 0; i < numRefColours; i++) {
          colour[i] = byteToFloat(refColours[i]);
        }
      }
      else {
        lineArray.getColor(0, colour);
      }
    }
    if (!hasAlpha || !useTransparency) {
      colour[3] = 1.0f;
    }

    // If monochrome
    if (monochrome) {
      // Set everything except white to black
      monochromatise(colour);
    }
    graphics.setStroke(
      getStroke(thickness, getStrokeDash(lineStyle, pixelWidth)));

    Color lastColor = new Color(colour[0], colour[1], colour[2], colour[3]);

    // Loop over each chunk
    for (int i = 0; i < vertexCounts.length; i++) {
      int numCoords = vertexCounts[i];
      linePath = new GeneralPath();
      graphics.setColor(lastColor);
      //VisAD stores strips as one complete section so we have to draw each
      //segment
      for (int seg = 0; seg < numCoords-1; seg++) {
        // Attempt to get the color from the geometry
        if (colours == null) {
          // Get the color array
          if (refColours != null) {
            for (int j = 0; j < numRefColours; j++) {
              colour[j] = byteToFloat(refColours[baseColor + j]);
            }
          }
          else {
            lineArray.getColor(base, colour);
          }
        }
        if (!useTransparency || !hasAlpha) {
          colour[3] = 1.0f;
        }
        // If monochrome
        if (monochrome) {
          // Set everything except white to black
          monochromatise(colour);
        }
        Color color = new Color(colour[0], colour[1], colour[2], colour[3]);
        // Draw lines of one colour in a sigle GeneralPath
        // This makes the resulting image MUCH more slick
        // when plotting many small lines (eg. observations)
        if (!color.equals(lastColor)) {
          graphics.setColor(lastColor);
          lastColor = color;
          if (!transformToScreenCoords) linePath.transform(viewPort);
          //linePath = clip(linePath);
          graphics.draw(linePath);
          linePath = new GeneralPath();
        }

        // Get the (normalised) display coordinates
        float nX1 = coordinates[base];
        float nY1 = coordinates[base + 1];
        float nX2 = coordinates[base + 3];
        float nY2 = coordinates[base + 4];
        if (visible(nX1, nY1, nX2, nY2, graphics, transformToScreenCoords)) {
          linePath.moveTo(nX1, nY1);
          linePath.lineTo(nX2, nY2);
        }
        base += 3;
        baseColor += numRefColours;
      }
      // Translate them to device coordinates and plot
      if (!transformToScreenCoords) linePath.transform(viewPort);
      //linePath = clip(linePath);
      graphics.draw(linePath);
      base += 3;
      baseColor += numRefColours;
    }
  }

  /**
   * Convert a LineArray into plottable vectors
   *
   * @param lineArray   The LineStripArray
   * @param colours     The list of colors at each vertex
   * @param thickness   The line thickness
   * @param lineStyle   The line style
   * @param graphics    The graphics
   */
  private void plot(LineArray lineArray, Color[] colours, float thickness,
                    int lineStyle, Graphics2D graphics) {

    boolean hasAlpha = hasAlpha(lineArray);
    boolean byRef = isByReference(lineArray);
    float[] colour = new float[4];
    byte[] refColours = null;
    int numRefColours = (hasAlpha)
                        ? 4
                        : 3;
    int baseColor = 0;
    // If a colour was supplied, use it
    if (colours != null) {
      colour[0] = ((float)colours[0].getRed()) / 255.0f;
      colour[1] = ((float)colours[0].getGreen()) / 255.0f;
      colour[2] = ((float)colours[0].getBlue()) / 255.0f;
      colour[3] = ((float)colours[0].getAlpha()) / 255.0f;
    }
    else {
      //lineArray.getColor(0, colour);
      if (byRef) {
        // DisplayImplJ3D stores ref as bytes
        refColours = lineArray.getColorRefByte();
        for (int i = 0; i < numRefColours; i++) {
          colour[i] = byteToFloat(refColours[i]);
        }
      }
      else {
        lineArray.getColor(0, colour);
      }
    }
    if (!useTransparency || !hasAlpha) {
      colour[3] = 1.0f;
    }

    // If monochrome
    if (monochrome) {
      // Set everything except white to black
      monochromatise(colour);
    }
    /*
    if (lineStyle == LineAttributes.PATTERN_DASH) {
        // float[] dash = { 24.0f * pixelWidth, 8.0f * pixelWidth };
        float size  = (float) Math.sqrt(width * width + height * height);
        float scale = size / 3000.0f;
        // System.err.println(size + "/" + scale + "Width: " + width + "/"
        //                    + height);
        float[] dash = { 18.0f * scale, 6.0f * scale };
        graphics.setStroke(getStroke(thickness * .5f, dash));
    } else {
        graphics.setStroke(getStroke(thickness * .5f));
    }
    */
    //graphics.setStroke(
    //  getStroke(thickness * .5f, getStrokeDash(lineStyle, pixelWidth)));
    graphics.setStroke(
      getStroke(thickness, getStrokeDash(lineStyle, pixelWidth)));

    // Temporary variables for retrieving coordinates
    int vertexCount = lineArray.getVertexCount();
    int nCoords = 3 * vertexCount;
    float[] coordinates = null;

    // Get the coordinates
    if (byRef) {
      coordinates = lineArray.getCoordRefFloat();
    }
    else {
      coordinates = new float[nCoords];
      lineArray.getCoordinates(0, coordinates);
    }

    /*
    float[] coordinates = new float[vertexCount * 3];
    lineArray.getCoordinates(0, coordinates);
    */
    coordinates = transformToScreen(coordinates);

    Color lastColor = new Color(colour[0], colour[1], colour[2], colour[3]);
    linePath = new GeneralPath();
    graphics.setColor(lastColor);
    for (int j = 0; j < vertexCount; j += 2) {
      if (colours == null) {
        //lineArray.getColor(i, colour);
        if (refColours != null) { // means we are BY_REFERENCE
          for (int i = 0; i < numRefColours; i++) {
            colour[i] = byteToFloat(refColours[baseColor + i]);
          }
        }
        else {
          lineArray.getColor(j, colour);
        }
      }
      if (!useTransparency || !hasAlpha) {
        colour[3] = 1.0f;
      }
      // If monochrome
      if (monochrome) {
        // Set everything except white to black
        monochromatise(colour);
      }

      Color color = new Color(colour[0], colour[1], colour[2], colour[3]);
      // Draw lines of one colour in a sigle GeneralPath
      // This makes the resulting image MUCH more slick
      // when plotting many small lines (eg. observations)
      if (!color.equals(lastColor)) {
        graphics.setColor(lastColor);
        lastColor = color;
        if (!transformToScreenCoords) linePath.transform(viewPort);
        //linePath = clip(linePath);
        graphics.draw(linePath);
        linePath = new GeneralPath();
      }

      // Get the (normalised) display coordinates
      float nX1 = coordinates[j * 3];
      float nY1 = coordinates[j * 3 + 1];
      float nX2 = coordinates[j * 3 + 3];
      float nY2 = coordinates[j * 3 + 4];
      if (!visible(nX1, nY1, nX2, nY2, graphics, transformToScreenCoords)) {
        continue;
      }
      linePath.moveTo(nX1, nY1);
      linePath.lineTo(nX2, nY2);
      baseColor += numRefColours * 2;
    }
    // Translate them to device coordinates and plot
    if (!transformToScreenCoords) linePath.transform(viewPort);
    //linePath = clip(linePath);
    graphics.draw(linePath);

  }

  /**
   * Convert a TriangleStripArray into plottable vectors
   *
   * @param triangleArray   The TriangleStripArray
   * @param colours     The list of colors at each vertex
   * @param thickness   The line thickness
   * @param graphics    The graphics
   */
  private void plot(TriangleStripArray triangleArray, Color[] colours,
                    float thickness, Graphics2D graphics) {

    int vertexFormat = triangleArray.getVertexFormat();
    if ((vertexFormat & GeometryArray.TEXTURE_COORDINATE_2)
        == GeometryArray.TEXTURE_COORDINATE_2) {
      return;
    }
    boolean hasAlpha = hasAlpha(triangleArray);
    boolean byRef = isByReference(triangleArray);
    byte[] refColours = null;
    int numRefColours = (hasAlpha)
                        ? 4
                        : 3;
    int baseColor = 0;
    float[] colour = new float[4];
    if (colours != null) {
      colour[0] = ((float)colours[0].getRed()) / 255.0f;
      colour[1] = ((float)colours[0].getGreen()) / 255.0f;
      colour[2] = ((float)colours[0].getBlue()) / 255.0f;
      colour[3] = ((float)colours[0].getAlpha()) / 255.0f;
    }
    else {
      if (byRef) {
        // DisplayImplJ3D stores ref as bytes
        refColours = triangleArray.getColorRefByte();
      }
    }
    if (!useTransparency || !hasAlpha) {
      colour[3] = 1.0f;
    }
    // If monochrome
    if (monochrome) {
      // Set everything except white to black
      monochromatise(colour);
    }

    graphics.setStroke(getStroke(thickness));
    // Temporary variables for retrieving coordinates
    int vertexCount = triangleArray.getVertexCount();
    int nCoords = 3 * vertexCount;
    float[] coordinates = null;

    // Get the coordinates
    if (byRef) {
      coordinates = triangleArray.getCoordRefFloat();
    }
    else {
      coordinates = new float[nCoords];
      triangleArray.getCoordinates(0, coordinates);
    }
    coordinates = transformToScreen(coordinates);

    int cCount = 0;

    // Find out how many strips
    int numStrips = triangleArray.getNumStrips();
    // Get the sizes of each strip
    int[] vertexCounts = new int[numStrips];
    triangleArray.getStripVertexCounts(vertexCounts);

    int base = 0;
    // Loop over each strip
    for (int i = 0; i < numStrips; i++) {
      int numCoords = vertexCounts[i];
      if (colours == null) {
        //triangleArray.getColor(cCount++, colour);
        if (refColours != null) { // means we are BY_REFERENCE
          for (int j = 0; j < numRefColours; j++) {
            colour[j] = byteToFloat(refColours[baseColor + j]);
          }
        }
        else {
          triangleArray.getColor(cCount++, colour);
        }
      }
      if (!useTransparency || !hasAlpha) {
        colour[3] = 1.0f;
      }
      // If monochrome
      if (monochrome) {
        // Set everything except white to black
        monochromatise(colour);
      }
      Color color = new Color(colour[0], colour[1], colour[2], colour[3]);
      graphics.setColor(color);

      float lastNormX2 = coordinates[base];
      float lastNormY2 = coordinates[base + 1];
      float lastNormX1 = coordinates[base + 3];
      float lastNormY1 = coordinates[base + 3 + 1];
      for (int j = 2; j < numCoords; j++) {

        float normalX = coordinates[base + j * 3];
        float normalY = coordinates[base + j * 3 + 1];
        float[][] triangle = new float[3][3];
        triangle[0][0] = lastNormX1;
        triangle[1][0] = lastNormY1;
        triangle[0][1] = lastNormX2;
        triangle[1][1] = lastNormY2;
        triangle[0][2] = normalX;
        triangle[1][2] = normalY;
        lastNormX2 = lastNormX1;
        lastNormX1 = normalX;
        lastNormY2 = lastNormY1;
        lastNormY1 = normalY;
        fillShapeReprojected(
          triangle, color, graphics, transformToScreenCoords);
      }
      base += 3 * numCoords;
      baseColor += numRefColours * numCoords;
    }

  }

  /**
   * Convert a TriangleArray into plottable vectors
   *
   * @param triangleArray   The TriangleArray
   * @param colours     The list of colors at each vertex
   * @param thickness   The line thickness
   * @param graphics    The graphics
   */
  private void plot(TriangleArray triangleArray, Color[] colours,
                    float thickness, Graphics2D graphics) {

    boolean hasAlpha = hasAlpha(triangleArray);
    boolean byRef = isByReference(triangleArray);
    byte[] refColours = null;
    int numRefColours = (hasAlpha)
                        ? 4
                        : 3;
    int baseColor = 0;
    float[] colour = new float[4];
    if (colours != null) {
      colour[0] = ((float)colours[0].getRed()) / 255.0f;
      colour[1] = ((float)colours[0].getGreen()) / 255.0f;
      colour[2] = ((float)colours[0].getBlue()) / 255.0f;
      colour[3] = ((float)colours[0].getAlpha()) / 255.0f;
    }
    else {
      //triangleArray.getColor(0, colour);
      if (byRef) {
        // DisplayImplJ3D stores ref as bytes
        refColours = triangleArray.getColorRefByte();
      }
    }
    if (!hasAlpha || !useTransparency) {
      colour[3] = 1.0f;
    }

    // If monochrome
    if (monochrome) {
      // Set everything except white to black
      monochromatise(colour);
    }

    graphics.setStroke(getStroke(thickness));

    // Temporary variables for retrieving coordinates
    int vertexCount = triangleArray.getVertexCount();
    int nCoords = 3 * vertexCount;
    float[] coordinates = null;

    // Get the coordinates
    if (byRef) {
      coordinates = triangleArray.getCoordRefFloat();
    }
    else {
      coordinates = new float[nCoords];
      triangleArray.getCoordinates(0, coordinates);
    }

    coordinates = transformToScreen(coordinates);

    for (int i = 0; i < 3 * vertexCount; i += 9) {
      if (colours == null) {
        //triangleArray.getColor(cCount++, colour);
        if (refColours != null) { // means we are BY_REFERENCE
          for (int j = 0; j < numRefColours; j++) {
            colour[j] = byteToFloat(refColours[baseColor + j]);
          }
        }
        else {
          //if (i < vertexCount * 3) {  is this necessary?
          triangleArray.getColor(i / 3, colour);
        }
      }
      // If monochrome
      if (!useTransparency || !hasAlpha) {
        colour[3] = 1.0f;
      }
      if (monochrome) {
        // Set everything except white to black 
        monochromatise(colour);
      }
      Color color = new Color(colour[0], colour[1], colour[2], colour[3]);
      graphics.setColor(color);

      // Get the (normalised) display coordinates
      float[][] vertices = new float[2][3];
      vertices[0][0] = coordinates[i];
      vertices[1][0] = coordinates[i + 1];
      vertices[0][1] = coordinates[i + 3];
      vertices[1][1] = coordinates[i + 4];
      vertices[0][2] = coordinates[i + 6];
      vertices[1][2] = coordinates[i + 7];
      // Must be clockwise (batik screws up otherwise)
      int clockwise = clockwise(vertices[0], vertices[1]);
      if (clockwise >= 0) {
        vertices[0] = reverseDirection(vertices[0]);
        vertices[1] = reverseDirection(vertices[1]);
      }
      fillShapeReprojected(
        vertices, color, graphics, transformToScreenCoords);
      baseColor += 3 * numRefColours;
    }
  }

  /**
   * Convert a QuadArray into plottable vectors
   *
   * @param quadArray   The QuadArray
   * @param colours     The list of colors at each vertex
   * @param thickness   The line thickness
   * @param graphics    The graphics
   */
  private void plot(QuadArray quadArray, Color[] colours, float thickness,
                    Graphics2D graphics) {
    boolean hasAlpha = hasAlpha(quadArray);
    boolean byRef = isByReference(quadArray);
    byte[] refColours = null;
    int numRefColours = (hasAlpha)
                        ? 4
                        : 3;
    int baseColor = 0;
    float[] colour = new float[4];
    if (colours != null) {
      colour[0] = ((float)colours[0].getRed()) / 255.0f;
      colour[1] = ((float)colours[0].getGreen()) / 255.0f;
      colour[2] = ((float)colours[0].getBlue()) / 255.0f;
      colour[3] = ((float)colours[0].getAlpha()) / 255.0f;
    }
    else {
      if (byRef) {
        // DisplayImplJ3D stores ref as bytes
        refColours = quadArray.getColorRefByte();
      }
    }
    if (!useTransparency || !hasAlpha) {
      colour[3] = 1.0f;
    }
    // If monochrome
    if (monochrome) {
      // Set everything except white to black
      monochromatise(colour);
    }


    graphics.setStroke(getStroke(thickness));

    // Temporary variables for retrieving coordinates
    int vertexCount = quadArray.getVertexCount();
    int nCoords = 3 * vertexCount;
    float[] coordinates = null;

    // Get the coordinates
    if (byRef) {
      coordinates = quadArray.getCoordRefFloat();
    }
    else {
      coordinates = new float[nCoords];
      quadArray.getCoordinates(0, coordinates);
    }
    coordinates = transformToScreen(coordinates);

    //        System.err.println("quad:" + vertexCount);
    for (int i = 0; i < 3 * vertexCount; i += 12) {
      if (colours == null) {
        //triangleArray.getColor(cCount++, colour);
        if (refColours != null) { // means we are BY_REFERENCE
          for (int j = 0; j < numRefColours; j++) {
            colour[j] = byteToFloat(refColours[baseColor + j]);
          }
        }
        else {
          //if (i < vertexCount * 4) {  is this necessary?
          quadArray.getColor(i / 4, colour);
        }
      }
      if (!useTransparency || !hasAlpha) {
        colour[3] = 1.0f;
      }
      // If monochrome
      if (monochrome) {
        // Set everything except white to black
        monochromatise(colour);
      }
      Color color = new Color(colour[0], colour[1], colour[2], colour[3]);
      graphics.setColor(color);

      // Get the (normalised) display coordinates
      float[][] vertices = new float[2][4];
      vertices[0][0] = coordinates[i];
      vertices[1][0] = coordinates[i + 1];
      vertices[0][1] = coordinates[i + 3];
      vertices[1][1] = coordinates[i + 4];
      vertices[0][2] = coordinates[i + 6];
      vertices[1][2] = coordinates[i + 7];
      vertices[0][3] = coordinates[i + 9];
      vertices[1][3] = coordinates[i + 10];
      // Must be clockwise (batik screws up otherwise)
      int clockwise = clockwise(vertices[0], vertices[1]);
      if (clockwise >= 0) {
        vertices[0] = reverseDirection(vertices[0]);
        vertices[1] = reverseDirection(vertices[1]);
      }
      fillShapeReprojected(
        vertices, color, graphics, transformToScreenCoords);
      baseColor += 4 * numRefColours;
    }
  }

  /**
   * Plot a PointArray into postscript format
   *
   * @param pointArray  The pointArray to be plotted
   * @param colours     The colour to plot the points
   * @param size        Size of the points
   * @param graphics    The graphics to plot to
   */
  private void plot(PointArray pointArray, Color[] colours, float size,
                    Graphics2D graphics) {
    boolean hasAlpha = hasAlpha(pointArray);
    boolean byRef = isByReference(pointArray);
    byte[] refColours = null;
    int numRefColours = (hasAlpha)
                        ? 4
                        : 3;
    int baseColor = 0;
    float[] colour = new float[4];
    if (colours != null) {
      colour[0] = ((float)colours[0].getRed()) / 255.0f;
      colour[1] = ((float)colours[0].getGreen()) / 255.0f;
      colour[2] = ((float)colours[0].getBlue()) / 255.0f;
      colour[3] = ((float)colours[0].getAlpha()) / 255.0f;
    } else {
      if (byRef) {
        // DisplayImplJ3D stores ref as bytes
        refColours = pointArray.getColorRefByte();
      }
    }
    if (!hasAlpha || !useTransparency) {
      colour[3] = 1.0f;
    }

    // If monochrome
    if (monochrome) {
      // Set everything except white to black
      monochromatise(colour);
    }

    // Temporary variables for retrieving coordinates
    int vertexCount = pointArray.getVertexCount();
    int nCoords = 3 * vertexCount;
    float[] coordinates = null;

    // Get the coordinates
    if (byRef) {
      coordinates = pointArray.getCoordRefFloat();
    }
    else {
      coordinates = new float[nCoords];
      pointArray.getCoordinates(0, coordinates);
    }
    coordinates = transformToScreen(coordinates);
    graphics.setStroke(getStroke(size));
    float hsize = 0.5f * size;

    // Loop over each point
    for (int i = 0; i < vertexCount; i++) {
      float x = coordinates[i * 3];
      float y = coordinates[i * 3 + 1];
      float[] vertices = {x, y};
      if (!transformToScreenCoords) {
        float[] device = new float[2];
        viewPort.transform(new float[] {x, y}, 0, vertices, 0, 1);
      }
      if (colours == null) {
        //pointArray.getColor(i, colour);
        //triangleArray.getColor(cCount++, colour);
        if (refColours != null) { // means we are BY_REFERENCE
          for (int j = 0; j < numRefColours; j++) {
            colour[j] = byteToFloat(refColours[i * numRefColours + j]);
          }
        }
        else {
          pointArray.getColor(i, colour);
        }
      }
      if (!useTransparency || !hasAlpha) {
        colour[3] = 1.0f;
      }
      // If monochrome
      if (monochrome) {
        // Set everything except white to black
        monochromatise(colour);
      }
      Color color = new Color(colour[0], colour[1], colour[2], colour[3]);
      graphics.setColor(color);
      graphics.fill(new Rectangle2D.Float(x - hsize, y - hsize, size, size));
      /*
      graphics.setStroke(getStroke(1));
      graphics.fillRect(
        (int)vertices[0], (int)vertices[1], (int)size, (int)size);
        */
    }
  }

  /**
   * Get the number of colour components for the geometry array
   * @param array the array to check
   *
   * @return true if vertexFormat has COLOR_4
   */
  private boolean hasAlpha(GeometryArray array) {
    int vertexFormat = array.getVertexFormat();
    if ((vertexFormat & GeometryArray.COLOR_4) == GeometryArray.COLOR_4) {
      return true;
    }
    return false;
  }

  /**
   * Get whether this array uses BY_REFERENCE for it's coordinates and colors
   * @param array the array to check
   *
   * @return true if vertexFormat has BY_REFERENCE
   */
  private boolean isByReference(GeometryArray array) {
    int vertexFormat = array.getVertexFormat();
    if ((vertexFormat & GeometryArray.BY_REFERENCE) != 0) {
      return true;
    }
    return false;
  }


  /**
   * Convert an IndexedTriangleStripArray into plottable vectors
   *
   * @param triangleArray   The IndexedTriangleStripArray
   * @param colours     The list of colors at each vertex
   * @param thickness   The line thickness
   * @param graphics    The graphics
   */
  private void plot(IndexedTriangleStripArray triangleArray, Color[] colours,
                    float thickness, Graphics2D graphics) {}

  /**
   * Get the BasicStroke with the given thickness
   *
   * @param thickness the thickness
   *
   * @return  the BasicStroke
   */
  private BasicStroke getStroke(float thickness) {
    BasicStroke stroke = new BasicStroke(thickness * (float)lineThickness,
                                         BasicStroke.CAP_BUTT,
                                         BasicStroke.JOIN_MITER);

    return stroke;
  }

  /**
   * Get the BasicStroke with the given thickness and dash style
   *
   * @param thickness  the line thickness
   * @param dash       the dash array
   *
   * @return  the stroke
   */
  private BasicStroke getStroke(float thickness, float[] dash) {
    BasicStroke stroke = new BasicStroke(thickness * (float)lineThickness,
                                         BasicStroke.CAP_ROUND,
                                         BasicStroke.JOIN_ROUND, 1.0f, dash,
                                         5.0f);

    return stroke;
  }

  /**
   * Make colours monochromatic by setting everything except white to
   * black
   *
   * @param colour color to monochromatise
   */
  private void monochromatise(float[] colour) {
    if ((colour[0] < 0.8f) && (colour[1] < 0.8f) && (colour[2] < 0.8f)) {
      colour[0] = 0.0f;
      colour[1] = 0.0f;
      colour[2] = 0.0f;
    }

    // Remove transparency
    if (colour.length == 4) {
      colour[3] = 1.0f;
    }
  }

  /**
   * Test the vertices to see if they form a clockwise or anticlockwise
   * shape.
   *
   * @param xCoords  the x coords
   * @param yCoords  the y coords
   *
   * @return 1 means clockwise, -1 means anticlockwise, 0 means
   *  indeterminate
   */
  private int clockwise(float[] xCoords, float[] yCoords) {
    int ccw = 0;
    int numCoords = xCoords.length;
    for (int i = 0; i < numCoords; i++) {
      int j = (i + 1) % numCoords;
      int k = (i + 2) % numCoords;
      float crossProduct = (xCoords[j] - xCoords[i]) *
                           (yCoords[k] - yCoords[j]);
      crossProduct -= (yCoords[j] - yCoords[i]) * (xCoords[k] - xCoords[j]);

      if (crossProduct > 0) {
        ccw++;
      }
      else if (crossProduct < 0) {
        ccw--;
      }
    }

    int clockwise = 0;
    if (ccw > 0) {
      clockwise = -1;
    }
    else if (ccw < 0) {
      clockwise = 1;
    }

    return clockwise;
  }

  /**
   * Test the vertices to see if they form a clockwise or anticlockwise
   * shape.
   * @param xCoords  the x coords
   * @param yCoords  the y coords
   *
   * @return 1 means clockwise, -1 means anticlockwise, 0 means
   *  indeterminate
   */
  private int clockwise(int[] xCoords, int[] yCoords) {
    int ccw = 0;
    int numCoords = xCoords.length;
    for (int i = 0; i < numCoords; i++) {
      int j = (i + 1) % numCoords;
      int k = (i + 2) % numCoords;
      float crossProduct = (xCoords[j] - xCoords[i]) *
                           (yCoords[k] - yCoords[j]);
      crossProduct -= (yCoords[j] - yCoords[i]) * (xCoords[k] - xCoords[j]);

      if (crossProduct > 0) {
        ccw++;
      }
      else if (crossProduct < 0) {
        ccw--;
      }
    }

    int clockwise = 0;
    if (ccw > 0) {
      clockwise = -1;
    }
    else if (ccw < 0) {
      clockwise = 1;
    }

    return clockwise;
  }

  /**
   * Reverse the order of the vertices in a triangle from clockwise to
   * anticlockwise and vice versa
   *
   * @param coords    The x (or y) coordinates of the triangle
   *
   * @return  the reversed coordinates
   */
  private int[] reverseDirection(int[] coords) {
    int temp = coords[2];
    coords[2] = coords[1];
    coords[1] = temp;

    return coords;
  }

  /**
   * Reverse the order of the vertices in a polygon from clockwise to
   * anticlockwise and vice versa
   *
   * @param coords    The x (or y) coordinates of the triangle
   *
   * @return  the reversed coordinates
   */
  private float[] reverseDirection(float[] coords) {
    int numCoords = coords.length;
    float[] temp = new float[numCoords];
    for (int i = 0; i < numCoords; i++) {
      temp[i] = coords[numCoords - i - 1];
    }

    return temp;
  }

  /**
   * Long winded way of checking if any part of an area is visible in an
   * area bounded by the graphics2d clipping region Significantly
   * increases the plotting time, but significantly decreases the size of
   * the resulting file Probably better to investigate ways of clipping
   * the lines using DelaunayCustom.clip()
   *
   * @param x1   The X coordinate of the start of the line
   * @param y1   The Y coordinate of the start of the line
   * @param x2   The X coordinate of the end of the line
   * @param y2   The Y coordinate of the end of the line
   * @param graphics  the graphics
   * @return a boolean flag, true means that the triangle is at least
   *          partially visible
   *
   * @return true if visible
   */
  private boolean visible(double x1, double y1, double x2, double y2,
                          Graphics2D graphics) {
    return visible(x1, y1, x2, y2, graphics, false);
  }

  /**
   * Long winded way of checking if any part of an area is visible in an
   * area bounded by the graphics2d clipping region Significantly
   * increases the plotting time, but significantly decreases the size of
   * the resulting file Probably better to investigate ways of clipping
   * the lines using DelaunayCustom.clip()
   *
   * @param x1   The X coordinate of the start of the line
   * @param y1   The Y coordinate of the start of the line
   * @param x2   The X coordinate of the end of the line
   * @param y2   The Y coordinate of the end of the line
   * @param graphics  the graphics
   * @param isScreen  true to transform xy to graphics space
   * @return a boolean flag, true means that the triangle is at least
   *          partially visible
   *
   * @return true if visible
   */
  private boolean visible(double x1, double y1, double x2, double y2,
                          Graphics2D graphics, boolean isScreen) {
    double[] coords = {x1, y1, x2, y2};
    double[] device = new double[4];
    if (!isScreen) {
      viewPort.transform(coords, 0, device, 0, 2);
    }
    else {
      device = coords;
    }

    // Find the area occupied by the line
    double xMin = Math.min(device[0], device[2]);
    double xMax = Math.max(device[0], device[2]);
    double yMin = Math.min(device[1], device[3]);
    double yMax = Math.max(device[1], device[3]);

    int x = (int)xMin;
    int y = (int)yMin;
    // Must add 1 to these, because otherwise a vertical or
    // horizontal line will have 0 area, and be treated as
    // not intersecting, even when it does
    int width = (int)(xMax - xMin) + 1;
    int height = (int)(yMax - yMin) + 1;

    // If any of this area intersects the clip, it is visible
    boolean visible = graphics.hitClip(x, y, width, height);

    return visible;
  }

  /**
   * Is the Shape visible
   *
   * @param shape  shape to check
   *
   * @return  true if visible
   */
  private boolean visible(Shape shape) {
    boolean visible = shape.intersects(0, 0, width, height);

    return visible;
  }

  /**
   * Long winded way of checking if any part of an area is visible in an
   * area bounded by the graphics2s clipping area Significantly increases
   * the plotting time, but significantly decreases the size of the
   * resulting file Probably better to investigate ways of clipping the
   * lines using DelaunayCustom.clip()
   *
   * @param xCoords -
   *                The X coordinates of the area
   * @param yCoords -
   *                The Y coordinates of the area
   * @param graphics  the graphics
   * @return a boolean flag, true means that the triangle is at least
   *          partially visible
   */
  private boolean visible(int[] xCoords, int[] yCoords, Graphics2D graphics) {
    int numVertices = xCoords.length;

    int xMin = Integer.MAX_VALUE;
    int yMin = Integer.MAX_VALUE;
    int xMax = Integer.MIN_VALUE;
    int yMax = Integer.MIN_VALUE;

    for (int i = 0; i < numVertices; i++) {
      float[] display = {xCoords[i], yCoords[i]};
      float[] device = new float[2];
      // Convert the display coordinates to device coordinates
      viewPort.transform(display, 0, device, 0, 2);

      if (device[0] > xMax) {
        xMax = (int)device[0];
      }
      if (device[0] < xMin) {
        xMin = (int)device[0];
      }
      if (device[1] > yMax) {
        yMax = (int)device[1];
      }
      if (device[1] < yMin) {
        yMin = (int)device[1];
      }
    }

    int x = (int)xMin;
    int y = (int)yMin;
    // Must add 1 to these, because otherwise a vertical or
    // horizontal line will have 0 area, and be treated as
    // not intersecting, even when it does
    int width = (int)(xMax - xMin) + 1;
    int height = (int)(yMax - yMin) + 1;

    // If any of this area intersects the clip, it is visible
    boolean visible = graphics.hitClip(x, y, width, height);

    return visible;
  }


  /**
   * Perform Sutherland-Hodgman Clipping on an array of vertices
   * Not the most efficient method, but it works
   * @param inVertexArray Array of vertices array[0] - x values,
   * array[1] y values
   * @return Clipped vertices
   */
  private int[][] clip(int[][] inVertexArray) {
    int[][] edge = new int[2][2];
    // Clip Left Edge
    edge[0][0] = 0;
    edge[1][0] = height;
    edge[0][1] = 0;
    edge[1][1] = 0;
    inVertexArray = SutherlandHodgmanPolygonClip(inVertexArray, edge);
    // Clip Bottom Edge
    edge[0][0] = 0;
    edge[1][0] = 0;
    edge[0][1] = width;
    edge[1][1] = 0;
    inVertexArray = SutherlandHodgmanPolygonClip(inVertexArray, edge);
    // Clip Right Edge
    edge[0][0] = width;
    edge[1][0] = 0;
    edge[0][1] = width;
    edge[1][1] = height;
    inVertexArray = SutherlandHodgmanPolygonClip(inVertexArray, edge);
    // Clip Top Edge
    edge[0][0] = width;
    edge[1][0] = height;
    edge[0][1] = width;
    edge[1][1] = height;
    inVertexArray = SutherlandHodgmanPolygonClip(inVertexArray, edge);

    return inVertexArray;
  }

  /**
   * Perform Sutherland-Hodgman Clipping on an array of vertices
   * Not the most efficient method, but it works
   * @param path   GeneralPath to clip
   *
   * @return Clipped vertices
   */
  private GeneralPath clip(GeneralPath path) {
    float[][] edge = new float[2][2];
    // Clip Left Edge
    edge[0][0] = 0;
    edge[1][0] = (height * 10);
    edge[0][1] = 0;
    edge[1][1] = -(height * 10);
    path = SutherlandHodgmanPolygonClip(path, edge);
    // Clip Bottom Edge
    edge[0][0] = -(width * 10);
    edge[1][0] = 0;
    edge[0][1] = (width * 10);
    edge[1][1] = 0;
    path = SutherlandHodgmanPolygonClip(path, edge);
    // Clip Right Edge
    edge[0][0] = (width * 10);
    edge[1][0] = -(height * 10);
    edge[0][1] = (width * 10);
    edge[1][1] = (height * 10);
    path = SutherlandHodgmanPolygonClip(path, edge);
    // Clip Top Edge
    edge[0][0] = (width * 10);
    edge[1][0] = (height * 10);
    edge[0][1] = -(width * 10);
    edge[1][1] = (height * 10);
    path = SutherlandHodgmanPolygonClip(path, edge);

    return path;
  }

  /**
   * Perform Sutherland-Hodgman Clipping on an array of vertices
   * Not the most efficient method, but it works
   *
   * @param path   GeneralPath to clip
   * @return Clipped vertices
   */
  private GeneralPath clip2(GeneralPath path) {
    float[][] edge = new float[2][2];
    // Clip Left Edge
    edge[0][0] = 0;
    edge[1][0] = height;
    edge[0][1] = 0;
    edge[1][1] = 0;
    path = SutherlandHodgmanPolygonClip(path, edge);
    // Clip Bottom Edge
    edge[0][0] = 0;
    edge[1][0] = 0;
    edge[0][1] = width;
    edge[1][1] = 0;
    path = SutherlandHodgmanPolygonClip(path, edge);
    // Clip Right Edge
    edge[0][0] = width;
    edge[1][0] = 0;
    edge[0][1] = width;
    edge[1][1] = height;
    path = SutherlandHodgmanPolygonClip(path, edge);
    // Clip Top Edge
    edge[0][0] = width;
    edge[1][0] = height;
    edge[0][1] = 0;
    edge[1][1] = height;
    path = SutherlandHodgmanPolygonClip(path, edge);

    return path;
  }

  /**
   * Perform Sutherland-Hodgman clipping against a single edge
   * @param inVertexArray Array of vertices
   * @param edge Edge definition - corners must be defined in
   * anticlockwise order
   * @return
   */
  private int[][] SutherlandHodgmanPolygonClip(int[][] inVertexArray,
          int[][] edge) {
    int numVertices = inVertexArray[0].length;
    int[][] outVertexArray = null;
    if (numVertices <= 1) {
      outVertexArray = new int[1][0];
      return outVertexArray;
    }
    ArrayList xList = new ArrayList();
    ArrayList yList = new ArrayList();

    int xs = inVertexArray[0][inVertexArray[0].length - 1];
    int ys = inVertexArray[1][inVertexArray[0].length - 1];
    for (int j = 0; j < inVertexArray[0].length; j++) {
      // Get the next point from the array
      int xp = inVertexArray[0][j];
      int yp = inVertexArray[1][j];
      // If the next point is within the clip
      if (insideEdge(xp, yp, edge)) {
        // If previous point was also within the clip
        if (insideEdge(xs, ys, edge)) {
          xList.add(new Integer(xp));
          yList.add(new Integer(yp));
        }
        else {
          // Otherwise, intersect at the clip edge
          int[] inter = intersect(new int[] {xs, ys}, new int[] {xp, yp},
                                  edge);
          xList.add(new Integer(inter[0]));
          yList.add(new Integer(inter[1]));
          xList.add(new Integer(xp));
          yList.add(new Integer(yp));
        }
      }
      else {
        // Join the last point to the clip
        if (insideEdge(xs, ys, edge)) {
          int[] inter = intersect(new int[] {xs, ys}, new int[] {xp, yp},
                                  edge);
          xList.add(new Integer(inter[0]));
          yList.add(new Integer(inter[1]));
        }
      }
      xs = xp;
      ys = yp;
    }

    outVertexArray = new int[2][xList.size()];
    for (int i = 0; i < xList.size(); i++) {
      Integer xInt = (Integer)xList.get(i);
      Integer yInt = (Integer)yList.get(i);
      outVertexArray[0][i] = xInt.intValue();
      outVertexArray[1][i] = yInt.intValue();
    }

    return outVertexArray;
  }

  /**
   * Perform Sutherland-Hodgman clipping against a single edge
   *
   * @param path   The path
   * @param edge Edge definition - corners must be defined in
   * anticlockwise order
   * @return
   */
  private GeneralPath SutherlandHodgmanPolygonClip(GeneralPath path,
          float[][] edge) {
    ArrayList xList = new ArrayList();
    ArrayList yList = new ArrayList();

    AffineTransform blank = AffineTransform.getScaleInstance(1.0, 1.0);
    java.awt.geom.PathIterator iterator = path.getPathIterator(blank);
    float[] coords = new float[6];
    iterator.currentSegment(coords);
    float xs = coords[0];
    float ys = coords[1];
    iterator.next();
    if (insideEdge(xs, ys, edge)) {
      xList.add(new Float(xs));
      yList.add(new Float(ys));
    }
    while(!iterator.isDone()) {
      // Get the next point from the array
      iterator.currentSegment(coords);
      float xp = coords[0];
      float yp = coords[1];

      // If the next point is within the clip
      if (insideEdge(xp, yp, edge)) {
        // If previous point was also within the clip
        if (insideEdge(xs, ys, edge)) {
          xList.add(new Float(xp));
          yList.add(new Float(yp));
        }
        else {
          // Otherwise, intersect at the clip edge
          float[] inter = intersect(new float[] {xs, ys}, new float[] {xp,
                  yp}, edge);
          xList.add(new Float(inter[0]));
          yList.add(new Float(inter[1]));
          xList.add(new Float(xp));
          yList.add(new Float(yp));
        }
      }
      else {
        // Join the last point to the clip
        if (insideEdge(xs, ys, edge)) {
          float[] inter = intersect(new float[] {xs, ys}, new float[] {xp,
                  yp}, edge);
          xList.add(new Float(inter[0]));
          yList.add(new Float(inter[1]));
        }
      }
      xs = xp;
      ys = yp;
      iterator.next();
    }

    GeneralPath clippedPath = new GeneralPath();
    if (xList.size() < 1) {
      return clippedPath;
    }
    Float xInt = (Float)xList.get(0);
    Float yInt = (Float)yList.get(0);
    clippedPath.moveTo(xInt.floatValue(), yInt.floatValue());
    for (int i = 1; i < xList.size(); i++) {
      xInt = (Float)xList.get(i);
      yInt = (Float)yList.get(i);
      clippedPath.lineTo(xInt.floatValue(), yInt.floatValue());
    }

    return clippedPath;
  }

  /**
   * Test if a point is "within" and edge of a rectangle
   * @param x X coordinate of point
   * @param y Y coordinate of point
   * @param edge The edge to compare - must be defined in anticlockwise
   * order
   * @return True if the point is within the rectangle
   */
  private boolean insideEdge(int x, int y, int[][] edge) {
    final int X = 0;
    final int Y = 1;
    boolean inside = false;
    // If bottom edge
    if (edge[X][1] > edge[X][0]) {
      if (y >= edge[Y][0]) {
        inside = true;
      }
    }
    // If Top Edge
    if (edge[X][1] < edge[X][0]) {
      if (y <= edge[Y][0]) {
        inside = true;
      }
    }
    // If Right Edge
    if (edge[Y][1] > edge[Y][0]) {
      if (x <= edge[X][1]) {
        inside = true;
      }
    }
    // If Left Edge
    if (edge[Y][1] < edge[Y][0]) {
      if (x >= edge[X][1]) {
        inside = true;
      }
    }

    return inside;
  }

  /**
   * Test if a point is "within" and edge of a rectangle
   * @param x X coordinate of point
   * @param y Y coordinate of point
   * @param edge The edge to compare - must be defined in anticlockwise
   * order
   * @return True if the point is within the rectangle
   */
  private boolean insideEdge(float x, float y, float[][] edge) {
    final int X = 0;
    final int Y = 1;
    boolean inside = false;
    // If bottom edge
    if (edge[X][1] > edge[X][0]) {
      if (y >= edge[Y][0]) {
        inside = true;
      }
    }
    // If Top Edge
    if (edge[X][1] < edge[X][0]) {
      if (y <= edge[Y][0]) {
        inside = true;
      }
    }
    // If Right Edge
    if (edge[Y][1] > edge[Y][0]) {
      if (x <= edge[X][1]) {
        inside = true;
      }
    }
    // If Left Edge
    if (edge[Y][1] < edge[Y][0]) {
      if (x >= edge[X][1]) {
        inside = true;
      }
    }

    return inside;
  }

  /**
   * Intersect a line between two vertices which crosses the edge of the
   * clipping region
   * @param first First point - int[0] is x value, int[1] is y value
   * @param second  Second point - int[0] is x value, int[1] is y value
   * @param edge The edge definition - Must be defined in anticlockwise
   *  order
   * @return
   */
  private int[] intersect(int[] first, int[] second, int[][] edge) {
    final int X = 0;
    final int Y = 1;

    int[] intersect = new int[2];

    // If the edge is horizontal
    if (edge[Y][0] == edge[Y][1]) {
      intersect[Y] = edge[Y][0];
      intersect[X] = first[X] +
                     (edge[Y][0] - first[Y]) * (second[X] - first[X]) /
                     (second[Y] - first[Y]);
    }
    else {
      intersect[X] = edge[X][0];
      intersect[Y] = first[Y] +
                     (edge[X][0] - first[X]) * (second[Y] - first[Y]) /
                     (second[X] - first[X]);
    }

    return intersect;
  }

  /**
   * Intersect a line between two vertices which crosses the edge of the
   * clipping region
   * @param first First point - int[0] is x value, int[1] is y value
   * @param second  Second point - int[0] is x value, int[1] is y value
   * @param edge The edge definition - Must be defined in anticlockwise
   *  order
   * @return
   */
  private float[] intersect(float[] first, float[] second, float[][] edge) {
    final int X = 0;
    final int Y = 1;

    float[] intersect = new float[2];

    // If the edge is horizontal
    if (edge[Y][0] == edge[Y][1]) {
      intersect[Y] = edge[Y][0];
      intersect[X] = first[X] +
                     (edge[Y][0] - first[Y]) * (second[X] - first[X]) /
                     (second[Y] - first[Y]);
    }
    else {
      intersect[X] = edge[X][0];
      intersect[Y] = first[Y] +
                     (edge[X][0] - first[X]) * (second[Y] - first[Y]) /
                     (second[X] - first[X]);
    }

    return intersect;
  }

  /**
   * Chart exception
   */
  public class ChartException extends Exception {

    /**
     * Create a chart exception
     *
     * @param reason  reason for the exception
     */
    public ChartException(String reason) {
      super(reason);
    }
  }

  /**
   * Provide a set of simple hatching patterns
   */
  public class Hatching {

    /** number of patterns */
    public static final int NUM_PATTERNS = 6;

    /** diagonal patter 1 */
    public static final int DIAGONAL1 = 0;

    /** diagonal pattern 2 */
    public static final int DIAGONAL2 = 1;

    /** diagonal both */
    public static final int DIAGONAL_BOTH = 2;

    /** horizontal pattern */
    public static final int HORIZONTAL = 3;

    /** vertical pattern */
    public static final int VERTICAL = 4;

    /** square pattern */
    public static final int SQUARE = 5;

    /** width */
    private int width = 300;

    /** height */
    private int height = 300;

    /**
     * Get the pattern as an image
     *
     * @param pattern  the pattern
     *
     * @return the image
     */
    public BufferedImage getPattern(int pattern) {
      // Create a 1 bit (monochrome) image
      BufferedImage fillTexture = new BufferedImage(width, height,
                                    BufferedImage.TYPE_INT_RGB);

      for (int i = 0; i < width; i++) {
        for (int j = 0; j < height; j++) {
          setPoint(i, j, Color.WHITE, fillTexture);
        }
      }

      for (int i = 0; i < width; i++) {
        for (int j = 0; j < height; j++) {
          if (isSet(pattern, i, j)) {
            setPoint(i, j, Color.BLACK, fillTexture);
          }
        }
      }

      return fillTexture;
    }

    /**
     * Test whether an x,y point for a given pattern is filled
     *
     * @param pattern The pattern
     * @param x       The x location of the point
     * @param y       The y location of the point
     *
     * @return true if filled
     */
    private boolean isSet(int pattern, int x, int y) {
      boolean isSet = false;
      int repeat = 3;

      if (pattern == DIAGONAL1) {
        int xx = width / repeat;
        int yy = height / repeat;
        if ((x % xx) == (y % yy)) {
          isSet = true;
        }
      }
      else if (pattern == DIAGONAL2) {
        int xx = width / repeat;
        int yy = height / repeat;
        if ((x % xx) == (yy - (y % yy))) {
          isSet = true;
        }
      }
      else if (pattern == DIAGONAL_BOTH) {
        int xx = width / repeat;
        int yy = height / repeat;
        if (((x % xx) == (y % yy)) || ((x % xx) == (yy - (y % yy)))) {
          isSet = true;
        }
      }
      else if (pattern == HORIZONTAL) {
        int yy = height / repeat;
        if ((y % yy) == 0) {
          isSet = true;
        }
      }
      else if (pattern == VERTICAL) {
        int xx = width / repeat;
        if ((x % xx) == 0) {
          isSet = true;
        }
      }
      else if (pattern == SQUARE) {
        int xx = width / repeat;
        int yy = height / repeat;
        if (((x % xx) == 0) || ((y % yy) == 0)) {
          isSet = true;
        }
      }

      return isSet;
    }

    /**
     * Set a point in a buffered image. Actually, set a 3x3 square,
     * it looks better.
     *
     * @param x
     *                The x location of the centre of the square
     * @param y
     *                The y location of the centre of the square
     * @param color color The colour of the square
     * @param image The buffered image to be plotted
     */
    private void setPoint(int x, int y, Color color, BufferedImage image) {
      int rgb = color.getRGB();
      for (int i = x - 1; i <= x + 1; i++) {
        for (int j = y - 1; j <= y + 1; j++) {
          if ((i >= 0) && (i < width) && (j >= 0) && (j < height)) {
            image.setRGB(i, j, rgb);
          }
        }
      }
    }
  }

  /**
   * Get the BasicStroke float arrays for dashing
   * @param dashStyle  dashing style
   * @param pixelWidth
   * @return array of alternating stroke lengths (on/off)
   */
  private float[] getStrokeDash(int dashStyle, float pixelWidth) {
    float[] dash = null;
    switch (dashStyle) {
      case LineAttributes.PATTERN_DOT:
        // 1 on, 7 off
        dash = new float[] {1.0f * pixelWidth, 7.0f * pixelWidth};
        break;
      case LineAttributes.PATTERN_DASH_DOT:
        // 7 on, 4 off, 1 on, 4 off
        dash = new float[] {7.0f * pixelWidth, 4.0f * pixelWidth,
                            1.0f * pixelWidth, 4 * pixelWidth};
        break;
      case LineAttributes.PATTERN_DASH: //
        // 8 on, 8 off
        dash = new float[] {8.0f * pixelWidth, 8.0f * pixelWidth};
        break;
      case LineAttributes.PATTERN_SOLID: //
      default: //
        break;
    }
    return dash;
  }

  /**
   * Set whether the vertices should be transformed from display coords
   * to screen coords before drawing.  This ends up bypassing the use of
   * the AffineTransform when drawing.  Use this if you are in a rotated
   * 3D display.
   *
   * @param value  true to transform
   */
  public void setTransformToScreenCoords(boolean value) {
    transformToScreenCoords = value;
  }

  /**
   * Transform coordinates from 3D display space to 2D space (AWT screen) space
   * @param coords display space coords
   *
   * @return screen coords
   */
  private float[] transformToScreen(float[] coords) {
    if (!transformToScreenCoords) return coords;
    int numvertex = coords.length / 3;
    float[] retCoords = new float[coords.length];
    for (int i = 0; i < numvertex; i++) {
      double[] xyz = {coords[i * 3], coords[i * 3 + 1], coords[i * 3 + 2]};
      int[] screenLocs = behavior.getScreenCoords(xyz);
      retCoords[i * 3] = screenLocs[0];
      retCoords[i * 3 + 1] = screenLocs[1];
      retCoords[i * 3 + 2] = 0;
    }
    return retCoords;

  }
}


 

Ads help maintain this website.