/* Copyright (C) 2020-2023 Stephan Kreutzer
 *
 * This file is part of hyperdex_1, a submodule of the
 * digital_publishing_workflow_tools package.
 *
 * hyperdex_1 is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3 or any later version,
 * as published by the Free Software Foundation.
 *
 * hyperdex_1 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 Affero General Public License 3 for more details.
 *
 * You should have received a copy of the GNU Affero General Public License 3
 * along with hyperdex_1. If not, see <http://www.gnu.org/licenses/>.
 */
/**
 * @file $/htx/gui/hyperdex/hyperdex_1/hyperdex_1.java
 * @brief Navigation interface for multidimensional graphs.
 * @author Stephan Kreutzer
 * @since 2020-07-27
 */



import javax.swing.*;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.text.MessageFormat;
import java.io.File;
import java.util.Map;
import java.util.HashMap;
import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.io.FileOutputStream;
import java.io.StringWriter;
import java.io.PrintWriter;
import java.io.FileNotFoundException;
import java.io.UnsupportedEncodingException;
import java.io.IOException;
import java.awt.*;
import java.awt.event.*;
import javax.xml.stream.XMLInputFactory;
import java.io.InputStream;
import java.io.FileInputStream;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.events.XMLEvent;
import javax.xml.namespace.QName;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.XMLStreamException;
import java.util.Set;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.AbstractMap;
import java.util.HashSet;
import java.util.ArrayList;
import java.security.SecureRandom;
import java.math.BigInteger;



