package lcm.spy; import java.awt.Color; import java.awt.Container; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowFocusListener; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Iterator; import info.monitorenter.gui.chart.IAxis; import info.monitorenter.gui.chart.ITrace2D; import info.monitorenter.gui.chart.ZoomableChart; import info.monitorenter.gui.chart.axis.AAxis; import info.monitorenter.gui.chart.axis.AxisLinear; import info.monitorenter.gui.chart.labelformatters.LabelFormatterNumber; import info.monitorenter.gui.chart.traces.Trace2DLtd; import javax.swing.*; /** * Chart that supports panning and zooming in the Google-maps style. * */ public class ZoomableChartScrollWheel extends ZoomableChart { private double mouseDownStartX, mouseDownStartY, mouseDownValPerPxX, mouseDownMinX, mouseDownMaxX; private ArrayList<Double> mouseDownValPerPxY = new ArrayList<Double>(); private ArrayList<Double> mouseDownMinY = new ArrayList<Double>(); private ArrayList<Double> mouseDownMaxY = new ArrayList<Double>(); private long lastFocusTime = -1; private JFrame frame = null; // internal color list private ArrayList<Color> colors = new ArrayList<Color>(); // color index private int colorNum = 0; // we need a list of the axes on the right, which we update ourselves private ArrayList<AAxis> rightYAxis = new ArrayList<AAxis>(); private JPopupMenu popup = new JPopupMenu(); ChartData chartData; /** * Constructor, taking in a chartData so that we can set up the chart * * @param chartData global data about all charts displayed in lcm-spy */ public ZoomableChartScrollWheel(ChartData chartData) { this.addMouseWheelListener(new MyMouseWheelListener(this)); this.getAxisX().setPaintGrid(true); this.getAxisY().setPaintGrid(true); this.setUseAntialiasing(true); this.setGridColor(Color.LIGHT_GRAY); this.getAxisX().getAxisTitle().setTitle("Time (sec)"); this.getAxisY().getAxisTitle().setTitle(""); this.chartData = chartData; colors.add(Color.RED); colors.add(Color.BLACK); colors.add(Color.BLUE); colors.add(Color.MAGENTA); colors.add(Color.CYAN); colors.add(Color.ORANGE); colors.add(Color.GREEN); this.setFixedWidthXAxisFormat(); this.setMinPaintLatency(16); // cap the frame-rate at 60fps } /** * Creates a new frame for this trace. Called either by ObjectPanel to create * a new graph or from the right-click menu to move a trace to a new graph. * * @param chartData global chart data for all of lcm-spy * @param trace data that this chart should display */ public static void newChartFrame(final ChartData chartData, final ITrace2D trace) { JFrame frame = new JFrame(trace.getName()); final ZoomableChartScrollWheel newChart = new ZoomableChartScrollWheel(chartData); trace.setColor(newChart.popColor()); newChart.addTrace(trace); newChart.updateRightClickMenu(); chartData.getCharts().add(newChart); Container content = frame.getContentPane(); content.add(newChart); newChart.addFrameFocusTimer(frame); frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { for (ITrace2D trace : newChart.getTraces()) { ((Trace2DLtd)trace).setMaxSize(chartData.sparklineChartSize); } chartData.getCharts().remove(newChart); } }); frame.setSize(600, 500); frame.setLocationByPlatform(true); frame.setVisible(true); } /** * Shows the right-click menu if appropriate. * * @param e MouseEvent to process * @return true if the right-click menu was shown */ private boolean maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger()) { popup.show(e.getComponent(), e.getX(), e.getY()); return true; } return false; } /** * Gets the next color for a new trace. Use this to keep colors as different * as possible. Increments the color counter. * * @return next color to use for a trace */ public Color popColor() { Color thisColor = colors.get(colorNum % colors.size()); colorNum++; return thisColor; } /** * Adds the newest trace color back onto the stack. */ public void pushColor() { colorNum--; } /** * Updates the right click menu to allow for moving * traces around. Should be called immediately after adding * a new trace. */ public void updateRightClickMenu() { // zap the old right click menu popup = new JPopupMenu(); Iterator<ITrace2D> iter = this.getTraces().iterator(); boolean firstFlag = true; StringBuilder frameTitle = new StringBuilder(); while (iter.hasNext()) { final ITrace2D trace = iter.next(); JMenuItem topItem = new JMenuItem(trace.getName()); topItem.setEnabled(false); if (!firstFlag) { popup.addSeparator(); } popup.add(topItem); popup.addSeparator(); boolean rightTraceFlag = false; for (final AAxis axis : rightYAxis) { if (axis.getTraces().contains(trace)) { // this trace is in the extra Y axis area JMenuItem newItem = new JMenuItem(" to main axis"); newItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ZoomableChartScrollWheel.this.removeAxisYRight(axis); ZoomableChartScrollWheel.this.removeTrace(trace); //rightYAxis.remove(axis); ZoomableChartScrollWheel.this.addTrace(trace); ZoomableChartScrollWheel.this.updateRightClickMenu(); } }); popup.add(newItem); rightTraceFlag = true; break; } } if (rightTraceFlag == false) { // this trace is on the normal Y axis JMenuItem newItem = new JMenuItem(" to separate axis"); if (this.getAxisY().getTraces().size() < 2) { newItem.setEnabled(false); } newItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { AxisLinear newAxis = new AxisLinear(); ZoomableChartScrollWheel.this.removeTrace(trace); ZoomableChartScrollWheel.this.addAxisYRight(newAxis); ZoomableChartScrollWheel.this.addTrace(trace, ZoomableChartScrollWheel.this.getAxisX(), newAxis); ZoomableChartScrollWheel.this.updateRightClickMenu(); } }); popup.add(newItem); } JMenuItem moveWindowItem = new JMenuItem(" move to new window"); moveWindowItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { for (AAxis axisL : rightYAxis) { if (axisL.getTraces().contains(trace)) { ZoomableChartScrollWheel.this.removeAxisYRight(axisL); break; } } ZoomableChartScrollWheel.this.removeTrace(trace); ZoomableChartScrollWheel.this.updateRightClickMenu(); ZoomableChartScrollWheel.newChartFrame(chartData, trace); } }); JMenuItem delItem = new JMenuItem(" remove"); delItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { for (AAxis axisL : rightYAxis) { if (axisL.getTraces().contains(trace)) { ZoomableChartScrollWheel.this.removeAxisYRight(axisL); break; } } ZoomableChartScrollWheel.this.removeTrace(trace); ZoomableChartScrollWheel.this.updateRightClickMenu(); } }); if (this.getAxisX().getTraces().size() < 2) { delItem.setEnabled(false); moveWindowItem.setEnabled(false); } popup.add(moveWindowItem); popup.add(delItem); if (!firstFlag) { frameTitle.append(", "); } frameTitle.append(trace.getName()); firstFlag = false; } if (this.frame != null) { this.frame.setTitle(frameTitle.toString()); } } public void addAxisYRight(AAxis<?> axisY) { super.addAxisYRight(axisY); rightYAxis.add(axisY); } public boolean removeAxisYRight(IAxis<?> axisY) { rightYAxis.remove(axisY); return super.removeAxisYRight(axisY); } /** * Move this frame to the front to get the user's attention */ public void toFront() { if (frame != null) { java.awt.EventQueue.invokeLater(new Runnable() { @Override public void run() { frame.toFront(); frame.repaint(); } }); } } /** * Saves the time this window was last in focus. Allows us to put new traces * on the last chart that was in focus * * @param frame frame to add the focus timer to */ public void addFrameFocusTimer(JFrame frame) { this.frame = frame; this.frame.addWindowFocusListener(new WindowFocusListener() { public void windowGainedFocus(WindowEvent we) { lastFocusTime = System.nanoTime()/1000; } public void windowLostFocus(WindowEvent we) { lastFocusTime = System.nanoTime()/1000; } }); } /** * Returns the last time this frame was focused. * * @return the last time the frame was focused */ public long getLastFocusTime() { return lastFocusTime; } /** * Handle mouse press events * */ public void mousePressed(MouseEvent e) { if (maybeShowPopup(e)) { return; } IAxis xAxis = this.getAxisX(); IAxis yAxis = this.getAxisY(); double xAxisRange = xAxis.getRange().getExtent(); mouseDownValPerPxY.clear(); mouseDownMinY.clear(); mouseDownMaxY.clear(); mouseDownStartX = e.getX(); mouseDownStartY = e.getY(); double xAxisWidth = this.getXChartEnd() - this.getXChartStart(); double yAxisHeight = this.getYChartStart() - this.getYChartEnd(); mouseDownValPerPxX = xAxisRange / xAxisWidth; mouseDownMinX = xAxis.getMin(); mouseDownMaxX = xAxis.getMax(); double yAxisRange = yAxis.getRange().getExtent(); mouseDownValPerPxY.add(yAxisRange / yAxisHeight); mouseDownMinY.add(yAxis.getMin()); mouseDownMaxY.add(yAxis.getMax()); for (AAxis yAxisRight : rightYAxis) { double yAxisRangeRight = yAxisRight.getMax() - yAxisRight.getMin(); mouseDownValPerPxY.add(yAxisRangeRight / yAxisHeight); mouseDownMinY.add(yAxisRight.getMin()); mouseDownMaxY.add(yAxisRight.getMax()); } } /** * Pan the chart when the mouse is dragged. */ public void mouseDragged(MouseEvent e) { // move the view if ((e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) != MouseEvent.BUTTON3_DOWN_MASK) { dragChart(e); } } /** * Handle mouse release events, including double-click and right click. */ public void mouseReleased(MouseEvent e) { if (e.getClickCount() == 2) { this.zoomAll(); setFixedWidthXAxisFormat(); e.consume(); } else { maybeShowPopup(e); } } /** * Implements panning the chart on mouse drag. * * @param e MouseEvent to process */ private void dragChart(MouseEvent e) { double deltaPxX = e.getX() - mouseDownStartX; double deltaPxY = e.getY() - mouseDownStartY; double deltaX = deltaPxX * mouseDownValPerPxX; double deltaY = deltaPxY * mouseDownValPerPxY.get(0); if (Double.isNaN(mouseDownMinX) || Double.isNaN(mouseDownMaxX) || Double.isNaN(mouseDownMinY.get(0)) || Double.isNaN(mouseDownMaxY.get(0)) ||Double.isNaN(deltaX) || Double.isNaN(deltaY) || Double.isInfinite(mouseDownMinX) || Double.isInfinite(mouseDownMaxX) || Double.isInfinite(mouseDownMinY.get(0)) || Double.isInfinite(mouseDownMaxY.get(0)) ||Double.isInfinite(deltaX) || Double.isInfinite(deltaY)) { return; } zoom(mouseDownMinX - deltaX, mouseDownMaxX - deltaX, mouseDownMinY.get(0) + deltaY, mouseDownMaxY.get(0) + deltaY); setVariableWidthXAxisFormat(); // do moving for right Y axes for (int i = 0; i < rightYAxis.size(); i++) { AAxis axis = rightYAxis.get(i); double deltaYRight = deltaPxY * mouseDownValPerPxY.get(i+1); zoom(axis, axis.translateValueToPx(mouseDownMinY.get(i+1) + deltaYRight), axis.translateValueToPx(mouseDownMaxY.get(i+1) + deltaYRight)); } } /** * Variable-width x-axis formatting causes jumps when data is "rolling in" * because the labels change width, which causes the X-axis to change width * which causes visual disruption. When using zoomAll, change to a fixed * width format on the x-axis. */ private void setFixedWidthXAxisFormat() { DecimalFormat fixedWidthFormat = new DecimalFormat("#"); LabelFormatterNumber fixedWidthFormatter = new LabelFormatterNumber(fixedWidthFormat); this.getAxisX().setFormatter(fixedWidthFormatter); } /** * When data is not "rolling in" use a varible format for * maximum flexibility in display. */ private void setVariableWidthXAxisFormat() { DecimalFormat variableWidthFormat = new DecimalFormat(); LabelFormatterNumber variableWidthFormatter = new LabelFormatterNumber(variableWidthFormat); this.getAxisX().setFormatter(variableWidthFormatter); } public class MyMouseWheelListener implements MouseWheelListener { private ZoomableChartScrollWheel chart; public MyMouseWheelListener(ZoomableChartScrollWheel chart) { this.chart = chart; } @Override public void mouseWheelMoved(MouseWheelEvent e) { int notches = e.getWheelRotation(); IAxis xAxis = chart.getAxisX(); IAxis yAxis = chart.getAxisY(); double xAxisRange = xAxis.getRange().getExtent(); double yAxisRange = yAxis.getRange().getExtent(); double zoomFactor; if (notches > 0) { zoomFactor = notches * 1.2; } else { zoomFactor = -notches * 0.8; } double xSqSize = xAxisRange * zoomFactor; double ySqSize = yAxisRange * zoomFactor; // compute percentage of chart the mouse pointer is at double xPercent = ((double) e.getX() - (double) chart.getXChartStart()) / (double)(chart.getXChartEnd() - chart.getXChartStart()); double yPercent = ((double) e.getY() - (double) chart.getYChartEnd()) / (double)(chart.getYChartStart() - chart.getYChartEnd()); // compute new bounds with the percentages remaining the same so whatever // is under the cursor will stay under the cursor // the left most value should be the value the pixel is at minus half of the square size double xValueUnderCursor = xAxis.translatePxToValue(e.getX()); double xMin = xValueUnderCursor - xSqSize * xPercent; double xMax = xValueUnderCursor + xSqSize * (1 - xPercent); double yValueUnderCursor = yAxis.translatePxToValue(e.getY()); double yMin = yValueUnderCursor - ySqSize * (1 - yPercent); double yMax = yValueUnderCursor + ySqSize * yPercent; if (Double.isNaN(xMin) || Double.isNaN(xMax) || Double.isNaN(yMin) || Double.isNaN(yMax)) { return; } chart.zoom(xMin, xMax, yMin, yMax); // also zoom right hand Y axes for (int i = 0; i < rightYAxis.size(); i++) { AAxis axis = rightYAxis.get(i); double axisRange = axis.getMax() - axis.getMin(); double sqSize =axisRange * zoomFactor; double underCursor = axis.translatePxToValue(e.getY()); double minVal = underCursor - sqSize * (1 - yPercent); double maxVal = underCursor + sqSize * yPercent; zoom(axis, axis.translateValueToPx(minVal), axis.translateValueToPx(maxVal)); } setVariableWidthXAxisFormat(); } } }