/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <https://www.compadre.org/osp/>
 */

package org.opensourcephysics.display3d.simple3d;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.event.MouseInputAdapter;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.controls.XMLControl;
import org.opensourcephysics.display.OSPLayout;
import org.opensourcephysics.display.TextPanel;
import org.opensourcephysics.display3d.core.interaction.InteractionEvent;
import org.opensourcephysics.display3d.core.interaction.InteractionListener;
import org.opensourcephysics.tools.VideoTool;

/**
 *
 * <p>Title: DrawingPanel3D</p>
 *
 * <p>Description: The simple3D implementation of a DrawingPanel3D.</p>
 *
 * <p>Interaction: The panel has only one target, the panel itself.
 * If enabled, the panel issues MOUSE_ENTER, MOUSE_EXIT,
 * MOUSE_MOVED, and MOUSE_DRAGGED InteractionEvents with target=null.
 * When the ALT key is held, the panel also issues MOUSE_PRESSED,
 * MOUSE_DRAGGED (again), and MOUSE_RELEASED InteractionEvents.
 * In this second case, the getInfo() method of the event returns a double[3]
 * with the coordinates of the point selected.</p>
 * <p>Even if the panel is disabled, the panel can be panned, zoomed and (in 3D
 * modes) rotated and the elements in it can be enabled.</p>
 * <p>The interaction capabilities are not XML serialized.</p>
 *
 * <p>Copyright: Open Source Physics project</p>
 * @author Francisco Esquembre
 * @version June 2005
 */
public class DrawingPanel3D extends javax.swing.JPanel implements org.opensourcephysics.display.Renderable, org.opensourcephysics.display3d.core.DrawingPanel3D, Printable, ActionListener {
  static private final int AXIS_DIVISIONS = 10;
  static private final Color bgColor = new Color(239, 239, 255);
  // Configuration variables
  private double xmin, xmax, ymin, ymax, zmin, zmax;
  private VisualizationHints visHints = null;
  private Camera camera = null;
  private String imageFile = null;
  // Implementation variables
  private boolean quickRedrawOn = false, squareAspect = true;
  private double centerX, centerY, centerZ, maximumSize;
  private double aconstant, bconstant;
  private int acenter, bcenter;
  private ArrayList<Object3D> list3D = new ArrayList<Object3D>();
  private ArrayList<org.opensourcephysics.display3d.core.Element> decorationList = new ArrayList<org.opensourcephysics.display3d.core.Element>();
  private ArrayList<org.opensourcephysics.display3d.simple3d.Element> elementList = new ArrayList<org.opensourcephysics.display3d.simple3d.Element>();
  private Object3D.Comparator3D comparator = new Object3D.Comparator3D();   // see class Comparator3D below
  // Variables for decoration
  private ElementArrow xAxis, yAxis, zAxis;
  private ElementText xText, yText, zText;
  private ElementSegment[] boxSides = new ElementSegment[12];
  // Variables for interaction
  private final InteractionTarget myTarget = new InteractionTarget(null, 0);
  private int trackersVisible, keyPressed = -1;
  private int lastX = 0, lastY = 0;
  private InteractionTarget targetHit = null, targetEntered = null;
  private double[] trackerPoint = null;
  private ArrayList<InteractionListener> listeners = new ArrayList<InteractionListener>();
  private ElementSegment[] trackerLines = null;
  // Variables for painting
  volatile private boolean dirtyImage = true;                               // offscreenImage needs to be recomputed
  // the image that will be copied to the screen
  volatile private BufferedImage offscreenImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
  // the image into which we will draw
  private BufferedImage workingImage = offscreenImage;
  private javax.swing.Timer updateTimer = new javax.swing.Timer(100, this); // delay before updating the panel
  private boolean needResize = true, needsToRecompute = true;
  // Variables for Messages
  protected TextPanel trMessageBox = new TextPanel();                       // text box in top right hand corner for message
  protected TextPanel tlMessageBox = new TextPanel();                       // text box in top left hand corner for message
  protected TextPanel brMessageBox = new TextPanel();                       // text box in lower right hand corner for message
  protected TextPanel blMessageBox = new TextPanel();                       // text box in lower left hand corner for mouse coordinates
  protected GlassPanel glassPanel = new GlassPanel();
  protected OSPLayout glassPanelLayout = new OSPLayout();
  protected Rectangle viewRect = null;                                      // the clipping rectangle within a scroll pane viewport
  //CJB
  //Scale factor
  private double factorX = 1.0;
  private double factorY = 1.0;
  private double factorZ = 1.0;
  private static int axisMode = MODE_XYZ;

  /**
   * The video capture tool for this panel.
   */
  protected VideoTool vidCap;

  //CJB
  private void BuildAxesPanel(int mode) {
    if((axisMode==mode)&&(xAxis!=null)) {
      return;
    }
    axisMode = mode;
    if(xAxis==null) {
      /* Decoration of the scene */
      // Create the bounding box
      Resolution axesRes = new Resolution(AXIS_DIVISIONS, 1, 1);
      for(int i = 0, n = boxSides.length; i<n; i++) {
        boxSides[i] = new ElementSegment();
        boxSides[i].getRealStyle().setResolution(axesRes);
        boxSides[i].setPanel(this);                      // Because I don't add it to the panel in the standard way
        decorationList.add(boxSides[i]);
      }
      boxSides[0].getStyle().setLineColor(new Color(128, 0, 0));
      boxSides[3].getStyle().setLineColor(new Color(0, 128, 0));
      boxSides[8].getStyle().setLineColor(new Color(0, 0, 255));
      // Create the axes
      String[] axesLabels = visHints.getAxesLabels();
      xAxis = new ElementArrow();
      xAxis.getRealStyle().setResolution(axesRes);
      xAxis.getStyle().setFillColor(new Color(128, 0, 0));
      xAxis.setPanel(this);
      decorationList.add(xAxis);
      xText = new ElementText();
      xText.setText(axesLabels[0]);
      xText.setJustification(org.opensourcephysics.display3d.core.ElementText.JUSTIFICATION_CENTER);
      xText.getRealStyle().setLineColor(Color.BLACK);
      xText.setFont(new Font("Dialog", Font.PLAIN, 12)); //$NON-NLS-1$
      xText.setPanel(this);
      decorationList.add(xText);
      yAxis = new ElementArrow();
      yAxis.getRealStyle().setResolution(axesRes);
      yAxis.getStyle().setFillColor(new Color(0, 128, 0));
      yAxis.setPanel(this);
      decorationList.add(yAxis);
      yText = new ElementText();
      yText.setText(axesLabels[1]);
      yText.setJustification(org.opensourcephysics.display3d.core.ElementText.JUSTIFICATION_CENTER);
      yText.getRealStyle().setLineColor(Color.BLACK);
      yText.setFont(new Font("Dialog", Font.PLAIN, 12)); //$NON-NLS-1$
      yText.setPanel(this);
      decorationList.add(yText);
      zAxis = new ElementArrow();
      zAxis.getRealStyle().setResolution(axesRes);
      zAxis.getStyle().setFillColor(new Color(0, 0, 255));
      zAxis.setPanel(this);
      decorationList.add(zAxis);
      zText = new ElementText();
      zText.setText(axesLabels[2]);
      zText.setJustification(org.opensourcephysics.display3d.core.ElementText.JUSTIFICATION_CENTER);
      zText.getRealStyle().setLineColor(Color.BLACK);
      zText.setFont(new Font("Dialog", Font.PLAIN, 12)); //$NON-NLS-1$
      zText.setPanel(this);
      decorationList.add(zText);
      // Create the trackers
      trackerLines = new ElementSegment[9];
      for(int i = 0, n = trackerLines.length; i<n; i++) {
        trackerLines[i] = new ElementSegment();
        trackerLines[i].getRealStyle().setResolution(axesRes);
        trackerLines[i].setVisible(false);
        trackerLines[i].setPanel(this);
        decorationList.add(trackerLines[i]);
      }
      setCursorMode();                                   // compute the correct value for trackersVisible
      /* End of decoration */
    } else {
      resetDecoration(xmax-xmin, ymax-ymin, zmax-zmin);
    }
  }
  //CJB

