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