net.fenyo.gnetwatch.GUI.BasicComponent.java Source code

Java tutorial

Introduction

Here is the source code for net.fenyo.gnetwatch.GUI.BasicComponent.java

Source

/*
 * GNetWatch
 * Copyright 2006, 2007, 2008 Alexandre Fenyo
 * gnetwatch@fenyo.net
 *
 * This file is part of GNetWatch.
 *
 * GNetWatch is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * GNetWatch is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GNetWatch; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package net.fenyo.gnetwatch.GUI;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.event.*;
import java.awt.font.TextLayout;
import java.awt.geom.Rectangle2D;
import java.text.DateFormat;
import java.util.Date;

import net.fenyo.gnetwatch.data.EventBytesReceived;
import net.fenyo.gnetwatch.data.EventGeneric;
import net.fenyo.gnetwatch.targets.Target;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * This class implements an AWT component capable of drawing time series with the Java2D API.
 * @author Alexandre Fenyo
 * @version $Id: BasicComponent.java,v 1.39 2008/08/06 16:51:42 fenyo Exp $
 */

public abstract class BasicComponent extends Component
        implements ComponentListener, WindowListener, KeyListener, MouseMotionListener, MouseListener {
    private static Log log = LogFactory.getLog(BasicComponent.class);

    private boolean manual_mode = false;
    private long manual_now = 0;
    private long manual_delay_per_interval = 0;
    private int lastPixelsOffsetValue = 0;

    final private Target target;

    private static Object sync_update = new Object();

    protected java.util.List<EventGeneric> events = null;

    private Image backing_store = null;
    protected Graphics2D backing_g = null;
    protected Dimension dimension = null;

    private final DateFormat date_format = DateFormat.getDateTimeInstance();

    private int fps = 0;
    private long last_paint = System.currentTimeMillis();
    private long last_paint_100ms = System.currentTimeMillis();
    private int last_fps_100ms = 0;

    // horizontal interval
    protected final int pixels_per_interval = 80; // 80 pix/interval ; multiple de 10

    // vertical intervals
    protected int value_per_vinterval = 200;
    private final Object sync_value_per_vinterval = new Object();
    protected final int pixels_per_vinterval = 80; // 80 pix/vinterval ; multiple de 10

    private final static int std_margin = 30;
    private final static int std_separator = 3;

    protected final static int axis_margin_bottom = std_margin;
    protected final static int axis_margin_top = std_margin;
    protected int axis_margin_left = std_margin;
    protected final static int axis_margin_right = std_margin;

    private int drag_x_start = 0;
    private long drag_now_start = 0;

    /**
     * Constructor.
     * @param target target this graphic component works on.
     */
    // GUI thread
    public BasicComponent(final Target target) {
        this.target = target;
    }

    public boolean isManualMode() {
        return manual_mode;
    }

    /**
     * Returns the horizontal scale.
     * @param none.
     * @return long horizontal scale.
     */
    public long getDelayPerInterval() {
        return 5000; // 5 secs/interval
    }

    protected long _getDelayPerInterval() {
        if (!manual_mode)
            return getDelayPerInterval();
        else
            return manual_delay_per_interval;
    }

    /**
     * Sets the list of events to display.
     * @param events events to display.
     * @return void.
     */
    protected void setEvents(final java.util.List<EventGeneric> events) {
        this.events = events;
    }

    /**
     * Returns the dimensions of this component.
     * @param none.
     * @return Dimension dimensions.
     */
    protected Dimension getDimension() {
        return dimension;
    }

    /**
     * Returns the associated target.
     * @param none.
     * @return Target target.
     */
    protected Target getTarget() {
        return target;
    }

    /**
     * Initialize the component and ask AWT to receive events.
     *
     */
    // GUI thread
    // lock survey: synchro << sync_tree << HERE
    public void init() {
        setPreferredSize(new Dimension(700, 400));
        addComponentListener(this);
        setFocusable(true);
        addKeyListener(this);
        addMouseMotionListener(this);
        addMouseListener(this);
    }

    /**
     * Called when the component is hidden.
     * @param e event.
     * @return void.
     */
    public void componentHidden(final ComponentEvent e) {
    }

    /**
     * Called when the component is moved.
     * @param e event.
     * @return void.
     */
    public void componentMoved(final ComponentEvent e) {
    }

    /**
     * When the component is resized, creates a new backing store,
     * reset margins and fetch events that can be displayed.
     * @param e event.
     * @return void.
     */
    // AWT thread
    // lock survey: THIS
    public void componentResized(final ComponentEvent e) {
        synchronized (sync_value_per_vinterval) {
            newBackingElts();
            setMargin();
        }
        updateValues();
    }

    /**
     * Called when the component appears first.
     * @param e event.
     * @return void.
     */
    // AWT thread
    public void componentShown(final ComponentEvent e) {
        componentResized(e);
    }

    /**
     * Called whenthe window is activated.
     * @param e event.
     * @return void.
     */
    public void windowActivated(final WindowEvent e) {
    }

    /**
     * Called whenthe window is closed.
     * @param e event.
     * @return void.
     */
    public void windowClosed(final WindowEvent e) {
    }

    /**
     * Called when the window is closing.
     * @param e event.
     * @return void.
     */
    // AWT thread
    public abstract void windowClosing(final WindowEvent e);

    /**
     * Called whenthe window is deactivated.
     * @param e event.
     * @return void.
     */
    public void windowDeactivated(final WindowEvent e) {
    }

    /**
     * Called whenthe window is deiconified.
     * @param e event.
     * @return void.
     */
    public void windowDeiconified(final WindowEvent e) {
    }

    /**
     * Called whenthe window is iconified.
     * @param e event.
     * @return void.
     */
    public void windowIconified(final WindowEvent e) {
    }

    /**
     * Called whenthe window is opened.
     * @param e event.
     * @return void.
     */
    public void windowOpened(final WindowEvent e) {
    }

    /**
     * Computes new margins.
     * @param none.
     * @return void.
     */
    // AWT thread
    // lock survey: sync_update << sync_value_per_vinterval << HERE
    //              synchro << sync_tree << sync_update << sync_value_per_vinterval << events << HERE
    //              sync_value_per_vinterval << HERE
    private void setMargin() {
        /*
        final String left_values_str = "" + (int) (value_per_vinterval *
            (1 + (dimension.height - axis_margin_top - axis_margin_bottom) / pixels_per_vinterval));
        */
        final String left_values_str = "1000M";
        final TextLayout layout = new TextLayout(left_values_str, backing_g.getFont(),
                backing_g.getFontRenderContext());
        final Rectangle2D bounds = layout.getBounds();
        axis_margin_left = (int) bounds.getWidth() + 5 + 10;
    }

    /**
     * Creates a backing store.
     * @param none.
     * @return void.
     */
    private void newBackingElts() {
        dimension = getSize();
        backing_store = createImage(dimension.width, dimension.height);
        backing_g = (Graphics2D) backing_store.getGraphics();
        backing_g.setBackground(Color.BLACK);
        backing_g.setColor(Color.WHITE);
        backing_g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        backing_g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    }

    /**
     * Removes events that can not be displayed.
     * @param none.
     * @return void.
     *
     */
    // lock survey: synchro << sync_tree << sync_update << sync_value_per_vinterval << events << HERE  
    private void removeOldEvents() {
        final Date begin = new Date(System.currentTimeMillis() - _getDelayPerInterval()
                * (dimension.width - axis_margin_left - axis_margin_right) / pixels_per_interval);
        while (events.size() >= 2
                && (events.get(1).getDate().before(begin) || events.get(1).getDate().equals(begin)))
            events.remove(0);
    }

    protected int getIntValue(final EventGeneric event) {
        return event.getIntValue();
    }

    /**
     * Updates the vertical scale.
     * @param none.
     * @return void.
     */
    // Queue & AWT thread
    // lock survey: sync_update << sync_value_per_vinterval << HERE
    // lock survey: synchro << sync_tree << sync_update << sync_value_per_vinterval << events << HERE  
    protected void updateVerticalScale() {
        if (manual_mode)
            return;

        final int previous_value_per_vinterval = value_per_vinterval;

        int max_value = 0;
        for (final EventGeneric event : events) {
            final int value = getIntValue(event);
            if (value > max_value)
                max_value = value;
        }

        int nintervals = (dimension.height - axis_margin_top - axis_margin_bottom) / pixels_per_vinterval - 1;
        if (nintervals < 1)
            nintervals = 1;
        value_per_vinterval = max_value / nintervals;

        // possible values for value_per_vinterval: 1, 2, 5, 10, 20, 25, 50, 100, 200, etc.
        final int base = (int) Math.pow(10, new Integer(value_per_vinterval).toString().length() - 1);
        if (value_per_vinterval != base)
            if (value_per_vinterval <= 2 * base)
                value_per_vinterval = 2 * base;
            else if (base >= 10 && value_per_vinterval <= 25 * base / 10)
                value_per_vinterval = 25 * base / 10;
            else if (value_per_vinterval <= 5 * base)
                value_per_vinterval = 5 * base;
            else
                value_per_vinterval = 10 * base;

        if (value_per_vinterval == 0)
            value_per_vinterval = 1;

        if (previous_value_per_vinterval != value_per_vinterval)
            setMargin();
    }

    /**
     * Takes a new event into account.
     * @param event new event.
     * @return void.
     */
    // Queue thread
    // lock survey: synchro << sync_tree << HERE  
    public void updateValues(final EventGeneric event) {
        synchronized (sync_update) { // hypothse pour autres pbs synchronized : sync_value_per_vinterval cre un pb de verrou
            synchronized (sync_value_per_vinterval) {
                synchronized (events) {
                    // lock survey: synchro << sync_tree << sync_update << sync_value_per_vinterval << events << HERE --XX  
                    if (manual_mode && events.size() > 0) {
                        if (event.getDate().after(new Date(manual_now))
                                && events.get(events.size() - 1).getDate().after(new Date(manual_now)))
                            return;

                        final long begin = manual_now - _getDelayPerInterval()
                                * (getDimension().width - axis_margin_left - axis_margin_right)
                                / pixels_per_interval;
                        if (event.getDate().before(new Date(begin)))
                            return;
                    }

                    for (int i = events.size() - 1; i >= 0; i--)
                        if (events.get(i).getDate().before(event.getDate())) {
                            if (i + 1 < events.size())
                                events.add(i + 1, event);
                            else
                                events.add(event);
                            if (!manual_mode)
                                removeOldEvents();
                            updateVerticalScale();
                            return;
                        }

                    events.add(0, event);
                    if (!manual_mode)
                        removeOldEvents();
                    updateVerticalScale();
                }
            }
        }
    }

    protected abstract Class getEventClass();

    /**
     * Fetches events that can be displayed.
     * @param none.
     * @return void.
     */
    // AWT thread
    // lock survey: HERE
    protected void updateValues() {
        synchronized (getTarget().getGUI().getSynchro()) {
            synchronized (getTarget().getGUI().sync_tree) {
                synchronized (sync_update) {
                    synchronized (sync_value_per_vinterval) {
                        getTarget().registerComponent(this, getEventClass());
                        final long end;
                        if (!manual_mode)
                            end = System.currentTimeMillis();
                        else
                            end = manual_now;
                        final long begin = end - _getDelayPerInterval()
                                * (getDimension().width - axis_margin_left - axis_margin_right)
                                / pixels_per_interval;
                        setEvents(getTarget().getEvents(new Date(begin), new Date(end), getEventClass()));
                    }
                    updateVerticalScale();
                    repaint();
                }
            }
        }
    }

    /**
     * Computes the "frames per second" indicator.
     * @param none.
     * @return void.
     */
    // AWT thread
    private void updateFPS() {
        final long current_time = System.currentTimeMillis();
        fps = 100 / (int) (current_time - last_paint + 1) + (9 * fps) / 10;
        last_paint = current_time;
    }

    /**
     * Displays the number of frames per second.
     * @param fps frames per second to display.
     * @return void.
     */
    // AWT thread
    private void paintFPS(final int fps) {
        backing_g.setColor(Color.GRAY);
        if (!manual_mode) {
            //  traduire
            backing_g.drawString(fps + " frames/s - press any key or mouse button to enter manual scaling mode", 1,
                    13);
            backing_g.setColor(Color.YELLOW);
            backing_g.drawString("AUTO", 60, 50);
        } else {
            //  traduire
            backing_g.drawString(
                    "keys: '<' horiz. scale down  '>' horiz. scale up  '-' vert. scale down  '+' vert. scale up  'n' current date  'a' automatic scaling mode",
                    1, 13);
            backing_g.setColor(Color.YELLOW);
            backing_g.drawString("MANUAL", 60, 50);
        }
    }

    /**
     * Formats a time string to be displayed.
     * @param time time.
     * @return String time to be displayed.
     */
    // AWT thread
    private String formatTime(final long time) {
        String str = date_format.format(new Date(time));
        return str.substring(str.lastIndexOf(' ') + 1);
    }

    private String formatDate(final long time) {
        String str = date_format.format(new Date(time));
        return str.substring(0, str.lastIndexOf(' '));
    }

    public boolean pixelsOffsetChanged() {
        final int new_val = (pixels_per_interval * (int) (System.currentTimeMillis() % _getDelayPerInterval()))
                / (int) _getDelayPerInterval();
        if (lastPixelsOffsetValue == new_val)
            return false;
        lastPixelsOffsetValue = new_val;
        return true;
    }

    /**
     * Paints axis.
     * @param none.
     * @return long current time displayed at the axis bottom.
     */
    // AWT thread
    private long paintAxis() {
        backing_g.setColor(new Color(50, 50, 50));
        backing_g.fillRect(axis_margin_left, axis_margin_top,
                dimension.width - axis_margin_left - axis_margin_right + 1,
                dimension.height - axis_margin_top - axis_margin_bottom + 1);

        backing_g.setColor(Color.YELLOW);
        backing_g.drawLine(axis_margin_left, dimension.height - axis_margin_bottom,
                dimension.width - axis_margin_right, dimension.height - axis_margin_bottom);
        backing_g.drawLine(axis_margin_left, axis_margin_top, axis_margin_left,
                dimension.height - axis_margin_bottom);

        backing_g.setColor(Color.YELLOW.darker());
        backing_g.drawLine(axis_margin_left + 1, axis_margin_top, dimension.width - axis_margin_right,
                axis_margin_top);
        backing_g.drawLine(dimension.width - axis_margin_right, axis_margin_top,
                dimension.width - axis_margin_right, dimension.height - axis_margin_bottom - 1);

        int vinterval_pos = dimension.height - axis_margin_bottom - pixels_per_vinterval;
        backing_g.setColor(Color.YELLOW.darker().darker().darker());
        while (vinterval_pos + 9 * (pixels_per_vinterval / 10) > axis_margin_top) {
            int cpt = 10;
            while (--cpt > 0)
                if (vinterval_pos + cpt * (pixels_per_vinterval / 10) > axis_margin_top)
                    backing_g.drawLine(axis_margin_left + 1, vinterval_pos + cpt * (pixels_per_vinterval / 10),
                            dimension.width - axis_margin_right - 1,
                            vinterval_pos + cpt * (pixels_per_vinterval / 10));
            vinterval_pos -= pixels_per_vinterval;
        }

        final long now;
        if (manual_mode)
            now = manual_now;
        else
            now = System.currentTimeMillis();

        final long time_to_display = now - now % _getDelayPerInterval();
        final int pixels_offset = (pixels_per_interval * (int) (now % _getDelayPerInterval()))
                / (int) _getDelayPerInterval();
        final int last_interval_pos = dimension.width - axis_margin_right - pixels_offset;

        backing_g.setClip(axis_margin_left, 0, dimension.width - axis_margin_left - axis_margin_right,
                dimension.height);
        int current_interval_pos = last_interval_pos + pixels_per_interval;
        long current_time_to_display = time_to_display + _getDelayPerInterval();
        boolean stop = false;
        while (stop == false) {
            backing_g.setColor(Color.YELLOW.darker());
            backing_g.drawLine(current_interval_pos, axis_margin_top, current_interval_pos,
                    dimension.height - axis_margin_bottom + std_separator);

            int cpt = 10;
            backing_g.setColor(Color.YELLOW.darker().darker().darker());
            while (--cpt > 0)
                if (current_interval_pos - cpt * (pixels_per_interval / 10) > axis_margin_left)
                    backing_g.drawLine(current_interval_pos - cpt * (pixels_per_interval / 10), axis_margin_top + 1,
                            current_interval_pos - cpt * (pixels_per_interval / 10),
                            dimension.height - axis_margin_bottom - 1);

            final String current_time_str = formatTime(current_time_to_display);
            final String current_date_str = formatDate(current_time_to_display);
            final TextLayout current_layout = new TextLayout(current_time_str, backing_g.getFont(),
                    backing_g.getFontRenderContext());
            final TextLayout current_layout_date = new TextLayout(current_date_str, backing_g.getFont(),
                    backing_g.getFontRenderContext());
            final Rectangle2D current_bounds = current_layout.getBounds();
            final Rectangle2D current_bounds_date = current_layout_date.getBounds();
            backing_g.setColor(Color.YELLOW.darker());
            backing_g.drawString(current_time_str, current_interval_pos - (int) (current_bounds.getWidth() / 2),
                    dimension.height - axis_margin_bottom + (int) current_bounds.getHeight() + 2 * std_separator);
            backing_g.setColor(Color.YELLOW.darker().darker());
            backing_g.drawString(current_date_str,
                    current_interval_pos - (int) (current_bounds_date.getWidth() / 2),
                    3 + ((int) current_bounds.getHeight()) + dimension.height - axis_margin_bottom
                            + (int) current_bounds.getHeight() + 2 * std_separator);
            if (current_interval_pos - current_bounds.getWidth() / 2 < axis_margin_left)
                stop = true;
            current_interval_pos -= pixels_per_interval;
            current_time_to_display -= _getDelayPerInterval();
        }
        backing_g.setClip(null);

        vinterval_pos = dimension.height - axis_margin_bottom - pixels_per_vinterval;
        int value = value_per_vinterval;
        while (vinterval_pos > axis_margin_top) {
            backing_g.setColor(Color.YELLOW.darker());
            backing_g.drawLine(axis_margin_left - std_separator, vinterval_pos, dimension.width - axis_margin_right,
                    vinterval_pos);
            final String value_str;
            if (value >= 1000000)
                value_str = "" + value / 1000000 + "M";
            else if (value >= 1000)
                value_str = "" + value / 1000 + "k";
            else
                value_str = "" + value;
            final TextLayout current_layout = new TextLayout(value_str, backing_g.getFont(),
                    backing_g.getFontRenderContext());
            final Rectangle2D current_bounds = current_layout.getBounds();
            backing_g.setColor(Color.YELLOW.darker());
            backing_g.drawString(value_str, axis_margin_left - (int) current_bounds.getWidth() - 2 * std_separator,
                    vinterval_pos + (int) (current_bounds.getHeight() / 2));
            vinterval_pos -= pixels_per_vinterval;
            value += value_per_vinterval;
        }

        return now;
    }

    /**
     * Paints the chart.
     * @param now current time.
     * @return void.
     */
    // AWT thread
    // AWT thread << sync_value_per_vinterval << events
    public void paintChart(final long now) {
        backing_g.setClip(axis_margin_left + 1, axis_margin_top,
                dimension.width - axis_margin_left - axis_margin_right - 1,
                dimension.height - axis_margin_top - axis_margin_bottom);

        synchronized (events) {
            final int npoints = events.size();
            final int point_x[] = new int[npoints];
            final int point_y[] = new int[npoints];

            final long time_to_display = now - now % _getDelayPerInterval();
            final int pixels_offset = (pixels_per_interval * (int) (now % _getDelayPerInterval()))
                    / (int) _getDelayPerInterval();
            final int last_interval_pos = dimension.width - axis_margin_right - pixels_offset;

            for (int i = 0; i < events.size(); i++) {
                final EventGeneric event = events.get(i);
                final long xpos = ((long) last_interval_pos)
                        + (((long) pixels_per_interval) * (event.getDate().getTime() - time_to_display))
                                / _getDelayPerInterval();
                if (xpos < -1000)
                    point_x[i] = -1000;
                else if (xpos > 1000 + dimension.width)
                    point_x[i] = 1000 + dimension.width;
                else
                    point_x[i] = (int) xpos;

                // cast to double to avoid overflow on int that lead to wrong results
                point_y[i] = (int) (dimension.height - axis_margin_bottom
                        - pixels_per_vinterval * (double) getIntValue(event) / value_per_vinterval);
                //        if (point_y[i] < 0) log.warn("y < 0: y=" + point_y[i]);
            }

            backing_g.setColor(Color.GREEN);
            backing_g.drawPolyline(point_x, point_y, events.size());

            backing_g.setColor(Color.RED);
            for (int i = 0; i < events.size(); i++)
                backing_g.drawRect(point_x[i] - 2, point_y[i] - 2, 4, 4);

            backing_g.setClip(null);
        }
    }

    /**
     * Repaints the component using the backing store.
     * @param g graphics context.
     * @return void.
     */
    // AWT thread
    public void paint(final Graphics g) {
        if (backing_store == null)
            return;
        backing_g.clearRect(0, 0, dimension.width, dimension.height);

        updateFPS();

        final long current_time = System.currentTimeMillis();
        if (current_time - last_paint_100ms >= 100) {
            last_paint_100ms = current_time;
            last_fps_100ms = fps;
        }

        synchronized (sync_value_per_vinterval) {
            final long now = paintAxis();
            paintChart(now);
        }

        paintFPS(last_fps_100ms);

        g.drawImage(backing_store, 0, 0, this);
    }

    public void keyPressed(final KeyEvent e) {
    }

    public void keyReleased(final KeyEvent e) {
    }

    public void keyTyped(final KeyEvent e) {
        if (!manual_mode) {
            // automatic mode
            manual_now = System.currentTimeMillis();
            manual_delay_per_interval = getDelayPerInterval();
            manual_mode = true;

        } else {
            // manual mode

            if (e.getKeyChar() == 'a') {
                manual_mode = false;
                updateValues();
            }

            if (e.getKeyChar() == 'n') {
                manual_now = System.currentTimeMillis();
                updateValues();
            }

            if (e.getKeyChar() == '<') {
                manual_delay_per_interval = manual_delay_per_interval * 2;
                updateValues();
            }

            if (e.getKeyChar() == '>') {
                manual_delay_per_interval = manual_delay_per_interval / 2;
                updateValues();
            }

            if (e.getKeyChar() == '+') {
                synchronized (sync_value_per_vinterval) {
                    value_per_vinterval = value_per_vinterval / 2;
                }
                repaint();
            }

            if (e.getKeyChar() == '-') {
                synchronized (sync_value_per_vinterval) {
                    value_per_vinterval = value_per_vinterval * 2;
                }
                repaint();
            }
        }
    }

    public void mouseClicked(MouseEvent e) {
    }

    public void mouseEntered(MouseEvent e) {
    }

    public void mouseExited(MouseEvent e) {
    }

    public void mousePressed(MouseEvent e) {
        if (!manual_mode) {
            manual_now = System.currentTimeMillis();
            manual_delay_per_interval = getDelayPerInterval();
            manual_mode = true;
        }

        drag_x_start = e.getX();
        drag_now_start = manual_now;
    }

    public void mouseReleased(MouseEvent e) {
        updateValues();
    }

    public void mouseDragged(final MouseEvent e) {
        if (manual_mode) {
            manual_now = drag_now_start
                    + manual_delay_per_interval * (drag_x_start - e.getX()) / pixels_per_interval;
        }
        repaint();
    }

    public void mouseMoved(final MouseEvent e) {
    }
}