/**
 * Copyright (C) 2001-2015 by RapidMiner and the contributors
 *
 * Complete list of developers available at our web site:
 *
 *      http://rapidminer.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses/.
 */
package com.rapidminer.gui.new_plotter.engine.jfreechart.link_and_brush;

import com.rapidminer.gui.new_plotter.engine.jfreechart.link_and_brush.listener.LinkAndBrushSelection;
import com.rapidminer.gui.new_plotter.engine.jfreechart.link_and_brush.listener.LinkAndBrushSelection.SelectionType;
import com.rapidminer.gui.new_plotter.engine.jfreechart.link_and_brush.listener.LinkAndBrushSelectionListener;
import com.rapidminer.gui.new_plotter.engine.jfreechart.link_and_brush.plots.LinkAndBrushPlot;
import com.rapidminer.gui.plotter.CoordinateTransformation;
import com.rapidminer.gui.plotter.NullCoordinateTransformation;
import com.rapidminer.tools.container.Pair;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;

import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.panel.Overlay;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.Pannable;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.Range;


/**
 * A Swing GUI component for displaying a {@link JFreeChart} object. The chart will be buffered.
 * <P>
 * 
 * @author Nils Woehler
 * 
 */
public class LinkAndBrushChartPanel extends ChartPanel {

	private static final long serialVersionUID = 1L;
	private boolean zoomOnLinkAndBrushSelection;
	private boolean blockSelectionOrZoom = false;

	/**
	 * This is a transformation which transforms the components coordinates to screen coordinates.
	 * If is null, no transformation is needed.
	 */
	private transient CoordinateTransformation coordinateTransformation = new NullCoordinateTransformation();

	private transient List<WeakReference<LinkAndBrushSelectionListener>> listeners = new LinkedList<WeakReference<LinkAndBrushSelectionListener>>();

	public LinkAndBrushChartPanel(JFreeChart chart, boolean zoomOnLinkAndBrushSelection) {
		super(chart, 600, 400, 200, 133, DEFAULT_MAXIMUM_DRAW_WIDTH, DEFAULT_MAXIMUM_DRAW_HEIGHT, DEFAULT_BUFFER_USED,
				false,  // copy
				false,  // properties
				false,  // save
				false,  // print
				false,  // zoom
				true);   // tooltips

		this.zoomOnLinkAndBrushSelection = zoomOnLinkAndBrushSelection;
		setInitialDelay(200);

		setMouseWheelEnabled(false);
	}

	public LinkAndBrushChartPanel(JFreeChart chart, int defaultWidth, int defaultHeigth, int minDrawWidth,
			int minDrawHeigth, boolean zoomOnLinkAndBrush) {
		super(chart, defaultWidth, defaultHeigth, minDrawWidth, minDrawHeigth, DEFAULT_MAXIMUM_DRAW_WIDTH,
				DEFAULT_MAXIMUM_DRAW_HEIGHT, DEFAULT_BUFFER_USED, false,  // copy
				false,  // properties
				false,  // save
				false,  // print
				false,  // zoom
				true);   // tooltips

		this.zoomOnLinkAndBrushSelection = zoomOnLinkAndBrush;
		setInitialDelay(200);

		setMouseWheelEnabled(false);
	}

	public LinkAndBrushChartPanel(JFreeChart chart, int defaultWidth, int defaultHeigth, int minDrawWidth,
			int minDrawHeigth, boolean zoomOnLinkAndBrush, boolean useBuffer) {
		super(chart, defaultWidth, defaultHeigth, minDrawWidth, minDrawHeigth, DEFAULT_MAXIMUM_DRAW_WIDTH,
				DEFAULT_MAXIMUM_DRAW_HEIGHT, useBuffer, false,  // copy
				false,  // properties
				false,  // save
				false,  // print
				false,  // zoom
				true);   // tooltips

		this.zoomOnLinkAndBrushSelection = zoomOnLinkAndBrush;

		setMouseWheelEnabled(false);
	}