  /**
   * Constructor DrawingPanel3D
   */
  public DrawingPanel3D() {
    // GlassPanel for messages
    glassPanel.setLayout(glassPanelLayout);
    super.setLayout(new BorderLayout());
    glassPanel.add(trMessageBox, OSPLayout.TOP_RIGHT_CORNER);
    glassPanel.add(tlMessageBox, OSPLayout.TOP_LEFT_CORNER);
    glassPanel.add(brMessageBox, OSPLayout.BOTTOM_RIGHT_CORNER);
    glassPanel.add(blMessageBox, OSPLayout.BOTTOM_LEFT_CORNER);
    glassPanel.setOpaque(false);
    super.add(glassPanel, BorderLayout.CENTER);
    setBackground(bgColor);
    setPreferredSize(new Dimension(300, 300));
    visHints = new VisualizationHints(this);
    camera = new Camera(this);
    addComponentListener(new java.awt.event.ComponentAdapter() {
      public void componentResized(java.awt.event.ComponentEvent e) {
        needResize = true;
        dirtyImage = true;
      }

    });
    IADMouseController mouseController = new IADMouseController();
    addMouseListener(mouseController);
    addMouseMotionListener(mouseController);
    addKeyListener(new java.awt.event.KeyAdapter() {
      public void keyPressed(java.awt.event.KeyEvent _e) {
        keyPressed = _e.getKeyCode();
        //            System.out.println("Key = "+keyPressed);
      }
      public void keyReleased(java.awt.event.KeyEvent _e) {
        keyPressed = -1;
      }

    });
    this.setFocusable(true);
    //CJB
    //Build the axes
    BuildAxesPanel(axisMode);
    //CJB
    // Set default for displayMode
    if(camera.is3dMode()) {
      visHints.setDecorationType(org.opensourcephysics.display3d.core.VisualizationHints.DECORATION_CUBE);
      visHints.setUseColorDepth(true);
    } else {
      visHints.setDecorationType(org.opensourcephysics.display3d.core.VisualizationHints.DECORATION_NONE);
      visHints.setUseColorDepth(false);
    }
    setPreferredMinMax(-1, 1, -1, 1, -1, 1);
  }

  // ---------------------------------
  // Begin W. Christian additions and changes
  // ---------------------------------

  /**
   * Performs an action for the update timer by rendering a new background image
   * @param  evt
   */
  public void actionPerformed(ActionEvent evt) { // render a new image if the current image is dirty
    if(dirtyImage||needsUpdate()) {
      render(); // renders the scene from within the timer thread
    }
  }

  public void setIgnoreRepaint(boolean ignoreRepaint) {
    super.setIgnoreRepaint(ignoreRepaint);
    glassPanel.setIgnoreRepaint(ignoreRepaint);
  }

  /**
   * Update the panel's buffered image from within a separate timer thread.
   */
  private void updatePanel() {
    if(getIgnoreRepaint()) {
      return; // the animation thread will take care of the update
    }
    updateTimer.setRepeats(false); // perform only one render event
    updateTimer.setCoalesce(true); // coalesce render events
    updateTimer.start();           // start update timer
  }

  /**
   * Paints the component by copying the offscreen image into the graphics context.
   * @param g Graphics
   */
  public void paintComponent(Graphics g) {
    // find the clipping rectangle within a scroll pane viewport
    viewRect = null;
    Container c = getParent();
    while(c!=null) {
      if(c instanceof JViewport) {
        viewRect = ((JViewport) c).getViewRect();
        glassPanel.setBounds(viewRect);
        glassPanelLayout.checkLayoutRect(glassPanel, viewRect);
        break;
      }
      c = c.getParent();
    }
    int xoff = (getWidth()-offscreenImage.getWidth())/2;
    int yoff = (getHeight()-offscreenImage.getHeight())/2;
    g.drawImage(offscreenImage, xoff, yoff, null); // copy image to the center of the panel
    if(dirtyImage||needsUpdate()) { // Paco : can this be commented out?
      updatePanel();                // starts an update timer event
    }
  }

  /**
   * Invalidates this component.  This component and all parents
   * above it are marked as needing to be laid out.  This method can
   * be called often, so it needs to execute quickly.
   * @see       #validate
   * @see       #doLayout
   * @see       LayoutManager
   * @since     JDK1.0
   */
  public void invalidate() {
    needResize = true;
    super.invalidate();
  }

  public BufferedImage render(BufferedImage image) {
    Graphics g = image.getGraphics();
    paintEverything(g, image.getWidth(null), image.getHeight(null));
    Rectangle viewRect = this.viewRect; // reference for thread safety
    if(viewRect!=null) {
      Rectangle r = new Rectangle(0, 0, image.getWidth(null), image.getHeight(null));
      glassPanel.setBounds(r);
      glassPanelLayout.checkLayoutRect(glassPanel, r);
      glassPanel.render(g);
      glassPanel.setBounds(viewRect);
      glassPanelLayout.checkLayoutRect(glassPanel, viewRect);
    } else {
      glassPanel.render(g);
    }
    g.dispose(); // Disposes of the graphics context and releases any system resources that it is using.
    return image;
  }

  public BufferedImage render() {
    if(!isShowing()||isIconified()) { // don't render if panel cannot be seen
      needsToRecompute = true;        // make sure we recompute later when we are showing
      return null;                    // no need to render if the frame is not visible
    }
    BufferedImage workingImage = checkImageSize(this.workingImage);
    synchronized(workingImage) {          // do not let threads access workingImage while it is being painted
      if(needResize) {
        computeConstants(workingImage.getWidth(), workingImage.getHeight());
        needResize = false;
      }
      render(workingImage);
      // swap the images
      this.workingImage = offscreenImage; // use current offscreen image for the next drawing
      offscreenImage = workingImage;      // recently drawn image is now the offscreenImage
      dirtyImage = false;                 // offscreenImage is up to date
    }
    // the offscreenImage is now ready to be copied to the screen
    // always update a Swing component from the event thread
    if(SwingUtilities.isEventDispatchThread()) {
      paintImmediately(getVisibleRect()); // we are already within the event thread so DO IT!
    } else {                              // paint within the event thread
      Runnable doNow = new Runnable() {   // runnable object will be called by invokeAndWait
        public void run() {
          paintImmediately(getVisibleRect());
        }

      };
      try {
        SwingUtilities.invokeAndWait(doNow);
      } // wait for the paint operation to finish; should be fast
        catch(InvocationTargetException ex) {}
      catch(InterruptedException ex) {}
    }
    if((vidCap!=null)&&(offscreenImage!=null)&&vidCap.isRecording()) { // buffered image should exists so use it.
      vidCap.addFrame(offscreenImage);
    }
    return workingImage;
  }

  /**
   * Whether the image is dirty or any of the elements has changed
   * @return boolean
   */
  private final boolean needsUpdate() {
    for(Iterator<Element> it = elementList.iterator(); it.hasNext(); ) {
      if((it.next()).getElementChanged()) {
        return true;
      }
    }
    return false;
  }

  /**
   * Checks the image to see if the working image has the correct Dimension.
   * @return <code>true <\code> if the offscreen image matches the panel;  <code>false <\code> otherwise
   */
  private BufferedImage checkImageSize(BufferedImage image) {
    int width = getWidth(), height = getHeight();
    if((width<=2)||(height<=2)) { // image is too small to draw anything useful
      return new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
    }
    if((image==null)||(width!=image.getWidth())||(height!=image.getHeight())) {
      // a new image with the correct size will be created
      return getGraphicsConfiguration().createCompatibleImage(width, height);
    }
    return image; // given image is the correct size
  }

