001: /*
002: * GNetWatch
003: * Copyright 2006, 2007 Alexandre Fenyo
004: * gnetwatch@fenyo.net
005: *
006: * This file is part of GNetWatch.
007: *
008: * GNetWatch is free software; you can redistribute it and/or modify
009: * it under the terms of the GNU General Public License as published by
010: * the Free Software Foundation; either version 2 of the License, or
011: * (at your option) any later version.
012: *
013: * GNetWatch is distributed in the hope that it will be useful,
014: * but WITHOUT ANY WARRANTY; without even the implied warranty of
015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016: * GNU General Public License for more details.
017: *
018: * You should have received a copy of the GNU General Public License
019: * along with GNetWatch; if not, write to the Free Software
020: * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
021: */
022:
023: package net.fenyo.gnetwatch.GUI;
024:
025: import java.awt.Color;
026: import java.awt.Component;
027: import java.awt.Dimension;
028: import java.awt.Graphics;
029: import java.awt.Graphics2D;
030: import java.awt.Image;
031: import java.awt.RenderingHints;
032: import java.awt.event.ComponentEvent;
033: import java.awt.event.ComponentListener;
034: import java.awt.event.WindowEvent;
035: import java.awt.event.WindowListener;
036: import java.awt.font.TextLayout;
037: import java.awt.geom.Rectangle2D;
038: import java.text.DateFormat;
039: import java.util.Date;
040:
041: import net.fenyo.gnetwatch.data.EventGeneric;
042: import net.fenyo.gnetwatch.targets.Target;
043:
044: import org.apache.commons.logging.Log;
045: import org.apache.commons.logging.LogFactory;
046:
047: /**
048: * This class implements an AWT component capable of drawing time series with the Java2D API.
049: * @author Alexandre Fenyo
050: * @version $Id: BasicComponent.java,v 1.23 2007/03/03 00:38:19 fenyo Exp $
051: */
052:
053: // any thread << sync_value_per_vinterval << sync_update << events
054: // repaint thread << sync_value_per_vinterval << events
055: // repaint thread << sync_value_per_vinterval << sync_update << registered_components
056: public abstract class BasicComponent extends Component implements
057: ComponentListener, WindowListener {
058: private static Log log = LogFactory.getLog(BasicComponent.class);
059:
060: final private Target target;
061:
062: private static Object sync_update = new Object();
063:
064: private java.util.List<EventGeneric> events = null;
065:
066: private Image backing_store = null;
067: private Graphics2D backing_g = null;
068: private Dimension dimension = null;
069:
070: private final DateFormat date_format = DateFormat
071: .getDateTimeInstance();
072:
073: private int fps = 0;
074: private long last_paint = System.currentTimeMillis();
075: private long last_paint_100ms = System.currentTimeMillis();
076: private int last_fps_100ms = 0;
077:
078: // horizontal interval
079: protected final int pixels_per_interval = 80; // 80 pix/intervalle ; multiple de 10
080:
081: // vertical intervals
082: private int value_per_vinterval = 200;
083: private final Object sync_value_per_vinterval = new Object();
084: private final int pixels_per_vinterval = 80; // 80 pix/vintervalle ; multiple de 10
085:
086: private final static int std_margin = 30;
087: private final static int std_separator = 3;
088:
089: private final static int axis_margin_bottom = std_margin;
090: private final static int axis_margin_top = std_margin;
091: protected int axis_margin_left = std_margin;
092: protected final static int axis_margin_right = std_margin;
093:
094: /**
095: * Constructor.
096: * @param target target this graphic component works on.
097: */
098: // GUI thread
099: public BasicComponent(final Target target) {
100: this .target = target;
101: }
102:
103: /**
104: * Returns the horizontal scale.
105: * @param none.
106: * @return long horizontal scale.
107: */
108: public long getDelayPerInterval() {
109: return 5000; // 5 secs/interval
110: }
111:
112: /**
113: * Sets the list of events to display.
114: * @param events events to display.
115: * @return void.
116: */
117: protected void setEvents(final java.util.List<EventGeneric> events) {
118: this .events = events;
119: }
120:
121: /**
122: * Returns the dimensions of this component.
123: * @param none.
124: * @return Dimension dimensions.
125: */
126: protected Dimension getDimension() {
127: return dimension;
128: }
129:
130: /**
131: * Returns the multithreaded synchronization lock.
132: * @param none.
133: * @return Object lock.
134: */
135: protected Object getSyncUpdate() {
136: return sync_update;
137: }
138:
139: /**
140: * Returns the associated target.
141: * @param none.
142: * @return Target target.
143: */
144: protected Target getTarget() {
145: return target;
146: }
147:
148: /**
149: * Initialize the component and ask AWT to receive events.
150: *
151: */
152: // GUI thread
153: public void init() {
154: setPreferredSize(new Dimension(700, 400));
155: addComponentListener(this );
156: }
157:
158: /**
159: * Called when the component is hidden.
160: * @param e event.
161: * @return void.
162: */
163: public void componentHidden(final ComponentEvent e) {
164: }
165:
166: /**
167: * Called when the component is moved.
168: * @param e event.
169: * @return void.
170: */
171: public void componentMoved(final ComponentEvent e) {
172: }
173:
174: /**
175: * When the component is resized, creates a new backing store,
176: * reset margins and fetch events that can be displayed.
177: * @param e event.
178: * @return void.
179: */
180: // AWT thread
181: // AWT thread << sync_value_per_vinterval
182: public void componentResized(final ComponentEvent e) {
183: synchronized (sync_value_per_vinterval) {
184: newBackingElts();
185: setMargin();
186: updateValues();
187: }
188: }
189:
190: /**
191: * Called when the component appears first.
192: * @param e event.
193: * @return void.
194: */
195: // AWT thread
196: public void componentShown(final ComponentEvent e) {
197: componentResized(e);
198: }
199:
200: /**
201: * Called whenthe window is activated.
202: * @param e event.
203: * @return void.
204: */
205: public void windowActivated(final WindowEvent e) {
206: }
207:
208: /**
209: * Called whenthe window is closed.
210: * @param e event.
211: * @return void.
212: */
213: public void windowClosed(final WindowEvent e) {
214: }
215:
216: /**
217: * Called when the window is closing.
218: * @param e event.
219: * @return void.
220: */
221: // AWT thread
222: public abstract void windowClosing(final WindowEvent e);
223:
224: /**
225: * Called whenthe window is deactivated.
226: * @param e event.
227: * @return void.
228: */
229: public void windowDeactivated(final WindowEvent e) {
230: }
231:
232: /**
233: * Called whenthe window is deiconified.
234: * @param e event.
235: * @return void.
236: */
237: public void windowDeiconified(final WindowEvent e) {
238: }
239:
240: /**
241: * Called whenthe window is iconified.
242: * @param e event.
243: * @return void.
244: */
245: public void windowIconified(final WindowEvent e) {
246: }
247:
248: /**
249: * Called whenthe window is opened.
250: * @param e event.
251: * @return void.
252: */
253: public void windowOpened(final WindowEvent e) {
254: }
255:
256: /**
257: * Computes new margins.
258: * @param none.
259: * @return void.
260: */
261: // AWT thread
262: // AWT thread << sync_value_per_vinterval
263: private void setMargin() {
264: final String left_values_str = ""
265: + (int) (value_per_vinterval * (1 + (dimension.height
266: - axis_margin_top - axis_margin_bottom)
267: / pixels_per_vinterval));
268: final TextLayout layout = new TextLayout(left_values_str,
269: backing_g.getFont(), backing_g.getFontRenderContext());
270: final Rectangle2D bounds = layout.getBounds();
271: axis_margin_left = (int) bounds.getWidth() + 5 + 10;
272: }
273:
274: /**
275: * Creates a backing store.
276: * @param none.
277: * @return void.
278: */
279: // AWT thread
280: // AWT thread << sync_value_per_vinterval
281: private void newBackingElts() {
282: dimension = getSize();
283: backing_store = createImage(dimension.width, dimension.height);
284: backing_g = (Graphics2D) backing_store.getGraphics();
285: backing_g.setBackground(Color.BLACK);
286: backing_g.setColor(Color.WHITE);
287: backing_g.setRenderingHint(
288: RenderingHints.KEY_TEXT_ANTIALIASING,
289: RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
290: backing_g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
291: RenderingHints.VALUE_ANTIALIAS_ON);
292: }
293:
294: /**
295: * Removes events that can not be displayed.
296: * @param none.
297: * @return void.
298: *
299: */
300: // Queue thread
301: private void removeOldEvents() {
302: final Date begin = new Date(
303: System.currentTimeMillis()
304: - getDelayPerInterval()
305: * (dimension.width - axis_margin_left - axis_margin_right)
306: / pixels_per_interval);
307: while (events.size() >= 2
308: && (events.get(1).getDate().before(begin) || events
309: .get(1).getDate().equals(begin)))
310: events.remove(0);
311: }
312:
313: /**
314: * Updates the vertical scale.
315: * @param none.
316: * @return void.
317: */
318: // Queue & AWT thread
319: protected void updateVerticalScale() {
320: final int previous_value_per_vinterval = value_per_vinterval;
321:
322: int max_value = 0;
323: int i = 0;
324: for (final EventGeneric event : events) {
325: final int value = event.getIntValue(events, i++);
326: if (value > max_value)
327: max_value = value;
328: }
329:
330: int nintervals = (dimension.height - axis_margin_top - axis_margin_bottom)
331: / pixels_per_vinterval - 1;
332: if (nintervals < 1)
333: nintervals = 1;
334: value_per_vinterval = max_value / nintervals;
335:
336: // possible values for value_per_vinterval: 1, 2, 5, 10, 20, 25, 50, 100, 200, etc.
337: final int base = (int) Math.pow(10, new Integer(
338: value_per_vinterval).toString().length() - 1);
339: if (value_per_vinterval != base)
340: if (value_per_vinterval <= 2 * base)
341: value_per_vinterval = 2 * base;
342: else if (base >= 10
343: && value_per_vinterval <= 25 * base / 10)
344: value_per_vinterval = 25 * base / 10;
345: else if (value_per_vinterval <= 5 * base)
346: value_per_vinterval = 5 * base;
347: else
348: value_per_vinterval = 10 * base;
349:
350: if (value_per_vinterval == 0)
351: value_per_vinterval = 1;
352:
353: if (previous_value_per_vinterval != value_per_vinterval)
354: setMargin();
355: }
356:
357: /**
358: * Takes a new event into account.
359: * @param event new event.
360: * @return void.
361: */
362: // Queue thread
363: // any thread ( << sync_value_per_vinterval ) << sync_value_per_vinterval << sync_update << events
364: public void updateValues(final EventGeneric event) {
365: synchronized (sync_value_per_vinterval) {
366: synchronized (sync_update) {
367: synchronized (events) {
368: for (int i = events.size() - 1; i >= 0; i--)
369: if (events.get(i).getDate().before(
370: event.getDate())) {
371: if (i + 1 < events.size())
372: events.add(i + 1, event);
373: else
374: events.add(event);
375: removeOldEvents();
376: updateVerticalScale();
377: return;
378: }
379: events.add(0, event);
380: removeOldEvents();
381: updateVerticalScale();
382: }
383: }
384: }
385: }
386:
387: /**
388: * Fetches events that can be displayed.
389: * @param none.
390: * @return void.
391: */
392: // AWT thread
393: // AWT thread << sync_value_per_vinterval << sync_update << registered_components
394: protected abstract void updateValues();
395:
396: /**
397: * Computes the "frames per second" indicator.
398: * @param none.
399: * @return void.
400: */
401: // AWT thread
402: private void updateFPS() {
403: final long current_time = System.currentTimeMillis();
404: fps = 100 / (int) (current_time - last_paint + 1) + (9 * fps)
405: / 10;
406: last_paint = current_time;
407: }
408:
409: /**
410: * Displays the number of frames per second.
411: * @param fps frames per second to display.
412: * @return void.
413: */
414: // AWT thread
415: private void paintFPS(final int fps) {
416: backing_g.setColor(Color.GRAY);
417: backing_g.drawString(fps + " frames/s", 1, 10);
418: }
419:
420: /**
421: * Formats a time string to be displayed.
422: * @param time time.
423: * @return String time to be displayed.
424: */
425: // AWT thread
426: private String formatTime(final long time) {
427: String str = date_format.format(new Date(time));
428: return str.substring(str.lastIndexOf(' ') + 1);
429: }
430:
431: /**
432: * Paints axis.
433: * @param none.
434: * @return long current time displayed at the axis bottom.
435: */
436: // AWT thread
437: private long paintAxis() {
438: backing_g.setColor(new Color(50, 50, 50));
439: backing_g.fillRect(axis_margin_left, axis_margin_top,
440: dimension.width - axis_margin_left - axis_margin_right
441: + 1, dimension.height - axis_margin_top
442: - axis_margin_bottom + 1);
443:
444: backing_g.setColor(Color.YELLOW);
445: backing_g.drawLine(axis_margin_left, dimension.height
446: - axis_margin_bottom, dimension.width
447: - axis_margin_right, dimension.height
448: - axis_margin_bottom);
449: backing_g
450: .drawLine(axis_margin_left, axis_margin_top,
451: axis_margin_left, dimension.height
452: - axis_margin_bottom);
453:
454: backing_g.setColor(Color.YELLOW.darker());
455: backing_g.drawLine(axis_margin_left + 1, axis_margin_top,
456: dimension.width - axis_margin_right, axis_margin_top);
457: backing_g.drawLine(dimension.width - axis_margin_right,
458: axis_margin_top, dimension.width - axis_margin_right,
459: dimension.height - axis_margin_bottom - 1);
460:
461: int vinterval_pos = dimension.height - axis_margin_bottom
462: - pixels_per_vinterval;
463: backing_g.setColor(Color.YELLOW.darker().darker().darker());
464: while (vinterval_pos + 9 * (pixels_per_vinterval / 10) > axis_margin_top) {
465: int cpt = 10;
466: while (--cpt > 0)
467: if (vinterval_pos + cpt * (pixels_per_vinterval / 10) > axis_margin_top)
468: backing_g.drawLine(axis_margin_left + 1,
469: vinterval_pos + cpt
470: * (pixels_per_vinterval / 10),
471: dimension.width - axis_margin_right - 1,
472: vinterval_pos + cpt
473: * (pixels_per_vinterval / 10));
474: vinterval_pos -= pixels_per_vinterval;
475: }
476:
477: final long now = System.currentTimeMillis();
478:
479: final long time_to_display = now - now % getDelayPerInterval();
480: final int pixels_offset = (pixels_per_interval * (int) (now % getDelayPerInterval()))
481: / (int) getDelayPerInterval();
482: final int last_interval_pos = dimension.width
483: - axis_margin_right - pixels_offset;
484:
485: backing_g.setClip(axis_margin_left, 0, dimension.width
486: - axis_margin_left - axis_margin_right,
487: dimension.height);
488: int current_interval_pos = last_interval_pos
489: + pixels_per_interval;
490: long current_time_to_display = time_to_display
491: + getDelayPerInterval();
492: boolean stop = false;
493: while (stop == false) {
494: backing_g.setColor(Color.YELLOW.darker());
495: backing_g.drawLine(current_interval_pos, axis_margin_top,
496: current_interval_pos, dimension.height
497: - axis_margin_bottom + std_separator);
498:
499: int cpt = 10;
500: backing_g.setColor(Color.YELLOW.darker().darker().darker());
501: while (--cpt > 0)
502: if (current_interval_pos - cpt
503: * (pixels_per_interval / 10) > axis_margin_left)
504: backing_g.drawLine(current_interval_pos - cpt
505: * (pixels_per_interval / 10),
506: axis_margin_top + 1, current_interval_pos
507: - cpt * (pixels_per_interval / 10),
508: dimension.height - axis_margin_bottom - 1);
509:
510: final String current_time_str = formatTime(current_time_to_display);
511: final TextLayout current_layout = new TextLayout(
512: current_time_str, backing_g.getFont(), backing_g
513: .getFontRenderContext());
514: final Rectangle2D current_bounds = current_layout
515: .getBounds();
516: backing_g.setColor(Color.YELLOW.darker());
517: backing_g.drawString(current_time_str, current_interval_pos
518: - (int) (current_bounds.getWidth() / 2),
519: dimension.height - axis_margin_bottom
520: + (int) current_bounds.getHeight() + 2
521: * std_separator);
522: if (current_interval_pos - current_bounds.getWidth() / 2 < axis_margin_left)
523: stop = true;
524: current_interval_pos -= pixels_per_interval;
525: current_time_to_display -= getDelayPerInterval();
526: }
527: backing_g.setClip(null);
528:
529: vinterval_pos = dimension.height - axis_margin_bottom
530: - pixels_per_vinterval;
531: int value = value_per_vinterval;
532: while (vinterval_pos > axis_margin_top) {
533: backing_g.setColor(Color.YELLOW.darker());
534: backing_g.drawLine(axis_margin_left - std_separator,
535: vinterval_pos, dimension.width - axis_margin_right,
536: vinterval_pos);
537: final String value_str = "" + value;
538: final TextLayout current_layout = new TextLayout(value_str,
539: backing_g.getFont(), backing_g
540: .getFontRenderContext());
541: final Rectangle2D current_bounds = current_layout
542: .getBounds();
543: backing_g.setColor(Color.YELLOW.darker());
544: backing_g.drawString(value_str, axis_margin_left
545: - (int) current_bounds.getWidth() - 2
546: * std_separator, vinterval_pos
547: + (int) (current_bounds.getHeight() / 2));
548: vinterval_pos -= pixels_per_vinterval;
549: value += value_per_vinterval;
550: }
551:
552: return now;
553: }
554:
555: /**
556: * Paints the chart.
557: * @param now current time.
558: * @return void.
559: */
560: // AWT thread
561: // AWT thread << sync_value_per_vinterval << events
562: public void paintChart(final long now) {
563: backing_g.setClip(axis_margin_left + 1, axis_margin_top,
564: dimension.width - axis_margin_left - axis_margin_right
565: - 1, dimension.height - axis_margin_top
566: - axis_margin_bottom);
567:
568: synchronized (events) {
569: final int npoints = events.size();
570: final int point_x[] = new int[npoints];
571: final int point_y[] = new int[npoints];
572:
573: final long time_to_display = now - now
574: % getDelayPerInterval();
575: final int pixels_offset = (pixels_per_interval * (int) (now % getDelayPerInterval()))
576: / (int) getDelayPerInterval();
577: final int last_interval_pos = dimension.width
578: - axis_margin_right - pixels_offset;
579:
580: for (int i = 0; i < events.size(); i++) {
581: final EventGeneric event = events.get(i);
582: point_x[i] = last_interval_pos
583: + (int) ((pixels_per_interval * (int) (event
584: .getDate().getTime() - time_to_display)) / getDelayPerInterval());
585:
586: // cast to double to avoid overflow on int that lead to wrong results
587: point_y[i] = (int) (dimension.height
588: - axis_margin_bottom - pixels_per_vinterval
589: * (double) event.getIntValue(events, i)
590: / value_per_vinterval);
591: if (point_y[i] < 0)
592: log.warn("y < 0: y=" + point_y[i]);
593: }
594:
595: backing_g.setColor(Color.GREEN);
596: backing_g.drawPolyline(point_x, point_y, events.size());
597:
598: backing_g.setColor(Color.RED);
599: for (int i = 0; i < events.size(); i++)
600: backing_g
601: .drawRect(point_x[i] - 2, point_y[i] - 2, 4, 4);
602:
603: backing_g.setClip(null);
604: }
605: }
606:
607: /**
608: * Repaints the component using the backing store.
609: * @param g graphics context.
610: * @return void.
611: */
612: // AWT thread
613: public void paint(final Graphics g) {
614: if (backing_store == null)
615: return;
616: backing_g.clearRect(0, 0, dimension.width, dimension.height);
617:
618: updateFPS();
619:
620: final long current_time = System.currentTimeMillis();
621: if (current_time - last_paint_100ms >= 100) {
622: last_paint_100ms = current_time;
623: last_fps_100ms = fps;
624: }
625: paintFPS(last_fps_100ms);
626:
627: synchronized (sync_value_per_vinterval) {
628: final long now = paintAxis();
629: paintChart(now);
630: }
631:
632: g.drawImage(backing_store, 0, 0, this);
633: }
634: }
|