net.panthema.BispanningGame.GamePanel.java Source code

Java tutorial

Introduction

Here is the source code for net.panthema.BispanningGame.GamePanel.java

Source

/*******************************************************************************
 * src/net/panthema/BispanningGame/GamePanel.java
 *
 * Main Window of the Java Applet.
 *
 *******************************************************************************
 * Copyright (C) 2014 Timo Bingmann <tb@panthema.net>
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package net.panthema.BispanningGame;

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Paint;
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.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;

import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JEditorPane;
import javax.swing.JFileChooser;
import javax.swing.JMenu;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.Timer;
import javax.swing.filechooser.FileNameExtensionFilter;

import org.apache.commons.collections15.Transformer;

import com.itextpdf.awt.DefaultFontMapper;
import com.itextpdf.awt.PdfGraphics2D;
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfWriter;

import edu.uci.ics.jung.algorithms.layout.AbstractLayout;
import edu.uci.ics.jung.algorithms.layout.FRLayout;
import edu.uci.ics.jung.algorithms.layout.StaticLayout;
import edu.uci.ics.jung.io.GraphIOException;
import edu.uci.ics.jung.io.GraphMLWriter;
import edu.uci.ics.jung.visualization.Layer;
import edu.uci.ics.jung.visualization.MultiLayerTransformer;
import edu.uci.ics.jung.visualization.VisualizationViewer;
import edu.uci.ics.jung.visualization.control.AbstractGraphMousePlugin;
import edu.uci.ics.jung.visualization.control.LayoutScalingControl;
import edu.uci.ics.jung.visualization.control.PickingGraphMousePlugin;
import edu.uci.ics.jung.visualization.control.PluggableGraphMouse;
import edu.uci.ics.jung.visualization.control.ScalingGraphMousePlugin;
import edu.uci.ics.jung.visualization.control.TranslatingGraphMousePlugin;
import edu.uci.ics.jung.visualization.decorators.ToStringLabeller;
import edu.uci.ics.jung.visualization.picking.ShapePickSupport;
import edu.uci.ics.jung.visualization.renderers.DefaultEdgeLabelRenderer;
import edu.uci.ics.jung.visualization.renderers.DefaultVertexLabelRenderer;
import edu.uci.ics.jung.visualization.renderers.Renderer.VertexLabel.Position;

public class GamePanel extends javax.swing.JPanel {
    private static final long serialVersionUID = 7526217664458188502L;

    private MyGraph mGraph;

    /** Jung2 visualization object */
    protected VisualizationViewer<Integer, MyEdge> mVV;

    /** Text area for game log */
    protected JTextArea logTextArea;

    /** Jung2 object for getting nearest vertex or edge */
    protected ShapePickSupport<Integer, MyEdge> mPickSupport;

    /** distance of picking support */
    protected final static float mPickDistance = 32;

    /** Jung2 layouting object */
    protected AbstractLayout<Integer, MyEdge> mLayout;

    /** Vertex Counter **/
    protected int mNextVertex;

    /** Edge over which the mouse hovers */
    protected MyEdge mHoverEdge;

    /** Number of turns played in game */
    protected int mTurnNum = 0;

    /** Edge marked by Alice */
    protected MyEdge mMarkedge = null;

    /** Flag whether to automatically play Bob's part */
    protected boolean mAutoPlayBob = true;

    /** Flag if a cycle/cut exists in the graph */
    protected boolean mHaveCycle = false;

    /** Scale the edge stroke thickness using mouse wheel */
    float edgeScale = 2.0f;

    /** Generate only random atomic bispannings graphs */
    protected boolean generateOnlyAtomic = false;

    /** Allow freer non-unique edge exchanges */
    protected boolean allowFreeExchange = true;

    /** Image of Alice and Bob */
    BufferedImage ImageAlice, ImageBob;

    public GamePanel() throws IOException {

        makeActions();

        setBackground(Color.WHITE);

        ImageAlice = ImageIO.read(
                getClass().getClassLoader().getResourceAsStream("net/panthema/BispanningGame/images/Alice.png"));
        ImageBob = ImageIO.read(
                getClass().getClassLoader().getResourceAsStream("net/panthema/BispanningGame/images/Bob.png"));

        logTextArea = new JTextArea();

        makeNewRandomGraph(8);
        mLayout = MyGraphLayoutFactory(mGraph);

        mVV = new VisualizationViewer<Integer, MyEdge>(mLayout);
        mVV.setSize(new Dimension(1000, 800));
        mVV.setBackground(Color.WHITE);

        // Bob's play does not repeat.
        mPlayBob.setRepeats(false);

        // set up mouse handling
        PluggableGraphMouse gm = new PluggableGraphMouse();
        gm.add(new MyEditingGraphMousePlugin<Integer, MyEdge>(MouseEvent.CTRL_MASK, new MyVertexFactory(),
                new MyEdgeFactory()));
        gm.add(new TranslatingGraphMousePlugin(MouseEvent.BUTTON3_MASK));
        gm.add(new MyGraphMousePlugin(MouseEvent.BUTTON1_MASK | MouseEvent.BUTTON3_MASK));
        gm.add(new PickingGraphMousePlugin<Integer, MyEdge>());
        gm.add(new ScalingGraphMousePlugin(new LayoutScalingControl(), 0, 1.1f, 0.9f));
        mVV.setGraphMouse(gm);

        // set vertex and label drawing
        mVV.getRenderContext().setVertexLabelRenderer(new DefaultVertexLabelRenderer(Color.black));
        mVV.getRenderContext().setVertexLabelTransformer(new Transformer<Integer, String>() {
            public String transform(Integer v) {
                return "v" + v;
            }
        });
        mVV.getRenderContext().setVertexLabelTransformer(new ToStringLabeller<Integer>());
        mVV.getRenderer().getVertexLabelRenderer().setPosition(Position.CNTR);

        mVV.getRenderer().setEdgeRenderer(new MyEdgeRenderer());
        mVV.getRenderContext().setVertexDrawPaintTransformer(new MyVertexDrawPaintTransformer<Integer>());
        mVV.getRenderContext().setVertexFillPaintTransformer(new MyVertexFillPaintTransformer());

        mVV.getRenderContext().setEdgeStrokeTransformer(new MyEdgeStrokeTransformer());
        MyQuadCurve<Integer, MyEdge> quadcurve = new MyQuadCurve<Integer, MyEdge>();
        mVV.getRenderContext().setEdgeShapeTransformer(quadcurve);
        mVV.getRenderContext().setParallelEdgeIndexFunction(quadcurve);

        mVV.getRenderContext().setEdgeDrawPaintTransformer(new MyEdgeDrawPaintTransformer());
        mVV.getRenderContext().setEdgeFillPaintTransformer(new MyEdgeFillPaintTransformer());
        mVV.getRenderContext().setEdgeArrowStrokeTransformer(new MyEdgeInnerStrokeTransformer());

        mVV.getRenderContext().setEdgeLabelRenderer(new DefaultEdgeLabelRenderer(Color.black));
        mVV.getRenderContext().setEdgeLabelTransformer(new Transformer<MyEdge, String>() {
            public String transform(MyEdge e) {
                return e.toString();
            }
        });
        mVV.getRenderContext().setLabelOffset(6);

        // create pick support to select closest nodes and edges
        mPickSupport = new ShapePickSupport<Integer, MyEdge>(mVV, mPickDistance);

        // add pre renderer to draw Alice and Bob
        mVV.addPreRenderPaintable(new MyGraphPreRenderer());

        // add post renderer to show error messages in background
        mVV.addPostRenderPaintable(new MyGraphPostRenderer());

        setLayout(new BorderLayout());
        add(mVV, BorderLayout.CENTER);

        JPanel panelSouth = new JPanel();
        add(panelSouth, BorderLayout.SOUTH);
        panelSouth.setLayout(new GridLayout(0, 2, 0, 0));

        JPanel panelButtons = new JPanel();
        panelSouth.add(panelButtons);
        panelButtons.setLayout(new GridLayout(2, 2, 0, 0));
        panelSouth.setPreferredSize(new Dimension(800, 60));

        final JButton btnNewRandomGraph = new JButton("New Random Graph");
        btnNewRandomGraph.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                JPopupMenu popup = new JPopupMenu();

                for (int i = 0; i < actionRandomGraph.length; ++i) {
                    if (actionRandomGraph[i] != null)
                        popup.add(actionRandomGraph[i]);
                }
                popup.addSeparator();
                popup.add(getActionNewGraphType());

                popup.show(btnNewRandomGraph, e.getX(), e.getY());
            }
        });
        panelButtons.add(btnNewRandomGraph);

        final JButton btnNewNamedGraph = new JButton("New Named Graph");
        btnNewNamedGraph.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                JPopupMenu popup = new JPopupMenu();

                for (int i = 0; i < actionNamedGraph.size(); ++i) {
                    if (actionNamedGraph.get(i) != null)
                        popup.add(actionNamedGraph.get(i));
                }

                popup.show(btnNewNamedGraph, e.getX(), e.getY());
            }
        });
        panelButtons.add(btnNewNamedGraph);

        final JButton btnRelayout = new JButton("Relayout");
        btnRelayout.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                relayoutGraph();
            }
        });
        panelButtons.add(btnRelayout);

        final JButton btnOptions = new JButton("Options");
        btnOptions.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                JPopupMenu popup = new JPopupMenu();

                addPopupActions(popup);
                popup.addSeparator();

                popup.show(btnOptions, e.getX(), e.getY());
            }
        });
        panelButtons.add(btnOptions);

        JScrollPane scrollPane = new JScrollPane(logTextArea);
        panelSouth.add(scrollPane);

        logTextArea.setEditable(false);

        setSize(new Dimension(1000, 800));
        relayoutGraph();
    }

    static void showStackTrace(Exception e) {
        StringWriter sw = new StringWriter();
        PrintWriter w = new PrintWriter(sw);
        e.printStackTrace(w);
        String st = sw.toString();
        JOptionPane.showMessageDialog(null, "Exception: " + st);
    }

    /**
     * Append new line to game text log.
     */
    void putLog(String text) {
        if (logTextArea.getDocument().getLength() != 0)
            text = "\n" + text;
        logTextArea.append(text);
        logTextArea.setCaretPosition(logTextArea.getDocument().getLength());
    }

    static AbstractLayout<Integer, MyEdge> MyGraphLayoutFactory(MyGraph g) {
        return new FRLayout<Integer, MyEdge>(g);
    }

    public class MyVertexDrawPaintTransformer<V> implements Transformer<V, Paint> {
        public Paint transform(V v) {
            return Color.black;
        }
    }

    public class MyVertexFillPaintTransformer implements Transformer<Integer, Paint> {
        public Paint transform(Integer v) {

            int count1 = 0, count2 = 0;

            for (MyEdge e : mGraph.getIncidentEdges(v)) {
                if (e.color == 1)
                    count1++;
                if (e.color == 2)
                    count2++;
            }

            if (count1 == 1 && count2 == 1)
                return Color.MAGENTA;
            if (count1 == 1)
                return Color.RED;
            if (count2 == 1)
                return new Color(0, 192, 255);

            return Color.LIGHT_GRAY;
        }
    }

    public class MyVertexFactory implements org.apache.commons.collections15.Factory<Integer> {
        public Integer create() {
            int i = 0;
            while (mGraph.containsVertex(i))
                ++i;
            return i;
        }
    }

    public class MyEdgeStrokeTransformer implements Transformer<MyEdge, Stroke> {
        protected final int THIN = 3;
        protected final int THICK = 6;

        public Stroke transform(MyEdge e) {

            int size = (e.inCycle || e.inCut || e.isUE) ? THICK : THIN;
            if (allowFreeExchange)
                size = THICK;

            if (e == mHoverEdge && (e.isUE || allowFreeExchange))
                size += 2;

            float[] dash_cut = { size * edgeScale };

            if (e.inCut)
                return new BasicStroke((int) (size * edgeScale), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
                        0.0f, dash_cut, 0.0f);
            else
                return new BasicStroke((int) (size * edgeScale), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
        }
    }

    public class MyEdgeDrawPaintTransformer implements Transformer<MyEdge, Paint> {
        public Paint transform(MyEdge e) {
            if (e.color == 1 && !e.inCycle)
                return Color.BLUE;
            if (e.color == 1 && e.inCycle)
                return new Color(0, 164, 255);
            if (e.color == 2 && !e.inCycle)
                return Color.RED;
            if (e.color == 2 && e.inCycle)
                return new Color(255, 164, 0);
            return Color.BLACK;
        }
    }

    // misuse EdgeArrowStrokeTransformer interface for stroke of inner line
    public class MyEdgeInnerStrokeTransformer implements Transformer<MyEdge, Stroke> {
        public Stroke transform(MyEdge e) {
            return new BasicStroke((int) (2 * edgeScale));
        }
    }

    public class MyEdgeFillPaintTransformer implements Transformer<MyEdge, Paint> {
        public Paint transform(MyEdge e) {
            if (e.origColor == 1)
                return Color.BLUE;
            if (e.origColor == 2)
                return Color.RED;
            return Color.BLACK;
        }
    }

    public class MyEdgeFactory implements org.apache.commons.collections15.Factory<MyEdge> {
        public MyEdge create() {
            for (int i = 0;; ++i) {
                boolean contains = false;
                for (MyEdge e : mGraph.getEdges()) {
                    if (e.id == i) {
                        contains = true;
                        break;
                    }
                }
                if (!contains)
                    return new MyEdge(i);
            }
        }
    }

    class MyGraphPreRenderer implements VisualizationViewer.Paintable {
        Font font;
        FontMetrics metrics;
        int swidthAlice, swidthBob, sheight;

        Color highLight = new Color(192, 255, 192);
        Color high = new Color(0, 192, 0);

        public void paint(Graphics _g) {
            Graphics2D g = (Graphics2D) _g;
            if (font == null) {
                font = new Font(g.getFont().getName(), Font.BOLD, 18);

                metrics = g.getFontMetrics(font);
                swidthAlice = metrics.stringWidth("Alice");
                swidthBob = metrics.stringWidth("Bob");
                sheight = metrics.getMaxAscent() + metrics.getMaxDescent();
            }

            Dimension d = mVV.getSize();

            int AliceHeight = 100 * ImageAlice.getHeight() / ImageAlice.getWidth();
            int BobHeight = 100 * ImageBob.getHeight() / ImageBob.getWidth();

            g.setFont(font);
            g.setStroke(new BasicStroke(2));

            if (!mHaveCycle) {
                g.setColor(highLight);
                g.fillRoundRect(0, 0, 103, AliceHeight + sheight + 6, 20, 20);
                g.setColor(high);
                g.drawRoundRect(0, 0, 103, AliceHeight + sheight + 6, 20, 20);
            } else {
                g.setColor(Color.BLACK);
                g.drawRoundRect(0, 0, 103, AliceHeight + sheight + 6, 20, 20);
            }

            g.setColor(Color.BLACK);
            g.drawImage(ImageAlice, 4, 6, 100, AliceHeight, 0, 0, ImageAlice.getWidth(), ImageAlice.getHeight(),
                    null);
            g.drawString("Alice", (100 - swidthAlice) / 2, AliceHeight + sheight - 3);

            if (mHaveCycle) {
                g.setColor(highLight);
                g.fillRoundRect(d.width - 105, d.height - BobHeight - sheight - 8, 103, BobHeight + sheight + 6, 20,
                        20);
                g.setColor(high);
                g.drawRoundRect(d.width - 105, d.height - BobHeight - sheight - 8, 103, BobHeight + sheight + 6, 20,
                        20);
            } else {
                g.setColor(Color.BLACK);
                g.drawRoundRect(d.width - 105, d.height - BobHeight - sheight - 8, 103, BobHeight + sheight + 6, 20,
                        20);
            }

            g.setColor(Color.BLACK);
            g.drawImage(ImageBob, d.width - 100, d.height - BobHeight - sheight - 2, d.width - 5,
                    d.height - sheight - 4, 0, 0, ImageBob.getWidth(), ImageBob.getHeight(), null);
            g.drawString("Bob", d.width - swidthBob - (100 - swidthBob) / 2, d.height - 8);
        }

        public boolean useTransform() {
            return false;
        }
    }

    class MyGraphPostRenderer implements VisualizationViewer.Paintable {
        Font font;
        FontMetrics metrics;
        int swidth, sheight;
        String str;

        public void paint(Graphics g) {
            Dimension d = mVV.getSize();
            if (font == null) {
                font = new Font(g.getFont().getName(), Font.BOLD, 20);
            }
            if (str != mGraph.message) {
                str = mGraph.message;
                if (str == null)
                    return;

                metrics = g.getFontMetrics(font);
                swidth = metrics.stringWidth(str);
                sheight = metrics.getMaxAscent() + metrics.getMaxDescent();
            }
            if (str == null)
                return;

            int x = (d.width - swidth) / 2;
            int y = (int) (d.height - sheight * 1.5);

            g.setFont(font);
            Color oldColor = g.getColor();
            g.setColor(Color.red);
            g.drawString(str, x, y);
            g.setColor(oldColor);
        }

        public boolean useTransform() {
            return false;
        }
    }

    class MyGraphMousePlugin extends AbstractGraphMousePlugin
            implements MouseListener, MouseMotionListener, MouseWheelListener {
        public MyGraphMousePlugin(int modifiers) {
            super(modifiers);
        }

        Point2D mClickPoint;

        public void mouseClicked(MouseEvent e) {

            // no mouse click when Bob is playing!
            if (mHaveCycle && mAutoPlayBob)
                return;

            if ((e.getModifiers() & MouseEvent.CTRL_MASK) != 0) {
                return;
            }

            if ((e.getModifiers() & MouseEvent.BUTTON3_MASK) != 0) {
                showPopup(e);
                return;
            }

            Point2D p = e.getPoint();

            p = mVV.getRenderContext().getMultiLayerTransformer().inverseTransform(Layer.VIEW, p);

            final MyEdge edge = mPickSupport.getEdge(mVV.getGraphLayout(), p.getX(), p.getY());

            if (edge == null)
                return;

            System.err.println("toggle " + edge);

            if (!mHaveCycle) {
                if (edge.isUE || allowFreeExchange) {
                    putLog("Turn " + (++mTurnNum) + ": Alice flips edge " + edge.id + " " + edge.colorName(false)
                            + " -> " + edge.colorName(true) + ".");
                    edge.flipColor();

                    mMarkedge = edge;
                    mHaveCycle = mGraph.markCycleFixes(edge);

                    if (mAutoPlayBob) {
                        mPlayBob.start();
                    }

                    updateGraphMessage();
                }
            } else {
                if (!edge.inCycle) {
                    System.out.println("Edge not in Cycle! Ignoring");
                } else {
                    edge.flipColor();

                    if (mGraph.markCycle(edge)) {
                        System.out.println("Edge does not solve cycle! Ignoring");
                        edge.flipColor();
                        mGraph.markCycle(mMarkedge);
                    } else {
                        // flip edge
                        putLog("Turn " + (++mTurnNum) + ": Bob flips edge " + edge.id + " " + edge.colorName(true)
                                + " -> " + edge.colorName(false) + ".");

                        mHaveCycle = false;
                        mGraph.calcUniqueExchanges();
                    }

                    updateGraphMessage();
                }
            }

            mVV.repaint();
        }

        public void showPopup(MouseEvent e) {

            JPopupMenu popup = new JPopupMenu();

            popup.add(new AbstractAction("Delete Vertex") {
                private static final long serialVersionUID = 571719411573657791L;

                public void actionPerformed(ActionEvent e) {
                    Point2D p = mVV.getRenderContext().getMultiLayerTransformer().inverseTransform(Layer.VIEW,
                            mClickPoint);

                    Integer v = mPickSupport.getVertex(mVV.getGraphLayout(), p.getX(), p.getY());
                    if (v == null)
                        return;

                    mGraph.removeVertex(v);
                    mVV.getRenderContext().getParallelEdgeIndexFunction().reset();
                    mGraph.graphChanged();
                    mVV.repaint();
                }
            });

            popup.add(new AbstractAction("Delete Edge") {
                private static final long serialVersionUID = 571719411573657794L;

                public void actionPerformed(ActionEvent e) {
                    Point2D p = mVV.getRenderContext().getMultiLayerTransformer().inverseTransform(Layer.VIEW,
                            mClickPoint);

                    MyEdge edge = mPickSupport.getEdge(mVV.getGraphLayout(), p.getX(), p.getY());
                    if (edge == null)
                        return;

                    mGraph.removeEdge(edge);
                    mVV.getRenderContext().getParallelEdgeIndexFunction().reset();
                    mGraph.graphChanged();
                    mVV.repaint();
                }
            });

            popup.addSeparator();
            addPopupActions(popup);

            mClickPoint = e.getPoint();
            popup.show(mVV, e.getX(), e.getY());
        }

        public void mouseEntered(MouseEvent e) {
            mouseMoved(e);
        }

        public void mouseMoved(MouseEvent e) {
            Point2D p = e.getPoint();

            p = mVV.getRenderContext().getMultiLayerTransformer().inverseTransform(Layer.VIEW, p);

            MyEdge edge = mPickSupport.getEdge(mVV.getGraphLayout(), p.getX(), p.getY());

            if (edge != mHoverEdge) {
                mHoverEdge = edge;
                mVV.repaint();
            }
        }

        public void mouseExited(MouseEvent e) {
            mHoverEdge = null;
            mVV.repaint();
        }

        public void mousePressed(MouseEvent e) {
        }

        public void mouseReleased(MouseEvent e) {
        }

        public void mouseDragged(MouseEvent e) {
        }

        public void mouseWheelMoved(MouseWheelEvent e) {

            // act only when CTRL is pressed
            if ((e.getModifiers() & MouseEvent.CTRL_MASK) == 0) {
                return;
            }

            int notches = e.getWheelRotation();

            if (notches < 0) { // mouse wheel moved UP
                edgeScale *= 0.9;
            } else { // mouse wheel moved DOWN
                edgeScale /= 0.9;
            }

            mVV.repaint();
        }
    }

    Timer mPlayBob = new Timer(1000, new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            // markCycleFixes has set isFix for all fix edges.
            ArrayList<MyEdge> fix1list = new ArrayList<MyEdge>();
            ArrayList<MyEdge> fix2list = new ArrayList<MyEdge>();
            // Bob prefers to re-color already flipped edges: fix1list

            for (MyEdge ei : mGraph.getEdges()) {
                if (ei.isFix && ei != mMarkedge) {
                    if (ei.color != ei.origColor) {
                        fix1list.add(ei);
                    } else {
                        fix2list.add(ei);
                    }
                }
            }

            fix1list.addAll(fix2list);

            for (MyEdge eFix : fix1list) {
                // flip edge
                putLog("Turn " + (++mTurnNum) + ": Bob flips edge " + eFix.id + " " + eFix.colorName(false) + " -> "
                        + eFix.colorName(true) + ".");

                eFix.flipColor();

                if (mGraph.markCycle(eFix)) {
                    System.out.println("Edge does not solve cycle! Ignoring");
                    eFix.flipColor();
                    mGraph.markCycle(mMarkedge);
                } else {
                    mHaveCycle = false;
                    mGraph.calcUniqueExchanges();
                    break;
                }
            }

            if (mHaveCycle) {
                System.out.println("Bob could not fix the graph?");
            }

            updateGraphMessage();
            repaint();
        }
    });

    AbstractAction[] actionRandomGraph = new AbstractAction[20 + 1];
    ArrayList<AbstractAction> actionNamedGraph = new ArrayList<AbstractAction>();

    void makeActions() {
        actionRandomGraph[0] = new AbstractAction("Empty Graph") {
            private static final long serialVersionUID = 571719411573657773L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(0);
            }
        };
        actionRandomGraph[4] = new AbstractAction("4 Vertices") {
            private static final long serialVersionUID = 571719411573657774L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(4);
            }
        };
        actionRandomGraph[5] = new AbstractAction("5 Vertices") {
            private static final long serialVersionUID = 571719411573657775L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(5);
            }
        };
        actionRandomGraph[6] = new AbstractAction("6 Vertices") {
            private static final long serialVersionUID = 571719411573657776L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(6);
            }
        };
        actionRandomGraph[7] = new AbstractAction("7 Vertices") {
            private static final long serialVersionUID = 571719411573657777L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(7);
            }
        };
        actionRandomGraph[8] = new AbstractAction("8 Vertices") {
            private static final long serialVersionUID = 571719411573657778L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(8);
            }
        };
        actionRandomGraph[9] = new AbstractAction("9 Vertices") {
            private static final long serialVersionUID = 571719411573657779L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(9);
            }
        };
        actionRandomGraph[10] = new AbstractAction("10 Vertices") {
            private static final long serialVersionUID = 571719411573657780L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(10);
            }
        };
        actionRandomGraph[11] = new AbstractAction("11 Vertices") {
            private static final long serialVersionUID = 571719411573657781L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(11);
            }
        };
        actionRandomGraph[12] = new AbstractAction("12 Vertices") {
            private static final long serialVersionUID = 571719411573657782L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(12);
            }
        };
        actionRandomGraph[13] = new AbstractAction("13 Vertices") {
            private static final long serialVersionUID = 571719411573657783L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(13);
            }
        };
        actionRandomGraph[14] = new AbstractAction("14 Vertices") {
            private static final long serialVersionUID = 571719411573657784L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(14);
            }
        };
        actionRandomGraph[15] = new AbstractAction("15 Vertices") {
            private static final long serialVersionUID = 571719411573657785L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(15);
            }
        };
        actionRandomGraph[16] = new AbstractAction("16 Vertices") {
            private static final long serialVersionUID = 571719411573657786L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(16);
            }
        };
        actionRandomGraph[17] = new AbstractAction("17 Vertices") {
            private static final long serialVersionUID = 571719411573657787L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(17);
            }
        };
        actionRandomGraph[18] = new AbstractAction("18 Vertices") {
            private static final long serialVersionUID = 571719411573657788L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(18);
            }
        };
        actionRandomGraph[19] = new AbstractAction("19 Vertices") {
            private static final long serialVersionUID = 571719411573657789L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(19);
            }
        };
        actionRandomGraph[20] = new AbstractAction("20 Vertices") {
            private static final long serialVersionUID = 571719411573657790L;

            public void actionPerformed(ActionEvent e) {
                makeNewRandomGraph(20);
            }
        };

        // Special Named Graphs

        actionNamedGraph.add(new AbstractAction("K4 (complete, 4 vertices)") {
            private static final long serialVersionUID = 571719411573657791L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V4:i0x0y0/i1x1y0/i2x1y1/i3x0y1/;E6:i0t0h1c1/i1t0h2c1/i2t0h3c2/i3t1h2c2/i4t1h3c2/i5t2h3c1/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("W5 (wheel, 5 vertices)") {
            private static final long serialVersionUID = 571719411573657792L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V5:i0x10y10/i1x10y0/i2x0y10/i3x10y20/i4x20y10/;E8:i0t0h1c1/i1t1h2c1/i2t2h0c2/i3t2h3c1/i4t3h0c2/i5t3h4c1/i6t4h0c2/i7t4h1c2/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("K4 + K4 (2-clique sum, 6 vertices)") {
            private static final long serialVersionUID = 571719411573657793L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V6:i0x0y0/i1x1y0/i2x1y1/i3x0y1/i4x2y0/i5x2y1/;E10:i0t0h1c1/i1t0h2c2/i2t0h3c1/i4t1h3c2/i5t2h3c2/i6t1h4c2/i7t1h5c1/i9t4h5c2/i10t4h2c1/i11t2h5c1/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("B 6,12 difficult (6 vertices)") {
            private static final long serialVersionUID = 571719411573457792L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V6:i0x0y12/i1x20y15/i2x40y12/i3x10y30/i4x20y0/i5x30y30/;E10:i0t0h3c2/i1t1h3c1/i2t2h3c1/i3t0h4c1/i4t1h4c2/i5t2h4c2/i6t0h5c1/i7t1h5c2/i8t2h5c1/i9t3h5c2/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("W6 (wheel, 6 vertices)") {
            private static final long serialVersionUID = 571719411573657794L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V6:i0x0y0/i1x0y-1000/i2x-951y-309/i3x-588y809/i4x588y809/i5x951y-309/;E10:i0t0h1c1/i1t1h2c1/i2t0h2c2/i3t2h3c1/i4t0h3c2/i5t3h4c1/i6t0h4c2/i7t4h5c1/i8t0h5c2/i9t5h1c2/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("B7,1 triangle free (7 vertices)") {
            private static final long serialVersionUID = 571719411573657796L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V7:i0x2y0/i1x2y2/i2x2y4/i3x0y1/i4x0y3/i5x4y1/i6x4y3/;E12:i0t3h0c2/i1t3h1c1/i2t3h2c1/i3t4h0c2/i4t4h1c2/i5t4h2c1/i6t5h0c1/i7t5h1c2/i8t5h2c2/i9t6h0c1/i10t6h1c1/i11t6h2c2/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("B8,1 triangle free (8 vertices)") {
            private static final long serialVersionUID = 571719411573657798L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V8:i0x10y20/i1x20y10/i2x40y35/i3x0y35/i4x40y15/i5x0y15/i6x30y30/i7x20y40/;E14:i0t0h4c1/i1t1h4c2/i2t2h4c1/i3t0h5c1/i4t1h5c2/i5t3h5c2/i6t0h6c2/i7t1h6c1/i8t2h6c2/i9t3h6c1/i10t0h7c2/i11t1h7c1/i12t2h7c1/i13t3h7c2/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("B9,1 difficult (9 vertices)") {
            private static final long serialVersionUID = 571719411573657796L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V9:i0x1y0/i1x3y2/i2x1y2/i3x1y1/i4x3y1/i5x0y1/i6x0y2/i7x2y2/i8x2y1/;E16:i0t0h4c1/i1t1h4c1/i2t0h5c2/i3t2h5c2/i4t3h5c1/i5t0h6c2/i6t2h6c1/i7t3h6c2/i8t1h7c1/i9t2h7c1/i10t3h7c2/i11t4h7c2/i12t0h8c1/i13t1h8c2/i14t2h8c2/i15t3h8c1/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("2x2 K4 grid (9 vertices)") {
            private static final long serialVersionUID = 571719411573657796L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V9:i0x0y0/i1x1y0/i2x2y0/i3x0y1/i4x1y1/i5x2y1/i6x0y2/i7x1y2/i8x2y2/;E16:i0t0h1c1/i1t0h4c2/i2t0h3c2/i3t1h2c2/i4t1h3c2/i5t1h5c1/i6t2h4c1/i7t2h5c1/i8t3h6c1/i9t3h7c1/i10t4h6c1/i11t4h8c2/i12t5h7c2/i13t5h8c2/i14t6h7c2/i15t7h8c1/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("B10,1 difficult (10 vertices)") {
            private static final long serialVersionUID = 571419411573657796L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V10:i0x20y6/i1x0y-25/i2x0y25/i3x-20y6/i4x30y-12/i5x-30y13/i6x30y13/i7x-30y-12/i8x-10y-10/i9x10y-10/;E18:i0t0h4c1/i1t1h4c2/i2t0h5c2/i3t2h5c1/i4t2h6c2/i5t3h6c1/i6t4h6c2/i7t1h7c1/i8t3h7c2/i9t5h7c1/i10t0h8c1/i11t1h8c2/i12t2h8c1/i13t3h8c2/i14t0h9c2/i15t1h9c1/i16t2h9c2/i17t3h9c1/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("B10,2 difficult (10 vertices)") {
            private static final long serialVersionUID = 571419411573657796L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V10:i0x10y10/i1x10y20/i2x30y20/i3x30y10/i4x0y25/i5x40y5/i6x40y25/i7x0y5/i8x20y10/i9x20y20/;E18:i0t0h4c2/i1t1h4c1/i2t2h5c2/i3t3h5c1/i4t2h6c1/i5t3h6c2/i6t4h6c1/i7t0h7c1/i8t1h7c2/i9t5h7c2/i10t0h8c1/i11t1h8c1/i12t2h8c2/i13t3h8c2/i14t0h9c2/i15t1h9c2/i16t2h9c1/i17t3h9c1/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("B11,1 difficult (11 vertices)") {
            private static final long serialVersionUID = 571419411573657796L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V11:i0x30y24/i1x0y24/i2x15y0/i3x25y17/i4x5y17/i5x15y20/i6x30y8/i7x0y8/i8x20y8/i9x15y30/i10x10y8/;E20:i0t0h5c1/i1t1h5c2/i2t0h6c2/i3t2h6c2/i4t3h6c1/i5t1h7c1/i6t2h7c1/i7t4h7c2/i8t2h8c2/i9t3h8c1/i10t4h8c1/i11t5h8c2/i12t0h9c2/i13t1h9c1/i14t3h9c2/i15t4h9c1/i16t2h10c1/i17t3h10c2/i18t4h10c2/i19t5h10c1/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("B11,2 difficult (11 vertices)") {
            private static final long serialVersionUID = 571419411573657797L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V11:i0x3y2/i1x1y2/i2x3y1/i3x1y1/i4x4y1/i5x0y1/i6x4y2/i7x0y2/i8x2y2/i9x2y0/i10x2y1/;E20:i0t0h4c1/i1t1h5c2/i2t0h6c2/i3t2h6c1/i4t4h6c2/i5t1h7c1/i6t3h7c2/i7t5h7c1/i8t0h8c2/i9t1h8c1/i10t2h8c1/i11t3h8c2/i12t2h9c2/i13t3h9c1/i14t4h9c2/i15t5h9c1/i16t0h10c1/i17t1h10c2/i18t2h10c2/i19t3h10c1/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("B12,1 difficult (12 vertices)") {
            private static final long serialVersionUID = 571419411573657796L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V12:i0x30y30/i1x10y30/i2x30y0/i3x10y0/i4x25y10/i5x20y20/i6x40y20/i7x0y20/i8x30y20/i9x10y20/i10x15y10/i11x20y0/;E22:i0t0h5c1/i1t1h5c2/i2t0h6c2/i3t2h6c1/i4t1h7c1/i5t3h7c2/i6t1h8c1/i7t2h8c2/i8t4h8c1/i9t6h8c2/i10t0h9c2/i11t3h9c1/i12t4h9c2/i13t7h9c1/i14t2h10c1/i15t3h10c1/i16t4h10c2/i17t5h10c2/i18t2h11c2/i19t3h11c2/i20t4h11c1/i21t5h11c1/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("B12,2 difficult (12 vertices)") {
            private static final long serialVersionUID = 571419411573657796L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V12:i0x10y0/i1x30y0/i2x30y20/i3x15y30/i4x12y40/i5x40y30/i6x0y10/i7x40y10/i8x25y30/i9x10y20/i10x0y30/i11x28y40/;E22:i0t0h5c1/i1t1h5c2/i2t0h6c1/i3t2h6c1/i4t1h7c2/i5t2h7c2/i6t2h8c1/i7t3h8c1/i8t4h8c2/i9t5h8c2/i10t1h9c1/i11t3h9c1/i12t4h9c2/i13t6h9c2/i14t0h10c2/i15t3h10c2/i16t4h10c1/i17t7h10c1/i18t2h11c2/i19t3h11c2/i20t4h11c1/i21t5h11c1/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("B12,3 difficult (12 vertices)") {
            private static final long serialVersionUID = 571419411573657796L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V12:i0x10y5/i1x10y25/i2x30y10/i3x30y20/i4x10y10/i5x10y20/i6x30y5/i7x30y25/i8x20y10/i9x3y15/i10x37y15/i11x20y20/;E22:i0t0h4c2/i1t1h5c1/i2t0h6c1/i3t2h6c2/i4t1h7c2/i5t3h7c1/i6t2h8c1/i7t3h8c2/i8t4h8c2/i9t5h8c1/i10t0h9c1/i11t1h9c2/i12t4h9c1/i13t5h9c2/i14t2h10c1/i15t3h10c2/i16t6h10c1/i17t7h10c2/i18t2h11c2/i19t3h11c1/i20t4h11c1/i21t5h11c2/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("B12,4 difficult (12 vertices)") {
            private static final long serialVersionUID = 571419411573657796L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V12:i0x10y10/i1x25y20/i2x40y10/i3x40y0/i4x10y0/i5x10y20/i6x50y10/i7x0y10/i8x40y20/i9x20y10/i10x30y10/i11x25y0/;E22:i0t0h5c1/i1t1h5c2/i2t2h6c1/i3t3h6c2/i4t0h7c2/i5t4h7c1/i6t5h7c2/i7t1h8c1/i8t2h8c2/i9t6h8c1/i10t0h9c1/i11t1h9c2/i12t3h9c1/i13t4h9c2/i14t1h10c1/i15t2h10c2/i16t3h10c1/i17t4h10c2/i18t0h11c2/i19t2h11c1/i20t3h11c2/i21t4h11c1/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("3x3 K4 grid (16 vertices)") {
            private static final long serialVersionUID = 571719411573657796L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V16:i0x0y0/i1x1y0/i2x2y0/i3x3y0/i4x0y1/i5x1y1/i6x2y1/i7x3y1/i8x0y2/i9x1y2/i10x2y2/i11x3y2/i12x0y3/i13x1y3/i14x2y3/i15x3y3/;E30:i0t0h1c1/i1t0h5c2/i2t0h4c2/i3t1h2c1/i4t1h4c2/i5t1h6c2/i6t2h3c2/i7t2h5c2/i8t2h7c1/i9t3h6c1/i10t3h7c1/i11t4h8c1/i12t4h9c1/i13t5h8c1/i14t5h10c1/i15t6h9c1/i16t6h11c2/i17t7h10c2/i18t7h11c2/i19t8h12c2/i20t8h13c2/i21t9h12c2/i22t9h14c2/i23t10h13c2/i24t10h15c1/i25t11h14c1/i26t11h15c1/i27t12h13c1/i28t13h14c1/i29t14h15c2/;");
            }
        });

        actionNamedGraph.add(new AbstractAction("B18,1 square-free (18 vertices)") {
            private static final long serialVersionUID = 571719411573657796L;

            public void actionPerformed(ActionEvent e) {
                loadGraphString(
                        "V18:i0x0y0/i1x100y0/i2x200y0/i3x300y0/i4x300y100/i5x300y200/i6x200y200/i7x100y200/i8x0y200/i9x0y100/i10x40y40/i11x150y25/i12x260y40/i13x260y160/i14x150y175/i15x40y160/i16x175y70/i17x125y130/;E34:i0t0h1c1/i1t1h2c2/i2t2h3c2/i3t3h4c1/i4t4h5c2/i5t5h6c1/i6t6h7c2/i7t7h8c1/i8t8h9c1/i9t9h0c2/i10t0h10c1/i11t3h12c1/i12t5h13c1/i13t8h15c2/i14t9h16c1/i15t4h17c2/i16t4h11c2/i17t9h14c2/i18t1h15c1/i19t1h17c1/i20t2h16c1/i21t2h13c2/i22t6h12c1/i23t6h16c2/i24t7h17c1/i25t7h10c2/i26t10h11c2/i27t10h13c2/i28t11h16c1/i29t11h15c2/i30t12h15c2/i31t12h14c2/i32t13h14c1/i33t14h17c1/;");
            }
        });
    }

    AbstractAction getActionNewGraphType() {
        return new AbstractAction(generateOnlyAtomic ? "Generate Composite or Atomic" : "Generate Only Atomic") {
            private static final long serialVersionUID = 571719711573657790L;

            public void actionPerformed(ActionEvent e) {
                generateOnlyAtomic = !generateOnlyAtomic;
            }
        };
    }

    public void addPopupActions(JPopupMenu popup) {

        popup.add(new AbstractAction("Center Graph") {
            private static final long serialVersionUID = 571719411574657791L;

            public void actionPerformed(ActionEvent e) {
                centerAndScaleGraph();
            }
        });

        popup.add(new AbstractAction("Relayout Graph") {
            private static final long serialVersionUID = 571719411573657791L;

            public void actionPerformed(ActionEvent e) {
                relayoutGraph();
            }
        });

        popup.add(new AbstractAction("Reset Board Colors") {
            private static final long serialVersionUID = 571719411573657796L;

            public void actionPerformed(ActionEvent e) {
                mGraph.updateOriginalColor();
                mTurnNum = 0;
                putLog("Resetting game graph's colors.");
                updateGraphMessage();
                mVV.repaint();
            }
        });

        popup.add(new AbstractAction(
                allowFreeExchange ? "Restrict to Unique Exchanges" : "Allow Free Edge Exchanges") {
            private static final long serialVersionUID = 571719411573657798L;

            public void actionPerformed(ActionEvent e) {
                allowFreeExchange = !allowFreeExchange;
                mVV.repaint();
            }
        });

        popup.add(new AbstractAction((mAutoPlayBob ? "Disable" : "Enable") + " Autoplay of Bob's Moves") {
            private static final long serialVersionUID = 571719413573657798L;

            public void actionPerformed(ActionEvent e) {
                mAutoPlayBob = !mAutoPlayBob;
            }
        });

        popup.addSeparator();

        JMenu newGraph = new JMenu("New Random Graph");
        for (int i = 0; i < actionRandomGraph.length; ++i) {
            if (actionRandomGraph[i] != null)
                newGraph.add(actionRandomGraph[i]);
        }
        newGraph.addSeparator();
        newGraph.add(getActionNewGraphType());
        popup.add(newGraph);

        JMenu newNamedGraph = new JMenu("New Named Graph");
        for (int i = 0; i < actionNamedGraph.size(); ++i) {
            if (actionNamedGraph.get(i) != null)
                newNamedGraph.add(actionNamedGraph.get(i));
        }
        popup.add(newNamedGraph);

        popup.add(new AbstractAction("Show GraphString") {
            private static final long serialVersionUID = 545719411573657792L;

            public void actionPerformed(ActionEvent e) {
                JEditorPane text = new JEditorPane("text/plain",
                        GraphString.write_graph(mGraph, mVV.getModel().getGraphLayout()));
                text.setEditable(false);
                text.setPreferredSize(new Dimension(300, 125));
                JOptionPane.showMessageDialog(null, text, "GraphString Serialization",
                        JOptionPane.INFORMATION_MESSAGE);
            }
        });

        popup.add(new AbstractAction("Load GraphString") {
            private static final long serialVersionUID = 8636579131902717983L;

            public void actionPerformed(ActionEvent e) {
                String input = JOptionPane.showInputDialog(null, "Enter GraphString:", "");
                if (input == null)
                    return;
                loadGraphString(input);
            }
        });

        popup.add(new AbstractAction("Show graph6") {
            private static final long serialVersionUID = 571719411573657792L;

            public void actionPerformed(ActionEvent e) {
                JTextArea text = new JTextArea(Graph6.write_graph6(mGraph));
                JOptionPane.showMessageDialog(null, text, "graph6 Serialization", JOptionPane.INFORMATION_MESSAGE);
            }
        });

        popup.add(new AbstractAction("Load graph6/sparse6") {
            private static final long serialVersionUID = 571719411573657792L;

            public void actionPerformed(ActionEvent e) {
                String input = JOptionPane.showInputDialog(null, "Enter graph6/sparse6 string:", "");
                if (input == null)
                    return;
                MyGraph g = Graph6.read_graph6(input);
                setNewGraph(g);
            }
        });

        popup.add(new AbstractAction("Read GraphML") {
            private static final long serialVersionUID = 571719411573657794L;

            public void actionPerformed(ActionEvent e) {
                try {
                    readGraphML();
                } catch (IOException e1) {
                    showStackTrace(e1);
                } catch (GraphIOException e1) {
                    showStackTrace(e1);
                }
            }
        });

        popup.add(new AbstractAction("Write GraphML") {
            private static final long serialVersionUID = 571719411573657795L;

            public void actionPerformed(ActionEvent e) {
                try {
                    writeGraphML();
                } catch (IOException e1) {
                    showStackTrace(e1);
                }
            }
        });

        popup.add(new AbstractAction("Write PDF") {
            private static final long serialVersionUID = 571719411573657793L;

            public void actionPerformed(ActionEvent e) {
                try {
                    writePdf();
                } catch (FileNotFoundException e1) {
                    showStackTrace(e1);
                } catch (DocumentException de) {
                    System.err.println(de.getMessage());
                }
            }
        });
    }

    void makeNewRandomGraph(int numVertex) {
        MyGraph g;
        do {
            g = MyGraph.getRandomGraph(numVertex);
        } while (numVertex != 0 && generateOnlyAtomic && !g.isAtomicBispanner());
        setNewGraph(g);
    }

    void relayoutGraph() {
        final AbstractLayout<Integer, MyEdge> layout = MyGraphLayoutFactory(mGraph);
        mLayout = layout;
        mVV.setGraphLayout(layout);
        centerAndScaleGraph();
    }

    void updateGraphMessage() {
        String msg = "";

        int round = mTurnNum / 2 + 1;
        int min_rounds = mGraph.getEdgeCount() / 2;

        if (mGraph.getVertexCount() == 0) {
        } else if (mGraph.finishedEdges() != mGraph.getEdgeCount()) {
            msg = "Round " + round;
            msg += " of minimum " + min_rounds;
            msg += ", remaining edges: " + (mGraph.getEdgeCount() - mGraph.finishedEdges()) + ".";
        } else {
            if (round == min_rounds)
                msg = "Congratulations! You won in the minimum number of rounds! Terrific!";
            else {
                msg = "Congratulations! You won after " + round + " of minimum " + min_rounds + " rounds!";
                msg += " You needed " + (round - min_rounds) + " extra rounds.";
            }
        }

        mGraph.message = msg;
    }

    void centerAndScaleGraph() {

        // clear layout
        MultiLayerTransformer mlTransformer = mVV.getRenderContext().getMultiLayerTransformer();
        mlTransformer.setToIdentity();

        if (mGraph.getVertexCount() == 0)
            return;

        // calculate bounding box of layout
        double xMin = Double.POSITIVE_INFINITY;
        double yMin = Double.POSITIVE_INFINITY;
        double xMax = Double.NEGATIVE_INFINITY;
        double yMax = Double.NEGATIVE_INFINITY;

        for (Integer v : mGraph.getVertices()) {
            Point2D p = mLayout.transform(v);
            if (p.getX() < xMin)
                xMin = p.getX();
            if (p.getX() > xMax)
                xMax = p.getX();
            if (p.getY() < yMin)
                yMin = p.getY();
            if (p.getY() > yMax)
                yMax = p.getY();
        }

        System.err.println("xMin: " + xMin + " xMax: " + xMax + " yMin: " + yMin + " yMax: " + yMax);

        // shift and scale layout
        Dimension vv_size = mVV.getSize();
        System.err.println("vv_size: " + vv_size);

        double xSize = xMax - xMin;
        double ySize = yMax - yMin;

        double xRatio = vv_size.getWidth() / xSize;
        double yRatio = vv_size.getHeight() / ySize;
        double ratio = 0.75 * Math.min(xRatio, yRatio);

        System.err.println("ratio: " + ratio);

        mlTransformer.getTransformer(Layer.LAYOUT).scale(ratio, ratio, new Point2D.Double(0, 0));

        double xShift = -xMin + (vv_size.getWidth() / ratio - xSize) / 2.0;
        double yShift = -yMin + (vv_size.getHeight() / ratio - ySize) / 2.0;
        mlTransformer.getTransformer(Layer.LAYOUT).translate(xShift, yShift);
    }

    void setNewGraph(MyGraph g) {

        mGraph = g;
        mGraph.graphChanged();
        mHoverEdge = null;
        mMarkedge = null;
        mHaveCycle = false;
        mTurnNum = 0;
        mNextVertex = g.getVertexCount();

        putLog("Starting new game with " + mGraph.getEdgeCount() + " edges.");

        if (mGraph.getVertexCount() > 0 && mGraph.getVertexCount() <= 14) {
            if (mGraph.isAtomicBispanner()) {
                putLog("New graph is an atomic bispanning graph.");
            } else {
                putLog("New graph is a composite bispanning graph.");
            }
        }

        mGraph.calcUniqueExchanges();
        mGraph.updateOriginalColor();

        updateGraphMessage();

        if (mVV != null) {

            if (g.mInitialLayout != null)
                mLayout = g.mInitialLayout;
            else
                mLayout = MyGraphLayoutFactory(mGraph);

            mVV.setGraphLayout(mLayout);
            centerAndScaleGraph();
        }
    }

    public void loadGraphString(String input) {
        try {
            MyGraph g = GraphString.read_graph(input);
            setNewGraph(g);
        } catch (IOException e1) {
            JOptionPane.showMessageDialog(null, "Error in GraphString: " + e1, "GraphString",
                    JOptionPane.INFORMATION_MESSAGE);
        }
    }

    public void writePdf() throws FileNotFoundException, DocumentException {

        // Query user for filename
        JFileChooser chooser = new JFileChooser();
        chooser.setDialogTitle("Specify PDF file to save");
        chooser.setCurrentDirectory(new File("."));
        FileNameExtensionFilter filter = new FileNameExtensionFilter("PDF Documents", "pdf");
        chooser.setFileFilter(filter);

        if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION)
            return;

        File outfile = chooser.getSelectedFile();
        if (!outfile.getAbsolutePath().endsWith(".pdf")) {
            outfile = new File(outfile.getAbsolutePath() + ".pdf");
        }

        // Calculate page size rectangle
        Dimension size = mVV.getSize();
        Rectangle rsize = new Rectangle(size.width, size.height);

        // Open the PDF file for writing - and create a Graphics2D object
        Document document = new Document(rsize);
        PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(outfile));
        document.open();

        PdfContentByte contentByte = writer.getDirectContent();
        PdfGraphics2D graphics2d = new PdfGraphics2D(contentByte, size.width, size.height, new DefaultFontMapper());

        // Create a container to hold the visualization
        Container container = new Container();
        container.addNotify();
        container.add(mVV);
        container.setVisible(true);
        container.paintComponents(graphics2d);

        // Dispose of the graphics and close the document
        graphics2d.dispose();
        document.close();

        // Put mVV back onto visible plane
        setLayout(new BorderLayout());
        add(mVV, BorderLayout.CENTER);
    }

    public void writeGraphML() throws IOException {

        // Query user for filename
        JFileChooser chooser = new JFileChooser();
        chooser.setDialogTitle("Specify GraphML file to save");
        chooser.setCurrentDirectory(new File("."));
        FileNameExtensionFilter filter = new FileNameExtensionFilter("GraphML File", "graphml");
        chooser.setFileFilter(filter);

        if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION)
            return;

        File outfile = chooser.getSelectedFile();
        if (!outfile.getAbsolutePath().endsWith(".graphml")) {
            outfile = new File(outfile.getAbsolutePath() + ".graphml");
        }

        // construct graphml writer
        GraphMLWriter<Integer, MyEdge> graphWriter = new GraphMLWriter<Integer, MyEdge>();

        PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(outfile)));

        graphWriter.addVertexData("x", null, "0", new Transformer<Integer, String>() {
            public String transform(Integer v) {
                return Double.toString(mLayout.getX(v));
            }
        });

        graphWriter.addVertexData("y", null, "0", new Transformer<Integer, String>() {
            public String transform(Integer v) {
                return Double.toString(mLayout.getY(v));
            }
        });

        graphWriter.addEdgeData("color", null, "0", new Transformer<MyEdge, String>() {
            public String transform(MyEdge e) {
                return Integer.toString(e.color);
            }
        });

        graphWriter.save(mGraph, out);
    }

    public void readGraphML() throws IOException, GraphIOException {

        MyGraphMLReader gml = new MyGraphMLReader(this);
        setNewGraph(gml.newGraph);

        mLayout = new StaticLayout<Integer, MyEdge>(mGraph, gml);
        mVV.setGraphLayout(mLayout);
        centerAndScaleGraph();
    }
}