org.griphyn.vdl.karajan.monitor.monitors.swing.GraphPanel.java Source code

Java tutorial

Introduction

Here is the source code for org.griphyn.vdl.karajan.monitor.monitors.swing.GraphPanel.java

Source

/*
 * Swift Parallel Scripting Language (http://swift-lang.org)
 * Code from Java CoG Kit Project (see notice below) with modifications.
 *
 * Copyright 2005-2014 University of Chicago
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//----------------------------------------------------------------------
//This code is developed as part of the Java CoG Kit project
//The terms of the license can be found at http://www.cogkit.org/license
//This message may not be removed or altered.
//----------------------------------------------------------------------

/*
 * Created on Jul 24, 2013
 */
package org.griphyn.vdl.karajan.monitor.monitors.swing;

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimerTask;
import java.util.prefs.Preferences;

import javax.swing.JButton;
import javax.swing.JColorChooser;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JToolTip;
import javax.swing.UIManager;

import org.griphyn.vdl.karajan.monitor.SystemState;
import org.griphyn.vdl.karajan.monitor.Unit;
import org.griphyn.vdl.karajan.monitor.common.DataSampler;
import org.griphyn.vdl.karajan.monitor.common.DataSampler.Series;
import org.griphyn.vdl.karajan.monitor.common.GlobalTimer;
import org.griphyn.vdl.karajan.monitor.items.StatefulItemClass;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartMouseEvent;
import org.jfree.chart.ChartMouseListener;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.NumberTickUnit;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.event.ChartChangeEvent;
import org.jfree.chart.event.ChartChangeEventType;
import org.jfree.chart.event.ChartChangeListener;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.Range;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.xy.XYDataset;

public class GraphPanel extends JPanel implements ChartMouseListener {
    private static final Stroke DEFAULT_STROKE = new BasicStroke(1.0f);
    private static final int TOOLTIP_DISPLAY_DELAY = 1000;

    public static final DateFormat DATE = DateFormat.getDateInstance();
    public static final DateFormat TIME = DateFormat.getTimeInstance();

    private SystemState state;
    private DataSampler sampler;
    private JFreeChart chart;
    private final List<String> enabled;
    private final Map<Unit, Integer> datasetMapping;
    private final Map<Unit, List<String>> seriesMapping;
    private JPanel legend;
    private GraphsPanel gp;
    private ChartPanel cp;
    private ColorPalette palette;
    private TimerTask tooltipTimerTask;
    private JToolTip toolTip;
    private double toolTipValue;
    private int maxRange;

    public GraphPanel(SystemState state, GraphsPanel gp) {
        this.state = state;
        sampler = (DataSampler) state.getItemByID(DataSampler.ID, StatefulItemClass.WORKFLOW);
        this.gp = gp;
        this.setLayout(new BorderLayout());
        enabled = new ArrayList<String>();
        datasetMapping = new HashMap<Unit, Integer>();
        seriesMapping = new HashMap<Unit, List<String>>();
        palette = ColorPalette.newDefaultPalette();
        createChart();
    }