  /**
   * Gets the iconified flag from the top level frame.
   * @return boolean true if frame is iconified; false otherwise
   */
  private boolean isIconified() {
    Component c = getTopLevelAncestor();
    if(c instanceof Frame) {
      return(((Frame) c).getExtendedState()&Frame.ICONIFIED)==1;
    }
    return false;
  }

  // ---------------------------------
  // end of W. Christian additions and changes
  // ---------------------------------
  // ---------------------------------
  // Implementation of core.DrawingPanel3D
  // ---------------------------------
  public java.awt.Component getComponent() {
    return this;
  }

  public void setBackgroundImage(String _imageFile) {
    this.imageFile = _imageFile;
  }

  public String getBackgroundImage() {
    return this.imageFile;
  }

  public void setPreferredMinMax(double minX, double maxX, double minY, double maxY, double minZ, double maxZ) {
    this.xmin = minX;
    this.xmax = maxX;
    this.ymin = minY;
    this.ymax = maxY;
    this.zmin = minZ;
    this.zmax = maxZ;
    centerX = (xmax+xmin)/2.0;
    centerY = (ymax+ymin)/2.0;
    centerZ = (zmax+zmin)/2.0;
    maximumSize = getMaximum3DSize();
    resetDecoration(xmax-xmin, ymax-ymin, zmax-zmin);
    camera.reset();
    needsToRecompute = true;
    dirtyImage = true;
  }

  final public double getPreferredMinX() {
    return this.xmin;
  }

  final public double getPreferredMaxX() {
    return this.xmax;
  }

  final public double getPreferredMinY() {
    return this.ymin;
  }

  final public double getPreferredMaxY() {
    return this.ymax;
  }

  final public double getPreferredMinZ() {
    return this.zmin;
  }

  final public double getPreferredMaxZ() {
    return this.zmax;
  }

  final double[] getCenter() {
    return new double[] {centerX, centerY, centerZ};
  }

  final double getMaximum3DSize() {
    double dx = xmax-xmin, dy = ymax-ymin, dz = zmax-zmin;
    switch(camera.getProjectionMode()) {
       case org.opensourcephysics.display3d.core.Camera.MODE_PLANAR_XY :
         return Math.max(dx, dy);
       case org.opensourcephysics.display3d.core.Camera.MODE_PLANAR_XZ :
         return Math.max(dx, dz);
       case org.opensourcephysics.display3d.core.Camera.MODE_PLANAR_YZ :
         return Math.max(dy, dz);
       default :
         return Math.max(Math.max(dx, dy), dz); /* 3D */
    }
  }

  public void zoomToFit() {
    double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
    double minY = Double.POSITIVE_INFINITY, maxY = Double.NEGATIVE_INFINITY;
    double minZ = Double.POSITIVE_INFINITY, maxZ = Double.NEGATIVE_INFINITY;
    double[] firstPoint = new double[3], secondPoint = new double[3];
    Iterator<org.opensourcephysics.display3d.core.Element> it = getElements().iterator();
    while(it.hasNext()) {
      ((org.opensourcephysics.display3d.simple3d.Element) it.next()).getExtrema(firstPoint, secondPoint);
      minX = Math.min(Math.min(minX, firstPoint[0]), secondPoint[0]);
      maxX = Math.max(Math.max(maxX, firstPoint[0]), secondPoint[0]);
      minY = Math.min(Math.min(minY, firstPoint[1]), secondPoint[1]);
      maxY = Math.max(Math.max(maxY, firstPoint[1]), secondPoint[1]);
      minZ = Math.min(Math.min(minZ, firstPoint[2]), secondPoint[2]);
      maxZ = Math.max(Math.max(maxZ, firstPoint[2]), secondPoint[2]);
    }
    double max = Math.max(Math.max(maxX-minX, maxY-minY), maxZ-minZ);
    if(max==0.0) {
      max = 2;
    }
    if(minX>=maxX) {
      minX = maxX-max/2;
      maxX = minX+max;
    }
    if(minY>=maxY) {
      minY = maxY-max/2;
      maxY = minY+max;
    }
    if(minZ>=maxZ) {
      minZ = maxZ-max/2;
      maxZ = minZ+max;
    }
    setPreferredMinMax(minX, maxX, minY, maxY, minZ, maxZ);
  }

  public void setSquareAspect(boolean square) {
    // added by W. Christian
    if(squareAspect!=square) { // only recompute if there is a change
      needsToRecompute = true;
      updatePanel();
    }
    squareAspect = square;
    // computeConstants(); // removed by W. Christian
  }

  public boolean isSquareAspect() {
    return squareAspect;
  }

  public org.opensourcephysics.display3d.core.VisualizationHints getVisualizationHints() {
    return visHints;
  }

  public org.opensourcephysics.display3d.core.Camera getCamera() {
    return camera;
  }

  /**
   * Gets the video capture tool. May be null.
   *
   * @return the video capture tool
   */
  public VideoTool getVideoTool() {
    return vidCap;
  }

  /**
   * Sets the video capture tool. May be set to null.
   *
   * @param videoCap the video capture tool
   */
  public void setVideoTool(VideoTool videoCap) {
    if(vidCap!=null) {
      vidCap.setVisible(false); // hide the current video capture tool
    }
    vidCap = videoCap;
  }

  public void addElement(org.opensourcephysics.display3d.core.Element element) {
    if(!(element instanceof org.opensourcephysics.display3d.simple3d.Element)) {
      throw new UnsupportedOperationException("Can't add element to panel (incorrect implementation)"); //$NON-NLS-1$
    }
    if(!elementList.contains(element)) {
      elementList.add((org.opensourcephysics.display3d.simple3d.Element) element);
    }
    //CJB
    //Scale factor
    switch(DrawingPanel3D.axisMode) {
       case org.opensourcephysics.display3d.core.DrawingPanel3D.MODE_XZY :
         ((Element) element).setSizeX(((Element) element).getSizeX()*this.factorX);
         ((Element) element).setSizeZ(((Element) element).getSizeY()*this.factorZ);
         ((Element) element).setSizeY(((Element) element).getSizeZ()*this.factorY);
         ((Element) element).setX(((Element) element).getX()*this.factorX);
         ((Element) element).setZ(((Element) element).getY()*this.factorZ);
         ((Element) element).setY(((Element) element).getZ()*this.factorY);
         break;
       case org.opensourcephysics.display3d.core.DrawingPanel3D.MODE_YXZ :
         ((Element) element).setSizeY(((Element) element).getSizeX()*this.factorY);
         ((Element) element).setSizeX(((Element) element).getSizeY()*this.factorX);
         ((Element) element).setSizeZ(((Element) element).getSizeZ()*this.factorZ);
         ((Element) element).setY(((Element) element).getX()*this.factorY);
         ((Element) element).setX(((Element) element).getY()*this.factorX);
         ((Element) element).setZ(((Element) element).getZ()*this.factorZ);
         break;
       case org.opensourcephysics.display3d.core.DrawingPanel3D.MODE_YZX :
         ((Element) element).setSizeZ(((Element) element).getSizeX()*this.factorZ);
         ((Element) element).setSizeX(((Element) element).getSizeY()*this.factorX);
         ((Element) element).setSizeY(((Element) element).getSizeZ()*this.factorY);
         ((Element) element).setZ(((Element) element).getX()*this.factorZ);
         ((Element) element).setX(((Element) element).getY()*this.factorX);
         ((Element) element).setY(((Element) element).getZ()*this.factorY);
         break;
       case org.opensourcephysics.display3d.core.DrawingPanel3D.MODE_ZXY :
         ((Element) element).setSizeY(((Element) element).getSizeX()*this.factorY);
         ((Element) element).setSizeZ(((Element) element).getSizeY()*this.factorZ);
         ((Element) element).setSizeX(((Element) element).getSizeZ()*this.factorX);
         ((Element) element).setY(((Element) element).getX()*this.factorY);
         ((Element) element).setZ(((Element) element).getY()*this.factorZ);
         ((Element) element).setX(((Element) element).getZ()*this.factorX);
         break;
       case org.opensourcephysics.display3d.core.DrawingPanel3D.MODE_ZYX :
         ((Element) element).setSizeZ(((Element) element).getSizeX()*this.factorZ);
         ((Element) element).setSizeY(((Element) element).getSizeY()*this.factorY);
         ((Element) element).setSizeX(((Element) element).getSizeZ()*this.factorX);
         ((Element) element).setZ(((Element) element).getX()*this.factorZ);
         ((Element) element).setY(((Element) element).getY()*this.factorY);
         ((Element) element).setX(((Element) element).getZ()*this.factorX);
         break;
       default :
         ((Element) element).setSizeX(((Element) element).getSizeX()*this.factorX);
         ((Element) element).setSizeY(((Element) element).getSizeY()*this.factorY);
         ((Element) element).setSizeZ(((Element) element).getSizeZ()*this.factorZ);
         ((Element) element).setX(((Element) element).getX()*this.factorX);
         ((Element) element).setY(((Element) element).getY()*this.factorY);
         ((Element) element).setZ(((Element) element).getZ()*this.factorZ);
       //CJB
    }
    ((Element) element).setPanel(this);
    dirtyImage = true; // element has been added so image is dirty
  }

