1 /* 2 * Copyright (c) 2004, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package sun.tools.jconsole; 27 28 import java.awt.*; 29 import java.awt.event.*; 30 import java.beans.*; 31 import java.io.*; 32 import java.lang.reflect.Array; 33 import java.util.*; 34 35 import javax.accessibility.*; 36 import javax.swing.*; 37 import javax.swing.border.*; 38 import javax.swing.filechooser.*; 39 import javax.swing.filechooser.FileFilter; 40 41 42 import com.sun.tools.jconsole.JConsoleContext; 43 44 import static sun.tools.jconsole.Formatter.*; 45 import static sun.tools.jconsole.ProxyClient.*; 46 47 @SuppressWarnings("serial") 48 public class Plotter extends JComponent 49 implements Accessible, ActionListener, PropertyChangeListener { 50 51 public static enum Unit { 52 NONE, BYTES, PERCENT 53 } 54 55 static final String[] rangeNames = { 56 Messages.ONE_MIN, 57 Messages.FIVE_MIN, 58 Messages.TEN_MIN, 59 Messages.THIRTY_MIN, 60 Messages.ONE_HOUR, 61 Messages.TWO_HOURS, 62 Messages.THREE_HOURS, 63 Messages.SIX_HOURS, 64 Messages.TWELVE_HOURS, 65 Messages.ONE_DAY, 66 Messages.SEVEN_DAYS, 67 Messages.ONE_MONTH, 68 Messages.THREE_MONTHS, 69 Messages.SIX_MONTHS, 70 Messages.ONE_YEAR, 71 Messages.ALL 72 }; 73 74 static final int[] rangeValues = { 75 1, 76 5, 77 10, 78 30, 79 1 * 60, 80 2 * 60, 81 3 * 60, 82 6 * 60, 83 12 * 60, 84 1 * 24 * 60, 85 7 * 24 * 60, 86 1 * 31 * 24 * 60, 87 3 * 31 * 24 * 60, 88 6 * 31 * 24 * 60, 89 366 * 24 * 60, 90 -1 91 }; 92 93 94 final static long SECOND = 1000; 95 final static long MINUTE = 60 * SECOND; 96 final static long HOUR = 60 * MINUTE; 97 final static long DAY = 24 * HOUR; 98 99 final static Color bgColor = new Color(250, 250, 250); 100 final static Color defaultColor = Color.blue.darker(); 101 102 final static int ARRAY_SIZE_INCREMENT = 4000; 103 104 private static Stroke dashedStroke; 105 106 private TimeStamps times = new TimeStamps(); 107 private ArrayList<Sequence> seqs = new ArrayList<Sequence>(); 108 private JPopupMenu popupMenu; 109 private JMenu timeRangeMenu; 110 private JRadioButtonMenuItem[] menuRBs; 111 private JMenuItem saveAsMI; 112 private JFileChooser saveFC; 113 114 private int viewRange = -1; // Minutes (value <= 0 means full range) 115 private Unit unit; 116 private int decimals; 117 private double decimalsMultiplier; 118 private Border border = null; 119 private Rectangle r = new Rectangle(1, 1, 1, 1); 120 private Font smallFont = null; 121 122 // Initial margins, may be recalculated as needed 123 private int topMargin = 10; 124 private int bottomMargin = 45; 125 private int leftMargin = 65; 126 private int rightMargin = 70; 127 private final boolean displayLegend; 128 Plotter()129 public Plotter() { 130 this(Unit.NONE, 0); 131 } 132 Plotter(Unit unit)133 public Plotter(Unit unit) { 134 this(unit, 0); 135 } 136 Plotter(Unit unit, int decimals)137 public Plotter(Unit unit, int decimals) { 138 this(unit,decimals,true); 139 } 140 141 // Note: If decimals > 0 then values must be decimally shifted left 142 // that many places, i.e. multiplied by Math.pow(10.0, decimals). Plotter(Unit unit, int decimals, boolean displayLegend)143 public Plotter(Unit unit, int decimals, boolean displayLegend) { 144 this.displayLegend = displayLegend; 145 setUnit(unit); 146 setDecimals(decimals); 147 148 enableEvents(AWTEvent.MOUSE_EVENT_MASK); 149 150 addMouseListener(new MouseAdapter() { 151 @Override 152 public void mousePressed(MouseEvent e) { 153 if (getParent() instanceof PlotterPanel) { 154 getParent().requestFocusInWindow(); 155 } 156 } 157 }); 158 159 } 160 setUnit(Unit unit)161 public void setUnit(Unit unit) { 162 this.unit = unit; 163 } 164 setDecimals(int decimals)165 public void setDecimals(int decimals) { 166 this.decimals = decimals; 167 this.decimalsMultiplier = Math.pow(10.0, decimals); 168 } 169 createSequence(String key, String name, Color color, boolean isPlotted)170 public void createSequence(String key, String name, Color color, boolean isPlotted) { 171 Sequence seq = getSequence(key); 172 if (seq == null) { 173 seq = new Sequence(key); 174 } 175 seq.name = name; 176 seq.color = (color != null) ? color : defaultColor; 177 seq.isPlotted = isPlotted; 178 179 seqs.add(seq); 180 } 181 setUseDashedTransitions(String key, boolean b)182 public void setUseDashedTransitions(String key, boolean b) { 183 Sequence seq = getSequence(key); 184 if (seq != null) { 185 seq.transitionStroke = b ? getDashedStroke() : null; 186 } 187 } 188 setIsPlotted(String key, boolean isPlotted)189 public void setIsPlotted(String key, boolean isPlotted) { 190 Sequence seq = getSequence(key); 191 if (seq != null) { 192 seq.isPlotted = isPlotted; 193 } 194 } 195 196 // Note: If decimals > 0 then values must be decimally shifted left 197 // that many places, i.e. multiplied by Math.pow(10.0, decimals). addValues(long time, long... values)198 public synchronized void addValues(long time, long... values) { 199 assert (values.length == seqs.size()); 200 times.add(time); 201 for (int i = 0; i < values.length; i++) { 202 seqs.get(i).add(values[i]); 203 } 204 repaint(); 205 } 206 getSequence(String key)207 private Sequence getSequence(String key) { 208 for (Sequence seq : seqs) { 209 if (seq.key.equals(key)) { 210 return seq; 211 } 212 } 213 return null; 214 } 215 216 /** 217 * @return the displayed time range in minutes, or -1 for all data 218 */ getViewRange()219 public int getViewRange() { 220 return viewRange; 221 } 222 223 /** 224 * @param minutes the displayed time range in minutes, or -1 to diaplay all data 225 */ setViewRange(int minutes)226 public void setViewRange(int minutes) { 227 if (minutes != viewRange) { 228 int oldValue = viewRange; 229 viewRange = minutes; 230 /* Do not i18n this string */ 231 firePropertyChange("viewRange", oldValue, viewRange); 232 if (popupMenu != null) { 233 for (int i = 0; i < menuRBs.length; i++) { 234 if (rangeValues[i] == viewRange) { 235 menuRBs[i].setSelected(true); 236 break; 237 } 238 } 239 } 240 repaint(); 241 } 242 } 243 244 @Override getComponentPopupMenu()245 public JPopupMenu getComponentPopupMenu() { 246 if (popupMenu == null) { 247 popupMenu = new JPopupMenu(Messages.CHART_COLON); 248 timeRangeMenu = new JMenu(Messages.PLOTTER_TIME_RANGE_MENU); 249 timeRangeMenu.setMnemonic(Resources.getMnemonicInt(Messages.PLOTTER_TIME_RANGE_MENU)); 250 popupMenu.add(timeRangeMenu); 251 menuRBs = new JRadioButtonMenuItem[rangeNames.length]; 252 ButtonGroup rbGroup = new ButtonGroup(); 253 for (int i = 0; i < rangeNames.length; i++) { 254 menuRBs[i] = new JRadioButtonMenuItem(rangeNames[i]); 255 rbGroup.add(menuRBs[i]); 256 menuRBs[i].addActionListener(this); 257 if (viewRange == rangeValues[i]) { 258 menuRBs[i].setSelected(true); 259 } 260 timeRangeMenu.add(menuRBs[i]); 261 } 262 263 popupMenu.addSeparator(); 264 265 saveAsMI = new JMenuItem(Messages.PLOTTER_SAVE_AS_MENU_ITEM); 266 saveAsMI.setMnemonic(Resources.getMnemonicInt(Messages.PLOTTER_SAVE_AS_MENU_ITEM)); 267 saveAsMI.addActionListener(this); 268 popupMenu.add(saveAsMI); 269 } 270 return popupMenu; 271 } 272 actionPerformed(ActionEvent ev)273 public void actionPerformed(ActionEvent ev) { 274 JComponent src = (JComponent)ev.getSource(); 275 if (src == saveAsMI) { 276 saveAs(); 277 } else { 278 int index = timeRangeMenu.getPopupMenu().getComponentIndex(src); 279 setViewRange(rangeValues[index]); 280 } 281 } 282 saveAs()283 private void saveAs() { 284 if (saveFC == null) { 285 saveFC = new SaveDataFileChooser(); 286 } 287 int ret = saveFC.showSaveDialog(this); 288 if (ret == JFileChooser.APPROVE_OPTION) { 289 saveDataToFile(saveFC.getSelectedFile()); 290 } 291 } 292 saveDataToFile(File file)293 private void saveDataToFile(File file) { 294 try { 295 PrintStream out = new PrintStream(new FileOutputStream(file)); 296 297 // Print header line 298 out.print("Time"); 299 for (Sequence seq : seqs) { 300 out.print(","+seq.name); 301 } 302 out.println(); 303 304 // Print data lines 305 if (seqs.size() > 0 && seqs.get(0).size > 0) { 306 for (int i = 0; i < seqs.get(0).size; i++) { 307 double excelTime = toExcelTime(times.time(i)); 308 out.print(String.format(Locale.ENGLISH, "%.6f", excelTime)); 309 for (Sequence seq : seqs) { 310 out.print("," + getFormattedValue(seq.value(i), false)); 311 } 312 out.println(); 313 } 314 } 315 316 out.close(); 317 JOptionPane.showMessageDialog(this, 318 Resources.format(Messages.FILE_CHOOSER_SAVED_FILE, 319 file.getAbsolutePath(), 320 file.length())); 321 } catch (IOException ex) { 322 String msg = ex.getLocalizedMessage(); 323 String path = file.getAbsolutePath(); 324 if (msg.startsWith(path)) { 325 msg = msg.substring(path.length()).trim(); 326 } 327 JOptionPane.showMessageDialog(this, 328 Resources.format(Messages.FILE_CHOOSER_SAVE_FAILED_MESSAGE, 329 path, 330 msg), 331 Messages.FILE_CHOOSER_SAVE_FAILED_TITLE, 332 JOptionPane.ERROR_MESSAGE); 333 } 334 } 335 336 @Override paintComponent(Graphics g)337 public void paintComponent(Graphics g) { 338 super.paintComponent(g); 339 340 int width = getWidth()-rightMargin-leftMargin-10; 341 int height = getHeight()-topMargin-bottomMargin; 342 if (width <= 0 || height <= 0) { 343 // not enough room to paint anything 344 return; 345 } 346 347 Color oldColor = g.getColor(); 348 Font oldFont = g.getFont(); 349 Color fg = getForeground(); 350 Color bg = getBackground(); 351 boolean bgIsLight = (bg.getRed() > 200 && 352 bg.getGreen() > 200 && 353 bg.getBlue() > 200); 354 355 356 ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, 357 RenderingHints.VALUE_ANTIALIAS_ON); 358 359 if (smallFont == null) { 360 smallFont = oldFont.deriveFont(9.0F); 361 } 362 363 r.x = leftMargin - 5; 364 r.y = topMargin - 8; 365 r.width = getWidth()-leftMargin-rightMargin; 366 r.height = getHeight()-topMargin-bottomMargin+16; 367 368 if (border == null) { 369 // By setting colors here, we avoid recalculating them 370 // over and over. 371 border = new BevelBorder(BevelBorder.LOWERED, 372 getBackground().brighter().brighter(), 373 getBackground().brighter(), 374 getBackground().darker().darker(), 375 getBackground().darker()); 376 } 377 378 border.paintBorder(this, g, r.x, r.y, r.width, r.height); 379 380 // Fill background color 381 g.setColor(bgColor); 382 g.fillRect(r.x+2, r.y+2, r.width-4, r.height-4); 383 g.setColor(oldColor); 384 385 long tMin = Long.MAX_VALUE; 386 long tMax = Long.MIN_VALUE; 387 long vMin = Long.MAX_VALUE; 388 long vMax = 1; 389 390 int w = getWidth()-rightMargin-leftMargin-10; 391 int h = getHeight()-topMargin-bottomMargin; 392 393 if (times.size > 1) { 394 tMin = Math.min(tMin, times.time(0)); 395 tMax = Math.max(tMax, times.time(times.size-1)); 396 } 397 long viewRangeMS; 398 if (viewRange > 0) { 399 viewRangeMS = viewRange * MINUTE; 400 } else { 401 // Display full time range, but no less than a minute 402 viewRangeMS = Math.max(tMax - tMin, 1 * MINUTE); 403 } 404 405 // Calculate min/max values 406 for (Sequence seq : seqs) { 407 if (seq.size > 0) { 408 for (int i = 0; i < seq.size; i++) { 409 if (seq.size == 1 || times.time(i) >= tMax - viewRangeMS) { 410 long val = seq.value(i); 411 if (val > Long.MIN_VALUE) { 412 vMax = Math.max(vMax, val); 413 vMin = Math.min(vMin, val); 414 } 415 } 416 } 417 } else { 418 vMin = 0L; 419 } 420 if (unit == Unit.BYTES || !seq.isPlotted) { 421 // We'll scale only to the first (main) value set. 422 // TODO: Use a separate property for this. 423 break; 424 } 425 } 426 427 // Normalize scale 428 vMax = normalizeMax(vMax); 429 if (vMin > 0) { 430 if (vMax / vMin > 4) { 431 vMin = 0; 432 } else { 433 vMin = normalizeMin(vMin); 434 } 435 } 436 437 438 g.setColor(fg); 439 440 // Axes 441 // Draw vertical axis 442 int x = leftMargin - 18; 443 int y = topMargin; 444 FontMetrics fm = g.getFontMetrics(); 445 446 g.drawLine(x, y, x, y+h); 447 448 int n = 5; 449 if ((""+vMax).startsWith("2")) { 450 n = 4; 451 } else if ((""+vMax).startsWith("3")) { 452 n = 6; 453 } else if ((""+vMax).startsWith("4")) { 454 n = 4; 455 } else if ((""+vMax).startsWith("6")) { 456 n = 6; 457 } else if ((""+vMax).startsWith("7")) { 458 n = 7; 459 } else if ((""+vMax).startsWith("8")) { 460 n = 8; 461 } else if ((""+vMax).startsWith("9")) { 462 n = 3; 463 } 464 465 // Ticks 466 ArrayList<Long> tickValues = new ArrayList<Long>(); 467 tickValues.add(vMin); 468 for (int i = 0; i < n; i++) { 469 long v = i * vMax / n; 470 if (v > vMin) { 471 tickValues.add(v); 472 } 473 } 474 tickValues.add(vMax); 475 n = tickValues.size(); 476 477 String[] tickStrings = new String[n]; 478 for (int i = 0; i < n; i++) { 479 long v = tickValues.get(i); 480 tickStrings[i] = getSizeString(v, vMax); 481 } 482 483 // Trim trailing decimal zeroes. 484 if (decimals > 0) { 485 boolean trimLast = true; 486 boolean removedDecimalPoint = false; 487 do { 488 for (String str : tickStrings) { 489 if (!(str.endsWith("0") || str.endsWith("."))) { 490 trimLast = false; 491 break; 492 } 493 } 494 if (trimLast) { 495 if (tickStrings[0].endsWith(".")) { 496 removedDecimalPoint = true; 497 } 498 for (int i = 0; i < n; i++) { 499 String str = tickStrings[i]; 500 tickStrings[i] = str.substring(0, str.length()-1); 501 } 502 } 503 } while (trimLast && !removedDecimalPoint); 504 } 505 506 // Draw ticks 507 int lastY = Integer.MAX_VALUE; 508 for (int i = 0; i < n; i++) { 509 long v = tickValues.get(i); 510 y = topMargin+h-(int)(h * (v-vMin) / (vMax-vMin)); 511 g.drawLine(x-2, y, x+2, y); 512 String s = tickStrings[i]; 513 if (unit == Unit.PERCENT) { 514 s += "%"; 515 } 516 int sx = x-6-fm.stringWidth(s); 517 if (y < lastY-13) { 518 if (checkLeftMargin(sx)) { 519 // Wait for next repaint 520 return; 521 } 522 g.drawString(s, sx, y+4); 523 } 524 // Draw horizontal grid line 525 g.setColor(Color.lightGray); 526 g.drawLine(r.x + 4, y, r.x + r.width - 4, y); 527 g.setColor(fg); 528 lastY = y; 529 } 530 531 // Draw horizontal axis 532 x = leftMargin; 533 y = topMargin + h + 15; 534 g.drawLine(x, y, x+w, y); 535 536 long t1 = tMax; 537 if (t1 <= 0L) { 538 // No data yet, so draw current time 539 t1 = System.currentTimeMillis(); 540 } 541 long tz = timeDF.getTimeZone().getOffset(t1); 542 long tickInterval = calculateTickInterval(w, 40, viewRangeMS); 543 if (tickInterval > 3 * HOUR) { 544 tickInterval = calculateTickInterval(w, 80, viewRangeMS); 545 } 546 long t0 = tickInterval - (t1 - viewRangeMS + tz) % tickInterval; 547 while (t0 < viewRangeMS) { 548 x = leftMargin + (int)(w * t0 / viewRangeMS); 549 g.drawLine(x, y-2, x, y+2); 550 551 long t = t1 - viewRangeMS + t0; 552 String str = formatClockTime(t); 553 g.drawString(str, x, y+16); 554 //if (tickInterval > (1 * HOUR) && t % (1 * DAY) == 0) { 555 if ((t + tz) % (1 * DAY) == 0) { 556 str = formatDate(t); 557 g.drawString(str, x, y+27); 558 } 559 // Draw vertical grid line 560 g.setColor(Color.lightGray); 561 g.drawLine(x, topMargin, x, topMargin + h); 562 g.setColor(fg); 563 t0 += tickInterval; 564 } 565 566 // Plot values 567 int start = 0; 568 int nValues = 0; 569 int nLists = seqs.size(); 570 if (nLists > 0) { 571 nValues = seqs.get(0).size; 572 } 573 if (nValues == 0) { 574 g.setColor(oldColor); 575 return; 576 } else { 577 Sequence seq = seqs.get(0); 578 // Find starting point 579 for (int p = 0; p < seq.size; p++) { 580 if (times.time(p) >= tMax - viewRangeMS) { 581 start = p; 582 break; 583 } 584 } 585 } 586 587 //Optimization: collapse plot of more than four values per pixel 588 int pointsPerPixel = (nValues - start) / w; 589 if (pointsPerPixel < 4) { 590 pointsPerPixel = 1; 591 } 592 593 // Draw graphs 594 // Loop backwards over sequences because the first needs to be painted on top 595 for (int i = nLists-1; i >= 0; i--) { 596 int x0 = leftMargin; 597 int y0 = topMargin + h + 1; 598 599 Sequence seq = seqs.get(i); 600 if (seq.isPlotted && seq.size > 0) { 601 // Paint twice, with white and with color 602 for (int pass = 0; pass < 2; pass++) { 603 g.setColor((pass == 0) ? Color.white : seq.color); 604 int x1 = -1; 605 long v1 = -1; 606 for (int p = start; p < nValues; p += pointsPerPixel) { 607 // Make sure we get the last value 608 if (pointsPerPixel > 1 && p >= nValues - pointsPerPixel) { 609 p = nValues - 1; 610 } 611 int x2 = (int)(w * (times.time(p)-(t1-viewRangeMS)) / viewRangeMS); 612 long v2 = seq.value(p); 613 if (v2 >= vMin && v2 <= vMax) { 614 int y2 = (int)(h * (v2 -vMin) / (vMax-vMin)); 615 if (x1 >= 0 && v1 >= vMin && v1 <= vMax) { 616 int y1 = (int)(h * (v1-vMin) / (vMax-vMin)); 617 618 if (y1 == y2) { 619 // fillrect is much faster 620 g.fillRect(x0+x1, y0-y1-pass, x2-x1, 1); 621 } else { 622 Graphics2D g2d = (Graphics2D)g; 623 Stroke oldStroke = null; 624 if (seq.transitionStroke != null) { 625 oldStroke = g2d.getStroke(); 626 g2d.setStroke(seq.transitionStroke); 627 } 628 g.drawLine(x0+x1, y0-y1-pass, x0+x2, y0-y2-pass); 629 if (oldStroke != null) { 630 g2d.setStroke(oldStroke); 631 } 632 } 633 } 634 } 635 x1 = x2; 636 v1 = v2; 637 } 638 } 639 640 // Current value 641 long v = seq.value(seq.size - 1); 642 if (v >= vMin && v <= vMax) { 643 if (bgIsLight) { 644 g.setColor(seq.color); 645 } else { 646 g.setColor(fg); 647 } 648 x = r.x + r.width + 2; 649 y = topMargin+h-(int)(h * (v-vMin) / (vMax-vMin)); 650 // a small triangle/arrow 651 g.fillPolygon(new int[] { x+2, x+6, x+6 }, 652 new int[] { y, y+3, y-3 }, 653 3); 654 } 655 g.setColor(fg); 656 } 657 } 658 659 int[] valueStringSlots = new int[nLists]; 660 for (int i = 0; i < nLists; i++) valueStringSlots[i] = -1; 661 for (int i = 0; i < nLists; i++) { 662 Sequence seq = seqs.get(i); 663 if (seq.isPlotted && seq.size > 0) { 664 // Draw current value 665 666 // TODO: collapse values if pointsPerPixel >= 4 667 668 long v = seq.value(seq.size - 1); 669 if (v >= vMin && v <= vMax) { 670 x = r.x + r.width + 2; 671 y = topMargin+h-(int)(h * (v-vMin) / (vMax-vMin)); 672 int y2 = getValueStringSlot(valueStringSlots, y, 2*10, i); 673 g.setFont(smallFont); 674 if (bgIsLight) { 675 g.setColor(seq.color); 676 } else { 677 g.setColor(fg); 678 } 679 String curValue = getFormattedValue(v, true); 680 if (unit == Unit.PERCENT) { 681 curValue += "%"; 682 } 683 int valWidth = fm.stringWidth(curValue); 684 String legend = (displayLegend?seq.name:""); 685 int legendWidth = fm.stringWidth(legend); 686 if (checkRightMargin(valWidth) || checkRightMargin(legendWidth)) { 687 // Wait for next repaint 688 return; 689 } 690 g.drawString(legend , x + 17, Math.min(topMargin+h, y2 + 3 - 10)); 691 g.drawString(curValue, x + 17, Math.min(topMargin+h + 10, y2 + 3)); 692 693 // Maybe draw a short line to value 694 if (y2 > y + 3) { 695 g.drawLine(x + 9, y + 2, x + 14, y2); 696 } else if (y2 < y - 3) { 697 g.drawLine(x + 9, y - 2, x + 14, y2); 698 } 699 } 700 g.setFont(oldFont); 701 g.setColor(fg); 702 703 } 704 } 705 g.setColor(oldColor); 706 } 707 checkLeftMargin(int x)708 private boolean checkLeftMargin(int x) { 709 // Make sure leftMargin has at least 2 pixels over 710 if (x < 2) { 711 leftMargin += (2 - x); 712 // Repaint from top (above any cell renderers) 713 SwingUtilities.getWindowAncestor(this).repaint(); 714 return true; 715 } 716 return false; 717 } 718 checkRightMargin(int w)719 private boolean checkRightMargin(int w) { 720 // Make sure rightMargin has at least 2 pixels over 721 if (w + 2 > rightMargin) { 722 rightMargin = (w + 2); 723 // Repaint from top (above any cell renderers) 724 SwingUtilities.getWindowAncestor(this).repaint(); 725 return true; 726 } 727 return false; 728 } 729 getValueStringSlot(int[] slots, int y, int h, int i)730 private int getValueStringSlot(int[] slots, int y, int h, int i) { 731 for (int s = 0; s < slots.length; s++) { 732 if (slots[s] >= y && slots[s] < y + h) { 733 // collide below us 734 if (slots[s] > h) { 735 return getValueStringSlot(slots, slots[s]-h, h, i); 736 } else { 737 return getValueStringSlot(slots, slots[s]+h, h, i); 738 } 739 } else if (y >= h && slots[s] > y - h && slots[s] < y) { 740 // collide above us 741 return getValueStringSlot(slots, slots[s]+h, h, i); 742 } 743 } 744 slots[i] = y; 745 return y; 746 } 747 calculateTickInterval(int w, int hGap, long viewRangeMS)748 private long calculateTickInterval(int w, int hGap, long viewRangeMS) { 749 long tickInterval = viewRangeMS * hGap / w; 750 if (tickInterval < 1 * MINUTE) { 751 tickInterval = 1 * MINUTE; 752 } else if (tickInterval < 5 * MINUTE) { 753 tickInterval = 5 * MINUTE; 754 } else if (tickInterval < 10 * MINUTE) { 755 tickInterval = 10 * MINUTE; 756 } else if (tickInterval < 30 * MINUTE) { 757 tickInterval = 30 * MINUTE; 758 } else if (tickInterval < 1 * HOUR) { 759 tickInterval = 1 * HOUR; 760 } else if (tickInterval < 3 * HOUR) { 761 tickInterval = 3 * HOUR; 762 } else if (tickInterval < 6 * HOUR) { 763 tickInterval = 6 * HOUR; 764 } else if (tickInterval < 12 * HOUR) { 765 tickInterval = 12 * HOUR; 766 } else if (tickInterval < 1 * DAY) { 767 tickInterval = 1 * DAY; 768 } else { 769 tickInterval = normalizeMax(tickInterval / DAY) * DAY; 770 } 771 return tickInterval; 772 } 773 normalizeMin(long l)774 private long normalizeMin(long l) { 775 int exp = (int)Math.log10((double)l); 776 long multiple = (long)Math.pow(10.0, exp); 777 int i = (int)(l / multiple); 778 return i * multiple; 779 } 780 normalizeMax(long l)781 private long normalizeMax(long l) { 782 int exp = (int)Math.log10((double)l); 783 long multiple = (long)Math.pow(10.0, exp); 784 int i = (int)(l / multiple); 785 l = (i+1)*multiple; 786 return l; 787 } 788 getFormattedValue(long v, boolean groupDigits)789 private String getFormattedValue(long v, boolean groupDigits) { 790 String str; 791 String fmt = "%"; 792 if (groupDigits) { 793 fmt += ","; 794 } 795 if (decimals > 0) { 796 fmt += "." + decimals + "f"; 797 str = String.format(fmt, v / decimalsMultiplier); 798 } else { 799 fmt += "d"; 800 str = String.format(fmt, v); 801 } 802 return str; 803 } 804 getSizeString(long v, long vMax)805 private String getSizeString(long v, long vMax) { 806 String s; 807 808 if (unit == Unit.BYTES && decimals == 0) { 809 s = formatBytes(v, vMax); 810 } else { 811 s = getFormattedValue(v, true); 812 } 813 return s; 814 } 815 getDashedStroke()816 private static synchronized Stroke getDashedStroke() { 817 if (dashedStroke == null) { 818 dashedStroke = new BasicStroke(1.0f, 819 BasicStroke.CAP_BUTT, 820 BasicStroke.JOIN_MITER, 821 10.0f, 822 new float[] { 2.0f, 3.0f }, 823 0.0f); 824 } 825 return dashedStroke; 826 } 827 extendArray(Object a1)828 private static Object extendArray(Object a1) { 829 int n = Array.getLength(a1); 830 Object a2 = 831 Array.newInstance(a1.getClass().getComponentType(), 832 n + ARRAY_SIZE_INCREMENT); 833 System.arraycopy(a1, 0, a2, 0, n); 834 return a2; 835 } 836 837 838 private static class TimeStamps { 839 // Time stamps (long) are split into offsets (long) and a 840 // series of times from the offsets (int). A new offset is 841 // stored when the time value doesn't fit in an int 842 // (approx every 24 days). An array of indices is used to 843 // define the starting point for each offset in the times 844 // array. 845 long[] offsets = new long[0]; 846 int[] indices = new int[0]; 847 int[] rtimes = new int[ARRAY_SIZE_INCREMENT]; 848 849 // Number of stored timestamps 850 int size = 0; 851 852 /** 853 * Returns the time stamp for index i 854 */ time(int i)855 public long time(int i) { 856 long offset = 0; 857 for (int j = indices.length - 1; j >= 0; j--) { 858 if (i >= indices[j]) { 859 offset = offsets[j]; 860 break; 861 } 862 } 863 return offset + rtimes[i]; 864 } 865 add(long time)866 public void add(long time) { 867 // May need to store a new time offset 868 int n = offsets.length; 869 if (n == 0 || time - offsets[n - 1] > Integer.MAX_VALUE) { 870 // Grow offset and indices arrays and store new offset 871 offsets = Arrays.copyOf(offsets, n + 1); 872 offsets[n] = time; 873 indices = Arrays.copyOf(indices, n + 1); 874 indices[n] = size; 875 } 876 877 // May need to extend the array size 878 if (rtimes.length == size) { 879 rtimes = (int[])extendArray(rtimes); 880 } 881 882 // Store the time 883 rtimes[size] = (int)(time - offsets[offsets.length - 1]); 884 size++; 885 } 886 } 887 888 private static class Sequence { 889 String key; 890 String name; 891 Color color; 892 boolean isPlotted; 893 Stroke transitionStroke = null; 894 895 // Values are stored in an int[] if all values will fit, 896 // otherwise in a long[]. An int can represent up to 2 GB. 897 // Use a random start size, so all arrays won't need to 898 // be grown during the same update interval 899 Object values = 900 new byte[ARRAY_SIZE_INCREMENT + (int)(Math.random() * 100)]; 901 902 // Number of stored values 903 int size = 0; 904 Sequence(String key)905 public Sequence(String key) { 906 this.key = key; 907 } 908 909 /** 910 * Returns the value at index i 911 */ value(int i)912 public long value(int i) { 913 return Array.getLong(values, i); 914 } 915 add(long value)916 public void add(long value) { 917 // May need to switch to a larger array type 918 if ((values instanceof byte[] || 919 values instanceof short[] || 920 values instanceof int[]) && 921 value > Integer.MAX_VALUE) { 922 long[] la = new long[Array.getLength(values)]; 923 for (int i = 0; i < size; i++) { 924 la[i] = Array.getLong(values, i); 925 } 926 values = la; 927 } else if ((values instanceof byte[] || 928 values instanceof short[]) && 929 value > Short.MAX_VALUE) { 930 int[] ia = new int[Array.getLength(values)]; 931 for (int i = 0; i < size; i++) { 932 ia[i] = Array.getInt(values, i); 933 } 934 values = ia; 935 } else if (values instanceof byte[] && 936 value > Byte.MAX_VALUE) { 937 short[] sa = new short[Array.getLength(values)]; 938 for (int i = 0; i < size; i++) { 939 sa[i] = Array.getShort(values, i); 940 } 941 values = sa; 942 } 943 944 // May need to extend the array size 945 if (Array.getLength(values) == size) { 946 values = extendArray(values); 947 } 948 949 // Store the value 950 if (values instanceof long[]) { 951 ((long[])values)[size] = value; 952 } else if (values instanceof int[]) { 953 ((int[])values)[size] = (int)value; 954 } else if (values instanceof short[]) { 955 ((short[])values)[size] = (short)value; 956 } else { 957 ((byte[])values)[size] = (byte)value; 958 } 959 size++; 960 } 961 } 962 963 // Can be overridden by subclasses getValue()964 long getValue() { 965 return 0; 966 } 967 getLastTimeStamp()968 long getLastTimeStamp() { 969 return times.time(times.size - 1); 970 } 971 getLastValue(String key)972 long getLastValue(String key) { 973 Sequence seq = getSequence(key); 974 return (seq != null && seq.size > 0) ? seq.value(seq.size - 1) : 0L; 975 } 976 977 978 // Called on EDT propertyChange(PropertyChangeEvent ev)979 public void propertyChange(PropertyChangeEvent ev) { 980 String prop = ev.getPropertyName(); 981 982 if (prop == JConsoleContext.CONNECTION_STATE_PROPERTY) { 983 ConnectionState newState = (ConnectionState)ev.getNewValue(); 984 985 switch (newState) { 986 case DISCONNECTED: 987 synchronized(this) { 988 long time = System.currentTimeMillis(); 989 times.add(time); 990 for (Sequence seq : seqs) { 991 seq.add(Long.MIN_VALUE); 992 } 993 } 994 break; 995 } 996 } 997 } 998 999 private static class SaveDataFileChooser extends JFileChooser { 1000 private static final long serialVersionUID = -5182890922369369669L; SaveDataFileChooser()1001 SaveDataFileChooser() { 1002 setFileFilter(new FileNameExtensionFilter("CSV file", "csv")); 1003 } 1004 1005 @Override approveSelection()1006 public void approveSelection() { 1007 File file = getSelectedFile(); 1008 if (file != null) { 1009 FileFilter filter = getFileFilter(); 1010 if (filter != null && filter instanceof FileNameExtensionFilter) { 1011 String[] extensions = 1012 ((FileNameExtensionFilter)filter).getExtensions(); 1013 1014 boolean goodExt = false; 1015 for (String ext : extensions) { 1016 if (file.getName().toLowerCase().endsWith("." + ext.toLowerCase())) { 1017 goodExt = true; 1018 break; 1019 } 1020 } 1021 if (!goodExt) { 1022 file = new File(file.getParent(), 1023 file.getName() + "." + extensions[0]); 1024 } 1025 } 1026 1027 if (file.exists()) { 1028 String okStr = Messages.FILE_CHOOSER_FILE_EXISTS_OK_OPTION; 1029 String cancelStr = Messages.FILE_CHOOSER_FILE_EXISTS_CANCEL_OPTION; 1030 int ret = 1031 JOptionPane.showOptionDialog(this, 1032 Resources.format(Messages.FILE_CHOOSER_FILE_EXISTS_MESSAGE, 1033 file.getName()), 1034 Messages.FILE_CHOOSER_FILE_EXISTS_TITLE, 1035 JOptionPane.OK_CANCEL_OPTION, 1036 JOptionPane.WARNING_MESSAGE, 1037 null, 1038 new Object[] { okStr, cancelStr }, 1039 okStr); 1040 if (ret != JOptionPane.OK_OPTION) { 1041 return; 1042 } 1043 } 1044 setSelectedFile(file); 1045 } 1046 super.approveSelection(); 1047 } 1048 } 1049 1050 @Override getAccessibleContext()1051 public AccessibleContext getAccessibleContext() { 1052 if (accessibleContext == null) { 1053 accessibleContext = new AccessiblePlotter(); 1054 } 1055 return accessibleContext; 1056 } 1057 1058 protected class AccessiblePlotter extends AccessibleJComponent { 1059 private static final long serialVersionUID = -3847205410473510922L; AccessiblePlotter()1060 protected AccessiblePlotter() { 1061 setAccessibleName(Messages.PLOTTER_ACCESSIBLE_NAME); 1062 } 1063 1064 @Override getAccessibleName()1065 public String getAccessibleName() { 1066 String name = super.getAccessibleName(); 1067 1068 if (seqs.size() > 0 && seqs.get(0).size > 0) { 1069 String keyValueList = ""; 1070 for (Sequence seq : seqs) { 1071 if (seq.isPlotted) { 1072 String value = "null"; 1073 if (seq.size > 0) { 1074 if (unit == Unit.BYTES) { 1075 value = Resources.format(Messages.SIZE_BYTES, seq.value(seq.size - 1)); 1076 } else { 1077 value = 1078 getFormattedValue(seq.value(seq.size - 1), false) + 1079 ((unit == Unit.PERCENT) ? "%" : ""); 1080 } 1081 } 1082 // Assume format string ends with newline 1083 keyValueList += 1084 Resources.format(Messages.PLOTTER_ACCESSIBLE_NAME_KEY_AND_VALUE, 1085 seq.key, value); 1086 } 1087 } 1088 name += "\n" + keyValueList + "."; 1089 } else { 1090 name += "\n" + Messages.PLOTTER_ACCESSIBLE_NAME_NO_DATA; 1091 } 1092 return name; 1093 } 1094 1095 @Override getAccessibleRole()1096 public AccessibleRole getAccessibleRole() { 1097 return AccessibleRole.CANVAS; 1098 } 1099 } 1100 } 1101