public class hyperdex_1
  extends JFrame
{
    public static void main(String[] args)
    {
        System.out.print("hyperdex_1 Copyright (C) 2020-2023 Stephan Kreutzer\n" +
                         "This program comes with ABSOLUTELY NO WARRANTY.\n" +
                         "This is free software, and you are welcome to redistribute it\n" +
                         "under certain conditions. See the GNU Affero General Public License 3\n" +
                         "or any later version for details. Also, see the source code repository\n" +
                         "https://gitlab.com/publishing-systems/digital_publishing_workflow_tools/ and\n" +
                         "the project website https://hypertext-systems.org.\n\n");

        hyperdex_1 instance = new hyperdex_1();

        try
        {
            instance.run(args);
        }
        catch (ProgramTerminationException ex)
        {
            instance.handleTermination(ex);
        }

        if (instance.resultInfoFile != null)
        {
            try
            {
                BufferedWriter writer = new BufferedWriter(
                                        new OutputStreamWriter(
                                        new FileOutputStream(instance.resultInfoFile),
                                        "UTF-8"));

                writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
                writer.write("<!-- This file was created by hyperdex_1, which is free software licensed under the GNU Affero General Public License 3 or any later version (see https://gitlab.com/publishing-systems/digital_publishing_workflow_tools/ and https://hypertext-systems.org). -->\n");
                writer.write("<hyperdex-1-result-information>\n");
                writer.write("  <success>\n");

                if (instance.getInfoMessages().size() > 0)
                {
                    writer.write("    <info-messages>\n");

                    for (int i = 0, max = instance.getInfoMessages().size(); i < max; i++)
                    {
                        InfoMessage infoMessage = instance.getInfoMessages().get(i);

                        writer.write("      <info-message number=\"" + i + "\">\n");
                        writer.write("        <timestamp>" + infoMessage.getTimestamp() + "</timestamp>\n");

                        String infoMessageText = infoMessage.getMessage();
                        String infoMessageId = infoMessage.getId();
                        String infoMessageBundle = infoMessage.getBundle();
                        Object[] infoMessageArguments = infoMessage.getArguments();

                        if (infoMessageBundle != null)
                        {
                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            infoMessageBundle = infoMessageBundle.replaceAll("&", "&amp;");
                            infoMessageBundle = infoMessageBundle.replaceAll("<", "&lt;");
                            infoMessageBundle = infoMessageBundle.replaceAll(">", "&gt;");

                            writer.write("        <id-bundle>" + infoMessageBundle + "</id-bundle>\n");
                        }

                        if (infoMessageId != null)
                        {
                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            infoMessageId = infoMessageId.replaceAll("&", "&amp;");
                            infoMessageId = infoMessageId.replaceAll("<", "&lt;");
                            infoMessageId = infoMessageId.replaceAll(">", "&gt;");

                            writer.write("        <id>" + infoMessageId + "</id>\n");
                        }

                        if (infoMessageText != null)
                        {
                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            infoMessageText = infoMessageText.replaceAll("&", "&amp;");
                            infoMessageText = infoMessageText.replaceAll("<", "&lt;");
                            infoMessageText = infoMessageText.replaceAll(">", "&gt;");

                            writer.write("        <message>" + infoMessageText + "</message>\n");
                        }

                        if (infoMessageArguments != null)
                        {
                            writer.write("        <arguments>\n");

                            int argumentCount = infoMessageArguments.length;

                            for (int j = 0; j < argumentCount; j++)
                            {
                                if (infoMessageArguments[j] == null)
                                {
                                    writer.write("          <argument number=\"" + j + "\">\n");
                                    writer.write("            <class></class>\n");
                                    writer.write("            <value>null</value>\n");
                                    writer.write("          </argument>\n");

                                    continue;
                                }

                                String className = infoMessageArguments[j].getClass().getName();

                                // Ampersand needs to be the first, otherwise it would double-encode
                                // other entities.
                                className = className.replaceAll("&", "&amp;");
                                className = className.replaceAll("<", "&lt;");
                                className = className.replaceAll(">", "&gt;");

                                String value = infoMessageArguments[j].toString();

                                // Ampersand needs to be the first, otherwise it would double-encode
                                // other entities.
                                value = value.replaceAll("&", "&amp;");
                                value = value.replaceAll("<", "&lt;");
                                value = value.replaceAll(">", "&gt;");

                                writer.write("          <argument number=\"" + j + "\">\n");
                                writer.write("            <class>" + className + "</class>\n");
                                writer.write("            <value>" + value + "</value>\n");
                                writer.write("          </argument>\n");
                            }

                            writer.write("        </arguments>\n");
                        }

                        Exception exception = infoMessage.getException();

                        if (exception != null)
                        {
                            writer.write("        <exception>\n");

                            String className = exception.getClass().getName();

                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            className = className.replaceAll("&", "&amp;");
                            className = className.replaceAll("<", "&lt;");
                            className = className.replaceAll(">", "&gt;");

                            writer.write("          <class>" + className + "</class>\n");

                            StringWriter stringWriter = new StringWriter();
                            PrintWriter printWriter = new PrintWriter(stringWriter);
                            exception.printStackTrace(printWriter);
                            String stackTrace = stringWriter.toString();

                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            stackTrace = stackTrace.replaceAll("&", "&amp;");
                            stackTrace = stackTrace.replaceAll("<", "&lt;");
                            stackTrace = stackTrace.replaceAll(">", "&gt;");

                            writer.write("          <stack-trace>" + stackTrace + "</stack-trace>\n");
                            writer.write("        </exception>\n");
                        }

                        writer.write("      </info-message>\n");
                    }

                    writer.write("    </info-messages>\n");
                }

                writer.write("  </success>\n");
                writer.write("</hyperdex-1-result-information>\n");
                writer.flush();
                writer.close();
            }
            catch (FileNotFoundException ex)
            {
                ex.printStackTrace();
                System.exit(-1);
            }
            catch (UnsupportedEncodingException ex)
            {
                ex.printStackTrace();
                System.exit(-1);
            }
            catch (IOException ex)
            {
                ex.printStackTrace();
                System.exit(-1);
            }
        }
    }

    public hyperdex_1()
    {
        super("hyperdex_1");

        addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent event)
            {
                event.getWindow().setVisible(false);
                event.getWindow().dispose();
                System.exit(2);
            }
        });
    }

    public int run(String[] args)
    {
        if (args.length < 2)
        {
            throw constructTermination("messageArgumentsMissing", null, getI10nString("messageArgumentsMissingUsage") + "\n\thyperdex_1 " + getI10nString("messageParameterList") + "\n");
        }

        File resultInfoFile = new File(args[1]);

        try
        {
            resultInfoFile = resultInfoFile.getCanonicalFile();
        }
        catch (SecurityException ex)
        {
            throw constructTermination("messageResultInfoFileCantGetCanonicalPath", ex, null, resultInfoFile.getAbsolutePath());
        }
        catch (IOException ex)
        {
            throw constructTermination("messageResultInfoFileCantGetCanonicalPath", ex, null, resultInfoFile.getAbsolutePath());
        }

        if (resultInfoFile.exists() == true)
        {
            if (resultInfoFile.isFile() == true)
            {
                if (resultInfoFile.canWrite() != true)
                {
                    throw constructTermination("messageResultInfoFileIsntWritable", null, null, resultInfoFile.getAbsolutePath());
                }
            }
            else
            {
                throw constructTermination("messageResultInfoPathIsntAFile", null, null, resultInfoFile.getAbsolutePath());
            }
        }

        hyperdex_1.resultInfoFile = resultInfoFile;
        File jobFile = new File(args[0]);

        try
        {
            jobFile = jobFile.getCanonicalFile();
        }
        catch (SecurityException ex)
        {
            throw constructTermination("messageJobFileCantGetCanonicalPath", ex, null, jobFile.getAbsolutePath());
        }
        catch (IOException ex)
        {
            throw constructTermination("messageJobFileCantGetCanonicalPath", ex, null, jobFile.getAbsolutePath());
        }

        if (jobFile.exists() != true)
        {
            throw constructTermination("messageJobFileDoesntExist", null, null, jobFile.getAbsolutePath());
        }

        if (jobFile.isFile() != true)
        {
            throw constructTermination("messageJobPathIsntAFile", null, null, jobFile.getAbsolutePath());
        }

        if (jobFile.canRead() != true)
        {
            throw constructTermination("messageJobFileIsntReadable", null, null, jobFile.getAbsolutePath());
        }

        System.out.println("hyperdex_1: " + getI10nStringFormatted("messageCallDetails", jobFile.getAbsolutePath(), resultInfoFile.getAbsolutePath()));


        File inputFile = null;
        boolean logData = false;
        boolean fullscreen = false;

        try
        {
            XMLInputFactory inputFactory = XMLInputFactory.newInstance();
            InputStream in = new FileInputStream(jobFile);
            XMLEventReader eventReader = inputFactory.createXMLEventReader(in);

            while (eventReader.hasNext() == true)
            {
                XMLEvent event = eventReader.nextEvent();

                if (event.isStartElement() == true)
                {
                    String tagName = event.asStartElement().getName().getLocalPart();

                    if (tagName.equals("input-file") == true)
                    {
                        if (inputFile != null)
                        {
                            throw constructTermination("messageJobFileElementConfiguredMoreThanOnce", null, null, jobFile.getAbsolutePath(), tagName);
                        }

                        Attribute attributePath = event.asStartElement().getAttributeByName(new QName("path"));

                        if (attributePath == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), tagName, "path");
                        }

                        String inputFilePath = attributePath.getValue();
                        inputFile = new File(inputFilePath);

                        if (inputFile.isAbsolute() != true)
                        {
                            inputFile = new File(jobFile.getAbsoluteFile().getParent() + File.separator + inputFilePath);
                        }

                        try
                        {
                            inputFile = inputFile.getCanonicalFile();
                        }
                        catch (SecurityException ex)
                        {
                            throw constructTermination("messageInputFileCantGetCanonicalPath", ex, null, inputFilePath, jobFile.getAbsolutePath());
                        }
                        catch (IOException ex)
                        {
                            throw constructTermination("messageInputFileCantGetCanonicalPath", ex, null, inputFilePath, jobFile.getAbsolutePath());
                        }

                        if (inputFile.exists() != true)
                        {
                            throw constructTermination("messageJobFileInputFileDoesntExist", null, null, jobFile.getAbsolutePath(), inputFile.getAbsolutePath());
                        }

                        if (inputFile.isFile() != true)
                        {
                            throw constructTermination("messageJobFileInputPathIsntAFile", null, null, jobFile.getAbsolutePath(), inputFile.getAbsolutePath());
                        }

                        if (inputFile.canRead() != true)
                        {
                            throw constructTermination("messageJobFileInputFileIsntReadable", null, null, jobFile.getAbsolutePath(), inputFile.getAbsolutePath());
                        }
                    }
                    else if (tagName.equals("output-file") == true)
                    {
                        if (this.outputFile != null)
                        {
                            throw constructTermination("messageJobFileElementConfiguredMoreThanOnce", null, null, jobFile.getAbsolutePath(), tagName);
                        }

                        Attribute attributePath = event.asStartElement().getAttributeByName(new QName("path"));

                        if (attributePath == null)
                        {
                            throw constructTermination("messageJobFileEntryIsMissingAnAttribute", null, null, jobFile.getAbsolutePath(), tagName, "path");
                        }

                        String outputFilePath = attributePath.getValue();
                        this.outputFile = new File(outputFilePath);

                        if (this.outputFile.isAbsolute() != true)
                        {
                            this.outputFile = new File(jobFile.getAbsoluteFile().getParent() + File.separator + outputFilePath);
                        }

                        try
                        {
                            this.outputFile = this.outputFile.getCanonicalFile();
                        }
                        catch (SecurityException ex)
                        {
                            throw constructTermination("messageOutputFileCantGetCanonicalPath", ex, null, outputFilePath, jobFile.getAbsolutePath());
                        }
                        catch (IOException ex)
                        {
                            throw constructTermination("messageOutputFileCantGetCanonicalPath", ex, null, outputFilePath, jobFile.getAbsolutePath());
                        }

                        if (this.outputFile.exists() == true)
                        {
                            throw constructTermination("messageOutputFileExists", null, null, this.outputFile.getAbsolutePath(), jobFile.getAbsolutePath());
                        }
                    }
                    else if (tagName.equals("options") == true)
                    {
                        Attribute attributeLogData = event.asStartElement().getAttributeByName(new QName("log-data"));

                        if (attributeLogData != null)
                        {
                            logData = attributeLogData.getValue().equals("true");
                        }

                        Attribute attributeFullscreen = event.asStartElement().getAttributeByName(new QName("fullscreen"));

                        if (attributeFullscreen != null)
                        {
                            fullscreen = attributeFullscreen.getValue().equals("true");
                        }

                        Attribute attributeEditIdOnNewNode = event.asStartElement().getAttributeByName(new QName("edit-id-on-new-node"));

                        if (attributeEditIdOnNewNode != null)
                        {
                            this.optionEditIdOnNewNode = attributeEditIdOnNewNode.getValue().equals("true");
                        }
                    }
                }
            }
        }
        catch (XMLStreamException ex)
        {
            throw constructTermination("messageJobFileErrorWhileReading", ex, null, jobFile.getAbsolutePath());
        }
        catch (SecurityException ex)
        {
            throw constructTermination("messageJobFileErrorWhileReading", ex, null, jobFile.getAbsolutePath());
        }
        catch (IOException ex)
        {
            throw constructTermination("messageJobFileErrorWhileReading", ex, null, jobFile.getAbsolutePath());
        }

        if (this.outputFile == null)
        {
            throw constructTermination("messageJobFileOutputFileIsntConfigured", null, null, jobFile.getAbsolutePath());
        }

        if (inputFile != null)
        {
            DataFileLoader loader = new DataFileLoader();

            try
            {
                InputStream in = new FileInputStream(inputFile);
                loader.load(in);
            }
            catch (IOException ex)
            {
                throw constructTermination("messageInputFileErrorWhileReading", ex, null, inputFile.getAbsolutePath());
            }


            /** @todo Check if egde source + target IDs exist as nodes. */
            /** @todo Check if every non-dimension-establishing edge leads to nodes that have dimension-establishing edges. */

            Map<String, Node> nodes = new HashMap<String, Node>();

            for (int i = 0, max = loader.GetNodes().size(); i < max; i++)
            {
                if (nodes.containsKey(loader.GetNodes().get(i).GetId()) == true)
                {
                    throw constructTermination("messageDuplicateNodeId", null, null, loader.GetNodes().get(i).GetId());
                }

                nodes.put(loader.GetNodes().get(i).GetId(), loader.GetNodes().get(i));
            }

            {
                // <Dimension-ID, <Source-ID, Target-ID>>
                Map<String, Map<String, String>> map = new HashMap<String, Map<String, String>>();

                for (int i = 0, max = loader.GetEdges().size(); i < max; i++)
                {
                    if (loader.GetEdges().get(i).GetIsDimensionEstablishing() != true)
                    {
                        continue;
                    }

                    if (this.currentDimensionY == null)
                    {
                        this.currentDimensionY = loader.GetEdges().get(i).GetIdDimension();
                    }

                    if (map.containsKey(loader.GetEdges().get(i).GetIdDimension()) != true)
                    {
                        map.put(loader.GetEdges().get(i).GetIdDimension(), new HashMap<String, String>());
                    }

                    // <Source-ID, Target-ID>
                    Map<String, String> map2 = map.get(loader.GetEdges().get(i).GetIdDimension());

                    if (map2.containsKey(loader.GetEdges().get(i).GetIdSource()) == true)
                    {
                        throw constructTermination("messageDuplicateNodeIdInEstablishingDimension", null, null, loader.GetEdges().get(i).GetIdDimension(), loader.GetEdges().get(i).GetIdSource());
                    }

                    map2.put(loader.GetEdges().get(i).GetIdSource(), loader.GetEdges().get(i).GetIdTarget());
                }

                // Check + build dimension chain.
                for (Map.Entry<String, Map<String, String>> dim : map.entrySet())
                {
                    String current = null;

                    // Find first.
                    for (Map.Entry<String, String> con : dim.getValue().entrySet())
                    {
                        boolean found = false;

                        /** @todo This is expensive! */
                        for (Map.Entry<String, String> entry : dim.getValue().entrySet())
                        {
                            if (con.getKey().equals(entry.getValue()) == true)
                            {
                                found = true;
                                break;
                            }
                        }

                        if (found == false)
                        {
                            current = con.getKey();
                            break;
                        }
                    }

                    if (current == null)
                    {
                        throw constructTermination("messageNoEstablishingDimensionStartNode", null, null, dim.getKey());
                    }

                    Set<String> covered = new LinkedHashSet<String>();
                    String last = null;

                    // From first, fill this.dimensions and find last.
                    while (true)
                    {
                        if (this.dimensions.containsKey(dim.getKey()) != true)
                        {
                            this.dimensions.put(dim.getKey(), new LinkedList<Node>());
                        }

                        this.dimensions.get(dim.getKey()).add(nodes.get(current));
                        covered.add(current);

                        if (dim.getValue().containsKey(current) == true)
                        {
                            // Target of connection is the next current.
                            current = dim.getValue().get(current);
                        }
                        else
                        {
                            last = current;
                            break;
                        }
                    }

                    for (Map.Entry<String, String> con : dim.getValue().entrySet())
                    {
                        if (covered.contains(con.getKey()) != true)
                        {
                            throw constructTermination("messageNodeMissingInEstablishingDimension", null, null, dim.getKey(), con.getKey());
                        }
                    }
                }
            }

            /** @todo Check for duplicate node IDs in the dimensional lists above. */
            /** @todo Check that no nodes are in two establishing-dimensions. */
            /** @todo Check that all nodes are in an establishing-dimension. */

            if (logData == true)
            {
                for (Map.Entry<String, List<Node>> entry : this.dimensions.entrySet())
                {
                    System.out.print(entry.getKey() + "\n");

                    for (int i = 0, max = entry.getValue().size(); i < max; i++)
                    {
                        System.out.print("    ");
                        System.out.print(entry.getValue().get(i).GetId());
                        System.out.print(": ");
                        System.out.print(entry.getValue().get(i).GetValue());
                        System.out.print("\n");
                    }

                    System.out.print("\n");
                }

                System.out.println("------------------------------\n");
            }


            for (int i = 0, max = loader.GetEdges().size(); i < max; i++)
            {
                if (loader.GetEdges().get(i).GetIdSource().equals(loader.GetEdges().get(i).GetIdTarget()) == true)
                {
                    throw constructTermination("messageEdgeIsConnectingANodeToItself", null, null, loader.GetEdges().get(i).GetIdTarget());
                }

                if (nodes.containsKey(loader.GetEdges().get(i).GetIdSource()) != true)
                {
                    throw constructTermination("messageEdgeIsReferencingASourceNodeWhichDoesntExist", null, null, loader.GetEdges().get(i).GetIdSource());
                }

                if (nodes.containsKey(loader.GetEdges().get(i).GetIdTarget()) != true)
                {
                    throw constructTermination("messageEdgeIsReferencingATargetNodeWhichDoesntExist", null, null, loader.GetEdges().get(i).GetIdTarget());
                }

                if (loader.GetEdges().get(i).GetIsDimensionEstablishing() == true)
                {
                    continue;
                }

                if (this.connections.containsKey(loader.GetEdges().get(i).GetIdSource()) != true)
                {
                    this.connections.put(loader.GetEdges().get(i).GetIdSource(), new LinkedList<Map.Entry<String, List<Node>>>());
                }

                List<Map.Entry<String, List<Node>>> dims = this.connections.get(loader.GetEdges().get(i).GetIdSource());
                int pos = -1;

                for (int j = 0; j < dims.size(); j++)
                {
                    if (dims.get(j).getKey().equals(loader.GetEdges().get(i).GetIdDimension()) == true)
                    {
                        pos = j;
                        break;
                    }
                }

                if (pos == -1)
                {
                    dims.add(new AbstractMap.SimpleEntry<String, List<Node>>(loader.GetEdges().get(i).GetIdDimension(), new LinkedList<Node>()));
                    pos = dims.size() - 1;
                }

                dims.get(pos).getValue().add(nodes.get(loader.GetEdges().get(i).GetIdTarget()));
            }

            for (Map.Entry<String, Node> node : nodes.entrySet())
            {
                if (this.connections.containsKey(node.getValue().GetId()) != true)
                {
                    this.connections.put(node.getValue().GetId(), new LinkedList<Map.Entry<String, List<Node>>>());

                    String establishingDimension = null;

                    for (Map.Entry<String, List<Node>> dim : this.dimensions.entrySet())
                    {
                        for (int i = 0, max = dim.getValue().size(); i < max; i++)
                        {
                            if (dim.getValue().get(i).GetId().equals(node.getValue().GetId()) == true)
                            {
                                establishingDimension = dim.getKey();
                                break;
                            }
                        }

                        if (establishingDimension != null)
                        {
                            break;
                        }
                    }

                    this.constructInfoMessage("messageNodeWithoutEdgeOtherThanItsEstablishingDimension", true, null, null, node.getValue().GetId(), node.getValue().GetValue(), establishingDimension);
                }
            }

            if (logData == true)
            {
                for (Map.Entry<String, List<Map.Entry<String, List<Node>>>> entry : this.connections.entrySet())
                {
                    System.out.print(entry.getKey() + "\n");

                    List<Map.Entry<String, List<Node>>> list = entry.getValue();

                    for (int i = 0; i < list.size(); i++)
                    {
                        Map.Entry<String, List<Node>> dim = list.get(i);

                        System.out.print("    " + dim.getKey() + "\n");

                        for (int j = 0; j < dim.getValue().size(); j++)
                        {
                            System.out.print("        ");
                            System.out.print(dim.getValue().get(j).GetId());
                            System.out.print(": ");
                            System.out.print(dim.getValue().get(j).GetValue());
                            System.out.print("\n");
                        }
                    }
                }

                System.out.println("\n------------------------------\n");
            }

            // Copy to free/release the reference to the DataFileLoader object.
            this.notes = new HashMap<String, String>(loader.GetNotes());
        }


        JPanel mainPanel = new JPanel();

        GridBagLayout gridbag = new GridBagLayout();
        mainPanel.setLayout(gridbag);

        JMenuBar menuBarMain = new JMenuBar();

        JMenu menuFile = new JMenu(getI10nString("guiMenuBarFileCaption"));

        JMenuItem itemNew = new JMenuItem(getI10nString("guiMenuNewCaption"));
        itemNew.addActionListener(new ActionListenerMenuItem(this, "new"));
        menuFile.add(itemNew);

        JMenuItem itemSaveAs = new JMenuItem(getI10nString("guiMenuSaveAsCaption"));
        itemSaveAs.addActionListener(new ActionListenerMenuItem(this, "saveAs"));
        menuFile.add(itemSaveAs);

        menuBarMain.add(menuFile);

        JMenu menuEdit = new JMenu(getI10nString("guiMenuBarEditCaption"));

        JMenuItem itemAddDimension = new JMenuItem(getI10nString("guiAddDimensionCaption"));
        itemAddDimension.addActionListener(new ActionListenerMenuItem(this, "addDimension"));
        menuEdit.add(itemAddDimension);

        JMenuItem itemRenameDimension = new JMenuItem(getI10nString("guiRenameDimensionCaption"));
        itemRenameDimension.addActionListener(new ActionListenerMenuItem(this, "renameDimension"));
        menuEdit.add(itemRenameDimension);

        /*
        JMenuItem itemAddEntry = new JMenuItem(getI10nString("guiAddEntryCaption"));
        itemAddEntry.addActionListener(new ActionListenerMenuItem(this, "addEntry"));
        menuEdit.add(itemAddEntry);
        */

        menuBarMain.add(menuEdit);

        JMenu menuMode = new JMenu(getI10nString("guiMenuBarModeCaption"));

        ButtonGroup buttonGroupMode = new ButtonGroup();

        JRadioButtonMenuItem itemView = new JRadioButtonMenuItem(getI10nString("guiModeOptionViewCaption"));
        itemView.addActionListener(new ActionListenerMenuItem(this, "modeView"));
        buttonGroupMode.add(itemView);
        buttonGroupMode.setSelected(itemView.getModel(), true);
        menuMode.add(itemView);

        JRadioButtonMenuItem itemEditNodes = new JRadioButtonMenuItem(getI10nString("guiModeOptionEditNodesCaption"));
        itemEditNodes.addActionListener(new ActionListenerMenuItem(this, "modeEditNodes"));
        buttonGroupMode.add(itemEditNodes);
        menuMode.add(itemEditNodes);

        JRadioButtonMenuItem itemEditEdgeOrder = new JRadioButtonMenuItem(getI10nString("guiModeOptionEditEdgeOrderCaption"));
        itemEditEdgeOrder.addActionListener(new ActionListenerMenuItem(this, "modeEditEdgeOrder"));
        buttonGroupMode.add(itemEditEdgeOrder);
        menuMode.add(itemEditEdgeOrder);

        JRadioButtonMenuItem itemEditNotes = new JRadioButtonMenuItem(getI10nString("guiModeOptionEditNotesCaption"));
        itemEditNotes.addActionListener(new ActionListenerMenuItem(this, "modeEditNotes"));
        buttonGroupMode.add(itemEditNotes);
        menuMode.add(itemEditNotes);

        JRadioButtonMenuItem itemEditIds = new JRadioButtonMenuItem(getI10nString("guiModeOptionEditIdsCaption"));
        itemEditIds.addActionListener(new ActionListenerMenuItem(this, "modeEditIds"));
        buttonGroupMode.add(itemEditIds);
        menuMode.add(itemEditIds);

        menuBarMain.add(menuMode);
        setJMenuBar(menuBarMain);

        JPanel dimensionPanel = new JPanel();
        dimensionPanel.setLayout(new GridLayout(1, 2));

        this.yDimensionSelection.setEditable(false);
        dimensionPanel.add(this.yDimensionSelection);

        this.xDimensionSelection.setEditable(false);
        this.xDimensionSelection.addItemListener(new ItemEventXAxisComboBox(this, 'x'));
        dimensionPanel.add(this.xDimensionSelection);

        this.yDimensionSelection.setText(this.currentDimensionY);

        GridBagConstraints gridbagConstraints = new GridBagConstraints();
        gridbagConstraints.anchor = GridBagConstraints.NORTH;
        gridbagConstraints.gridx = 0;
        gridbagConstraints.gridy = 0;
        gridbagConstraints.weightx = 1.0;
        gridbagConstraints.weighty = 0.0;
        gridbagConstraints.gridwidth = GridBagConstraints.REMAINDER;
        gridbagConstraints.fill = GridBagConstraints.HORIZONTAL;

        mainPanel.add(dimensionPanel, gridbagConstraints);


        JPanel dataPanel = new JPanel();
        dataPanel.setLayout(new GridLayout(1, 2));

        JScrollPane yAxisScrollPane = new JScrollPane(this.yAxisPanel);
        yAxisScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        yAxisScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        dataPanel.add(yAxisScrollPane);

        JScrollPane xAxisScrollPane = new JScrollPane(this.xAxisPanel);
        xAxisScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        xAxisScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        dataPanel.add(xAxisScrollPane);

        if (this.currentDimensionY != null)
        {
            this.changeAxisDimension('y', this.currentDimensionY, null, false);
        }

        gridbagConstraints = new GridBagConstraints();
        gridbagConstraints.anchor = GridBagConstraints.CENTER;
        gridbagConstraints.gridx = 0;
        gridbagConstraints.gridy = 1;
        gridbagConstraints.weightx = 1.0;
        gridbagConstraints.weighty = 1.0;
        gridbagConstraints.gridwidth = GridBagConstraints.REMAINDER;
        gridbagConstraints.fill = GridBagConstraints.BOTH;

        mainPanel.add(dataPanel, gridbagConstraints);


        getContentPane().add(mainPanel);

        if (fullscreen == true)
        {
            setExtendedState(JFrame.MAXIMIZED_BOTH); 
            //setUndecorated(true);
        }
        else
        {
            setLocation(100, 100);
            setSize(600, 400);
        }

        setVisible(true);

        return 0;
    }

    protected int changeAxisDimension(char axis, String newDimension, String nodeTarget, boolean force)
    {
        if (axis == 'x')
        {
            // Here comparing String object identity with null sometimes!
            if (this.currentDimensionX == newDimension)
            {
                if (force != true)
                {
                    return 0;
                }
            }

            if (this.xDimensionSelectionIsInUpdate == false)
            {
                // Needs to avoid triggering update events.
                this.xDimensionSelectionIsInUpdate = true;
                this.currentDimensionX = newDimension;

                this.xAxisPanel.removeAll();

                if (this.currentDimensionX != null &&
                    this.currentNodeY != null)
                {
                    if (this.currentMode.equals("view") == true ||
                        this.currentMode.equals("edit-notes") == true ||
                        this.currentMode.equals("edit-ids") == true)
                    {
                        List<Map.Entry<String, List<Node>>> dims = this.connections.get(this.currentNodeY);

                        for (int i = 0; i < dims.size(); i++)
                        {
                            if (dims.get(i).getKey().equals(this.currentDimensionX) != true)
                            {
                                continue;
                            }

                            List<Node> nodes = dims.get(i).getValue();
                            int count = nodes.size();

                            GridBagLayout gridbag = new GridBagLayout();
                            this.xAxisPanel.setLayout(gridbag);

                            GridBagConstraints gridbagConstraints = new GridBagConstraints();
                            gridbagConstraints.anchor = GridBagConstraints.PAGE_START;
                            gridbagConstraints.weightx = 1.0;
                            gridbagConstraints.weighty = 0.0;
                            gridbagConstraints.gridwidth = GridBagConstraints.REMAINDER;
                            gridbagConstraints.fill = GridBagConstraints.HORIZONTAL;

                            for (int j = 0; j < count; j++)
                            {
                                JButton button = new JButton(nodes.get(j).GetValue());
                                button.setHorizontalAlignment(javax.swing.SwingConstants.LEFT);
                                button.addActionListener(new ActionListenerButtonNode(this, nodes.get(j).GetId(), true));

                                if (this.notes.containsKey(nodes.get(j).GetId()) == true)
                                {
                                    button.setToolTipText(this.notes.get(nodes.get(j).GetId()));
                                }

                                this.xAxisPanel.add(button, gridbagConstraints);
                            }

                            break;
                        }
                    }
                    else if (this.currentMode.equals("edit-edge-order") == true)
                    {
                        List<Node> selectedNodes = new LinkedList<Node>();

                        {
                            List<Map.Entry<String, List<Node>>> dims = this.connections.get(this.currentNodeY);

                            for (int i = 0; i < dims.size(); i++)
                            {
                                if (dims.get(i).getKey().equals(this.currentDimensionX) != true)
                                {
                                    continue;
                                }

                                for (int j = 0, max = dims.get(i).getValue().size(); j < max; j++)
                                {
                                    selectedNodes.add(dims.get(i).getValue().get(j));
                                }

                                break;
                            }
                        }


                        int count = selectedNodes.size();

                        GridBagLayout gridbag = new GridBagLayout();
                        this.xAxisPanel.setLayout(gridbag);

                        GridBagConstraints gridbagConstraints = new GridBagConstraints();
                        gridbagConstraints.anchor = GridBagConstraints.PAGE_START;
                        gridbagConstraints.weightx = 1.0;
                        gridbagConstraints.weighty = 0.0;
                        gridbagConstraints.gridwidth = GridBagConstraints.REMAINDER;
                        gridbagConstraints.fill = GridBagConstraints.HORIZONTAL;

                        GridBagConstraints gridbagConstraints2 = new GridBagConstraints();
                        gridbagConstraints2.weightx = 0.0;
                        gridbagConstraints2.gridx = 0;

                        GridBagConstraints gridbagConstraints3 = new GridBagConstraints();
                        gridbagConstraints3.weightx = 0.0;
                        gridbagConstraints3.gridx = 1;

                        for (int i = 0; i < count; i++)
                        {
                            JPanel group = new JPanel();

                            GridBagLayout gridbagGroup = new GridBagLayout();
                            group.setLayout(gridbagGroup);

                            if (count > 1)
                            {
                                JButton buttonUp = new JButton("↑");
                                buttonUp.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
                                buttonUp.addActionListener(new ActionListenerOperationButton(this, selectedNodes.get(i).GetId(), "edge-up"));

                                JButton buttonDown = new JButton("↓");
                                buttonDown.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
                                buttonDown.addActionListener(new ActionListenerOperationButton(this, selectedNodes.get(i).GetId(), "edge-down"));

                                group.add(buttonUp, gridbagConstraints2);
                                group.add(buttonDown, gridbagConstraints3);
                            }

                            JButton button = new JButton(selectedNodes.get(i).GetValue());
                            button.setHorizontalAlignment(javax.swing.SwingConstants.LEFT);
                            button.addActionListener(new ActionListenerButtonNode(this, selectedNodes.get(i).GetId(), true));

                            if (this.notes.containsKey(selectedNodes.get(i).GetId()) == true)
                            {
                                button.setToolTipText(this.notes.get(selectedNodes.get(i).GetId()));
                            }

                            group.add(button, gridbagConstraints);

                            this.xAxisPanel.add(group, gridbagConstraints);
                        }
                    }
                    else if (this.currentMode.equals("edit-nodes") == true)
                    {
                        Set<String> selectedNodes = new HashSet<String>();

                        {
                            List<Map.Entry<String, List<Node>>> dims = this.connections.get(this.currentNodeY);

                            for (int i = 0; i < dims.size(); i++)
                            {
                                if (dims.get(i).getKey().equals(this.currentDimensionX) != true)
                                {
                                    continue;
                                }

                                for (int j = 0, max = dims.get(i).getValue().size(); j < max; j++)
                                {
                                    selectedNodes.add(dims.get(i).getValue().get(j).GetId());
                                }

                                break;
                            }
                        }


                        List<Node> nodes = this.dimensions.get(this.currentDimensionX);
                        int count = nodes.size();

                        GridBagLayout gridbag = new GridBagLayout();
                        this.xAxisPanel.setLayout(gridbag);

                        GridBagConstraints gridbagConstraints = new GridBagConstraints();
                        gridbagConstraints.anchor = GridBagConstraints.PAGE_START;
                        gridbagConstraints.weightx = 1.0;
                        gridbagConstraints.weighty = 0.0;
                        gridbagConstraints.gridwidth = GridBagConstraints.REMAINDER;
                        gridbagConstraints.fill = GridBagConstraints.HORIZONTAL;

                        GridBagConstraints gridbagConstraints2 = new GridBagConstraints();
                        gridbagConstraints2.weightx = 0.0;
                        gridbagConstraints2.gridx = 1;

                        for (int i = 0; i < count; i++)
                        {
                            JPanel group = new JPanel();

                            GridBagLayout gridbagGroup = new GridBagLayout();
                            group.setLayout(gridbagGroup);

                            JButton buttonToggleConnection = null;

                            if (selectedNodes.contains(nodes.get(i).GetId()) == true)
                            {
                                buttonToggleConnection = new JButton("-");
                                buttonToggleConnection.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
                                buttonToggleConnection.setForeground(Color.RED);
                                buttonToggleConnection.addActionListener(new ActionListenerOperationButton(this, nodes.get(i).GetId(), "disconnect"));
                            }
                            else
                            {
                                buttonToggleConnection = new JButton("+");
                                buttonToggleConnection.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
                                buttonToggleConnection.setForeground(Color.GREEN);
                                buttonToggleConnection.addActionListener(new ActionListenerOperationButton(this, nodes.get(i).GetId(), "connect"));
                            }

                            JButton button = new JButton(nodes.get(i).GetValue());
                            button.setHorizontalAlignment(javax.swing.SwingConstants.LEFT);
                            button.addActionListener(new ActionListenerButtonNode(this, nodes.get(i).GetId(), true));

                            if (this.notes.containsKey(nodes.get(i).GetId()) == true)
                            {
                                button.setToolTipText(this.notes.get(nodes.get(i).GetId()));
                            }

                            group.add(buttonToggleConnection, gridbagConstraints2);
                            group.add(button, gridbagConstraints);

                            this.xAxisPanel.add(group, gridbagConstraints);
                        }
                    }
                    else
                    {
                        throw constructTermination("messageUnknownMode", null, null, this.currentMode);
                    }
                }

                this.xAxisPanel.revalidate();
                this.xAxisPanel.repaint();


                this.xDimensionSelection.removeAllItems();

                if (nodeTarget == null)
                {
                    // This runs sometimes and had some code probably attempting to
                    // (re-)select a certain dimension for the x-axis, maybe based on
                    // nodeTarget? Seems to work without, as other events might select
                    // a node and refresh. Could be that it's still relevant or needs
                    // to be again for a certain case or to improve automatic re-selection.
                }
                else
                {
                    if (this.currentMode.equals("edit-nodes") == true)
                    {
                        Set<String> dims = this.dimensions.keySet();
                        HashSet<String> added = new HashSet<String>();

                        for(String dim : dims)
                        {
                            if (dim.equals(this.currentDimensionY) == true)
                            {
                                continue;
                            }

                            if (added.contains(dim) == true)
                            {
                                continue;
                            }

                            this.xDimensionSelection.addItem(dim);
                            added.add(dim);

                            if (this.currentDimensionX != null)
                            {
                                if (dim.equals(this.currentDimensionX) == true)
                                {
                                    this.xDimensionSelection.setSelectedItem(dim);
                                }
                            }
                        }

                        if (this.currentDimensionX == null)
                        {
                            this.xDimensionSelection.setSelectedItem(null);
                        }
                    }
                    else
                    {
                        List<Map.Entry<String, List<Node>>> dims = this.connections.get(nodeTarget);
                        HashSet<String> added = new HashSet<String>();

                        for (int i = 0, max = dims.size(); i < max; i++)
                        {
                            if (dims.get(i).getKey().equals(this.currentDimensionY) == true)
                            {
                                continue;
                            }

                            if (added.contains(dims.get(i).getKey()) == true)
                            {
                                continue;
                            }

                            this.xDimensionSelection.addItem(dims.get(i).getKey());
                            added.add(dims.get(i).getKey());

                            if (this.currentDimensionX != null)
                            {
                                if (dims.get(i).getKey().equals(this.currentDimensionX) == true)
                                {
                                    this.xDimensionSelection.setSelectedItem(dims.get(i).getKey());
                                }
                            }
                        }

                        if (this.currentDimensionX == null)
                        {
                            this.xDimensionSelection.setSelectedItem(null);
                        }
                    }
                }

                this.xDimensionSelection.revalidate();
                this.xDimensionSelection.repaint();

                this.xDimensionSelectionIsInUpdate = false;
            }
        }
        else if (axis == 'y')
        {
            this.yDimensionSelection.setText("");
            this.currentDimensionY = newDimension;
            this.currentDimensionX = null;

            this.yAxisPanel.removeAll();

            if (this.currentDimensionY != null)
            {
                this.yDimensionSelection.setText(this.currentDimensionY);

                int count = this.dimensions.get(this.currentDimensionY).size();

                GridBagLayout gridbag = new GridBagLayout();
                this.yAxisPanel.setLayout(gridbag);

                GridBagConstraints gridbagConstraints = new GridBagConstraints();
                gridbagConstraints.anchor = GridBagConstraints.PAGE_START;
                gridbagConstraints.weightx = 1.0;
                gridbagConstraints.weighty = 0.0;
                gridbagConstraints.gridwidth = GridBagConstraints.REMAINDER;
                gridbagConstraints.fill = GridBagConstraints.HORIZONTAL;

                GridBagConstraints gridbagConstraints2 = new GridBagConstraints();
                gridbagConstraints2.weightx = 0.0;
                gridbagConstraints2.gridx = 0;

                GridBagConstraints gridbagConstraints3 = new GridBagConstraints();
                gridbagConstraints3.weightx = 0.0;
                gridbagConstraints3.gridx = 1;

                GridBagConstraints gridbagConstraints4 = new GridBagConstraints();
                gridbagConstraints4.weightx = 0.0;
                gridbagConstraints4.gridx = 2;

                GridBagConstraints gridbagConstraints5 = new GridBagConstraints();
                gridbagConstraints5.weightx = 0.0;
                gridbagConstraints5.gridx = 3;

                GridBagConstraints gridbagConstraints6 = new GridBagConstraints();
                gridbagConstraints6.weightx = 0.0;
                gridbagConstraints6.gridx = 4;

                for (int i = 0; i < count; i++)
                {
                    if (this.currentNodeY == null)
                    {
                        this.currentNodeY = this.dimensions.get(this.currentDimensionY).get(i).GetId();
                    }

                    if (this.currentMode.equals("view") == true ||
                        this.currentMode.equals("edit-edge-order") == true)
                    {
                        JButton button = new JButton(this.dimensions.get(this.currentDimensionY).get(i).GetValue());
                        button.setHorizontalAlignment(javax.swing.SwingConstants.LEFT);
                        button.addActionListener(new ActionListenerButtonNode(this, this.dimensions.get(this.currentDimensionY).get(i).GetId(), false));

                        if (this.notes.containsKey(this.dimensions.get(this.currentDimensionY).get(i).GetId()) == true)
                        {
                            button.setToolTipText(this.notes.get(this.dimensions.get(this.currentDimensionY).get(i).GetId()));
                        }

                        if (nodeTarget != null)
                        {
                            if (nodeTarget.equals(this.dimensions.get(this.currentDimensionY).get(i).GetId()) == true)
                            {
                                //button.setBorder(BorderFactory.createLoweredBevelBorder());
                            }
                        }

                        this.yAxisPanel.add(button, gridbagConstraints);
                    }
                    else if (this.currentMode.equals("edit-nodes") == true)
                    {
                        JPanel group = new JPanel();

                        GridBagLayout gridbagGroup = new GridBagLayout();
                        group.setLayout(gridbagGroup);

                        JButton buttonUp = new JButton("↑");
                        buttonUp.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
                        buttonUp.addActionListener(new ActionListenerOperationButton(this, this.dimensions.get(this.currentDimensionY).get(i).GetId(), "node-up"));

                        JButton buttonDown = new JButton("↓");
                        buttonDown.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
                        buttonDown.addActionListener(new ActionListenerOperationButton(this, this.dimensions.get(this.currentDimensionY).get(i).GetId(), "node-down"));

                        JButton buttonNew = new JButton("+");
                        buttonNew.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
                        buttonNew.addActionListener(new ActionListenerOperationButton(this, this.dimensions.get(this.currentDimensionY).get(i).GetId(), "add"));

                        JButton buttonDelete = new JButton("-");
                        buttonDelete.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
                        buttonDelete.addActionListener(new ActionListenerOperationButton(this, this.dimensions.get(this.currentDimensionY).get(i).GetId(), "remove"));

                        JButton buttonEdit = new JButton(getI10nString("guiEntryEditButtonCaption"));
                        buttonEdit.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
                        buttonEdit.addActionListener(new ActionListenerOperationButton(this, this.dimensions.get(this.currentDimensionY).get(i).GetId(), "edit"));

                        JButton button = new JButton(this.dimensions.get(this.currentDimensionY).get(i).GetValue());
                        button.setHorizontalAlignment(javax.swing.SwingConstants.LEFT);
                        button.addActionListener(new ActionListenerButtonNode(this, this.dimensions.get(this.currentDimensionY).get(i).GetId(), false));

                        if (this.notes.containsKey(this.dimensions.get(this.currentDimensionY).get(i).GetId()) == true)
                        {
                            button.setToolTipText(this.notes.get(this.dimensions.get(this.currentDimensionY).get(i).GetId()));
                        }

                        if (nodeTarget != null)
                        {
                            if (nodeTarget.equals(this.dimensions.get(this.currentDimensionY).get(i).GetId()) == true)
                            {
                                //button.setBorder(BorderFactory.createLoweredBevelBorder());
                            }
                        }

                        group.add(buttonUp, gridbagConstraints2);
                        group.add(buttonDown, gridbagConstraints3);
                        group.add(buttonNew, gridbagConstraints4);
                        group.add(buttonDelete, gridbagConstraints5);
                        group.add(buttonEdit, gridbagConstraints6);
                        group.add(button, gridbagConstraints);

                        this.yAxisPanel.add(group, gridbagConstraints);
                    }
                    else if (this.currentMode.equals("edit-notes") == true)
                    {
                        JPanel group = new JPanel();

                        GridBagLayout gridbagGroup = new GridBagLayout();
                        group.setLayout(gridbagGroup);

                        JButton buttonNote = new JButton(getI10nString("guiEntryEditNoteButtonCaption"));
                        buttonNote.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
                        buttonNote.addActionListener(new ActionListenerOperationButton(this, this.dimensions.get(this.currentDimensionY).get(i).GetId(), "node-note"));

                        JButton button = new JButton(this.dimensions.get(this.currentDimensionY).get(i).GetValue());
                        button.setHorizontalAlignment(javax.swing.SwingConstants.LEFT);
                        button.addActionListener(new ActionListenerButtonNode(this, this.dimensions.get(this.currentDimensionY).get(i).GetId(), false));

                        if (this.notes.containsKey(this.dimensions.get(this.currentDimensionY).get(i).GetId()) == true)
                        {
                            button.setToolTipText(this.notes.get(this.dimensions.get(this.currentDimensionY).get(i).GetId()));
                        }

                        if (nodeTarget != null)
                        {
                            if (nodeTarget.equals(this.dimensions.get(this.currentDimensionY).get(i).GetId()) == true)
                            {
                                //button.setBorder(BorderFactory.createLoweredBevelBorder());
                            }
                        }

                        group.add(buttonNote, gridbagConstraints2);
                        group.add(button, gridbagConstraints);

                        this.yAxisPanel.add(group, gridbagConstraints);
                    }
                    else if (this.currentMode.equals("edit-ids") == true)
                    {
                        JPanel group = new JPanel();

                        GridBagLayout gridbagGroup = new GridBagLayout();
                        group.setLayout(gridbagGroup);

                        JButton buttonId = new JButton(getI10nString("guiEntryEditIdButtonCaption"));
                        buttonId.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
                        buttonId.addActionListener(new ActionListenerOperationButton(this, this.dimensions.get(this.currentDimensionY).get(i).GetId(), "node-id"));

                        JButton button = new JButton(this.dimensions.get(this.currentDimensionY).get(i).GetValue());
                        button.setHorizontalAlignment(javax.swing.SwingConstants.LEFT);
                        button.addActionListener(new ActionListenerButtonNode(this, this.dimensions.get(this.currentDimensionY).get(i).GetId(), false));

                        if (this.notes.containsKey(this.dimensions.get(this.currentDimensionY).get(i).GetId()) == true)
                        {
                            button.setToolTipText(this.notes.get(this.dimensions.get(this.currentDimensionY).get(i).GetId()));
                        }

                        if (nodeTarget != null)
                        {
                            if (nodeTarget.equals(this.dimensions.get(this.currentDimensionY).get(i).GetId()) == true)
                            {
                                //button.setBorder(BorderFactory.createLoweredBevelBorder());
                            }
                        }

                        group.add(buttonId, gridbagConstraints2);
                        group.add(button, gridbagConstraints);

                        this.yAxisPanel.add(group, gridbagConstraints);
                    }
                    else
                    {
                        throw constructTermination("messageUnknownMode", null, null, this.currentMode);
                    }
                }

                if (nodeTarget != null)
                {
                    // If commented out, y dimension change requires explicit
                    // selection of x node.
                    navigateNode(nodeTarget, false);
                }
            }

            this.yAxisPanel.revalidate();
            this.yAxisPanel.repaint();


            String newDimensionX = null;

            if (nodeTarget != null)
            {
                List<Map.Entry<String, List<Node>>> dims = this.connections.get(nodeTarget);

                for (int i = 0, max = dims.size(); i < max; i++)
                {
                    if (dims.get(i).getKey().equals(this.currentDimensionY) != true)
                    {
                        newDimensionX = dims.get(i).getKey();
                        break;
                    }
                }
            }

            changeAxisDimension('x', newDimensionX, nodeTarget, false);
        }
        else
        {
            throw constructTermination("messageUnknownAxis", null, null, axis);
        }

        return 0;
    }

    protected int navigateNode(String id, boolean dimensionalPivot)
    {
        if (dimensionalPivot == false)
        {
            this.currentNodeY = id;

            String previousDimension = this.currentDimensionX;

            List<Map.Entry<String, List<Node>>> dims = this.connections.get(id);
            String selectedDimension = null;

            for (int i = 0, max = dims.size(); i < max; i++)
            {
                Map.Entry<String, List<Node>> dim = dims.get(i);

                if (selectedDimension == null)
                {
                    // Assume the first dimension to be the default selection,
                    // until the previously selected dimension happens to be
                    // in the list as well.
                    selectedDimension = dim.getKey();

                    if (previousDimension == null)
                    {
                        break;
                    }
                }

                if (previousDimension.equals(dim.getKey()) == true)
                {
                    selectedDimension = dim.getKey();
                    break;
                }
            }

            this.changeAxisDimension('x', selectedDimension, id, true);
        }
        else
        {
            this.changeAxisDimension('y', this.currentDimensionX, id, false);
        }

        return 0;
    }

    protected int menuEvent(String command)
    {
        if (command.equals("saveAs") == true)
        {
            export();
        }
        else if (command.equals("modeView") == true)
        {
            this.currentMode = "view";

            this.changeAxisDimension('y', this.currentDimensionY, this.currentNodeY, false);
        }
        else if (command.equals("modeEditEdgeOrder") == true)
        {
            this.currentMode = "edit-edge-order";

            this.changeAxisDimension('y', this.currentDimensionY, this.currentNodeY, false);
        }
        else if (command.equals("modeEditNodes") == true)
        {
            this.currentMode = "edit-nodes";

            this.changeAxisDimension('y', this.currentDimensionY, this.currentNodeY, false);
        }
        else if (command.equals("modeEditNotes") == true)
        {
            this.currentMode = "edit-notes";

            this.changeAxisDimension('y', this.currentDimensionY, this.currentNodeY, false);
        }
        else if (command.equals("modeEditIds") == true)
        {
            this.currentMode = "edit-ids";

            this.changeAxisDimension('y', this.currentDimensionY, this.currentNodeY, false);
        }
        else if (command.equals("addDimension") == true)
        {
            NodeDialog dialog = new NodeDialog(this, null);
            dialog.setVisible(true);

            String newDimension = dialog.GetNewValue();

            if (newDimension == null)
            {
                return 1;
            }

            if (newDimension.length() <= 0)
            {
                return 2;
            }

            if (this.dimensions.containsKey(newDimension) == true)
            {
                return 3;
            }


            String newNode1 = null;

            {
                dialog = new NodeDialog(this, null);
                dialog.setVisible(true);

                newNode1 = dialog.GetNewValue();

                if (newNode1 == null)
                {
                    return 4;
                }

                if (newNode1.length() <= 0)
                {
                    return 5;
                }
            }

            String newNode2 = null;

            {
                dialog = new NodeDialog(this, null);
                dialog.setVisible(true);

                newNode2 = dialog.GetNewValue();

                if (newNode2 == null)
                {
                    return 6;
                }

                if (newNode2.length() <= 0)
                {
                    return 7;
                }
            }

            this.dimensions.put(newDimension, new LinkedList<Node>());

            /** @todo if this.optionEditIdOnNewNode == true, also ask for the IDs for both nodes? */

            SecureRandom random = new SecureRandom();
            String newId1 = new BigInteger(130, random).toString(32);

            this.dimensions.get(newDimension).add(new Node(newId1, newNode1));
            this.connections.put(newId1, new LinkedList<Map.Entry<String, List<Node>>>());

            String newId2 = new BigInteger(130, random).toString(32);

            this.dimensions.get(newDimension).add(new Node(newId2, newNode2));
            this.connections.put(newId2, new LinkedList<Map.Entry<String, List<Node>>>());

            this.currentDimensionY = newDimension;
            this.changeAxisDimension('y', this.currentDimensionY, null, true);
        }
        else if (command.equals("renameDimension") == true)
        {
            if (this.currentDimensionY != null)
            {
                NodeDialog dialog = new NodeDialog(this, this.currentDimensionY);
                dialog.setVisible(true);

                String newDimension = dialog.GetNewValue();

                if (newDimension != null)
                {
                    if (newDimension.length() > 0)
                    {
                        if (this.currentDimensionY.equals(newDimension) == true)
                        {
                            newDimension = null;
                        }
                    }
                    else
                    {
                        newDimension = null;
                    }
                }

                if (newDimension != null)
                {
                    if (this.currentDimensionY.equals(newDimension) != true)
                    {
                        if (this.dimensions.containsKey(newDimension) != true)
                        {
                            if (this.dimensions.containsKey(this.currentDimensionY) == true)
                            {
                                List<Node> nodes = this.dimensions.remove(this.currentDimensionY);
                                this.dimensions.put(newDimension, nodes);
                            }

                            for (Map.Entry<String, List<Map.Entry<String, List<Node>>>> con : this.connections.entrySet())
                            {
                                for (int i = 0, max = con.getValue().size(); i < max; i++)
                                {
                                    if (con.getValue().get(i).getKey().equals(this.currentDimensionY) == true)
                                    {
                                        con.getValue().set(i, new AbstractMap.SimpleEntry<String, List<Node>>(newDimension, con.getValue().get(i).getValue()));
                                    }
                                }
                            }

                            this.changeAxisDimension('x', null, null, true);
                            this.changeAxisDimension('y', newDimension, null, true);
                        }
                        else
                        {

                        }
                    }
                }
            }
        }
        else if (command.equals("addEntry") == true)
        {
            NewEntryDialog newEntryDialog = new NewEntryDialog(this, this.dimensions.keySet());
            newEntryDialog.setVisible(true);

            Map<String, String> result = newEntryDialog.GetNewValues();

            if (result != null)
            {
                /** @todo Add new values as nodes if not already existing.
                  * Connect with pre-existing or newly created node. */

                for (Map.Entry<String, String> pair : result.entrySet())
                {
                    //System.out.println(pair.getKey() + ": " + pair.getValue());
                }
            }
        }
        else if (command.equals("new") == true)
        {
            this.dimensions.clear();
            this.connections.clear();

            this.currentMode = "view";
            this.currentDimensionX = null;
            this.currentDimensionY = null;
            this.currentNodeY = null;

            this.yAxisPanel.removeAll();
            this.yDimensionSelection.setText("");
            this.xAxisPanel.removeAll();
            this.xDimensionSelection.removeAllItems();

            this.yAxisPanel.revalidate();
            this.yAxisPanel.repaint();
            this.xAxisPanel.revalidate();
            this.xAxisPanel.repaint();
        }
        else
        {
            throw constructTermination("messageUnknownMenuCommand", null, null, command);
        }

        return 0;
    }

    public int addNode(String precedingSiblingNodeId)
    {
        if (this.currentDimensionY == null)
        {
            return 1;
        }

        String nodeValue = null;

        {
            NodeDialog dialog = new NodeDialog(this, null);
            dialog.setVisible(true);

            nodeValue = dialog.GetNewValue();

            if (nodeValue == null)
            {
                return 3;
            }

            if (nodeValue.length() <= 0)
            {
                return 4;
            }
        }

        List<Node> nodes = this.dimensions.get(this.currentDimensionY);
        int targetPosition = -1;
        boolean duplicateFound = false;

        for (int i = 0, max = nodes.size(); i < max; i++)
        {
            if (nodes.get(i).GetValue().equals(nodeValue) == true)
            {
                duplicateFound = true;
            }

            if (nodes.get(i).GetId().equals(precedingSiblingNodeId) == true)
            {
                targetPosition = i + 1;
            }
        }

        if (duplicateFound == true)
        {
            if (JOptionPane.showConfirmDialog(this,
                                              getI10nString("guiNewNodeDuplicateConfirmationMessage"),
                                              getI10nString("guiNewNodeDuplicateConfirmationDialogTitle"),
                                              JOptionPane.YES_NO_OPTION) != 0)
            {
                return 2;
            }
        }

        String idNew = null;

        if (this.optionEditIdOnNewNode == true)
        {
            do
            {
                NodeDialog dialog = new NodeDialog(this, null);
                dialog.setVisible(true);

                idNew = dialog.GetNewValue();

                if (idNew == null)
                {
                    break;
                }

                if (idNew.length() <= 0)
                {
                    break;
                }

                boolean idExists = false;

                for (Map.Entry<String, List<Node>> dim : this.dimensions.entrySet())
                {
                    for (int i = 0, max = dim.getValue().size(); i < max; i++)
                    {
                        if (dim.getValue().get(i).GetId().equals(idNew) == true)
                        {
                            JOptionPane.showMessageDialog(this,
                                                          getI10nStringFormatted("guiDialogNodeIdExistsAlreadyMessage", idNew),
                                                          getI10nString("guiDialogNodeIdExistsAlreadyTitle"),
                                                          JOptionPane.WARNING_MESSAGE);

                            idExists = true;
                            break;
                        }
                    }

                    if (idExists == true)
                    {
                        break;
                    }
                }

                if (idExists == true)
                {
                    continue;
                }
                else
                {
                    break;
                }
 
            } while (true);
        }

        if (idNew == null)
        {
            SecureRandom random = new SecureRandom();
            idNew = new BigInteger(130, random).toString(32);
        }

        nodes.add(targetPosition, new Node(idNew, nodeValue));

        this.connections.put(idNew, new LinkedList<Map.Entry<String, List<Node>>>());

        this.changeAxisDimension('y', this.currentDimensionY, idNew, false);
        this.changeAxisDimension('x', this.currentDimensionX, null, true);

        return 0;
    }

    public int moveNode(String nodeId, String direction)
    {
        if (this.currentDimensionY == null)
        {
            return 1;
        }

        List<Node> nodes = this.dimensions.get(this.currentDimensionY);
        int count = nodes.size();
        int targetPosition = -1;

        for (int i = 0; i < count; i++)
        {
            if (nodes.get(i).GetId().equals(nodeId) == true)
            {
                targetPosition = i;
                break;
            }
        }

        if (targetPosition == -1)
        {
            return 2;
        }

        if (direction.equals("up") == true)
        {
            if (targetPosition >= 1)
            {
                Node node = nodes.get(targetPosition);
                nodes.remove(targetPosition);
                nodes.add(targetPosition - 1, node);

                this.changeAxisDimension('y', this.currentDimensionY, nodeId, false);
            }
        }
        else if (direction.equals("down") == true)
        {
            if (targetPosition < (count - 1))
            {
                Node node = nodes.get(targetPosition);
                nodes.remove(targetPosition);
                nodes.add(targetPosition + 1, node);

                this.changeAxisDimension('y', this.currentDimensionY, nodeId, false);
            }
        }
        else
        {
            throw constructTermination("messageUnknownNodeMoveDirection", null, null, direction);
        }

        return 0;
    }

    public int moveEdge(String nodeId, String direction)
    {
        if (this.currentNodeY == null)
        {
            return 1;
        }

        if (this.currentDimensionX == null)
        {
            return 2;
        }

        List<Node> nodes = null;

        {
            List<Map.Entry<String, List<Node>>> conn = this.connections.get(this.currentNodeY);

            for (int i = 0, max = conn.size(); i < max; i++)
            {
                if (conn.get(i).getKey().equals(this.currentDimensionX) == true)
                {
                    nodes = conn.get(i).getValue();
                    break;
                }
            }
        }

        if (nodes == null)
        {
            return 3;
        }

        int count = nodes.size();
        int targetPosition = -1;

        for (int i = 0; i < count; i++)
        {
            if (nodes.get(i).GetId().equals(nodeId) == true)
            {
                targetPosition = i;
                break;
            }
        }

        if (targetPosition == -1)
        {
            return 4;
        }

        if (direction.equals("up") == true)
        {
            if (targetPosition >= 1)
            {
                Node node = nodes.get(targetPosition);
                nodes.remove(targetPosition);
                nodes.add(targetPosition - 1, node);

                this.changeAxisDimension('x', this.currentDimensionX, this.currentNodeY, true);
            }
        }
        else if (direction.equals("down") == true)
        {
            if (targetPosition < (count - 1))
            {
                Node node = nodes.get(targetPosition);
                nodes.remove(targetPosition);
                nodes.add(targetPosition + 1, node);

                this.changeAxisDimension('x', this.currentDimensionX, this.currentNodeY, true);
            }
        }
        else
        {
            throw constructTermination("messageUnknownEdgeMoveDirection", null, null, direction);
        }

        return 0;
    }

    public int removeNode(String nodeId)
    {
        List<Node> nodes = this.dimensions.get(this.currentDimensionY);

        if (nodes.size() <= 2)
        {
            /**
             * @todo Currently there can't be a dimension with less than two nodes,
             *     as the extension to GraphML doesn't store dimensions separately
             *     apart from a connection between two nodes, as well as if the last
             *     node is deleted, the user would be stuck on an empty dimension with
             *     no way to navigate away from. With two nodes left, one could rename
             *     them or delete the entire dimension.
             */
            return 1;
        }

        for (int i = 0, count = nodes.size(); i < count; i++)
        {
            if (nodes.get(i).GetId().equals(nodeId) == true)
            {
                nodes.remove(i);
                break;
            }
        }


        this.connections.remove(nodeId);

        for (Map.Entry<String, List<Map.Entry<String, List<Node>>>> conn : this.connections.entrySet())
        {
            List<Map.Entry<String, List<Node>>> dims = conn.getValue();

            for (int i = 0, max = dims.size(); i < max; i++)
            {
                for (int j = 0, max2 = dims.get(i).getValue().size(); j < max2; j++)
                {
                    if (dims.get(i).getValue().get(j).GetId().equals(nodeId) == true)
                    {
                        dims.get(i).getValue().remove(j);
                        break;
                    }
                }
            }
        }

        this.currentNodeY = null;
        this.changeAxisDimension('y', this.currentDimensionY, null, true);
        this.changeAxisDimension('x', this.currentDimensionX, null, true);

        return 0;
    }

    public int updateNode(String nodeId)
    {
        if (this.currentDimensionY == null)
        {
            return 1;
        }

        List<Node> nodes = this.dimensions.get(this.currentDimensionY);

        String valueNew = null;

        {
            Node node = null;

            for (int i = 0, count = nodes.size(); i < count; i++)
            {
                if (nodes.get(i).GetId().equals(nodeId) == true)
                {
                    node = nodes.get(i);
                    break;
                }
            }

            if (node == null)
            {
                return -1;
            }

            String valueOld = node.GetValue();

            NodeDialog dialog = new NodeDialog(this, valueOld);
            dialog.setVisible(true);

            valueNew = dialog.GetNewValue();

            if (valueNew == null)
            {
                return 3;
            }

            if (valueNew.length() <= 0)
            {
                return 4;
            }

            if (valueNew.equals(valueOld) == true)
            {
                return 5;
            }
        }

        int targetPosition = -1;
        boolean duplicateFound = false;

        for (int i = 0, max = nodes.size(); i < max; i++)
        {
            if (nodes.get(i).GetValue().equals(valueNew) == true)
            {
                duplicateFound = true;
                break;
            }

            if (nodes.get(i).GetId().equals(nodeId) == true)
            {
                targetPosition = i;
            }
        }

        if (duplicateFound == true)
        {
            return 2;
        }

        nodes.get(targetPosition).SetValue(valueNew);

        this.changeAxisDimension('y', this.currentDimensionY, nodeId, false);

        return 0;
    }

    public int toggleConnection(String nodeId, boolean connect)
    {
        if (this.currentNodeY == null)
        {
            return 1;
        }

        if (this.currentDimensionX == null)
        {
            return 2;
        }

        if (this.currentDimensionY == null)
        {
            return 3;
        }

        if (this.connections.containsKey(this.currentNodeY) != true)
        {
            throw constructTermination("messageCurrentNodeYMissingInConnections", null, null, this.currentNodeY);
        }

        if (connect == false)
        {
            List<Map.Entry<String, List<Node>>> dims = this.connections.get(this.currentNodeY);
            int pos = -1;

            for (int i = 0, max = dims.size(); i < max; i++)
            {
                if (dims.get(i).getKey().equals(this.currentDimensionX) == true)
                {
                    pos = i;
                    break;
                }
            }

            if (pos == -1)
            {
                throw constructTermination("messageRemoveConnectionDidntFindCurrentDimensionXInConnectionsOfCurrentNodeY", null, null, this.currentDimensionX, this.currentNodeY);
            }

            boolean removed = false;

            for (int i = 0, max = dims.get(pos).getValue().size(); i < max; i++)
            {
                if (dims.get(pos).getValue().get(i).GetId().equals(nodeId) == true)
                {
                    dims.get(pos).getValue().remove(i);
                    removed = true;

                    break;
                }
            }

            if (removed == false)
            {
                throw constructTermination("messageRemoveConnectionDidntFindTargetNode", null, null, nodeId, this.currentDimensionX);
            }


            dims = this.connections.get(nodeId);
            pos = -1;

            for (int i = 0, max = dims.size(); i < max; i++)
            {
                if (dims.get(i).getKey().equals(this.currentDimensionY) == true)
                {
                    pos = i;
                    break;
                }
            }

            if (pos == -1)
            {
                throw constructTermination("messageRemoveConnectionDidntFindCurrentDimensionYInConnectionsOfTargetNode", null, null, this.currentDimensionY, nodeId);
            }

            removed = false;

            for (int i = 0, max = dims.get(pos).getValue().size(); i < max; i++)
            {
                if (dims.get(pos).getValue().get(i).GetId().equals(this.currentNodeY) == true)
                {
                    dims.get(pos).getValue().remove(i);
                    removed = true;

                    break;
                }
            }

            if (removed == false)
            {
                throw constructTermination("messageRemoveConnectionDidntFindCurrentNodeY", null, null, this.currentNodeY, this.currentDimensionY);
            }
        }
        else
        {
            Node node = null;

            for (int i = 0, max = this.dimensions.get(this.currentDimensionX).size(); i < max; i++)
            {
                if (this.dimensions.get(this.currentDimensionX).get(i).GetId().equals(nodeId) == true)
                {
                    node = this.dimensions.get(this.currentDimensionX).get(i);
                    break;
                }
            }

            if (node == null)
            {
                throw constructTermination("messageAddConnectionDidntFindTargetNodeInCurrentDimensionX", null, null, nodeId, this.currentDimensionX);
            }

            List<Map.Entry<String, List<Node>>> dims = this.connections.get(this.currentNodeY);
            int pos = -1;

            for (int i = 0, max = dims.size(); i < max; i++)
            {
                if (dims.get(i).getKey().equals(this.currentDimensionX) == true)
                {
                    pos = i;
                    break;
                }
            }

            if (pos == -1)
            {
                dims.add(new AbstractMap.SimpleEntry<String, List<Node>>(this.currentDimensionX, new LinkedList<Node>()));
                pos = dims.size() - 1;
            }

            dims.get(pos).getValue().add(node);


            node = null;

            for (int i = 0, max = this.dimensions.get(this.currentDimensionY).size(); i < max; i++)
            {
                if (this.dimensions.get(this.currentDimensionY).get(i).GetId().equals(this.currentNodeY) == true)
                {
                    node = this.dimensions.get(this.currentDimensionY).get(i);
                    break;
                }
            }

            if (node == null)
            {
                throw constructTermination("messageAddConnectionDidntFindCurrentNodeYInCurrentDimensionY", null, null, this.currentNodeY, this.currentDimensionY);
            }

            dims = this.connections.get(nodeId);
            pos = -1;

            for (int i = 0, max = dims.size(); i < max; i++)
            {
                if (dims.get(i).getKey().equals(this.currentDimensionY) == true)
                {
                    pos = i;
                    break;
                }
            }

            if (pos == -1)
            {
                dims.add(new AbstractMap.SimpleEntry<String, List<Node>>(this.currentDimensionY, new LinkedList<Node>()));
                pos = dims.size() - 1;
            }

            dims.get(pos).getValue().add(node);
        }


        this.changeAxisDimension('x', this.currentDimensionX, nodeId, true);

        return 0;
    }

    public int nodeEditNote(String nodeId)
    {
        if (this.currentDimensionY == null)
        {
            return 1;
        }

        List<Node> nodes = this.dimensions.get(this.currentDimensionY);
        Node node = null;

        for (int i = 0, max = nodes.size(); i < max; i++)
        {
            if (nodes.get(i).GetId().equals(nodeId) == true)
            {
                node = nodes.get(i);
                break;
            }
        }

        if (node == null)
        {
            return 2;
        }

        this.currentNodeY = node.GetId();
        this.changeAxisDimension('y', this.currentDimensionY, node.GetId(), false);

        String note = "";

        if (this.notes.containsKey(node.GetId()) == true)
        {
            note = this.notes.get(node.GetId());
        }

        NodeDialog dialog = new NodeDialog(this, note);
        dialog.setVisible(true);

        note = dialog.GetNewValue();

        if (note != null)
        {
            if (note.length() > 0)
            {
                this.notes.put(node.GetId(), note);
            }
            else
            {
                this.notes.remove(node.GetId());
            }
        }

        return 0;
    }

    public int nodeEditId(String nodeId)
    {
        if (this.currentDimensionY == null)
        {
            return 1;
        }

        List<Node> nodes = this.dimensions.get(this.currentDimensionY);
        Node node = null;

        for (int i = 0, max = nodes.size(); i < max; i++)
        {
            if (nodes.get(i).GetId().equals(nodeId) == true)
            {
                node = nodes.get(i);
                break;
            }
        }

        if (node == null)
        {
            return 2;
        }

        String idOld = node.GetId();

        this.currentNodeY = idOld;
        this.changeAxisDimension('y', this.currentDimensionY, idOld, false);

        NodeDialog dialog = new NodeDialog(this, idOld);
        dialog.setVisible(true);

        String idNew = dialog.GetNewValue();

        if (idNew != null)
        {
            if (idNew.length() <= 0)
            {
                return 4;
            }

            if (idNew.equals(idOld) == true)
            {
                return 0;
            }

            for (Map.Entry<String, List<Node>> dim : this.dimensions.entrySet())
            {
                for (int i = 0, max = dim.getValue().size(); i < max; i++)
                {
                    if (dim.getValue().get(i).GetId().equals(idNew) == true)
                    {
                        JOptionPane.showMessageDialog(this,
                                                      getI10nStringFormatted("guiDialogNodeIdExistsAlreadyMessage", idNew),
                                                      getI10nString("guiDialogNodeIdExistsAlreadyTitle"),
                                                      JOptionPane.WARNING_MESSAGE);
                        return 3;
                    }
                }
            }

            for (Map.Entry<String, List<Map.Entry<String, List<Node>>>> entry : this.connections.entrySet())
            {
                if (entry.getKey().equals(idOld) == true)
                {
                    List<Map.Entry<String, List<Node>>> value = entry.getValue();
                    this.connections.remove(idOld);
                    this.connections.put(idNew, value);

                    break;
                }
            }

            for (Map.Entry<String, String> entry : this.notes.entrySet())
            {
                if (entry.getKey().equals(idOld) == true)
                {
                    String value = entry.getValue();
                    this.notes.remove(idOld);
                    this.notes.put(idNew, value);

                    break;
                }
            }

            node.SetId(idNew);

            this.currentNodeY = idNew;
            this.changeAxisDimension('y', this.currentDimensionY, idNew, false);
        }

        return 0;
    }

    public int export()
    {
        try
        {
            BufferedWriter writer = new BufferedWriter(
                                    new OutputStreamWriter(
                                    new FileOutputStream(this.outputFile),
                                    "UTF-8"));

            writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
            writer.write("<graphml xsi:schemaLocation=\"http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://graphml.graphdrawing.org/xmlns\">");
            writer.write("<!-- This file was created by hyperdex_1, which is free software licensed under the GNU Affero General Public License 3 or any later version (see https://gitlab.com/publishing-systems/digital_publishing_workflow_tools/ and https://hypertext-systems.org). -->");
            writer.write("<key id=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/node-title.20200601T122500Z\" for=\"node\" attr.name=\"label\" attr.type=\"string\"/>");
            writer.write("<key id=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/node-text.20200601T122500Z\" for=\"node\" attr.name=\"description\" attr.type=\"string\"/>");
            writer.write("<key id=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/node-on-dimension.20200601T122500Z\" for=\"node\" attr.name=\"type\" attr.type=\"string\"/>");
            writer.write("<key id=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/dimension-target.20200601T122500Z\" for=\"edge\" attr.name=\"dimension\" attr.type=\"string\"/>");
            writer.write("<key id=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/dimension-establishing.20200601T122500Z\" for=\"edge\" attr.name=\"is establishing the dimension\" attr.type=\"boolean\"/>");
            writer.write("<graph id=\"\" edgedefault=\"directed\">");

            for (Map.Entry<String, List<Node>> entry : this.dimensions.entrySet())
            {
                List<Node> nodes = entry.getValue();

                for (int i = 0, max = nodes.size(); i < max; i++)
                {
                    writer.write("<node id=\"");

                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    String id = nodes.get(i).GetId().replaceAll("&", "&amp;");
                    id = id.replaceAll("<", "&lt;");
                    id = id.replaceAll(">", "&gt;");
                    id = id.replaceAll("\"", "&quot;");
                    id = id.replaceAll("'", "&apos;");

                    writer.write(id);
                    writer.write("\">");

                    writer.write("<data key=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/node-title.20200601T122500Z\">");

                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    String value = nodes.get(i).GetValue().replaceAll("&", "&amp;");
                    value = value.replaceAll("<", "&lt;");
                    value = value.replaceAll(">", "&gt;");

                    writer.write(value);
                    writer.write("</data>");

                    if (this.notes.containsKey(nodes.get(i).GetId()) == true)
                    {
                        writer.write("<data key=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/node-text.20200601T122500Z\">");

                        // Ampersand needs to be the first, otherwise it would double-encode
                        // other entities.
                        value = this.notes.get(nodes.get(i).GetId()).replaceAll("&", "&amp;");
                        value = value.replaceAll("<", "&lt;");
                        value = value.replaceAll(">", "&gt;");

                        writer.write(value);
                        writer.write("</data>");
                    }

                    writer.write("<data key=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/node-on-dimension.20200601T122500Z\">");

                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    value = entry.getKey().replaceAll("&", "&amp;");
                    value = value.replaceAll("<", "&lt;");
                    value = value.replaceAll(">", "&gt;");

                    /** @todo Add a consistency check on open/import, whether the edges pointing
                      * towards a node indicate the target/node being on the same dimension as the
                      * node itself is on? On load/import, currently, this isn't set per a node's
                      * dimension/"type" as exported here, but exclusively constructed from edges
                      * pointing towards the node, and maybe there's a consistency check in place
                      * already or would lead to two nodes of equal title on two different dimensions. */
                    writer.write(value);
                    writer.write("</data>");

                    writer.write("</node>");
                }
            }

            int edgeCount = 0;

            for (Map.Entry<String, List<Node>> entry : this.dimensions.entrySet())
            {
                List<Node> nodes = entry.getValue();

                // Ampersand needs to be the first, otherwise it would double-encode
                // other entities.
                String idDimension = entry.getKey().replaceAll("&", "&amp;");
                idDimension = idDimension.replaceAll("<", "&lt;");
                idDimension = idDimension.replaceAll(">", "&gt;");

                for (int i = 1, max = nodes.size(); i < max; i++)
                {
                    edgeCount += 1;

                    writer.write("<edge id=\"edge-");
                    writer.write(String.valueOf(edgeCount));
                    writer.write("\" source=\"");

                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    String idSource = nodes.get(i - 1).GetId().replaceAll("&", "&amp;");
                    idSource = idSource.replaceAll("<", "&lt;");
                    idSource = idSource.replaceAll(">", "&gt;");
                    idSource = idSource.replaceAll("\"", "&quot;");
                    idSource = idSource.replaceAll("'", "&apos;");

                    writer.write(idSource);
                    writer.write("\" target=\"");
 
                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    String idTarget = nodes.get(i).GetId().replaceAll("&", "&amp;");
                    idTarget = idTarget.replaceAll("<", "&lt;");
                    idTarget = idTarget.replaceAll(">", "&gt;");
                    idTarget = idTarget.replaceAll("\"", "&quot;");
                    idTarget = idTarget.replaceAll("'", "&apos;");

                    writer.write(idTarget);
                    writer.write("\">");
                    writer.write("<data key=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/dimension-target.20200601T122500Z\">");
                    writer.write(idDimension);
                    writer.write("</data>");
                    writer.write("<data key=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/dimension-establishing.20200601T122500Z\">true</data>");
                    writer.write("</edge>");
                }
            }

            for (Map.Entry<String, List<Map.Entry<String, List<Node>>>> entry : this.connections.entrySet())
            {
                // Ampersand needs to be the first, otherwise it would double-encode
                // other entities.
                String idSource = entry.getKey().replaceAll("&", "&amp;");
                idSource = idSource.replaceAll("<", "&lt;");
                idSource = idSource.replaceAll(">", "&gt;");
                idSource = idSource.replaceAll("\"", "&quot;");
                idSource = idSource.replaceAll("'", "&apos;");

                List<Map.Entry<String, List<Node>>> dim = entry.getValue();

                for (int i = 0, max = dim.size(); i < max; i++)
                {
                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    String idDimension = dim.get(i).getKey().replaceAll("&", "&amp;");
                    idDimension = idDimension.replaceAll("<", "&lt;");
                    idDimension = idDimension.replaceAll(">", "&gt;");

                    List<Node> targets = dim.get(i).getValue();

                    for (int j = 0, max2 = targets.size(); j < max2; j++)
                    {
                        edgeCount += 1;

                        writer.write("<edge id=\"edge-");
                        writer.write(String.valueOf(edgeCount));
                        writer.write("\" source=\"");
                        writer.write(idSource);
                        writer.write("\" target=\"");

                        // Ampersand needs to be the first, otherwise it would double-encode
                        // other entities.
                        String idTarget = targets.get(j).GetId().replaceAll("&", "&amp;");
                        idTarget = idTarget.replaceAll("<", "&lt;");
                        idTarget = idTarget.replaceAll(">", "&gt;");
                        idTarget = idTarget.replaceAll("\"", "&quot;");
                        idTarget = idTarget.replaceAll("'", "&apos;");

                        writer.write(idTarget);
                        writer.write("\">");
                        writer.write("<data key=\"htx-scheme-id://org.hypertext-systems.20180702T071630Z/dimgra.20200601T122500Z/dimgra-1.20200601T122500Z/dimension-target.20200601T122500Z\">");
                        writer.write(idDimension);
                        writer.write("</data>");
                        writer.write("</edge>");
                    }
                }
            }

            writer.write("</graph>");
            writer.write("</graphml>");

            writer.flush();
            writer.close();
        }
        catch (FileNotFoundException ex)
        {
            throw constructTermination("messageOutputFileErrorWhileWriting", ex, null, this.outputFile.getAbsolutePath());
        }
        catch (UnsupportedEncodingException ex)
        {
            throw constructTermination("messageOutputFileErrorWhileWriting", ex, null, this.outputFile.getAbsolutePath());
        }
        catch (IOException ex)
        {
            throw constructTermination("messageOutputFileErrorWhileWriting", ex, null, this.outputFile.getAbsolutePath());
        }

        return 0;
    }

    public String getCurrentNodeY()
    {
        return this.currentNodeY;
    }

    public InfoMessage constructInfoMessage(String id,
                                            boolean outputToConsole,
                                            Exception exception,
                                            String message,
                                            Object ... arguments)
    {
        if (message == null)
        {
            if (arguments == null)
            {
                message = "hyperdex_1: " + getI10nString(id);
            }
            else
            {
                message = "hyperdex_1: " + getI10nStringFormatted(id, arguments);
            }
        }

        if (outputToConsole == true)
        {
            System.out.println(message);

            if (exception != null)
            {
                System.out.println(exception.getMessage());
                exception.printStackTrace();
            }
        }

        return new InfoMessage(id, exception, message, L10N_BUNDLE, arguments);
    }

    public ProgramTerminationException constructTermination(String id, Exception cause, String message, Object ... arguments)
    {
        if (message == null)
        {
            if (arguments == null)
            {
                message = "hyperdex_1: " + getI10nString(id);
            }
            else
            {
                message = "hyperdex_1: " + getI10nStringFormatted(id, arguments);
            }
        }

        return new ProgramTerminationException(id, cause, message, L10N_BUNDLE, arguments);
    }

    public int handleTermination(ProgramTerminationException ex)
    {
        String message = ex.getMessage();
        String id = ex.getId();
        String bundle = ex.getBundle();
        Object[] arguments = ex.getArguments();
        boolean normalTermination = ex.isNormalTermination();

        if (message != null)
        {
            System.err.println(message);
        }

        Throwable innerException = ex.getCause();

        if (innerException != null)
        {
            System.out.println(innerException.getMessage());
            innerException.printStackTrace();
        }

        if (hyperdex_1.resultInfoFile != null)
        {
            try
            {
                BufferedWriter writer = new BufferedWriter(
                                        new OutputStreamWriter(
                                        new FileOutputStream(hyperdex_1.resultInfoFile),
                                        "UTF-8"));

                writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
                writer.write("<!-- This file was created by hyperdex_1, which is free software licensed under the GNU Affero General Public License 3 or any later version (see https://gitlab.com/publishing-systems/digital_publishing_workflow_tools/ and https://hypertext-systems.org). -->\n");
                writer.write("<hyperdex-1-result-information>\n");

                if (normalTermination == false)
                {
                    writer.write("  <failure>\n");
                }
                else
                {
                    writer.write("  <success>\n");
                }

                writer.write("    <timestamp>" + ex.getTimestamp() + "</timestamp>\n");

                if (bundle != null)
                {
                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    bundle = bundle.replaceAll("&", "&amp;");
                    bundle = bundle.replaceAll("<", "&lt;");
                    bundle = bundle.replaceAll(">", "&gt;");

                    writer.write("    <id-bundle>" + bundle + "</id-bundle>\n");
                }

                if (id != null)
                {
                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    id = id.replaceAll("&", "&amp;");
                    id = id.replaceAll("<", "&lt;");
                    id = id.replaceAll(">", "&gt;");

                    writer.write("    <id>" + id + "</id>\n");
                }

                if (message != null)
                {
                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    message = message.replaceAll("&", "&amp;");
                    message = message.replaceAll("<", "&lt;");
                    message = message.replaceAll(">", "&gt;");

                    writer.write("    <message>" + message + "</message>\n");
                }

                if (arguments != null)
                {
                    writer.write("    <arguments>\n");

                    int argumentCount = arguments.length;

                    for (int i = 0; i < argumentCount; i++)
                    {
                        if (arguments[i] == null)
                        {
                            writer.write("      <argument number=\"" + i + "\">\n");
                            writer.write("        <class></class>\n");
                            writer.write("        <value>null</value>\n");
                            writer.write("      </argument>\n");

                            continue;
                        }

                        String className = arguments[i].getClass().getName();

                        // Ampersand needs to be the first, otherwise it would double-encode
                        // other entities.
                        className = className.replaceAll("&", "&amp;");
                        className = className.replaceAll("<", "&lt;");
                        className = className.replaceAll(">", "&gt;");

                        String value = arguments[i].toString();

                        // Ampersand needs to be the first, otherwise it would double-encode
                        // other entities.
                        value = value.replaceAll("&", "&amp;");
                        value = value.replaceAll("<", "&lt;");
                        value = value.replaceAll(">", "&gt;");

                        writer.write("      <argument number=\"" + i + "\">\n");
                        writer.write("        <class>" + className + "</class>\n");
                        writer.write("        <value>" + value + "</value>\n");
                        writer.write("      </argument>\n");
                    }

                    writer.write("    </arguments>\n");
                }

                if (innerException != null)
                {
                    writer.write("    <exception>\n");

                    String className = innerException.getClass().getName();

                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    className = className.replaceAll("&", "&amp;");
                    className = className.replaceAll("<", "&lt;");
                    className = className.replaceAll(">", "&gt;");

                    writer.write("      <class>" + className + "</class>\n");

                    StringWriter stringWriter = new StringWriter();
                    PrintWriter printWriter = new PrintWriter(stringWriter);
                    innerException.printStackTrace(printWriter);
                    String stackTrace = stringWriter.toString();

                    // Ampersand needs to be the first, otherwise it would double-encode
                    // other entities.
                    stackTrace = stackTrace.replaceAll("&", "&amp;");
                    stackTrace = stackTrace.replaceAll("<", "&lt;");
                    stackTrace = stackTrace.replaceAll(">", "&gt;");

                    writer.write("      <stack-trace>" + stackTrace + "</stack-trace>\n");
                    writer.write("    </exception>\n");
                }

                if (this.getInfoMessages().size() > 0)
                {
                    writer.write("    <info-messages>\n");

                    for (int i = 0, max = this.getInfoMessages().size(); i < max; i++)
                    {
                        InfoMessage infoMessage = this.getInfoMessages().get(i);

                        writer.write("      <info-message>\n");
                        writer.write("        <timestamp>" + infoMessage.getTimestamp() + "</timestamp>\n");

                        String infoMessageText = infoMessage.getMessage();
                        String infoMessageId = ex.getId();
                        String infoMessageBundle = ex.getBundle();
                        Object[] infoMessageArguments = ex.getArguments();

                        if (infoMessageBundle != null)
                        {
                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            infoMessageBundle = infoMessageBundle.replaceAll("&", "&amp;");
                            infoMessageBundle = infoMessageBundle.replaceAll("<", "&lt;");
                            infoMessageBundle = infoMessageBundle.replaceAll(">", "&gt;");

                            writer.write("        <id-bundle>" + infoMessageBundle + "</id-bundle>\n");
                        }

                        if (infoMessageId != null)
                        {
                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            infoMessageId = infoMessageId.replaceAll("&", "&amp;");
                            infoMessageId = infoMessageId.replaceAll("<", "&lt;");
                            infoMessageId = infoMessageId.replaceAll(">", "&gt;");

                            writer.write("        <id>" + infoMessageId + "</id>\n");
                        }

                        if (infoMessageText != null)
                        {
                            // Ampersand needs to be the first, otherwise it would double-encode
                            // other entities.
                            infoMessageText = infoMessageText.replaceAll("&", "&amp;");
                            infoMessageText = infoMessageText.replaceAll("<", "&lt;");
                            infoMessageText = infoMessageText.replaceAll(">", "&gt;");

                            writer.write("        <message>" + infoMessageText + "</message>\n");
                        }

                        if (infoMessageArguments != null)
                        {
                            writer.write("        <arguments>\n");

                            int argumentCount = infoMessageArguments.length;

                            for (int j = 0; j < argumentCount; j++)
                            {
                                if (infoMessageArguments[j] == null)
                                {
                                    writer.write("          <argument number=\"" + j + "\">\n");
                                    writer.write("            <class></class>\n");
                                    writer.write("            <value>null</value>\n");
                                    writer.write("          </argument>\n");

                                    continue;
                                }

                                String className = infoMessageArguments[j].getClass().getName();

                                // Ampersand needs to be the first, otherwise it would double-encode
                                // other entities.
                                className = className.replaceAll("&", "&amp;");
                                className = className.replaceAll("<", "&lt;");
                                className = className.replaceAll(">", "&gt;");

                                String value = infoMessageArguments[j].toString();

                                // Ampersand needs to be the first, otherwise it would double-encode
                                // other entities.
                                value = value.replaceAll("&", "&amp;");
                                value = value.replaceAll("<", "&lt;");
                                value = value.replaceAll(">", "&gt;");

                                writer.write("          <argument number=\"" + j + "\">\n");
                                writer.write("            <class>" + className + "</class>\n");
                                writer.write("            <value>" + value + "</value>\n");
                                writer.write("          </argument>\n");
                            }

                            writer.write("        </arguments>\n");
                        }

                        writer.write("      </info-message>\n");
                    }

                    writer.write("    </info-messages>\n");
                }

                if (normalTermination == false)
                {
                    writer.write("  </failure>\n");
                }
                else
                {
                    writer.write("  </success>\n");
                }

                writer.write("</hyperdex-1-result-information>\n");
                writer.flush();
                writer.close();
            }
            catch (FileNotFoundException ex2)
            {
                ex2.printStackTrace();
            }
            catch (UnsupportedEncodingException ex2)
            {
                ex2.printStackTrace();
            }
            catch (IOException ex2)
            {
                ex2.printStackTrace();
            }
        }

        hyperdex_1.resultInfoFile = null;

        System.exit(-1);
        return -1;
    }

    public List<InfoMessage> getInfoMessages()
    {
        return this.infoMessages;
    }

    public Locale getLocale()
    {
        return Locale.getDefault();
    }

    /**
     * @brief This method interprets l10n strings from a .properties file as encoded in UTF-8.
     */
    private String getI10nString(String key)
    {
        if (this.l10n == null)
        {
            this.l10n = ResourceBundle.getBundle(L10N_BUNDLE, this.getLocale());
        }

        try
        {
            return new String(this.l10n.getString(key).getBytes("UTF-8"), "UTF-8");
        }
        catch (UnsupportedEncodingException ex)
        {
            return this.l10n.getString(key);
        }
    }

    private String getI10nStringFormatted(String i10nStringName, Object ... arguments)
    {
        MessageFormat formatter = new MessageFormat("");
        formatter.setLocale(this.getLocale());

        formatter.applyPattern(getI10nString(i10nStringName));
        return formatter.format(arguments);
    }

    protected Map<String, List<Node>> dimensions = new HashMap<String, List<Node>>();
    // <Source-ID, <Dimension-ID, Nodes>>
    protected Map<String, List<Map.Entry<String, List<Node>>>> connections = new HashMap<String, List<Map.Entry<String, List<Node>>>>();
    protected Map<String, String> notes = new HashMap<String, String>();

    protected String currentMode = "view";
    protected String currentDimensionX = null;
    protected String currentDimensionY = null;
    protected String currentNodeY = null;

    protected JPanel yAxisPanel = new JPanel();
    protected JTextField yDimensionSelection = new JTextField("");
    protected JPanel xAxisPanel = new JPanel();
    protected JComboBox xDimensionSelection = new JComboBox();
    protected boolean xDimensionSelectionIsInUpdate = false;

    protected boolean optionEditIdOnNewNode = false;

    protected File outputFile = null;
    public static File resultInfoFile = null;
    protected List<InfoMessage> infoMessages = new ArrayList<InfoMessage>();

    private static final String L10N_BUNDLE = "l10n.l10nHyperdex1";
    private ResourceBundle l10n;
}