  public void removeElement(org.opensourcephysics.display3d.core.Element element) {
    elementList.remove(element);
    dirtyImage = true; // element has been added so image is dirty
  }

  public void removeAllElements() {
    elementList.clear();
    dirtyImage = true; // element has been added so image is dirty
  }

  public synchronized java.util.List<org.opensourcephysics.display3d.core.Element> getElements() {
    return new ArrayList<org.opensourcephysics.display3d.core.Element>(elementList);
  }

  //CJB
  public void setScaleFactor(double factorX, double factorY, double factorZ) {
    this.factorX = factorX;
    this.factorY = factorY;
    this.factorZ = factorZ;
  }

  public double getScaleFactorX() {
    return factorX;
  }

  public double getScaleFactorY() {
    return factorY;
  }

  public double getScaleFactorZ() {
    return factorZ;
  }

  public void setAxesMode(int mode) {
    BuildAxesPanel(mode);
    for(int i = 0; i<elementList.size(); i++) {
      Element el = elementList.get(i);
      el.setXYZ(el.getX(), el.getY(), el.getZ());
      el.setSizeXYZ(el.getSizeX(), el.getSizeY(), el.getSizeZ());
    }
  }

  public int getAxesMode() {
    return axisMode;
  }

  //CJB

  /**
   * Shows a message in a yellow text box in the lower right hand corner.
   *
   * @param msg
   */
  public void setMessage(String msg) {
    brMessageBox.setText(msg); // the default message box
  }

  /**
   * Shows a message in a yellow text box.
   * The location must be one of the following:
   * <ul>
   *   <li> DrawingPanel3D.BOTTOM_LEFT;
   *   <li> DrawingPanel3D.BOTTOM_RIGHT;
   *   <li> DrawingPanel3D.TOP_RIGHT;
   *   <li> DrawingPanel3D.TOP_LEFT;
   * </ul>
   * @param msg
   * @param location
   */
  public void setMessage(String msg, int location) {
    switch(location) {
       case BOTTOM_LEFT :
         blMessageBox.setText(msg);
         break;
       default :
       case BOTTOM_RIGHT :
         brMessageBox.setText(msg);
         break;
       case TOP_RIGHT :
         trMessageBox.setText(msg);
         break;
       case TOP_LEFT :
         tlMessageBox.setText(msg);
         break;
    }
  }

  // ---------------------------------
  // Implementation of core.InteractionSource
  // ---------------------------------
  public org.opensourcephysics.display3d.core.interaction.InteractionTarget getInteractionTarget(int target) {
    return myTarget;
  }

  public void addInteractionListener(InteractionListener listener) {
    if((listener==null)||listeners.contains(listener)) {
      return;
    }
    listeners.add(listener);
  }

  public void removeInteractionListener(InteractionListener listener) {
    listeners.remove(listener);
  }

  /**
   * Invokes the interactionPerformed() method of all registered
   * interaction listeners
   * @param event InteractionEvent
   */
  private void invokeActions(InteractionEvent event) {
    Iterator<InteractionListener> it = listeners.iterator();
    while(it.hasNext()) {
      it.next().interactionPerformed(event);
    }
  }

  // ----------------------------------------------------
  // All the painting stuff
  // ----------------------------------------------------

  /**
   * Paints everyting assuming an object of the given width and height in pixels.
   * @param g Graphics
   * @param width int
   * @param height int
   */
  private synchronized void paintEverything(Graphics g, int width, int height) {
    // W. Christian recompute scale if something has changed
    if(needsToRecompute||(width!=getWidth())||(height!=getHeight())) {
      computeConstants(width, height);
    }
    java.util.List<org.opensourcephysics.display3d.core.Element> tempList = getElements();
    tempList.addAll(decorationList);
    g.setColor(getBackground());
    g.fillRect(0, 0, width, height); // fill the component with the background color
    paintDrawableList(g, tempList);
  }

  private void paintDrawableList(Graphics g, java.util.List<org.opensourcephysics.display3d.core.Element> tempList) {
    Graphics2D g2 = (Graphics2D) g;
    Iterator<org.opensourcephysics.display3d.core.Element> it = tempList.iterator();
    if(quickRedrawOn||!visHints.isRemoveHiddenLines()) { // Do a quick sketch of the scene
      while(it.hasNext()) {
        ((Element) it.next()).drawQuickly(g2);
      }
      return;
    }
    // Collect objects, sort and draw them one by one. Takes time!!!
    list3D.clear();
    while(it.hasNext()) { // Collect all Objects3D
      Object3D[] objects = ((Element) it.next()).getObjects3D();
      if(objects==null) {
        continue;
      }
      for(int i = 0, n = objects.length; i<n; i++) {
        // providing NaN as distance can be used by Drawables3D to hide a given Object3D
        if(!Double.isNaN(objects[i].getDistance())) {
          list3D.add(objects[i]);
        }
      }
    }
    if(list3D.size()<=0) {
      return;
    }
    Object3D[] objects = list3D.toArray(new Object3D[0]);
    Arrays.sort(objects, comparator);
    for(int i = 0, n = objects.length; i<n; i++) {
      Object3D obj = objects[i];
      obj.getElement().draw(g2, obj.getIndex());
    }
  }

  // ----------------------------------------------------
  // Printable interface
  // ----------------------------------------------------
  public int print(Graphics g, PageFormat pageFormat, int pageIndex) throws PrinterException {
    if(pageIndex>=1) {
      return Printable.NO_SUCH_PAGE;
    }
    if(g==null) {
      return Printable.NO_SUCH_PAGE;
    }
    Graphics2D g2 = (Graphics2D) g;
    double scalex = pageFormat.getImageableWidth()/getWidth();
    double scaley = pageFormat.getImageableHeight()/getHeight();
    double scale = Math.min(scalex, scaley);
    g2.translate((int) pageFormat.getImageableX(), (int) pageFormat.getImageableY());
    g2.scale(scale, scale);
    paintEverything(g2, getWidth(), getHeight());
    return Printable.PAGE_EXISTS;
  }

  // ----------------------------------------------------
  // Projection, package and private methods
  // ----------------------------------------------------

