/* -------------------------------------------------------------------------- *
 * OpenSim: FunctionPanel.java                                                *
 * -------------------------------------------------------------------------- *
 * OpenSim is a toolkit for musculoskeletal modeling and simulation,          *
 * developed as an open source project by a worldwide community. Development  *
 * and support is coordinated from Stanford University, with funding from the *
 * U.S. NIH and DARPA. See http://opensim.stanford.edu and the README file    *
 * for more information including specific grant numbers.                     *
 *                                                                            *
 * Copyright (c) 2005-2017 Stanford University and the Authors                *
 * Author(s): Ayman Habib, Peter Loan                                         *
 *                                                                            *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may    *
 * not use this file except in compliance with the License. You may obtain a  *
 * copy of the License at http://www.apache.org/licenses/LICENSE-2.0          *
 *                                                                            *
 * Unless required by applicable law or agreed to in writing, software        *
 * distributed under the License is distributed on an "AS IS" BASIS,          *
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   *
 * See the License for the specific language governing permissions and        *
 * limitations under the License.                                             *
 * -------------------------------------------------------------------------- */

/*
 * FunctionPanel.java
 *
 * Created on November 15, 2007, 10:11 AM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.opensim.view.functionEditor;

import java.awt.AWTEvent;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Vector;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.event.EventListenerList;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.annotations.XYTextAnnotation;
import org.jfree.chart.axis.Axis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.event.AxisChangeEvent;
import org.jfree.chart.event.AxisChangeListener;
import org.jfree.chart.labels.StandardXYToolTipGenerator;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.TextAnchor;
import org.opensim.modeling.ArrayInt;

/**
 *
 * @author Peter Loan
 */
