0001: /*
0002: * Copyright 2004-2006 Sun Microsystems, Inc. All Rights Reserved.
0003: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
0004: *
0005: * This code is free software; you can redistribute it and/or modify it
0006: * under the terms of the GNU General Public License version 2 only, as
0007: * published by the Free Software Foundation. Sun designates this
0008: * particular file as subject to the "Classpath" exception as provided
0009: * by Sun in the LICENSE file that accompanied this code.
0010: *
0011: * This code is distributed in the hope that it will be useful, but WITHOUT
0012: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
0013: * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
0014: * version 2 for more details (a copy is included in the LICENSE file that
0015: * accompanied this code).
0016: *
0017: * You should have received a copy of the GNU General Public License version
0018: * 2 along with this work; if not, write to the Free Software Foundation,
0019: * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
0020: *
0021: * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
0022: * CA 95054 USA or visit www.sun.com if you need additional information or
0023: * have any questions.
0024: */
0025:
0026: package sun.tools.jconsole;
0027:
0028: import java.awt.*;
0029: import java.awt.event.*;
0030: import java.beans.*;
0031: import java.io.*;
0032: import java.lang.reflect.Array;
0033: import java.text.*;
0034: import java.util.*;
0035:
0036: import javax.accessibility.*;
0037: import javax.swing.*;
0038: import javax.swing.border.*;
0039: import javax.swing.event.*;
0040: import javax.swing.filechooser.*;
0041: import javax.swing.filechooser.FileFilter;
0042:
0043: import com.sun.tools.jconsole.JConsoleContext;
0044: import com.sun.tools.jconsole.JConsoleContext.ConnectionState;
0045:
0046: import static com.sun.tools.jconsole.JConsoleContext.ConnectionState.*;
0047:
0048: import static sun.tools.jconsole.Formatter.*;
0049: import static sun.tools.jconsole.ProxyClient.*;
0050: import static sun.tools.jconsole.Resources.*;
0051: import static sun.tools.jconsole.Utilities.*;
0052:
0053: @SuppressWarnings("serial")
0054: public class Plotter extends JComponent implements Accessible,
0055: ActionListener, PropertyChangeListener {
0056:
0057: public static enum Unit {
0058: NONE, BYTES, PERCENT
0059: }
0060:
0061: static final String[] rangeNames = { Resources.getText(" 1 min"),
0062: Resources.getText(" 5 min"), Resources.getText("10 min"),
0063: Resources.getText("30 min"), Resources.getText(" 1 hour"),
0064: Resources.getText(" 2 hours"),
0065: Resources.getText(" 3 hours"),
0066: Resources.getText(" 6 hours"),
0067: Resources.getText("12 hours"), Resources.getText(" 1 day"),
0068: Resources.getText(" 7 days"),
0069: Resources.getText(" 1 month"),
0070: Resources.getText(" 3 months"),
0071: Resources.getText(" 6 months"),
0072: Resources.getText(" 1 year"), Resources.getText("All") };
0073:
0074: static final int[] rangeValues = { 1, 5, 10, 30, 1 * 60, 2 * 60,
0075: 3 * 60, 6 * 60, 12 * 60, 1 * 24 * 60, 7 * 24 * 60,
0076: 1 * 31 * 24 * 60, 3 * 31 * 24 * 60, 6 * 31 * 24 * 60,
0077: 366 * 24 * 60, -1 };
0078:
0079: final static long SECOND = 1000;
0080: final static long MINUTE = 60 * SECOND;
0081: final static long HOUR = 60 * MINUTE;
0082: final static long DAY = 24 * HOUR;
0083:
0084: final static Color bgColor = new Color(250, 250, 250);
0085: final static Color defaultColor = Color.blue.darker();
0086:
0087: final static int ARRAY_SIZE_INCREMENT = 4000;
0088:
0089: private static Stroke dashedStroke;
0090:
0091: private TimeStamps times = new TimeStamps();
0092: private ArrayList<Sequence> seqs = new ArrayList<Sequence>();
0093: private JPopupMenu popupMenu;
0094: private JMenu timeRangeMenu;
0095: private JRadioButtonMenuItem[] menuRBs;
0096: private JMenuItem saveAsMI;
0097: private JFileChooser saveFC;
0098:
0099: private int viewRange = -1; // Minutes (value <= 0 means full range)
0100: private Unit unit;
0101: private int decimals;
0102: private double decimalsMultiplier;
0103: private Border border = null;
0104: private Rectangle r = new Rectangle(1, 1, 1, 1);
0105: private Font smallFont = null;
0106:
0107: // Initial margins, may be recalculated as needed
0108: private int topMargin = 10;
0109: private int bottomMargin = 45;
0110: private int leftMargin = 65;
0111: private int rightMargin = 70;
0112:
0113: public Plotter() {
0114: this (Unit.NONE, 0);
0115: }
0116:
0117: public Plotter(Unit unit) {
0118: this (unit, 0);
0119: }
0120:
0121: // Note: If decimals > 0 then values must be decimally shifted left
0122: // that many places, i.e. multiplied by Math.pow(10.0, decimals).
0123: public Plotter(Unit unit, int decimals) {
0124: setUnit(unit);
0125: setDecimals(decimals);
0126:
0127: enableEvents(AWTEvent.MOUSE_EVENT_MASK);
0128:
0129: addMouseListener(new MouseAdapter() {
0130: public void mousePressed(MouseEvent e) {
0131: if (getParent() instanceof PlotterPanel) {
0132: getParent().requestFocusInWindow();
0133: }
0134: }
0135: });
0136:
0137: }
0138:
0139: public void setUnit(Unit unit) {
0140: this .unit = unit;
0141: }
0142:
0143: public void setDecimals(int decimals) {
0144: this .decimals = decimals;
0145: this .decimalsMultiplier = Math.pow(10.0, decimals);
0146: }
0147:
0148: public void createSequence(String key, String name, Color color,
0149: boolean isPlotted) {
0150: Sequence seq = getSequence(key);
0151: if (seq == null) {
0152: seq = new Sequence(key);
0153: }
0154: seq.name = name;
0155: seq.color = (color != null) ? color : defaultColor;
0156: seq.isPlotted = isPlotted;
0157:
0158: seqs.add(seq);
0159: }
0160:
0161: public void setUseDashedTransitions(String key, boolean b) {
0162: Sequence seq = getSequence(key);
0163: if (seq != null) {
0164: seq.transitionStroke = b ? getDashedStroke() : null;
0165: }
0166: }
0167:
0168: public void setIsPlotted(String key, boolean isPlotted) {
0169: Sequence seq = getSequence(key);
0170: if (seq != null) {
0171: seq.isPlotted = isPlotted;
0172: }
0173: }
0174:
0175: // Note: If decimals > 0 then values must be decimally shifted left
0176: // that many places, i.e. multiplied by Math.pow(10.0, decimals).
0177: public synchronized void addValues(long time, long... values) {
0178: assert (values.length == seqs.size());
0179: times.add(time);
0180: for (int i = 0; i < values.length; i++) {
0181: seqs.get(i).add(values[i]);
0182: }
0183: repaint();
0184: }
0185:
0186: private Sequence getSequence(String key) {
0187: for (Sequence seq : seqs) {
0188: if (seq.key.equals(key)) {
0189: return seq;
0190: }
0191: }
0192: return null;
0193: }
0194:
0195: /**
0196: * @return the displayed time range in minutes, or -1 for all data
0197: */
0198: public int getViewRange() {
0199: return viewRange;
0200: }
0201:
0202: /**
0203: * @param minutes the displayed time range in minutes, or -1 to diaplay all data
0204: */
0205: public void setViewRange(int minutes) {
0206: if (minutes != viewRange) {
0207: int oldValue = viewRange;
0208: viewRange = minutes;
0209: /* Do not i18n this string */
0210: firePropertyChange("viewRange", oldValue, viewRange);
0211: if (popupMenu != null) {
0212: for (int i = 0; i < menuRBs.length; i++) {
0213: if (rangeValues[i] == viewRange) {
0214: menuRBs[i].setSelected(true);
0215: break;
0216: }
0217: }
0218: }
0219: repaint();
0220: }
0221: }
0222:
0223: public JPopupMenu getComponentPopupMenu() {
0224: if (popupMenu == null) {
0225: popupMenu = new JPopupMenu(Resources.getText("Chart:"));
0226: timeRangeMenu = new JMenu(Resources
0227: .getText("Plotter.timeRangeMenu"));
0228: timeRangeMenu
0229: .setMnemonic(getMnemonicInt("Plotter.timeRangeMenu"));
0230: popupMenu.add(timeRangeMenu);
0231: menuRBs = new JRadioButtonMenuItem[rangeNames.length];
0232: ButtonGroup rbGroup = new ButtonGroup();
0233: for (int i = 0; i < rangeNames.length; i++) {
0234: menuRBs[i] = new JRadioButtonMenuItem(rangeNames[i]);
0235: rbGroup.add(menuRBs[i]);
0236: menuRBs[i].addActionListener(this );
0237: if (viewRange == rangeValues[i]) {
0238: menuRBs[i].setSelected(true);
0239: }
0240: timeRangeMenu.add(menuRBs[i]);
0241: }
0242:
0243: popupMenu.addSeparator();
0244:
0245: saveAsMI = new JMenuItem(getText("Plotter.saveAsMenuItem"));
0246: saveAsMI
0247: .setMnemonic(getMnemonicInt("Plotter.saveAsMenuItem"));
0248: saveAsMI.addActionListener(this );
0249: popupMenu.add(saveAsMI);
0250: }
0251: return popupMenu;
0252: }
0253:
0254: public void actionPerformed(ActionEvent ev) {
0255: JComponent src = (JComponent) ev.getSource();
0256: if (src == saveAsMI) {
0257: saveAs();
0258: } else {
0259: int index = timeRangeMenu.getPopupMenu().getComponentIndex(
0260: src);
0261: setViewRange(rangeValues[index]);
0262: }
0263: }
0264:
0265: private void saveAs() {
0266: if (saveFC == null) {
0267: saveFC = new SaveDataFileChooser();
0268: }
0269: int ret = saveFC.showSaveDialog(this );
0270: if (ret == JFileChooser.APPROVE_OPTION) {
0271: saveDataToFile(saveFC.getSelectedFile());
0272: }
0273: }
0274:
0275: private void saveDataToFile(File file) {
0276: try {
0277: PrintStream out = new PrintStream(
0278: new FileOutputStream(file));
0279:
0280: // Print header line
0281: out.print("Time");
0282: for (Sequence seq : seqs) {
0283: out.print("," + seq.name);
0284: }
0285: out.println();
0286:
0287: // Print data lines
0288: if (seqs.size() > 0 && seqs.get(0).size > 0) {
0289: for (int i = 0; i < seqs.get(0).size; i++) {
0290: double excelTime = toExcelTime(times.time(i));
0291: out.print(String.format(Locale.ENGLISH, "%.6f",
0292: excelTime));
0293: for (Sequence seq : seqs) {
0294: out
0295: .print(","
0296: + getFormattedValue(seq
0297: .value(i), false));
0298: }
0299: out.println();
0300: }
0301: }
0302:
0303: out.close();
0304: JOptionPane.showMessageDialog(this , getText(
0305: "FileChooser.savedFile", file.getAbsolutePath(),
0306: file.length()));
0307: } catch (IOException ex) {
0308: String msg = ex.getLocalizedMessage();
0309: String path = file.getAbsolutePath();
0310: if (msg.startsWith(path)) {
0311: msg = msg.substring(path.length()).trim();
0312: }
0313: JOptionPane.showMessageDialog(this , getText(
0314: "FileChooser.saveFailed.message", path, msg),
0315: getText("FileChooser.saveFailed.title"),
0316: JOptionPane.ERROR_MESSAGE);
0317: }
0318: }
0319:
0320: public void paintComponent(Graphics g) {
0321: super .paintComponent(g);
0322:
0323: Color oldColor = g.getColor();
0324: Font oldFont = g.getFont();
0325: Color fg = getForeground();
0326: Color bg = getBackground();
0327: boolean bgIsLight = (bg.getRed() > 200 && bg.getGreen() > 200 && bg
0328: .getBlue() > 200);
0329:
0330: ((Graphics2D) g).setRenderingHint(
0331: RenderingHints.KEY_ANTIALIASING,
0332: RenderingHints.VALUE_ANTIALIAS_ON);
0333:
0334: if (smallFont == null) {
0335: smallFont = oldFont.deriveFont(9.0F);
0336: }
0337:
0338: r.x = leftMargin - 5;
0339: r.y = topMargin - 8;
0340: r.width = getWidth() - leftMargin - rightMargin;
0341: r.height = getHeight() - topMargin - bottomMargin + 16;
0342:
0343: if (border == null) {
0344: // By setting colors here, we avoid recalculating them
0345: // over and over.
0346: border = new BevelBorder(BevelBorder.LOWERED,
0347: getBackground().brighter().brighter(),
0348: getBackground().brighter(), getBackground()
0349: .darker().darker(), getBackground()
0350: .darker());
0351: }
0352:
0353: border.paintBorder(this , g, r.x, r.y, r.width, r.height);
0354:
0355: // Fill background color
0356: g.setColor(bgColor);
0357: g.fillRect(r.x + 2, r.y + 2, r.width - 4, r.height - 4);
0358: g.setColor(oldColor);
0359:
0360: long tMin = Long.MAX_VALUE;
0361: long tMax = Long.MIN_VALUE;
0362: long vMin = Long.MAX_VALUE;
0363: long vMax = 1;
0364:
0365: int w = getWidth() - rightMargin - leftMargin - 10;
0366: int h = getHeight() - topMargin - bottomMargin;
0367:
0368: if (times.size > 1) {
0369: tMin = Math.min(tMin, times.time(0));
0370: tMax = Math.max(tMax, times.time(times.size - 1));
0371: }
0372: long viewRangeMS;
0373: if (viewRange > 0) {
0374: viewRangeMS = viewRange * MINUTE;
0375: } else {
0376: // Display full time range, but no less than a minute
0377: viewRangeMS = Math.max(tMax - tMin, 1 * MINUTE);
0378: }
0379:
0380: // Calculate min/max values
0381: for (Sequence seq : seqs) {
0382: if (seq.size > 0) {
0383: for (int i = 0; i < seq.size; i++) {
0384: if (seq.size == 1
0385: || times.time(i) >= tMax - viewRangeMS) {
0386: long val = seq.value(i);
0387: if (val > Long.MIN_VALUE) {
0388: vMax = Math.max(vMax, val);
0389: vMin = Math.min(vMin, val);
0390: }
0391: }
0392: }
0393: } else {
0394: vMin = 0L;
0395: }
0396: if (unit == Unit.BYTES || !seq.isPlotted) {
0397: // We'll scale only to the first (main) value set.
0398: // TODO: Use a separate property for this.
0399: break;
0400: }
0401: }
0402:
0403: // Normalize scale
0404: vMax = normalizeMax(vMax);
0405: if (vMin > 0) {
0406: if (vMax / vMin > 4) {
0407: vMin = 0;
0408: } else {
0409: vMin = normalizeMin(vMin);
0410: }
0411: }
0412:
0413: g.setColor(fg);
0414:
0415: // Axes
0416: // Draw vertical axis
0417: int x = leftMargin - 18;
0418: int y = topMargin;
0419: FontMetrics fm = g.getFontMetrics();
0420:
0421: g.drawLine(x, y, x, y + h);
0422:
0423: int n = 5;
0424: if (("" + vMax).startsWith("2")) {
0425: n = 4;
0426: } else if (("" + vMax).startsWith("3")) {
0427: n = 6;
0428: } else if (("" + vMax).startsWith("4")) {
0429: n = 4;
0430: } else if (("" + vMax).startsWith("6")) {
0431: n = 6;
0432: } else if (("" + vMax).startsWith("7")) {
0433: n = 7;
0434: } else if (("" + vMax).startsWith("8")) {
0435: n = 8;
0436: } else if (("" + vMax).startsWith("9")) {
0437: n = 3;
0438: }
0439:
0440: // Ticks
0441: ArrayList<Long> tickValues = new ArrayList<Long>();
0442: tickValues.add(vMin);
0443: for (int i = 0; i < n; i++) {
0444: long v = i * vMax / n;
0445: if (v > vMin) {
0446: tickValues.add(v);
0447: }
0448: }
0449: tickValues.add(vMax);
0450: n = tickValues.size();
0451:
0452: String[] tickStrings = new String[n];
0453: for (int i = 0; i < n; i++) {
0454: long v = tickValues.get(i);
0455: tickStrings[i] = getSizeString(v, vMax);
0456: }
0457:
0458: // Trim trailing decimal zeroes.
0459: if (decimals > 0) {
0460: boolean trimLast = true;
0461: boolean removedDecimalPoint = false;
0462: do {
0463: for (String str : tickStrings) {
0464: if (!(str.endsWith("0") || str.endsWith("."))) {
0465: trimLast = false;
0466: break;
0467: }
0468: }
0469: if (trimLast) {
0470: if (tickStrings[0].endsWith(".")) {
0471: removedDecimalPoint = true;
0472: }
0473: for (int i = 0; i < n; i++) {
0474: String str = tickStrings[i];
0475: tickStrings[i] = str.substring(0,
0476: str.length() - 1);
0477: }
0478: }
0479: } while (trimLast && !removedDecimalPoint);
0480: }
0481:
0482: // Draw ticks
0483: int lastY = Integer.MAX_VALUE;
0484: for (int i = 0; i < n; i++) {
0485: long v = tickValues.get(i);
0486: y = topMargin + h - (int) (h * (v - vMin) / (vMax - vMin));
0487: g.drawLine(x - 2, y, x + 2, y);
0488: String s = tickStrings[i];
0489: if (unit == Unit.PERCENT) {
0490: s += "%";
0491: }
0492: int sx = x - 6 - fm.stringWidth(s);
0493: if (y < lastY - 13) {
0494: if (checkLeftMargin(sx)) {
0495: // Wait for next repaint
0496: return;
0497: }
0498: g.drawString(s, sx, y + 4);
0499: }
0500: // Draw horizontal grid line
0501: g.setColor(Color.lightGray);
0502: g.drawLine(r.x + 4, y, r.x + r.width - 4, y);
0503: g.setColor(fg);
0504: lastY = y;
0505: }
0506:
0507: // Draw horizontal axis
0508: x = leftMargin;
0509: y = topMargin + h + 15;
0510: g.drawLine(x, y, x + w, y);
0511:
0512: long t1 = tMax;
0513: if (t1 <= 0L) {
0514: // No data yet, so draw current time
0515: t1 = System.currentTimeMillis();
0516: }
0517: long tz = timeDF.getTimeZone().getOffset(t1);
0518: long tickInterval = calculateTickInterval(w, 40, viewRangeMS);
0519: if (tickInterval > 3 * HOUR) {
0520: tickInterval = calculateTickInterval(w, 80, viewRangeMS);
0521: }
0522: long t0 = tickInterval - (t1 - viewRangeMS + tz) % tickInterval;
0523: while (t0 < viewRangeMS) {
0524: x = leftMargin + (int) (w * t0 / viewRangeMS);
0525: g.drawLine(x, y - 2, x, y + 2);
0526:
0527: long t = t1 - viewRangeMS + t0;
0528: String str = formatClockTime(t);
0529: g.drawString(str, x, y + 16);
0530: //if (tickInterval > (1 * HOUR) && t % (1 * DAY) == 0) {
0531: if ((t + tz) % (1 * DAY) == 0) {
0532: str = formatDate(t);
0533: g.drawString(str, x, y + 27);
0534: }
0535: // Draw vertical grid line
0536: g.setColor(Color.lightGray);
0537: g.drawLine(x, topMargin, x, topMargin + h);
0538: g.setColor(fg);
0539: t0 += tickInterval;
0540: }
0541:
0542: // Plot values
0543: int start = 0;
0544: int nValues = 0;
0545: int nLists = seqs.size();
0546: if (nLists > 0) {
0547: nValues = seqs.get(0).size;
0548: }
0549: if (nValues == 0) {
0550: g.setColor(oldColor);
0551: return;
0552: } else {
0553: Sequence seq = seqs.get(0);
0554: // Find starting point
0555: for (int p = 0; p < seq.size; p++) {
0556: if (times.time(p) >= tMax - viewRangeMS) {
0557: start = p;
0558: break;
0559: }
0560: }
0561: }
0562:
0563: //Optimization: collapse plot of more than four values per pixel
0564: int pointsPerPixel = (nValues - start) / w;
0565: if (pointsPerPixel < 4) {
0566: pointsPerPixel = 1;
0567: }
0568:
0569: // Draw graphs
0570: // Loop backwards over sequences because the first needs to be painted on top
0571: for (int i = nLists - 1; i >= 0; i--) {
0572: int x0 = leftMargin;
0573: int y0 = topMargin + h + 1;
0574:
0575: Sequence seq = seqs.get(i);
0576: if (seq.isPlotted && seq.size > 0) {
0577: // Paint twice, with white and with color
0578: for (int pass = 0; pass < 2; pass++) {
0579: g.setColor((pass == 0) ? Color.white : seq.color);
0580: int x1 = -1;
0581: long v1 = -1;
0582: for (int p = start; p < nValues; p += pointsPerPixel) {
0583: // Make sure we get the last value
0584: if (pointsPerPixel > 1
0585: && p >= nValues - pointsPerPixel) {
0586: p = nValues - 1;
0587: }
0588: int x2 = (int) (w
0589: * (times.time(p) - (t1 - viewRangeMS)) / viewRangeMS);
0590: long v2 = seq.value(p);
0591: if (v2 >= vMin && v2 <= vMax) {
0592: int y2 = (int) (h * (v2 - vMin) / (vMax - vMin));
0593: if (x1 >= 0 && v1 >= vMin && v1 <= vMax) {
0594: int y1 = (int) (h * (v1 - vMin) / (vMax - vMin));
0595:
0596: if (y1 == y2) {
0597: // fillrect is much faster
0598: g.fillRect(x0 + x1, y0 - y1 - pass,
0599: x2 - x1, 1);
0600: } else {
0601: Graphics2D g2d = (Graphics2D) g;
0602: Stroke oldStroke = null;
0603: if (seq.transitionStroke != null) {
0604: oldStroke = g2d.getStroke();
0605: g2d
0606: .setStroke(seq.transitionStroke);
0607: }
0608: g.drawLine(x0 + x1, y0 - y1 - pass,
0609: x0 + x2, y0 - y2 - pass);
0610: if (oldStroke != null) {
0611: g2d.setStroke(oldStroke);
0612: }
0613: }
0614: }
0615: }
0616: x1 = x2;
0617: v1 = v2;
0618: }
0619: }
0620:
0621: // Current value
0622: long v = seq.value(seq.size - 1);
0623: if (v >= vMin && v <= vMax) {
0624: if (bgIsLight) {
0625: g.setColor(seq.color);
0626: } else {
0627: g.setColor(fg);
0628: }
0629: x = r.x + r.width + 2;
0630: y = topMargin + h
0631: - (int) (h * (v - vMin) / (vMax - vMin));
0632: // a small triangle/arrow
0633: g.fillPolygon(new int[] { x + 2, x + 6, x + 6 },
0634: new int[] { y, y + 3, y - 3 }, 3);
0635: }
0636: g.setColor(fg);
0637: }
0638: }
0639:
0640: int[] valueStringSlots = new int[nLists];
0641: for (int i = 0; i < nLists; i++)
0642: valueStringSlots[i] = -1;
0643: for (int i = 0; i < nLists; i++) {
0644: Sequence seq = seqs.get(i);
0645: if (seq.isPlotted && seq.size > 0) {
0646: // Draw current value
0647:
0648: // TODO: collapse values if pointsPerPixel >= 4
0649:
0650: long v = seq.value(seq.size - 1);
0651: if (v >= vMin && v <= vMax) {
0652: x = r.x + r.width + 2;
0653: y = topMargin + h
0654: - (int) (h * (v - vMin) / (vMax - vMin));
0655: int y2 = getValueStringSlot(valueStringSlots, y,
0656: 2 * 10, i);
0657: g.setFont(smallFont);
0658: if (bgIsLight) {
0659: g.setColor(seq.color);
0660: } else {
0661: g.setColor(fg);
0662: }
0663: String curValue = getFormattedValue(v, true);
0664: if (unit == Unit.PERCENT) {
0665: curValue += "%";
0666: }
0667: int valWidth = fm.stringWidth(curValue);
0668: String legend = seq.name;
0669: int legendWidth = fm.stringWidth(legend);
0670: if (checkRightMargin(valWidth)
0671: || checkRightMargin(legendWidth)) {
0672: // Wait for next repaint
0673: return;
0674: }
0675: g.drawString(legend, x + 17, Math.min(
0676: topMargin + h, y2 + 3 - 10));
0677: g.drawString(curValue, x + 17, Math.min(topMargin
0678: + h + 10, y2 + 3));
0679:
0680: // Maybe draw a short line to value
0681: if (y2 > y + 3) {
0682: g.drawLine(x + 9, y + 2, x + 14, y2);
0683: } else if (y2 < y - 3) {
0684: g.drawLine(x + 9, y - 2, x + 14, y2);
0685: }
0686: }
0687: g.setFont(oldFont);
0688: g.setColor(fg);
0689:
0690: }
0691: }
0692: g.setColor(oldColor);
0693: }
0694:
0695: private boolean checkLeftMargin(int x) {
0696: // Make sure leftMargin has at least 2 pixels over
0697: if (x < 2) {
0698: leftMargin += (2 - x);
0699: // Repaint from top (above any cell renderers)
0700: SwingUtilities.getWindowAncestor(this ).repaint();
0701: return true;
0702: }
0703: return false;
0704: }
0705:
0706: private boolean checkRightMargin(int w) {
0707: // Make sure rightMargin has at least 2 pixels over
0708: if (w + 2 > rightMargin) {
0709: rightMargin = (w + 2);
0710: // Repaint from top (above any cell renderers)
0711: SwingUtilities.getWindowAncestor(this ).repaint();
0712: return true;
0713: }
0714: return false;
0715: }
0716:
0717: private int getValueStringSlot(int[] slots, int y, int h, int i) {
0718: for (int s = 0; s < slots.length; s++) {
0719: if (slots[s] >= y && slots[s] < y + h) {
0720: // collide below us
0721: if (slots[s] > h) {
0722: return getValueStringSlot(slots, slots[s] - h, h, i);
0723: } else {
0724: return getValueStringSlot(slots, slots[s] + h, h, i);
0725: }
0726: } else if (y >= h && slots[s] > y - h && slots[s] < y) {
0727: // collide above us
0728: return getValueStringSlot(slots, slots[s] + h, h, i);
0729: }
0730: }
0731: slots[i] = y;
0732: return y;
0733: }
0734:
0735: private long calculateTickInterval(int w, int hGap, long viewRangeMS) {
0736: long tickInterval = viewRangeMS * hGap / w;
0737: if (tickInterval < 1 * MINUTE) {
0738: tickInterval = 1 * MINUTE;
0739: } else if (tickInterval < 5 * MINUTE) {
0740: tickInterval = 5 * MINUTE;
0741: } else if (tickInterval < 10 * MINUTE) {
0742: tickInterval = 10 * MINUTE;
0743: } else if (tickInterval < 30 * MINUTE) {
0744: tickInterval = 30 * MINUTE;
0745: } else if (tickInterval < 1 * HOUR) {
0746: tickInterval = 1 * HOUR;
0747: } else if (tickInterval < 3 * HOUR) {
0748: tickInterval = 3 * HOUR;
0749: } else if (tickInterval < 6 * HOUR) {
0750: tickInterval = 6 * HOUR;
0751: } else if (tickInterval < 12 * HOUR) {
0752: tickInterval = 12 * HOUR;
0753: } else if (tickInterval < 1 * DAY) {
0754: tickInterval = 1 * DAY;
0755: } else {
0756: tickInterval = normalizeMax(tickInterval / DAY) * DAY;
0757: }
0758: return tickInterval;
0759: }
0760:
0761: private long normalizeMin(long l) {
0762: int exp = (int) Math.log10((double) l);
0763: long multiple = (long) Math.pow(10.0, exp);
0764: int i = (int) (l / multiple);
0765: return i * multiple;
0766: }
0767:
0768: private long normalizeMax(long l) {
0769: int exp = (int) Math.log10((double) l);
0770: long multiple = (long) Math.pow(10.0, exp);
0771: int i = (int) (l / multiple);
0772: l = (i + 1) * multiple;
0773: return l;
0774: }
0775:
0776: private String getFormattedValue(long v, boolean groupDigits) {
0777: String str;
0778: String fmt = "%";
0779: if (groupDigits) {
0780: fmt += ",";
0781: }
0782: if (decimals > 0) {
0783: fmt += "." + decimals + "f";
0784: str = String.format(fmt, v / decimalsMultiplier);
0785: } else {
0786: fmt += "d";
0787: str = String.format(fmt, v);
0788: }
0789: return str;
0790: }
0791:
0792: private String getSizeString(long v, long vMax) {
0793: String s;
0794:
0795: if (unit == Unit.BYTES && decimals == 0) {
0796: s = formatBytes(v, vMax);
0797: } else {
0798: s = getFormattedValue(v, true);
0799: }
0800: return s;
0801: }
0802:
0803: private static synchronized Stroke getDashedStroke() {
0804: if (dashedStroke == null) {
0805: dashedStroke = new BasicStroke(1.0f, BasicStroke.CAP_BUTT,
0806: BasicStroke.JOIN_MITER, 10.0f, new float[] { 2.0f,
0807: 3.0f }, 0.0f);
0808: }
0809: return dashedStroke;
0810: }
0811:
0812: private static Object extendArray(Object a1) {
0813: int n = Array.getLength(a1);
0814: Object a2 = Array.newInstance(a1.getClass().getComponentType(),
0815: n + ARRAY_SIZE_INCREMENT);
0816: System.arraycopy(a1, 0, a2, 0, n);
0817: return a2;
0818: }
0819:
0820: private static class TimeStamps {
0821: // Time stamps (long) are split into offsets (long) and a
0822: // series of times from the offsets (int). A new offset is
0823: // stored when the the time value doesn't fit in an int
0824: // (approx every 24 days). An array of indices is used to
0825: // define the starting point for each offset in the times
0826: // array.
0827: long[] offsets = new long[0];
0828: int[] indices = new int[0];
0829: int[] rtimes = new int[ARRAY_SIZE_INCREMENT];
0830:
0831: // Number of stored timestamps
0832: int size = 0;
0833:
0834: /**
0835: * Returns the time stamp for index i
0836: */
0837: public long time(int i) {
0838: long offset = 0;
0839: for (int j = indices.length - 1; j >= 0; j--) {
0840: if (i >= indices[j]) {
0841: offset = offsets[j];
0842: break;
0843: }
0844: }
0845: return offset + rtimes[i];
0846: }
0847:
0848: public void add(long time) {
0849: // May need to store a new time offset
0850: int n = offsets.length;
0851: if (n == 0 || time - offsets[n - 1] > Integer.MAX_VALUE) {
0852: // Grow offset and indices arrays and store new offset
0853: offsets = Arrays.copyOf(offsets, n + 1);
0854: offsets[n] = time;
0855: indices = Arrays.copyOf(indices, n + 1);
0856: indices[n] = size;
0857: }
0858:
0859: // May need to extend the array size
0860: if (rtimes.length == size) {
0861: rtimes = (int[]) extendArray(rtimes);
0862: }
0863:
0864: // Store the time
0865: rtimes[size] = (int) (time - offsets[offsets.length - 1]);
0866: size++;
0867: }
0868: }
0869:
0870: private static class Sequence {
0871: String key;
0872: String name;
0873: Color color;
0874: boolean isPlotted;
0875: Stroke transitionStroke = null;
0876:
0877: // Values are stored in an int[] if all values will fit,
0878: // otherwise in a long[]. An int can represent up to 2 GB.
0879: // Use a random start size, so all arrays won't need to
0880: // be grown during the same update interval
0881: Object values = new byte[ARRAY_SIZE_INCREMENT
0882: + (int) (Math.random() * 100)];
0883:
0884: // Number of stored values
0885: int size = 0;
0886:
0887: public Sequence(String key) {
0888: this .key = key;
0889: }
0890:
0891: /**
0892: * Returns the value at index i
0893: */
0894: public long value(int i) {
0895: return Array.getLong(values, i);
0896: }
0897:
0898: public void add(long value) {
0899: // May need to switch to a larger array type
0900: if ((values instanceof byte[] || values instanceof short[] || values instanceof int[])
0901: && value > Integer.MAX_VALUE) {
0902: long[] la = new long[Array.getLength(values)];
0903: for (int i = 0; i < size; i++) {
0904: la[i] = Array.getLong(values, i);
0905: }
0906: values = la;
0907: } else if ((values instanceof byte[] || values instanceof short[])
0908: && value > Short.MAX_VALUE) {
0909: int[] ia = new int[Array.getLength(values)];
0910: for (int i = 0; i < size; i++) {
0911: ia[i] = Array.getInt(values, i);
0912: }
0913: values = ia;
0914: } else if (values instanceof byte[]
0915: && value > Byte.MAX_VALUE) {
0916: short[] sa = new short[Array.getLength(values)];
0917: for (int i = 0; i < size; i++) {
0918: sa[i] = Array.getShort(values, i);
0919: }
0920: values = sa;
0921: }
0922:
0923: // May need to extend the array size
0924: if (Array.getLength(values) == size) {
0925: values = extendArray(values);
0926: }
0927:
0928: // Store the value
0929: if (values instanceof long[]) {
0930: ((long[]) values)[size] = value;
0931: } else if (values instanceof int[]) {
0932: ((int[]) values)[size] = (int) value;
0933: } else if (values instanceof short[]) {
0934: ((short[]) values)[size] = (short) value;
0935: } else {
0936: ((byte[]) values)[size] = (byte) value;
0937: }
0938: size++;
0939: }
0940: }
0941:
0942: // Can be overridden by subclasses
0943: long getValue() {
0944: return 0;
0945: }
0946:
0947: long getLastTimeStamp() {
0948: return times.time(times.size - 1);
0949: }
0950:
0951: long getLastValue(String key) {
0952: Sequence seq = getSequence(key);
0953: return (seq != null && seq.size > 0) ? seq.value(seq.size - 1)
0954: : 0L;
0955: }
0956:
0957: // Called on EDT
0958: public void propertyChange(PropertyChangeEvent ev) {
0959: String prop = ev.getPropertyName();
0960:
0961: if (prop == JConsoleContext.CONNECTION_STATE_PROPERTY) {
0962: ConnectionState newState = (ConnectionState) ev
0963: .getNewValue();
0964:
0965: switch (newState) {
0966: case DISCONNECTED:
0967: synchronized (this ) {
0968: long time = System.currentTimeMillis();
0969: times.add(time);
0970: for (Sequence seq : seqs) {
0971: seq.add(Long.MIN_VALUE);
0972: }
0973: }
0974: break;
0975: }
0976: }
0977: }
0978:
0979: private static class SaveDataFileChooser extends JFileChooser {
0980: SaveDataFileChooser() {
0981: setFileFilter(new FileNameExtensionFilter("CSV file", "csv"));
0982: }
0983:
0984: public void approveSelection() {
0985: File file = getSelectedFile();
0986: if (file != null) {
0987: FileFilter filter = getFileFilter();
0988: if (filter != null
0989: && filter instanceof FileNameExtensionFilter) {
0990: String[] extensions = ((FileNameExtensionFilter) filter)
0991: .getExtensions();
0992:
0993: boolean goodExt = false;
0994: for (String ext : extensions) {
0995: if (file.getName().toLowerCase().endsWith(
0996: "." + ext.toLowerCase())) {
0997: goodExt = true;
0998: break;
0999: }
1000: }
1001: if (!goodExt) {
1002: file = new File(file.getParent(), file
1003: .getName()
1004: + "." + extensions[0]);
1005: }
1006: }
1007:
1008: if (file.exists()) {
1009: String okStr = getText("FileChooser.fileExists.okOption");
1010: String cancelStr = getText("FileChooser.fileExists.cancelOption");
1011: int ret = JOptionPane.showOptionDialog(this ,
1012: getText("FileChooser.fileExists.message",
1013: file.getName()),
1014: getText("FileChooser.fileExists.title"),
1015: JOptionPane.OK_CANCEL_OPTION,
1016: JOptionPane.WARNING_MESSAGE, null,
1017: new Object[] { okStr, cancelStr }, okStr);
1018: if (ret != JOptionPane.OK_OPTION) {
1019: return;
1020: }
1021: }
1022: setSelectedFile(file);
1023: }
1024: super .approveSelection();
1025: }
1026: }
1027:
1028: public AccessibleContext getAccessibleContext() {
1029: if (accessibleContext == null) {
1030: accessibleContext = new AccessiblePlotter();
1031: }
1032: return accessibleContext;
1033: }
1034:
1035: protected class AccessiblePlotter extends AccessibleJComponent {
1036: protected AccessiblePlotter() {
1037: setAccessibleName(getText("Plotter.accessibleName"));
1038: }
1039:
1040: public String getAccessibleName() {
1041: String name = super .getAccessibleName();
1042:
1043: if (seqs.size() > 0 && seqs.get(0).size > 0) {
1044: String keyValueList = "";
1045: for (Sequence seq : seqs) {
1046: if (seq.isPlotted) {
1047: String value = "null";
1048: if (seq.size > 0) {
1049: if (unit == Unit.BYTES) {
1050: value = getText("Size Bytes", seq
1051: .value(seq.size - 1));
1052: } else {
1053: value = getFormattedValue(seq
1054: .value(seq.size - 1), false)
1055: + ((unit == Unit.PERCENT) ? "%"
1056: : "");
1057: }
1058: }
1059: // Assume format string ends with newline
1060: keyValueList += getText(
1061: "Plotter.accessibleName.keyAndValue",
1062: seq.key, value);
1063: }
1064: }
1065: name += "\n" + keyValueList + ".";
1066: } else {
1067: name += "\n" + getText("Plotter.accessibleName.noData");
1068: }
1069: return name;
1070: }
1071:
1072: public AccessibleRole getAccessibleRole() {
1073: return AccessibleRole.CANVAS;
1074: }
1075: }
1076: }
|