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