	@Override
	public void restoreAutoBounds() {
		Plot plot = getChart().getPlot();
		if (plot == null) {
			return;
		}
		// here we tweak the notify flag on the plot so that only
		// one notification happens even though we update multiple
		// axes...
		boolean savedNotify = plot.isNotify();
		plot.setNotify(false);

		if (plot instanceof LinkAndBrushPlot) {

			LinkAndBrushPlot LABPlot = (LinkAndBrushPlot) plot;

			List<Pair<Integer, Range>> zoomedDomainAxisRanges = new LinkedList<Pair<Integer, Range>>();
			List<Pair<Integer, Range>> zoomedRangeAxisRanges = new LinkedList<Pair<Integer, Range>>();

			zoomedDomainAxisRanges.addAll(LABPlot.restoreAutoDomainAxisBounds(zoomOnLinkAndBrushSelection));
			zoomedRangeAxisRanges.addAll(LABPlot.restoreAutoRangeAxisBounds(zoomOnLinkAndBrushSelection));

			if (zoomOnLinkAndBrushSelection) {
				informLinkAndBrushSelectionListeners(new LinkAndBrushSelection(SelectionType.RESTORE_AUTO_BOUNDS,
						zoomedDomainAxisRanges, zoomedRangeAxisRanges));
			} else {
				informLinkAndBrushSelectionListeners(new LinkAndBrushSelection(SelectionType.RESTORE_SELECTION,
						zoomedDomainAxisRanges, zoomedRangeAxisRanges));
			}

		} else {
			restoreAutoDomainBounds();
			restoreAutoRangeBounds();
		}

		plot.setNotify(savedNotify);
	}

	@Override
	public void zoom(Rectangle2D selection) {
		// get the origin of the zoom selection in the Java2D space used for
		// drawing the chart (that is, before any scaling to fit the panel)
		Point2D selectOrigin = translateScreenToJava2D(new Point((int) Math.ceil(selection.getX()),
				(int) Math.ceil(selection.getY())));
		PlotRenderingInfo plotInfo = getChartRenderingInfo().getPlotInfo();
		Rectangle2D scaledDataArea = getScreenDataArea((int) selection.getCenterX(), (int) selection.getCenterY());
		if ((selection.getHeight() > 0) && (selection.getWidth() > 0)) {

			double hLower = (selection.getMinX() - scaledDataArea.getMinX()) / scaledDataArea.getWidth();
			double hUpper = (selection.getMaxX() - scaledDataArea.getMinX()) / scaledDataArea.getWidth();
			double vLower = (scaledDataArea.getMaxY() - selection.getMaxY()) / scaledDataArea.getHeight();
			double vUpper = (scaledDataArea.getMaxY() - selection.getMinY()) / scaledDataArea.getHeight();

			Plot p = getChart().getPlot();
			if (p instanceof LinkAndBrushPlot) {

				PlotOrientation orientation = null;
				if (p instanceof XYPlot) {
					XYPlot xyPlot = (XYPlot) p;
					orientation = xyPlot.getOrientation();
				}
				if (p instanceof CategoryPlot) {
					CategoryPlot categoryPlot = (CategoryPlot) p;
					orientation = categoryPlot.getOrientation();
				}

				// here we tweak the notify flag on the plot so that only
				// one notification happens even though we update multiple
				// axes...

				boolean savedNotify = p.isNotify();
				p.setNotify(false);
				LinkAndBrushPlot LABPlot = (LinkAndBrushPlot) p;

				List<Pair<Integer, Range>> zoomedDomainAxisRanges = new LinkedList<Pair<Integer, Range>>();
				List<Pair<Integer, Range>> zoomedRangeAxisRanges = new LinkedList<Pair<Integer, Range>>();

				if (orientation == PlotOrientation.HORIZONTAL) {
					zoomedDomainAxisRanges.addAll(LABPlot.calculateDomainAxesZoom(vLower, vUpper,
							zoomOnLinkAndBrushSelection));
					zoomedRangeAxisRanges.addAll(LABPlot.calculateRangeAxesZoom(hLower, hUpper, plotInfo, selectOrigin,
							zoomOnLinkAndBrushSelection));
				} else {
					zoomedDomainAxisRanges.addAll(LABPlot.calculateDomainAxesZoom(hLower, hUpper,
							zoomOnLinkAndBrushSelection));
					zoomedRangeAxisRanges.addAll(LABPlot.calculateRangeAxesZoom(vLower, vUpper, plotInfo, selectOrigin,
							zoomOnLinkAndBrushSelection));
				}
				p.setNotify(savedNotify);

				if (zoomOnLinkAndBrushSelection) {
					informLinkAndBrushSelectionListeners(new LinkAndBrushSelection(SelectionType.ZOOM_IN,
							zoomedDomainAxisRanges, zoomedRangeAxisRanges));
				} else {
					informLinkAndBrushSelectionListeners(new LinkAndBrushSelection(SelectionType.SELECTION,
							zoomedDomainAxisRanges, zoomedRangeAxisRanges));
				}

			} else {
				super.zoom(selection);
			}
		}
	}