  /**
   * This will be called by VisualizationHints whenever hints change.
   * @see VisualizationHints
   */
  void hintChanged(int hintThatChanged) {
    switch(hintThatChanged) {
       case VisualizationHints.HINT_ANY : {
         String[] labels = visHints.getAxesLabels();
         xText.setText(labels[0]);
         yText.setText(labels[1]);
         zText.setText(labels[2]);
         setCursorMode();
       }
       // do not break!
       case VisualizationHints.HINT_DECORATION_TYPE :
         switch(visHints.getDecorationType()) {
            case org.opensourcephysics.display3d.core.VisualizationHints.DECORATION_NONE :
              for(int i = 0, n = boxSides.length; i<n; i++) {
                boxSides[i].setVisible(false);
              }
              xAxis.setVisible(false);
              yAxis.setVisible(false);
              zAxis.setVisible(false);
              xText.setVisible(false);
              yText.setVisible(false);
              zText.setVisible(false);
              break;
            case org.opensourcephysics.display3d.core.VisualizationHints.DECORATION_CUBE :
              for(int i = 0, n = boxSides.length; i<n; i++) {
                boxSides[i].setVisible(true);
              }
              xAxis.setVisible(false);
              yAxis.setVisible(false);
              zAxis.setVisible(false);
              xText.setVisible(false);
              yText.setVisible(false);
              zText.setVisible(false);
              break;
            case org.opensourcephysics.display3d.core.VisualizationHints.DECORATION_AXES :
              for(int i = 0, n = boxSides.length; i<n; i++) {
                boxSides[i].setVisible(false);
              }
              xAxis.setVisible(true);
              yAxis.setVisible(true);
              zAxis.setVisible(true);
              xText.setVisible(true);
              yText.setVisible(true);
              zText.setVisible(true);
              break;
         }
         break;
       case VisualizationHints.HINT_AXES_LABELS :
         String[] labels = visHints.getAxesLabels();
         xText.setText(labels[0]);
         yText.setText(labels[1]);
         zText.setText(labels[2]);
         break;
       case VisualizationHints.HINT_CURSOR_TYPE :
         setCursorMode();
         break;
       case VisualizationHints.HINT_SHOW_COORDINATES :
         break; // Actually no dirtyImage is needed...
    }
    dirtyImage = true; // hint has changed so image is dirtry
  }

  /**
   * This will be called by Camera whenever it changes.
   * @see Camera
   */
  void cameraChanged(int howItChanged) {
    //     System.out.println ("Camera mode is now "+camera.getProjectionMode());
    switch(howItChanged) {
       case Camera.CHANGE_MODE :
         double dx = xmax-xmin, dy = ymax-ymin, dz = zmax-zmin;
         maximumSize = getMaximum3DSize();
         resetDecoration(dx, dy, dz);
         // W. Christian constants should be computed when everything is painted
         // computeConstants();
         needsToRecompute = true;
         updatePanel(); // always update if we change display mode
         break;
    }
    reportTheNeedToProject();
    dirtyImage = true; // camera has changed so image is dirtry
  }

  /**
   * Converts a 3D point of the scene into a 2D point of the screen.
   * It also provides a number measuring the relative distance of the point
   * to the camera.
   * distance = 1.0 means at the center of the scene,
   * distance > 1.0 means farther than the center of the scene,
   * distance < 1.0 means closer than the center of the scene,
   * @param coordinate The coordinates of the point of the scene
   * The input coordinates are not modified.
   * @param pixel A place-holder for the coordinates of the point of the screen.
   * It returns a,b and the distance
   * @return The coordinates of the point of the screen and a number
   * which reports about the distance to us
   */
  double[] project(double[] p, double[] pixel) {
    double[] projected = camera.getTransformation().direct(p.clone());
    double factor = 1.8;
    switch(camera.getProjectionMode()) {
       case org.opensourcephysics.display3d.core.Camera.MODE_NO_PERSPECTIVE :
       case org.opensourcephysics.display3d.core.Camera.MODE_PERSPECTIVE_OFF :
         factor = 1.3;
         break;
       case org.opensourcephysics.display3d.core.Camera.MODE_PERSPECTIVE :
       case org.opensourcephysics.display3d.core.Camera.MODE_PERSPECTIVE_ON :
         factor = 1;
         break;
    }
    pixel[0] = acenter+projected[0]*factor*aconstant;
    pixel[1] = bcenter-projected[1]*factor*bconstant;
    pixel[2] = projected[2];
    return pixel;
  }

  /**
   * Converts a world size at a given point into a size in the screen
   * @param p double[] The coordinates of the point at which the 3D
   * size was measured.
   * @param size double[] The size in the X,Y,Z coordinates
   * @param pixelSize double[] A place-holder for the result
   * @return double[] returns the same input pixelSize
   */
  double[] projectSize(double[] p, double[] size, double[] pixelSize) {
    camera.projectSize(p, size, pixelSize);
    double factor = 1.8;
    switch(camera.getProjectionMode()) {
       case org.opensourcephysics.display3d.core.Camera.MODE_NO_PERSPECTIVE :
       case org.opensourcephysics.display3d.core.Camera.MODE_PERSPECTIVE_OFF :
         factor = 1.3;
         break;
       case org.opensourcephysics.display3d.core.Camera.MODE_PERSPECTIVE :
       case org.opensourcephysics.display3d.core.Camera.MODE_PERSPECTIVE_ON :
         factor = 1;
         break;
    }
    pixelSize[0] *= factor*aconstant;
    pixelSize[1] *= factor*bconstant;
    return pixelSize;
  }

  /**
   * Computes the display color of a given drawable3D based on its original color and its depth.
   * Transparency of the original color is not affected.
   * @param _aColor the original color
   * @param _depth the depth value of the color
   */
  Color projectColor(Color _aColor, double _depth) {
    if(!visHints.isUseColorDepth()) {
      return _aColor;
    }
    // if      (_depth<0.9) return _aColor.brighter().brighter();
    // else if (_depth>1.1) return _aColor.darker().darker();
    // else return _aColor;
    float[] crc = new float[4]; // Stands for ColorRGBComponent
    try {
      _aColor.getRGBComponents(crc);
      // Do not affect transparency
      for(int i = 0; i<3; i++) {
        crc[i] /= _depth;
        crc[i] = (float) Math.max(Math.min(crc[i], 1.0), 0.0);
      }
      return new Color(crc[0], crc[1], crc[2], crc[3]);
    } catch(Exception _exc) {
      return _aColor;
    }
  }

  /**
   * Converts a point on the screen into a world point
   * It only works properly for planar display modes
   */
  private double[] worldPoint(int a, int b) {
    double factor = 1.8;
    switch(camera.getProjectionMode()) {
       case org.opensourcephysics.display3d.core.Camera.MODE_PLANAR_XY :
         return new double[] {centerX+(a-acenter)/(factor*aconstant), centerY+(bcenter-b)/(factor*bconstant), zmax};
       case org.opensourcephysics.display3d.core.Camera.MODE_PLANAR_XZ :
         return new double[] {centerX+(a-acenter)/(factor*aconstant), ymax, centerZ+(bcenter-b)/(factor*bconstant)};
       case org.opensourcephysics.display3d.core.Camera.MODE_PLANAR_YZ :
         return new double[] {xmax, centerY+(a-acenter)/(factor*aconstant), centerZ+(bcenter-b)/(factor*bconstant)};
       default : /* 3D */
         return new double[] {centerX, centerY, centerZ};
    }
  }

