/******************************************************************************* * Mission Control Technologies, Copyright (c) 2009-2012, United States Government * as represented by the Administrator of the National Aeronautics and Space * Administration. All rights reserved. * * The MCT platform is 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. * * MCT includes source code licensed under additional open source licenses. See * the MCT Open Source Licenses file included with this distribution or the About * MCT Licenses dialog available at runtime from the MCT Help menu for additional * information. *******************************************************************************/ package plotter.xy; import java.awt.Color; import java.awt.Component; import java.awt.Graphics; import java.awt.event.InputEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.geom.Point2D; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.swing.JComponent; /** * A slope line allows the user to click and drag on a plot, drawing a line. * Slope lines are allowed to be attached to multiple plots at the same time. * This works because the user can only draw a slope line on one plot at a time. * @author Adam Crume */ public class SlopeLine extends JComponent implements MouseListener, MouseMotionListener { private static final long serialVersionUID = 1L; private static final int CURSOR_BOX_SIZE = 4; /** X coordinate of the point where the user started drawing a slope line. */ private int startX; /** Y coordinate of the point where the user started drawing a slope line. */ private int startY; /** Plot contents displaying the slope line, or null if the slope line is not displayed. */ private XYPlotContents contents; // Note that the end points of the slope line are not guaranteed to be in any particular order. /** X coordinate of end 1 of the slope line, or -1 to flag that the line is not yet visible. */ private int x1; /** Y coordinate of end 1 of the slope line. */ private int y1; /** X coordinate of end 2 of the slope line. */ private int x2; /** Y coordinate of end 2 of the slope line. */ private int y2; /** Maps plots to listeners for that plot. The key null is used for listeners that listen to all plots. May be null. */ private Map<XYPlot, Set<Listener>> listeners; private boolean showSlopeLine = false; @Override protected void paintComponent(Graphics g) { if(contents != null && x1 != -1) { Color savedColor = g.getColor(); try { g.setColor(getForeground()); int b = CURSOR_BOX_SIZE; // Draw a small rectangle where the user originally pressed the mouse button. g.drawRect(startX - b / 2, startY - b / 2, b, b); // Draw the slope line itself. g.drawLine(x1, y1, x2, y2); } finally { g.setColor(savedColor); } } } @Override public void mouseClicked(MouseEvent e) { // ignore } @Override public void mouseEntered(MouseEvent e) { // ignore } @Override public void mouseExited(MouseEvent e) { // ignore } @Override public void mousePressed(MouseEvent e) { showSlopeLine = false; if ((e.getModifiersEx() & Integer.MAX_VALUE) == InputEvent.BUTTON1_DOWN_MASK) showSlopeLine = true; else return; startX = e.getX(); startY = e.getY(); x1 = -1; contents = (XYPlotContents) e.getSource(); contents.add(this); contents.revalidate(); XYPlot plot = (XYPlot) contents.getParent(); Set<Listener> set = getListenersForPlot(plot); if(set != null) { Point2D p = new Point2D.Double(startX, startY); plot.toLogical(p, p); double startLogicalX = p.getX(); double startLogicalY = p.getY(); for(Listener listener : set) { listener.slopeLineAdded(this, plot, startLogicalX, startLogicalY); } } } @Override public void mouseReleased(MouseEvent e) { if (!showSlopeLine) return; if(contents == null) { return; } contents.remove(this); int minX = Math.min(x1, x2); int maxX = Math.max(x1, x2); int minY = Math.min(y1, y2); int maxY = Math.max(y1, y2); repaintLine(contents, minX, minY, maxX, maxY); XYPlot plot = (XYPlot) contents.getParent(); Set<Listener> set = getListenersForPlot(plot); if(set != null) { for(Listener listener : set) { listener.slopeLineRemoved(this, plot); } } contents = null; } @Override public void mouseDragged(MouseEvent e) { if (!showSlopeLine) return; // Old bounding box int minX = Math.min(x1, x2); int maxX = Math.max(x1, x2); int minY = Math.min(y1, y2); int maxY = Math.max(y1, y2); // Extend the line to the edges of the contents int dx = e.getX() - startX; int dy = e.getY() - startY; if(dx == 0 && dy == 0) { x1 = -1; } else if(Math.abs(dx) > Math.abs(dy)) { double m = dy / (double) dx; double b = startY - m * startX; x1 = 0; y1 = (int) (b + .5); x2 = getWidth(); y2 = (int) (m * x2 + b + .5); } else { double m = dx / (double) dy; double b = startX - m * startY; y1 = 0; x1 = (int) (b + .5); y2 = getHeight(); x2 = (int) (m * y2 + b + .5); } // Union the old bounding box with the new one minX = Math.min(minX, Math.min(x1, x2)); maxX = Math.max(maxX, Math.max(x1, x2)); minY = Math.min(minY, Math.min(y1, y2)); maxY = Math.max(maxY, Math.max(y1, y2)); // Repaint the union of the old and new bounding boxes repaintLine(this, minX, minY, maxX, maxY); XYPlot plot = (XYPlot) contents.getParent(); Set<Listener> set = getListenersForPlot(plot); if(set != null) { Point2D p = new Point2D.Double(startX, startY); plot.toLogical(p, p); double startLogicalX = p.getX(); double startLogicalY = p.getY(); p.setLocation(e.getX(), e.getY()); plot.toLogical(p, p); double endLogicalX = p.getX(); double endLogicalY = p.getY(); for(Listener listener : set) { listener.slopeLineUpdated(this, plot, startLogicalX, startLogicalY, endLogicalX, endLogicalY); } } } /** * Returns all listeners that should receive events for the plot. * A return value of null is equivalent to the empty set. * @param plot plot with changes * @return relevant listeners, may be null */ private Set<Listener> getListenersForPlot(XYPlot plot) { if(listeners != null) { Set<Listener> wildcardListeners = listeners.get(null); Set<Listener> specificListeners = listeners.get(plot); if(wildcardListeners == null) { return specificListeners; } else if(specificListeners == null) { return wildcardListeners; } else { Set<Listener> s = new HashSet<Listener>(wildcardListeners); s.addAll(specificListeners); return s; } } return null; } /** * Given the slope line's bounding box, repaints an area large enough to contain the bounding box and the cursor rectangle. * @param c component to repaint * @param minX minimum X coordinate of the bounding box * @param minY minimum Y coordinate of the bounding box * @param maxX maximum X coordinate of the bounding box * @param maxY maximum Y coordinate of the bounding box */ private void repaintLine(Component c, int minX, int minY, int maxX, int maxY) { int width = maxX - minX + 1; int height = maxY - minY + 1; if(width < CURSOR_BOX_SIZE) { minX = (minX + maxX) / 2 - CURSOR_BOX_SIZE / 2 - 1; width = CURSOR_BOX_SIZE + 2; } if(height < CURSOR_BOX_SIZE) { minY = (minY + maxY) / 2 - CURSOR_BOX_SIZE / 2 - 1; height = CURSOR_BOX_SIZE + 2; } c.repaint(minX, minY, width, height); } @Override public void mouseMoved(MouseEvent e) { // ignore } /** * Attaches this slope line to a plot. * @param plot plot to attach to */ public void attach(XYPlot plot) { XYPlotContents contents = plot.getContents(); if(contents == null) { throw new IllegalArgumentException("Plot does not contain an XYPlotContents component"); } contents.addMouseListener(this); contents.addMouseMotionListener(this); } /** * Adds a listener for slope changes. * @param plot plot to listen to, or null for all plots this line is attached to * @param listener listener to add */ public void addListenerForPlot(XYPlot plot, Listener listener) { if(listeners == null) { listeners = new HashMap<XYPlot, Set<Listener>>(); } Set<Listener> set = listeners.get(plot); if(set == null) { set = new HashSet<Listener>(); listeners.put(plot, set); } set.add(listener); } /** * Listens for slope changes. * @author Adam Crume */ public interface Listener { /** * Notifies that a slope line has been started. * @param line line that was added * @param plot plot the line is displayed on * @param startX X coordinate of the start point * @param startY Y coordinate of the start point */ void slopeLineAdded(SlopeLine line, XYPlot plot, double startX, double startY); /** * Notifies that a slope line has been updated. * @param line line that was updated * @param plot plot the line is displayed on * @param startX X coordinate of the start point * @param startY Y coordinate of the start point * @param endX X coordinate of the end point * @param endY Y coordinate of the end point */ void slopeLineUpdated(SlopeLine line, XYPlot plot, double startX, double startY, double endX, double endY); /** * Notifies that a slope line has been hidden. * @param line line that was hidden * @param plot plot that the line was displayed on */ void slopeLineRemoved(SlopeLine line, XYPlot plot); } }