	/**
	 * If set to <code>true</code>, will zoom on selection, otherwise will do a selection.
	 * 
	 * @param zoomOnLinkAndBrushSelection
	 */
	public void setZoomOnLinkAndBrushSelection(boolean zoomOnLinkAndBrushSelection) {
		this.zoomOnLinkAndBrushSelection = zoomOnLinkAndBrushSelection;
	}

	/**
	 * Returns whether the panel will zoom on selection or not.
	 * 
	 * @return
	 */
	public boolean getZoomOnLinkAndBrushSelection() {
		return zoomOnLinkAndBrushSelection;
	}

	@Override
	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		if (getChart() == null) {
			return;
		}
		Graphics2D g2 = (Graphics2D) g.create();

		// first determine the size of the chart rendering area...
		Dimension size = getSize();
		Insets insets = getInsets();
		Rectangle2D available = new Rectangle2D.Double(insets.left, insets.top,
				size.getWidth() - insets.left - insets.right, size.getHeight() - insets.top - insets.bottom);

		// work out if scaling is required...
		boolean scale = false;
		double drawWidth = available.getWidth();
		double drawHeight = available.getHeight();
		setChartFieldValue(getChartFieldByName("scaleX"), 1.0);
		// this.scaleX = 1.0;
		setChartFieldValue(getChartFieldByName("scaleY"), 1.0);
		// this.scaleY = 1.0;

		if (drawWidth < getMinimumDrawWidth()) {
			setChartFieldValue(getChartFieldByName("scaleX"), drawWidth / getMinimumDrawWidth());
			// this.scaleX = drawWidth / getMinimumDrawWidth();
			drawWidth = getMinimumDrawWidth();
			scale = true;
		} else if (drawWidth > getMaximumDrawWidth()) {
			setChartFieldValue(getChartFieldByName("scaleX"), drawWidth / getMaximumDrawWidth());
			// this.scaleX = drawWidth / getMaximumDrawWidth();
			drawWidth = getMaximumDrawWidth();
			scale = true;
		}

		if (drawHeight < getMinimumDrawHeight()) {
			setChartFieldValue(getChartFieldByName("scaleY"), drawHeight / getMinimumDrawHeight());
			// this.scaleY = drawHeight / getMinimumDrawHeight();
			drawHeight = getMinimumDrawHeight();
			scale = true;
		} else if (drawHeight > getMaximumDrawHeight()) {
			setChartFieldValue(getChartFieldByName("scaleY"), drawHeight / getMaximumDrawHeight());
			// this.scaleY = drawHeight / getMaximumDrawHeight();
			drawHeight = getMaximumDrawHeight();
			scale = true;
		}

		Rectangle2D chartArea = new Rectangle2D.Double(0.0, 0.0, drawWidth, drawHeight);

