package fi.csc.microarray.client.visualisation.methods; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import javax.swing.JPanel; import org.jfree.chart.ChartPanel; import org.jfree.chart.ChartRenderingInfo; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.CategoryAxis; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.plot.CategoryPlot; import org.jfree.chart.plot.HCPlot; import org.jfree.chart.plot.Plot; import org.jfree.chart.plot.XYPlot; import org.jfree.ui.OverlayLayout; import org.jfree.ui.RectangleEdge; import fi.csc.microarray.client.visualisation.Visualisation; import fi.csc.microarray.constants.VisualConstants; /** * JFreeChart handles area selections with zoom whereas we wan't selection to hapen. This * class wraps and extends ChartPanel functionality. * * @author klemela * */ public class SelectableChartPanel extends JPanel implements MouseListener, MouseMotionListener { private TransparentPanel transparentPanel; private ChartPanel chartPanel; private SelectionChangeListener selectionListener; private Rectangle2D.Double selection; //Maybe in future selectable charts have also the zoom functionality private final boolean isZoomable = false; //This class has it's own overlay panel for drawing selection rectangles to //make horizontal selection possible in category plots. With //XYPlot it's also possible to use JFC:s zoom outline private final boolean useZoomOutline = false; public interface SelectionChangeListener{ /** * Data points inside given rectangle should be added to selection if they aren't * there already and removed if they are. If the rectangle is null, selection should be * cleared. This makes it possible to handle all situations with single logic. If selection * is made without pressing control key, this method is called first with null argument to * clear the selection and again with new selection rectangle. * * @param newSelection */ public void selectionChanged(Rectangle2D.Double newSelection); } public SelectableChartPanel(JFreeChart chart, SelectionChangeListener selectionListener) { this(chart, selectionListener, true); } public SelectableChartPanel(JFreeChart chart, SelectionChangeListener selectionListener, boolean scalable) { super(new OverlayLayout()); this.selectionListener = selectionListener; if(scalable){ chartPanel = Visualisation.makePanel(chart); } else { chartPanel = Visualisation.makenNonScalablePanel(chart); } transparentPanel = new TransparentPanel(); this.add(transparentPanel); this.add(chartPanel); chartPanel.addMouseListener(this); chartPanel.addMouseMotionListener(this); chartPanel.setMouseZoomable(isZoomable); } public Rectangle.Double translateToChart(Rectangle mouseCoords){ //Screen coordinates are integers Point corner1 = new Point((int)mouseCoords.getMinX(), (int)mouseCoords.getMinY()); Point corner2 = new Point((int)mouseCoords.getMaxX(), (int)mouseCoords.getMaxY()); Point.Double translated1 = translateToChart(corner1); Point.Double translated2 = translateToChart(corner2); return createOrderedRectangle(translated1, translated2); } public Point2D.Double translateToChart(Point mouseCoords) { Insets insets = getInsets(); int mouseX = (int) ((mouseCoords.getX() - insets.left) / chartPanel.getScaleX()); int mouseY = (int) ((mouseCoords.getY() - insets.top) / chartPanel.getScaleY()); Point2D p = chartPanel.translateScreenToJava2D(new Point(mouseX, mouseY)); Plot plot = chartPanel.getChart().getPlot(); if (plot instanceof XYPlot) { XYPlot xyPlot = (XYPlot) plot; ChartRenderingInfo info = chartPanel.getChartRenderingInfo(); Rectangle2D dataArea = info.getPlotInfo().getDataArea(); ValueAxis domainAxis = xyPlot.getDomainAxis(); RectangleEdge domainAxisEdge = xyPlot.getDomainAxisEdge(); ValueAxis rangeAxis = xyPlot.getRangeAxis(); RectangleEdge rangeAxisEdge = xyPlot.getRangeAxisEdge(); /*multiplication by getScale to take care of the scaling of Java2D Graphics from Java2D * coordinates to screen coordinates. Function java2DToValue is used to take care of the * internal scaling of JFreeChart which scales data values to Java2D coordinates */ double chartX = domainAxis.java2DToValue(p.getX() * chartPanel.getScaleX(), dataArea, domainAxisEdge); double chartY = rangeAxis.java2DToValue(p.getY() * chartPanel.getScaleY(), dataArea, rangeAxisEdge); return new Point2D.Double(chartX, chartY); } else if (plot instanceof CategoryPlot) { CategoryPlot catPlot = (CategoryPlot) plot; ChartRenderingInfo info = chartPanel.getChartRenderingInfo(); Rectangle2D dataArea = info.getPlotInfo().getDataArea(); CategoryAxis domainAxis = catPlot.getDomainAxis(); RectangleEdge domainAxisEdge = catPlot.getDomainAxisEdge(); int categoryCount = catPlot.getCategories().size(); ValueAxis rangeAxis = catPlot.getRangeAxis(); RectangleEdge rangeAxisEdge = catPlot.getRangeAxisEdge(); //See comment from getSelectionRectangle method for x coordinate translation double firstCategoryX = domainAxis.getCategoryJava2DCoordinate( catPlot.getDomainGridlinePosition(), 0, categoryCount, dataArea, domainAxisEdge); double lastCategoryX = domainAxis.getCategoryJava2DCoordinate( catPlot.getDomainGridlinePosition(), categoryCount - 1, categoryCount, dataArea, domainAxisEdge); /*multiplication by getScale to take care of the scaling of Java2D Graphics from Java2D * coordinates to screen coordinates. Function java2DToValue is used to take care of the * internal scaling of JFreeChart which scales data values to Java2D coordinates */ double scaledX = p.getX() * chartPanel.getScaleX(); double chartY = rangeAxis.java2DToValue(p.getY() * chartPanel.getScaleY(), dataArea, rangeAxisEdge); //Category axis doesn't have integer values, so we calculate it double relativeX = (scaledX - firstCategoryX) / (lastCategoryX - firstCategoryX) * (categoryCount - 1); return new Point2D.Double(relativeX, chartY); } else if (plot instanceof HCPlot) { //HCPlot hcPlot = (HCPlot)plot; Insets chartInsets = getInsets(); int x = (int) ((p.getX() - chartInsets.left) * chartPanel.getScaleX()); int y = (int) ((p.getY() - chartInsets.top) * chartPanel.getScaleY()); return new Point2D.Double(x, y); } else { throw new UnsupportedOperationException("Only Category, XY and HC plots are supported until now"); } } /** * Gives two corners of selection rectangle relative to original data values. If the * chart is categoryChart, the x values are scaled to category indexes so that click on first * category returns 0, click on last return (categoryCount - 1) and other values are interpolated * or extrapolated from these. If the selection was made with a single click, the width and * height have value of 0; * * @return Selection rectangle, null if selection should be cleared */ public Rectangle2D.Double getSelectionRectangle(){ return selection; } public void mouseClicked(MouseEvent e) { if (!e.isControlDown()) { setSelection(null); } //Rectangle made by single click is grown by couple pixels to make clicking easier. //Growing should be done by screen pixels, and this is the latest point for that for now. //If accurate single click detection is needed some day later, this should be moved to //SelectionChangeListeners and implement needed methods or parameters. Rectangle rect = new Rectangle((int)e.getPoint().getX(), (int)e.getPoint().getY(), 0 ,0); rect.grow(3, 3); Rectangle.Double translatedRect = translateToChart(rect); setSelection(translatedRect); } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } private Point startCoords; private boolean isDragged = false; public void mousePressed(MouseEvent e) { if (useZoomOutline){ chartPanel.setMouseZoomable(false,true); chartPanel.mousePressed(e); chartPanel.setMouseZoomable(true,false); } if(isZoomable){ chartPanel.mousePressed(e); } startCoords = e.getPoint(); isDragged = false; } public void mouseReleased(MouseEvent e) { if (useZoomOutline){ chartPanel.setMouseZoomable(false,true); chartPanel.mouseReleased(e); chartPanel.setMouseZoomable(true,false); } if(isZoomable){ chartPanel.mouseReleased(e); } // Hide the selection rectangle transparentPanel.setArea(null); transparentPanel.repaint(); if (isDragged) { if (!e.isControlDown()) { setSelection(null); } Point2D.Double translatedStart = translateToChart(startCoords); Point2D.Double translatedEnd = translateToChart(e.getPoint()); setSelection(createOrderedRectangle(translatedStart, translatedEnd)); } } public void mouseDragged(MouseEvent e) { isDragged = true; transparentPanel.setArea(createOrderedRectangle(startCoords, e.getPoint()).getBounds()); transparentPanel.repaint(); } public void mouseMoved(MouseEvent e) { } /** * Creates rectangle, in which the first coordinate pair is upper left corner * * @param p1 * @param p2 * @return */ private Rectangle2D.Double createOrderedRectangle(Point2D p1, Point2D p2){ double x = p1.getX() < p2.getX() ? p1.getX() : p2.getX(); double y = p1.getY() < p2.getY() ? p1.getY() : p2.getY(); double w = Math.abs(p2.getX() - p1.getX()); double h = Math.abs(p2.getY() - p1.getY()); return new Rectangle2D.Double(x, y, w, h); } private void setSelection(Rectangle2D.Double selection){ this.selection = selection; if(selectionListener != null){ selectionListener.selectionChanged(this.selection); } } protected class TransparentPanel extends JPanel { private Rectangle2D area; public TransparentPanel() { this.setOpaque(false); } @Override public void paintComponent(Graphics g) { super.paintComponents(g); if (area != null) { Graphics2D g2d = (Graphics2D) g; g2d.setColor(Color.DARK_GRAY); g2d.setStroke(VisualConstants.dashLine); g2d.draw(area); } } protected void setArea(Rectangle area) { this.area = area; } } public ChartPanel getChartPanel() { return chartPanel; } }