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