de.uka.aifb.com.systemDynamics.gui.ModelExecutionChartPanel.java Source code

Java tutorial

Introduction

Here is the source code for de.uka.aifb.com.systemDynamics.gui.ModelExecutionChartPanel.java

Source

/* ======================================================================================================
 * SystemDynamics: Java application for modeling, visualization and execution of System Dynamics models
 * ======================================================================================================
 *
 * (C) Copyright 2007-2008, Joachim Melcher, Institut AIFB, Universitaet Karlsruhe (TH), Germany
 *
 * Project Info:  http://sourceforge.net/projects/system-dynamics
 *
 * This program 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.
 *
 * This program 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 this program; if
 * not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301, USA.
 */

package de.uka.aifb.com.systemDynamics.gui;

import de.uka.aifb.com.systemDynamics.SystemDynamics;
import de.uka.aifb.com.systemDynamics.model.*;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import java.util.*;
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import org.jfree.chart.*;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.*;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.RangeType;
import org.jfree.data.xy.*;
import org.jfree.ui.*;

/*
 * Changes:
 * ========
 *
 * 2008-01-24: createPanel was rewritten: table column names internationalization
 */

/**
 * This class implements a panel for drawing the charts of the model execution.
 * 
 * @author Joachim Melcher, Institut AIFB, Universitaet Karlsruhe (TH), Germany
 * @version 1.2
 */
public class ModelExecutionChartPanel extends JPanel implements FocusListener {

    private static final long serialVersionUID = 1L;

    private Locale locale;
    private ResourceBundle messages;

    private NumberFormat integerNumberFormatter;

    private Model model;
    private LevelNode[] levelNodes;

    private XYSeries[] xySeriesArray;
    private JFreeChart chart;
    private int nextRound;

    private JButton axesButton;
    private JButton executionButton;

    /**
     * Constructor.
     * 
     * @param start {@link de.uka.aifb.com.systemDynamics.SystemDynamics} instance
     * @param model {@link de.uka.aifb.com.systemDynamics.model.Model} instance
     */
    public ModelExecutionChartPanel(SystemDynamics start, Model model) {
        super(null);

        if (start == null) {
            throw new IllegalArgumentException("'start' must not be null");
        }
        if (model == null) {
            throw new IllegalArgumentException("'model' must not be null");
        }

        this.model = model;

        locale = start.getLocale();
        messages = start.getMessages();

        integerNumberFormatter = NumberFormat.getIntegerInstance(locale);

        createPanel();
    }

    /**
     * Gets the execution button.
     * 
     * @return execution button
     */
    public JButton getExecutionButton() {
        return executionButton;
    }