  /**
   * Converts into a world distance a distance on the screen
   * It only works properly for planar display modes
   */
  private double[] worldDistance(int dx, int dy) {
    double factor = 1.8;
    switch(camera.getProjectionMode()) {
       case org.opensourcephysics.display3d.core.Camera.MODE_PLANAR_XY :
         return new double[] {dx/(factor*aconstant), -dy/(factor*bconstant), 0.0};
       case org.opensourcephysics.display3d.core.Camera.MODE_PLANAR_XZ :
         return new double[] {dx/(factor*aconstant), 0.0, -dy/(factor*bconstant)};
       case org.opensourcephysics.display3d.core.Camera.MODE_PLANAR_YZ :
         return new double[] {0.0, dx/(factor*aconstant), -dy/(factor*bconstant)};
       default : /* 3D */
         return new double[] {dx/(1.3*aconstant), dy/(1.3*bconstant), 0.0};
    }
  }

  /**
   * Computes the constants for the given size in pixels.
   * @param width int
   * @param height int
   */
  private void computeConstants(int width, int height) {
    acenter = width/2;
    bcenter = height/2;
    if(squareAspect) {
      width = height = Math.min(width, height);
    }
    aconstant = 0.5*width/maximumSize;
    bconstant = 0.5*height/maximumSize;
    reportTheNeedToProject();
    needsToRecompute = false;
  }

  private void reportTheNeedToProject() {
    Iterator<org.opensourcephysics.display3d.core.Element> it = getElements().iterator();
    while(it.hasNext()) {
      ((Element) it.next()).setNeedToProject(true);
    }
    it = new ArrayList<org.opensourcephysics.display3d.core.Element>(decorationList).iterator();
    while(it.hasNext()) {
      ((Element) it.next()).setNeedToProject(true);
    }
  }

  private void resetDecoration(double _dx, double _dy, double _dz) {
    boxSides[0].setXYZ(xmin, ymin, zmin);
    boxSides[0].setSizeXYZ(_dx, 0.0, 0.0);
    boxSides[1].setXYZ(xmax, ymin, zmin);
    boxSides[1].setSizeXYZ(0.0, _dy, 0.0);
    boxSides[2].setXYZ(xmin, ymax, zmin);
    boxSides[2].setSizeXYZ(_dx, 0.0, 0.0);
    boxSides[3].setXYZ(xmin, ymin, zmin);
    boxSides[3].setSizeXYZ(0.0, _dy, 0.0);
    boxSides[4].setXYZ(xmin, ymin, zmax);
    boxSides[4].setSizeXYZ(_dx, 0.0, 0.0);
    boxSides[5].setXYZ(xmax, ymin, zmax);
    boxSides[5].setSizeXYZ(0.0, _dy, 0.0);
    boxSides[6].setXYZ(xmin, ymax, zmax);
    boxSides[6].setSizeXYZ(_dx, 0.0, 0.0);
    boxSides[7].setXYZ(xmin, ymin, zmax);
    boxSides[7].setSizeXYZ(0.0, _dy, 0.0);
    boxSides[8].setXYZ(xmin, ymin, zmin);
    boxSides[8].setSizeXYZ(0.0, 0.0, _dz);
    boxSides[9].setXYZ(xmax, ymin, zmin);
    boxSides[9].setSizeXYZ(0.0, 0.0, _dz);
    boxSides[10].setXYZ(xmax, ymax, zmin);
    boxSides[10].setSizeXYZ(0.0, 0.0, _dz);
    boxSides[11].setXYZ(xmin, ymax, zmin);
    boxSides[11].setSizeXYZ(0.0, 0.0, _dz);
    xAxis.setXYZ(xmin, ymin, zmin);
    xAxis.setSizeXYZ(_dx, 0.0, 0.0);
    xText.setXYZ(xmax+_dx*0.02, ymin, zmin);
    yAxis.setXYZ(xmin, ymin, zmin);
    yAxis.setSizeXYZ(0.0, _dy, 0.0);
    yText.setXYZ(xmin, ymax+_dy*0.02, zmin);
    zAxis.setXYZ(xmin, ymin, zmin);
    zAxis.setSizeXYZ(0.0, 0.0, _dz);
    zText.setXYZ(xmin, ymin, zmax+_dz*0.02);
  }

  // ----------------------------------------------------
  // Private methods for the cursor
  // ----------------------------------------------------
  private void setCursorMode() {
    switch(visHints.getCursorType()) {
       case org.opensourcephysics.display3d.core.VisualizationHints.CURSOR_NONE :
         trackersVisible = 0;
         break;
       case org.opensourcephysics.display3d.core.VisualizationHints.CURSOR_CUBE :
         trackersVisible = 9;
         break;
       default :
       case org.opensourcephysics.display3d.core.VisualizationHints.CURSOR_XYZ :
         trackersVisible = 3;
         break;
       case org.opensourcephysics.display3d.core.VisualizationHints.CURSOR_CROSSHAIR :
         trackersVisible = 3;
         break;
    }
  }

  private void showTrackers(boolean value) {
    for(int i = 0, n = trackerLines.length; i<n; i++) {
      if(i<trackersVisible) {
        trackerLines[i].setVisible(value);
      } else {
        trackerLines[i].setVisible(false);
      }
    }
  }

  private void positionTrackers() {
    switch(visHints.getCursorType()) {
       case org.opensourcephysics.display3d.core.VisualizationHints.CURSOR_NONE :
         return;
       default :
       case org.opensourcephysics.display3d.core.VisualizationHints.CURSOR_XYZ :
         trackerLines[0].setXYZ(trackerPoint[0], ymin, zmin);
         trackerLines[0].setSizeXYZ(0, trackerPoint[1]-ymin, 0);
         trackerLines[1].setXYZ(xmin, trackerPoint[1], zmin);
         trackerLines[1].setSizeXYZ(trackerPoint[0]-xmin, 0, 0);
         trackerLines[2].setXYZ(trackerPoint[0], trackerPoint[1], zmin);
         trackerLines[2].setSizeXYZ(0, 0, trackerPoint[2]-zmin);
         break;
       case org.opensourcephysics.display3d.core.VisualizationHints.CURSOR_CUBE :
         trackerLines[0].setXYZ(xmin, trackerPoint[1], trackerPoint[2]);
         trackerLines[0].setSizeXYZ(trackerPoint[0]-xmin, 0, 0);
         trackerLines[1].setXYZ(trackerPoint[0], ymin, trackerPoint[2]);
         trackerLines[1].setSizeXYZ(0, trackerPoint[1]-ymin, 0);
         trackerLines[2].setXYZ(trackerPoint[0], trackerPoint[1], zmin);
         trackerLines[2].setSizeXYZ(0, 0, trackerPoint[2]-zmin);
         trackerLines[3].setXYZ(trackerPoint[0], ymin, zmin);
         trackerLines[3].setSizeXYZ(0, trackerPoint[1]-ymin, 0);
         trackerLines[4].setXYZ(xmin, trackerPoint[1], zmin);
         trackerLines[4].setSizeXYZ(trackerPoint[0]-xmin, 0, 0);
         trackerLines[5].setXYZ(trackerPoint[0], ymin, zmin);
         trackerLines[5].setSizeXYZ(0, 0, trackerPoint[2]-zmin);
         trackerLines[6].setXYZ(xmin, ymin, trackerPoint[2]);
         trackerLines[6].setSizeXYZ(trackerPoint[0]-xmin, 0, 0);
         trackerLines[7].setXYZ(xmin, trackerPoint[1], zmin);
         trackerLines[7].setSizeXYZ(0, 0, trackerPoint[2]-zmin);
         trackerLines[8].setXYZ(xmin, ymin, trackerPoint[2]);
         trackerLines[8].setSizeXYZ(0, trackerPoint[1]-ymin, 0);
         break;
       case org.opensourcephysics.display3d.core.VisualizationHints.CURSOR_CROSSHAIR :
         trackerLines[0].setXYZ(xmin, trackerPoint[1], trackerPoint[2]);
         trackerLines[0].setSizeXYZ(xmax-xmin, 0.0, 0.0);
         trackerLines[1].setXYZ(trackerPoint[0], ymin, trackerPoint[2]);
         trackerLines[1].setSizeXYZ(0.0, ymax-ymin, 0.0);
         trackerLines[2].setXYZ(trackerPoint[0], trackerPoint[1], zmin);
         trackerLines[2].setSizeXYZ(0.0, 0.0, zmax-zmin);
         break;
    }
  }