		// are we using the chart buffer?
		if ((Boolean) getChartFieldValueByName("useBuffer")) {

			// do we need to resize the buffer?
			if ((getChartFieldValueByName("chartBuffer") == null)
					|| ((Integer) getChartFieldValueByName("chartBufferWidth") != available.getWidth())
					|| ((Integer) getChartFieldValueByName("chartBufferHeight") != available.getHeight())) {
				setChartFieldValue(getChartFieldByName("chartBufferWidth"), (int) available.getWidth());
				// this.chartBufferWidth = (int) available.getWidth();
				setChartFieldValue(getChartFieldByName("chartBufferHeight"), (int) available.getHeight());
				// this.chartBufferHeight = (int) available.getHeight();
				GraphicsConfiguration gc = g2.getDeviceConfiguration();
				setChartFieldValue(getChartFieldByName("chartBuffer"), gc.createCompatibleImage(
						(Integer) getChartFieldValueByName("chartBufferWidth"),
						(Integer) getChartFieldValueByName("chartBufferHeight"), Transparency.TRANSLUCENT));
				// this.chartBuffer = gc.createCompatibleImage(this.chartBufferWidth,
				// this.chartBufferHeight, Transparency.TRANSLUCENT);
				setRefreshBuffer(true);
			}

			// do we need to redraw the buffer?
			if (getRefreshBuffer()) {

				setRefreshBuffer(false); // clear the flag

				Rectangle2D bufferArea = new Rectangle2D.Double(0, 0,
						(Integer) getChartFieldValueByName("chartBufferWidth"),
						(Integer) getChartFieldValueByName("chartBufferHeight"));

				Graphics2D bufferG2 = (Graphics2D) ((Image) getChartFieldValueByName("chartBuffer")).getGraphics();
				Rectangle r = new Rectangle(0, 0, (Integer) getChartFieldValueByName("chartBufferWidth"),
						(Integer) getChartFieldValueByName("chartBufferHeight"));
				bufferG2.setPaint(getBackground());
				bufferG2.fill(r);
				if (scale) {
					AffineTransform saved = bufferG2.getTransform();
					AffineTransform st = AffineTransform.getScaleInstance((Double) getChartFieldValueByName("scaleX"),
							(Double) getChartFieldValueByName("scaleY"));
					bufferG2.transform(st);
					getChart().draw(bufferG2, chartArea, getAnchor(), getChartRenderingInfo());
					bufferG2.setTransform(saved);
				} else {
					getChart().draw(bufferG2, bufferArea, getAnchor(), getChartRenderingInfo());
				}

			}

			// zap the buffer onto the panel...
			g2.drawImage((Image) getChartFieldValueByName("chartBuffer"), insets.left, insets.top, this);

		}

		// or redrawing the chart every time...
		else {

			AffineTransform saved = g2.getTransform();
			g2.translate(insets.left, insets.top);
			if (scale) {
				AffineTransform st = AffineTransform.getScaleInstance((Double) getChartFieldValueByName("scaleX"),
						(Double) getChartFieldValueByName("scaleY"));
				g2.transform(st);
			}
			getChart().draw(g2, chartArea, getAnchor(), getChartRenderingInfo());
			g2.setTransform(saved);

		}

		Iterator iterator = ((List) getChartFieldValueByName("overlays")).iterator();
		while (iterator.hasNext()) {
			Overlay overlay = (Overlay) iterator.next();
			overlay.paintOverlay(g2, this);
		}

		// redraw the zoom rectangle (if present) - if useBuffer is false,
		// we use XOR so we can XOR the rectangle away again without redrawing
		// the chart
		drawZoomRectangle(g2, !(Boolean) getChartFieldValueByName("useBuffer"));

		g2.dispose();