    /**
     * Creates panel.
     */
    private void createPanel() {
        setLayout(new BorderLayout());

        // CENTER: chart
        ChartPanel chartPanel = new ChartPanel(createChart());
        // no context menu
        chartPanel.setPopupMenu(null);
        // not zoomable
        chartPanel.setMouseZoomable(false);
        add(chartPanel, BorderLayout.CENTER);

        // LINE_END: series table
        JPanel tablePanel = new JPanel(new GridBagLayout());
        String[] columnNames = { messages.getString("ModelExecutionChartPanel.Table.ColumnNames.ExtraAxis"),
                messages.getString("ModelExecutionChartPanel.Table.ColumnNames.LevelNode") };
        final MyTableModel tableModel = new MyTableModel(columnNames, xySeriesArray.length);
        for (int i = 0; i < xySeriesArray.length; i++) {
            tableModel.addEntry((String) xySeriesArray[i].getKey());
        }
        JTable table = new JTable(tableModel);
        table.setRowSelectionAllowed(false);
        JScrollPane tableScrollPane = new JScrollPane(table);
        int width = (int) Math.min(300, table.getPreferredSize().getWidth());
        int height = (int) Math.min(200, table.getPreferredSize().getHeight());
        tableScrollPane.getViewport().setPreferredSize(new Dimension(width, height));
        tableScrollPane.setMaximumSize(tableScrollPane.getViewport().getPreferredSize());
        axesButton = new JButton(messages.getString("ModelExecutionChartPanel.AxesButton.Text"));
        axesButton.setToolTipText(messages.getString("ModelExecutionChartPanel.AxesButton.ToolTipText"));
        axesButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                // create XYSeriesCollections (and renderer)
                XYSeriesCollection standardData = new XYSeriesCollection();
                XYLineAndShapeRenderer standardRenderer = new XYLineAndShapeRenderer(true, false);
                LinkedList<XYSeriesCollection> extraDataList = new LinkedList<XYSeriesCollection>();
                LinkedList<XYLineAndShapeRenderer> extraRendererList = new LinkedList<XYLineAndShapeRenderer>();
                for (int i = 0; i < tableModel.getRowCount(); i++) {
                    if (tableModel.getValueAt(i, 0).equals(Boolean.FALSE)) {
                        standardData.addSeries(xySeriesArray[i]);
                        standardRenderer.setSeriesPaint(standardData.getSeriesCount() - 1,
                                DefaultDrawingSupplier.DEFAULT_PAINT_SEQUENCE[i
                                        % DefaultDrawingSupplier.DEFAULT_PAINT_SEQUENCE.length]);
                    } else {
                        // extra axis
                        XYSeriesCollection extraData = new XYSeriesCollection();
                        extraData.addSeries(xySeriesArray[i]);
                        extraDataList.add(extraData);
                        XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(true, false);
                        extraRendererList.add(renderer);
                        renderer.setSeriesPaint(0, DefaultDrawingSupplier.DEFAULT_PAINT_SEQUENCE[i
                                % DefaultDrawingSupplier.DEFAULT_PAINT_SEQUENCE.length]);
                    }
                }
                LinkedList<XYSeriesCollection> dataList = new LinkedList<XYSeriesCollection>();
                LinkedList<XYLineAndShapeRenderer> rendererList = new LinkedList<XYLineAndShapeRenderer>();
                if (!standardData.getSeries().isEmpty()) {
                    dataList.add(standardData);
                    rendererList.add(standardRenderer);
                }
                for (XYSeriesCollection data : extraDataList) {
                    dataList.add(data);
                }
                for (XYLineAndShapeRenderer renderer : extraRendererList) {
                    rendererList.add(renderer);
                }

                // creates axes
                LinkedList<NumberAxis> axesList = new LinkedList<NumberAxis>();
                if (!standardData.getSeries().isEmpty()) {
                    NumberAxis axis = new NumberAxis(messages.getString("ModelExecutionChartPanel.Value"));
                    axis.setNumberFormatOverride(NumberFormat.getInstance(locale));
                    axesList.add(axis);
                }
                for (XYSeriesCollection data : extraDataList) {
                    NumberAxis axis = new NumberAxis((String) data.getSeries(0).getKey());
                    axis.setNumberFormatOverride(NumberFormat.getInstance(locale));
                    axesList.add(axis);
                }

                // store data and axes in plot
                XYPlot plot = chart.getXYPlot();
                plot.clearRangeAxes();
                plot.setRangeAxes(axesList.toArray(new NumberAxis[0]));
                for (int i = 0; i < plot.getDatasetCount(); i++) {
                    plot.setDataset(i, null);
                }
                int datasetIndex = 0;
                Iterator<XYSeriesCollection> datasetIterator = dataList.iterator();
                Iterator<XYLineAndShapeRenderer> rendererIterator = rendererList.iterator();
                while (datasetIterator.hasNext()) {
                    plot.setDataset(datasetIndex, datasetIterator.next());
                    plot.setRenderer(datasetIndex, rendererIterator.next());
                    datasetIndex++;
                }
                for (int i = 0; i < plot.getDatasetCount(); i++) {
                    plot.mapDatasetToRangeAxis(i, i);
                }
            }
        });
        GridBagConstraints c = new GridBagConstraints();
        c.anchor = GridBagConstraints.CENTER;
        c.gridx = 0;
        c.gridy = 0;
        c.insets = new Insets(0, 0, 10, 0);
        tablePanel.add(tableScrollPane, c);
        c.gridx = 0;
        c.gridy = 1;
        tablePanel.add(axesButton, c);
        add(tablePanel, BorderLayout.LINE_END);

        // PAGE_END: number of rounds and execution button
        JPanel commandPanel = new JPanel();
        commandPanel.add(new JLabel(messages.getString("ModelExecutionChartPanel.NumberRounds")));
        final JTextField numberRoundsField = new JTextField("1", 5);
        numberRoundsField.addFocusListener(this);
        commandPanel.add(numberRoundsField);
        executionButton = new JButton(messages.getString("ModelExecutionChartPanel.ExecutionButton.Text"));
        executionButton.setToolTipText(messages.getString("ModelExecutionChartPanel.ExecutionButton.ToolTipText"));
        executionButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                int numberRounds = 0;
                boolean correctNumber = false;
                try {
                    numberRounds = integerNumberFormatter.parse(numberRoundsField.getText()).intValue();
                } catch (ParseException parseExcep) {
                    // do nothing
                }

                if (numberRounds >= 1) {
                    correctNumber = true;
                }

                if (correctNumber) {
                    ModelExecutionThread executionThread = new ModelExecutionThread(numberRounds);
                    executionThread.start();
                } else {
                    JOptionPane.showMessageDialog(null,
                            messages.getString("ModelExecutionChartPanel.Error.Message"),
                            messages.getString("ModelExecutionChartPanel.Error.Title"), JOptionPane.ERROR_MESSAGE);
                }
            }
        });
        commandPanel.add(executionButton);
        add(commandPanel, BorderLayout.PAGE_END);
    }

    /**
     * Creates the XY line chart.
     * 
     * @return XY line chart
     */
    private JFreeChart createChart() {
        levelNodes = new LevelNode[model.getLevelNodes().size()];
        int i = 0;
        for (LevelNode levelNode : model.getLevelNodes()) {
            levelNodes[i++] = levelNode;
        }
        // sort level nodes alphabetically
        Arrays.sort(levelNodes);

        xySeriesArray = new XYSeries[levelNodes.length];
        XYSeriesCollection data = new XYSeriesCollection();
        for (i = 0; i < xySeriesArray.length; i++) {
            XYSeries xySeries = new XYSeries(levelNodes[i].getNodeName());
            xySeries.add(0.0, levelNodes[i].getCurrentValue());
            data.addSeries(xySeries);
            xySeriesArray[i] = xySeries;
        }
        nextRound = 1;

        chart = ChartFactory.createXYLineChart(null, messages.getString("ModelExecutionChartPanel.Round"),
                messages.getString("ModelExecutionChartPanel.Value"), data, PlotOrientation.VERTICAL, true, false,
                false);
        XYPlot plot = chart.getXYPlot();

        // horizontal axis range: 0 ... maximal rounds
        ((NumberAxis) (chart.getXYPlot().getDomainAxis())).setRangeType(RangeType.POSITIVE);
        plot.getDomainAxis().setAutoRangeMinimumSize(20);

        // only integer values as labels for horizontal axis
        plot.getDomainAxis().setStandardTickUnits(NumberAxis.createIntegerTickUnits());

        // number formatting according to current locale
        ((NumberAxis) (plot.getDomainAxis())).setNumberFormatOverride(NumberFormat.getIntegerInstance(locale));
        ((NumberAxis) (plot.getRangeAxis())).setNumberFormatOverride(NumberFormat.getInstance(locale));

        // legend at top position
        chart.getLegend().setPosition(RectangleEdge.TOP);

        return chart;
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    //                             methods of interface FocusListener
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Performs a gained focus event.
     * 
     * @param e event
     */
    public void focusGained(FocusEvent e) {
        Component c = e.getComponent();
        if (c instanceof JTextField) {
            ((JTextField) c).selectAll();
        }
    }

    /**
     * Performs a lost focus event.
     * 
     * @param e event
     */
    public void focusLost(FocusEvent e) {
        // do nothing
    }

    /**
     * This inner class implements a table model with a boolean and a String column.
     */
    private class MyTableModel extends AbstractTableModel {

        private static final long serialVersionUID = 1L;

        private String[] columnNames;
        private Object[][] data;
        private int nextRow;

        /**
         * Constructor.
         * 
         * @param columnNames column names
         * @param numberRows number of rows
         */
        private MyTableModel(String[] columnNames, int numberRows) {
            if (columnNames == null) {
                throw new IllegalArgumentException("'columnNames' must not be null.");
            }

            this.columnNames = columnNames;
            data = new Object[numberRows][columnNames.length];
        }

        /**
         * Adds the specified entry (with boolean value <code>false</close> to the next free row.
         * 
         * @param entryName entry name
         */
        private void addEntry(String entryName) {
            if (entryName == null) {
                throw new IllegalArgumentException("'entryName' must not be null.");
            }

            data[nextRow][0] = false;
            data[nextRow][1] = entryName;

            nextRow++;
        }

        /**
         * Gets the number of columns.
         * 
         * @return number of columns
         */
        public int getColumnCount() {
            return columnNames.length;
        }

        /**
         * Gets the number of rows.
         * 
         * @return number of rows
         */
        public int getRowCount() {
            return data.length;
        }

        /**
         * Gets the column name of the specified column.
         * 
         * @param col column index
         * @return column name
         */
        @Override
        public String getColumnName(int col) {
            return columnNames[col];
        }

        /**
         * Gets the value stored at the specified position.
         * 
         * @param row row index
         * @param col column index
         * @return stored value
         */
        public Object getValueAt(int row, int col) {
            return data[row][col];
        }

        /**
         * Sets the specified value at the given position.
         * 
         * @param value value to store
         * @param row row index
         * @param col column index
         */
        @Override
        public void setValueAt(Object value, int row, int col) {
            if (col == 0) {
                data[row][col] = value;
                fireTableCellUpdated(row, col);
            }
        }

        /**
         * Gets the <code>Class</code> type of the specified column.
         * 
         * @param c column index
         * @return <code>Class</code> type
         */
        @Override
        public Class getColumnClass(int c) {
            if (c < 0 || c > 1) {
                throw new ArrayIndexOutOfBoundsException();
            }

            if (c == 0) {
                return Boolean.class;
            } else {
                return String.class;
            }
        }

        /**
         * Checks whether the specified cell is editable.
         * 
         * @param row row index
         * @param col column index
         * @return <code>true</code> iff the specified cell is editable
         */
        @Override
        public boolean isCellEditable(int row, int col) {
            return (col == 0);
        }
    }

    /**
     * This inner class implements a thread for model execution within the swing GUI.
     */
    private class ModelExecutionThread extends Thread {

        private int numberRounds;

        /**
         * Constructor.
         * 
         * @param numberRounds number of rounds to execute
         */
        private ModelExecutionThread(int numberRounds) {
            if (numberRounds < 1) {
                throw new IllegalArgumentException("'numberRounds' must be at least 1.");
            }

            this.numberRounds = numberRounds;
        }

        /**
         * Executes the model for the specified number of rounds. A progress monitor shows progress
         * information.
         */
        @Override
        public void run() {
            NumberFormat numberFormatter = NumberFormat.getIntegerInstance(locale);
            executionButton.setEnabled(false);
            setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

            ProgressMonitor progressMonitor = new ProgressMonitor(ModelExecutionChartPanel.this,
                    messages.getString("ModelExecutionThread.ProgressMonitor.Text"), "", 0, numberRounds);
            for (int i = 0; i < numberRounds; i++) {
                if (progressMonitor.isCanceled()) {
                    // stop execution (i.e. for loop)
                    break;
                }
                progressMonitor.setNote(numberFormatter.format(i) + " "
                        + messages.getString("ModelExecutionThread.ProgressMonitor.Note.Text1") + " "
                        + numberFormatter.format(numberRounds) + " "
                        + messages.getString("ModelExecutionThread.ProgressMonitor.Note.Text2"));
                progressMonitor.setProgress(i);
                model.computeNextValues();
                for (int j = 0; j < xySeriesArray.length; j++) {
                    xySeriesArray[j].add(nextRound, levelNodes[j].getCurrentValue());
                }
                nextRound++;
            }
            progressMonitor.close();

            setCursor(null);
            executionButton.setEnabled(true);
        }
    }
}