  // ----------------------------------------------------
  // Interaction
  // ----------------------------------------------------
  private InteractionTarget getTargetHit(int x, int y) {
    Iterator<org.opensourcephysics.display3d.core.Element> it = getElements().iterator();
    InteractionTarget target = null;
    while(it.hasNext()) {
      target = ((Element) it.next()).getTargetHit(x, y);
      if(target!=null) {
        return target;
      }
    }
    return null;
  }

  private void setMouseCursor(Cursor cursor) {
    Container c = getTopLevelAncestor();
    setCursor(cursor);
    if(c!=null) {
      c.setCursor(cursor);
    }
  }

  private void displayPosition(double[] _point) {
    visHints.displayPosition(camera.getProjectionMode(), _point);
  }

  // returns true if the tracker was moved
  private boolean mouseDraggedComputations(java.awt.event.MouseEvent e) {
    if(e.isControlDown()) { // Panning
      if(camera.is3dMode()) {
        double fx = camera.getFocusX(), fy = camera.getFocusY(), fz = camera.getFocusZ();
        double dx = (e.getX()-lastX)*maximumSize*0.01, dy = (e.getY()-lastY)*maximumSize*0.01;
        switch(keyPressed) {
           case 88 :        // X is pressed
             if((camera.cosAlpha>=0)&&(Math.abs(camera.sinAlpha)<camera.cosAlpha)) {
               camera.setFocusXYZ(fx+dy, fy, fz);
             } else if((camera.sinAlpha>=0)&&(Math.abs(camera.cosAlpha)<camera.sinAlpha)) {
               camera.setFocusXYZ(fx+dx, fy, fz);
             } else if((camera.cosAlpha<0)&&(Math.abs(camera.sinAlpha)<-camera.cosAlpha)) {
               camera.setFocusXYZ(fx-dy, fy, fz);
             } else {
               camera.setFocusXYZ(fx-dx, fy, fz);
             }
             break;
           case 89 :        // Y is pressed
             if((camera.cosAlpha>=0)&&(Math.abs(camera.sinAlpha)<camera.cosAlpha)) {
               camera.setFocusXYZ(fx, fy-dx, fz);
             } else if((camera.sinAlpha>=0)&&(Math.abs(camera.cosAlpha)<camera.sinAlpha)) {
               camera.setFocusXYZ(fx, fy+dy, fz);
             } else if((camera.cosAlpha<0)&&(Math.abs(camera.sinAlpha)<-camera.cosAlpha)) {
               camera.setFocusXYZ(fx, fy+dx, fz);
             } else {
               camera.setFocusXYZ(fx, fy-dy, fz);
             }
             break;
           case 90 :        // Z is pressed
             if(camera.cosBeta>=0) {
               camera.setFocusXYZ(fx, fy, fz+dy);
             } else {
               camera.setFocusXYZ(fx, fy, fz-dy);
             }
             break;
           default :
             if(camera.cosBeta<0) {
               dy = -dy;
             }
             if((camera.cosAlpha>=0)&&(Math.abs(camera.sinAlpha)<camera.cosAlpha)) {
               camera.setFocusXYZ(fx, fy-dx, fz+dy);
             } else if((camera.sinAlpha>=0)&&(Math.abs(camera.cosAlpha)<camera.sinAlpha)) {
               camera.setFocusXYZ(fx+dx, fy, fz+dy);
             } else if((camera.cosAlpha<0)&&(Math.abs(camera.sinAlpha)<-camera.cosAlpha)) {
               camera.setFocusXYZ(fx, fy+dx, fz-dy);
             } else {
               camera.setFocusXYZ(fx-dx, fy, fz-dy);
             }
             break;
        }
      }
      return false;
    }                       // End of panning
    if(e.isShiftDown()) { // Zooming
      camera.setDistanceToScreen(camera.getDistanceToScreen()-(e.getY()-lastY)*maximumSize*0.01);
      return false;
    }
    if(camera.is3dMode()&&(targetHit==null)&&!e.isAltDown()) { // Rotating (in 3D)
      camera.setAzimuthAndAltitude(camera.getAzimuth()-(e.getX()-lastX)*0.01, camera.getAltitude()+(e.getY()-lastY)*0.005);
      return false;
    }
    if(trackerPoint==null) {
      return true;
    }
    // In all other cases, you are moving the tracker
    double[] point = worldDistance(e.getX()-lastX, e.getY()-lastY);
    if(!camera.is3dMode()) { // 2D modes
      switch(keyPressed) {
         case 88 :
           trackerPoint[0] += point[0];
           break;            // X is pressed
         case 89 :
           trackerPoint[1] += point[1];
           break;            // Y is pressed
         case 90 :
           trackerPoint[2] += point[2];
           break;            // Z is pressed
         default :
           trackerPoint[0] += point[0];
           trackerPoint[1] += point[1];
           trackerPoint[2] += point[2];
           break;            // No key is pressed
      }
    }                        // End of 2D modes
      else {
      switch(keyPressed) {
         case 88 :           // X is pressed
           if((camera.cosAlpha>=0)&&(Math.abs(camera.sinAlpha)<camera.cosAlpha)) {
             trackerPoint[0] += point[1];
           } else if((camera.sinAlpha>=0)&&(Math.abs(camera.cosAlpha)<camera.sinAlpha)) {
             trackerPoint[0] -= point[0];
           } else if((camera.cosAlpha<0)&&(Math.abs(camera.sinAlpha)<-camera.cosAlpha)) {
             trackerPoint[0] -= point[1];
           } else {
             trackerPoint[0] += point[0];
           }
           break;
         case 89 :           // Y is pressed
           if((camera.cosAlpha>=0)&&(Math.abs(camera.sinAlpha)<camera.cosAlpha)) {
             trackerPoint[1] += point[0];
           } else if((camera.sinAlpha>=0)&&(Math.abs(camera.cosAlpha)<camera.sinAlpha)) {
             trackerPoint[1] += point[1];
           } else if((camera.cosAlpha<0)&&(Math.abs(camera.sinAlpha)<-camera.cosAlpha)) {
             trackerPoint[1] -= point[0];
           } else {
             trackerPoint[1] -= point[1];
           }
           break;
         case 90 :           // Z is pressed
           if(camera.cosBeta>=0) {
             trackerPoint[2] -= point[1];
           } else {
             trackerPoint[2] -= point[2];
           }
           break;
         default :           // No key is pressed
           if(camera.cosBeta>=0) {
             trackerPoint[2] -= point[1];
           } else {
             trackerPoint[2] += point[1];
           }
           if((camera.cosAlpha>=0)&&(Math.abs(camera.sinAlpha)<camera.cosAlpha)) {
             trackerPoint[1] += point[0];
           } else if((camera.sinAlpha>=0)&&(Math.abs(camera.cosAlpha)<camera.sinAlpha)) {
             trackerPoint[0] -= point[0];
           } else if((camera.cosAlpha<0)&&(Math.abs(camera.sinAlpha)<-camera.cosAlpha)) {
             trackerPoint[1] -= point[0];
           } else {
             trackerPoint[0] += point[0];
           }
           break;
      }
    }                        // End of 3D modes
    return true;
  }

  private void resetInteraction() {
    targetHit = null;
    showTrackers(false);
    displayPosition(null);
    // blMessageBox.setText(null);
    // repaint();  removed by W. Christian
    dirtyImage = true;
    updatePanel();
  }

