| /* |
| * Copyright (c) 2004, 2013, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code 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 General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package sun.tools.jconsole; |
| |
| import java.awt.*; |
| import java.awt.event.*; |
| import java.beans.*; |
| import java.io.*; |
| import java.lang.reflect.Array; |
| import java.util.*; |
| |
| import javax.accessibility.*; |
| import javax.swing.*; |
| import javax.swing.border.*; |
| import javax.swing.filechooser.*; |
| import javax.swing.filechooser.FileFilter; |
| |
| |
| import com.sun.tools.jconsole.JConsoleContext; |
| |
| import static sun.tools.jconsole.Formatter.*; |
| import static sun.tools.jconsole.ProxyClient.*; |
| |
| @SuppressWarnings("serial") |
| public class Plotter extends JComponent |
| implements Accessible, ActionListener, PropertyChangeListener { |
| |
| public static enum Unit { |
| NONE, BYTES, PERCENT |
| } |
| |
| static final String[] rangeNames = { |
| Messages.ONE_MIN, |
| Messages.FIVE_MIN, |
| Messages.TEN_MIN, |
| Messages.THIRTY_MIN, |
| Messages.ONE_HOUR, |
| Messages.TWO_HOURS, |
| Messages.THREE_HOURS, |
| Messages.SIX_HOURS, |
| Messages.TWELVE_HOURS, |
| Messages.ONE_DAY, |
| Messages.SEVEN_DAYS, |
| Messages.ONE_MONTH, |
| Messages.THREE_MONTHS, |
| Messages.SIX_MONTHS, |
| Messages.ONE_YEAR, |
| Messages.ALL |
| }; |
| |
| static final int[] rangeValues = { |
| 1, |
| 5, |
| 10, |
| 30, |
| 1 * 60, |
| 2 * 60, |
| 3 * 60, |
| 6 * 60, |
| 12 * 60, |
| 1 * 24 * 60, |
| 7 * 24 * 60, |
| 1 * 31 * 24 * 60, |
| 3 * 31 * 24 * 60, |
| 6 * 31 * 24 * 60, |
| 366 * 24 * 60, |
| -1 |
| }; |
| |
| |
| final static long SECOND = 1000; |
| final static long MINUTE = 60 * SECOND; |
| final static long HOUR = 60 * MINUTE; |
| final static long DAY = 24 * HOUR; |
| |
| final static Color bgColor = new Color(250, 250, 250); |
| final static Color defaultColor = Color.blue.darker(); |
| |
| final static int ARRAY_SIZE_INCREMENT = 4000; |
| |
| private static Stroke dashedStroke; |
| |
| private TimeStamps times = new TimeStamps(); |
| private ArrayList<Sequence> seqs = new ArrayList<Sequence>(); |
| private JPopupMenu popupMenu; |
| private JMenu timeRangeMenu; |
| private JRadioButtonMenuItem[] menuRBs; |
| private JMenuItem saveAsMI; |
| private JFileChooser saveFC; |
| |
| private int viewRange = -1; // Minutes (value <= 0 means full range) |
| private Unit unit; |
| private int decimals; |
| private double decimalsMultiplier; |
| private Border border = null; |
| private Rectangle r = new Rectangle(1, 1, 1, 1); |
| private Font smallFont = null; |
| |
| // Initial margins, may be recalculated as needed |
| private int topMargin = 10; |
| private int bottomMargin = 45; |
| private int leftMargin = 65; |
| private int rightMargin = 70; |
| private final boolean displayLegend; |
| |
| public Plotter() { |
| this(Unit.NONE, 0); |
| } |
| |
| public Plotter(Unit unit) { |
| this(unit, 0); |
| } |
| |
| public Plotter(Unit unit, int decimals) { |
| this(unit,decimals,true); |
| } |
| |
| // Note: If decimals > 0 then values must be decimally shifted left |
| // that many places, i.e. multiplied by Math.pow(10.0, decimals). |
| public Plotter(Unit unit, int decimals, boolean displayLegend) { |
| this.displayLegend = displayLegend; |
| setUnit(unit); |
| setDecimals(decimals); |
| |
| enableEvents(AWTEvent.MOUSE_EVENT_MASK); |
| |
| addMouseListener(new MouseAdapter() { |
| @Override |
| public void mousePressed(MouseEvent e) { |
| if (getParent() instanceof PlotterPanel) { |
| getParent().requestFocusInWindow(); |
| } |
| } |
| }); |
| |
| } |
| |
| public void setUnit(Unit unit) { |
| this.unit = unit; |
| } |
| |
| public void setDecimals(int decimals) { |
| this.decimals = decimals; |
| this.decimalsMultiplier = Math.pow(10.0, decimals); |
| } |
| |
| public void createSequence(String key, String name, Color color, boolean isPlotted) { |
| Sequence seq = getSequence(key); |
| if (seq == null) { |
| seq = new Sequence(key); |
| } |
| seq.name = name; |
| seq.color = (color != null) ? color : defaultColor; |
| seq.isPlotted = isPlotted; |
| |
| seqs.add(seq); |
| } |
| |
| public void setUseDashedTransitions(String key, boolean b) { |
| Sequence seq = getSequence(key); |
| if (seq != null) { |
| seq.transitionStroke = b ? getDashedStroke() : null; |
| } |
| } |
| |
| public void setIsPlotted(String key, boolean isPlotted) { |
| Sequence seq = getSequence(key); |
| if (seq != null) { |
| seq.isPlotted = isPlotted; |
| } |
| } |
| |
| // Note: If decimals > 0 then values must be decimally shifted left |
| // that many places, i.e. multiplied by Math.pow(10.0, decimals). |
| public synchronized void addValues(long time, long... values) { |
| assert (values.length == seqs.size()); |
| times.add(time); |
| for (int i = 0; i < values.length; i++) { |
| seqs.get(i).add(values[i]); |
| } |
| repaint(); |
| } |
| |
| private Sequence getSequence(String key) { |
| for (Sequence seq : seqs) { |
| if (seq.key.equals(key)) { |
| return seq; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * @return the displayed time range in minutes, or -1 for all data |
| */ |
| public int getViewRange() { |
| return viewRange; |
| } |
| |
| /** |
| * @param minutes the displayed time range in minutes, or -1 to diaplay all data |
| */ |
| public void setViewRange(int minutes) { |
| if (minutes != viewRange) { |
| int oldValue = viewRange; |
| viewRange = minutes; |
| /* Do not i18n this string */ |
| firePropertyChange("viewRange", oldValue, viewRange); |
| if (popupMenu != null) { |
| for (int i = 0; i < menuRBs.length; i++) { |
| if (rangeValues[i] == viewRange) { |
| menuRBs[i].setSelected(true); |
| break; |
| } |
| } |
| } |
| repaint(); |
| } |
| } |
| |
| @Override |
| public JPopupMenu getComponentPopupMenu() { |
| if (popupMenu == null) { |
| popupMenu = new JPopupMenu(Messages.CHART_COLON); |
| timeRangeMenu = new JMenu(Messages.PLOTTER_TIME_RANGE_MENU); |
| timeRangeMenu.setMnemonic(Resources.getMnemonicInt(Messages.PLOTTER_TIME_RANGE_MENU)); |
| popupMenu.add(timeRangeMenu); |
| menuRBs = new JRadioButtonMenuItem[rangeNames.length]; |
| ButtonGroup rbGroup = new ButtonGroup(); |
| for (int i = 0; i < rangeNames.length; i++) { |
| menuRBs[i] = new JRadioButtonMenuItem(rangeNames[i]); |
| rbGroup.add(menuRBs[i]); |
| menuRBs[i].addActionListener(this); |
| if (viewRange == rangeValues[i]) { |
| menuRBs[i].setSelected(true); |
| } |
| timeRangeMenu.add(menuRBs[i]); |
| } |
| |
| popupMenu.addSeparator(); |
| |
| saveAsMI = new JMenuItem(Messages.PLOTTER_SAVE_AS_MENU_ITEM); |
| saveAsMI.setMnemonic(Resources.getMnemonicInt(Messages.PLOTTER_SAVE_AS_MENU_ITEM)); |
| saveAsMI.addActionListener(this); |
| popupMenu.add(saveAsMI); |
| } |
| return popupMenu; |
| } |
| |
| public void actionPerformed(ActionEvent ev) { |
| JComponent src = (JComponent)ev.getSource(); |
| if (src == saveAsMI) { |
| saveAs(); |
| } else { |
| int index = timeRangeMenu.getPopupMenu().getComponentIndex(src); |
| setViewRange(rangeValues[index]); |
| } |
| } |
| |
| private void saveAs() { |
| if (saveFC == null) { |
| saveFC = new SaveDataFileChooser(); |
| } |
| int ret = saveFC.showSaveDialog(this); |
| if (ret == JFileChooser.APPROVE_OPTION) { |
| saveDataToFile(saveFC.getSelectedFile()); |
| } |
| } |
| |
| private void saveDataToFile(File file) { |
| try { |
| PrintStream out = new PrintStream(new FileOutputStream(file)); |
| |
| // Print header line |
| out.print("Time"); |
| for (Sequence seq : seqs) { |
| out.print(","+seq.name); |
| } |
| out.println(); |
| |
| // Print data lines |
| if (seqs.size() > 0 && seqs.get(0).size > 0) { |
| for (int i = 0; i < seqs.get(0).size; i++) { |
| double excelTime = toExcelTime(times.time(i)); |
| out.print(String.format(Locale.ENGLISH, "%.6f", excelTime)); |
| for (Sequence seq : seqs) { |
| out.print("," + getFormattedValue(seq.value(i), false)); |
| } |
| out.println(); |
| } |
| } |
| |
| out.close(); |
| JOptionPane.showMessageDialog(this, |
| Resources.format(Messages.FILE_CHOOSER_SAVED_FILE, |
| file.getAbsolutePath(), |
| file.length())); |
| } catch (IOException ex) { |
| String msg = ex.getLocalizedMessage(); |
| String path = file.getAbsolutePath(); |
| if (msg.startsWith(path)) { |
| msg = msg.substring(path.length()).trim(); |
| } |
| JOptionPane.showMessageDialog(this, |
| Resources.format(Messages.FILE_CHOOSER_SAVE_FAILED_MESSAGE, |
| path, |
| msg), |
| Messages.FILE_CHOOSER_SAVE_FAILED_TITLE, |
| JOptionPane.ERROR_MESSAGE); |
| } |
| } |
| |
| @Override |
| public void paintComponent(Graphics g) { |
| super.paintComponent(g); |
| |
| int width = getWidth()-rightMargin-leftMargin-10; |
| int height = getHeight()-topMargin-bottomMargin; |
| if (width <= 0 || height <= 0) { |
| // not enough room to paint anything |
| return; |
| } |
| |
| Color oldColor = g.getColor(); |
| Font oldFont = g.getFont(); |
| Color fg = getForeground(); |
| Color bg = getBackground(); |
| boolean bgIsLight = (bg.getRed() > 200 && |
| bg.getGreen() > 200 && |
| bg.getBlue() > 200); |
| |
| |
| ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, |
| RenderingHints.VALUE_ANTIALIAS_ON); |
| |
| if (smallFont == null) { |
| smallFont = oldFont.deriveFont(9.0F); |
| } |
| |
| r.x = leftMargin - 5; |
| r.y = topMargin - 8; |
| r.width = getWidth()-leftMargin-rightMargin; |
| r.height = getHeight()-topMargin-bottomMargin+16; |
| |
| if (border == null) { |
| // By setting colors here, we avoid recalculating them |
| // over and over. |
| border = new BevelBorder(BevelBorder.LOWERED, |
| getBackground().brighter().brighter(), |
| getBackground().brighter(), |
| getBackground().darker().darker(), |
| getBackground().darker()); |
| } |
| |
| border.paintBorder(this, g, r.x, r.y, r.width, r.height); |
| |
| // Fill background color |
| g.setColor(bgColor); |
| g.fillRect(r.x+2, r.y+2, r.width-4, r.height-4); |
| g.setColor(oldColor); |
| |
| long tMin = Long.MAX_VALUE; |
| long tMax = Long.MIN_VALUE; |
| long vMin = Long.MAX_VALUE; |
| long vMax = 1; |
| |
| int w = getWidth()-rightMargin-leftMargin-10; |
| int h = getHeight()-topMargin-bottomMargin; |
| |
| if (times.size > 1) { |
| tMin = Math.min(tMin, times.time(0)); |
| tMax = Math.max(tMax, times.time(times.size-1)); |
| } |
| long viewRangeMS; |
| if (viewRange > 0) { |
| viewRangeMS = viewRange * MINUTE; |
| } else { |
| // Display full time range, but no less than a minute |
| viewRangeMS = Math.max(tMax - tMin, 1 * MINUTE); |
| } |
| |
| // Calculate min/max values |
| for (Sequence seq : seqs) { |
| if (seq.size > 0) { |
| for (int i = 0; i < seq.size; i++) { |
| if (seq.size == 1 || times.time(i) >= tMax - viewRangeMS) { |
| long val = seq.value(i); |
| if (val > Long.MIN_VALUE) { |
| vMax = Math.max(vMax, val); |
| vMin = Math.min(vMin, val); |
| } |
| } |
| } |
| } else { |
| vMin = 0L; |
| } |
| if (unit == Unit.BYTES || !seq.isPlotted) { |
| // We'll scale only to the first (main) value set. |
| // TODO: Use a separate property for this. |
| break; |
| } |
| } |
| |
| // Normalize scale |
| vMax = normalizeMax(vMax); |
| if (vMin > 0) { |
| if (vMax / vMin > 4) { |
| vMin = 0; |
| } else { |
| vMin = normalizeMin(vMin); |
| } |
| } |
| |
| |
| g.setColor(fg); |
| |
| // Axes |
| // Draw vertical axis |
| int x = leftMargin - 18; |
| int y = topMargin; |
| FontMetrics fm = g.getFontMetrics(); |
| |
| g.drawLine(x, y, x, y+h); |
| |
| int n = 5; |
| if ((""+vMax).startsWith("2")) { |
| n = 4; |
| } else if ((""+vMax).startsWith("3")) { |
| n = 6; |
| } else if ((""+vMax).startsWith("4")) { |
| n = 4; |
| } else if ((""+vMax).startsWith("6")) { |
| n = 6; |
| } else if ((""+vMax).startsWith("7")) { |
| n = 7; |
| } else if ((""+vMax).startsWith("8")) { |
| n = 8; |
| } else if ((""+vMax).startsWith("9")) { |
| n = 3; |
| } |
| |
| // Ticks |
| ArrayList<Long> tickValues = new ArrayList<Long>(); |
| tickValues.add(vMin); |
| for (int i = 0; i < n; i++) { |
| long v = i * vMax / n; |
| if (v > vMin) { |
| tickValues.add(v); |
| } |
| } |
| tickValues.add(vMax); |
| n = tickValues.size(); |
| |
| String[] tickStrings = new String[n]; |
| for (int i = 0; i < n; i++) { |
| long v = tickValues.get(i); |
| tickStrings[i] = getSizeString(v, vMax); |
| } |
| |
| // Trim trailing decimal zeroes. |
| if (decimals > 0) { |
| boolean trimLast = true; |
| boolean removedDecimalPoint = false; |
| do { |
| for (String str : tickStrings) { |
| if (!(str.endsWith("0") || str.endsWith("."))) { |
| trimLast = false; |
| break; |
| } |
| } |
| if (trimLast) { |
| if (tickStrings[0].endsWith(".")) { |
| removedDecimalPoint = true; |
| } |
| for (int i = 0; i < n; i++) { |
| String str = tickStrings[i]; |
| tickStrings[i] = str.substring(0, str.length()-1); |
| } |
| } |
| } while (trimLast && !removedDecimalPoint); |
| } |
| |
| // Draw ticks |
| int lastY = Integer.MAX_VALUE; |
| for (int i = 0; i < n; i++) { |
| long v = tickValues.get(i); |
| y = topMargin+h-(int)(h * (v-vMin) / (vMax-vMin)); |
| g.drawLine(x-2, y, x+2, y); |
| String s = tickStrings[i]; |
| if (unit == Unit.PERCENT) { |
| s += "%"; |
| } |
| int sx = x-6-fm.stringWidth(s); |
| if (y < lastY-13) { |
| if (checkLeftMargin(sx)) { |
| // Wait for next repaint |
| return; |
| } |
| g.drawString(s, sx, y+4); |
| } |
| // Draw horizontal grid line |
| g.setColor(Color.lightGray); |
| g.drawLine(r.x + 4, y, r.x + r.width - 4, y); |
| g.setColor(fg); |
| lastY = y; |
| } |
| |
| // Draw horizontal axis |
| x = leftMargin; |
| y = topMargin + h + 15; |
| g.drawLine(x, y, x+w, y); |
| |
| long t1 = tMax; |
| if (t1 <= 0L) { |
| // No data yet, so draw current time |
| t1 = System.currentTimeMillis(); |
| } |
| long tz = timeDF.getTimeZone().getOffset(t1); |
| long tickInterval = calculateTickInterval(w, 40, viewRangeMS); |
| if (tickInterval > 3 * HOUR) { |
| tickInterval = calculateTickInterval(w, 80, viewRangeMS); |
| } |
| long t0 = tickInterval - (t1 - viewRangeMS + tz) % tickInterval; |
| while (t0 < viewRangeMS) { |
| x = leftMargin + (int)(w * t0 / viewRangeMS); |
| g.drawLine(x, y-2, x, y+2); |
| |
| long t = t1 - viewRangeMS + t0; |
| String str = formatClockTime(t); |
| g.drawString(str, x, y+16); |
| //if (tickInterval > (1 * HOUR) && t % (1 * DAY) == 0) { |
| if ((t + tz) % (1 * DAY) == 0) { |
| str = formatDate(t); |
| g.drawString(str, x, y+27); |
| } |
| // Draw vertical grid line |
| g.setColor(Color.lightGray); |
| g.drawLine(x, topMargin, x, topMargin + h); |
| g.setColor(fg); |
| t0 += tickInterval; |
| } |
| |
| // Plot values |
| int start = 0; |
| int nValues = 0; |
| int nLists = seqs.size(); |
| if (nLists > 0) { |
| nValues = seqs.get(0).size; |
| } |
| if (nValues == 0) { |
| g.setColor(oldColor); |
| return; |
| } else { |
| Sequence seq = seqs.get(0); |
| // Find starting point |
| for (int p = 0; p < seq.size; p++) { |
| if (times.time(p) >= tMax - viewRangeMS) { |
| start = p; |
| break; |
| } |
| } |
| } |
| |
| //Optimization: collapse plot of more than four values per pixel |
| int pointsPerPixel = (nValues - start) / w; |
| if (pointsPerPixel < 4) { |
| pointsPerPixel = 1; |
| } |
| |
| // Draw graphs |
| // Loop backwards over sequences because the first needs to be painted on top |
| for (int i = nLists-1; i >= 0; i--) { |
| int x0 = leftMargin; |
| int y0 = topMargin + h + 1; |
| |
| Sequence seq = seqs.get(i); |
| if (seq.isPlotted && seq.size > 0) { |
| // Paint twice, with white and with color |
| for (int pass = 0; pass < 2; pass++) { |
| g.setColor((pass == 0) ? Color.white : seq.color); |
| int x1 = -1; |
| long v1 = -1; |
| for (int p = start; p < nValues; p += pointsPerPixel) { |
| // Make sure we get the last value |
| if (pointsPerPixel > 1 && p >= nValues - pointsPerPixel) { |
| p = nValues - 1; |
| } |
| int x2 = (int)(w * (times.time(p)-(t1-viewRangeMS)) / viewRangeMS); |
| long v2 = seq.value(p); |
| if (v2 >= vMin && v2 <= vMax) { |
| int y2 = (int)(h * (v2 -vMin) / (vMax-vMin)); |
| if (x1 >= 0 && v1 >= vMin && v1 <= vMax) { |
| int y1 = (int)(h * (v1-vMin) / (vMax-vMin)); |
| |
| if (y1 == y2) { |
| // fillrect is much faster |
| g.fillRect(x0+x1, y0-y1-pass, x2-x1, 1); |
| } else { |
| Graphics2D g2d = (Graphics2D)g; |
| Stroke oldStroke = null; |
| if (seq.transitionStroke != null) { |
| oldStroke = g2d.getStroke(); |
| g2d.setStroke(seq.transitionStroke); |
| } |
| g.drawLine(x0+x1, y0-y1-pass, x0+x2, y0-y2-pass); |
| if (oldStroke != null) { |
| g2d.setStroke(oldStroke); |
| } |
| } |
| } |
| } |
| x1 = x2; |
| v1 = v2; |
| } |
| } |
| |
| // Current value |
| long v = seq.value(seq.size - 1); |
| if (v >= vMin && v <= vMax) { |
| if (bgIsLight) { |
| g.setColor(seq.color); |
| } else { |
| g.setColor(fg); |
| } |
| x = r.x + r.width + 2; |
| y = topMargin+h-(int)(h * (v-vMin) / (vMax-vMin)); |
| // a small triangle/arrow |
| g.fillPolygon(new int[] { x+2, x+6, x+6 }, |
| new int[] { y, y+3, y-3 }, |
| 3); |
| } |
| g.setColor(fg); |
| } |
| } |
| |
| int[] valueStringSlots = new int[nLists]; |
| for (int i = 0; i < nLists; i++) valueStringSlots[i] = -1; |
| for (int i = 0; i < nLists; i++) { |
| Sequence seq = seqs.get(i); |
| if (seq.isPlotted && seq.size > 0) { |
| // Draw current value |
| |
| // TODO: collapse values if pointsPerPixel >= 4 |
| |
| long v = seq.value(seq.size - 1); |
| if (v >= vMin && v <= vMax) { |
| x = r.x + r.width + 2; |
| y = topMargin+h-(int)(h * (v-vMin) / (vMax-vMin)); |
| int y2 = getValueStringSlot(valueStringSlots, y, 2*10, i); |
| g.setFont(smallFont); |
| if (bgIsLight) { |
| g.setColor(seq.color); |
| } else { |
| g.setColor(fg); |
| } |
| String curValue = getFormattedValue(v, true); |
| if (unit == Unit.PERCENT) { |
| curValue += "%"; |
| } |
| int valWidth = fm.stringWidth(curValue); |
| String legend = (displayLegend?seq.name:""); |
| int legendWidth = fm.stringWidth(legend); |
| if (checkRightMargin(valWidth) || checkRightMargin(legendWidth)) { |
| // Wait for next repaint |
| return; |
| } |
| g.drawString(legend , x + 17, Math.min(topMargin+h, y2 + 3 - 10)); |
| g.drawString(curValue, x + 17, Math.min(topMargin+h + 10, y2 + 3)); |
| |
| // Maybe draw a short line to value |
| if (y2 > y + 3) { |
| g.drawLine(x + 9, y + 2, x + 14, y2); |
| } else if (y2 < y - 3) { |
| g.drawLine(x + 9, y - 2, x + 14, y2); |
| } |
| } |
| g.setFont(oldFont); |
| g.setColor(fg); |
| |
| } |
| } |
| g.setColor(oldColor); |
| } |
| |
| private boolean checkLeftMargin(int x) { |
| // Make sure leftMargin has at least 2 pixels over |
| if (x < 2) { |
| leftMargin += (2 - x); |
| // Repaint from top (above any cell renderers) |
| SwingUtilities.getWindowAncestor(this).repaint(); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean checkRightMargin(int w) { |
| // Make sure rightMargin has at least 2 pixels over |
| if (w + 2 > rightMargin) { |
| rightMargin = (w + 2); |
| // Repaint from top (above any cell renderers) |
| SwingUtilities.getWindowAncestor(this).repaint(); |
| return true; |
| } |
| return false; |
| } |
| |
| private int getValueStringSlot(int[] slots, int y, int h, int i) { |
| for (int s = 0; s < slots.length; s++) { |
| if (slots[s] >= y && slots[s] < y + h) { |
| // collide below us |
| if (slots[s] > h) { |
| return getValueStringSlot(slots, slots[s]-h, h, i); |
| } else { |
| return getValueStringSlot(slots, slots[s]+h, h, i); |
| } |
| } else if (y >= h && slots[s] > y - h && slots[s] < y) { |
| // collide above us |
| return getValueStringSlot(slots, slots[s]+h, h, i); |
| } |
| } |
| slots[i] = y; |
| return y; |
| } |
| |
| private long calculateTickInterval(int w, int hGap, long viewRangeMS) { |
| long tickInterval = viewRangeMS * hGap / w; |
| if (tickInterval < 1 * MINUTE) { |
| tickInterval = 1 * MINUTE; |
| } else if (tickInterval < 5 * MINUTE) { |
| tickInterval = 5 * MINUTE; |
| } else if (tickInterval < 10 * MINUTE) { |
| tickInterval = 10 * MINUTE; |
| } else if (tickInterval < 30 * MINUTE) { |
| tickInterval = 30 * MINUTE; |
| } else if (tickInterval < 1 * HOUR) { |
| tickInterval = 1 * HOUR; |
| } else if (tickInterval < 3 * HOUR) { |
| tickInterval = 3 * HOUR; |
| } else if (tickInterval < 6 * HOUR) { |
| tickInterval = 6 * HOUR; |
| } else if (tickInterval < 12 * HOUR) { |
| tickInterval = 12 * HOUR; |
| } else if (tickInterval < 1 * DAY) { |
| tickInterval = 1 * DAY; |
| } else { |
| tickInterval = normalizeMax(tickInterval / DAY) * DAY; |
| } |
| return tickInterval; |
| } |
| |
| private long normalizeMin(long l) { |
| int exp = (int)Math.log10((double)l); |
| long multiple = (long)Math.pow(10.0, exp); |
| int i = (int)(l / multiple); |
| return i * multiple; |
| } |
| |
| private long normalizeMax(long l) { |
| int exp = (int)Math.log10((double)l); |
| long multiple = (long)Math.pow(10.0, exp); |
| int i = (int)(l / multiple); |
| l = (i+1)*multiple; |
| return l; |
| } |
| |
| private String getFormattedValue(long v, boolean groupDigits) { |
| String str; |
| String fmt = "%"; |
| if (groupDigits) { |
| fmt += ","; |
| } |
| if (decimals > 0) { |
| fmt += "." + decimals + "f"; |
| str = String.format(fmt, v / decimalsMultiplier); |
| } else { |
| fmt += "d"; |
| str = String.format(fmt, v); |
| } |
| return str; |
| } |
| |
| private String getSizeString(long v, long vMax) { |
| String s; |
| |
| if (unit == Unit.BYTES && decimals == 0) { |
| s = formatBytes(v, vMax); |
| } else { |
| s = getFormattedValue(v, true); |
| } |
| return s; |
| } |
| |
| private static synchronized Stroke getDashedStroke() { |
| if (dashedStroke == null) { |
| dashedStroke = new BasicStroke(1.0f, |
| BasicStroke.CAP_BUTT, |
| BasicStroke.JOIN_MITER, |
| 10.0f, |
| new float[] { 2.0f, 3.0f }, |
| 0.0f); |
| } |
| return dashedStroke; |
| } |
| |
| private static Object extendArray(Object a1) { |
| int n = Array.getLength(a1); |
| Object a2 = |
| Array.newInstance(a1.getClass().getComponentType(), |
| n + ARRAY_SIZE_INCREMENT); |
| System.arraycopy(a1, 0, a2, 0, n); |
| return a2; |
| } |
| |
| |
| private static class TimeStamps { |
| // Time stamps (long) are split into offsets (long) and a |
| // series of times from the offsets (int). A new offset is |
| // stored when the time value doesn't fit in an int |
| // (approx every 24 days). An array of indices is used to |
| // define the starting point for each offset in the times |
| // array. |
| long[] offsets = new long[0]; |
| int[] indices = new int[0]; |
| int[] rtimes = new int[ARRAY_SIZE_INCREMENT]; |
| |
| // Number of stored timestamps |
| int size = 0; |
| |
| /** |
| * Returns the time stamp for index i |
| */ |
| public long time(int i) { |
| long offset = 0; |
| for (int j = indices.length - 1; j >= 0; j--) { |
| if (i >= indices[j]) { |
| offset = offsets[j]; |
| break; |
| } |
| } |
| return offset + rtimes[i]; |
| } |
| |
| public void add(long time) { |
| // May need to store a new time offset |
| int n = offsets.length; |
| if (n == 0 || time - offsets[n - 1] > Integer.MAX_VALUE) { |
| // Grow offset and indices arrays and store new offset |
| offsets = Arrays.copyOf(offsets, n + 1); |
| offsets[n] = time; |
| indices = Arrays.copyOf(indices, n + 1); |
| indices[n] = size; |
| } |
| |
| // May need to extend the array size |
| if (rtimes.length == size) { |
| rtimes = (int[])extendArray(rtimes); |
| } |
| |
| // Store the time |
| rtimes[size] = (int)(time - offsets[offsets.length - 1]); |
| size++; |
| } |
| } |
| |
| private static class Sequence { |
| String key; |
| String name; |
| Color color; |
| boolean isPlotted; |
| Stroke transitionStroke = null; |
| |
| // Values are stored in an int[] if all values will fit, |
| // otherwise in a long[]. An int can represent up to 2 GB. |
| // Use a random start size, so all arrays won't need to |
| // be grown during the same update interval |
| Object values = |
| new byte[ARRAY_SIZE_INCREMENT + (int)(Math.random() * 100)]; |
| |
| // Number of stored values |
| int size = 0; |
| |
| public Sequence(String key) { |
| this.key = key; |
| } |
| |
| /** |
| * Returns the value at index i |
| */ |
| public long value(int i) { |
| return Array.getLong(values, i); |
| } |
| |
| public void add(long value) { |
| // May need to switch to a larger array type |
| if ((values instanceof byte[] || |
| values instanceof short[] || |
| values instanceof int[]) && |
| value > Integer.MAX_VALUE) { |
| long[] la = new long[Array.getLength(values)]; |
| for (int i = 0; i < size; i++) { |
| la[i] = Array.getLong(values, i); |
| } |
| values = la; |
| } else if ((values instanceof byte[] || |
| values instanceof short[]) && |
| value > Short.MAX_VALUE) { |
| int[] ia = new int[Array.getLength(values)]; |
| for (int i = 0; i < size; i++) { |
| ia[i] = Array.getInt(values, i); |
| } |
| values = ia; |
| } else if (values instanceof byte[] && |
| value > Byte.MAX_VALUE) { |
| short[] sa = new short[Array.getLength(values)]; |
| for (int i = 0; i < size; i++) { |
| sa[i] = Array.getShort(values, i); |
| } |
| values = sa; |
| } |
| |
| // May need to extend the array size |
| if (Array.getLength(values) == size) { |
| values = extendArray(values); |
| } |
| |
| // Store the value |
| if (values instanceof long[]) { |
| ((long[])values)[size] = value; |
| } else if (values instanceof int[]) { |
| ((int[])values)[size] = (int)value; |
| } else if (values instanceof short[]) { |
| ((short[])values)[size] = (short)value; |
| } else { |
| ((byte[])values)[size] = (byte)value; |
| } |
| size++; |
| } |
| } |
| |
| // Can be overridden by subclasses |
| long getValue() { |
| return 0; |
| } |
| |
| long getLastTimeStamp() { |
| return times.time(times.size - 1); |
| } |
| |
| long getLastValue(String key) { |
| Sequence seq = getSequence(key); |
| return (seq != null && seq.size > 0) ? seq.value(seq.size - 1) : 0L; |
| } |
| |
| |
| // Called on EDT |
| public void propertyChange(PropertyChangeEvent ev) { |
| String prop = ev.getPropertyName(); |
| |
| if (prop == JConsoleContext.CONNECTION_STATE_PROPERTY) { |
| ConnectionState newState = (ConnectionState)ev.getNewValue(); |
| |
| switch (newState) { |
| case DISCONNECTED: |
| synchronized(this) { |
| long time = System.currentTimeMillis(); |
| times.add(time); |
| for (Sequence seq : seqs) { |
| seq.add(Long.MIN_VALUE); |
| } |
| } |
| break; |
| } |
| } |
| } |
| |
| private static class SaveDataFileChooser extends JFileChooser { |
| private static final long serialVersionUID = -5182890922369369669L; |
| SaveDataFileChooser() { |
| setFileFilter(new FileNameExtensionFilter("CSV file", "csv")); |
| } |
| |
| @Override |
| public void approveSelection() { |
| File file = getSelectedFile(); |
| if (file != null) { |
| FileFilter filter = getFileFilter(); |
| if (filter != null && filter instanceof FileNameExtensionFilter) { |
| String[] extensions = |
| ((FileNameExtensionFilter)filter).getExtensions(); |
| |
| boolean goodExt = false; |
| for (String ext : extensions) { |
| if (file.getName().toLowerCase().endsWith("." + ext.toLowerCase())) { |
| goodExt = true; |
| break; |
| } |
| } |
| if (!goodExt) { |
| file = new File(file.getParent(), |
| file.getName() + "." + extensions[0]); |
| } |
| } |
| |
| if (file.exists()) { |
| String okStr = Messages.FILE_CHOOSER_FILE_EXISTS_OK_OPTION; |
| String cancelStr = Messages.FILE_CHOOSER_FILE_EXISTS_CANCEL_OPTION; |
| int ret = |
| JOptionPane.showOptionDialog(this, |
| Resources.format(Messages.FILE_CHOOSER_FILE_EXISTS_MESSAGE, |
| file.getName()), |
| Messages.FILE_CHOOSER_FILE_EXISTS_TITLE, |
| JOptionPane.OK_CANCEL_OPTION, |
| JOptionPane.WARNING_MESSAGE, |
| null, |
| new Object[] { okStr, cancelStr }, |
| okStr); |
| if (ret != JOptionPane.OK_OPTION) { |
| return; |
| } |
| } |
| setSelectedFile(file); |
| } |
| super.approveSelection(); |
| } |
| } |
| |
| @Override |
| public AccessibleContext getAccessibleContext() { |
| if (accessibleContext == null) { |
| accessibleContext = new AccessiblePlotter(); |
| } |
| return accessibleContext; |
| } |
| |
| protected class AccessiblePlotter extends AccessibleJComponent { |
| private static final long serialVersionUID = -3847205410473510922L; |
| protected AccessiblePlotter() { |
| setAccessibleName(Messages.PLOTTER_ACCESSIBLE_NAME); |
| } |
| |
| @Override |
| public String getAccessibleName() { |
| String name = super.getAccessibleName(); |
| |
| if (seqs.size() > 0 && seqs.get(0).size > 0) { |
| String keyValueList = ""; |
| for (Sequence seq : seqs) { |
| if (seq.isPlotted) { |
| String value = "null"; |
| if (seq.size > 0) { |
| if (unit == Unit.BYTES) { |
| value = Resources.format(Messages.SIZE_BYTES, seq.value(seq.size - 1)); |
| } else { |
| value = |
| getFormattedValue(seq.value(seq.size - 1), false) + |
| ((unit == Unit.PERCENT) ? "%" : ""); |
| } |
| } |
| // Assume format string ends with newline |
| keyValueList += |
| Resources.format(Messages.PLOTTER_ACCESSIBLE_NAME_KEY_AND_VALUE, |
| seq.key, value); |
| } |
| } |
| name += "\n" + keyValueList + "."; |
| } else { |
| name += "\n" + Messages.PLOTTER_ACCESSIBLE_NAME_NO_DATA; |
| } |
| return name; |
| } |
| |
| @Override |
| public AccessibleRole getAccessibleRole() { |
| return AccessibleRole.CANVAS; |
| } |
| } |
| } |