public class FunctionPanel extends ChartPanel
                        implements KeyListener {

   private boolean picking = false;
   private boolean dragging = false;
   /** Crosshairs are displayed while dragging points or when
    * the crosshairs checkbox is selected.
    */
   private boolean showCrosshairs = false;
   private boolean mandatoryCrosshairs = false;
   private boolean domainAxisAutoRange = true;
   private boolean rangeAxisAutoRange = true;
   FunctionNode dragNode = null;
   int dragScreenXOld = -1, dragScreenYOld = -1;
   int lastMouseX = -1, lastMouseY = -1;
   double dragDataXOld = -99.9, dragDataYOld = -99.9;
   int lastLeftButtonClickCount = 0;
   FunctionNode lastLeftButtonClickNode = null;
   protected FunctionNode rightClickNode = null;
   private int rightClickX = -1;
   private int rightClickY = -1;
   private Point boxSelectPoint = null;
   private transient Rectangle2D boxSelectRectangle = null;
   protected ArrayList<FunctionNode> selectedNodes = new ArrayList<FunctionNode>(0);
   private ArrayList<FunctionNode> oldBoxSelectNodes = null;
   private final java.awt.Color boxSelectColor = java.awt.Color.green; // to get purple select box
   private XYLineAndShapeRendererWithHighlight renderer;
   protected JPopupMenu addNodePopUpMenu;
   private JPopupMenu nodePopUpMenu;
   private XYTextAnnotation crosshairAnnotation = null;
   public static final String DUPLICATE_NODE_COMMAND = "DUPLICATE_NODE";
   public static final String DELETE_NODE_COMMAND = "DELETE_NODE";
   public static final String ADD_NODE_COMMAND = "ADD_NODE";
   protected EventListenerList functionPanelListeners;
/*
   class EnforceExcitationRange implements AxisChangeListener {
      public void axisChanged(AxisChangeEvent event) {
         if (zero_to_one_checkbox is checked) {
            Axis axis = event.getAxis();
            if (axis instanceof ValueAxis) {
               ValueAxis va = (ValueAxis)axis;
               if (va.isAutoRange()) {
                  va.setAutoRange(false);
                  va.setRangeWithMargins(0.0, 1.0);
               }
            }
         }
      }
   }
*/
   /** Creates a new instance of FunctionPanel */
   public FunctionPanel(JFreeChart chart) {
      super(chart);
      //this.setFocusable(true);
      this.functionPanelListeners = new EventListenerList();
      this.renderer = (XYLineAndShapeRendererWithHighlight) chart.getXYPlot().getRenderer();
      this.enableEvents(AWTEvent.INPUT_METHOD_EVENT_MASK);
      this.enableEvents(AWTEvent.KEY_EVENT_MASK);
      this.addKeyListener(this);
      this.nodePopUpMenu = createNodePopupMenu();
      this.addNodePopUpMenu = createAddNodePopupMenu();
      this.crosshairAnnotation = new XYTextAnnotation("", 0, 0);
      this.crosshairAnnotation.setTextAnchor(TextAnchor.BOTTOM_LEFT);
      // Make sure the X and Y ranges are not zero, which messes up the display
      chart.getXYPlot().getDomainAxis().setAutoRangeMinimumSize(0.000001);
      chart.getXYPlot().getRangeAxis().setAutoRangeMinimumSize(0.000001);
      //chart.getXYPlot().getRangeAxis().setAutoRange(false);
      //chart.getXYPlot().getRangeAxis().setRangeWithMargins(0.0, 1.0);
      //chart.getXYPlot().getRangeAxis().addChangeListener(new EnforceExcitationRange());
   }

   public void updateCursorLocation(MouseEvent e) {
      lastMouseX = e.getX();
      lastMouseY = e.getY();
   }

   public XYLineAndShapeRendererWithHighlight getRenderer() {
      return this.renderer;
   }

   public void mouseEntered(MouseEvent e) {
      e.getComponent().requestFocusInWindow();
      updateCursorLocation(e);
      updateCrosshairs(e.getX(), e.getY());
   }

   public void mouseExited(MouseEvent e) {
      updateCursorLocation(e);
      updateCrosshairs(-1, -1);
      picking = false;
      endBoxSelect();
      super.mouseExited(e);
   }

   public void mousePressed(MouseEvent e) {
      this.requestFocusInWindow();
      updateCursorLocation(e);
      FunctionNode leftClickNode = null;
      int keyMods = e.getModifiers();
      if ((keyMods & InputEvent.BUTTON1_MASK) > 0) {
         leftClickNode = findNodeAt(e.getX(), e.getY());

         // Some code to handle double clicking on an object, but which does so in a way that the sequence
         // CTRL-Click and Click does not count as a double click.  This avoids
         // treating as double click the case where the user selects an object
         // (CTRL-Click) and quickly starts dragging (Click & Drag) 
         if (leftClickNode != null && picking == false) {
            if (e.getClickCount() == lastLeftButtonClickCount+1 && leftClickNode == lastLeftButtonClickNode) {
               //handleDoubleClick(leftClickNode);
               return; 
            } else {
               lastLeftButtonClickCount = e.getClickCount();
               lastLeftButtonClickNode = leftClickNode;
            } 
         } else {
            lastLeftButtonClickCount = -1;
            lastLeftButtonClickNode = null;
         }
      }

      if ((keyMods & InputEvent.BUTTON3_MASK) > 0) {
         rightClickNode = findNodeAt(e.getX(), e.getY());
         rightClickX = e.getX();
         rightClickY = e.getY();
      } else if (picking == true && (keyMods & InputEvent.BUTTON1_MASK) > 0) {
         if (leftClickNode == null) {
            // Picking mode is on, but the user clicked away from a control point.
            // So clear the selections (unless Shift is pressed) and prepare for a box select.
            if ((keyMods & InputEvent.SHIFT_MASK) <= 0)
               clearSelectedNodes();
            startBoxSelect(e);
         } else {
            if ((keyMods & InputEvent.SHIFT_MASK) > 0) {
               toggleSelectedNode(leftClickNode.series, leftClickNode.node);
            } else {
               replaceSelectedNode(leftClickNode.series, leftClickNode.node);
            }
         }
      } else if ((leftClickNode != null) && listContainsNode(leftClickNode, selectedNodes) == true) {
         XYPlot xyPlot = getChart().getXYPlot();
         dragNode = leftClickNode;
         dragScreenXOld = e.getX();
         dragScreenYOld = e.getY();
         RectangleEdge xAxisLocation = xyPlot.getDomainAxisEdge();
         RectangleEdge yAxisLocation = xyPlot.getRangeAxisEdge();
         Rectangle2D dataArea = getScreenDataArea();
         dragDataXOld = xyPlot.getDomainAxis().java2DToValue((double)e.getX(), dataArea, xAxisLocation);
         dragDataYOld = xyPlot.getRangeAxis().java2DToValue((double)e.getY(), dataArea, yAxisLocation);
         setDragging(true);
         // During dragging, the crosshairs lock onto the center of the dragNode
         double crosshairX = xyPlot.getDataset().getXValue(dragNode.series, dragNode.node);
         double crosshairY = xyPlot.getDataset().getYValue(dragNode.series, dragNode.node);
         double crosshairScreenX = xyPlot.getDomainAxis().valueToJava2D(crosshairX, dataArea, xAxisLocation);
         double crosshairScreenY = xyPlot.getRangeAxis().valueToJava2D(crosshairY, dataArea, yAxisLocation);
         updateCrosshairs((int)crosshairScreenX, (int)crosshairScreenY);
         picking = false;
      } else {
         super.mousePressed(e);
      }
   }

   public void mouseDragged(MouseEvent e) {
      updateCursorLocation(e);
      int keyMods = e.getModifiers();
      if (picking == true && (keyMods & InputEvent.BUTTON1_MASK) > 0) {
         // do nothing; you're still in picking mode
         dragScreenXOld = e.getX();
         dragScreenYOld = e.getY();
         updateCrosshairs(e.getX(), e.getY());
         doBoxSelect(e);
      } else if (getDragging() == true && (keyMods & InputEvent.BUTTON1_MASK) > 0) {
         if (e.getX() != dragScreenXOld || e.getY() != dragScreenYOld) {
            if (dragNode != null) {
               XYPlot xyPlot = getChart().getXYPlot();
               RectangleEdge xAxisLocation = xyPlot.getDomainAxisEdge();
               RectangleEdge yAxisLocation = xyPlot.getRangeAxisEdge();
               Rectangle2D dataArea = getScreenDataArea();
               double dragDataXNew = xyPlot.getDomainAxis().java2DToValue((double)e.getX(), dataArea, xAxisLocation);
               double dragDataYNew = xyPlot.getRangeAxis().java2DToValue((double)e.getY(), dataArea, yAxisLocation);
               if (dragDataXNew > xyPlot.getDomainAxis().getUpperBound())
                  dragDataXNew = xyPlot.getDomainAxis().getUpperBound();
               else if (dragDataXNew < xyPlot.getDomainAxis().getLowerBound())
                  dragDataXNew = xyPlot.getDomainAxis().getLowerBound();
               if (dragDataYNew > xyPlot.getRangeAxis().getUpperBound())
                  dragDataYNew = xyPlot.getRangeAxis().getUpperBound();
               else if (dragDataYNew < xyPlot.getRangeAxis().getLowerBound())
                  dragDataYNew = xyPlot.getRangeAxis().getLowerBound();
               // the amount to drag the objects is the distance between dragPtOld and dragPtNew
               double dragVector[] = new double[2];
               dragVector[0] = dragDataXNew - dragDataXOld;
               dragVector[1] = dragDataYNew - dragDataYOld;
               // drag the selected objects
               dragSelectedNodes(dragNode.series, dragNode.node, dragVector);
               // store the new point as the old, for use next time
               dragDataXOld = dragDataXNew;
               dragDataYOld = dragDataYNew;
               // During dragging, the crosshairs lock onto the center of the dragNode
               double crosshairX = xyPlot.getDataset().getXValue(dragNode.series, dragNode.node);
               double crosshairY = xyPlot.getDataset().getYValue(dragNode.series, dragNode.node);
               double crosshairScreenX = xyPlot.getDomainAxis().valueToJava2D(crosshairX, dataArea, xAxisLocation);
               double crosshairScreenY = xyPlot.getRangeAxis().valueToJava2D(crosshairY, dataArea, yAxisLocation);
               updateCrosshairs((int)crosshairScreenX, (int)crosshairScreenY);
            }
         }
         dragScreenXOld = e.getX();
         dragScreenYOld = e.getY();
      } else {
         super.mouseDragged(e);
      }
   }

   public void mouseMoved(MouseEvent e) {
      updateCursorLocation(e);
      if (showCrosshairs == true) {
         updateCrosshairs(e.getX(), e.getY());
      } else {
         super.mouseMoved(e);
      }
   }

   public void mouseReleased(MouseEvent e) {
      updateCursorLocation(e);
      int keyMods = e.getModifiers();
      if (picking == true && (keyMods & InputEvent.BUTTON1_MASK) > 0) {
         endBoxSelect();
      } else if (getDragging() == true && (keyMods & InputEvent.BUTTON1_MASK) > 0) {
         dragNode = null;
         dragScreenXOld = -1;
         dragScreenYOld = -1;
         dragDataXOld = -99.9;
         dragDataYOld = -99.9;
         setDragging(false);
      } else if ((keyMods & InputEvent.BUTTON3_MASK) > 0 && e.isPopupTrigger()) {
         if (rightClickNode != null && this.nodePopUpMenu != null)
            this.nodePopUpMenu.show(this, e.getX(), e.getY());
         else if (this.addNodePopUpMenu != null)
            this.addNodePopUpMenu.show(this, e.getX(), e.getY());
      } else {
         super.mouseReleased(e);
      }
   }

   private void startBoxSelect(MouseEvent e) {
      if (this.boxSelectRectangle == null) {
         Rectangle2D screenDataArea = getScreenDataArea(e.getX(), e.getY());
         if (screenDataArea != null) {
            this.boxSelectPoint = getPointInRectangle(e.getX(), e.getY(), screenDataArea);
         }
         else {
            this.boxSelectPoint = null;
         }
      }
   }

   private void doBoxSelect(MouseEvent e) {
      // if no initial boxSelect point was set, ignore dragging...
      if (this.boxSelectPoint == null)
         return;

      Graphics2D g2 = (Graphics2D) getGraphics();

      // Use XOR to erase the old rectangle, if any.
      g2.setXORMode(boxSelectColor);
      if (this.boxSelectRectangle != null)
         g2.draw(this.boxSelectRectangle);
      // Save the current paint color. You need to restore it
      // after highlighting the control points in order for
      // the XOR drawing of the box to work properly.
      Paint savedPaint = g2.getPaint();
      g2.setPaintMode();

      Rectangle2D scaledDataArea = getScreenDataArea(
         (int) this.boxSelectPoint.getX(), (int) this.boxSelectPoint.getY());
      // Box can be dragged in any direction, so compute proper min and max
      // given direction of drag and bounds of data area.
      double xmin=0, xmax=0, ymin=0, ymax=0;
      if (e.getX() < this.boxSelectPoint.getX()) {
         xmin = Math.max(e.getX(), scaledDataArea.getMinX());
         xmax = this.boxSelectPoint.getX();
      } else {
         xmin = this.boxSelectPoint.getX();
         xmax = Math.min(e.getX(), scaledDataArea.getMaxX());
      }
      if (e.getY() < this.boxSelectPoint.getY()) {
         ymin = Math.max(e.getY(), scaledDataArea.getMinY());
         ymax = this.boxSelectPoint.getY();
      } else {
         ymin = this.boxSelectPoint.getY();
         ymax = Math.min(e.getY(), scaledDataArea.getMaxY());
      }
      this.boxSelectRectangle = new Rectangle2D.Double(xmin, ymin, xmax - xmin, ymax - ymin);

      XYPlot xyPlot = getChart().getXYPlot();
      ArrayList<FunctionNode> newBoxSelectNodes = getBoxSelectNodes(this.boxSelectRectangle);
      // For the nodes that were picked up with the latest box resizing, toggle their select state
      for (int i=0; i<newBoxSelectNodes.size(); i++) {
         if (oldBoxSelectNodes == null || listContainsNode(newBoxSelectNodes.get(i), oldBoxSelectNodes) == false) {
            toggleSelectedNode(newBoxSelectNodes.get(i).series, newBoxSelectNodes.get(i).node);
            // Draw the control point with the new color
            xyPlot.getRenderer().drawItem(g2, null, getScreenDataArea(), null, xyPlot,
               xyPlot.getDomainAxis(), xyPlot.getRangeAxis(), xyPlot.getDataset(), newBoxSelectNodes.get(i).series,
               newBoxSelectNodes.get(i).node, null, 1);
         }
      }
      // For the nodes that dropped out with the latest box resizing, toggle their select state
      if (oldBoxSelectNodes != null) {
         for (int i=0; i<oldBoxSelectNodes.size(); i++) {
            if (listContainsNode(oldBoxSelectNodes.get(i), newBoxSelectNodes) == false) {
               toggleSelectedNode(oldBoxSelectNodes.get(i).series, oldBoxSelectNodes.get(i).node);
               // Draw the control point with the new color
               xyPlot.getRenderer().drawItem(g2, null, getScreenDataArea(), null, xyPlot,
                  xyPlot.getDomainAxis(), xyPlot.getRangeAxis(), xyPlot.getDataset(), oldBoxSelectNodes.get(i).series,
                  oldBoxSelectNodes.get(i).node, null, 1);
            }
         }
      }
      oldBoxSelectNodes = newBoxSelectNodes;

      // Use XOR to draw the new rectangle.
      g2.setPaint(savedPaint);
      g2.setXORMode(boxSelectColor);
      if (this.boxSelectRectangle != null)
         g2.draw(this.boxSelectRectangle);

      g2.dispose();
   }

   private void endBoxSelect() {
      // use XOR to erase the last zoom rectangle.
      if (this.boxSelectRectangle != null) {
         Graphics2D g2 = (Graphics2D) getGraphics();
         g2.setXORMode(boxSelectColor);
         g2.draw(this.boxSelectRectangle);
         this.boxSelectPoint = null;
         this.boxSelectRectangle = null;
         this.oldBoxSelectNodes = null;
      }
   }

   private ArrayList<FunctionNode> getBoxSelectNodes(Rectangle2D box) {
      ArrayList<FunctionNode> nodes = new ArrayList<FunctionNode>(0);
      XYPlot xyPlot = getChart().getXYPlot();
      XYDataset xyDataset = xyPlot.getDataset();
      // Compute dataBox (data coordinates, X right, Y up)
      // from box (screen coordinates, X right, Y down)
      RectangleEdge xAxisLocation = xyPlot.getDomainAxisEdge();
      RectangleEdge yAxisLocation = xyPlot.getRangeAxisEdge();
      Rectangle2D dataArea = getScreenDataArea();
      double dataXMin = xyPlot.getDomainAxis().java2DToValue(box.getMinX(), dataArea, xAxisLocation);
      double dataXMax = xyPlot.getDomainAxis().java2DToValue(box.getMaxX(), dataArea, xAxisLocation);
      double dataYMin = xyPlot.getRangeAxis().java2DToValue(box.getMaxY(), dataArea, yAxisLocation);
      double dataYMax = xyPlot.getRangeAxis().java2DToValue(box.getMinY(), dataArea, yAxisLocation);
      Rectangle2D dataBox = new Rectangle2D.Double(dataXMin, dataYMin, dataXMax - dataXMin, dataYMax - dataYMin);
      for (int i=0; i<xyDataset.getSeriesCount(); i++) {
         if (renderer.getSeriesShapesVisible(i)) {
            for (int j=0; j<xyDataset.getItemCount(i); j++) {
               double x = xyDataset.getXValue(i, j);
               double y = xyDataset.getYValue(i, j);
               if (dataBox.contains(x, y) == true)
                  nodes.add(new FunctionNode(i, j));
            }
         }
      }
      return nodes;
   }

   /* This method is in ChartPanel, but is private. */
   private Point getPointInRectangle(int x, int y, Rectangle2D area) {
       x = (int) Math.max(Math.ceil(area.getMinX()), Math.min(x, 
               Math.floor(area.getMaxX())));   
       y = (int) Math.max(Math.ceil(area.getMinY()), Math.min(y, 
               Math.floor(area.getMaxY())));
       return new Point(x, y);
   }

   public void updateCrosshairs(int screenX, int screenY) {
      XYPlot xyPlot = getChart().getXYPlot();
      if (showCrosshairs == true) {
         RectangleEdge xAxisLocation = xyPlot.getDomainAxisEdge();
         RectangleEdge yAxisLocation = xyPlot.getRangeAxisEdge();
         Rectangle2D dataArea = getScreenDataArea();
         double crosshairX = xyPlot.getDomainAxis().java2DToValue((double)screenX, dataArea, xAxisLocation);
         double crosshairY = xyPlot.getRangeAxis().java2DToValue((double)screenY, dataArea, yAxisLocation);
         if (crosshairX < xyPlot.getDomainAxis().getLowerBound() ||
             crosshairX > xyPlot.getDomainAxis().getUpperBound() ||
             crosshairY < xyPlot.getRangeAxis().getLowerBound() ||
             crosshairY > xyPlot.getRangeAxis().getUpperBound()) {
            xyPlot.setDomainCrosshairVisible(false);
            xyPlot.setRangeCrosshairVisible(false);
            crosshairAnnotation.setText("");
         } else {
            xyPlot.setDomainCrosshairVisible(true);
            xyPlot.setRangeCrosshairVisible(true);
            /** The xyPlot's crosshairs are updated with the screen coordinates of the XY location.
             * The annotation's text and location is updated with the data coordinates.
             * The format used for displaying the XY data coordinates is taken from the
             * format used for the X and Y axis tick labels. This format is not normally
             * accessible here, but a method was added to NumberTickUnit to provide it.
             * If this turns out to be problematic, the format could always be changed
             * to use a fixed number of significant digits.
             */
            NumberAxis dna = (NumberAxis)xyPlot.getDomainAxis();
            NumberFormat dnf = (NumberFormat)dna.getTickUnit().getFormatter().clone();
            dnf.setMaximumFractionDigits(dnf.getMaximumFractionDigits()+3);
            String xString = dnf.format(crosshairX);
            NumberAxis rna = (NumberAxis)xyPlot.getRangeAxis();
            NumberFormat rnf = (NumberFormat)rna.getTickUnit().getFormatter().clone();
            rnf.setMaximumFractionDigits(rnf.getMaximumFractionDigits()+3);
            String yString = rnf.format(crosshairY);
            crosshairAnnotation.setText("(" + xString + ", " + yString + ")");
            crosshairAnnotation.setX(crosshairX);
            crosshairAnnotation.setY(crosshairY);
            xyPlot.setDomainCrosshairValue(crosshairX);
            xyPlot.setRangeCrosshairValue(crosshairY);
            // JPL 11/19/09: the chart's plotInfo does not appear to be updated when the
            // FunctionPanel is resized. So the following call to handleClick will pass in
            // the wrong plot area for calculating crosshair coordinates. So instead, set
            // the crosshair directly with the two lines above this comment.
            //xyPlot.handleClick(screenX, screenY, this.getChartRenderingInfo().getPlotInfo());
         }
      }
   }

   public void setDragging(boolean state) {
      XYPlot xyPlot = getChart().getXYPlot();
      dragging = state;
      if (state == true) {
         // Save the autoRange state, then set it to false for dragging.
         domainAxisAutoRange = xyPlot.getDomainAxis().isAutoRange();
         rangeAxisAutoRange = xyPlot.getRangeAxis().isAutoRange();
         xyPlot.getDomainAxis().setAutoRange(false);
         xyPlot.getRangeAxis().setAutoRange(false);
      } else {
         // Restore the autoRange state
         xyPlot.getDomainAxis().setAutoRange(domainAxisAutoRange);
         xyPlot.getRangeAxis().setAutoRange(rangeAxisAutoRange);
      }
      if (mandatoryCrosshairs == false)
         setCrosshairsState(state);
   }

   public boolean getDragging() {
      return this.dragging;
   }

   public void setMandatoryCrosshairs(boolean state) {
      mandatoryCrosshairs = state;
      setCrosshairsState(state);
      if (state == true)
         updateCrosshairs(-1, -1);
   }

   public void setCrosshairsState(boolean state) {
      XYPlot xyPlot = getChart().getXYPlot();
      // With two ways to turn on/off crosshairs, it's possible
      // to call this method twice in a row with state=true.
      // But you want to add the annotation only once.
      if (state == true && showCrosshairs == false) {
         crosshairAnnotation.setText("");
         xyPlot.addAnnotation(crosshairAnnotation);
      } else if (state == false) {
         xyPlot.removeAnnotation(crosshairAnnotation);
      }
      if (state == false) {
         xyPlot.setDomainCrosshairVisible(false);
         xyPlot.setRangeCrosshairVisible(false);
      }
      showCrosshairs = state;
   }

   public void keyPressed(KeyEvent e) {
      if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
         picking = true;
      } else if (e.getKeyCode() == KeyEvent.VK_I) {
         zoomPlot(lastMouseX, lastMouseY, true);
      } else if (e.getKeyCode() == KeyEvent.VK_O) {
         zoomPlot(lastMouseX, lastMouseY, false);
      } else if (e.getKeyCode() == KeyEvent.VK_L || e.getKeyCode() == KeyEvent.VK_R ||
                 e.getKeyCode() == KeyEvent.VK_U || e.getKeyCode() == KeyEvent.VK_D) {
         panPlot(e.getKeyCode());
      } else if (e.getKeyCode() == KeyEvent.VK_DELETE) {
         deleteSelectedNodes();
      }
   }

   public void keyTyped(KeyEvent e) {
   }

   private void zoomPlot(int screenX, int screenY, boolean zoomIn) {
      XYPlot xyPlot = getChart().getXYPlot();
      RectangleEdge xAxisLocation = xyPlot.getDomainAxisEdge();
      RectangleEdge yAxisLocation = xyPlot.getRangeAxisEdge();
      Rectangle2D dataArea = getScreenDataArea();
      double XOrthoChange = -(xyPlot.getDomainAxis().getUpperBound() - xyPlot.getDomainAxis().getLowerBound()) * 0.02;
      double YOrthoChange = -(xyPlot.getRangeAxis().getUpperBound() - xyPlot.getRangeAxis().getLowerBound()) * 0.02;
      if (zoomIn == false) {
         XOrthoChange = -XOrthoChange;
         YOrthoChange = -YOrthoChange;
      }
      double XPercent = (screenX - (dataArea.getMinX() + dataArea.getMaxX()) * 0.5) / ((dataArea.getMaxX() - dataArea.getMinX()) * 0.5);
      double YPercent = -(screenY - (dataArea.getMinY() + dataArea.getMaxY()) * 0.5) / ((dataArea.getMaxY() - dataArea.getMinY()) * 0.5);
      double newMinX = xyPlot.getDomainAxis().getLowerBound() - XOrthoChange * (XPercent + 1.0) * 0.5;
      double newMaxX = xyPlot.getDomainAxis().getUpperBound() - XOrthoChange * (XPercent - 1.0) * 0.5;
      double newMinY = xyPlot.getRangeAxis().getLowerBound() - YOrthoChange * (YPercent + 1.0) * 0.5;
      double newMaxY = xyPlot.getRangeAxis().getUpperBound() - YOrthoChange * (YPercent - 1.0) * 0.5;
      xyPlot.getDomainAxis().setLowerBound(newMinX);
      xyPlot.getDomainAxis().setUpperBound(newMaxX);
      xyPlot.getRangeAxis().setLowerBound(newMinY);
      xyPlot.getRangeAxis().setUpperBound(newMaxY);
   }

   private void panPlot(int keyCode) {
      XYPlot xyPlot = getChart().getXYPlot();
      RectangleEdge xAxisLocation = xyPlot.getDomainAxisEdge();
      RectangleEdge yAxisLocation = xyPlot.getRangeAxisEdge();
      Rectangle2D dataArea = getScreenDataArea();
      if (keyCode == KeyEvent.VK_U) {
         double YOrthoChange = -0.008 * (xyPlot.getRangeAxis().getUpperBound() - xyPlot.getRangeAxis().getLowerBound());
         double newMinY = xyPlot.getRangeAxis().getLowerBound() + YOrthoChange;
         double newMaxY = xyPlot.getRangeAxis().getUpperBound() + YOrthoChange;
         xyPlot.getRangeAxis().setLowerBound(newMinY);
         xyPlot.getRangeAxis().setUpperBound(newMaxY);
      } else if (keyCode == KeyEvent.VK_D) {
         double YOrthoChange = 0.008 * (xyPlot.getRangeAxis().getUpperBound() - xyPlot.getRangeAxis().getLowerBound());
         double newMinY = xyPlot.getRangeAxis().getLowerBound() + YOrthoChange;
         double newMaxY = xyPlot.getRangeAxis().getUpperBound() + YOrthoChange;
         xyPlot.getRangeAxis().setLowerBound(newMinY);
         xyPlot.getRangeAxis().setUpperBound(newMaxY);
      } else if (keyCode == KeyEvent.VK_R) {
         double XOrthoChange = -0.008 * (xyPlot.getDomainAxis().getUpperBound() - xyPlot.getDomainAxis().getLowerBound());
         double newMinX = xyPlot.getDomainAxis().getLowerBound() + XOrthoChange;
         double newMaxX = xyPlot.getDomainAxis().getUpperBound() + XOrthoChange;
         xyPlot.getDomainAxis().setLowerBound(newMinX);
         xyPlot.getDomainAxis().setUpperBound(newMaxX);
      } else if (keyCode == KeyEvent.VK_L) {
         double XOrthoChange = 0.008 * (xyPlot.getDomainAxis().getUpperBound() - xyPlot.getDomainAxis().getLowerBound());
         double newMinX = xyPlot.getDomainAxis().getLowerBound() + XOrthoChange;
         double newMaxX = xyPlot.getDomainAxis().getUpperBound() + XOrthoChange;
         xyPlot.getDomainAxis().setLowerBound(newMinX);
         xyPlot.getDomainAxis().setUpperBound(newMaxX);
      }
   }

   public void keyReleased(KeyEvent e) {
      if (e.getKeyCode() == KeyEvent.VK_CONTROL)
      {
         if (picking == true)
            endBoxSelect();
         picking = false;
      }
      else if (e.getKeyCode() == KeyEvent.VK_F1) {
         //mandatoryCrosshairs = false;
         //setCrosshairsState(false);
      }
   }

   private FunctionNode findNodeAt(int x, int y) {
      XYPlot xyPlot = getChart().getXYPlot();
      XYDataset xyDataset = xyPlot.getDataset();
      RectangleEdge xAxisLocation = xyPlot.getDomainAxisEdge();
      RectangleEdge yAxisLocation = xyPlot.getRangeAxisEdge();
      Rectangle2D dataArea = getScreenDataArea();
      // Loop through the nodes from last to first, so if the circles
      // for two or more nodes overlap, you get the one drawn on top.
      for (int i=xyDataset.getSeriesCount()-1; i>=0; i--) {
         if (renderer.getSeriesShapesVisible(i)) {
            for (int j=xyDataset.getItemCount(i)-1; j>=0; j--) {
               double sx = xyPlot.getDomainAxis().valueToJava2D(xyDataset.getXValue(i, j), dataArea, xAxisLocation);
               double sy = xyPlot.getRangeAxis().valueToJava2D(xyDataset.getYValue(i, j), dataArea, yAxisLocation);
               double distance = Math.sqrt((sx-x)*(sx-x) + (sy-y)*(sy-y));
               if (distance < 6.0) {
                  return new FunctionNode(i, j);
               }
            }
         }
      }
      return null;
   }

   protected JPopupMenu createNodePopupMenu() {

       JPopupMenu menu = new JPopupMenu("Node Commands");

       JMenuItem duplicateNodeItem = new JMenuItem("Duplicate control point");
       duplicateNodeItem.setActionCommand(DUPLICATE_NODE_COMMAND);
       duplicateNodeItem.addActionListener(this);
       menu.add(duplicateNodeItem);

       JMenuItem deleteNodeItem = new JMenuItem("Delete control point");
       deleteNodeItem.setActionCommand(DELETE_NODE_COMMAND);
       deleteNodeItem.addActionListener(this);
       menu.add(deleteNodeItem);

       return menu;
   }

   protected JPopupMenu createAddNodePopupMenu() {

       JPopupMenu menu = new JPopupMenu("Node Commands");

       JMenuItem addNodeItem = new JMenuItem("Add control point");
       addNodeItem.setActionCommand(ADD_NODE_COMMAND);
       addNodeItem.addActionListener(this);
       menu.add(addNodeItem);

       return menu;
   }

   public void actionPerformed(ActionEvent event) {
      String command = event.getActionCommand();

      if (command.equals(DUPLICATE_NODE_COMMAND)) {
         duplicateNode(rightClickNode.series, rightClickNode.node);
         rightClickNode = null;
      }
      else if (command.equals(DELETE_NODE_COMMAND)) {
         deleteNode(rightClickNode.series, rightClickNode.node);
         rightClickNode = null;
      }
      else if (command.equals(ADD_NODE_COMMAND)) {
         // TODO: maybe show a pop-up to let user select the series?
         addNode(0, rightClickX, rightClickY);
         rightClickNode = null;
      }
   }
 
   public ArrayList<FunctionNode> getSelectedNodes() {
      return selectedNodes;
   }

   public boolean isNodeSelected(int series, int node) {
      if (findNodeInList(series, node, selectedNodes) >= 0)
         return true;
      return false;
   }

   public void clearSelectedNodes() {   // Made public so that after nodes are removed nothing remains selected -Ayman
      // Unhighlight the selected nodes.
      for (int i = 0; i < selectedNodes.size(); i++)
         renderer.unhighlightNode(selectedNodes.get(i).series, selectedNodes.get(i).node);

      // Clear the selected nodes.
      selectedNodes.clear();

      this.repaint();

      // Now notify all listeners about the change.
      Object[] listeners = this.functionPanelListeners.getListenerList();
      for (int i = listeners.length - 2; i >= 0; i -= 2) {
         if (listeners[i] == FunctionPanelListener.class) {
            ((FunctionPanelListener) listeners[i + 1]).clearSelectedNodes();
         }
      }
   }

   public void toggleSelectedNode(int series, int node) {
      // Look for the node in the selected list.
      int nodeIndex = findNodeInList(series, node, selectedNodes);
      // If the node is already in the list, remove it
      if (nodeIndex >= 0) {
         selectedNodes.remove(nodeIndex);
         renderer.unhighlightNode(series, node);
      } else {
         // If the node is not already in the list, add it
         FunctionNode selectedNode = new FunctionNode(series, node);
         selectedNodes.add(selectedNode);
         renderer.highlightNode(series, node);
      }

      // Now notify all listeners about the change.
      Object[] listeners = this.functionPanelListeners.getListenerList();
      for (int i = listeners.length - 2; i >= 0; i -= 2) {
         if (listeners[i] == FunctionPanelListener.class) {
            ((FunctionPanelListener) listeners[i + 1]).toggleSelectedNode(series, node);
         }
      }
   }

   public void replaceSelectedNode(int series, int node) {
      // If the node is already in the list of selected ones, do nothing (a la Illustrator)
      // If the node is not already in the list, make this node the only selected one
      if (findNodeInList(series, node, selectedNodes) < 0)
      {
         // Unhighlight and clear the selected nodes. Do not call clearSelectedNodes
         // because this will notify all listeners unnecessarily.
         for (int i = 0; i < selectedNodes.size(); i++)
            renderer.unhighlightNode(selectedNodes.get(i).series, selectedNodes.get(i).node);
         selectedNodes.clear();
         // Set the selected node to this one.
         if (series >= 0 && node >= 0) {
            selectedNodes.add(new FunctionNode(series, node));
            renderer.highlightNode(series, node);
         }
      }

      // Now notify all listeners about the change.
      Object[] listeners = this.functionPanelListeners.getListenerList();
      for (int i = listeners.length - 2; i >= 0; i -= 2) {
         if (listeners[i] == FunctionPanelListener.class) {
            ((FunctionPanelListener) listeners[i + 1]).replaceSelectedNode(series, node);
         }
      }
   }

   protected int addNode(int series, int screenX, int screenY) {
      XYPlot xyPlot = getChart().getXYPlot();
      RectangleEdge xAxisLocation = xyPlot.getDomainAxisEdge();
      RectangleEdge yAxisLocation = xyPlot.getRangeAxisEdge();
      Rectangle2D dataArea = getScreenDataArea();
      double newNodeX = xyPlot.getDomainAxis().java2DToValue(screenX, dataArea, xAxisLocation);
      double newNodeY = xyPlot.getRangeAxis().java2DToValue(screenY, dataArea, yAxisLocation);

      // Notify all listeners about the change.
      Object[] listeners = this.functionPanelListeners.getListenerList();
      for (int i = listeners.length - 2; i >= 0; i -= 2) {
         if (listeners[i] == FunctionPanelListener.class) {
            ((FunctionPanelListener) listeners[i + 1]).addNode(series, newNodeX, newNodeY);
         }
      }

      // Now add the point to the series, first figuring out what its index will be.
      XYSeriesCollection seriesCollection = (XYSeriesCollection)xyPlot.getDataset();
      XYSeries dSeries = seriesCollection.getSeries(series);
      int index = dSeries.getItemCount();
      for (int i=0; i<dSeries.getItemCount(); i++) {
         if (dSeries.getDataItem(i).getX().doubleValue() > newNodeX) {
            index = i;
            break;
         }
      }
      dSeries.add(newNodeX, newNodeY);

      updateSelectedNodesAfterAddition(series, index);
      return index;
   }
 
   public void deleteNode(int series, int node) {
      // Notify all listeners about the change. There should be only
      // one listener, so when one returns success, remove the node from
      // the series, adjust the list of selected nodes, and return.
      Object[] listeners = this.functionPanelListeners.getListenerList();
      for (int i = listeners.length - 2; i >= 0; i -= 2) {
         if (listeners[i] == FunctionPanelListener.class) {
            boolean success = ((FunctionPanelListener) listeners[i + 1]).deleteNode(series, node);
            if (success) {
               XYSeriesCollection seriesCollection = (XYSeriesCollection)getChart().getXYPlot().getDataset();
               seriesCollection.getSeries(series).remove(node);
               seriesCollection.getSeries(series).fireSeriesChanged();
               updateSelectedNodesAfterDeletion(series, node);
               return;
            }
         }
      }
   }

   public void deleteSelectedNodes() {
      XYSeriesCollection seriesCollection = (XYSeriesCollection)getChart().getXYPlot().getDataset();
      for (int series=0; series < seriesCollection.getSeriesCount(); series++) {
         // If the series' shapes (circles) are turned off, don't allow deletion.
         if (!renderer.getSeriesShapesVisible(series))
            continue;
         Vector<Integer> sortedIndices = new Vector<Integer>(selectedNodes.size());
         for (int i=0; i<selectedNodes.size(); i++) {
            int index = selectedNodes.get(i).node;
            if (selectedNodes.get(i).series == series)
               sortedIndices.add(new Integer(index));
         }
         Collections.sort(sortedIndices);
         // Make an array of ints holding the indices sorted from highest to lowest
         ArrayInt sortedNodes  = new ArrayInt(0, 0, sortedIndices.size());
         for (int i=sortedIndices.size()-1; i>=0; i--)
            sortedNodes.append(sortedIndices.get(i));

         // Notify all listeners about the change. There should be only
         // one listener, so when one returns success, remove the nodes from
         // the series, clear the selected node list, and go on to the next series.
         Object[] listeners = this.functionPanelListeners.getListenerList();
         for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == FunctionPanelListener.class) {
               boolean success = ((FunctionPanelListener) listeners[i + 1]).deleteNodes(series, sortedNodes);
               if (success) {
                  for (int j = 0; j < sortedNodes.getSize(); j++) {
                     int index = sortedNodes.getitem(j);
                     renderer.unhighlightNode(series, index);
                     seriesCollection.getSeries(series).remove(index);
                     seriesCollection.getSeries(series).fireSeriesChanged();
                     updateSelectedNodesAfterDeletion(series, index);
                  }
                  break;
               }
            }
         }
      }
      //this.repaint();
   }

   public void updateSelectedNodesAfterDeletion(int series, int node) {
      XYSeriesCollection seriesCollection = (XYSeriesCollection)getChart().getXYPlot().getDataset();
      XYSeries dSeries = seriesCollection.getSeries(series);

      // Unhighlight all nodes in the series that are after the deleted one.
      for (int i=node; i<dSeries.getItemCount(); i++) {
         renderer.unhighlightNode(series, i);
      }

      // If the deleted node was selected, remove it from selectedNodes.
      // For all nodes in the series after the deleted one, decrement the node number.
      for (int i=selectedNodes.size()-1; i>=0; i--) {
         if (selectedNodes.get(i).series == series && selectedNodes.get(i).node >= node) {
            if (selectedNodes.get(i).node == node) {
               selectedNodes.remove(i);
            } else {
               selectedNodes.get(i).node--;
            }
         }
      }

      // For each selected node in the series after the deleted one, highlight it.
      for (int i=0; i<selectedNodes.size(); i++) {
         if (selectedNodes.get(i).series == series && selectedNodes.get(i).node >= node) {
            renderer.highlightNode(series, selectedNodes.get(i).node);
         }
      }
   }

   public void updateSelectedNodesAfterAddition(int series, int node) {
      XYSeriesCollection seriesCollection = (XYSeriesCollection)getChart().getXYPlot().getDataset();
      XYSeries dSeries = seriesCollection.getSeries(series);

      // Unhighlight all nodes in the series that are after the added one.
      for (int i=node; i<dSeries.getItemCount(); i++) {
         renderer.unhighlightNode(series, i);
      }

      // For all nodes in the series after the added one, increment the node number.
      for (int i=0; i<selectedNodes.size(); i++) {
         if (selectedNodes.get(i).series == series && selectedNodes.get(i).node >= node) {
            selectedNodes.get(i).node++;
         }
      }

      // For each selected node in the series after the added one, highlight it.
      for (int i=0; i<selectedNodes.size(); i++) {
         if (selectedNodes.get(i).series == series && selectedNodes.get(i).node > node) {
            renderer.highlightNode(series, selectedNodes.get(i).node);
         }
      }
   }

   public void duplicateNode(int series, int node) {
      // TODO: it would be really slick to adjust the list of
      // selected nodes to account for the duplicated one, but you
      // don't know if the duplicateNode will be successful
      // (e.g., some functions may not allow adding nodes).
      // So instead just clear the list of selected nodes.
      clearSelectedNodes();

      // Now notify all listeners about the change.
      Object[] listeners = this.functionPanelListeners.getListenerList();
      for (int i = listeners.length - 2; i >= 0; i -= 2) {
         if (listeners[i] == FunctionPanelListener.class) {
            ((FunctionPanelListener) listeners[i + 1]).duplicateNode(series, node);
         }
      }
   }

   public void dragSelectedNodes(int series, int node, double dragVector[]) {
      // Now notify all listeners about the change.
      Object[] listeners = this.functionPanelListeners.getListenerList();
      for (int i = listeners.length - 2; i >= 0; i -= 2) {
         if (listeners[i] == FunctionPanelListener.class) {
            ((FunctionPanelListener) listeners[i + 1]).dragSelectedNodes(series, node, dragVector);
         }
      }
   }

   private int findNodeInList(int series, int node, ArrayList<FunctionNode> nodeList) {
      if (nodeList == null)
         return -1;
      for (int i = 0; i < nodeList.size(); i++)
         if (nodeList.get(i).series == series && nodeList.get(i).node == node)
            return i;
      return -1;
   }

   private boolean listContainsNode(FunctionNode node, ArrayList<FunctionNode> nodeList) {
      if (nodeList == null)
         return false;
      for (int i = 0; i < nodeList.size(); i++) {
         if (nodeList.get(i).series == node.series && nodeList.get(i).node == node.node)
            return true;
      }
      return false;
   }

   public void addFunctionPanelListener(FunctionPanelListener listener) {
       if (listener == null) {
           throw new IllegalArgumentException("Null 'listener' argument.");
       }
       this.functionPanelListeners.add(FunctionPanelListener.class, listener);
   }

   public void removeFunctionPanelListener(FunctionPanelListener listener) {
       this.functionPanelListeners.remove(FunctionPanelListener.class, listener);
   }

    public JPopupMenu getNodePopUpMenu() {
        return nodePopUpMenu;
    }

    public void setNodePopUpMenu(JPopupMenu nodePopUpMenu) {
        this.nodePopUpMenu = nodePopUpMenu;
    }

    public static JFreeChart createFunctionChart(String title,
                                                 String xAxisLabel,
                                                 String yAxisLabel,
                                                 XYDataset dataset,
                                                 boolean legend,
                                                 boolean tooltips) {

        NumberAxis xAxis = new NumberAxis(xAxisLabel);
        xAxis.setAutoRangeIncludesZero(false);
        NumberAxis yAxis = new NumberAxis(yAxisLabel);
        XYItemRenderer renderer = new XYLineAndShapeRenderer(true, false);
        FunctionPlot plot = new FunctionPlot(dataset, xAxis, yAxis, renderer);
        plot.setOrientation(PlotOrientation.VERTICAL);
        if (tooltips) {
            renderer.setBaseToolTipGenerator(new StandardXYToolTipGenerator());
        }

        JFreeChart chart = new JFreeChart(title, JFreeChart.DEFAULT_TITLE_FONT, plot, legend);

        return chart;
    }

    public int getRightClickX() {
        return rightClickX;
    }

    public int getRightClickY() {
        return rightClickY;
    }
}