  /**
   * The inner class that will handle all mouse related events.
   */
  private class IADMouseController extends MouseInputAdapter {
    public void mousePressed(MouseEvent _evt) {
      requestFocus();
      if(_evt.isPopupTrigger()||(_evt.getModifiers()==InputEvent.BUTTON3_MASK)) {
        return;
      }
      //         quickRedrawOn = visHints.isAllowQuickRedraw() || keyPressed==83;  // 's' is pressed
      /*
      if(visHints.isAllowQuickRedraw()&&((_evt.getModifiers()&InputEvent.BUTTON1_MASK)!=0)) {
         quickRedrawOn = true;
      } else {
         quickRedrawOn = false;
      }
*/
      lastX = _evt.getX();
      lastY = _evt.getY();
      targetHit = getTargetHit(lastX, lastY);
      if(targetHit!=null) {
        Element el = targetHit.getElement();
        trackerPoint = el.getHotSpot(targetHit);
        el.invokeActions(new InteractionEvent(el, InteractionEvent.MOUSE_PRESSED, targetHit.getActionCommand(), targetHit, _evt));
        trackerPoint = el.getHotSpot(targetHit);     // because the listener may change the position of the element
      } else if(myTarget.isEnabled()) {              // No interactive has been hit
        if((!camera.is3dMode())||_evt.isAltDown()) { // In 2D by default, in 3D only if you hold ALT down
          // You are trying to track a given point
          trackerPoint = worldPoint(_evt.getX(), _evt.getY());
          invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_PRESSED, myTarget.getActionCommand(), trackerPoint, _evt));
        } else {
          invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_PRESSED, myTarget.getActionCommand(), null, _evt));
          resetInteraction();
          return;
        }
      } else {
        resetInteraction();
        return;
      }
      displayPosition(trackerPoint);
      positionTrackers();
      showTrackers(true);
      // repaint();  removed by W. Christian
      dirtyImage = true;
      updatePanel();
    }

    public void mouseReleased(MouseEvent _evt) {
      if(_evt.isPopupTrigger()||(_evt.getModifiers()==InputEvent.BUTTON3_MASK)) {
        return;
      }
      if(targetHit!=null) {
        Element el = targetHit.getElement();
        el.invokeActions(new InteractionEvent(el, InteractionEvent.MOUSE_RELEASED, targetHit.getActionCommand(), targetHit, _evt));
      } else if(myTarget.isEnabled()) {
        if((!camera.is3dMode())||_evt.isAltDown()) { // In 2D by default, in 3D only if you hold ALT down
          invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_RELEASED, myTarget.getActionCommand(), trackerPoint, _evt));
        } else {
          invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_RELEASED, myTarget.getActionCommand(), null, _evt));
        }
      }
      quickRedrawOn = false;
      resetInteraction();
      // setMouseCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
    }

    public void mouseDragged(MouseEvent _evt) {
      if(_evt.isPopupTrigger()||(_evt.getModifiers()==InputEvent.BUTTON3_MASK)) {
        return;
      }
      quickRedrawOn = visHints.isAllowQuickRedraw()&&(keyPressed!=83);
      boolean trackerMoved = mouseDraggedComputations(_evt);
      lastX = _evt.getX();
      lastY = _evt.getY();
      if(!trackerMoved) { // Report any listener that the projection has changed. Data is NULL!
        invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_DRAGGED, myTarget.getActionCommand(), null, _evt));
        resetInteraction();
        return;
      }
      if(targetHit!=null) {
        Element el = targetHit.getElement();
        el.updateHotSpot(targetHit, trackerPoint);
        el.invokeActions(new InteractionEvent(el, InteractionEvent.MOUSE_DRAGGED, targetHit.getActionCommand(), targetHit, _evt));
        trackerPoint = el.getHotSpot(targetHit); // The listener may change the position of the element
        displayPosition(trackerPoint);
        positionTrackers();
        showTrackers(true);                      // should trackers appear only in 3D mode?
      } else if(myTarget.isEnabled()) {
        invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_DRAGGED, myTarget.getActionCommand(), trackerPoint, _evt));
        displayPosition(trackerPoint);
        positionTrackers();
        showTrackers(true); // should trackers appear only in 3D mode?
      }
      // repaint();  removed by W. Christian
      dirtyImage = true;
      updatePanel();
    }

    public void mouseEntered(MouseEvent _evt) {
      setMouseCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
      if(myTarget.isEnabled()) {
        invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_ENTERED, myTarget.getActionCommand(), null, _evt));
      }
      targetHit = targetEntered = null;
    }

    public void mouseExited(MouseEvent _evt) {
      setMouseCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
      if(myTarget.isEnabled()) {
        invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_EXITED, myTarget.getActionCommand(), null, _evt));
      }
      targetHit = targetEntered = null;
    }

    public void mouseClicked(MouseEvent _evt) {}

    public void mouseMoved(MouseEvent _evt) {
      InteractionTarget target = getTargetHit(_evt.getX(), _evt.getY());
      if(target!=null) {
        if(targetEntered==null) {
          target.getElement().invokeActions(new InteractionEvent(target.getElement(), InteractionEvent.MOUSE_ENTERED, target.getActionCommand(), target, _evt));
        }
        setMouseCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
      } else { // No target under the cursor
        if(targetEntered!=null) {
          targetEntered.getElement().invokeActions(new InteractionEvent(targetEntered.getElement(), InteractionEvent.MOUSE_EXITED, targetEntered.getActionCommand(), targetEntered, _evt));
        } else if(myTarget.isEnabled()) {
          invokeActions(new InteractionEvent(DrawingPanel3D.this, InteractionEvent.MOUSE_MOVED, myTarget.getActionCommand(), null, _evt));
        }
        setMouseCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
      }
      targetEntered = target;
    }

  }

  private class GlassPanel extends javax.swing.JPanel {
    public void render(Graphics g) {
      Component[] c = glassPanelLayout.getComponents();
      for(int i = 0, n = c.length; i<n; i++) {
        if(c[i]==null) {
          continue;
        }
        g.translate(c[i].getX(), c[i].getY());
        c[i].print(g);
        g.translate(-c[i].getX(), -c[i].getY());
      }
    }

  }

  // ----------------------------------------------------
  // Lights
  // ----------------------------------------------------
  public void setLightEnabled(boolean _state, int nlight) {} // simple3d supports no light control

  // ----------------------------------------------------
  // XML loader
  // ----------------------------------------------------

  /**
   * Returns an XML.ObjectLoader to save and load object data.
   * @return the XML.ObjectLoader
   */
  public static XML.ObjectLoader getLoader() {
    return new DrawingPanel3DLoader();
  }

  static private class DrawingPanel3DLoader extends org.opensourcephysics.display3d.core.DrawingPanel3D.Loader {
    public Object createObject(XMLControl control) {
      return new DrawingPanel3D();
    }

    public Object loadObject(XMLControl control, Object obj) {
      super.loadObject(control, obj);
      DrawingPanel3D panel = (DrawingPanel3D) obj;
      // Load the visualization hints
      VisualizationHints hints = (VisualizationHints) control.getObject("visualization hints"); //$NON-NLS-1$
      hints.setPanel(panel);
      panel.visHints = hints;
      panel.hintChanged(VisualizationHints.HINT_DECORATION_TYPE);
      // Load the camera
      Camera cam = (Camera) control.getObject("camera"); //$NON-NLS-1$
      cam.setPanel(panel);
      panel.camera = cam;
      panel.cameraChanged(Camera.CHANGE_ANY);
      // Order a render()
      panel.needsToRecompute = true;
      panel.dirtyImage = true; // new data so image is dirtry
      panel.updatePanel();
      return obj;
    }

  } // End of static class DrawingPanel3DLoader

}

/*
 * Open Source Physics software is free software; you can redistribute
 * it and/or modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License,
 * or(at your option) any later version.
 *
 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be released
 * under the GNU GPL license.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2019  The Open Source Physics project
 *                     https://www.compadre.org/osp
 */