jchrest.gui.VisualSearchPane.java Source code

Java tutorial

Introduction

Here is the source code for jchrest.gui.VisualSearchPane.java

Source

// Copyright (c) 2012, Peter C. R. Lane
// Released under Open Works License, http://owl.apotheon.org/

package jchrest.gui;

import jchrest.domainSpecifics.Scene;
import jchrest.domainSpecifics.Fixation;
import jchrest.domainSpecifics.generic.GenericDomain;
import jchrest.domainSpecifics.chess.ChessDomain;
import jchrest.architecture.Chrest;
import jchrest.architecture.Node;
import jchrest.lib.*;

import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.*;

// Import the JFreeChart classes
import org.jfree.chart.*;
import org.jfree.chart.plot.*;
import org.jfree.data.*;
import org.jfree.data.general.*;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.ui.RectangleInsets;

/**
 * This panel provides an interface for building and testing  
 * models on visual search problems.
 *
 * @author Peter C. R. Lane
 */
public class VisualSearchPane extends JPanel {
    private final Chrest _model;
    private final Scenes _scenes;
    private final SceneDisplay _sceneDisplay;
    private int _time;

    public VisualSearchPane(Chrest model, Scenes scenes) {
        super();

        _model = model;
        _scenes = scenes;
        //TODO: fix this when perceiver functionality working correctly.
        //_model.getPerceiver().setScene (_scenes.get (0));
        _model.setClocks(0);
        _sceneDisplay = new SceneDisplay(_scenes.get(0));
        _domainSelector = new JComboBox(new String[] { "Generic", "Chess" });
        _domainSelector.addActionListener(new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                int index = _domainSelector.getSelectedIndex();
                if (index == 0) {
                    _model.setDomain(new GenericDomain(_model, null, 3));
                } else { // if (index == 1) 
                    //TODO: fix this when perceiver functionality working correctly.
                    //_model.setDomain (new ChessDomain (_model));
                }
            }
        });

        JTabbedPane jtb = new JTabbedPane();
        jtb.addTab("Train", trainPanel());
        jtb.addTab("Recall", recallPanel());
        jtb.addTab("Log", logPanel());
        jtb.addTab("Analyse", analysePanel());

        setLayout(new BorderLayout());
        add(jtb);
    }

    // -- set up the training panel
    private JPanel trainPanel() {
        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));

        panel.add(constructTrainingOptions());
        panel.add(runTrainingButtons());
        if (freeChartLoaded()) {
            panel.add(createPanel());
        }

        return panel;
    }

    private JSpinner _maxTrainingCycles;
    private JSpinner _numFixations;
    private JSpinner _maxNetworkSize;

    private JPanel constructTrainingOptions() {
        _maxTrainingCycles = new JSpinner(new SpinnerNumberModel(5, 1, 1000, 1));
        _numFixations = new JSpinner(new SpinnerNumberModel(20, 1, 1000, 1));
        _maxNetworkSize = new JSpinner(new SpinnerNumberModel(100000, 1, 10000000, 1));

        JPanel panel = new JPanel();
        panel.setLayout(new SpringLayout());
        Utilities.addLabel(panel, "Domain of scenes:", _domainSelector);
        Utilities.addLabel(panel, "Number of scenes:", new JLabel("" + _scenes.size()));
        Utilities.addLabel(panel, "Maximum training cycles:", _maxTrainingCycles);
        Utilities.addLabel(panel, "Number of fixations per scene:", _numFixations);
        Utilities.addLabel(panel, "Maximum network size:", _maxNetworkSize);

        Utilities.makeCompactGrid(panel, 5, 2, 3, 3, 10, 5);
        panel.setMaximumSize(panel.getPreferredSize());

        JPanel ePanel = new JPanel();
        ePanel.setLayout(new GridLayout(1, 1));
        ePanel.add(panel);

        return ePanel;
    }

    // confirm if FreeChart has been included in classpath
    private boolean freeChartLoaded() {
        try {
            new XYSeriesCollection();

            return true;
        } catch (NoClassDefFoundError ex) {
            return false;
        }
    }

    private JPanel runTrainingButtons() {
        JPanel panel = new JPanel();

        _trainingTimes = new XYSeries("CHREST model");
        _trainAction = new TrainAction();
        JButton trainButton = new JButton(_trainAction);
        trainButton.setToolTipText("Train a fresh CHREST model up to the given network size");
        _stopAction = new StopAction();
        _stopAction.setEnabled(false);
        JButton stopButton = new JButton(_stopAction);
        stopButton.setToolTipText("Stop the current training");
        _feedback = new JProgressBar(0, 100);
        _feedback.setToolTipText("Proportion of target network size");
        _feedback.setValue(0);
        _feedback.setStringPainted(true);
        panel.add(trainButton);
        panel.add(stopButton);
        panel.add(_feedback);

        return panel;
    }

    private XYSeries _trainingTimes;

    private ChartPanel createPanel() {
        XYSeriesCollection dataset = new XYSeriesCollection();
        dataset.addSeries(_trainingTimes);

        JFreeChart chart = ChartFactory.createXYLineChart("Plot of network size vs. number of training patterns",
                "Number of training patterns", "Network size", dataset,
                org.jfree.chart.plot.PlotOrientation.VERTICAL, true, true, false);

        XYPlot plot = (XYPlot) chart.getPlot();
        plot.setBackgroundPaint(Color.lightGray);
        plot.setDomainGridlinePaint(Color.white);
        plot.setRangeGridlinePaint(Color.white);
        plot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 5.0));
        plot.setDomainCrosshairVisible(true);
        plot.setRangeCrosshairVisible(true);

        XYItemRenderer r = plot.getRenderer();
        if (r instanceof XYLineAndShapeRenderer) {
            XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) r;
            renderer.setBaseShapesVisible(true);
            renderer.setBaseShapesFilled(true);
            renderer.setDrawSeriesLineAsPath(true);
        }

        return new ChartPanel(chart);
    }

    private class Pair {
        private int _first, _second;

        Pair(int first, int second) {
            _first = first;
            _second = second;
        }

        int getFirst() {
            return _first;
        }

        int getSecond() {
            return _second;
        }
    }

    private class TrainingThread extends SwingWorker<List<Pair>, Pair> {
        private Chrest _model;
        private Scenes _scenes;
        private int _maxCycles, _maxSize, _numFixations, _time;

        TrainingThread(Chrest model, Scenes scenes, int maxCycles, int maxSize, int numFixations, int time) {
            _model = model;
            _scenes = scenes;
            _maxCycles = maxCycles;
            _maxSize = maxSize;
            _numFixations = numFixations;
            _time = time;
        }

        @Override
        public List<Pair> doInBackground() {
            _model.clearShortAndLongTermMemory(0);
            _model.freeze();
            List<Pair> results = new ArrayList<Pair>();
            Pair result = new Pair(0, 0);
            results.add(result);
            publish(result);

            int stepSize = (_maxCycles * _scenes.size()) / 100;

            int cycle = 0;
            int positionsSeen = 0;
            while ((cycle < _maxCycles) && (_model.getLtmSize(_time) < _maxSize) && !isCancelled()) {
                for (int i = 0, lastSceneIndex = _scenes.size(); i < lastSceneIndex
                        && (_model.getLtmSize(_time) < _maxSize) && !isCancelled(); i++) {
                    //TODO: fix this when perceiver functionality working correctly.
                    //_model.learnScene (_scenes.get (i), _numFixations, _time, null);
                    positionsSeen += 1;
                    if (positionsSeen % stepSize == 0) {
                        result = new Pair(positionsSeen, _model.getLtmSize(_time));
                        publish(result);
                        setProgress(100 * _model.getLtmSize(_time) / _maxSize);
                        results.add(result);
                    }
                }
                cycle += 1;
            }
            _model.makeTemplates(_model.getMaximumClockValue());

            result = new Pair(positionsSeen, _model.getLtmSize(_time));
            results.add(result);
            publish(result);

            return results;
        }

        @Override
        protected void process(List<Pair> results) {
            for (Pair pair : results) {
                _trainingTimes.add(pair.getFirst(), pair.getSecond());
            }
        }

        protected void done() {
            _feedback.setValue(100);
            _stopAction.setEnabled(false);
            _trainAction.setEnabled(true);
            _model.unfreeze();
        }
    }

    private class TrainAction extends AbstractAction implements ActionListener {

        public TrainAction() {
            super("Train");
        }

        private int getNumFixations() {
            return ((SpinnerNumberModel) (_numFixations.getModel())).getNumber().intValue();
        }

        private int getMaxCycles() {
            return ((SpinnerNumberModel) (_maxTrainingCycles.getModel())).getNumber().intValue();
        }

        private int getMaxNetworkSize() {
            return ((SpinnerNumberModel) (_maxNetworkSize.getModel())).getNumber().intValue();
        }

        private int getCurrentTime() {
            return _time;
        }

        public void actionPerformed(ActionEvent e) {
            _model.setEngagedInExperiment();
            _task = new TrainingThread(_model, _scenes, getMaxCycles(), getMaxNetworkSize(), getNumFixations(),
                    getCurrentTime());
            _task.addPropertyChangeListener(new java.beans.PropertyChangeListener() {
                public void propertyChange(java.beans.PropertyChangeEvent evt) {
                    if ("progress".equals(evt.getPropertyName())) {
                        _feedback.setValue((Integer) evt.getNewValue());
                    }
                }
            });

            _trainingTimes.clear(); // make sure we start graph afresh

            _feedback.setValue(0);
            _trainAction.setEnabled(false);
            _stopAction.setEnabled(true);

            _task.execute();
        }
    }

    private TrainingThread _task;
    private JProgressBar _feedback;
    private TrainAction _trainAction;
    private StopAction _stopAction;

    private class StopAction extends AbstractAction implements ActionListener {
        public StopAction() {
            super("Stop");
        }

        public void actionPerformed(ActionEvent e) {
            _task.cancel(true);
            _trainAction.setEnabled(true);
            _stopAction.setEnabled(false);
        }
    }

    private JLabel _recallSceneLabel;
    private SceneDisplay _recalledSceneDisplay;

    // -- set up the recall panel
    private JPanel recallPanel() {
        JPanel panel = new JPanel();
        panel.setLayout(new GridLayout(1, 1));

        JSplitPane sp = new JSplitPane(JSplitPane.VERTICAL_SPLIT, recallSetupPanel(), recallResultsPanel());

        panel.add(sp);

        return panel;
    }

    private JPanel recallSetupPanel() {
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());

        panel.add(constructSelector(), BorderLayout.NORTH);
        panel.add(_sceneDisplay);
        panel.add(constructButtons(), BorderLayout.EAST);

        return panel;
    }

    private JLabel _precision, _recall, _omission, _commission;

    private JPanel recallResultsPanel() {
        _recallSceneLabel = new JLabel("RECALLED SCENE");
        //TODO: fix this when perceiver functionality working correctly.
        //    _recalledSceneDisplay = new SceneDisplay (new Scene (
        //      "empty",
        //      _scenes.get(0).getWidth(), 
        //      _scenes.get(0).getHeight(),
        //      null
        //    ));
        _precision = new JLabel("");
        _recall = new JLabel("");
        _omission = new JLabel("");
        _commission = new JLabel("");

        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());

        JPanel statistics = new JPanel();
        // Note: GridLayout used because it keeps a fixed size for the 
        // right-hand side, whereas SpringLayout dynamically changes.
        statistics.setLayout(new GridLayout(4, 2));
        statistics.add(new JLabel("Precision: ", SwingConstants.RIGHT));
        statistics.add(_precision);
        statistics.add(new JLabel("Recall: ", SwingConstants.RIGHT));
        statistics.add(_recall);
        statistics.add(new JLabel("Errors of omission: ", SwingConstants.RIGHT));
        statistics.add(_omission);
        statistics.add(new JLabel("Errors of commission: ", SwingConstants.RIGHT));
        statistics.add(_commission);

        // TODO: there must be a better solution to stop the statistics panel spreading out
        JPanel statisticsPanel = new JPanel();
        statisticsPanel.setLayout(new BorderLayout());
        statisticsPanel.add(statistics, BorderLayout.NORTH);

        panel.add(_recallSceneLabel, BorderLayout.NORTH);
        panel.add(_recalledSceneDisplay);
        panel.add(statisticsPanel, BorderLayout.EAST);

        return panel;
    }

    private JComboBox _domainSelector;
    private JComboBox _sceneSelector;

    private JPanel constructSelector() {
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        panel.add(new JLabel("Scene: "), BorderLayout.WEST);

        _sceneSelector = new JComboBox(_scenes.getSceneNames());
        _sceneSelector.addActionListener(new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                Scene newScene = _scenes.get(_sceneSelector.getSelectedIndex());
                //TODO: fix this when perceiver functionality working correctly.
                //        _model.getPerceiver().setScene (newScene);
                _sceneDisplay.updateScene(newScene);
            }
        });
        panel.add(_sceneSelector);

        return panel;
    }

    class AnalyseAction extends AbstractAction implements ActionListener {
        private JSpinner _numFixations;

        public AnalyseAction(JSpinner numFixations) {
            super("Analyse Scenes");

            _numFixations = numFixations;
        }

        public void actionPerformed(ActionEvent e) {
            _analysisTask = new AnalysisThread(_numFixations);
            _analysisTask.addPropertyChangeListener(new java.beans.PropertyChangeListener() {
                public void propertyChange(java.beans.PropertyChangeEvent evt) {
                    if ("progress".equals(evt.getPropertyName())) {
                        _feedbackAnalysis.setValue((Integer) evt.getNewValue());
                    }
                }
            });
            _feedbackAnalysis.setValue(0);
            _analyseAction.setEnabled(false);
            _stopAnalysisAction.setEnabled(true);

            _analysisTask.execute();
        }
    }

    private class AnalysisThread extends SwingWorker<Void, Void> {
        private Map<Integer, Integer> _recallFrequencies;
        private JSpinner _numFixations;

        public AnalysisThread(JSpinner numFixations) {
            _numFixations = numFixations;
        }

        @Override
        public Void doInBackground() {
            _recallFrequencies = new HashMap<Integer, Integer>();

            // loop through each scene, doing recall
            for (int i = 0; i < _scenes.size() && !isCancelled(); i++) {
                Scene scene = _scenes.get(i);
                //TODO: fix this when perceiver functionality working correctly.
                //          _model.scanScene (scene, ((SpinnerNumberModel)(_numFixations.getModel())).getNumber().intValue (), 0, null, false);
                //          for (Node node : _model.getPerceiver().getRecognisedNodes ()) {
                //            int id = node.getReference ();
                //            if (_recallFrequencies.containsKey (id)) {
                //              _recallFrequencies.put (id, _recallFrequencies.get(id) + 1);
                //            } else {
                //              _recallFrequencies.put (id, 1);
                //            }
                //          }
                setProgress(100 * i / _scenes.size());
            }
            return null;
        }

        protected void done() {
            // finally, show results in window
            for (Integer key : _recallFrequencies.keySet()) {
                _analysisScreen.append("" + key + ", " + _recallFrequencies.get(key) + "\n");
            }

            _feedbackAnalysis.setValue(100);
            _stopAnalysisAction.setEnabled(false);
            _analyseAction.setEnabled(true);
        }
    }

    class RecallAction extends AbstractAction implements ActionListener {
        private JSpinner _numFixations;

        public RecallAction(JSpinner numFixations) {
            super("Recall Scene");

            _numFixations = numFixations;
        }

        public void actionPerformed(ActionEvent e) {
            Scene scene = _scenes.get(_sceneSelector.getSelectedIndex());
            //TODO: fix this when perceiver functionality working correctly.
            //      Scene recalledScene = _model.scanAndRecallScene (
            //        scene, 
            //        ((SpinnerNumberModel)(_numFixations.getModel())).getNumber().intValue (),
            //        _model.getDomainSpecifics().getCurrentTime(), //TODO: this is probably wrong but inserted to get S/LTM history views working.
            //        null
            //      );

            //      _recallSceneLabel.setText (recalledScene.getName ());
            //      _recalledSceneDisplay.updateScene (recalledScene);
            //      _precision.setText ("" + scene.computePrecision (recalledScene, true));
            //      _recall.setText ("" + scene.computeRecall (recalledScene, true));
            //      _omission.setText ("" + scene.computeErrorsOfOmission (recalledScene));
            //      _commission.setText ("" + scene.computeErrorsOfCommission (recalledScene));
            //      _sceneDisplay.setFixations (_model.getPerceiver().getFixations ());
            //      // log results
            //      addLog ("\n" + recalledScene.getName ());
            //      addLog ("Fixations: ");
            //      for (Fixation fixation : _model.getPerceiver().getFixations ()) {
            //        addLog ("   " + fixation.toString ());
            //      }
            addLog("Chunks used: ");
            for (Node node : _model.getStm(Modality.VISUAL)
                    .getContents(_model.getCurrentExperiment().getCurrentTime())) {
                addLog("   " + "Node: " + node.getReference() + " "
                        + node.getImage(VisualSearchPane.this._time).toString());
                if (_model.canCreateTemplates() && node.isTemplate(VisualSearchPane.this._time)) {
                    addLog("     Template:");

                    addLog("        filled item slots: ");
                    List<ItemSquarePattern> filledItemSlots = node.getFilledItemSlots(VisualSearchPane.this._time);
                    if (filledItemSlots != null) {
                        for (ItemSquarePattern isp : filledItemSlots) {
                            addLog("         " + isp.toString());
                        }
                    }

                    addLog("        filled position slots: ");
                    List<ItemSquarePattern> filledPositionSlots = node
                            .getFilledPositionSlots(VisualSearchPane.this._time);
                    if (filledPositionSlots != null) {
                        for (ItemSquarePattern isp : filledPositionSlots) {
                            addLog("         " + isp.toString());
                        }
                    }

                }
            }
            //TODO: fix this when perceiver functionality working correctly.
            //      addLog ("Performance: ");
            //      addLog ("   Precision: " + scene.computePrecision (recalledScene, true));
            //      addLog ("   Recall: " + scene.computeRecall (recalledScene, true));
            //      addLog ("   Errors of Omission: " + scene.computeErrorsOfOmission (recalledScene));
            //      addLog ("   Errors of Commission: " + scene.computeErrorsOfCommission (recalledScene));
        }
    }

    private JPanel constructButtons() {

        Box buttons = Box.createVerticalBox();
        JSpinner numFixations = new JSpinner(new SpinnerNumberModel(20, 1, 1000, 1));

        JPanel labelledSpinner = new JPanel();
        labelledSpinner.setLayout(new GridLayout(1, 2));
        labelledSpinner.add(new JLabel("Number of fixations: "));
        labelledSpinner.add(numFixations);

        JButton recallButton = new JButton(new RecallAction(numFixations));
        recallButton.setToolTipText("Scan shown scene and display results");

        final JCheckBox showFixations = new JCheckBox("Show fixations", false);
        showFixations.setToolTipText("Show fixations for recalled scene");
        showFixations.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                _sceneDisplay.setShowFixations(showFixations.isSelected());
            }
        });

        buttons.add(Box.createRigidArea(new Dimension(0, 20)));
        buttons.add(labelledSpinner);
        buttons.add(recallButton);
        buttons.add(Box.createRigidArea(new Dimension(0, 20)));
        buttons.add(showFixations);
        buttons.add(Box.createRigidArea(new Dimension(0, 20)));

        // TODO: There must be a better solution to this problem!
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        panel.add(buttons, BorderLayout.NORTH);

        return panel;
    }

    // -- setup the log display
    private JTextArea _logScreen;

    private JPanel logPanel() {
        _logScreen = new JTextArea();
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        panel.add(new JScrollPane(_logScreen));

        Box buttons = Box.createHorizontalBox();
        buttons.add(new JButton(new SaveAction(_logScreen)));
        buttons.add(new JButton(new ClearAction(_logScreen)));
        panel.add(buttons, BorderLayout.SOUTH);

        return panel;
    }

    private class SaveAction extends AbstractAction implements ActionListener {
        private JTextArea _text;

        public SaveAction(JTextArea text) {
            super("Save");
            _text = text;
        }

        public void actionPerformed(ActionEvent e) {
            File file = FileUtilities.getSaveFilename(_text);
            if (file != null) {
                try {
                    FileWriter fw = new FileWriter(file);
                    _text.write(fw);
                    fw.close();
                } catch (IOException ioe) {
                    ; // ignore any problems
                }
            }
        }
    }

    private class ClearAction extends AbstractAction implements ActionListener {
        private JTextArea _text;

        public ClearAction(JTextArea text) {
            super("Clear");
            _text = text;
        }

        public void actionPerformed(ActionEvent e) {
            _text.setText("");
        }
    }

    private void addLog(String string) {
        _logScreen.append(string + "\n");
    }

    // -- setup the analyse display
    private JTextArea _analysisScreen;

    private JPanel analysePanel() {
        _analysisScreen = new JTextArea();

        Box buttons = Box.createVerticalBox();
        buttons.add(new JLabel("Find frequency of nodes used by model when scanning scenes"));

        JSpinner numFixations = new JSpinner(new SpinnerNumberModel(20, 1, 1000, 1));

        JPanel labelledSpinner = new JPanel();
        labelledSpinner.setLayout(new GridLayout(1, 2));
        labelledSpinner.add(new JLabel("Number of fixations: ", SwingConstants.RIGHT));
        labelledSpinner.add(numFixations);

        buttons.add(labelledSpinner);
        buttons.add(runAnalysisButtons(numFixations));

        // main panel
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());

        panel.add(buttons, BorderLayout.NORTH);

        panel.add(new JScrollPane(_analysisScreen));

        Box displayButtons = Box.createHorizontalBox();
        displayButtons.add(new JButton(new SaveAction(_analysisScreen)));
        displayButtons.add(new JButton(new ClearAction(_analysisScreen)));
        panel.add(displayButtons, BorderLayout.SOUTH);

        return panel;
    }

    private AnalysisThread _analysisTask;
    private AnalyseAction _analyseAction;
    private StopAnalysisAction _stopAnalysisAction;
    private JProgressBar _feedbackAnalysis;

    private JPanel runAnalysisButtons(JSpinner numFixations) {
        JPanel panel = new JPanel();

        _analyseAction = new AnalyseAction(numFixations);
        JButton analyseButton = new JButton(_analyseAction);
        analyseButton.setToolTipText("Analyse CHREST model on all scenes");
        _stopAnalysisAction = new StopAnalysisAction();
        _stopAnalysisAction.setEnabled(false);
        JButton stopAnalysisButton = new JButton(_stopAnalysisAction);
        stopAnalysisButton.setToolTipText("Stop the current analysis");
        _feedbackAnalysis = new JProgressBar(0, 100);
        _feedbackAnalysis.setToolTipText("Proportion of scenes analysed");
        _feedbackAnalysis.setValue(0);
        _feedbackAnalysis.setStringPainted(true);
        panel.add(analyseButton);
        panel.add(stopAnalysisButton);
        panel.add(_feedbackAnalysis);

        return panel;
    }

    private class StopAnalysisAction extends AbstractAction implements ActionListener {
        public StopAnalysisAction() {
            super("Stop");
        }

        public void actionPerformed(ActionEvent e) {
            _analysisTask.cancel(true);
            _analyseAction.setEnabled(true);
            _stopAnalysisAction.setEnabled(false);
        }
    }
}