    private void createChart() {
        chart = ChartFactory.createTimeSeriesChart(null, "Time", null, null, false, false, false);
        cp = new ChartPanel(chart);
        // avoid stretching fonts and such
        cp.setMaximumDrawWidth(Integer.MAX_VALUE);
        cp.setMaximumDrawHeight(Integer.MAX_VALUE);
        XYPlot plot = chart.getXYPlot();
        plot.setBackgroundPaint(UIManager.getColor("TextField.background"));
        plot.setDomainCrosshairVisible(true);
        plot.setDomainCrosshairLockedOnData(false);
        cp.addChartMouseListener(this);
        cp.setLayout(new DummyLayoutManager());
        cp.add(toolTip = new JToolTip());
        toolTip.setVisible(false);

        cp.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseExited(MouseEvent e) {
                disableToolTip();
            }
        });
        chart.addChangeListener(new ChartChangeListener() {
            @Override
            public void chartChanged(ChartChangeEvent e) {
                if (e.getType() == ChartChangeEventType.DATASET_UPDATED) {
                    updateMaxRange();
                    updateToolTipLocation();
                }
            }
        });

        for (String s : enabled) {
            addSeries(sampler.getSeries(s));
        }

        JPanel p = new JPanel();
        p.setLayout(new BorderLayout());
        p.add(cp, BorderLayout.CENTER);
        legend = new JPanel();
        legend.setLayout(new FlowLayout());

        rebuildLegend();

        this.add(p, BorderLayout.CENTER);
        this.add(legend, BorderLayout.SOUTH);
    }

    @Override
    public void chartMouseClicked(ChartMouseEvent e) {
        if (isPointWithinChartArea(e.getTrigger().getX(), e.getTrigger().getY())) {
            enableTooltip(valueFromPosition(e));
        }
    }

    private double valueFromPosition(ChartMouseEvent e) {
        Rectangle2D chartArea = cp.getScreenDataArea();
        XYPlot plot = chart.getXYPlot();
        return plot.getDomainAxis().java2DToValue(e.getTrigger().getX(), chartArea, plot.getDomainAxisEdge());
    }

    @Override
    public void chartMouseMoved(ChartMouseEvent e) {
        disableToolTip();

        if (isPointWithinChartArea(e.getTrigger().getX(), e.getTrigger().getY())) {
            chart.getXYPlot().setDomainCrosshairVisible(true);
            double vx = valueFromPosition(e);
            chart.getXYPlot().setDomainCrosshairValue(vx);
            scheduleTooltipDisplay(vx);
        } else {
            chart.getXYPlot().setDomainCrosshairVisible(false);
            synchronized (this) {
                if (tooltipTimerTask != null) {
                    tooltipTimerTask.cancel();
                }
            }
        }
    }

    private boolean isPointWithinChartArea(int x, int y) {
        Rectangle2D chartArea = cp.getScreenDataArea();
        return chartArea.contains(x, y);
    }

    private synchronized void scheduleTooltipDisplay(final double x) {
        if (tooltipTimerTask != null) {
            tooltipTimerTask.cancel();
        }
        tooltipTimerTask = new TimerTask() {
            @Override
            public void run() {
                tooltipTimerTask = null;
                enableTooltip(x);
            }
        };
        try {
            GlobalTimer.getTimer().schedule(tooltipTimerTask, TOOLTIP_DISPLAY_DELAY);
        } catch (IllegalStateException e) {
            System.err.println(this + ": " + e.getMessage());
        }
    }

    protected synchronized void disableToolTip() {
        toolTip.setVisible(false);
    }

    protected synchronized void enableTooltip(double x) {
        toolTip.setTipText(getTooltipText(x));
        toolTip.setSize(toolTip.getPreferredSize());
        toolTip.setVisible(true);
        this.toolTipValue = x;

        updateToolTipLocation();
    }

    private String getTooltipText(double x) {
        StringBuilder sb = new StringBuilder();
        sb.append("<html><b>Date:</b> ");
        sb.append(DATE.format(new Date((long) x)));
        sb.append("<br>");
        sb.append("<b>Time:</b>s ");
        sb.append(TIME.format(new Date((long) x)));
        sb.append("<br>");

        XYPlot plot = chart.getXYPlot();

        for (int i = 0; i < enabled.size(); i++) {
            String key = enabled.get(i);
            Unit unit = sampler.getSeries(key).getUnit();
            int di = getDatasetIndex(unit);
            int si = getSeriesIndex(unit, key);
            Color color = (Color) chart.getXYPlot().getRenderer(di).getSeriesPaint(si);

            double val = binSearch(plot.getDataset(di), si, x);
            sb.append("<b style='color: ");
            sb.append(colorToHTML(color));
            sb.append("'>");
            sb.append(sampler.getSeries(key).getLabel());
            sb.append(":</b> ");
            sb.append(unit.format(val));
            sb.append("<br>");
        }
        sb.append("</html>");
        return sb.toString();
    }

    private Object colorToHTML(Color color) {
        return "#" + String.format("%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue());
    }

    private double binSearch(XYDataset dataset, int si, double x) {
        return binSearch(dataset, si, x, 0, dataset.getItemCount(si) - 1);
    }

    private double binSearch(XYDataset d, int si, double x, int begin, int end) {
        if (begin >= end) {
            return d.getYValue(si, end);
        }
        int mid = (begin + end) / 2;
        double vmid = d.getXValue(si, mid);
        if (x == vmid) {
            return d.getYValue(si, mid);
        } else if (x < vmid) {
            return binSearch(d, si, x, begin, mid - 1);
        } else {
            return binSearch(d, si, x, mid + 1, end);
        }
    }

    private int getSeriesIndex(Unit unit, String key) {
        List<String> series = seriesMapping.get(unit);
        for (int i = 0; i < series.size(); i++) {
            if (key.equals(series.get(i))) {
                return i;
            }
        }
        return 0;
    }

    private int getDatasetIndex(Unit unit) {
        return datasetMapping.get(unit);
    }

    private void updateToolTipLocation() {
        if (!toolTip.isVisible()) {
            return;
        }
        XYPlot plot = chart.getXYPlot();
        Rectangle2D chartArea = cp.getScreenDataArea();
        if (chartArea.getWidth() == 0) {
            // no idea why that happens
            return;
        }
        double vx = plot.getDomainAxis().valueToJava2D(this.toolTipValue, chartArea, plot.getDomainAxisEdge());
        if (vx + toolTip.getWidth() > cp.getScreenDataArea().getMaxX()) {
            toolTip.setLocation((int) vx - toolTip.getWidth(), 10);
        } else {
            toolTip.setLocation((int) vx, 10);
        }
    }

    private void addSeries(Series<?> series) {
        Unit unit = series.getUnit();
        XYPlot plot = chart.getXYPlot();
        Integer datasetIndex = datasetMapping.get(unit);
        TimeSeriesCollection col;
        if (datasetIndex == null) {
            col = new TimeSeriesCollection();
            int nextIndex = getNextDatasetIndex(plot);
            datasetMapping.put(unit, nextIndex);
            plot.setDataset(nextIndex, col);
            plot.setRenderer(nextIndex, new XYLineAndShapeRenderer(true, false));

            NumberAxis axis = new AutoNumberAxis(unit);
            plot.setRangeAxis(nextIndex, axis);
            plot.mapDatasetToRangeAxis(nextIndex, nextIndex);

            seriesMapping.put(unit, new ArrayList<String>());
        } else {
            col = (TimeSeriesCollection) plot.getDataset(datasetIndex);
        }
        TimeSeries ts = new SeriesWrapper(series, sampler);
        seriesMapping.get(unit).add(series.getKey());
        col.addSeries(ts);
        setColor(series.getKey(), palette.allocate());
    }

    private int getNextDatasetIndex(XYPlot plot) {
        for (int i = 0; i < plot.getDatasetCount(); i++) {
            if (plot.getDataset(i) == null) {
                return i;
            }
        }
        return plot.getDatasetCount();
    }

    private void rebuildLegend() {
        legend.removeAll();
        for (int i = 0; i < enabled.size(); i++) {
            makeLegendEntry(legend, chart, sampler.getSeries(enabled.get(i)).getLabel(), i);
        }
        legend.add(new JLabel());
        legend.add(new JLabel());
        JButton newb = IconLoader.makeImageButton("gui/icons/plus.png", "New...");

        newb.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                displayAddPopup((JButton) e.getSource());
            }
        });

        legend.add(newb);
    }

    protected void displayAddPopup(JButton src) {
        JPopupMenu p = new JPopupMenu();

        Map<String, List<Series<?>>> series = sampler.getCategories();

        for (Map.Entry<String, List<Series<?>>> e1 : series.entrySet()) {
            JMenu sm = new JMenu(e1.getKey());
            for (final Series<?> s : e1.getValue()) {
                if (!enabled.contains(s.getKey())) {
                    JMenuItem mi = new JMenuItem(s.getLabel());

                    mi.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            enable(s);
                        }
                    });
                    sm.add(mi);
                }
            }
            p.add(sm);
        }

        p.show(src, 4, 4);
    }

    private void makeLegendEntry(JPanel l, final JFreeChart chart, final String label, final int series) {
        Color color = (Color) chart.getPlot().getLegendItems().get(series).getLinePaint();
        final ColorButton cb = new ColorButton(color);
        cb.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                displaySeriesPopup(chart, label, series, cb);
            }
        });
        l.add(cb);
        l.add(new JLabel(label));
    }

    protected void removeSeries(JFreeChart chart, int seriesIndex) {
        Color color = (Color) chart.getPlot().getLegendItems().get(seriesIndex).getLinePaint();
        palette.release(color);
        String key = enabled.remove(seriesIndex);
        XYPlot plot = chart.getXYPlot();
        Series<?> series = sampler.getSeries(key);
        Unit unit = series.getUnit();

        Integer datasetIndex = datasetMapping.get(unit);
        TimeSeriesCollection col = (TimeSeriesCollection) plot.getDataset(datasetIndex);

        List<String> colIndices = seriesMapping.get(unit);
        int colIndex = colIndices.indexOf(key);
        colIndices.remove(key);

        col.removeSeries(colIndex);
        if (col.getSeriesCount() == 0) {
            plot.setDataset(datasetIndex, null);
            plot.setRangeAxis(datasetIndex, null);
            seriesMapping.remove(unit);
            datasetMapping.remove(unit);
        }

        rebuildLegend();
        repaint();
        gp.saveLayout();
    }

    protected void displaySeriesPopup(final JFreeChart chart, final String label, final int series,
            final ColorButton button) {
        JPopupMenu p = new JPopupMenu();
        JMenuItem color = new JMenuItem("Color...");
        p.add(color);
        color.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                displayColorPicker(chart, label, series, button);
            }
        });

        JMenuItem remove = new JMenuItem("Remove");
        p.add(remove);
        remove.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                removeSeries(chart, series);
            }
        });

        p.show(button, 4, 4);
    }

    protected void displayColorPicker(JFreeChart chart, String label, int series, ColorButton button) {
        Color color = (Color) chart.getPlot().getLegendItems().get(series).getLinePaint();
        Color newColor = JColorChooser.showDialog(this, "Pick a color for " + label, color);
        if (newColor != null) {
            palette.release(color);
            setColor(enabled.get(series), newColor);
            gp.saveLayout();
        }
    }

    public void setColor(String key, Color color) {
        Series<?> series = sampler.getSeries(key);
        Integer datasetIndex = datasetMapping.get(series.getUnit());

        List<String> l = seriesMapping.get(series.getUnit());
        int colIndex = l.indexOf(key);

        int seriesIndex = enabled.indexOf(key);

        chart.getXYPlot().getRenderer(datasetIndex).setSeriesPaint(colIndex, color);
        chart.getXYPlot().getRenderer(datasetIndex).setSeriesStroke(colIndex, DEFAULT_STROKE);
        int i = 0;
        for (Component c : legend.getComponents()) {
            if (c instanceof ColorButton) {
                i++;
                if (i == seriesIndex + 1) {
                    ((ColorButton) c).setColor(color);
                }
            }
        }
    }

    public void enable(Series<?> s) {
        enable(s, true);
    }

    public void enable(Series<?> s, boolean save) {
        enabled.add(s.getKey());
        addSeries(s);
        rebuildLegend();
        repaint();
        if (save) {
            gp.saveLayout();
        }
    }

    public void setMaxRange(int value) {
        this.maxRange = value;
        updateMaxRange();
    }

    private void updateMaxRange() {
        ValueAxis x = chart.getXYPlot().getDomainAxis();
        if (maxRange == 0) {
            x.setAutoRange(true);
        } else {
            Range dr = chart.getXYPlot().getDataRange(x);
            x.setRange(Math.max(dr.getLowerBound(), dr.getUpperBound() - maxRange * 60 * 1000), dr.getUpperBound());
        }
    }

    public void store(Preferences p) {
        p.putInt("enabledCount", enabled.size());
        for (int i = 0; i < enabled.size(); i++) {
            Preferences gp = p.node("series" + i);
            gp.put("key", enabled.get(i));
            Color color = (Color) chart.getPlot().getLegendItems().get(i).getLinePaint();
            gp.putInt("color.r", color.getRed());
            gp.putInt("color.g", color.getGreen());
            gp.putInt("color.b", color.getBlue());
        }
    }

    public static GraphPanel load(Preferences p, SystemState state, GraphsPanel gps) {
        DataSampler sampler = (DataSampler) state.getItemByID(DataSampler.ID, StatefulItemClass.WORKFLOW);
        GraphPanel g = new GraphPanel(state, gps);
        int ec = p.getInt("enabledCount", 0);
        for (int i = 0; i < ec; i++) {
            Preferences gp = p.node("series" + i);
            String key = gp.get("key", null);
            if (key == null) {
                throw new RuntimeException("Null series key");
            }
            int cr = gp.getInt("color.r", 255);
            int cg = gp.getInt("color.g", 0);
            int cb = gp.getInt("color.b", 0);
            g.enable(sampler.getSeries(key), false);
            g.setColor(key, new Color(cr, cg, cb));
        }
        return g;
    }

    private static class AutoNumberAxis extends NumberAxis {
        private Unit unit;
        private double min, max;
        private String unitPrefix;

        public AutoNumberAxis(Unit unit) {
            super(unit.getName());
            this.unit = unit;
            this.unitPrefix = "";
            this.setTickUnit(new NumberTickUnit(unit.getMultiplier(1), new DecimalFormat("###")));
        }

        @Override
        public void setRange(Range range, boolean turnOffAutoRange, boolean notify) {
            super.setRange(range, turnOffAutoRange, notify);
            this.min = range.getLowerBound();
            this.max = range.getUpperBound();
            recalculateUnits();
        }

        private void recalculateUnits() {
            double absmax = Math.max(Math.abs(min), Math.abs(max));
            String prefix = unit.getUnitPrefix(Double.valueOf(absmax));

            this.unitPrefix = prefix;
            this.setLabel(prefix + unit.getName());
            double mult = unit.getMultiplier(absmax);
            double range = max - min;
            double m2 = mult;
            while (range / m2 > 20) {
                m2 = m2 * 10;
            }

            this.setTickUnit(new NumberTickUnit(m2, new ShiftingDecimalFormat(mult, "###")));
        }
    }

    private static class ShiftingDecimalFormat extends DecimalFormat {
        private double multiplier;

        public ShiftingDecimalFormat(double multiplier, String fmt) {
            super(fmt);
            this.multiplier = multiplier;
        }

        @Override
        public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) {
            return super.format(number / multiplier, result, fieldPosition);
        }

        @Override
        public StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition) {
            return super.format(number / multiplier, result, fieldPosition);
        }
    }
}