		setAnchor(null);
		setVerticalTraceLine(null);
		setHorizontalTraceLine(null);
	}

	@SuppressWarnings("unchecked")
	public List<Overlay> getOverlayList() {
		return ((List<Overlay>) getChartFieldValueByName("overlays"));
	}

	public void setOverlayList(List<Overlay> list) {
		setChartFieldValue(getChartFieldByName("overlays"), list);
	}

	@Override
	public void mousePressed(MouseEvent e) {
		// this is used to only allow left mouse button zoom / selection
		if (!SwingUtilities.isLeftMouseButton(e)) {
			blockSelectionOrZoom = true;
		} else {
			blockSelectionOrZoom = false;
		}
		super.mousePressed(e);
	}

	@Override
	public void mouseDragged(MouseEvent e) {
		// when not allowed to zoom / select, return
		if (blockSelectionOrZoom) {
			return;
		}
		// if the popup menu has already been triggered, then ignore dragging...
		if (getChartFieldValueByName("popup") != null && ((JPopupMenu) getChartFieldValueByName("popup")).isShowing()) {
			return;
		}

		// handle panning if we have a start point
		if (getChartFieldValueByName("panLast") != null) {
			double dx = e.getX() - ((Point) getChartFieldValueByName("panLast")).getX();
			double dy = e.getY() - ((Point) getChartFieldValueByName("panLast")).getY();
			if (dx == 0.0 && dy == 0.0) {
				return;
			}
			double wPercent = -dx / ((Double) getChartFieldValueByName("panW"));
			double hPercent = dy / ((Double) getChartFieldValueByName("panH"));
			boolean old = getChart().getPlot().isNotify();
			getChart().getPlot().setNotify(false);
			Pannable p = (Pannable) getChart().getPlot();
			if (p.getOrientation() == PlotOrientation.VERTICAL) {
				p.panDomainAxes(wPercent, getChartRenderingInfo().getPlotInfo(), (Point) getChartFieldValueByName("panLast"));
				p.panRangeAxes(hPercent, getChartRenderingInfo().getPlotInfo(), (Point) getChartFieldValueByName("panLast"));
			} else {
				p.panDomainAxes(hPercent, getChartRenderingInfo().getPlotInfo(), (Point) getChartFieldValueByName("panLast"));
				p.panRangeAxes(wPercent, getChartRenderingInfo().getPlotInfo(), (Point) getChartFieldValueByName("panLast"));
			}
			setChartFieldValue((getChartFieldByName("panLast")), e.getPoint());
			getChart().getPlot().setNotify(old);
			return;
		}

		// if no initial zoom point was set, ignore dragging...
		if (getChartFieldValueByName("zoomPoint") == null) {
			return;
		}
		Graphics2D g2 = (Graphics2D) getGraphics();

		// erase the previous zoom rectangle (if any). We only need to do
		// this is we are using XOR mode, which we do when we're not using
		// the buffer (if there is a buffer, then at the end of this method we
		// just trigger a repaint)
		if (!(Boolean) getChartFieldValueByName("useBuffer")) {
			drawZoomRectangle(g2, true);
		}

		boolean hZoom = false;
		boolean vZoom = false;
		if ((PlotOrientation) getChartFieldValueByName("orientation") == PlotOrientation.HORIZONTAL) {
			hZoom = (Boolean) getChartFieldValueByName("rangeZoomable");
			vZoom = (Boolean) getChartFieldValueByName("domainZoomable");
		} else {
			hZoom = (Boolean) getChartFieldValueByName("domainZoomable");
			vZoom = (Boolean) getChartFieldValueByName("rangeZoomable");
		}
		Point2D zoomPoint = (Point2D) getChartFieldValueByName("zoomPoint");
		Rectangle2D scaledDataArea = getScreenDataArea((int) zoomPoint.getX(), (int) zoomPoint.getY());
		if (hZoom && vZoom) {
			// selected rectangle shouldn't extend outside the data area...
			double xmax = Math.min(e.getX(), scaledDataArea.getMaxX());
			double ymax = Math.min(e.getY(), scaledDataArea.getMaxY());
			setChartFieldValue(
					getChartFieldByName("zoomRectangle"),
					new Rectangle2D.Double(zoomPoint.getX(), zoomPoint.getY(), xmax - zoomPoint.getX(), ymax
							- zoomPoint.getY()));
		} else if (hZoom) {
			double xmax = Math.min(e.getX(), scaledDataArea.getMaxX());
			setChartFieldValue(getChartFieldByName("zoomRectangle"),
					new Rectangle2D.Double(zoomPoint.getX(), scaledDataArea.getMinY(), xmax - zoomPoint.getX(),
							scaledDataArea.getHeight()));
		} else if (vZoom) {
			double ymax = Math.min(e.getY(), scaledDataArea.getMaxY());
			setChartFieldValue(getChartFieldByName("zoomRectangle"), new Rectangle2D.Double(scaledDataArea.getMinX(),
					zoomPoint.getY(), scaledDataArea.getWidth(), ymax - zoomPoint.getY()));
		}

		// Draw the new zoom rectangle...
		if ((Boolean) getChartFieldValueByName("useBuffer")) {
			repaint();
		} else {
			// with no buffer, we use XOR to draw the rectangle "over" the
			// chart...
			drawZoomRectangle(g2, true);
		}
		g2.dispose();

	}

	@Override
	public void mouseReleased(MouseEvent e) {

		// if we've been panning, we need to reset now that the mouse is
		// released...
		Rectangle2D zoomRectangle = (Rectangle2D) getChartFieldValueByName("zoomRectangle");
		Point2D zoomPoint = (Point2D) getChartFieldValueByName("zoomPoint");
		if (getChartFieldValueByName("panLast") != null) {
			setChartFieldValue((getChartFieldByName("panLast")), null);
			setCursor(Cursor.getDefaultCursor());
		} else if (zoomRectangle != null) {
			boolean hZoom = false;
			boolean vZoom = false;
			if ((PlotOrientation) getChartFieldValueByName("orientation") == PlotOrientation.HORIZONTAL) {
				hZoom = (Boolean) getChartFieldValueByName("rangeZoomable");
				vZoom = (Boolean) getChartFieldValueByName("domainZoomable");
			} else {
				hZoom = (Boolean) getChartFieldValueByName("domainZoomable");
				vZoom = (Boolean) getChartFieldValueByName("rangeZoomable");
			}

			boolean zoomTrigger1 = hZoom
					&& Math.abs(e.getX() - zoomPoint.getX()) >= (Integer) getChartFieldValueByName("zoomTriggerDistance");
			boolean zoomTrigger2 = vZoom
					&& Math.abs(e.getY() - zoomPoint.getY()) >= (Integer) getChartFieldValueByName("zoomTriggerDistance");
			if (zoomTrigger1 || zoomTrigger2) {
				if ((hZoom && (e.getX() < zoomPoint.getX())) || (vZoom && (e.getY() < zoomPoint.getY()))) {
					restoreAutoBounds();
				} else {
					double x, y, w, h;
					Rectangle2D screenDataArea = getScreenDataArea((int) zoomPoint.getX(), (int) zoomPoint.getY());
					double maxX = screenDataArea.getMaxX();
					double maxY = screenDataArea.getMaxY();
					// for mouseReleased event, (horizontalZoom || verticalZoom)
					// will be true, so we can just test for either being false;
					// otherwise both are true
					if (!vZoom) {
						x = zoomPoint.getX();
						y = screenDataArea.getMinY();
						w = Math.min(zoomRectangle.getWidth(), maxX - zoomPoint.getX());
						h = screenDataArea.getHeight();
					} else if (!hZoom) {
						x = screenDataArea.getMinX();
						y = zoomPoint.getY();
						w = screenDataArea.getWidth();
						h = Math.min(zoomRectangle.getHeight(), maxY - zoomPoint.getY());
					} else {
						x = zoomPoint.getX();
						y = zoomPoint.getY();
						w = Math.min(zoomRectangle.getWidth(), maxX - zoomPoint.getX());
						h = Math.min(zoomRectangle.getHeight(), maxY - zoomPoint.getY());
					}
					Rectangle2D zoomArea = new Rectangle2D.Double(x, y, w, h);
					zoom(zoomArea);
				}
				setChartFieldValue(getChartFieldByName("zoomPoint"), null);
				setChartFieldValue(getChartFieldByName("zoomRectangle"), null);
			} else {
				// erase the zoom rectangle
				Graphics2D g2 = (Graphics2D) getGraphics();
				if ((Boolean) getChartFieldValueByName("useBuffer")) {
					repaint();
				} else {
					drawZoomRectangle(g2, true);
				}
				g2.dispose();
				setChartFieldValue(getChartFieldByName("zoomPoint"), null);
				setChartFieldValue(getChartFieldByName("zoomRectangle"), null);
			}

		}

		else if (e.isPopupTrigger()) {
			if (getChartFieldValueByName("popup") != null) {
				displayPopupMenu(e.getX(), e.getY());
			}
		}

	}

	/**
	 * Draws zoom rectangle (if present). The drawing is performed in XOR mode, therefore when this
	 * method is called twice in a row, the second call will completely restore the state of the
	 * canvas.
	 * 
	 * @param g2
	 *            the graphics device.
	 * @param xor
	 *            use XOR for drawing?
	 */
	private void drawZoomRectangle(Graphics2D g2, boolean xor) {
		Rectangle2D zoomRectangle = (Rectangle2D) getChartFieldValueByName("zoomRectangle");
		if (zoomRectangle != null) {
			// fix rectangle parameters when chart is transformed
			zoomRectangle = coordinateTransformation.transformRectangle(zoomRectangle, this);
			if (!(coordinateTransformation instanceof NullCoordinateTransformation)) {
				g2 = coordinateTransformation.getTransformedGraphics(this);
			}
			if (xor) {
				// Set XOR mode to draw the zoom rectangle
				g2.setXORMode(Color.gray);
			}
			if ((Boolean) getChartFieldValueByName("fillZoomRectangle")) {
				g2.setPaint((Paint) getChartFieldValueByName("zoomFillPaint"));
				g2.fill(zoomRectangle);
			} else {
				g2.setPaint((Paint) getChartFieldValueByName("zoomOutlinePaint"));
				g2.draw(zoomRectangle);
			}
			if (xor) {
				// Reset to the default 'overwrite' mode
				g2.setPaintMode();
			}
		}
	}

	/**
	 * Add a {@link LinkAndBrushSelectionListener}. The listener is saved as a {@link WeakReference}
	 * . Thus listener must not be hidden (anonymous) classes!
	 */
	public void addLinkAndBrushSelectionListener(LinkAndBrushSelectionListener l) {
		listeners.add(new WeakReference<LinkAndBrushSelectionListener>(l));
	}

	/**
	 * Remove a {@link LinkAndBrushSelectionListener}.
	 * 
	 * @param l
	 */
	public void removeLinkAndBrushSelectionListener(LinkAndBrushSelectionListener l) {
		Iterator<WeakReference<LinkAndBrushSelectionListener>> it = listeners.iterator();
		while (it.hasNext()) {
			WeakReference<LinkAndBrushSelectionListener> wrl = it.next();
			LinkAndBrushSelectionListener listener = wrl.get();
			if (listener == l || listener == null) {
				it.remove();
			}
		}
	}

	/**
	 * Informs all {@link LinkAndBrushSelectionListener} of a {@link LinkAndBrushSelection}.
	 * 
	 * @param e
	 */
	public void informLinkAndBrushSelectionListeners(LinkAndBrushSelection e) {
		// create a copy to avoid ConcurrentModificationException when informing and remove listener
		// happens
		List<WeakReference<LinkAndBrushSelectionListener>> listenersCopy = new LinkedList<WeakReference<LinkAndBrushSelectionListener>>();
		listenersCopy.addAll(listeners);
		Iterator<WeakReference<LinkAndBrushSelectionListener>> it = listenersCopy.iterator();
		while (it.hasNext()) {
			WeakReference<LinkAndBrushSelectionListener> wrl = it.next();
			LinkAndBrushSelectionListener l = wrl.get();
			if (l != null) {
				l.selectedLinkAndBrushRectangle(e);
			} else {
				it.remove();
			}
		}
	}

	/**
	 * Returns the value of a {@link Field} of the {@link ChartPanel}. If the field can not be
	 * found, returns {@code null}.
	 * 
	 * @param name
	 *            the name of the {@link Field}
	 * @return
	 */
	private Object getChartFieldValueByName(String name) {
		try {
			Field field = ChartPanel.class.getDeclaredField(name);
			field.setAccessible(true);
			Object value = field.get(this);
			field.setAccessible(false);
			return value;
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * Returns the {@link Field} of the {@link ChartPanel}. If the field can not be found, returns
	 * {@code null}.
	 * 
	 * @param name
	 *            the name of the {@link Field}
	 * @return
	 */
	private Field getChartFieldByName(String name) {
		try {
			Field field = ChartPanel.class.getDeclaredField(name);
			field.setAccessible(true);
			return field;
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * Sets the given value for the given {@link Field}.
	 * 
	 * @param field
	 *            the {@link Field} to change
	 * @param value
	 *            the new value for the {@link Field}
	 */
	private void setChartFieldValue(Field field, Object value) {
		try {
			field.setAccessible(true);
			field.set(this, value);
			field.setAccessible(false);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * This method sets the coordinate transformation for this component.
	 */
	public void setCoordinateTransformation(CoordinateTransformation transformation) {
		this.coordinateTransformation = transformation;
	}
}