class SceneDisplay extends JPanel {
    private Scene _scene;
    private SceneView _sceneView;
    private List<Fixation> _fixations;
    private boolean _showFixations;

    public SceneDisplay(Scene scene) {
        super();

        setLayout(new GridLayout(1, 1));
        _scene = scene;
        _sceneView = new SceneView();
        _fixations = new ArrayList<Fixation>();
        _showFixations = false;
        add(new JScrollPane(_sceneView));
    }

    public void updateScene(Scene scene) {
        _scene = scene;
        _sceneView.update();
    }

    public void setFixations(List<Fixation> fixations) {
        _fixations = fixations;
        _sceneView.update();
    }

    public void setShowFixations(boolean showFixations) {
        _showFixations = showFixations;
        _sceneView.update();
    }

    class SceneView extends JPanel {
        private int _maxX, _maxY;

        SceneView() {
            super();

            setBackground(Color.WHITE);
            update();
        }

        private int offsetX = 10;
        private int offsetY = 10;
        private int scale = 20;

        public void update() {

            _maxX = scale * (_scene.getWidth() + 2); // + 2 for row heading and gaps
            _maxY = scale * (_scene.getHeight() + 2); // + 2 for column heading and gaps

            setPreferredSize(new Dimension(_maxX, _maxY));
            if (getParent() != null)
                ((JComponent) getParent()).updateUI();
            repaint();
        }

