diff -r 836adbf7a2cd -r 3317bb8137f4 jdk/src/jdk.jconsole/share/classes/sun/tools/jconsole/Plotter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jdk/src/jdk.jconsole/share/classes/sun/tools/jconsole/Plotter.java Sun Aug 17 15:54:13 2014 +0100 @@ -0,0 +1,1100 @@ +/* + * 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 seqs = new ArrayList(); + 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 tickValues = new ArrayList(); + 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 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; + } + } +}