        public void paint(Graphics g) {
            ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            super.paint(g); // make sure the background of the JPanel is drawn
            Graphics2D g2 = (Graphics2D) g;
            int fov = 2; // TODO ???

            g2.setBackground(Color.WHITE);
            g2.clearRect(0, 0, _maxX, _maxY);

            // draw lines of grid
            for (int i = 0; i <= _scene.getHeight(); ++i) {
                g2.drawLine(offsetX, offsetY + scale * i, offsetX + scale * _scene.getWidth(), offsetY + scale * i);
            }
            for (int i = 0; i <= _scene.getWidth(); ++i) {
                g2.drawLine(offsetX + scale * i, offsetY, offsetX + scale * i,
                        offsetY + scale * _scene.getHeight());
            }
            // add row labels
            for (int i = 0; i < _scene.getHeight(); ++i) {
                g2.drawString("" + (i + 1), offsetX + scale * (_scene.getWidth() + 1),
                        offsetY + scale * (i + 1) - 5);
            }
            // add column labels
            for (int i = 0; i < _scene.getWidth(); ++i) {
                g2.drawString("" + (i + 1), offsetX + scale * i + 5, offsetY + scale * (_scene.getHeight() + 1));
            }

            // draw entries within grid
            for (int i = 0; i < _scene.getHeight(); ++i) {
                for (int j = 0; j < _scene.getWidth(); ++j) {
                    if (!_scene.isSquareEmpty(j, i) && !_scene.isSquareBlind(j, i)) {
                        String items = "";
                        for (PrimitivePattern itemOnSquare : _scene.getSquareContentsAsListPattern(j, i)) {
                            items += ", " + ((ItemSquarePattern) itemOnSquare).getItem();
                        }

                        //Draw "items" after removing the first comma and space since this 
                        //is erroneous: commas should separate items.
                        g2.drawString(items.replaceFirst(", ", ""), offsetX + 5 + scale * j,
                                offsetY + scale - 5 + scale * i);
                    }
                }
            }

            // draw fixations
            int prevX = -1;
            int prevY = -1;
            if (_showFixations) {
                g2.setColor(Color.RED); // first fixation in red
                g2.setStroke(new BasicStroke(6)); // with thick border
                for (Fixation fixation : _fixations) {
                    int nextX = offsetX + scale * fixation.getColFixatedOn() + 5;
                    int nextY = offsetY + scale * fixation.getRowFixatedOn() + 5;
                    if (prevX == -1 && prevY == -1) {
                        ; // draw nothing for first oval
                    } else {
                        g2.drawLine(prevX, prevY, nextX + 5, nextY + 5);
                    }
                    g2.drawOval(nextX, nextY, scale - 10, scale - 10);
                    prevX = nextX + 5;
                    prevY = nextY + 5;
                    g2.setColor(Color.BLUE); // remaining fixations in blue
                    g2.setStroke(new BasicStroke(2)); // and narrower
                }
            }
        }
    }
}